@jctrans/dw-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,587 @@
1
+ # @jctrans/dw-sdk
2
+
3
+ JCTRans 数据仓库前端 TypeScript GraphQL SDK — **Query Builder 风格**,**SSR 安全**,**零重型依赖**。
4
+
5
+ ## 特性
6
+
7
+ - 🔗 **ORM 风格链式调用** — `select().arg().eq().ge().page().query()` 一气呵成
8
+ - 🛡️ **SSR 安全设计** — 工厂函数 + 外部注入 headers,彻底杜绝 Cross-Request State Pollution
9
+ - 📦 **零重型依赖** — 仅依赖原生 `fetch`,不引入 `@apollo/client`、`graphql` 等 AST 库
10
+ - 🔒 **类型安全** — 全链路 TypeScript 泛型推断,字段名 IDE 自动补全 + 编译期校验
11
+ - 🤝 **对齐 Java 后端** — 操作符、端点、query 结构、类型映射与 `data-common` / `data-sdk17` 完全一致
12
+ - 🧩 **可扩展** — 核心与生成层分离,`api-builder` 可增量生成新的主题域 Manager
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ pnpm add @jctrans/dw-sdk
18
+ ```
19
+
20
+ > **前置条件**:项目需已安装 `typescript >= 5.0`。
21
+
22
+ ## 快速开始
23
+
24
+ ```ts
25
+ import { createDwSdk } from '@jctrans/dw-sdk'
26
+
27
+ // 创建 SDK 实例(每次请求上下文调用一次)
28
+ const sdk = createDwSdk({
29
+ baseUrl: 'https://api.jctrans.net.cn',
30
+ headers: { Authorization: 'Bearer xxx' },
31
+ })
32
+
33
+ // 链式查询
34
+ const { records, total } = await sdk.opportunity
35
+ .opportunityEffectiveBoardPage() // 进入商机有效看板
36
+ .select('companyId', 'periodType', 'statYear', 'inquiryCount', 'responseCount')
37
+ .arg('companyId', 12345) // 必填参数
38
+ .eq('statYear', 2026) // 筛选:年份 = 2026
39
+ .ge('inquiryCount', 10) // 筛选:发盘数 >= 10
40
+ .page(1, 20) // 第 1 页,每页 20 条
41
+ .query()
42
+
43
+ // records 类型自动推断为 OpportunityEffectiveBoard[]
44
+ console.log(`共 ${total} 条,当前 ${records.length} 条`)
45
+ ```
46
+
47
+ ### 封装 API(推荐前端使用)
48
+
49
+ 封装层将底层字段(statYear/statMonth/statQuarter/periodType)映射为语义化入参,自动校验并返回统一结果结构:
50
+
51
+ ```ts
52
+ import { createDwSdk } from '@jctrans/dw-sdk'
53
+
54
+ const sdk = createDwSdk({ baseUrl: 'https://api.jctrans.net.cn' })
55
+
56
+ // 季度查询
57
+ const res = await sdk.opportunity.board.query({
58
+ compId: 12345,
59
+ dateType: 'QUARTER',
60
+ dateValue: '2026-Q2',
61
+ })
62
+
63
+ // 年度查询
64
+ const res = await sdk.opportunity.board.query({
65
+ compId: 12345,
66
+ dateType: 'YEAR',
67
+ dateValue: '2026',
68
+ })
69
+
70
+ // 月度查询
71
+ const res = await sdk.opportunity.board.query({
72
+ compId: 12345,
73
+ dateType: 'MONTH',
74
+ dateValue: '2026-06',
75
+ })
76
+
77
+ if (res.success) {
78
+ // res.data.records, res.data.total, res.data.current, res.data.pages
79
+ console.log(`共 ${res.data.total} 条`)
80
+ } else {
81
+ // res.error → 参数校验失败或查询错误
82
+ console.error(res.error)
83
+ }
84
+ ```
85
+
86
+ ## 基础用例
87
+
88
+ ### 1. 全字段分页查询
89
+
90
+ ```ts
91
+ const result = await sdk.opportunity.opportunityEffectiveBoardPage()
92
+ .select(
93
+ 'companyId', 'periodType', 'statYear', 'statMonth', 'statQuarter',
94
+ 'inquiryCount', 'responseCount', 'receivedResponseCount',
95
+ 'potentialCustomerCount', 'oneOnOneMeetCount', 'sessionCount',
96
+ 'viewedResponseCount', 'imCommunicateCount', 'imEffectiveCommunicateCount',
97
+ 'viewRecommendAgentCount', 'findAgentDemandCount', 'findAgentContactCount',
98
+ 'browseStoreCount', 'storeContactCount', 'visitorManageContactCount',
99
+ 'publishFindAgentDemandCount', 'createTime'
100
+ )
101
+ .arg('companyId', 12345)
102
+ .page(1, 50)
103
+ .query()
104
+
105
+ // result: OpportunityEffectiveBoardPageResponse
106
+ ```
107
+
108
+ ### 2. 按公司 + 时间维度查询
109
+
110
+ ```ts
111
+ const result = await sdk.opportunity.opportunityEffectiveBoardPage()
112
+ .select('periodType', 'statYear', 'statMonth', 'inquiryCount', 'responseCount')
113
+ .arg('companyId', 12345)
114
+ .eq('periodType', 'month') // 月维度
115
+ .eq('statYear', 2026)
116
+ .ge('statMonth', 1)
117
+ .le('statMonth', 6) // 上半年
118
+ .page(1, 12)
119
+ .query()
120
+ ```
121
+
122
+ ### 3. 多条件筛选
123
+
124
+ ```ts
125
+ const result = await sdk.opportunity.opportunityEffectiveBoardPage()
126
+ .select('companyId', 'periodType', 'statYear', 'inquiryCount')
127
+ .arg('companyId', 12345)
128
+ .eq('periodType', 'year') // 等于
129
+ .ne('statYear', 2020) // 不等于
130
+ .gt('inquiryCount', 100) // 大于
131
+ .lt('inquiryCount', 10000) // 小于
132
+ .in('statYear', [2024, 2025, 2026]) // IN
133
+ .like('periodType', '%month%') // 模糊匹配(此处仅作示例)
134
+ .page(1, 20)
135
+ .query()
136
+ ```
137
+
138
+ ### 4. 空值筛选
139
+
140
+ ```ts
141
+ // 查询 createTime 不为空的记录
142
+ const result = await sdk.opportunity.opportunityEffectiveBoardPage()
143
+ .select('companyId', 'periodType', 'statYear', 'createTime')
144
+ .arg('companyId', 12345)
145
+ .isNotNull('createTime') // 对齐 Java Conditions.isNotNull
146
+ .page(1, 20)
147
+ .query()
148
+ ```
149
+
150
+ ### 5. 不指定字段(使用默认)
151
+
152
+ ```ts
153
+ // select 可省略,默认返回 companyId 字段
154
+ const result = await sdk.opportunity.opportunityEffectiveBoardPage()
155
+ .arg('companyId', 12345)
156
+ .eq('statYear', 2026)
157
+ .page(1, 20)
158
+ .query()
159
+ ```
160
+
161
+ ### 6. 调试:查看 GraphQL payload
162
+
163
+ ```ts
164
+ const graphQL = sdk.opportunity.opportunityEffectiveBoardPage()
165
+ .select('companyId', 'periodType', 'inquiryCount')
166
+ .arg('companyId', 12345)
167
+ .eq('statYear', 2026)
168
+ .page(1, 20)
169
+ .toGraphQL()
170
+
171
+ // 输出:
172
+ // query { opportunityEffectiveBoardPage(
173
+ // dto: { current: 1, size: 20, companyId: 12345, statYear: 2026 }
174
+ // ) { total current size pages records { companyId periodType inquiryCount } } }
175
+ ```
176
+
177
+ ## 封装 API 用例(sdk.opportunity.board)
178
+
179
+ 封装 API 提供 `{ success, data, error }` 统一返回结构,参数校验失败不抛异常,前端可直接展示 `error` 字段。
180
+
181
+ ### 1. 季度查询
182
+
183
+ ```ts
184
+ const res = await sdk.opportunity.board.query({
185
+ compId: 12345,
186
+ dateType: 'QUARTER',
187
+ dateValue: '2026-Q2', // Q 取值 1-4
188
+ })
189
+ ```
190
+
191
+ ### 2. 年度查询
192
+
193
+ ```ts
194
+ const res = await sdk.opportunity.board.query({
195
+ compId: 12345,
196
+ dateType: 'YEAR',
197
+ dateValue: '2026',
198
+ })
199
+ ```
200
+
201
+ ### 3. 月度查询
202
+
203
+ ```ts
204
+ const res = await sdk.opportunity.board.query({
205
+ compId: 12345,
206
+ dateType: 'MONTH',
207
+ dateValue: '2026-06', // MM 取值 01-12
208
+ })
209
+ ```
210
+
211
+ ### 4. 参数校验错误处理
212
+
213
+ ```ts
214
+ const res = await sdk.opportunity.board.query({
215
+ compId: 0, // ❌ 必须 > 0
216
+ dateType: 'QUARTER',
217
+ dateValue: '2026-Q5', // ❌ Q 取值 1-4
218
+ })
219
+
220
+ // res.success → false
221
+ // res.error → "参数错误:compId 必须为大于 0 的数字"
222
+ ```
223
+
224
+ ### 5. 入参校验规则
225
+
226
+ | 字段 | 必填 | 规则 |
227
+ |------|------|------|
228
+ | `compId` | ✅ | `number`,> 0 |
229
+ | `dateType` | ✅ | `'QUARTER' \| 'YEAR' \| 'MONTH'` |
230
+ | `dateValue` | ✅ | 格式依 dateType 而定 |
231
+
232
+ **dateValue 格式:**
233
+
234
+ | dateType | 格式 | 示例 |
235
+ |----------|------|------|
236
+ | `QUARTER` | `YYYY-QN`(Q=1-4) | `2026-Q2` |
237
+ | `YEAR` | `YYYY` | `2026` |
238
+ | `MONTH` | `YYYY-MM`(MM=01-12) | `2026-06` |
239
+
240
+ ### 6. 封装层返回值类型
241
+
242
+ ```ts
243
+ interface BoardQueryResult {
244
+ success: boolean
245
+ data: BoardPageData | null // 成功时包含 records, total, current, size, pages
246
+ error: string | null // 失败时为错误描述
247
+ }
248
+ ```
249
+
250
+ ## API 参考
251
+
252
+ ### createDwSdk(config)
253
+
254
+ 创建 SDK 实例的工厂函数。**每次请求上下文调用一次**,绝不在模块顶层或全局作用域调用。
255
+
256
+ | 参数 | 类型 | 必填 | 说明 |
257
+ |------|------|------|------|
258
+ | `config.baseUrl` | `string` | ✅ | GraphQL 端点基础路径(如 `https://api.jctrans.net.cn`) |
259
+ | `config.headers` | `Record<string, string>` | ❌ | 外部注入的请求头(SSR 下从 H3Event 提取 Cookie) |
260
+ | `config.timeout` | `number` | ❌ | 超时毫秒,默认 `15000` |
261
+ | `config.fetch` | `typeof fetch` | ❌ | 自定义 fetch 实现,默认 `globalThis.fetch` |
262
+
263
+ ### Query Builder 方法
264
+
265
+ 所有方法均返回 `this`(多态类型),支持无限链式调用。
266
+
267
+ | 方法 | 参数 | 说明 |
268
+ |------|------|------|
269
+ | `select(...fields)` | `...string[]` | 指定查询字段,缺省返回 `companyId` |
270
+ | `arg(name, value)` | `string, unknown` | 设置额外参数(对齐 Java QueryBuilder.arg()) |
271
+ | `eq(field, value)` | `string, unknown` | 等于条件 |
272
+ | `ne(field, value)` | `string, unknown` | 不等于条件 |
273
+ | `gt(field, value)` | `string, unknown` | 大于条件 |
274
+ | `ge(field, value)` | `string, unknown` | 大于等于条件 |
275
+ | `lt(field, value)` | `string, unknown` | 小于条件 |
276
+ | `le(field, value)` | `string, unknown` | 小于等于条件 |
277
+ | `like(field, value)` | `string, string` | 模糊匹配(后端 SQL LIKE) |
278
+ | `in(field, values)` | `string, unknown[]` | IN 条件 |
279
+ | `isNull(field)` | `string` | 判断为空(对齐 Java Conditions.isNull) |
280
+ | `isNotNull(field)` | `string` | 判断不为空(对齐 Java Conditions.isNotNull) |
281
+ | `page(current, size)` | `number, number` | 分页,默认第 1 页 20 条(current: 当前页码, size: 每页条数) |
282
+ | `query()` | — | **终止方法**,执行分页查询 |
283
+ | `toGraphQL()` | — | 调试用,返回当前构造的 GraphQL payload |
284
+
285
+ ### GraphQLClientError
286
+
287
+ ```ts
288
+ import { GraphQLClientError } from '@jctrans/dw-sdk'
289
+
290
+ try {
291
+ await sdk.opportunity.opportunityEffectiveBoardPage()
292
+ .arg('companyId', 12345)
293
+ .query()
294
+ } catch (err) {
295
+ if (err instanceof GraphQLClientError) {
296
+ console.error(`[${err.status}] ${err.message}`)
297
+ // err.body — 服务端返回的原始响应体
298
+ }
299
+ }
300
+ ```
301
+
302
+ | 属性 | 类型 | 说明 |
303
+ |------|------|------|
304
+ | `message` | `string` | 错误描述 |
305
+ | `status` | `number` | HTTP 状态码(0 = 网络错误,200 = GraphQL 业务错误,408 = 超时) |
306
+ | `body` | `string` | 原始响应体 |
307
+ | `url` | `string` | 请求的完整 URL(便于 Network 面板定位) |
308
+ | `name` | `string` | 固定为 `'GraphQLClientError'` |
309
+
310
+ ### BoardService(封装层)
311
+
312
+ 通过 `sdk.opportunity.board` 访问。
313
+
314
+ #### board.query(params)
315
+
316
+ | 参数 | 类型 | 必填 | 说明 |
317
+ |------|------|------|------|
318
+ | `params.compId` | `number` | ✅ | 公司 ID,必须 > 0 |
319
+ | `params.dateType` | `'QUARTER' \| 'YEAR' \| 'MONTH'` | ✅ | 日期聚合类型 |
320
+ | `params.dateValue` | `string` | ✅ | 日期值,格式依 dateType 而定 |
321
+
322
+ **返回值: `Promise<BoardQueryResult>`**
323
+
324
+ ```ts
325
+ interface BoardQueryResult {
326
+ success: boolean
327
+ data: {
328
+ total: number
329
+ current: number
330
+ size: number
331
+ pages: number
332
+ records: OpportunityEffectiveBoard[]
333
+ } | null
334
+ error: string | null
335
+ }
336
+ ```
337
+
338
+ **dateType → 底层字段映射:**
339
+
340
+ | dateType | 映射到底层调用 |
341
+ |----------|----------------|
342
+ | `QUARTER` | `.eq('periodType', 'quarter')` + `.eq('statQuarter', N)` + `.arg('statYear', YYYY)` |
343
+ | `YEAR` | `.eq('periodType', 'year')` + `.arg('statYear', YYYY)` |
344
+ | `MONTH` | `.eq('periodType', 'month')` + `.eq('statMonth', N)` + `.arg('statYear', YYYY)` |
345
+
346
+ ## 与 Java 后端对齐
347
+
348
+ SDK 与 `jctrans-data-service` 的 Java 代码完全对齐。
349
+
350
+ ### 端点
351
+
352
+ ```
353
+ POST {baseUrl}/api/{domain}/{topic}/graphql
354
+ ```
355
+
356
+ 每个查询使用独立的端点 URL,domain 和 topic 在路径中可见:
357
+
358
+ | domain | topic | 完整 URL |
359
+ |--------|-------|---------|
360
+ | `opportunity` | `opportunityEffectiveBoardPage` | `/api/opportunity/opportunityEffectiveBoardPage/graphql` |
361
+
362
+ 这种设计使得浏览器 DevTools → Network 面板可按路径分类筛选,便于调试。
363
+
364
+ 发送的请求 body 为 `GraphQLRequest`,包含 `query` 字段(及可选的 `operationName`、`variables`)。
365
+
366
+ ### Network 面板可见性
367
+
368
+ 每个查询在 Network 面板中展示为独立的 POST 请求,URL 中包含 domain/topic 路径:
369
+
370
+ ```
371
+ POST /api/opportunity/opportunityEffectiveBoardPage/graphql
372
+ Body: {"query": "query { opportunityEffectiveBoardPage(dto: ...) { ... } }"}
373
+ Response: {"data": {"opportunityEffectiveBoardPage": {"total": ..., "records": [...]}}}
374
+ ```
375
+
376
+ ### 操作符
377
+
378
+ | TS SDK | Java Conditions.java |
379
+ |--------|---------------------|
380
+ | `eq` | `eq` |
381
+ | `ne` | `ne` |
382
+ | `gt` | `gt` |
383
+ | `ge` | `ge` |
384
+ | `lt` | `lt` |
385
+ | `le` | `le` |
386
+ | `like` | `like` |
387
+ | `in` | `in` |
388
+ | `isNull` | `isNull` |
389
+ | `isNotNull` | `isNotNull` |
390
+
391
+ ### query 结构
392
+
393
+ 产出的 GraphQL query 与后端 `opportunity.graphqls` schema 对齐:
394
+
395
+ ```
396
+ query { topic(dto: { current: N, size: N, companyId: N, statYear: N, ... })
397
+ { total current size pages records { fields } } }
398
+ ```
399
+
400
+ > **注意**:`eq` 条件中匹配 DTO 字段(`companyId`/`statYear`/`statMonth`/`statQuarter`)的会提取到 `dto` 入参中,
401
+ > 非 DTO 字段或非 `eq` 操作符的条件仍以 `conditions` 数组方式传递。
402
+
403
+ ## SSR 使用指南
404
+
405
+ ### Nuxt 3 Server API
406
+
407
+ ```ts
408
+ // server/api/opportunity/board.ts(底层 API)
409
+ import { createDwSdk } from '@jctrans/dw-sdk'
410
+
411
+ export default defineEventHandler(async (event) => {
412
+ const sdk = createDwSdk({
413
+ baseUrl: useRuntimeConfig().apiBaseUrl,
414
+ // ✅ 从 H3Event 提取 Cookie 透传给后端
415
+ headers: {
416
+ Cookie: getHeader(event, 'cookie') ?? '',
417
+ Authorization: getHeader(event, 'authorization') ?? '',
418
+ },
419
+ timeout: 10_000, // SSR 下使用较短超时
420
+ })
421
+
422
+ const query = getQuery(event)
423
+ return sdk.opportunity.opportunityEffectiveBoardPage()
424
+ .select('companyId', 'periodType', 'statYear', 'inquiryCount', 'responseCount')
425
+ .arg('companyId', Number(query.companyId))
426
+ .eq('statYear', Number(query.year) || 2026)
427
+ .page(Number(query.page) || 1, Number(query.size) || 20)
428
+ .query()
429
+ })
430
+ ```
431
+
432
+ ```ts
433
+ // server/api/opportunity/board-wrapped.ts(封装 API)
434
+ import { createDwSdk } from '@jctrans/dw-sdk'
435
+
436
+ export default defineEventHandler(async (event) => {
437
+ const sdk = createDwSdk({
438
+ baseUrl: useRuntimeConfig().apiBaseUrl,
439
+ headers: {
440
+ Cookie: getHeader(event, 'cookie') ?? '',
441
+ Authorization: getHeader(event, 'authorization') ?? '',
442
+ },
443
+ })
444
+
445
+ const body = await readBody(event)
446
+ // body: { compId: 12345, dateType: 'QUARTER', dateValue: '2026-Q2' }
447
+ return sdk.opportunity.board.query(body)
448
+ })
449
+ ```
450
+
451
+ ### Nuxt 3 Composables(客户端)
452
+
453
+ ```ts
454
+ // composables/useOpportunity.ts
455
+ import { createDwSdk, type BoardQueryResult } from '@jctrans/dw-sdk'
456
+
457
+ export function useOpportunity() {
458
+ const sdk = createDwSdk({ baseUrl: '/api' })
459
+
460
+ /** 底层 API(Query Builder) */
461
+ async function fetchBoard(
462
+ companyId: number,
463
+ page = 1,
464
+ size = 20
465
+ ): Promise<OpportunityEffectiveBoardPageResponse> {
466
+ return sdk.opportunity.opportunityEffectiveBoardPage()
467
+ .select('companyId', 'periodType', 'statYear', 'statMonth',
468
+ 'inquiryCount', 'responseCount', 'receivedResponseCount')
469
+ .arg('companyId', companyId)
470
+ .page(page, size)
471
+ .query()
472
+ }
473
+
474
+ /** 封装 API(推荐—语义化入参,自动校验) */
475
+ async function queryBoard(
476
+ compId: number,
477
+ dateType: 'QUARTER' | 'YEAR' | 'MONTH',
478
+ dateValue: string
479
+ ): Promise<BoardQueryResult> {
480
+ return sdk.opportunity.board.query({ compId, dateType, dateValue })
481
+ }
482
+
483
+ return { fetchBoard, queryBoard }
484
+ }
485
+ ```
486
+
487
+ ### SSR 避坑清单
488
+
489
+ | ❌ 错误做法 | ✅ 正确做法 |
490
+ |------------|------------|
491
+ | 模块顶层 `export const sdk = createDwSdk(...)` | 每次请求/组件内调用 `createDwSdk()` |
492
+ | 在 SDK 内部读取 `document.cookie` | headers 由外部注入 |
493
+ | 跨请求复用同一个 Query 实例 | 每次 `opportunityEffectiveBoardPage()` 返回全新实例 |
494
+ | 在实例上缓存上次请求的 Promise | 每次 `execute()` 返回独立 Promise |
495
+ | 全局单例 Client | 工厂函数创建,用完即 GC |
496
+
497
+ ## 架构
498
+
499
+ ```
500
+ ┌──────────────────────────────────────────────────────────────────┐
501
+ │ createDwSdk(config) │
502
+ │ ├── GraphQLClient (fetch 封装) │
503
+ │ │ POST {baseUrl}/api/{domain}/{topic}/graphql │
504
+ │ │ ├── /api/opportunity/opportunityEffectiveBoardPage/graphql │
505
+ │ │ ├── /api/order/adsOrderPage/graphql (待生成) │
506
+ │ │ └── /api/logistics/... (待生成) │
507
+ │ ├── OpportunityManager (商机域) │
508
+ │ │ ├── opportunityEffectiveBoardPage() → Query Builder │
509
+ │ │ └── board (getter) → BoardService (封装层) │
510
+ │ ├── OrderManager (订单域, 待生成) │
511
+ │ └── LogisticsManager (物流域, 待生成) │
512
+ └──────────────────────────────────────────────────────────────────┘
513
+ ```
514
+
515
+ | 层 | 目录 | 说明 |
516
+ |----|------|------|
517
+ | **基建层** | `src/core/` | `GraphQLClient` + `BaseQuery`,手写维护 |
518
+ | **生成层** | `src/generated/` | 各主题域 Manager + Query + types,由 `api-builder` 自动生成 |
519
+ | **封装层** | `src/services/` | 语义化入参封装,参数校验,统一返回结构 |
520
+ | **入口** | `src/index.ts` | `createDwSdk()` 工厂函数 + 类型导出 |
521
+
522
+ ## 类型系统
523
+
524
+ SDK 提供全链路类型推断,调用方无需手动标注泛型:
525
+
526
+ ```
527
+ createDwSdk()
528
+ → DwSdk
529
+ → sdk.opportunity // OpportunityManager
530
+ → sdk.opportunity.opportunityEffectiveBoardPage() // OpportunityEffectiveBoardQuery
531
+ → .query() // Promise<OpportunityEffectiveBoardPageResponse>
532
+ → result.records // OpportunityEffectiveBoard[]
533
+ ```
534
+
535
+ 字段名通过 `keyof` 联合类型约束,IDE 自动补全:
536
+
537
+ ```ts
538
+ .('')
539
+ // IDE 弹出: 'companyId' | 'periodType' | 'statYear' | 'statMonth' | 'statQuarter'
540
+ // | 'inquiryCount' | 'responseCount' | ...
541
+ ```
542
+
543
+ ### 当前前端可补全能力
544
+
545
+ ```ts
546
+ . // 可补全: opportunity
547
+ sdk.opportunity
548
+ .opportunityEffectiveBoardPage()
549
+ // 可链式补全: select / arg / eq / ne / gt / ge / lt / le
550
+ // / like / in / isNull / isNotNull / page / query / toGraphQL
551
+ .board
552
+ // BoardService: .query({ compId, dateType, dateValue })
553
+ // 返回 BoardQueryResult: { success, data, error }
554
+ ```
555
+
556
+ ### 封装层导出类型
557
+
558
+ ```ts
559
+ import type {
560
+ DateType, // 'QUARTER' | 'YEAR' | 'MONTH'
561
+ OpportunityBoardParams,// { compId: number; dateType: DateType; dateValue: string }
562
+ BoardQueryResult, // { success: boolean; data: BoardPageData | null; error: string | null }
563
+ BoardPageData, // { total; current; size; pages; records }
564
+ } from '@jctrans/dw-sdk'
565
+ ```
566
+
567
+ ### 如何让前端都"点得出来"新能力
568
+
569
+ 1. 先由 `api-builder` 生成 `src/generated/{domain}/` 下的 Manager + Query + types。
570
+ 2. 在 `src/index.ts` 的 `DwSdk` 接口增加新字段。
571
+ 3. 在 `createDwSdk()` 返回对象中实例化该 Manager。
572
+ 4. 发布新版本后,前端更新依赖并重启 TS Server(或重启 IDE)。
573
+
574
+ ## 扩展新主题域
575
+
576
+ `api-builder` 增量生成新主题域后,按以下步骤接入:
577
+
578
+ 1. 将生成的 `src/generated/{domain}/` 目录放入项目中
579
+ 2. 在 `src/index.ts` 中 import 新 Manager
580
+ 3. 在 `DwSdk` 接口中添加字段
581
+ 4. 在 `createDwSdk()` 函数体中实例化
582
+
583
+ 调用方无需任何改动,TypeScript 自动推断新字段类型。
584
+
585
+ ## 许可
586
+
587
+ UNLICENSED — JCTRans 内部使用