Files
order_site/docs/ affiliate_dash_发货平台/affiliate-dash对接.md
T
yml2213 316ee6d135 履约配置页 affiliate-dash 覆盖收尾:preferredMatchEnabled 开关补入平台配置 UI
- 分析结论:履约路由面板已含 affiliate_dash(开关/优先级/规则),职责划分成立
- 缺口修复:指定匹配优先开关此前无 UI 入口,现平台配置 affiliate-dash tab 增加 Switch
- 后端 effective 透出 preferredMatchEnabled;前端类型/panel 补齐
- 前后端 typecheck 通过,224 测试全绿
2026-08-05 17:52:14 +08:00

758 lines
51 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.
# 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 拼串不同,勿混用;参数名是 **`app_key`** 而非旧接口的 `api_key`,写错会 401):
```text
app_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 = [
`app_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 次,间隔为固定递增序列 `15s/15s/30s/3m/10m/20m/30m/30m/30m/1h/3h/3h/3h/6h/6h`,非严格指数退避)。
---
## 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_site 从 201 响应**同步**拿到 order_no,写入 task 上下文,task → link_generated
→ 异步回调 order.created 仅作对账/补发(幂等,不改变 task 状态)
└→ 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`
返回订单当前状态、金额、失败原因等(⚠️ 实际响应**不含**「发货链接有效期」字段,有效期仅在 delivery-link 接口返回);用于 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",
"occurred_at": "…", // 真实实现含此字段
"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/用户) |
> ⚠️ **状态机合法性**(对照 `apps/backend/src/domain/task-status.ts` 的 `TASK_TRANSITIONS`):
> - affiliate_dash 流程中 task 须经过 `link_generated → claimed → waiting_binding → redeeming` 才能进入履约态:`link_generated`/`claimed` **不能**直接跳 `redeeming`(不在转移表)。落地时:领取页打开置 `claimed`、提交绑定置 `waiting_binding`、`delivery/submit` 成功或收到 delivering 回调置 `redeeming`。
> - `redeeming → redeemed/completed/retry_pending/manual_review` 均合法;`redeeming → closed` 不在转移表(`updateTask` 实际不校验转移表,属软约束,仍建议先经 `manual_review` 再 `closed`)。
> - `retry_pending/manual_review → redeeming` 合法,支持 ship_failed 重试闭环。
---
## 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_no`(格式 `DT` + 12 hex,唯一且稳定,见 §14.2),重复请求安全;
- **回调幂等**`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`
---
## 14. 深度分析(契约验证结论 · v1 → v2)
> 本节基于对 affiliate_dash 实际代码(`backend/internal/middleware/open_auth.go`、`backend/internal/service/callback.go`、`backend/internal/handler/open_v1.go`、`backend/internal/service/delivery.go`、`frontend/src/openapi/endpoints.ts` 等)与 order_site 履约体系(`domain/task-status.ts`、`services/fulfillment/executors/*`、`services/claim/*`)的逐项核对。以下结论是阶段 1~6 实现的直接依据。
### 14.1 契约验证对照(文档 vs affiliate_dash 实际实现)
| # | 契约项 | 结论 |
|---| --- | --- |
| 1 | 签名串参数名:文档原写 `api_key=`,实现为 `app_key=`open_auth.go:194 `BuildOpenV1Sign`;前端在线文档 OpenApiDocs.tsx:425-449 与调试页 ApiDebugger.tsx:65 均用 `app_key=`) | ❌ 已在本版修正;按旧写法签名必然 401 |
| 2 | body 先 sha256、参数字典序、四头、nonce 持久化防重放(长度 8~96)、±300sOPEN_SIGN_SKEW=300)、scopesproducts:read/orders:read/orders:write/shipping:read/wallet:read | ✅ 一致 |
| 3 | 回调 `content=body_sha256=<..>&timestamp=<..>`、头 X-Event-ID/X-Timestamp/X-Sign、事件枚举 order.created/order.shipping.updated/order.cancelled | ✅ 一致 |
| 4 | 回调重试 16 次 | ⚠️ 次数一致(CALLBACK_MAX_ATTEMPTS=16),但为固定递增间隔序列而非严格指数退避(callback.go:30-46 |
| 5 | POST /orders 字段、新建 201 / 幂等 200 + idempotent=true、余额不足 400、order 响应字段(order_no/order_status/can_ship/cannot_ship_reason/amount/base_amount/service_fee_amount/currency + client_order_no/product/quantity/fee_type/buyer_reference/provider_order_no/failure_reason/data/result/created_at/delivered_at/cancelled_at | ✅ 一致 |
| 6 | GET /products 字段与分页 page/sizestatus 恒为 active);GET /orders/{order_no}delivery-link → delivery_url/expires_atdelivery → status/can_ship/cannot_ship_reason/product/buyer_name/game_channel/game_uid/role_name/pay_score/data/goodbind → bind_uuid/bind_url/qr_urlbind-result → bound/game_account/role_name/game_channel/expected_game_account/mismatchsubmit → order_no/status/message/provider_order_nowallet → available_balance/currency | ✅ 一致(唯一修正:查询订单响应不含「发货链接有效期」,见 §6.2) |
| 7 | 回调 payload 字段 + 顶层 `occurred_at`(真实存在,§7 已补) | ✅ 一致 |
| 8 | order_status 五枚举 paid/delivering/delivered/ship_failed/cancelledmismatch 提交被拒为 HTTP 400 | ✅ 一致(语义细节见下) |
**mismatch 语义细节**`bind-result.mismatch=true` 判定的是「绑定账号 ≠ 下单 `data.game_account`(预期账号)」(delivery.go:227-233);`submit` 的拒绝条件是「绑定账号 ≠ **请求体提交的** game_account」(delivery.go:354-357)。正常流程提交预期账号时两者等价;若页面让用户提交的是绑定账号本身,则 mismatch=true 时也能提交通过 —— 对账/风控时留意。
### 14.2 order_site 落地要点
- **client_order_no 用 `task_no`**`utils/random.ts` `randomId('DT', 6)``DT` + 12 hex,≤64 字符满足 affiliate_dash 限制)。order_site 自身无 client_order_no 字段,本地下单幂等由 `orders UNIQUE(provider, platform, shop_id, platform_order_id)` + `listTasks(order.id)` 已建不重建承担;affiliate_dash 侧幂等由 `client_order_no=task_no` 承担,preparePaidTask 失败重试可安全重放同一 task_no。
- **preparePaidTask 同步拿到 order_no**201 响应即含),写入 `context_json.affiliateDash.orderNo`;无需等回调。失败必须降级 `MANUAL_REVIEW` + `deps.notifyTaskAutoManualReview`(范式:kuaishou-feifei-executor.ts:36-51)。
- **状态迁移路径**:见 §8 注 —— 领取页打开 `link_generated → claimed`,绑定 `→ waiting_binding`submit 成功/回调 delivering `→ redeeming`delivered `→ redeemed`+核销),ship_failed `→ retry_pending`cancelled `→ manual_review → closed`
- **回调幂等**affiliate_dash 投递带 `X-Event-ID`order_site 现无独立去重表 —— 建议 migration `013_affiliate_dash.sql` 新建 `webhook_events(event_id UNIQUE)` 去重表;事件处理用**状态合并式更新**(仿 `syncKuaishouFeifeiTaskStatus`,重复通知可重入)。接收方注意 `app.ts:31-38``express.json` 已保存 `req.rawBody`(验签必需),挂载回调路由时确认 rawBody 可用。
- **核销联动**`delivered` 回调 → 复用 `consumeKuaishouIndustryVouchersForTask``platforms/kuaishou-industry/voucher-service.ts:143`),成功 → `redeemed/completed`,失败 → `manual_review`
- **领取页分发**`flowType` 扩展 `'affiliate_dash'``buildClaimDetailPayload``services/claim/kuaishou-cloud-claim-context.ts:89-92`)按 executor_key 增加分支,返回 affiliate_dash 专用 payload(商品/预期账号/当前状态/二维码相关);前端 `ClaimPage.tsx:337-373` 增加渲染分支。
### 14.3 改动文件清单
**后端(16 项)**
| # | 文件 | 改动 |
|---|------|------|
| 1 | `services/fulfillment/executors/types.ts` | `FULFILLMENT_EXECUTOR_KEYS``AFFILIATE_DASH`;按需加 `isAffiliateDashExecutor` 守卫 |
| 2 | `services/fulfillment/executors/affiliate-dash-executor.ts`(新) | 仿 kuaishou-feifei-executor.ts`{ key, preparePaidTask, resolveDeliveryLink }` |
| 3 | `services/fulfillment/executors/registry.ts` | `EXECUTORS` Map 注册 |
| 4 | `services/fulfillment/affiliate-dash/index.ts`(新) | 业务实现:prepare / sync 状态合并 + flow normalize |
| 5 | `services/platforms/affiliate-dash/`(新) | `config.ts` + `http-client.ts`(签名 §3.1 + 请求封装 + 超时)+ `order-service.ts` + `notify-service.ts`(验签,`timingSafeEqual` 模式) |
| 6 | `services/fulfillment/routing-config-service.ts` | `ROUTABLE_EXECUTOR_KEYS` / `DEFAULT_EXECUTOR_PRIORITY` 加入 affiliate_dash |
| 7 | `services/fulfillment/product-resolution-service.ts` | candidates 加 affiliate_dash 匹配项 + item_snapshot 上下文 |
| 8 | `services/bootstrap/fulfillment-bootstrap-service.ts` | `CORE_PROFILES` 加 affiliate_dash profile |
| 9 | `services/fulfillment/planner.ts` | 动态 profile 解析 + `buildFulfillmentTaskContext``affiliateDash` 块 |
| 10 | `services/claim/kuaishou-cloud-claim-context.ts` | 详情 payload 加 affiliate_dash 分支 |
| 11 | `services/claim/kuaishou-cloud-claim-service.ts` | executor_key 分发加分支 |
| 12 | `repositories/task-repo.ts` | 仿 `findKuaishouFeifeiTaskByOrder` 加按 `context_json #>> '{affiliateDash,orderNo}'` 查任务 |
| 13 | `routes/affiliate-dash.ts`(新)+ `app.ts` | webhook 路由,挂载 `/api/v1/open/affiliate-dash` |
| 14 | `db/migrations/013_affiliate_dash.sql`(新) | `webhook_events` 去重表(可选:affiliate 流水表) |
| 15 | `routes/admin/platform-config/` + `services/admin/platform-config/` | 平台配置读写(appKey/secret 等) |
| 16 | `services/admin/write/` | admin 手动重试/转人工按 executor 分发分支 |
**前端(6 项)**
| # | 文件 | 改动 |
|---|------|------|
| 17 | `types/claim.ts` | `ClaimAffiliateDashFlowInfo` + flowType 扩展 |
| 18 | `pages/claim/ClaimPage.tsx` + `claim-snapshot.ts` | `isAffiliateDashFlow` 判定与渲染分支 |
| 19 | `pages/claim/ClaimAffiliateDashSteps.tsx`(新) | 领取步骤(查 delivery → 输 UID → bind → 轮询 bind-result → submit |
| 20 | `services/claim.ts` | claim 侧操作 API |
| 21 | `pages/admin/AdminTasksPage.tsx` 等 | executor label/color 显示 |
| 22 | `domain/task-status.ts` | 如需新增状态/转移同步(按 §8 注已有路径则无需) |
### 14.4 剩余风险与待确认决策点
| 项 | 说明 |
| --- | --- |
| 自建页 vs delivery_url 兜底 | 方案 B 以自建页为主,`delivery-link` 作降级跳转;需确认前端是否允许 iframe 内嵌 affiliate_dash H5(扫码场景无碍,跳转场景有跨域限制) |
| 91 拆单 | 一个 91 单拆多 unit → 每 task 独立 `client_order_no`=task_no),互不影响 |
| 对账任务 | 回调为主 + 可选定时 `GET /orders/{order_no}` 对账(回调丢失兜底) |
| admin 手动重试 | ship_failed / manual_review 的手动「重新 submit」入口按 executor 分发(文件清单 #16 |
| 回调订阅确认 | 阶段 0 配置回调时确认 affiliate_dash 后台事件订阅粒度(order.created 是否必须订阅,或仅 shipping.updated 即可) |
---
## 15. 阶段 1 落地记录(v2.1 · 已完成)
产出文件(`apps/backend/src/`):
| 文件 | 内容 |
| --- | --- |
| `services/platforms/affiliate-dash/config.ts` | `get/assertAffiliateDashConfig`runtime + saved 合并);常量 `AFFILIATE_DASH_EXECUTOR_KEY='affiliate_dash'``AFFILIATE_DASH_WEBHOOK_PATH='/api/v1/open/affiliate-dash'` |
| `services/platforms/affiliate-dash/source-config-service.ts` | saved config 读写(`APP_CONFIG_KEYS.affiliateDash`,密钥存 DB 不进 git;含 `skuMapping` 归一) |
| `services/platforms/affiliate-dash/sign.ts` | `buildClientSign`(§3.1,参数名 `app_key=`+ `buildCallbackSign`/`verifyCallbackSign`(§3.2`timingSafeEqual`+ `createClientSignHeaders` |
| `services/platforms/affiliate-dash/http-client.ts` | `affiliateDashRequest`GET/POST、四头自动签名、AbortController 超时 502/504、错误归一、`logExternalHttpPacket` 全链路日志) |
| `services/platforms/affiliate-dash/verify-callback.ts` | `verifyAffiliateDashCallback`(时间容差 ±300s → 验签 → 事件解析;X-Event-ID 幂等留待阶段 5 |
| `services/platforms/affiliate-dash/order-service.ts` | 建单/查询/delivery-link/delivery/bind/bind-result/submit/wallet 全端点 + 字段映射 |
| `services/platforms/affiliate-dash/product-service.ts` | `listAffiliateDashProducts` + `listAllAffiliateDashProducts`(翻页拉全) |
| 配置接入 | `runtime-config.ts``defaults.ts``env-overrides.ts``AFFILIATE_DASH_*`)、`app-config-keys.ts` |
| 测试 | `sign.test.ts`(黄金向量)、`verify-callback.test.ts`(通过/篡改/超容差/缺头)、`http-client.test.ts`;12/12 通过,全量 211 通过,`tsc --noEmit` 通过 |
**真实联调验收**`listAffiliateDashProducts` 直连线上 `https://skin.khhao.com` → total=33 商品,字段映射正确,签名链路与 affiliate_dash `BuildOpenV1Sign` 一致。
**联调中发现并修复**:签名 `path` 必须为**纯路径**(不含 query string);首次实现把 `/products?page=1&size=5` 整串参与签名导致线上 401「签名校验失败」,已改为 `URL.pathname` 参与签名(§3.1 表头 `path=<仅路径>` 属实)。
---
## 16. 阶段 2 落地记录(v2.2 · 已完成)
产出文件(`apps/backend/src/services/fulfillment/`):
| 文件 | 内容 |
| --- | --- |
| `executors/types.ts` | `FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH='affiliate_dash'` + `isAffiliateDashExecutor` 守卫 |
| `executors/affiliate-dash-executor.ts`(新) | `{ key, preparePaidTask, resolveDeliveryLink }` 仿 feifei:补 claim token → `prepareAffiliateDashTask`;失败降级 `MANUAL_REVIEW` + `notifyTaskAutoManualReview``resolveDeliveryLink` 返回本站统一 claimUrl |
| `executors/registry.ts` | `EXECUTORS` Map 注册 affiliateDashExecutor |
| `affiliate-dash/index.ts`(新) | `isAffiliateDashTask``normalizeAffiliateDashFlow`context_json `affiliateDash` 块)、`prepareAffiliateDashTask`(幂等建单、`client_order_no=task_no``data` 透传 91单号/game_account、写 `LINK_GENERATED` + task event)、`syncAffiliateDashTaskStatus`(状态合并:delivering→redeeming、delivered→核销→redeemed/manual_review、ship_failed→retry_pending、cancelled→closed)、`buildAffiliateDashClientOrderNo` |
| 测试 | `registry.test.ts` 新增 affiliate_dash 映射 + 动作暴露断言(4→4 项) |
**真实建单联调(线上 skin.khhao.com**
| 步骤 | 结果 |
| --- | --- |
| 建单 `lucky_coin_x2`20 积分) | `FO20260805145602108a92eac8d``paid``can_ship=true` ✅ |
| 同 client_order_no 幂等重试 | 同单号 + `idempotent=true`,不重复扣款 ✅ |
| GET /orders/{order_no} 查询 | `paid`,金额/状态正确 ✅ |
| GET /orders/{order_no}/delivery | `data` 透传(91单号 + game_account)正确 ✅ |
**联调中发现并修复**`POST /orders` 响应是 `data.order`(非 `data` 直接),首次实现 map 错了层级导致 orderNo 全空;已改为取 `data.order` 并暴露 `idempotent` 标志。幂等校验要求**同单号参数完全一致**,否则 400「参数与原订单不一致」——重试时必须复用原参数。
---
## 17. 阶段 3 落地记录(v2.3 · 已完成)
产出文件(`apps/backend/src/` + `apps/frontend/src/`):
| 文件 | 内容 |
| --- | --- |
| `services/bootstrap/fulfillment-bootstrap-service.ts` | `CORE_PROFILES` 追加 affiliate_dash profilerequiresClaim: true, external_platform |
| `services/fulfillment/routing-config-service.ts` | `ROUTABLE_EXECUTOR_KEYS` + `DEFAULT_EXECUTOR_PRIORITY``AFFILIATE_DASH`(路由可命中) |
| `services/fulfillment/planner.ts` | `buildFulfillmentTaskContext``affiliateDash` 块(flowType/sku/productName/orderNo/... 与 `AffiliateDashFlow` 对齐);`resolveDynamicFulfillmentProfile` 加 AFFILIATE_DASH 分支 + 新增 `resolveDynamicAffiliateDashProfile`configId `affiliate_dash:${sku}` |
| `services/fulfillment/product-resolution-service.ts` | `resolveConfiguredItemCandidate` 91 分支加 affiliate_dash 候选(`resolveAffiliateDashSkuByProductNo`key=snapshot.productNo+ resolvedSkuCode/resolvedSkuName/snapshot.affiliateDash/matchMode |
| `services/fulfillment/affiliate-dash/index.ts` | `prepareAffiliateDashTask` 下单前 `preflightAffiliateDashWallet`(余额 ≤0 → `affiliate_dash_wallet_not_enough` 提前拦截;查询失败不阻塞) |
| `config/env-overrides.ts` | `AFFILIATE_DASH_SKU_MAPPING_JSON` env 注入(仿 KUAISHOU_FEIFEI_PRODUCT_RULES_JSON |
| `routes/admin/platform-config/affiliate-dash.ts`(新)+ `platform-config.ts` | GET/POST 配置、POST matchproductNo→sku)、POST products、POST wallet |
| `services/admin/platform-config/affiliate-dash-service.ts`(新) | get/update/match/listProducts/walletaudit 不落密钥 |
| 前端 `types/admin/.../affiliate-dash.ts` + `services/admin/platform-config/affiliate-dash.ts` + 两处 index export | 类型与 API 封装 |
| 前端 `AdminPlatformFulfillmentPage.tsx` | `ROUTING_EXECUTORS` 加 affiliate_dashgold+ Tabs 面板 `AffiliateDashFulfillmentPanel`(配置表单/密钥、skuMapping 行编辑、映射测试、商品目录、钱包余额) |
**联调验证(模拟 91 进单 → resolveOrderItemForFulfillment**
- `productNo='X91-LUCKY-90'` + skuMapping 命中 → `selectedExecutorKey='affiliate_dash'``matchMode='affiliate_dash_sku_mapping'``resolvedSkuCode='lucky_coin_x90'`、snapshot.affiliateDash 完整 ✅
- 未命中映射的商品 → 不选 affiliate_dash,回退 ✅
**验证**:前后端 typecheck 通过;后端全量 212 测试通过。
---
## 18. 阶段 4 落地记录(v2.4 · 已完成)
产出文件:
**后端(`apps/backend/src/`**
| 文件 | 内容 |
| --- | --- |
| `services/claim/kuaishou-cloud-claim-context.ts` | `buildClaimDetailPayload``affiliate_dash` 分支 + `buildAffiliateDashClaimDetailPayload`flowType/affiliateDash 块/product/task/order/orderItem/result |
| `services/claim/kuaishou-cloud-claim-service.ts` | `getKuaishouCloudClaimDetail` affiliate_dash 分支:`syncAffiliateDashTaskStatus` + `refreshAffiliateDashBindState`(有 bindUuid 时拉 bind-result 实时刷新 bound/账号/mismatch,可重入);`submitClaimUid` affiliate_dash 分支 → `bindAffiliateDashClaimForTask`bind 拿 bindUuid/二维码,task → WAITING_BINDING);新增 `submitAffiliateDashClaim`submit → REDEEMING + task event |
| `services/fulfillment/affiliate-dash/index.ts` | `AffiliateDashFlow``bound/boundAccount/gameChannel` 字段(bind-result 状态) |
| `routes/claims.ts` | 新增 `POST /:token/affiliate-dash/submit`claimWriteRateLimit |
**前端(`apps/frontend/src/`**
| 文件 | 内容 |
| --- | --- |
| `types/claim.ts` | `ClaimAffiliateDashFlowInfo` + `ClaimDetailData.affiliateDash` |
| `services/claim.ts` | `submitAffiliateDashClaim(token, {gameAccount, bindUuid})` |
| `pages/claim/claim-snapshot.ts` | `isAffiliateDashFlow`、hasRedeemResult/currentStep/progressText/resultTitle/resultDescription 的 affiliate_dash 分支(step: 无UID=1/绑定中=2/已绑定待提交=3/结果=4) |
| `pages/claim/claim-poll.ts` | affiliate_dash 轮询(未绑定用 BINDING_PREPARE_POLL_MS,已绑定用 FEIFEI_POLL_MS |
| `pages/claim/ClaimAffiliateDashSteps.tsx`(新) | `AffiliateDashClaimPanel`(二维码/绑定链接、bound 状态、mismatch 警告、提交发货按钮)+ `AffiliateDashResultStep` |
| `pages/claim/ClaimPage.tsx` | 分发分支 + `handleSubmitAffiliateDash` + `openBindUrl`/二维码生成支持 affiliateDash.bindUrl |
**验证**:前后端 typecheck 通过;后端全量 212 测试通过;claim 详情分流脚本验证(flowType=affiliate_dash、affiliateDash 块完整、product 正确、其他 flow 为 null)。
**领取闭环状态迁移(与 §8 状态机一致)**:输 UID → submitClaimUidbind 触发,task `link_generated → waiting_binding`)→ 轮询详情(bind-result 刷新 bound)→ 提交发货(submitAffiliateDashClaimtask `waiting_binding → redeeming`)→ 回调 delivered → redeemed + 核销。
---
## 19. 核销时机优化(v2.4.1 · 已完成)
**需求**:核销时机从「delivered 回调」提前到「给用户链接(91 卡信息到手、建单成功)」——参考 kuaishou-feifei 的「给用户链接即核销」模式。
**改动**`apps/backend/src/services/fulfillment/affiliate-dash/index.ts`):
| 位置 | 行为 |
| --- | --- |
| `prepareAffiliateDashTask` | 建单成功(orderNo 拿到、task → link_generated、91 侧即可取 claimUrl)后**立即**调用 `consumeKuaishouIndustryVouchersForTask`:有凭证 → 核销并写回 `kuaishouIndustryVoucher` 上下文(`consumeStatus=success`);无凭证 → `not_required`;**核销失败不阻塞**下单与链接发放(`consumeStatus=failed` + warn 日志,delivered 兜底) |
| `syncAffiliateDashTaskStatus`delivered 分支) | `alreadyConsumed` 短路:`consumeStatus ∈ {success, not_required}` 时不再重复调核销,直接 redeemed;`failed/空` 才补核销(原逻辑保留为兜底) |
**效果**:核销由「发货完成后(分钟~小时级)」提前到「建单成功(秒级)」;91 卡信息一经消耗即核销,delivered 回调只做状态收敛。已核销成功的凭证在 delivered 时 `resolveTaskVouchers` 返回空,自然走 redeemed。
**验证**typecheck 通过;后端全量 212 测试通过。
---
## 20. 阶段 5 落地记录(v2.5 · 已完成)
**回调路由打通**:挂载 `POST /api/v1/open/affiliate-dash`(与线上回调 URL `https://ks.khhao.com/api/v1/open/affiliate-dash` 一致)。
| 文件 | 内容 |
| --- | --- |
| `db/migrations/013_affiliate_dash_webhook_events.sql` | `webhook_events` 表 + `(provider, event_id)` 唯一索引(回调幂等去重) |
| `repositories/webhook-event-repo.ts` | `insertWebhookEventOnce``ON CONFLICT (provider, event_id) DO NOTHING`,冲突返回 `inserted=false` |
| `repositories/task-repo.ts` | `findAffiliateDashTaskByOrder`:按 `context_json #>> '{affiliateDash,orderNo}'` / `clientOrderNo` 查任务(仿 `findKuaishouFeifeiTaskByOrder` 同构 SQL |
| `services/platforms/affiliate-dash/notify-service.ts` | `handleAffiliateDashNotify`:验签(X-Timestamp 容差→重算签名)→ 查任务(未匹配 404 触发重试)→ 幂等闸门(重复 eventId 直接 200 跳过)→ `createTaskEvent('affiliate_dash_notify_received')``syncAffiliateDashTaskStatus`(状态合并式可重入) |
| `routes/affiliate-dash.ts` | `POST /`rate limit 300/min`req.rawBody` 验签(express.json verify 已存),全链路日志 |
| `app.ts` | 挂载 `/api/v1/open/affiliate-dash` |
**处理顺序设计**:去重闸门放在「找到任务」之后 —— 任务未建/已删时返回 404 让对方重试,而不是被 eventId 去重永久拦截;找到任务后冲突才视为重复投递直接 200。
**验证**typecheck 通过;后端全量 212 测试通过;脚本验证前置错误分支——缺签名头 401 / 时间戳超容差 401 / 验签失败 401 / 缺订单号 400 全部正确;「任务不存在→404」及幂等/同步路径依赖真实 DB,留待阶段 6 端到端演练(本沙箱无 postgres)。
---
## 21. 履约匹配逻辑深度分析(v2.6)
### 21.1 匹配链路全貌
```
91 订单 itemninetyone/order-service.ts:40-95
├─ externalSkuCode = productNo ← 91 商品编码(可能含 "----店铺ID" 后缀,parseOpen91ProductNo 拆分)
├─ externalSkuName = productName ← 91 商品名
├─ snapshot.productNo = productNo
└─ snapshot.productName = productName
▼ resolveConfiguredItemCandidateproduct-resolution-service.ts:212
├─ cloudtentaclesMatch = 商品名归一化模糊匹配(网络 listSku)
├─ kuaishouFeifeiMatch = 商品名规则 contains
└─ affiliateDashMatch = skuMapping[productNo] 精确映射(resolveAffiliateDashSkuByProductNo
▼ resolveFulfillmentRouterouting-config-service.ts:136
① 商品路由规则 rulesexact/contains,按商品名)→ ② 全局优先级
kuaishou_cloud → kuaishou_feifei → affiliate_dash → ③ manual_review 兜底
▼ planner.resolveDynamicAffiliateDashProfileplanner.ts:397
snapshot.affiliateDash.sku → profileconfigId: "affiliate_dash:<sku>"
```
### 21.2 痛点清单
| # | 痛点 | 影响 | 证据 |
| --- | --- | --- | --- |
| P1 | **优先级与可信度倒挂**affiliate_dash 是精确编码映射(运营显式配置、高可信),却排在 cloudtentacles / feifei 的**名字模糊匹配**之后 | 91 商品名若同时像 cloudtentacles 商品,会被 cloud 抢单走错通道;映射白配 | `DEFAULT_EXECUTOR_PRIORITY`routing-config-service.ts:74-78);阶段 3 联调能走对仅因当时 cloud/feifei 未命中 |
| P2 | **productNo 未拆分 `----` 后缀**`resolveAffiliateDashSkuByProductNo` 直接用 `snapshot.productNo` 原值查映射,而 open-91 的 productNo 可能为 `商品名----店铺ID``parseOpen91ProductNo` 专门拆它,但该函数未进解析链) | 带后缀的 91 编码精确匹配 miss → 直接落 manual_review | product-resolution-service.ts:305-330ninetyone/order-service.ts:30 |
| P3 | **匹配键无归一化**skuMapping key 只做原样精确匹配,无 trim/大小写/变体处理 | 编码大小写变体(`x91-lucky-90` vs `X91-LUCKY-90`miss | product-resolution-service.ts:317-318 |
| P4 | **缺名字兜底**affiliate_dash 有 33 个商品(含 displayName),但匹配只有编码精确映射;cloudtentacles / feifei 都有名字匹配,唯独 affiliate_dash 没有 | 新 91 商品未配映射即 manual_review,无法像 feifei 那样按名字自动兜底 | product-service.ts:58-75displayName 未被用于匹配) |
| P5 | **余额/健康度不参与路由决策**affiliate_dash 候选 available 只看映射命中;余额不足时选中 → prepare 预检失败 → manual_review,不会自动切下一通道 | 单通道阻塞,无自动降级 | 阶段 3 仅在 prepare 前预检(getAffiliateDashWallet |
| P6 | **双轨配置冲突**:路由规则(商品名→executor)与 skuMapping(编码→sku)两套独立配置,同一商品可能被两种规则重复描述、运营维护两处 | 配置漂移、规则打架 | routing-config-service.ts:159-177 vs product-resolution-service.ts:305-330 |
| P7 | **重复计算**`hasConfiguredOrderItems`(下单前)与 `resolveOrderItemForFulfillment`(履约时)各跑一次 `resolveConfiguredItemCandidate`cloudtentacles 名字匹配是**网络调用**(listSku) | 同一订单解析两次、重复拉商品列表 | product-resolution-service.ts:181-195cloudtentacles-name-match-service.ts:111 |
| P8 | **命名语义误导**`skuMapping`productNo→sku)实际 key 是 **91 外部编码**、value 是 **affiliate_dash sku**;文档/代码里 productNo 一词混用两套体系 | 运营配置时易搞反 | product-resolution-service.ts:40-45;文档 §12 |
### 21.3 优化建议(分档)
- **快赢档(P1/P2/P3,改动集中 product-resolution-service.ts,低风险)**
- P1`resolveConfiguredItemCandidate` 中 affiliateDashMatch 精确命中时**直接优先选择 affiliate_dash**(映射命中视为运营显式意图),不受全局优先级影响;或把 `DEFAULT_EXECUTOR_PRIORITY` 调整为 affiliate_dash 前置。建议加开关「映射命中优先」(默认开)。
- P2productNo 先走 `parseOpen91ProductNo` 拆掉 `----店铺ID` 再用 productName 段查映射。
- P3:映射查询前 trim + 大小写归一;可选支持 contains 前缀匹配。
- **中档(P4**affiliate_dash 商品 displayName 归一化名字匹配兜底(仿 cloudtentacles 匹配器),开关控制,避免误配。
- **后档(P5-P8)**:候选层余额/健康度(带缓存)、路由与映射配置合一、解析结果缓存去重、命名修正。
### 21.4 待验证点
- 91 卡券(91kaquan/kuaishou)真实 productNo 是否带 `----店铺ID` 后缀(决定 P2 是否为实际故障)。
- 33 个 affiliate_dash 商品 displayName 与 91 商品名的重合度(决定 P4 兜底收益)。
---
## 22. 履约匹配优化实施(v2.7 · 已完成)
**背景确认**:91 商品编码全部为**中文名**(如「幸运币90个」「星际漫游服装礼包」),无独立编码体系;运营按「中文名 → affiliate_dash sku」配置显式映射表(33 条,见 §22.3)。
### 22.1 匹配策略(两级)
1. **指定匹配(显式映射)最高优先**skuMapping 命中(91 中文名 → affiliate_dash sku)即**强制走 affiliate_dash**,不受 cloudtentacles / feifei 名字匹配抢占、不受全局优先级影响。受通道 `enabled` 约束:affiliate_dash 停用或商品不可用时回退原逻辑(规则 → 全局优先级)。
2. **默认匹配(名字匹配)**:无指定匹配时,三平台按商品名匹配,按可配置优先级选择(`defaultExecutorPriority`,现状 cloud → feifei → affiliate_dash,管理后台可改顺序)。
### 22.2 改动清单
| 文件 | 改动 |
| --- | --- |
| `routing-config-service.ts` | `resolveFulfillmentRoute` 新增 `preferredExecutorKey` 参数:指定匹配在**规则匹配之前**优先尝试,命中即选;停用/不可用回退 |
| `product-resolution-service.ts` | 91 分支:映射命中且 `preferredMatchEnabled!==false` 时传 `preferredExecutorKey=AFFILIATE_DASH``matchAffiliateDashSku` 归一化匹配(NFKC 全角→半角、去空白、小写),先原样精确再归一化遍历 |
| 配置层 | `preferredMatchEnabled` 字段(默认 true):types/runtime-config.ts、defaults.ts、source-config-service.ts、config.ts merge 全链 |
| 测试 | routing-config-service.test.ts +4preferred 优先/停用回退/不可用回退/无 preferred 走优先级);product-resolution-service.test.ts 新建 +5(精确/全角/空格/大小写/未命中) |
**安全性**:「不耽误生产」保证——现有 cloud/feifei 商品未配置映射表(或映射表为空),`preferredExecutorKey` 为空走原逻辑,行为零变化;映射表只影响显式配置的 91 商品,且可通过 `preferredMatchEnabled=false` 一键回退。
### 22.3 91 中文名 → affiliate_dash sku 映射表(可直接导入配置)
```json
{
"套装-Alan Walker": "suit_alan_walker",
"套装-暗影哥特": "suit_shadow_gothic",
"黑色高级特训官上衣": "top_black_elite_trainer",
"M416-仓鼠灰灰": "m416_hamster_gray",
"萌熊伴侣背包": "bag_cute_bear",
"套装-双彩绵绵": "suit_dual_fluffy",
"套装-糯粉咩咩": "suit_pink_sheep",
"套装-恋恋初桃": "suit_first_peach",
"套装-浪漫天命": "suit_romantic_destiny",
"西部牛仔大礼包": "pack_western_cowboy",
"烟雾弹-糯粉咩咩": "smoke_pink_sheep",
"破片手榴弹-糯粉咩咩": "frag_pink_sheep",
"套装-仓鼠灰灰": "suit_hamster_gray",
"套装-萌熊伴侣": "suit_cute_bear",
"糯粉咩咩背包": "bag_pink_sheep",
"糯粉咩咩头盔": "helmet_pink_sheep",
"仓鼠灰灰背包": "bag_hamster_gray",
"仓鼠灰灰头盔": "helmet_hamster_gray",
"套装-西部谜踪": "suit_western_mystery",
"国宝胖达头盔": "helmet_panda_treasure",
"套装-胖达圆圆": "suit_panda_round",
"套装-胖达团团": "suit_panda_tuan",
"熔岩游骑兵礼包": "pack_lava_ranger",
"套装-狂沙舞者": "suit_sand_dancer",
"星际漫游服装礼包": "pack_star_roam_outfit",
"星际漫游枪械礼包": "pack_star_roam_weapon",
"套装-绵云熊熊": "suit_cloud_bear",
"荣耀勋章2个": "honor_medal_x2",
"荣耀勋章30个": "honor_medal_x30",
"荣耀勋章90个": "honor_medal_x90",
"幸运币2个": "lucky_coin_x2",
"幸运币30个": "lucky_coin_x30",
"幸运币90个": "lucky_coin_x90"
}
```
### 22.4 行为验证
用上述映射表模拟 91 商品名解析,全部符合预期:
| 输入 | executor | sku |
| --- | --- | --- |
| 幸运币90个 | affiliate_dash | lucky_coin_x90 |
| 星际漫游服装礼包 | affiliate_dash | pack_star_roam_outfit |
| 幸运币90个(全角) | affiliate_dash | lucky_coin_x90 |
| 幸运币 90 个(带空格) | affiliate_dash | lucky_coin_x90 |
| 神秘新商品(未映射) | (无)回退 | — |
**验证**typecheck 通过;后端全量 221 测试通过(新增 9 个)。
---
## 23. 透传优化:91 编码 == affiliate_dash sku 自动命中(v2.8 · 已完成)
**需求**:91 商品编码可直接设置为英文(= affiliate_dash sku,如 `suit_alan_walker`),映射表未命中时**自动透传**,无需在 order_site 维护映射表。
### 23.1 匹配层级(`resolveAffiliateDashSkuByProductNo`,改为 async
1. **① 指定映射**`matchAffiliateDashSku`):skuMapping 精确/归一命中 → `matchMode='affiliate_dash_sku_mapping'`
2. **② 透传**`matchAffiliateDashSkuPassthrough`):映射未命中时,productNo 与 affiliate_dash 商品 sku 完全一致 → `matchMode='affiliate_dash_sku_passthrough'`
3. 两层任一命中都触发「指定匹配最高优先」(`preferredExecutorKey=affiliate_dash`),规则/全局优先级不抢占
**商品列表缓存**`getAffiliateDashSkuSet()` 内存缓存 5 分钟(`listAllAffiliateDashProducts` 拉全量 33 个 sku);拉取失败返回上次缓存或空集合——匹配 miss 走其他通道,**不阻塞下单**。
### 23.2 行为验证(线上商品列表)
| 输入(91 编码) | executor | sku | matchMode |
| --- | --- | --- | --- |
| `suit_alan_walker`(真实商品) | affiliate_dash | suit_alan_walker | affiliate_dash_sku_passthrough |
| `suit_cloud_bear`(真实商品) | affiliate_dash | suit_cloud_bear | affiliate_dash_sku_passthrough |
| `not_a_real_sku` | (无)回退 | — | — |
**验证**typecheck 通过;后端全量 224 测试通过(透传纯函数测试 +3)。
---
## 24. affiliate-dash 配置位置重构(v2.9 · 已完成)
**需求**affiliate_dash 平台配置原先杂糅在「履约配置」页(`AdminPlatformFulfillmentPage` 的 affiliate-dash tab),与履约路由/通道混在一起;项目已有独立的「平台配置」页(`AdminPlatformShopsPage`,按平台管理来源接入、凭据与店铺能力),应归位。
**改动**(纯前端 UI 位置移动,后端与数据通路零变化——后端路由与前端 API 本就挂在 `/api/v1/admin/platform-config/affiliate-dash/*`):
| 文件 | 改动 |
| --- | --- |
| `AdminPlatformShopsPage.tsx` | 新增 `affiliateDash` tablabel「affiliate-dash」);`AffiliateDashPlatformPanel` 组件整体迁入(自管理状态:对接配置/密钥/钱包/商品目录/映射表/映射测试),加载函数并入 `loadConfigs``Promise.all` |
| `AdminPlatformFulfillmentPage.tsx` | 移除 affiliate-dash tab、`AffiliateDashFulfillmentPanel` 组件及全部 affiliate 相关 import/类型 |
**页面归属**
- **平台配置**`AdminPlatformShopsPage`):affiliate-dash、kuaishou-feifei、kuaishou-lewan、行业电子凭证、内部通知 —— 平台接入凭据
- **履约配置**`AdminPlatformFulfillmentPage`):履约路由、kuaishou-feifei 履约、kuaishou-lewan 履约 —— 通道路由与履约参数
**验证**:前端 `tsc -b --noEmit` 通过;后端未改动。
---
## 25. 履约配置页 affiliate-dash 覆盖分析(v2.10 · 已完成)
**需求**:确认履约配置页是否也需要 affiliate-dash 相关内容(账号归平台配置页)。
### 分析结论
| 配置项 | 归属 | 现状 |
| --- | --- | --- |
| 对接凭据(appKey/appSecret/callbackSecret/baseUrl/timeout/容差) | 平台配置 | ✅ 平台配置页 affiliate-dash tab |
| 钱包 / 商品目录 / 映射测试 | 平台配置 | ✅ 平台配置页 affiliate-dash tab |
| 91 编码 → sku 映射表 | 平台配置 | ✅ 平台配置页 affiliate-dash tab |
| **履约路由**:通道开关(affiliate_dash enabled | 履约配置 | ✅ 履约路由面板已含(ROUTING_EXECUTORS |
| **通道优先级**defaultExecutorPriority 含 affiliate_dash,可上移/下移) | 履约配置 | ✅ 履约路由面板已含 |
| **商品路由规则**(商品名 → affiliate_dash) | 履约配置 | ✅ 履约路由面板规则编辑器已含 |
| **指定匹配优先开关**preferredMatchEnabled | 平台配置 | ⚠️ 新增字段未暴露 UI → **本次已补** |
**结论**:「账号在平台配置、路由在履约配置」的职责划分**已成立**——履约路由面板在阶段 3 就已纳入 affiliate_dash(通道开关/优先级/规则)。本次仅修复一个真实缺口:`preferredMatchEnabled`(映射命中优先,默认 true)此前只能靠默认值或环境变量,后台改不了;现已在平台配置 affiliate-dash tab 增加开关,前后端 effective/source 均透出。
**未新增**:履约配置页不加独立「affiliate-dash 履约面板」——feifei/lewan 有独立面板是因为履约参数多(productRules、deliveryItems 等),affiliate_dash 履约参数只有路由+优先级+规则,已覆盖,加面板属重复。
**验证**:前端 `tsc -b --noEmit` 通过;后端 typecheck 通过;224 测试全绿。