@youzan-cloud/zan-saas-cli 0.1.0-beta.7 → 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.
- package/internal/registry/manifest.json +21 -17
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/zan-saas-order/SKILL.md +12 -1
- package/skills/zan-saas-order/references/branch-counts.md +45 -0
- package/skills/zan-saas-order/references/order-get.md +2 -0
- package/skills/zan-saas-order/references/order-list.md +2 -0
- package/skills/zan-saas-order/references/order-scan.md +2 -0
- package/skills/zan-saas-org/SKILL.md +2 -0
- package/skills/zan-saas-org/references/unit-list.md +2 -0
- package/skills/zan-saas-shared/SKILL.md +4 -0
- package/skills/zan-saas-shared/references/gateway-errors.md +29 -0
- package/skills/zan-saas-shared/references/shop-context.md +3 -1
- package/skills/zan-saas-valuecard/SKILL.md +8 -2
- package/skills/zan-saas-valuecard/references/transaction-list.md +25 -0
- package/skills/zan-saas-valuecard/references/transaction-scan.md +23 -0
- package/src/cli/context-help.js +3 -1
- package/src/commands/native/access/commands.index.json +1 -1
- package/src/commands/native/access/role/list/command.js +4 -32
- package/src/commands/native/order/commands.index.json +3 -3
- package/src/commands/native/order/order/get/command.js +14 -9
- package/src/commands/native/order/order/get/schema.json +197 -41
- package/src/commands/native/order/order/list/command.js +9 -6
- package/src/commands/native/order/order/list/schema.json +140 -10
- package/src/commands/native/order/order/scan/command.js +8 -5
- package/src/commands/native/order/order/scan/scan.js +1 -1
- package/src/commands/native/order/order/scan/schema.json +135 -8
- package/src/commands/native/order/shared/query.js +57 -9
- package/src/commands/native/order/shared/search.js +1 -1
- package/src/commands/native/org/commands.index.json +4 -4
- package/src/commands/native/valuecard/commands.index.json +20 -2
- package/src/commands/native/valuecard/domain.json +1 -1
- package/src/commands/native/valuecard/transaction/list/command.js +110 -0
- package/src/commands/native/valuecard/transaction/list/projection.js +71 -0
- package/src/commands/native/valuecard/transaction/list/schema.json +888 -0
- package/src/commands/native/valuecard/transaction/scan/command.js +130 -0
- package/src/commands/native/valuecard/transaction/scan/projection.js +58 -0
- package/src/commands/native/valuecard/transaction/scan/scan.js +70 -0
- package/src/commands/native/valuecard/transaction/scan/schema.json +954 -0
- package/src/commands/system/shop-contracts.js +35 -4
- package/src/execution/error-details.js +13 -18
- package/src/execution/gateway-guidance.js +43 -0
- package/src/execution/shop-query.js +6 -3
- package/src/identity/shop-location.js +18 -13
- package/src/identity/unit-membership.js +42 -0
- package/src/transport/api.js +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const object = properties => ({
|
|
4
|
-
type: 'object', additionalProperties: false, required
|
|
3
|
+
const object = (properties, required = Object.keys(properties)) => ({
|
|
4
|
+
type: 'object', additionalProperties: false, required, properties
|
|
5
5
|
});
|
|
6
6
|
const id = { type: 'string', pattern: '^[1-9][0-9]*$', description: '正整数 ID 文本。' };
|
|
7
7
|
const nullableText = { type: ['string', 'null'], description: '已核实的名称或时间;缺失为 null。' };
|
|
@@ -36,10 +36,37 @@ const effect = action => object({
|
|
|
36
36
|
const chosenOutput = action => object({
|
|
37
37
|
context: nullable(context), selection: nullable(selection), binding, effect: effect(action)
|
|
38
38
|
});
|
|
39
|
+
const diagnosticCode = { anyOf: [
|
|
40
|
+
{ type: 'integer', minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER },
|
|
41
|
+
{ type: 'string', pattern: '^-?\\d{1,12}$' }
|
|
42
|
+
] };
|
|
43
|
+
const trace = { type: 'string', pattern: '^[a-zA-Z0-9_-]{1,128}$' };
|
|
44
|
+
const locationError = command => object({
|
|
45
|
+
success: { const: false }, code: { const: -10001 }, message: { type: 'string' },
|
|
46
|
+
details: object({
|
|
47
|
+
category: { enum: ['validation', 'configuration', 'authentication', 'permission', 'http',
|
|
48
|
+
'transport', 'protocol', 'business_rejected', 'limit', 'scope_unresolved'] },
|
|
49
|
+
stage: { enum: ['scope', 'configuration', 'headquarters-binding', 'target-membership'] },
|
|
50
|
+
command: { const: command },
|
|
51
|
+
targetScope: object({ kdtId: { anyOf: [id,
|
|
52
|
+
{ type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER }
|
|
53
|
+
], description: '失败阶段核验的总部或目标店铺,不表示已切换位置。' } }),
|
|
54
|
+
requestState: { enum: ['not_sent', 'sent', 'unknown'] },
|
|
55
|
+
nextStep: { type: 'string', minLength: 1 },
|
|
56
|
+
api: { type: 'string', pattern: '^[a-z][a-z0-9._-]*/\\d+\\.\\d+\\.\\d+$' },
|
|
57
|
+
httpStatus: { type: 'integer', minimum: 100, maximum: 599 },
|
|
58
|
+
businessCode: diagnosticCode, gatewayCode: diagnosticCode,
|
|
59
|
+
trace_id: trace, request_id: trace
|
|
60
|
+
}, ['category', 'stage', 'command', 'targetScope', 'requestState', 'nextStep'])
|
|
61
|
+
});
|
|
39
62
|
const notes = [
|
|
40
63
|
'上下文及本地保存不证明员工权限;账号、组织关系和业务授权分别核验。失败后不自动换店、换身份或重放。',
|
|
41
64
|
'会话模式由宿主逐次注入 ZAN_SAAS_SESSION_ID。已有会话不跟随其他会话,查询和恢复不更新最近位置。'
|
|
42
65
|
];
|
|
66
|
+
const locationNotes = [
|
|
67
|
+
'总部绑定或目录核验失败时可返回受限 details:category、stage、实际 command、targetScope、requestState 和 nextStep;可用时保留 API、HTTP 状态、数字业务码及安全 trace/request 标识,不回显响应正文、凭证或上游自由文本。',
|
|
68
|
+
'targetScope 是失败检查的对象,总部请求失败时可为总部 ID,不代表已选择目标。失败不改变当前位置或最近位置;旧的本地错误及公共参数解析错误仍保留各自模型。'
|
|
69
|
+
];
|
|
43
70
|
const switchInput = {
|
|
44
71
|
kdtId: id, sessionOnly: { type: 'boolean', description: '仅更新本会话,需要宿主会话标识;省略为false。' }
|
|
45
72
|
};
|
|
@@ -51,15 +78,19 @@ const contracts = {
|
|
|
51
78
|
'显式初始化可核实当前账号并创建绑定。首次继承一次;无最近位置返回needs_selection;恢复已有记录不改时间和最近位置。']
|
|
52
79
|
},
|
|
53
80
|
'shop.unit.use': {
|
|
81
|
+
error: { anyOf: [object({ success: { const: false }, code: { const: -10001 },
|
|
82
|
+
message: { type: 'string' } }), locationError('shop.unit.use')] },
|
|
54
83
|
input: { ...object(switchInput), required: ['kdtId'] },
|
|
55
|
-
output: chosenOutput('use_unit'), notes: [...notes,
|
|
84
|
+
output: chosenOutput('use_unit'), notes: [...notes, ...locationNotes,
|
|
56
85
|
'需要已初始化会话及当前租户。只支持已核实的当前连锁总部、门店或网店,不改变租户根。']
|
|
57
86
|
},
|
|
58
87
|
'shop.resolve': {
|
|
88
|
+
error: { anyOf: [object({ success: { const: false }, code: { const: -10001 },
|
|
89
|
+
message: { type: 'string' } }), locationError('shop.resolve')] },
|
|
59
90
|
input: { ...object({ kdtId: id, rootKdtId: { ...id, description: '目标所属根组织;省略时目标须为账号层根。' } }),
|
|
60
91
|
required: ['kdtId'] },
|
|
61
92
|
output: object({ context, selection, effect: effect('resolve'), revision: { type: 'null' } }),
|
|
62
|
-
notes: [...notes, '不保存当前位置。把返回context固定为本次任务每次调用的显式上下文;任务结束后丢弃。']
|
|
93
|
+
notes: [...notes, ...locationNotes, '不保存当前位置。把返回context固定为本次任务每次调用的显式上下文;任务结束后丢弃。']
|
|
63
94
|
}
|
|
64
95
|
};
|
|
65
96
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { executionFor } = require('./request-state');
|
|
4
4
|
const { issueReasons } = require('./error-model');
|
|
5
5
|
const { ParserError } = require('./parser-error');
|
|
6
|
+
const { gatewayGuidance } = require('./gateway-guidance');
|
|
6
7
|
const contexts = new WeakMap();
|
|
7
8
|
const reasons = new Set(issueReasons);
|
|
8
9
|
const constraintKeys = ['type', 'enum', 'const', 'minimum', 'maximum', 'exclusiveMinimum',
|
|
@@ -203,7 +204,8 @@ function localContextReason(error, details, execution) {
|
|
|
203
204
|
execution.observedAttempts === 0 ? error.diagnosticReason : undefined;
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
function describeRecovery(details, execution, missingAuthentication = false, contextReason, responseReason
|
|
207
|
+
function describeRecovery(details, execution, missingAuthentication = false, contextReason, responseReason,
|
|
208
|
+
gateway = gatewayGuidance(details, undefined, execution)) {
|
|
207
209
|
if (contextReason) return { action: 'stop_and_inspect', nextStep:
|
|
208
210
|
['context_missing', 'target_missing'].includes(contextReason)
|
|
209
211
|
? '未登录先执行 zan-saas auth login;登录后用 zan-saas shop list 和 zan-saas shop use <kdtId> 选店。缺少上下文时仅可显式指定店铺做 --dry-run,不自动切店或重放。'
|
|
@@ -221,14 +223,13 @@ function describeRecovery(details, execution, missingAuthentication = false, con
|
|
|
221
223
|
if (details.category === 'provider_unavailable' && details.command === 'privacy.field.decrypt') {
|
|
222
224
|
return { action: 'stop_and_inspect', nextStep: '当前仅支持离线预览,请加 --dry-run;真实解密服务尚未接入。' };
|
|
223
225
|
}
|
|
226
|
+
if (responseReason) return { action: 'stop_and_inspect',
|
|
227
|
+
nextStep: `${responseReason.nextStep} 不自动重放、切店或换身份。` };
|
|
228
|
+
if (gateway) return { action: gateway.action, nextStep: gateway.nextStep };
|
|
224
229
|
if (missingAuthentication || details.category === 'authentication') {
|
|
225
230
|
return { action: 'complete_authentication',
|
|
226
231
|
nextStep: '检查 zan-saas auth status --format json,按公共认证流程完成登录;不在聊天中提供凭证,不自动重放原调用。' };
|
|
227
232
|
}
|
|
228
|
-
if (gatewayAccessDenied(details)) {
|
|
229
|
-
return { action: 'request_access_review',
|
|
230
|
-
nextStep: '请检查当前账号是否有该店铺权限,以及店铺应用是否已获该接口的开放能力授权;可提供接口和 trace_id 给管理员排查,不自动重放。' };
|
|
231
|
-
}
|
|
232
233
|
if (details.category === 'permission') {
|
|
233
234
|
return { action: 'request_access_review',
|
|
234
235
|
nextStep: '核对原账号、目标店铺与服务端授权,可提供脱敏接口及 trace_id 给管理员;不切店、换身份或自动重放。' };
|
|
@@ -237,8 +238,6 @@ function describeRecovery(details, execution, missingAuthentication = false, con
|
|
|
237
238
|
return { action: 'stop_and_inspect',
|
|
238
239
|
nextStep: '店铺核验失败,后续业务请求尚未发送;本次已有核验请求记录,核对原店铺和凭证后停止,不自动切店或重放。' };
|
|
239
240
|
}
|
|
240
|
-
if (responseReason) return { action: 'stop_and_inspect',
|
|
241
|
-
nextStep: `${responseReason.nextStep} 不自动重放、切店或换身份。` };
|
|
242
241
|
if (details.requestState === 'sent' && details.category === 'business_rejected') {
|
|
243
242
|
return { action: 'stop_and_inspect',
|
|
244
243
|
nextStep: '接口已拒绝本次查询;将诊断中的 api、businessCode 或 gatewayCode、trace_id 提供给管理员核查服务端原因。错误码本身不能证明是权限或数据问题,不自动重放、切店或换身份。' };
|
|
@@ -275,19 +274,14 @@ function businessRejectionMessage(details) {
|
|
|
275
274
|
return `接口拒绝了本次查询${code ? `(${code})` : ''},已停止处理。`;
|
|
276
275
|
}
|
|
277
276
|
|
|
278
|
-
function gatewayAccessDenied(details) {
|
|
279
|
-
return details?.requestState === 'sent' &&
|
|
280
|
-
['business_rejected', 'permission'].includes(details.category) &&
|
|
281
|
-
[4204, '4204'].includes(details.gatewayCode);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
277
|
function finalizeError(error, registeredContext) {
|
|
285
278
|
if (error instanceof ParserError) return structuredClone(error.result);
|
|
286
279
|
const context = contexts.get(error) || registeredContext;
|
|
287
280
|
if (context?.schema.errors?.diagnosticsVersion !== 2) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
error.
|
|
281
|
+
const gateway = gatewayGuidance(error.cliDetails, context);
|
|
282
|
+
if (gateway) {
|
|
283
|
+
error.message = gateway.message;
|
|
284
|
+
error.cliDetails.nextStep = gateway.nextStep;
|
|
291
285
|
}
|
|
292
286
|
return { success: false, code: -10001, message: error.message,
|
|
293
287
|
...(error.cliDetails ? { details: error.cliDetails } : {}) };
|
|
@@ -372,7 +366,8 @@ function diagnosticError(error, context, execution) {
|
|
|
372
366
|
if (error.issuesTruncated) details.issuesTruncated = true;
|
|
373
367
|
const contextReason = localContextReason(error, details, execution);
|
|
374
368
|
const responseReason = declaredResponseReason(error, context, details, execution);
|
|
375
|
-
const
|
|
369
|
+
const gateway = !inputError && !contextReason && !responseReason ? gatewayGuidance(details, context, execution) : undefined;
|
|
370
|
+
const recovery = describeRecovery(details, execution, error.diagnosticReason === 'missing_authentication', contextReason, responseReason, gateway);
|
|
376
371
|
const groupConflict = inputError && context.path === 'item.group.list' &&
|
|
377
372
|
issues.some(issue => issue.path === '/parentGroupId' && issue.reason === 'constraint' && issue.relatedPaths?.includes('/title'))
|
|
378
373
|
? context.schema.errors.parentTitleConflict : undefined;
|
|
@@ -382,9 +377,9 @@ function diagnosticError(error, context, execution) {
|
|
|
382
377
|
const result = { success: false, code: -10001,
|
|
383
378
|
message: inputError ? (groupConflict?.message || issueMessage(issues)) :
|
|
384
379
|
contextReason ? localContextMessages[contextReason] :
|
|
385
|
-
gatewayAccessDenied(details) ? '网关拒绝访问:没有操作权限。' :
|
|
386
380
|
details.category === 'provider_unavailable' && context.path === 'privacy.field.decrypt' ? '当前仅支持离线预览,真实解密服务尚未接入' :
|
|
387
381
|
responseReason ? responseReason.message :
|
|
382
|
+
gateway ? gateway.message :
|
|
388
383
|
details.category === 'business_rejected' && details.requestState === 'sent' ? businessRejectionMessage(details) :
|
|
389
384
|
issues.some(issue => issue.source === 'output') ?
|
|
390
385
|
`返回数据字段 ${issues.find(issue => issue.source === 'output').path} 不符合要求,已停止查询` :
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const authenticationStep = '先区分当前凭证来源与目标店铺、应用授权范围;个人登录使用 zan-saas auth login 重新登录授权,显式环境身份由凭证提供方更新或补齐配置。zan-saas auth status 仅查看本地保存状态,不能证明环境 token 有效;不在聊天中提供凭证,不自动切换来源或刷新,不自动重放。';
|
|
4
|
+
const supportStep = '先按同版 help/schema 核对参数契约;参数符合且持续失败时,将诊断中实际保留的 api、gatewayCode 和追踪号提供给支持人员排查,不提供凭证、客户标识或请求 body,不自动重放。';
|
|
5
|
+
const guidance = new Map([
|
|
6
|
+
[4101, { message: '网关限制了本次请求:调用过于频繁或受限。', action: 'stop_and_inspect',
|
|
7
|
+
nextStep: '停止本轮后续请求,检查调用频率、并发与可用配额;降低频率后由调用方重新决定是否发起。持续受限时提供诊断中实际保留的 api、gatewayCode 和追踪号排查,不保证固定恢复时间,不自动等待或重试,不自动重放。' }],
|
|
8
|
+
[4201, { message: '网关拒绝了本次请求:请求凭证无效。', action: 'complete_authentication',
|
|
9
|
+
nextStep: `核查凭证值以及店铺与应用授权;不能仅凭此码确定密码、员工权限或某一项授权失效。${authenticationStep}` }],
|
|
10
|
+
[4202, { message: '网关拒绝了本次请求:请求凭证已过期。', action: 'complete_authentication',
|
|
11
|
+
nextStep: authenticationStep }],
|
|
12
|
+
[4203, { message: '网关拒绝了本次请求:请求凭证不存在。', action: 'complete_authentication',
|
|
13
|
+
nextStep: authenticationStep }],
|
|
14
|
+
[4204, { message: '网关拒绝访问:没有操作权限。', action: 'request_access_review',
|
|
15
|
+
nextStep: '请检查当前账号是否有该店铺权限,以及店铺应用是否已获该接口的开放能力授权;不能仅凭此码确定根因,持续拒绝时提供诊断中实际保留的 api、gatewayCode 和追踪号给管理员核查服务端限制,不自动申请授权、切店或换身份,不自动重放。' }],
|
|
16
|
+
[4009, { message: '网关无法解析本次请求的客户标识 open_id。', action: 'stop_and_inspect',
|
|
17
|
+
nextStep: '按同版契约核对客户标识、标识类型与目标店铺;这不是登录凭证问题,也不能据此认定客户在所有店铺均不存在,不自动登录或切店,不自动重放。' }],
|
|
18
|
+
[5001, { message: '本次请求失败:网关报告上游系统异常。', action: 'stop_and_inspect', nextStep: supportStep }],
|
|
19
|
+
[5002, { message: '本次请求失败:网关报告上游业务异常。', action: 'stop_and_inspect', nextStep: supportStep }]
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function gatewayGuidance(details, context, execution) {
|
|
23
|
+
if (details?.requestState !== 'sent' ||
|
|
24
|
+
(execution && (execution.requestState !== 'sent' || execution.coverage !== 'complete')) ||
|
|
25
|
+
details.issues?.length) return undefined;
|
|
26
|
+
const raw = details.gatewayCode;
|
|
27
|
+
if (typeof raw !== 'number' && !(typeof raw === 'string' && /^[1-9]\d{3}$/.test(raw))) return undefined;
|
|
28
|
+
const code = Number(raw);
|
|
29
|
+
const selected = guidance.get(code);
|
|
30
|
+
if (!selected) return undefined;
|
|
31
|
+
const compatible = details.category === 'business_rejected' ||
|
|
32
|
+
(details.category === 'authentication' && [4201, 4202, 4203].includes(code)) ||
|
|
33
|
+
(details.category === 'permission' && code === 4204) ||
|
|
34
|
+
(details.category === 'http' && ((details.httpStatus === 429 && code === 4101) ||
|
|
35
|
+
(details.httpStatus >= 500 && [5001, 5002].includes(code))));
|
|
36
|
+
if (!compatible) return undefined;
|
|
37
|
+
const customerOption = code === 4009 && context?.command?.options.some(option => option.long === '--yz-open-id');
|
|
38
|
+
const bindingStep = details.stage === 'shop-binding' ? '店铺核验失败,后续业务请求尚未发送。' : '';
|
|
39
|
+
const customerStep = customerOption ? '核对本命令 --yz-open-id 的客户标识。' : '';
|
|
40
|
+
return { ...selected, nextStep: `${bindingStep}${customerStep}${selected.nextStep}` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { gatewayGuidance };
|
|
@@ -12,20 +12,23 @@ function failure(command, category, stage, scope = {}, requestState = 'not_sent'
|
|
|
12
12
|
return new QueryError(category, stage, scope, facts, requestState, command, nextStep);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async function createBoundShopQuery(target, command, bindingStage = 'shop-binding', {
|
|
15
|
+
async function createBoundShopQuery(target, command, bindingStage = 'shop-binding', {
|
|
16
|
+
onResponse, transportOptions, environment, beforeRequest
|
|
17
|
+
} = {}) {
|
|
16
18
|
const scope = Object.freeze({ kdtId: target.kdtId });
|
|
17
19
|
if (!Number.isSafeInteger(scope.kdtId) || scope.kdtId < 1) {
|
|
18
20
|
throw failure(command, 'validation', 'scope');
|
|
19
21
|
}
|
|
20
22
|
let env;
|
|
21
23
|
try {
|
|
22
|
-
env = await snapshotApiEnvironment(
|
|
24
|
+
env = await snapshotApiEnvironment(environment, scope.kdtId);
|
|
23
25
|
} catch (error) {
|
|
24
26
|
const rejected = failure(command, 'configuration', 'configuration', scope);
|
|
25
27
|
rejected.diagnosticReason = error.diagnosticReason;
|
|
26
28
|
throw rejected;
|
|
27
29
|
}
|
|
28
30
|
async function read(path, body, stage, successCodes = [200]) {
|
|
31
|
+
if (beforeRequest) beforeRequest(path, stage);
|
|
29
32
|
const { result, facts, rawBody, responseBytes } = await readApiEnvelope(path, body,
|
|
30
33
|
(category, details, state, at) => failure(command, category,
|
|
31
34
|
at === 'configuration' || category === transportOptions?.limitCategory ? at : stage,
|
|
@@ -43,7 +46,7 @@ async function createBoundShopQuery(target, command, bindingStage = 'shop-bindin
|
|
|
43
46
|
if (data.sid !== scope.kdtId) {
|
|
44
47
|
throw failure(command, 'scope_unresolved', bindingStage, scope, 'sent', facts);
|
|
45
48
|
}
|
|
46
|
-
return { read, binding };
|
|
49
|
+
return { read, binding, environment: env };
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
async function createShopQuery(target, command, options = {}) {
|
|
@@ -4,7 +4,9 @@ const account = require('../transport/account-api');
|
|
|
4
4
|
const { oauthConfig } = require('../auth/config');
|
|
5
5
|
const { readCredential } = require('../auth/credential-store');
|
|
6
6
|
const { authSource } = require('../auth/credential-source');
|
|
7
|
-
const {
|
|
7
|
+
const { snapshotApiIdentity } = require('../transport/api');
|
|
8
|
+
const { createBoundShopQuery, failure } = require('../execution/shop-query');
|
|
9
|
+
const { verifyMembership } = require('./unit-membership');
|
|
8
10
|
const sessions = require('./session-store');
|
|
9
11
|
const { readSavedContext } = require('./context-store');
|
|
10
12
|
const { createContext } = require('./context');
|
|
@@ -61,22 +63,25 @@ async function rootLocation(kdtId, actor) {
|
|
|
61
63
|
}) };
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
async function unitLocation(kdtId, root, actor) {
|
|
66
|
+
async function unitLocation(kdtId, root, actor, command) {
|
|
65
67
|
id(kdtId);
|
|
66
68
|
if (kdtId === root.context.hq) return root;
|
|
67
69
|
if (root.context.orgType !== 'hq') {
|
|
68
70
|
throw sessions.fail('unit_scope_unresolved', '当前店铺没有已核实的连锁总部,不能进入下级组织。');
|
|
69
71
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const scope = { kdtId };
|
|
73
|
+
const reject = (category, stage, state, facts) => {
|
|
74
|
+
const error = failure(command, category, stage, scope, state, facts);
|
|
75
|
+
error.cliDetails.nextStep = '未改变当前位置。核对原总部及目标,目录未读完不代表目标不属于该总部;不自动重试或初始化分店应用。';
|
|
76
|
+
return error;
|
|
77
|
+
};
|
|
78
|
+
const { read, binding } = await createBoundShopQuery({ kdtId: Number(root.context.hq) },
|
|
79
|
+
command, 'headquarters-binding', { environment: actor.pinned,
|
|
80
|
+
transportOptions: { shopApp: false, identityCheck: false, maxResponseBytes: () => 256 * 1024 } });
|
|
81
|
+
if (binding.data.role !== 1) {
|
|
82
|
+
throw reject('scope_unresolved', 'headquarters-binding', 'sent', binding.facts);
|
|
79
83
|
}
|
|
84
|
+
const data = await verifyMembership(read, kdtId, reject, { notFoundCategory: 'permission' });
|
|
80
85
|
if (![2001, 2002].includes(data.role)) {
|
|
81
86
|
throw sessions.fail('unit_not_supported', '本版仅支持已核实的连锁门店或网店位置。');
|
|
82
87
|
}
|
|
@@ -135,7 +140,7 @@ async function select(kdtId, options = {}, env = process.env, explicitContext) {
|
|
|
135
140
|
const rootId = options.unit ? expected.binding.location?.context.hq : kdtId;
|
|
136
141
|
if (!rootId) throw sessions.fail('unit_scope_unresolved', '请先选择当前连锁。');
|
|
137
142
|
const root = await rootLocation(rootId, actor);
|
|
138
|
-
const location = options.unit ? await unitLocation(kdtId, root, actor) : root;
|
|
143
|
+
const location = options.unit ? await unitLocation(kdtId, root, actor, 'shop.unit.use') : root;
|
|
139
144
|
const effect = sessions.commit(location, expected, options, env);
|
|
140
145
|
return result(options.unit ? 'use_unit' : 'use_root', effect, env);
|
|
141
146
|
}
|
|
@@ -145,7 +150,7 @@ async function resolve(kdtId, options = {}, env = process.env) {
|
|
|
145
150
|
const rootId = options.rootKdtId === undefined ? kdtId : id(options.rootKdtId);
|
|
146
151
|
const actor = await identify(env);
|
|
147
152
|
const root = await rootLocation(rootId, actor);
|
|
148
|
-
const location = await unitLocation(kdtId, root, actor);
|
|
153
|
+
const location = await unitLocation(kdtId, root, actor, 'shop.resolve');
|
|
149
154
|
return { ...location, effect: { action: 'resolve', currentChanged: false, recentChanged: false },
|
|
150
155
|
revision: null };
|
|
151
156
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { object } = require('../execution/query-api');
|
|
4
|
+
|
|
5
|
+
const organizationPath = 'youzan.shop.chain.descendent.organization.list/1.0.1';
|
|
6
|
+
const maxOrganizationPages = 20;
|
|
7
|
+
const pageSize = 50;
|
|
8
|
+
|
|
9
|
+
function organizationBody(pageNo) {
|
|
10
|
+
return { page_num: pageNo, page_size: pageSize, with_dept_info: false,
|
|
11
|
+
with_shop_status_and_lifecycle_end_time: false, with_crm_shop: false };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function verifyMembership(read, targetId, reject, { maxPages = maxOrganizationPages,
|
|
15
|
+
notFoundCategory = 'scope_unresolved' } = {}) {
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
let total;
|
|
18
|
+
let lastFacts;
|
|
19
|
+
for (let pageNo = 1; pageNo <= maxPages; pageNo += 1) {
|
|
20
|
+
const { data, facts } = await read(organizationPath, organizationBody(pageNo), 'target-membership');
|
|
21
|
+
lastFacts = facts;
|
|
22
|
+
const invalid = category => reject(category, 'target-membership', 'sent', facts);
|
|
23
|
+
if (!object(data) || !Array.isArray(data.organization_list) ||
|
|
24
|
+
data.page_num !== pageNo || data.page_size !== pageSize ||
|
|
25
|
+
!Number.isSafeInteger(data.total) || data.total < 0 ||
|
|
26
|
+
data.organization_list.length !== Math.min(pageSize, Math.max(0, data.total - seen.size)) ||
|
|
27
|
+
(total !== undefined && total !== data.total)) throw invalid('protocol');
|
|
28
|
+
total = data.total;
|
|
29
|
+
for (const unit of data.organization_list) {
|
|
30
|
+
if (!object(unit) || !Number.isSafeInteger(unit.kdt_id) || unit.kdt_id < 1 ||
|
|
31
|
+
!Number.isSafeInteger(unit.role) || unit.role < 0 || unit.role === 9 ||
|
|
32
|
+
seen.has(unit.kdt_id)) throw invalid('protocol');
|
|
33
|
+
seen.add(unit.kdt_id);
|
|
34
|
+
}
|
|
35
|
+
const member = data.organization_list.find(unit => String(unit.kdt_id) === String(targetId));
|
|
36
|
+
if (member) return member;
|
|
37
|
+
if (seen.size === total) throw invalid(notFoundCategory);
|
|
38
|
+
}
|
|
39
|
+
throw reject('scope_unresolved', 'target-membership', 'sent', lastFacts);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { organizationPath, organizationBody, maxOrganizationPages, verifyMembership };
|
package/src/transport/api.js
CHANGED
|
@@ -246,7 +246,7 @@ async function readResponseBody(response, maxResponseBytes) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
async function snapshotApiEnvironment(env = apiEnvironment(), kdtId) {
|
|
249
|
-
if (env.ZAN_SAAS_SESSION_ID !== undefined && env.ZAN_SAAS_KDT_ID &&
|
|
249
|
+
if (!boundTargets.has(env) && env.ZAN_SAAS_SESSION_ID !== undefined && env.ZAN_SAAS_KDT_ID &&
|
|
250
250
|
env.ZAN_SAAS_KDT_ID !== String(kdtId)) {
|
|
251
251
|
throw new ApiConfigurationError('显式请求店铺头与本次命令目标冲突。', 'context_override_conflict');
|
|
252
252
|
}
|