Files
order_site/docs/91卡券/6.开发设计.md
T
2026-05-27 14:22:23 +08:00

8.0 KiB
Raw Blame History

91卡券接入开发设计

1. 目标

基于以下对外接口文档完成当前项目接入:

目标能力:

  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 路由统一返回:

{
  "code": 200,
  "message": "接口调用成功",
  "data": {}
}

业务失败也返回 data.orderStatus = 30 的业务响应。

仅在真正的协议级错误下返回:

{
  "code": 400,
  "message": "验签失败",
  "data": null
}

5. 配置设计

新增运行时配置项:

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 实现:

  • 算法: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卡券接入”。