diff --git a/docs/抖音-码速达/09-皮肤领取流程.md b/docs/抖音-码速达/09-皮肤领取流程.md index 8390db32..946f9b39 100644 --- a/docs/抖音-码速达/09-皮肤领取流程.md +++ b/docs/抖音-码速达/09-皮肤领取流程.md @@ -1,61 +1,54 @@ -# 09 - 皮肤自动发货:买家领取流程 +# 09 - 皮肤自动发货:买家领取流程(一条链接闭环) -> 本文档描述抖音码速达**皮肤类商品**的交付改造:从「固定兑换链接 + 用户输订单号」 -> 改为「每单一条领取链接 + 买家填游戏账号领取」。 -> 代码位置:`apps/backend/src/services/{fulfillment,claim}/`、`apps/backend/src/services/platforms/douyin-mashida/`、 -> `apps/frontend/src/pages/claim-skin/`。 +> 本文档描述抖音码速达**皮肤类商品**的交付改造。 +> 代码位置:`apps/backend/src/services/fulfillment/`、`apps/backend/src/services/claim/`、 +> `apps/backend/src/services/platforms/douyin-mashida/`、`apps/frontend/src/pages/claim/`。 -## 1. 为什么要改 +## 1. 目标流程(与旧快手皮肤自动发货完全一致) -旧流程(`https://<域名>/redeem`,见文档 07)有三个毛病: +```text +买家付款 → 平台推送 → 建单 + 发券(cert_link = 该订单的领取链接) + → 按模板把【这一条链接】发给买家(order.message) + ↓ +买家打开 https://<域名>/claim/ + ├─ 输 UID(游戏账号 / 角色ID) + ├─ 拿到二维码 → 扫码绑定 + └─ 系统比对「绑定 UID == 填的 UID」→ 相同即发货完成,并核销平台券 +``` -| 问题 | 说明 | +**关键点:自始至终只有一条链接**。不再有"核销成功后再发一条去使用链接", +买家不需要输订单号,也不需要第二个页面。 + +## 2. 交付通道:复用现成的 `affiliate_dash` + +皮肤单**不另建执行器**,直接走仓库里已有的 `affiliate_dash` 通道(`services/fulfillment/executors/affiliate-dash-executor.ts` ++ `services/claim/kuaishou-cloud-claim-service.ts`):它本来就实现了 +「输 UID → 建 affiliate-dash 单 → 出二维码 → 扫码绑定 → 比对 UID → 完成」。 + +抖音侧只做三件事: + +| 事项 | 做法 | | --- | --- | -| 不认人 | 链接是**同一个**,谁拿到都能打开;靠买家手输订单号来"认领" | -| 多一步 | 买家要先去抖店订单里复制 19 位订单号,再回到页面粘贴 | -| 不合规风险 | 任何人输入别人的订单号都能看到脱敏摘要(虽无手机号,但没必要暴露) | +| 商品映射 | 内部 SKU(`sku_order_list[].code`,如「幸运币2个」)经权威表 `AFFILIATE_DASH_SKU_MAPPING` 转成英文 sku,产出 `affiliateDashMatch`,规划器/执行器原样复用 | +| 分流 | 命中映射表 → `affiliate_dash`;未命中 → **显式**回落 `manual_dispatch`(接单平台/人工) | +| 链接与通知 | `cert_link` 用领取链接;付款后按模板发一条消息 | -新流程:**一个订单一条链接**,链接自带 token,打开就知道是谁的单,直接领。 +> ⚠️ 未命中必须**显式**回落人工:线上/联调环境的 `unmatchedExecutorKey` 可能是空串 +> (dev 库实测就是空串),只靠全局兜底会让未命中的订单既不建任务也不进人工 = 静默漏单。 -## 2. 新流程 +## 3. 链接必须是**路径式**,且只有一个 -```text -买家付款 - └─ 平台推送 trade_TradePaid → 建单 + order_items - └─ 按【内部 SKU】分流 - ├─ 命中 AFFILIATE_DASH_SKU_MAPPING(皮肤/礼包类)→ 自动发货 - │ ├─ 建履约任务(皮肤执行器,requires_claim) - │ ├─ 生成 claim token → 领取链接 = claimLinkBase + '/' + token - │ ├─ coupons.syncV2:cert_link = 该订单的领取链接(买家在抖店卡券里点开直达) - │ └─ order.message:按 messageTemplate 把链接推给买家 - └─ 未命中 → manual_dispatch(接单平台/人工) -``` - -买家侧: - -```text -打开 https://<域名>/claim/ ← 路径式,没有 #(与打手领取页 #/claim 分开) - ├─ 展示:物品图片 + 内部 SKU + 数量 + 订单号 + 商品标题 - ├─ token 失效 → 友好提示,不展示表单 - ├─ 待领取 → 填「游戏账号 / 角色ID」→ 二次确认 → 领取 - └─ 领取成功 → 保存账号到任务 + 调 coupons.verifyV3 核销平台券 → 成功态 -``` - -## 3. 为什么链接必须是**路径式** - -仓库里已经有一个 `#/claim/{token}` 页面,那是**打手**用的(要走游戏 UID 绑定、领取身份校验那一套)。 -皮肤买家链接如果用 hash 形式,买家会被带进打手流程。 - -因此: - -- 买家页走**独立 BrowserRouter 分支**(照 `/redeem` 的做法,见 `apps/frontend/src/App.tsx`) -- 后端拼链接**只**用抖音配置的 `claimLinkBase`,**不要**经过 `buildClaimUrl()` - (后者读全局 `orders.claimBaseUrl`,默认值是 hash 形式 `…/#/claim`) -- 后台面板对 `claimLinkBase` 里出现 `#` 的情况会直接报错提示 +- 领取页走**独立 BrowserRouter 分支**的 `/claim/`(照 `/redeem` 的做法,见 `apps/frontend/src/App.tsx`), + **不带 `#`**;主站里 hash 形式的 `#/claim/:token` 仍保留(pathname 为 `/`,两套互不影响)。 +- 链接由抖音配置的 `claimLinkBase` 拼:`claimLinkBase`(去尾斜杠)+ `/` + token。 + **绝不回退** `buildClaimUrl` / `orders.claimBaseUrl`(那是 hash 的打手领取页)。 +- 同一订单项若有多个履约单元(多个 token),固定取 task id 最小的那个并打 warn, + 保证「同一张券每次都解析出同一条链接」。 +- 老订单的券里已经写死了 `/redeem` 链接(发出去收不回来),旧页面**保留可用**。 ## 4. 商品图怎么来 -图片按 affiliate_dash 的**英文 sku** 命名,放在前端 `apps/frontend/public/images/products/`(Vite publicDir,原样对外): +图片按 affiliate_dash 的**英文 sku** 命名,放在前端 `apps/frontend/public/images/products/`(Vite publicDir): ```text 内部 SKU(平台 sku_order_list[].code) 英文 sku(= 图片文件名) @@ -63,32 +56,26 @@ 套装-浪漫天命 → suit_romantic_destiny.webp ``` -映射复用**已有**的权威表 `services/platforms/affiliate-dash/sku-mapping.ts`(`AFFILIATE_DASH_SKU_MAPPING`), -由 `product-image.ts` 的 `resolveProductImageUrl()` 输出 `/images/products/<英文sku>.webp`。 -映射不到返回空串,前端显示占位图(不猜、不拼不存在的文件)。 +由 `services/platforms/affiliate-dash/product-image.ts` 的 `resolveProductImageUrl()` 输出 +`/images/products/<英文sku>.webp`;映射不到返回空串。领取详情接口会把结果放在 +`product.imageUrl`,领取页渲染成居中大图,**空值或加载失败都降级为占位块**(不出现破图)。 -单测里有一条**守卫**:映射表每个 sku 都必须有对应图片文件,新增商品漏图会直接测试失败。 +单测里有守卫:映射表每个 sku 都必须有对应图片文件,新增商品漏图会直接测试失败。 ## 5. 配置项(后台「抖音码速达接入配置」面板) | 字段 | 作用 | 留空/关闭时 | | --- | --- | --- | -| `claimLinkBase` | 皮肤订单领取页基址,如 `https://<域名>/claim` | 回退到通用 `certLink` | -| `messageTemplate` | 付款后发给买家的领取消息,支持 `{link}` `{sku}` `{orderNo}` `{productTitle}` | 用默认文案 | -| `claimMessageEnabled` | 是否在付款后发领取消息 | 关闭则只把链接写进卡券 | +| `claimLinkBase` | 领取页基址,如 `https://<域名>/claim` | 回退通用 `certLink` | +| `messageTemplate` | 付款后发给买家的消息,支持 `{link}` `{sku}` `{orderNo}` `{productTitle}` | 用默认文案 | +| `claimMessageEnabled` | 是否在付款后发这条领取消息 | 关闭则只把链接写进卡券 | | `certLink` | **通用兜底**兑换链接(非皮肤订单、或解析不出领取链接时用) | — | -## 6. 兼容性 +⚠️ 调后台保存接口必须带**全量配置**(保存是整体替换,只有 `secret` 传空表示不修改), +脚本里要写成 `{ ...当前配置, 新字段 }`。`claimLinkBase` 带 `#` 会被前端与 preflight 判为误配。 -- 旧订单的卡券里已经写死了旧链接,**永远指向 `/redeem`**(收不回来)→ 旧页面**保留可用**,不删。 -- 新订单才会走领取链接;`certLink` 继续作为兜底,非皮肤订单行为完全不变。 -- 买家游戏账号存在 `fulfillment_tasks.claim_submission_json`: - **不写** `work_orders.material_json`(那是发给打手的工单素材,写进去等于泄露买家隐私)。 +## 6. 下一步(尚未实现) -## 7. 下一步(尚未实现) - -领取成功后任务停在「已领取」,真正把物品发到买家账号仍需要履约: - -- 皮肤类是否全自动发放(对接发货平台/卡密),还是先出一张人工工单; -- 失败/退款场景要能 `coupons.cancelVerify` 冲正并把链接置回可用; -- 接单平台类订单的领取链接沿用同一套 token + 页面,还是另有形态。 +- 领取完成后的**实际发货**(谁来把物品发到买家账号):皮肤类是否全自动、还是先出人工工单; +- 失败/退款场景用 `coupons.cancelVerify` 冲正,并让领取链接回到可用; +- 接单平台类订单的领取链接形态(当前只做了皮肤)。