@youzan-cloud/zan-saas-cli 0.1.0-beta.4 → 0.1.0-beta.8

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.
Files changed (66) hide show
  1. package/bin/zan-saas-cli.js +8 -11
  2. package/internal/registry/manifest.json +25 -20
  3. package/npm-shrinkwrap.json +2 -2
  4. package/package.json +1 -1
  5. package/skills/zan-saas/SKILL.md +3 -3
  6. package/skills/zan-saas/references/host-entry.md +8 -0
  7. package/skills/zan-saas-order/SKILL.md +12 -1
  8. package/skills/zan-saas-order/references/branch-counts.md +45 -0
  9. package/skills/zan-saas-order/references/order-get.md +2 -0
  10. package/skills/zan-saas-order/references/order-list.md +2 -0
  11. package/skills/zan-saas-order/references/order-scan.md +2 -0
  12. package/skills/zan-saas-org/SKILL.md +6 -0
  13. package/skills/zan-saas-org/references/unit-list.md +2 -0
  14. package/skills/zan-saas-shared/SKILL.md +20 -5
  15. package/skills/zan-saas-shared/references/authentication.md +9 -4
  16. package/skills/zan-saas-shared/references/gateway-errors.md +29 -0
  17. package/skills/zan-saas-shared/references/shop-context.md +39 -0
  18. package/skills/zan-saas-valuecard/SKILL.md +8 -2
  19. package/skills/zan-saas-valuecard/references/transaction-list.md +25 -0
  20. package/skills/zan-saas-valuecard/references/transaction-scan.md +23 -0
  21. package/src/auth/credential-source.js +26 -0
  22. package/src/auth/credential-store.js +61 -5
  23. package/src/auth/session.js +80 -27
  24. package/src/cli/command-paths.js +3 -2
  25. package/src/cli/context-help.js +3 -1
  26. package/src/cli/program.js +20 -4
  27. package/src/commands/native/access/commands.index.json +1 -1
  28. package/src/commands/native/access/role/list/command.js +4 -32
  29. package/src/commands/native/customer/commands.index.json +1 -1
  30. package/src/commands/native/customer/customer-tag/list/command.js +2 -2
  31. package/src/commands/native/member/commands.index.json +1 -1
  32. package/src/commands/native/order/commands.index.json +3 -3
  33. package/src/commands/native/order/order/get/command.js +14 -9
  34. package/src/commands/native/order/order/get/schema.json +197 -41
  35. package/src/commands/native/order/order/list/command.js +9 -6
  36. package/src/commands/native/order/order/list/schema.json +140 -10
  37. package/src/commands/native/order/order/scan/command.js +8 -5
  38. package/src/commands/native/order/order/scan/scan.js +1 -1
  39. package/src/commands/native/order/order/scan/schema.json +135 -8
  40. package/src/commands/native/order/shared/query.js +57 -9
  41. package/src/commands/native/order/shared/search.js +1 -1
  42. package/src/commands/native/org/commands.index.json +4 -4
  43. package/src/commands/native/valuecard/commands.index.json +20 -2
  44. package/src/commands/native/valuecard/domain.json +1 -1
  45. package/src/commands/native/valuecard/transaction/list/command.js +110 -0
  46. package/src/commands/native/valuecard/transaction/list/projection.js +71 -0
  47. package/src/commands/native/valuecard/transaction/list/schema.json +888 -0
  48. package/src/commands/native/valuecard/transaction/scan/command.js +130 -0
  49. package/src/commands/native/valuecard/transaction/scan/projection.js +58 -0
  50. package/src/commands/native/valuecard/transaction/scan/scan.js +70 -0
  51. package/src/commands/native/valuecard/transaction/scan/schema.json +954 -0
  52. package/src/commands/system/api.js +1 -1
  53. package/src/commands/system/auth.js +13 -2
  54. package/src/commands/system/contracts.js +27 -19
  55. package/src/commands/system/shop-contracts.js +97 -0
  56. package/src/commands/system/shop.js +128 -27
  57. package/src/execution/error-details.js +13 -18
  58. package/src/execution/gateway-guidance.js +43 -0
  59. package/src/execution/shop-query.js +6 -3
  60. package/src/identity/context-store.js +14 -10
  61. package/src/identity/file-store.js +147 -0
  62. package/src/identity/session-store.js +261 -0
  63. package/src/identity/shop-location.js +158 -0
  64. package/src/identity/unit-membership.js +42 -0
  65. package/src/transport/api.js +78 -11
  66. package/src/transport/shop-app.js +8 -1
@@ -231,12 +231,34 @@
231
231
  "additionalProperties": false,
232
232
  "properties": {
233
233
  "requests": {
234
- "type": "array",
235
- "minItems": 1,
236
- "maxItems": 1,
237
- "items": {
238
- "$ref": "#/definitions/bindingRequest"
239
- }
234
+ "description": "同店保留原计划;总部显式分店先核实总部、在其目录中有界核实成员、绑定分店,再读订单。目录项展示第一页模板,最多20页,不表示已核实。",
235
+ "anyOf": [
236
+ {
237
+ "type": "array",
238
+ "minItems": 1,
239
+ "maxItems": 1,
240
+ "items": {
241
+ "$ref": "#/definitions/bindingRequest"
242
+ }
243
+ },
244
+ {
245
+ "type": "array",
246
+ "minItems": 3,
247
+ "maxItems": 3,
248
+ "additionalItems": false,
249
+ "items": [
250
+ {
251
+ "$ref": "#/definitions/headquartersBindingRequest"
252
+ },
253
+ {
254
+ "$ref": "#/definitions/membershipRequest"
255
+ },
256
+ {
257
+ "$ref": "#/definitions/targetBindingRequest"
258
+ }
259
+ ]
260
+ }
261
+ ]
240
262
  },
241
263
  "initialRanges": {
242
264
  "type": "array",
@@ -347,7 +369,9 @@
347
369
  "order",
348
370
  "orders",
349
371
  "scan",
350
- "output-model"
372
+ "output-model",
373
+ "headquarters-binding",
374
+ "target-membership"
351
375
  ]
352
376
  },
353
377
  "command": {
@@ -1352,6 +1376,106 @@
1352
1376
  "description": "本地已进入发送边界的观测次数下界,不是服务端执行次数。"
1353
1377
  }
1354
1378
  }
1379
+ },
1380
+ "headquartersBindingRequest": {
1381
+ "type": "object",
1382
+ "additionalProperties": false,
1383
+ "properties": {
1384
+ "method": {
1385
+ "const": "POST"
1386
+ },
1387
+ "path": {
1388
+ "const": "youzan.shop.basic.get/3.0.0"
1389
+ },
1390
+ "body": {
1391
+ "type": "object",
1392
+ "additionalProperties": false,
1393
+ "properties": {},
1394
+ "required": []
1395
+ },
1396
+ "stage": {
1397
+ "const": "headquarters-binding"
1398
+ },
1399
+ "kdtId": {
1400
+ "$ref": "#/definitions/shopId"
1401
+ }
1402
+ },
1403
+ "required": [
1404
+ "method",
1405
+ "path",
1406
+ "body",
1407
+ "stage",
1408
+ "kdtId"
1409
+ ]
1410
+ },
1411
+ "targetBindingRequest": {
1412
+ "type": "object",
1413
+ "additionalProperties": false,
1414
+ "properties": {
1415
+ "method": {
1416
+ "const": "POST"
1417
+ },
1418
+ "path": {
1419
+ "const": "youzan.shop.basic.get/3.0.0"
1420
+ },
1421
+ "body": {
1422
+ "type": "object",
1423
+ "additionalProperties": false,
1424
+ "properties": {},
1425
+ "required": []
1426
+ },
1427
+ "stage": {
1428
+ "const": "shop-binding"
1429
+ },
1430
+ "kdtId": {
1431
+ "$ref": "#/definitions/shopId"
1432
+ }
1433
+ },
1434
+ "required": [
1435
+ "method",
1436
+ "path",
1437
+ "body",
1438
+ "stage",
1439
+ "kdtId"
1440
+ ]
1441
+ },
1442
+ "membershipRequest": {
1443
+ "type": "object",
1444
+ "additionalProperties": false,
1445
+ "properties": {
1446
+ "method": {
1447
+ "const": "POST"
1448
+ },
1449
+ "path": {
1450
+ "const": "youzan.shop.chain.descendent.organization.list/1.0.1"
1451
+ },
1452
+ "body": {
1453
+ "const": {
1454
+ "page_num": 1,
1455
+ "page_size": 50,
1456
+ "with_dept_info": false,
1457
+ "with_shop_status_and_lifecycle_end_time": false,
1458
+ "with_crm_shop": false
1459
+ }
1460
+ },
1461
+ "stage": {
1462
+ "const": "target-membership"
1463
+ },
1464
+ "kdtId": {
1465
+ "$ref": "#/definitions/shopId"
1466
+ },
1467
+ "maxPages": {
1468
+ "const": 20
1469
+ }
1470
+ },
1471
+ "required": [
1472
+ "method",
1473
+ "path",
1474
+ "body",
1475
+ "stage",
1476
+ "kdtId",
1477
+ "maxPages"
1478
+ ]
1355
1479
  }
1356
1480
  },
1357
1481
  "modules": {
@@ -1440,6 +1564,9 @@
1440
1564
  "应用凭证实测不证明店铺员工身份授权与权限隔离。",
1441
1565
  "商品行 item_id/sku_id 为OneID,channel_item_id/channel_sku_id 为历史渠道身份;oid仍为交易行,不用当前档案重写历史。",
1442
1566
  "历史统一商品或规格字段为空字符串时,省略对应OneID并保留明确渠道身份,不用渠道ID补造;null和其他错型仍失败。",
1443
- "时间区间必须包含 .. 并显式两端;不接受旧成对参数或裸时间等值,非零毫秒在请求前拒绝。"
1567
+ "时间区间必须包含 .. 并显式两端;不接受旧成对参数或裸时间等值,非零毫秒在请求前拒绝。",
1568
+ "省略 --kdt-id 只查 context.shopId,总部默认仅总部本身,不包含分店。总部显式分店目标先核验总部角色及sid,再查下级组织目录,最后绑定分店sid;已返回的总部字段必须一致。查询不保存位置,不扩大解密权限。",
1569
+ "成员核验每页50条、最多20页。目录完整未命中为permission,未读完为scope_unresolved,字段异常为protocol;接口拒绝保留阶段与安全诊断。空订单也不能绕过前置。新增请求共享累计响应预算,同店请求保持兼容。",
1570
+ "所有总部、目录、分店绑定、探测与翻页请求均计入maxRequests,发送前检查;前置未完成即预算耗尽时失败,不输出未核实的订单或零值。"
1444
1571
  ]
1445
1572
  }
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const { resolveTargetShop } = require('../../../../execution/target-shop');
4
- const { createShopQuery, bindingPath, failure } = require('../../../../execution/shop-query');
4
+ const { createBoundShopQuery, bindingPath, failure } = require('../../../../execution/shop-query');
5
+ const { organizationPath, organizationBody, maxOrganizationPages,
6
+ verifyMembership } = require('../../../../identity/unit-membership');
5
7
  const { parseIncludeModules } = require('../../../../execution/command-io');
6
8
  const { copyErrorDetails } = require('../../../../execution/error-details');
7
9
 
@@ -10,7 +12,7 @@ const OUTPUT_LIMIT = 5 * 1024 * 1024;
10
12
 
11
13
  function reject(command, category, stage, scope = {}, requestState = 'not_sent', facts = {}) {
12
14
  const error = failure(command, category, stage, scope, requestState, facts);
13
- error.cliDetails.nextStep = stage === 'shop-binding' ?
15
+ error.cliDetails.nextStep = ['headquarters-binding', 'target-membership', 'shop-binding'].includes(stage) ?
14
16
  '主订单请求尚未发送。核对原凭证绑定店铺与显式目标,不自动重试、切店或换身份。' :
15
17
  category === 'limit' ? '本次结果超出字节限制,未输出部分订单;停止并核对原范围,不截断或自动重试。' :
16
18
  '核对原店铺、订单号、上下文与服务端授权;结构不符时停止,不自动重试、解密或换身份。';
@@ -49,21 +51,67 @@ function includes(value, allowed, command, scope) {
49
51
  catch { throw reject(command, 'validation', 'input', scope); }
50
52
  }
51
53
 
54
+ function bindingPreview(scope, context) {
55
+ const target = { method: 'POST', path: bindingPath, body: {} };
56
+ if (context?.orgType !== 'hq' || scope.kdtId === context.shopId) return [target];
57
+ return [
58
+ { ...target, stage: 'headquarters-binding', kdtId: context.hq },
59
+ { method: 'POST', path: organizationPath, body: organizationBody(1),
60
+ stage: 'target-membership', kdtId: context.hq, maxPages: maxOrganizationPages },
61
+ { ...target, stage: 'shop-binding', kdtId: scope.kdtId }
62
+ ];
63
+ }
64
+
52
65
  async function createOrderQuery(scope, command, { responseLimit = RESPONSE_LIMIT, onRequest,
53
- bounded = false, shopApp } = {}) {
66
+ bounded = false, shopApp = false, context, maxRequests = maxOrganizationPages + 3 } = {}) {
54
67
  let responseBytes = 0;
68
+ let requests = 0;
69
+ let requestStage;
70
+ function beforeRequest(path, stage) {
71
+ if (requests >= maxRequests || responseBytes >= responseLimit) {
72
+ throw reject(command, 'limit', stage, scope, 'not_sent', { api: path });
73
+ }
74
+ requests++;
75
+ requestStage = stage;
76
+ }
55
77
  function onResponse(response, path) {
56
78
  responseBytes += response.responseBytes;
57
79
  if (responseBytes > responseLimit) {
58
- throw reject(command, 'limit', path === bindingPath ? 'shop-binding' : 'order', scope,
80
+ throw reject(command, 'limit', requestStage, scope,
59
81
  'sent', { api: path, httpStatus: response.httpStatus });
60
82
  }
61
83
  }
62
84
  try {
63
- const read = await createShopQuery({ kdtId: Number(scope.kdtId) }, command, {
64
- onResponse, transportOptions: { onRequest, shopApp,
65
- maxResponseBytes: bounded ? () => responseLimit - responseBytes : undefined }
66
- });
85
+ const options = { beforeRequest, onResponse, transportOptions: { onRequest, shopApp,
86
+ maxResponseBytes: bounded ? () => responseLimit - responseBytes : undefined } };
87
+ let environment;
88
+ const crossShop = context?.orgType === 'hq' && scope.kdtId !== context.shopId;
89
+ if (crossShop) {
90
+ const root = await createBoundShopQuery({ kdtId: Number(context.hq) }, command,
91
+ 'headquarters-binding', { ...options, transportOptions: { ...options.transportOptions, shopApp: false } });
92
+ if (context.shopId !== context.hq || root.binding.data.role !== 1) {
93
+ throw reject(command, 'scope_unresolved', 'headquarters-binding', scope, 'sent', root.binding.facts);
94
+ }
95
+ const member = await verifyMembership(root.read, scope.kdtId,
96
+ (category, stage, state, facts) => reject(command, category, stage, scope, state, facts),
97
+ { notFoundCategory: 'permission' });
98
+ if (![2001, 2002].includes(member.role)) {
99
+ throw reject(command, 'permission', 'target-membership', scope, 'sent');
100
+ }
101
+ environment = root.environment;
102
+ }
103
+ const target = await createBoundShopQuery({ kdtId: Number(scope.kdtId) }, command,
104
+ 'shop-binding', { ...options, environment,
105
+ transportOptions: { ...options.transportOptions, ...(crossShop && { shopApp: false }) } });
106
+ if (crossShop && Object.hasOwn(target.binding.data, 'root_kdt_id')) {
107
+ if (!Number.isSafeInteger(target.binding.data.root_kdt_id) || target.binding.data.root_kdt_id < 1) {
108
+ throw reject(command, 'protocol', 'shop-binding', scope, 'sent', target.binding.facts);
109
+ }
110
+ if (String(target.binding.data.root_kdt_id) !== context.hq) {
111
+ throw reject(command, 'scope_unresolved', 'shop-binding', scope, 'sent', target.binding.facts);
112
+ }
113
+ }
114
+ const read = target.read;
67
115
  return async (path, body, stage = 'order') => {
68
116
  try { return await read(path, body, stage); }
69
117
  catch (error) { throw adaptError(error, command, scope); }
@@ -80,5 +128,5 @@ function serialize(output, format, command, scope, outputLimit = OUTPUT_LIMIT) {
80
128
  }
81
129
 
82
130
  module.exports = {
83
- bindingPath, reject, targetScope, includes, createOrderQuery, serialize, RESPONSE_LIMIT, OUTPUT_LIMIT
131
+ bindingPath, bindingPreview, reject, targetScope, includes, createOrderQuery, serialize, RESPONSE_LIMIT, OUTPUT_LIMIT
84
132
  };
@@ -97,7 +97,7 @@ function adaptPage(data, input, context, facts, command = 'order.order.list') {
97
97
  }
98
98
 
99
99
  async function createOrderPageReader(scope, context, command, options) {
100
- const read = await createOrderQuery(scope, command, options);
100
+ const read = await createOrderQuery(scope, command, { ...options, context });
101
101
  return async input => {
102
102
  const { pageNo, pageSize } = input.pagination;
103
103
  if (input.scope.kdtId !== scope.kdtId || !Number.isSafeInteger(pageNo) || pageNo < 1 || pageNo > 100 ||
@@ -9,7 +9,7 @@
9
9
  "availability": "available",
10
10
  "at": "shop",
11
11
  "module": "org/staff/get/command.js",
12
- "fingerprint": "0e6c6fd91988b916f4d5690677c712059ec65f7d6ce003eed7d7192ce61f6d96"
12
+ "fingerprint": "d35a2747712ebbd21c5bce834a71328620c2787a5bb1573ae9131021f7557b4e"
13
13
  },
14
14
  {
15
15
  "path": "org.staff-role.get",
@@ -18,7 +18,7 @@
18
18
  "availability": "available",
19
19
  "at": "shop",
20
20
  "module": "org/staff-role/get/command.js",
21
- "fingerprint": "cfa719f5122449c0a57e5db59bfd760d17d717f95088c9d886586b53304a7041"
21
+ "fingerprint": "811c7e532d54d940f706d16a7515d1fc8bb4ff42d6274ec77522d3883ad85e97"
22
22
  },
23
23
  {
24
24
  "path": "org.unit.get",
@@ -27,7 +27,7 @@
27
27
  "availability": "available",
28
28
  "at": "all",
29
29
  "module": "org/unit/get/command.js",
30
- "fingerprint": "d11775de4f1c6d7500310c3f8b373bee7e829ab5afe586ae617037571b4d0597"
30
+ "fingerprint": "19198e6fe03e50b0852f103b5a021f83e6d4de7372590374c68be60ac74d8571"
31
31
  },
32
32
  {
33
33
  "path": "org.unit.list",
@@ -36,7 +36,7 @@
36
36
  "availability": "available",
37
37
  "at": "hq",
38
38
  "module": "org/unit/list/command.js",
39
- "fingerprint": "6bee5cdcffb1893319280f50f9b49e752d16dbadcae5413bce3bbfcf476a9fc6"
39
+ "fingerprint": "898b0604f54d2ae28724e816eb1ffd0ada2fd544ed7aa0c69fffc3958f3408ce"
40
40
  }
41
41
  ]
42
42
  }
@@ -9,7 +9,7 @@
9
9
  "availability": "available",
10
10
  "at": "all",
11
11
  "module": "valuecard/card/get/command.js",
12
- "fingerprint": "d5819f697caf55a03a7a111abfa9c86764fb2597a536be9e5e5d705c1a91829e"
12
+ "fingerprint": "c8456b31165de4102619f8ceaa6be3d172ffaa997e7ec1d1ae59c80e0fb3438d"
13
13
  },
14
14
  {
15
15
  "path": "valuecard.card.list",
@@ -18,7 +18,25 @@
18
18
  "availability": "available",
19
19
  "at": "all",
20
20
  "module": "valuecard/card/list/command.js",
21
- "fingerprint": "d96d40fd6d68d96f62a809de403fe79274bb4ed671fd166eb6ecba22acd55179"
21
+ "fingerprint": "d353ad89174f2ebec068d9073a6765c637c02cfb644bc3cdfdf3c3f80fa578d9"
22
+ },
23
+ {
24
+ "path": "valuecard.transaction.list",
25
+ "kind": "atomic",
26
+ "risk": "read",
27
+ "availability": "available",
28
+ "at": "all",
29
+ "module": "valuecard/transaction/list/command.js",
30
+ "fingerprint": "32269b8370250350f36c8069306d531111b6c283ff0f20d97de04c318f9e2f44"
31
+ },
32
+ {
33
+ "path": "valuecard.transaction.scan",
34
+ "kind": "atomic",
35
+ "risk": "read",
36
+ "availability": "available",
37
+ "at": "all",
38
+ "module": "valuecard/transaction/scan/command.js",
39
+ "fingerprint": "ad4226dd57317e497547b5bc690131009d1b9da2e3e52115b4c006653d89e443"
22
40
  }
23
41
  ]
24
42
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "description": "储值卡:余额卡与礼品卡的资产和余额"
2
+ "description": "储值卡:余额卡与礼品卡的资产、余额、单页流水及有界精确查找"
3
3
  }
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const { Option } = require('commander');
4
+ const { DiagnosticCommand } = require('../../../../../cli/diagnostic-command');
5
+ const { withTargetShopDefault } = require('../../../../../execution/target-shop-default');
6
+ const { resolveTargetShop } = require('../../../../../execution/target-shop');
7
+ const { inputIssue, parseIntegerOption, withInputScope } = require('../../../../../execution/error-details');
8
+ const { validateCommandValue } = require('../../../../../execution/command-schema');
9
+ const { parseDateTimeRange } = require('../../../../../execution/range');
10
+ const { createShopQuery, failure, bindingPath } = require('../../../../../execution/shop-query');
11
+ const { project } = require('./projection');
12
+ const schema = require('./schema.json');
13
+
14
+ const command = 'valuecard.transaction.list';
15
+ const responseLimit = 10 * 1024 * 1024;
16
+ const outputLimit = 5 * 1024 * 1024;
17
+
18
+ function body(input) {
19
+ return { biz_type: input.bizType.toUpperCase(), begin_time: input.occurredAt.start.slice(0, 19),
20
+ end_time: input.occurredAt.end.slice(0, 19), page: input.pageNo, page_size: input.pageSize };
21
+ }
22
+
23
+ function buildInput(options, context) {
24
+ const scope = resolveTargetShop(options, context, command);
25
+ return withInputScope(scope, () => {
26
+ let occurredAt;
27
+ try {
28
+ occurredAt = parseDateTimeRange(options.occurredAt, { precision: 'seconds', requireBoth: true, strictOrder: true });
29
+ } catch {
30
+ throw inputIssue('/occurredAt', 'invalid_format', {
31
+ format: '北京时间 START..END,两端为有效日历时间 YYYY-MM-DD HH:mm:ss,可带零毫秒,开始须早于结束'
32
+ });
33
+ }
34
+ const input = { ...scope, bizType: options.bizType, occurredAt,
35
+ pageNo: parseIntegerOption(options.pageNo, schema, '/pageNo'),
36
+ pageSize: parseIntegerOption(options.pageSize, schema, '/pageSize'),
37
+ format: options.format, dryRun: options.dryRun === true };
38
+ validateCommandValue(schema, 'input', input);
39
+ if (input.pageNo * input.pageSize > 5000) {
40
+ throw inputIssue('/pageNo', 'constraint', { format: 'pageNo * pageSize <= 5000' }, ['/pageSize']);
41
+ }
42
+ return input;
43
+ });
44
+ }
45
+
46
+ async function execute(input) {
47
+ const scope = { kdtId: input.kdtId };
48
+ const read = await createShopQuery(scope, command, {
49
+ transportOptions: { maxResponseBytes: () => responseLimit, limitCategory: 'limit', limitStage: 'response-bytes', shopApp: false }
50
+ });
51
+ const response = await read(schema.api.path, body(input), 'transactions');
52
+ try {
53
+ const output = { data: project(response.data, input) };
54
+ validateCommandValue(schema, 'output', output);
55
+ return output;
56
+ } catch {
57
+ throw failure(command, 'protocol', 'transactions-model', scope, 'sent', response.facts);
58
+ }
59
+ }
60
+
61
+ function createCommand({ context } = {}) {
62
+ const leaf = new DiagnosticCommand('list', { path: command, schema, parameterCategory: 'parameter' })
63
+ .description('按一种业务类型和明确业务时间,读取一页储值交易流水及本金、赠送金和记录余额')
64
+ .allowExcessArguments(false)
65
+ .option('--kdt-id <id>', '目标店铺正安全整数;省略取上下文 shopId,非总部不可跨店')
66
+ .requiredOption('--biz-type <type>', '单个小写业务类型,完整字典见 schema;不接受 CSV 或 all')
67
+ .requiredOption('--occurred-at <START..END>', '北京时间 YYYY-MM-DD HH:mm:ss,两端必填且开始早于结束;仅接受零毫秒')
68
+ .option('--page-no <n>', '页码从 1 开始,pageNo * pageSize <= 5000', '1')
69
+ .option('--page-size <n>', '每页容量 1..50,默认 20;每次只读一页', '20')
70
+ .addOption(new Option('--format <format>', 'json 或 pretty,仅改变缩进').choices(['json', 'pretty']).default('json'))
71
+ .option('--dry-run', '本地脱敏请求预览,不读凭证、不联网')
72
+ .addHelpText('after', `
73
+ 业务类型:recharge、group_give_recharge、refund、recharge_refund、pay、card_account_adjustment_add、
74
+ card_account_adjustment_subtract、card_balance_import_add、card_balance_import_subtract、recede_card、
75
+ gift_card_active、recharge_bonus、refund_close_transfer_in、refund_close_transfer_out、combine_in、combine_out。
76
+ 输出 data.transactions 与 data.pagination,金额为人民币元两位字符串,时间为北京时间毫秒文本。
77
+ billCardNo 不保证等于 card.cardNo;amount 不代表统一增减方向,reportedBalance 不保证交易前后时点。
78
+ 不按客户、卡号或订单筛选,不自动翻页或重试。最多一次同店绑定和一次流水请求。
79
+ hasMore=null 表示总数未知;nextPageNo=null 不单独证明耗尽,达到窗口时缩小时间范围重新查询。
80
+ 不承诺稳定排序、跨页快照、全历史或自动余额对账;失败时不输出部分流水。
81
+ 产品支持范围由服务端决定:新储值商家可查全部卡交易,老储值商家仅支持礼品卡交易。
82
+ 示例:zan-saas valuecard transaction list --biz-type recharge --occurred-at "2026-11-01 00:00:00..2026-11-11 12:00:00" --kdt-id 63077 --dry-run
83
+ `)
84
+ .action(async options => {
85
+ const input = buildInput(options, context);
86
+ let output;
87
+ if (input.dryRun) {
88
+ output = { command, kind: 'atomic', risk: 'read', target: { kdtId: input.kdtId }, requests: [
89
+ { method: 'POST', path: bindingPath, body: {} },
90
+ { method: 'POST', path: schema.api.path, body: body(input) }
91
+ ] };
92
+ validateCommandValue(schema, 'preview', output);
93
+ } else output = await execute(input);
94
+ const text = `${JSON.stringify(output, null, input.format === 'pretty' ? 2 : 0)}\n`;
95
+ if (Buffer.byteLength(text, 'utf8') > outputLimit) {
96
+ throw failure(command, 'limit', 'output-bytes', { kdtId: input.kdtId }, input.dryRun ? 'not_sent' : 'sent');
97
+ }
98
+ process.stdout.write(text);
99
+ });
100
+ const seen = new Set();
101
+ for (const option of leaf.options) {
102
+ leaf.on(`option:${option.name()}`, () => {
103
+ if (seen.has(option.long)) throw leaf.issue('constraint', option);
104
+ seen.add(option.long);
105
+ });
106
+ }
107
+ return leaf;
108
+ }
109
+
110
+ module.exports = { kind: 'atomic', risk: 'read', at: 'all', createCommand: withTargetShopDefault(createCommand) };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const { object } = require('../../../../../execution/query-api');
4
+ const { normalizeMoney } = require('../../../../../execution/money');
5
+ const { fromBeijingSeconds } = require('../../../../../execution/beijing-date-time');
6
+
7
+ function requiredText(value) {
8
+ if (typeof value !== 'string' || !value.trim() || /[\u0000-\u001f\u007f-\u009f]/u.test(value)) {
9
+ throw new TypeError('Invalid transaction text');
10
+ }
11
+ return value;
12
+ }
13
+
14
+ function money(value) {
15
+ if (!Number.isSafeInteger(value)) throw new TypeError('Invalid transaction money');
16
+ return normalizeMoney(value, { $comment: 'upstream-money:fen' });
17
+ }
18
+
19
+ function projectTransaction(item) {
20
+ if (!object(item)) throw new TypeError('Invalid transaction');
21
+ const transaction = {
22
+ transactionNo: requiredText(item.consumption_no), billCardNo: requiredText(item.group_no),
23
+ occurredAt: fromBeijingSeconds(item.consumption_time), bizType: requiredText(item.biz_type),
24
+ validity: requiredText(item.valid),
25
+ amount: { totalAmount: money(item.consumption_amount), principalAmount: money(item.principal_consumption_amount),
26
+ bonusAmount: money(item.bonus_consumption_amount) },
27
+ reportedBalance: { totalBalance: money(item.sum_dnom), principalBalance: money(item.principal_dnom),
28
+ bonusBalance: money(item.bonus_dnom) }
29
+ };
30
+ if (BigInt(item.sum_dnom) !== BigInt(item.principal_dnom) + BigInt(item.bonus_dnom)) {
31
+ throw new TypeError('Invalid reported balance');
32
+ }
33
+ if (Object.hasOwn(item, 'kdt_id')) {
34
+ if (!Number.isSafeInteger(item.kdt_id) || item.kdt_id < 1) throw new TypeError('Invalid record shop');
35
+ transaction.kdtId = String(item.kdt_id);
36
+ }
37
+ const optional = { mobile: 'mobile', tradeNo: 'trade_no', externalRequestNo: 'out_request_no',
38
+ remark: 'remark', externalRemark: 'out_remark', nickname: 'nick_name', acceptShopName: 'accept_shop_name' };
39
+ for (const [target, source] of Object.entries(optional)) {
40
+ if (!Object.hasOwn(item, source)) continue;
41
+ if (typeof item[source] !== 'string') throw new TypeError('Invalid optional transaction text');
42
+ transaction[target] = item[source];
43
+ }
44
+ return transaction;
45
+ }
46
+
47
+ function projectPagination(paginator, items, input) {
48
+ const invalid = () => new TypeError('Invalid transaction pagination');
49
+ if (!object(paginator) || !Array.isArray(items)) throw invalid();
50
+ const pageNo = paginator.page;
51
+ const pageSize = Object.hasOwn(paginator, 'page_size') ? paginator.page_size : input.pageSize;
52
+ const hasTotal = Object.hasOwn(paginator, 'total_count');
53
+ const total = hasTotal ? paginator.total_count : null;
54
+ if (hasTotal && (!Number.isSafeInteger(total) || total < 0)) throw invalid();
55
+ if (!Number.isSafeInteger(pageNo) || pageNo !== input.pageNo || pageNo < 1 ||
56
+ !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 50 || pageNo * pageSize > 5000 ||
57
+ items.length > pageSize || (total !== null && items.length > total)) throw invalid();
58
+ const hasMore = total === null ? null : pageNo * pageSize < total;
59
+ const nextFits = (pageNo + 1) * pageSize <= 5000;
60
+ return { pageNo, pageSize, returned: items.length, ...(total === null ? {} : { total }), hasMore,
61
+ nextPageNo: hasMore === true && nextFits ? pageNo + 1 : null,
62
+ windowLimitReached: hasMore !== false && !nextFits };
63
+ }
64
+
65
+ function project(data, input) {
66
+ if (!object(data)) throw new TypeError('Invalid transaction data');
67
+ const pagination = projectPagination(data.paginator, data.items, input);
68
+ return { transactions: data.items.map(projectTransaction), pagination };
69
+ }
70
+
71
+ module.exports = { project, projectTransaction, projectPagination };