Files
order_site/docs/91卡券/6.开发设计.md
T
2026-05-21 19:29:00 +08:00

321 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 91卡券接入开发设计
## 1. 目标
基于以下对外接口文档完成当前项目接入:
- [3.异步卡密下单.md](/Users/yml/codes/order-site-workspace/docs/91卡券/3.异步卡密下单.md:1)
- [4.查询订单接口.md](/Users/yml/codes/order-site-workspace/docs/91卡券/4.查询订单接口.md:1)
目标能力:
1. `91卡券` 调用我方异步下单接口;
2. 我方将订单作为独立接入平台订单写入当前项目;
3. 已配置商品自动走现有快手 Cloud 履约链路;
4. 未配置商品进入后台待补全队列,可手动补齐履约信息后重试;
5. 我方生成领取链接 `https://221329.cc.cd/#/claim/{token}`
6. `91卡券` 调用查询订单接口;
7. 我方在 `cards` 中返回领取链接,由 `91卡券` 自动发货给买家。
## 2. 设计原则
- `91卡券` 作为**新的订单来源**,不复用 `agiso`
- `91卡券` 不是新的履约执行器,而是外部售卖、订单输入和自动发货通道。
- 当前项目真正的交付物仍然是 `claimUrl`
- 查询接口返回 `20` 的判断标准是“领取链接已生成并可发”,不是“快手最终兑换完成”。
- `buyNum > 1` 时,必须拆成多个 task,并返回多个 card。
- 未命中履约配置时不丢单,先返回 `10` 并沉淀到后台待补全队列。
## 3. 来源建模
新增独立来源标识:
- `provider = '91kaquan'`
- `platform = 'kuaishou'`
- `shopId = '91kaquan'`
- `shopName = '91卡券'`
原因:
- 避免与 `agiso` webhook 语义混淆;
- 避免复用历史后台拉单来源;
- 便于后续单独做日志、排障和绑定配置。
## 4. 接口设计
### 4.1 路由
新增独立公开路由组:
- `POST /api/v1/open/91/orders/create`
- `POST /api/v1/open/91/orders/query`
不放进现有 `/api/v1/webhooks`,因为:
- `91` 不是 webhook 来源;
- `91` 需要自己的响应结构:`code/message/data`
- 与现有项目 `buildSuccessPayload(code=0)` 不兼容。
### 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'
}
}
```
建议环境变量:
- `NINETYONE_USER_ID`
- `NINETYONE_SECRET`
- `NINETYONE_VERSION`
- `NINETYONE_SHOP_ID`
- `NINETYONE_SHOP_NAME`
- `NINETYONE_TIMESTAMP_TOLERANCE_SECONDS`
- `NINETYONE_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`
这样可直接复用现有:
- 商品匹配;
- 履约绑定;
- 快手 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](/Users/yml/codes/order-site-workspace/docs/91卡券/卡密加密说明.md:1) 实现:
- 算法:`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. 商品匹配设计
当前项目的订单商品匹配依赖:
- `provider`
- `platform`
- `shopId`
- `externalSkuCode` / `externalItemId` / `externalSkuName`
因此接入 `91` 后,运营侧需要新增对应绑定规则,使:
- `provider = '91kaquan'`
- `platform = 'kuaishou'`
- `shopId = '91kaquan'`
- `externalSkuCode = productNo`
最终匹配到现有快手 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卡券接入”。