# 91卡券接入开发设计 ## 1. 目标 基于以下对外接口文档完成当前项目接入: - [3.异步卡密下单.md](./3.异步卡密下单.md) - [4.查询订单接口.md](./4.查询订单接口.md) 目标能力: 1. `91卡券` 调用我方异步下单接口; 2. 我方将订单作为独立接入平台订单写入当前项目; 3. 已配置商品自动走现有快手 Cloud 履约链路; 4. 未配置商品进入后台待补全队列,可手动补齐履约信息后重试; 5. 我方生成领取链接 `https://你的域名/#/claim/{token}`; 6. `91卡券` 调用查询订单接口; 7. 我方在 `cards` 中返回领取链接,由 `91卡券` 自动发货给买家。 ## 2. 设计原则 - `91卡券` 作为**独立订单来源**。 - `91卡券` 不是新的履约执行器,而是外部售卖、订单输入和自动发货通道。 - 当前项目真正的交付物仍然是 `claimUrl`。 - 查询接口返回 `20` 的判断标准是“领取链接已生成并可发”,不是“快手最终兑换完成”。 - `buyNum > 1` 时,必须拆成多个 task,并返回多个 card。 - 未命中履约配置时不丢单,先返回 `10` 并沉淀到后台待补全队列。 ## 3. 来源建模 新增独立来源标识: - `provider = '91kaquan'` - `platform = 'kuaishou'` - `shopId = '91kaquan'` - `shopName = '91卡券'` 原因: - 避免与其他平台回调语义混淆; - 避免复用历史后台拉单来源; - 便于后续单独做日志、排障和绑定配置。 ## 4. 接口设计 ### 4.1 路由 新增独立公开路由组: - `POST /api/v1/open/91/orders/create` - `POST /api/v1/open/91/orders/query` 接口使用独立公开路由组,原因: - `91` 需要自己的响应结构:`code/message/data`; - 与后台管理接口的统一响应结构隔离; - 便于按 91 的签名、时间戳和错误码规范单独演进。 ### 4.2 返回格式 对 `91` 路由统一返回: ```json { "code": 200, "message": "接口调用成功", "data": {} } ``` 业务失败也返回 `data.orderStatus = 30` 的业务响应。 仅在真正的协议级错误下返回: ```json { "code": 400, "message": "验签失败", "data": null } ``` ## 5. 配置设计 新增运行时配置项: ```js platforms: { ninetyone: { userId: '', secret: '', version: '1.0', shopId: '91kaquan', shopName: '91卡券', timestampToleranceSeconds: 600, cardsEncoding: 'base64json' } } ``` 建议环境变量: - `KAQUAN91_USER_ID` - `KAQUAN91_SECRET` - `KAQUAN91_VERSION` - `KAQUAN91_SHOP_ID` - `KAQUAN91_SHOP_NAME` - `KAQUAN91_TIMESTAMP_TOLERANCE_SECONDS` - `KAQUAN91_CARDS_ENCODING` ## 6. 签名设计 签名规则沿用文档约定: 1. 仅使用请求 JSON body 中实际传入的参数参与签名; 2. 除 `sign` 外,所有 body 参数按 ASCII 升序排序; 3. 拼接为 QueryString; 4. 前后加商户密钥; 5. 计算大写 MD5; 6. 已传入的空值参数参与签名; 7. 不额外加入 `userId`、商户号、内部配置项或未传入的可选字段。 需要实现: - 生成签名原串; - 验签; - 校验 `timestamp` 是否超出容忍窗口。 ## 7. 下单设计 ### 7.1 输入映射 `91` 下单请求映射为内部 source event: - `platformOrderId = orderNo` - `provider = '91kaquan'` - `platform = 'kuaishou'` - `shopId = runtimeConfig.platforms.ninetyone.shopId` - `shopName = runtimeConfig.platforms.ninetyone.shopName` - `payStatus = 'paid'` - `orderStatus = 'paid'` - `items = [{ externalSkuCode: productNo, externalItemId: productNo, externalSkuName: productNo, quantity: buyNum }]` ### 7.2 为什么直接标记 paid `91` 调用异步卡密下单接口时,业务上已经代表买家付款完成,当前项目需要立即进入履约任务创建。 ### 7.3 对现有链路的复用 调用现有: - `upsertOrderFromSource` - `resolveOrderItemForFulfillment` - `syncDeliveryTasksForOrder` 这样可直接复用现有: - 91 商品名到 cloudtentacles 商品名的匹配; - cloudtentacles 覆盖规则(套装、多数量发货); - 快手 Cloud task 创建; - claim token 生成。 ### 7.4 成功判定 下单接口成功判定标准: - 请求合法; - 订单已成功写入; - 商品已匹配时至少创建出 1 个 task; - 商品未匹配时进入待补全队列。 满足以上条件即返回: - `orderStatus = 10` 不在下单接口返回 `20`。 ## 8. 查询设计 ### 8.1 查询目标 查询接口的职责不是看订单是否最终兑换完成,而是判断是否已经具备“可自动发货给买家”的内容。 这里的可交付内容就是 `claimUrl`。 ### 8.2 claimUrl 判定 对订单下的每个 task: 1. 优先读取 `primary_claim_token`; 2. 若没有,则读取 `claim_token`; 3. 若仍没有,且是 `kuaishou_ct_assisted`,调用现有 `ensureTaskClaimLink(task)` 补生成; 4. 用 `buildClaimUrl(token)` 组装最终链接。 ### 8.3 查询状态判定 - `30`: - 订单不存在; - 没有匹配到任务; - task 进入失败/人工处理态且无法生成领取链接。 - `10`: - 订单存在; - 商品履约配置尚未补齐; - 任务已创建; - 但并非所有 task 都已准备好 `claimUrl`。 - `20`: - 所有 task 都已有可交付的领取链接。 ## 9. cards 设计 ### 9.1 card 字段映射 每个 task 映射为一个 card: - `cardNo = claimUrl` - `cardPwd = ''` - `expireTime = claim_expires_at` - `jumpLink = claimUrl` ### 9.2 编码方案 按 [卡密加密说明.md](./卡密加密说明.md) 实现: - 算法:`AES` - 模式:`ECB` - 填充:`PKCS7Padding` - 数据块:`128 位` - 输出:`Base64` - 密钥:开放平台 `AppSecret`,长度固定 `32` 个字符 Node 侧实现等价为: - `aes-256-ecb` - 开启自动填充 - 明文为 `JSON.stringify(cards)` - 输出 Base64 字符串 编码层仍然保持独立模块,后续如果联调方要求兼容别的编码方式,只替换该模块即可。 ### 9.3 多数量处理 若 `buyNum = N`: - 内部创建 `N` 个 task; - 查询成功时返回 `N` 个 card; - 每个 card 对应一个独立领取链接。 ## 10. 商品匹配设计 当前项目的 `91` 下单流程直接使用 `productNo` 做履约匹配: - 默认把 `productNo` 当作商品名,匹配 cloudtentacles 商品列表里的同名商品; - 如果 `productNo` 使用 `商品名----店铺编号` 格式,前半段用于商品匹配,后半段作为快手小店 `shopId` 用于核销; - 特殊商品通过 cloudtentacles 覆盖规则处理,例如一个 91 商品发多个 cloudtentacles 商品,或某个商品发多次。 最终目标是让 91 卡券只传商品名,系统按商品名和覆盖规则自动创建快手 Cloud 履约任务。 ## 11. 日志与排障 建议所有 `91` 请求单独打日志,至少记录: - requestId - orderNo - productNo - query/create - 验签结果 - 命中 SKU - orderId - task 数量 - 最终返回的 `orderStatus` ## 12. 代码落点 建议新增: - `apps/backend/src/routes/open-91.js` - `apps/backend/src/services/open-91/shared.js` - `apps/backend/src/services/open-91/order-create-service.js` - `apps/backend/src/services/open-91/order-query-service.js` 需要修改: - `apps/backend/src/config/defaults.ts` - `apps/backend/src/config/runtime.js` - `apps/backend/src/types/runtime-config.js` - `apps/backend/src/index.js` ## 13. 测试范围 至少补这些单测: 1. 验签: - 正常签名通过; - 错误签名失败; - 空值参数参与签名。 2. 下单: - 业务请求映射为内部 source event; - 商品未配置时返回 `10`,并进入后台待补全队列。 3. 查询: - 全部 task 都有链接时返回 `20`; - 部分 task 缺链接时返回 `10`; - 无订单时返回 `30`。 4. cards: - 单卡编码正确; - 多卡数量与 task 数量一致。 ## 14. 当前阶段不做的事 - 不实现 `callbackUrl` 主动回调; - 不在 `91` 查询成功后回写快手侧最终兑换结果; - 不实现除 `aes-256-ecb-base64` 之外的卡密加密算法; - 不新增独立顶级菜单,后台入口复用“平台配置 -> 91卡券接入”。