Files
order_site/docs/91卡券/6.开发设计.md
T
yml2213 7bacb7d5eb docs: 重构项目文档并补充三块载体、成本中心与人工支出方案
合并本轮全部文档改动(原 5 个 docs 提交压成 1 个)。

口径与索引:
- 新增 docs/README.md 文档总索引:三来源口径(91 + 快手 send-code = kuaishou-91)、
  「一个订单 = 骨架 + 买家凭证 + 一个执行载体」、成本只改一处、文档状态机
- 过期阶段文档移入 docs/archive/2026-10-08/(保留内容,不删除)

分析报告:
- docs/项目全量分析报告-2026-10-17.md:架构、五条业务链路、P0/P1/P2 风险与改进路线
- docs/载体判定与成本中心落地-复核与草案-2026-10-17.md:生产只读克隆量化、上一轮结论复核与 4 处修正

方案文档:
- docs/成本中心与三块履约模型.md:三块执行载体、标准商品、成本版本与快照、人工支出登记
- docs/财务系统集成方案.md:复式记账模型、分录表、分期落地
- docs/影子总账说明.md:影子期与转正期的边界、对账与转正标准

SQL 草案(docs/drafts/,未接入迁移):
- 载体判定与人工块量化.sql:量化查询、carrier_type 判定与回填、守恒校验
- 成本中心与销售事实-DDL草案.sql:product_catalog / channel_product_mappings /
  cost_items / cost_versions / cost_snapshots / cost_exceptions / sales_daily_facts
- 人工支出登记-DDL草案.sql:manual_fulfillment_cases / manual_expense_records / manual_expense_events

业务口径确认(2026-10-17):
- 收入 = 用户实付(orders.total_amount),与快手 paymentFen 数据核验 100% 一致
- 打手报酬 = 支出/成本(最终提现打款),按总额法
- 人工块以「支出可追溯」为先,SLA 监管延后
- 退款/仲裁暂未接入,先预留字段与反向冲正关系

三份草案已在一次性沙箱库按依赖顺序零错误应用(10 张新表、27 个 CHECK 约束)。
2026-10-17 22:03:03 +08:00

9.8 KiB
Raw Blame History

91卡券接入开发设计

1. 目标

来源口径(2026-10-17):91卡券 是 kuaishou-91 来源的订单骨架通道;快手 send-code 是同一来源的数据补全与兜底通道。二者共享匹配上下文与店铺键空间,不能拆成两个来源或两个适配器。

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

目标能力:

  1. 91卡券 调用我方异步下单接口;
  2. 我方将订单作为独立接入平台订单写入当前项目;
  3. 已配置商品自动走现有 affiliate-dash 履约链路;
  4. 未配置商品进入后台待补全队列,可手动补齐履约信息后重试;
  5. 我方生成领取链接 https://你的域名/#/claim/{token};
  6. 91卡券 调用查询订单接口;
  7. 我方在 cards 中返回领取链接,由 91卡券 自动发货给买家。

2. 设计原则

  • 91卡券 是 kuaishou-91 来源的订单骨架通道,不是新的履约执行器;快手 send-code 是同来源的补全/兜底通道。
  • 外部售卖、订单输入和自动发货通道仍由 91 承担;匹配层的来源键统一为 kuaishou-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: 'aes-256-ecb-base64'
  }
}

建议环境变量:

  • 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 商品名到 affiliate-dash sku 的匹配(resolveAffiliateDashSkuByProductNo,含 sku 透传兜底);
  • affiliate-dash 履约 task 创建;
  • 按数量拆 task(delivery-task-service.ts 按 unit_index 为每个数量各建一个 task);
  • claim token / 领取链接生成。

7.4 成功判定

下单接口成功判定标准:

  • 请求合法;
  • 订单已成功写入;
  • 商品已匹配时至少创建出 1 个 task;
  • 商品未匹配时进入待补全队列。

满足以上条件即返回:

  • orderStatus = 10

不在下单接口返回 20。

8. 查询设计

8.1 查询目标

查询接口的职责不是看订单是否最终兑换完成,而是判断是否已经具备“可自动发货给买家”的内容。

这里的可交付内容就是 claimUrl。

8.2 claimUrl 判定

对订单下的每个 task,调用 resolveTaskDeliveryLink(task)(apps/backend/src/services/fulfillment/delivery-link-service.ts),由该 task 对应的履约执行器解析可交付链接:

  1. 读取 primary_claim_token,为空则回退 claim_token;
  2. 同时读取 primary_claim_expires_at / claim_expires_at 作为过期时间;
  3. 有 token 才返回 claimUrl = buildClaimUrl(token),否则返回 null(视为暂不可交付)。

说明:kuaishou_ct_assisted(乐玩)/ kuaishou-feifei 通道已下线,其历史 task 由只读 stub 处理,不再生成领取链接;领取链接现由 affiliate-dash 执行器的 resolveDeliveryLink 提供。

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 当作商品名(中文),经 affiliate-dash 的 SKU 映射表(apps/backend/src/services/platforms/affiliate-dash/sku-mapping.ts)转成 affiliate-dash 商品的英文 sku;
  • 若映射表未命中,再用 productNo 与 affiliate-dash 商品列表的 sku 做透传兜底匹配(matchAffiliateDashSkuPassthrough);
  • 如果 productNo 使用 商品名----店铺编号 格式,前半段作为商品名匹配、后半段作为快手小店 shopId(parseOpen91ProductNo);
  • 匹配成功后由履约路由选中 affiliate-dash 执行器,多数量在 delivery-task-service.ts 里按数量拆成多个 task。

最终目标是让 91 卡券只传商品名,系统按 SKU 映射与透传兜底自动创建 affiliate-dash 履约任务。

11. 日志与排障

建议所有 91 请求单独打日志,至少记录:

  • requestId
  • orderNo
  • productNo
  • query/create
  • 验签结果
  • 命中 SKU
  • orderId
  • task 数量
  • 最终返回的 orderStatus

12. 代码落点

建议新增:

  • apps/backend/src/routes/open-91.ts(挂载于 apps/backend/src/app.ts:103,前缀 /api/v1/open/91)
  • apps/backend/src/services/open-91/config.ts
  • apps/backend/src/services/open-91/payload.ts
  • apps/backend/src/services/open-91/crypto.ts
  • apps/backend/src/services/open-91/response.ts
  • apps/backend/src/services/open-91/order-create-service.ts
  • apps/backend/src/services/open-91/order-query-service.ts
  • apps/backend/src/services/open-91/consume-on-deliver.ts

需要修改:

  • apps/backend/src/config/defaults.ts
  • apps/backend/src/config/runtime.ts
  • apps/backend/src/config/env-overrides.ts
  • apps/backend/src/types/runtime-config.ts
  • apps/backend/src/services/platforms/ninetyone/order-service.ts
  • apps/backend/src/services/fulfillment/product-resolution-service.ts
  • apps/backend/src/index.ts

13. 测试范围

至少补这些单测:

  1. 验签:
    • 正常签名通过;
    • 错误签名失败;
    • 空值参数参与签名。
  2. 下单:
    • 业务请求映射为内部 source event;
    • 商品未配置时返回 10,并进入后台待补全队列。
  3. 查询:
    • 全部 task 都有链接时返回 20;
    • 部分 task 缺链接时返回 10;
    • 无订单时返回 30。
  4. cards:
    • 单卡编码正确;
    • 多卡数量与 task 数量一致。

14. 当前阶段不做的事

  • 不实现 callbackUrl 主动回调;
  • 不在 91 查询成功后回写快手侧最终兑换结果;
  • 不实现除 aes-256-ecb-base64 之外的卡密加密算法;
  • 不新增独立顶级菜单;91 待补全订单目前只有后端管理接口(/api/v1/admin/platform-config/ninetyone/orders 查询、.../:id/retry 重试、.../:id/fail 标记失败),前端暂无专属页面。