Files
affiliate_dash/docs/order_site-affiliate-dash对接.md
2026-08-11 16:23:55 +08:00

344 lines
16 KiB
Markdown
Raw Permalink 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.
# affiliate_dash 商户接入对接文档(方案 B:统一领取页深度集成)
> 文档对象:order_site 后端 / 前端对接开发
> 版本:v1(评审稿)
> 对接方向:order_site 作为 **affiliate_dash 的一个商户**,走商户开放接口 `/api/client/v1`,由 affiliate_dash 完成皮肤/道具履约,order_site 保持自己的统一领取页并自建发货交互。
---
## 0. 一句话结论
order_site 在 affiliate_dash 开通一个商户账号 + API 客户端,下单时调用 `POST /api/client/v1/orders` 幂等建单扣款,领取页通过 `delivery` 系列接口(查询 → 绑定 → 轮询 → 提交)在**本站页面内**完成发货交互,最终由 affiliate_dash 回调 `order.shipping.updated` 驱动 order_site 任务状态与快手电子凭证核销闭环。
---
## 1. 角色与职责边界
| 方 | 承担 | 不承担 |
| --- | --- | --- |
| **order_site** | 91 进单、商品匹配、履约任务、统一领取页、玩家信息收集、回调接收、电子凭证核销、人工兜底 | 不直接对接皮肤源头;不管理 affiliate_dash 钱包 |
| **affiliate_dash** | 商户建单扣款、发货数据查询、绑定、对源头履约、回调推送 | 不接触 91 卡券,不生成 order_site 的 claimUrl |
> 对接方式与现有 `kuaishou-feifei` 执行器同构:order_site 新增一个 `affiliate_dash` executor,复用一个 `preparePaidTask` + `resolveDeliveryLink`,对外仍然只返回本站统一领取链接。
---
## 2. 对接前准备(affiliate_dash 侧,管理员手动完成)
| 项 | 说明 |
| --- | --- |
| 商户账号 | affiliate_dash 后台新建商户(如 `order_site`),状态 active |
| API 客户端 | 商户后台「API 密钥」新建,scopes 至少含 `products:read``orders:read``orders:write``shipping:read``wallet:read`;保存 `app_key` / `secret`(secret 仅创建时展示一次) |
| 回调配置 | 商户后台「回调」配置接收 URL(指向 order_site 回调路由),订阅事件 `order.created``order.shipping.updated``order.cancelled`,保存回调 secret |
| 钱包充值 | affiliate_dash 无真实支付,**下单即扣商户钱包积分**;上线前需充值足够积分 |
| 环境 | 记录 `BASE_URL`(如 `https://affiliate.example.com`),确认网络可达;时间偏差容差 ±300 秒,服务器需 NTP 同步 |
---
## 3. 鉴权与签名
### 3.1 请求签名(order_site → affiliate_dash `/api/client/v1`
Header
| Header | 必填 | 说明 |
| --- | --- | --- |
| `X-App-Key` | 是 | 商户 API 客户端 app_key |
| `X-Timestamp` | 是 | Unix 秒时间戳 |
| `X-Nonce` | 是 | 随机串 8~96 位;同 Key 有效期内不可重复(服务端持久化防重放) |
| `X-Sign` | 是 | 见下 |
签名字符串(**body 先 sha256**,与源头侧 `/api/open/v1` 的原始 body 拼串不同,勿混用):
```text
api_key=<app_key>&body_sha256=<sha256hex(body)>&method=<GET|POST>&nonce=<nonce>&path=<仅路径>&timestamp=<unix秒>
```
规则:
1. 参数按 ASCII 字典序排序(固定为上面顺序);
2. value 原样拼接不做 URL encodeGET 请求 body 为空字符串(`body_sha256` 为空串的 sha256);
3. `X-Sign = hex(HMAC-SHA256(secret, 签名字符串))`,小写。
TypeScript 参考:
```ts
import { createHmac, createHash } from 'node:crypto'
function sha256Hex(input: string): string {
return createHash('sha256').update(input, 'utf8').digest('hex')
}
function hmacSha256Hex(secret: string, content: string): string {
return createHmac('sha256', secret).update(content, 'utf8').digest('hex')
}
export function buildClientSign(params: {
appKey: string
appSecret: string
method: string
path: string
body: string
timestamp: string
nonce: string
}): string {
const content = [
`api_key=${params.appKey}`,
`body_sha256=${sha256Hex(params.body)}`,
`method=${params.method.toUpperCase()}`,
`nonce=${params.nonce}`,
`path=${params.path}`,
`timestamp=${params.timestamp}`,
].sort().join('&')
return hmacSha256Hex(params.appSecret, content)
}
```
### 3.2 回调验签(affiliate_dash → order_site
Header`X-Event-ID``X-Timestamp``X-Sign`
```text
content = body_sha256=<sha256hex(原始body)>&timestamp=<X-Timestamp>
X-Sign = hex(HMAC-SHA256(回调secret, content))
```
校验顺序:`X-Timestamp` 在 ±300 秒内 → 按 `X-Event-ID` 幂等去重 → 重算签名比对(`timingSafeEqual`)→ 处理业务。校验失败返回 4xx,成功尽快返回 2xxaffiliate_dash 会指数退避重试最多 16 次)。
---
## 4. 商品映射
- 拉取:`GET /api/client/v1/products`(分页 `page`/`size`),关键字段:
| 字段 | 说明 |
| --- | --- |
| `list[].sku` | 下单时作为 `sku` 传入 |
| `list[].display_name` | 商户展示名 |
| `list[].price_amount` | 单价(积分) |
| `list[].stock` | 库存,`-1` 不限 |
| `list[].status` | `active` 可售 |
- order_site 侧在履约配置(`fulfillment_profiles.config_json`)维护映射:**91 productNo / order_site 商品 → affiliate_dash sku**。
- 建议:admin 配置页支持手动关联 + 定时同步(商品下架/缺货在领取前拦截,转人工)。
---
## 5. 完整时序(方案 B
```text
91 卡券下单
└→ order_site 建内部订单 + 履约 taskexecutor_key = affiliate_dash
└→ preparePaidTask
POST /api/client/v1/orders
{ client_order_no: <order_site 内部单号>, sku, quantity, buyer_reference, data: { 91单号, 预期游戏账号 } }
→ affiliate_dash 扣钱包积分,建单(order_no, order_status=paid
→ 异步回调 order.created
→ order_site 保存 order_no 到 task 上下文,task → link_generated
└→ resolveDeliveryLink:返回本站统一 claimUrl
91 把 claimUrl 发给用户
用户打开 order_site 领取页(executor_key=affiliate_dash 分支)
└→ 展示商品/预期账号
└→ 用户输入/确认游戏 UID → POST /orders/{order_no}/delivery/bind
→ 展示 bind_url / qr_url(或跳转绑定)
└→ 轮询 GET /orders/{order_no}/delivery/bind-result2~3 秒)
→ bound=true(含 mismatch 提示)→ 展示角色信息
└→ POST /orders/{order_no}/delivery/submit { game_account, bind_uuid }
→ affiliate_dash 进入 delivering,内部对源头发货
affiliate_dash 履约完成(delivered / ship_failed
└→ 回调 order.shipping.updated
→ order_site 验签 + 幂等 → 更新 task 状态
→ deliveredtask → redeemed/completed,触发快手电子凭证核销
→ ship_failedtask → retry_pending/manual_review,展示失败原因
```
---
## 6. 接口明细(order_site 实际使用的端点)
### 6.1 创建订单 `POST /api/client/v1/orders`scope `orders:write`
| body | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `client_order_no` | string | 是 | **幂等单号**,同一商户下唯一;建议用 order_site 内部单号 |
| `sku` | string | 是 | affiliate_dash 商品 sku |
| `quantity` | int | 否 | 默认 1 |
| `buyer_reference` | string | 否 | 买家标识/备注(可填 91 单号或用户标识) |
| `data` | object | 否 | 透传业务数据(≤2048B);约定字段 `game_account`/`game_channel`/`role_name` 会用于发货处理;回调原样带回 |
响应 `data.order``order_no``client_order_no``order_status`=paid)、`can_ship``cannot_ship_reason``amount``base_amount``service_fee_amount``currency` 等。
注意:
- 成功创建 HTTP **201**;命中幂等返回 **200**`idempotent=true` + 既有订单,**不重复扣款**
- **无真实支付,下单即扣钱包积分**,余额不足返回 400 —— order_site 必须处理该降级;
- 下单后以 `order_no` 为准进行后续查询/发货/回调匹配。
### 6.2 查询订单 `GET /api/client/v1/orders/{order_no}``orders:read`
返回订单当前状态、金额、发货链接有效期、失败原因等;用于 order_site 主动对账/兜底轮询。
### 6.3 获取发货链接 `GET /api/client/v1/orders/{order_no}/delivery-link``orders:read`/`shipping:read`
返回 `delivery_url`(可直接打开的 affiliate_dash Web 发货页)。方案 B 下作为**备选**(如自建页临时不可用时直接跳转)。
### 6.4 查询发货数据 `GET /api/client/v1/orders/{order_no}/delivery``orders:read`/`shipping:read`
自建发货页的渲染数据:`status``can_ship``cannot_ship_reason``product``buyer_name``game_channel``game_uid``role_name``pay_score``data`(下单透传,用于预填玩家账号)、`good`(商品展示详情)。
### 6.5 发起绑定 `POST /api/client/v1/orders/{order_no}/delivery/bind``orders:write`
| body | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `game_account` | string | 是 | 玩家编号 / UID |
返回 `bind_uuid`(提交发货凭证)、`bind_url`(绑定跳转地址)、`qr_url`(绑定二维码)。同一订单重复绑定以最新凭证为准。
### 6.6 查询绑定结果 `GET /api/client/v1/orders/{order_no}/delivery/bind-result?bind_uuid=..``orders:read`/`shipping:read`
| 字段 | 说明 |
| --- | --- |
| `bound` | 是否完成绑定;未绑定 `false`,自建页每 2~3 秒轮询 |
| `game_account` / `role_name` / `game_channel` | 绑定成功后返回 |
| `expected_game_account` | 下单 `data.game_account` 透传的预期账号(有传才返回) |
| `mismatch` | 绑定账号与预期不一致时为 `true`**提交发货会被拒绝** |
### 6.7 提交发货 `POST /api/client/v1/orders/{order_no}/delivery/submit``orders:write`
| body | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `game_account` | string | 是 | 玩家编号 / UID |
| `bind_uuid` | string | 是 | 绑定凭证 |
返回 `order_no``status`=delivering)、`message``provider_order_no`。同一订单重复提交直接返回既有结果。**最终成败以回调为准**。
### 6.8 余额查询 `GET /api/client/v1/wallet``wallet:read`
可选:下单前预检余额,或在订单创建 400 时告警。返回 `available_balance``currency` 等。
---
## 7. 回调契约
| 事件 | 触发时机 |
| --- | --- |
| `order.created` | 商户下单成功(扣款完成,order_status=paid |
| `order.shipping.updated` | 发货状态变化(delivering / delivered / ship_failed |
| `order.cancelled` | 订单取消/退款 |
payload(统一结构):
```json
{
"event_id": "…",
"event": "order.shipping.updated",
"data": {
"order_no": "FO20260730000123",
"client_order_no": "shop-10001",
"product_sku": "suit_pink_sheep",
"quantity": 1,
"base_amount": 100,
"service_fee_amount": 1,
"amount": 101,
"currency": "POINT",
"order_status": "delivered",
"can_ship": false,
"cannot_ship_reason": "",
"provider_order_no": "…",
"failure_reason": "",
"data": { "91单号": "…", "game_account": "4808146277" }
}
}
```
order_site 处理要求:
1. 验签(见 3.2);
2.`X-Event-ID` / `event_id` 幂等(DB 唯一键或去重表);
3. `order_status` → task 状态迁移(见下);
4. 尽快返回 2xx;失败重试可能重复投递(最多 16 次指数退避)。
---
## 8. 状态映射
| affiliate_dash `order_status` | order_site task 状态 / 动作 |
| --- | --- |
| `paid` | 已建单待履约(task `link_generated`,领取页可开始发货) |
| `delivering` | 履约中(task `redeeming`,领取页展示处理中) |
| `delivered` | 履约成功(task → `redeemed` / `completed`;触发快手电子凭证核销) |
| `ship_failed` | 履约失败(task → `retry_pending`;可重新 `delivery/submit` 或转 `manual_review` |
| `cancelled` | 已取消/退款(task → `closed`,通知 91/用户) |
---
## 9. 错误与降级
| 场景 | affiliate_dash 返回 | order_site 处理 |
| --- | --- | --- |
| 钱包余额不足 | 创建订单 400 | task → `manual_review` + 告警「affiliate_dash 余额不足」,充值后重试 |
| 商品下架/缺货 | 创建订单 400 | 匹配阶段拦截,转人工 |
| 下单超时/网络失败 | 无响应 | 用同 `client_order_no` 幂等重试;仍失败转 `manual_dispatch` |
| 绑定账号不匹配 | `mismatch=true`,submit 被拒 | 领取页提示买家绑回预期账号或重新下单 |
| `ship_failed` | 回调 failure_reason | 领取页/后台展示原因,支持重试或转人工 |
---
## 10. 幂等与一致性设计
- **下单幂等**`client_order_no` = order_site 内部单号(如 task 关联单号),重复请求安全;
- **回调幂等**`X-Event-ID` 去重;
- **提交发货幂等**:重复 `delivery/submit` 返回既有结果;
- **对账兜底**order_site 定时用 `GET /orders/{order_no}` 对账(可选,回调为主);
- **时间**:双方服务器 NTP 同步,容差 ±300 秒。
---
## 11. order_site 配置清单(履约配置扩展)
`fulfillment_profiles` 新增 profile
```json
{
"profile_key": "affiliate_dash",
"name": "affiliate_dash 履约",
"executor_key": "affiliate_dash",
"requires_claim": true,
"auto_dispatch": false,
"config_json": {
"baseUrl": "https://affiliate.example.com",
"appKey": "ak_…",
"appSecret": "sk_…",
"callbackSecret": "…",
"skuMapping": { "91商品编码": "affiliate_dash sku" },
"fallbackExecutor": "manual_dispatch",
"precheckBalance": true
}
}
```
商品匹配规则(复用 `fulfillment-routing.ts`):91 productNo → 命中 affiliate_dash 映射 → `executor_key = affiliate_dash`;未命中走现有 kuaishou / manual 流程。
---
## 12. 对接计划(分阶段)
| 阶段 | 内容 | 交付物 | 验收点 |
| --- | --- | --- | --- |
| **0. affiliate_dash 侧准备**(手动) | 建商户、API 客户端、回调配置、充值 | 商户可登录、app_key/secret 可用 | 用 curl 调 `GET /products` 通 |
| **1. 平台客户端与签名** | `services/platforms/affiliate-dash/`:签名、请求封装、验签 | `client.ts``verify-callback.ts` | 单元测试:签名向量、验签通过/篡改拒绝 |
| **2. executor** | `affiliate-dash-executor.ts``preparePaidTask`(建单+存 order_no+ `resolveDeliveryLink`(统一 claimUrl);注册进 `registry.ts` | executor + 注册 | 测试订单:91 进单 → task 状态 `link_generated`affiliate_dash 出现订单 |
| **3. 履约配置与商品映射** | 新增 profile、admin 配置页(sku 映射/余额预检) | 配置页 + 迁移 | 配置页可保存,路由规则生效 |
| **4. 领取页(方案 B 自建)** | 前端按 executor 分发到 affiliate-dash 流程:`delivery` 查数据 → 输入 UID → `bind` → 轮询 `bind-result``submit`;异常分支(mismatch/失败重试) | 领取页分支 | 端到端:用户领取 → 绑定 → 提交 → affiliate_dash 显示 delivering |
| **5. 回调路由与状态同步** | `POST /webhooks/affiliate-dash`:验签 + 幂等 + task 状态迁移 + 核销触发 | 回调路由 | 发货完成回调 → task `redeemed` + 电子凭证核销闭环 |
| **6. 联调与上线** | 错误/降级演练、对账、监控告警 | 上线 checklist | 全链路绿灯,余额不足/绑定不匹配演练通过 |
---
## 13. 参考代码位置
- order_site executor 模式:`apps/backend/src/services/fulfillment/executors/kuaishou-feifei-executor.ts`(最接近,同为「建单 + 统一领取链接」)、`registry.ts`(注册)、`types.ts`(接口定义)
- order_site 平台对接样例:`apps/backend/src/services/platforms/kuaishou-feifei/`
- order_site 履约配置:`apps/backend/src/repositories/fulfillment-profile-repo.ts``apps/backend/src/routes/admin/platform-config/fulfillment-routing.ts`
- affiliate_dash 接口契约(在线文档/字段定义):`frontend/src/openapi/endpoints.ts`;鉴权实现:`backend/internal/middleware/open_auth.go`(商户侧)、`backend/internal/service/callback.go`(回调签名 `BuildCallbackSign`