16 KiB
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_dashexecutor,复用一个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 拼串不同,勿混用):
api_key=<app_key>&body_sha256=<sha256hex(body)>&method=<GET|POST>&nonce=<nonce>&path=<仅路径>×tamp=<unix秒>
规则:
- 参数按 ASCII 字典序排序(固定为上面顺序);
- value 原样拼接不做 URL encode;GET 请求 body 为空字符串(
body_sha256为空串的 sha256); X-Sign = hex(HMAC-SHA256(secret, 签名字符串)),小写。
TypeScript 参考:
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
content = body_sha256=<sha256hex(原始body)>×tamp=<X-Timestamp>
X-Sign = hex(HMAC-SHA256(回调secret, content))
校验顺序:X-Timestamp 在 ±300 秒内 → 按 X-Event-ID 幂等去重 → 重算签名比对(timingSafeEqual)→ 处理业务。校验失败返回 4xx,成功尽快返回 2xx(affiliate_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)
91 卡券下单
└→ order_site 建内部订单 + 履约 task(executor_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-result(2~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 状态
→ delivered:task → redeemed/completed,触发快手电子凭证核销
→ ship_failed:task → 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(统一结构):
{
"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 处理要求:
- 验签(见 3.2);
- 按
X-Event-ID/event_id幂等(DB 唯一键或去重表); order_status→ task 状态迁移(见下);- 尽快返回 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:
{
"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)