合并本轮全部文档改动(原 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 约束)。
320 lines
23 KiB
Markdown
320 lines
23 KiB
Markdown
# 抖音码速达 · 总览(进度 / 代码 / 配置 / 上线)
|
||
|
||
> 更新:2026-10-15 **状态:最小接入已跑通,且按 SKU 的三层分流已上线**(真实网关签名验证 / 真实推送订阅 / 真实 orderDetail 建单 / 真实 coupons.syncV2 同步成功 / 工单发布顺带核销抖音券),两端自检全绿
|
||
> 本文是这套内容的**入口**,具体细节在 04~12,最新增量见 [§九点六](#九点六2026-10-14--10-15-增量)。
|
||
|
||
---
|
||
|
||
## 一、一句话现状
|
||
|
||
按 [05-最小接入方案分析.md](05-最小接入方案分析.md) 落地的**最小闭环六步全部完成**;2026-10-17 复核:后端 740 项测试(737 pass / 3 skipped)、前端 143 项测试全绿,迁移 `094` / `095` 已在本地临时库实测通过。
|
||
|
||
**没做的**:兑换后的实际交付物(卡密/充值)、`order.searchList` 对账、兑换页二次校验(手机后 4 位)。
|
||
|
||
> ✅ **原「按 SKU 的三层分类」已上线**(2026-10-14):抖音与 `kuaishou-91` 来源(91 回调 + 快手 send-code)同构(命中映射 → `affiliate_dash` 自动发货;未命中 → 接单平台建工单;都不中 → 无载体=待人工),载体唯一有硬守卫。总设计 [12-按SKU分类改造总设计.md](12-按SKU分类改造总设计.md)、分层实测 [10-三层分流设计.md](10-三层分流设计.md)、运营动作 [11-接单模板待补清单.md](11-接单模板待补清单.md)。
|
||
|
||
**只走真实公网联调**(离线模拟已按要求删除):
|
||
|
||
1. `ngrok http 80` 起隧道拿公网地址
|
||
2. `douyin-mashida:set-public-url` 一键把 `certLink` 切过去 + 打印平台侧待配项 + 自检隧道
|
||
3. `douyin-mashida:subscribe` **订阅推送**(`message.config`)—— 不做这步平台一条消息都不会推
|
||
4. `douyin-mashida:preflight` 前置自检(配置就绪度 / **真实网关连通性** / 公网回调可达 / 域名一致性)
|
||
|
||
详见 [08-公网联调.md](08-公网联调.md)。
|
||
|
||
> ⚠️ localhost 永远收不到平台推送、也发不出买家能打开的链接 —— **必须挂隧道**。
|
||
|
||
---
|
||
|
||
## 二、闭环与落点
|
||
|
||
```text
|
||
① trade_TradePaid 推送
|
||
POST /api/v1/open/douyin-mashida/message
|
||
└─ 验签(MD5 小写)→ webhook_events 幂等 → order.orderDetail 拉详情
|
||
└─ upsertOrderFromSource 建单(provider=douyin_mashida / platform=doudian)
|
||
└─ 每个订单项生成 DYC 券码 → coupons.syncV2 交给平台
|
||
|
||
② 买家打开 https://<域名>/redeem(路径式,不带 #)
|
||
输订单号 → POST /api/v1/redeem/lookup(状态分流)
|
||
|
||
③ 确认兑换 → POST /api/v1/redeem/confirm
|
||
└─ coupons.verifyV3 上报核销
|
||
└─ order.message 把「去使用链接」发给买家(best-effort)
|
||
|
||
④ refund_RefundSuccess / trade_TradeCanceled 推送
|
||
└─ 该订单未核销的券码作废
|
||
```
|
||
|
||
| 环节 | 入口 | 核心实现 |
|
||
| --- | --- | --- |
|
||
| 消息接收 | `routes/douyin-mashida.ts`(挂在全局 body parser **之前**) | `notify-service.ts` |
|
||
| 签名 | — | `sign.ts`(出网 + 入站两套,含 golden 向量单测) |
|
||
| 拉详情/发券/核销/通知 | — | `gateway-client.ts` + `http-client.ts` |
|
||
| 建单 | 复用 `services/order/order-service.ts:upsertOrderFromSource` | `order-mapper.ts` |
|
||
| 券码 | 迁移 `094` | `redeem-certificate-repo.ts` + `certificate-service.ts` |
|
||
| 兑换 | `routes/redeem.ts` | `redeem-service.ts` |
|
||
| 后台配置 | `routes/admin/platform-config/douyin-mashida.ts` | `douyin-mashida-service.ts`(凭据脱敏)+ 前端面板 `DouyinMashidaPanel.tsx` |
|
||
| 本地联调 | `scripts/douyin-mashida-gateway-stub.ts` / `douyin-mashida-push.ts` | 网关桩 + 推送模拟,见文档 08 |
|
||
| 公网联调 | `set-public-url` + `preflight` + `subscribe` 三个脚本 + ngrok | 一键切地址 / 自检隧道 / **推送订阅**,见文档 08 |
|
||
| 前端页面 | `pages/redeem/RedeemPage.tsx` | `App.tsx` 的独立 `BrowserRouter` 分支 |
|
||
|
||
---
|
||
|
||
## 三、代码清单(23 个文件 / 3107 行)
|
||
|
||
### 新增 · 后端
|
||
|
||
| 文件 | 行 | 作用 |
|
||
| --- | ---: | --- |
|
||
| `services/platforms/douyin-mashida/sign.ts` | 130 | 两套签名:出网 `md5(secret+Σ(名+值)+secret)`、推送 `md5(secret+data+pub_time+topic+secret)` |
|
||
| `services/platforms/douyin-mashida/source-config-service.ts` | 154 | 配置读写 + 默认值 + `assertDouyinMashidaConfig` |
|
||
| `services/platforms/douyin-mashida/http-client.ts` | 165 | 网关 POST、超时、报文日志、`code !== 10000` 抛 502 |
|
||
| `services/platforms/douyin-mashida/gateway-client.ts` | 109 | 4 个出网方法:`orderDetail` / `syncV2` / `verifyV3` / `order.message`(+`cancelVerify`) |
|
||
| `services/platforms/douyin-mashida/order-mapper.ts` | 214 | 消息+详情 → `SourceOrderEvent`;抖店状态映射;子单号入 item 快照 |
|
||
| `services/platforms/douyin-mashida/notify-service.ts` | 384 | 验签 → 幂等 → 分派(付款建单发券 / 退款作废) |
|
||
| `services/platforms/douyin-mashida/*.test.ts` | 407 | 3 个测试文件 / 24 用例 |
|
||
| `services/redeem/certificate-service.ts` | 312 | 发券、syncV2、核销上报、核销后通知、作废 |
|
||
| `services/redeem/redeem-service.ts` | 269 | 兑换状态机(7 态)+ 券码掩码 + 订单号校验 |
|
||
| `services/redeem/redeem-service.test.ts` | 69 | 6 用例 |
|
||
| `repositories/redeem-certificate-repo.ts` | 233 | DYC 券码生成(Crockford Base32)、发券幂等、状态流转 |
|
||
| `routes/douyin-mashida.ts` | 67 | 消息入口(`express.text` 收原文,兼容 `data=` 表单) |
|
||
| `routes/redeem.ts` | 43 | `/lookup` + `/confirm`,按 `orderNo`+IP 限流 |
|
||
| `routes/admin/platform-config/douyin-mashida.ts` | 49 | 后台 GET/POST + 审计 |
|
||
| `services/admin/platform-config/douyin-mashida-service.ts` | 75 | 配置读写,secret 出参置空 + 掩码,掩码回写不覆盖 |
|
||
| `db/migrations/094_redeem_certificates.sql` | 57 | `redeem_certificates` 表 |
|
||
| `db/migrations/095_redeem_certificates_platform_cert.sql` | 30 | 记录**平台侧实际券码**(平台会改名,核销必须用它) |
|
||
| `scripts/douyin-mashida-gateway-stub.ts` | 320 | **本地网关桩**:假装码速达,校验签名并回 `code=10000` |
|
||
| `scripts/douyin-mashida-push.ts` | 300 | **推送模拟器**:造签名消息打本地后端 |
|
||
| `scripts/douyin-mashida-set-public-url.ts` | 290 | **公网联调切换**:改 `certLink` 为隧道地址 + 打印平台侧配置 + 自检隧道 |
|
||
| `scripts/douyin-mashida-preflight.ts` | 330 | **前置自检**:配置就绪度 + 真实网关连通性(可选验签名)+ 公网回调 + 域名一致性 |
|
||
| `scripts/douyin-mashida-subscribe.ts` | 250 | **推送订阅**:调 `message.config` 配置推送地址与 topics + `messagetest` 自测 |
|
||
|
||
### 新增 · 前端
|
||
|
||
| 文件 | 行 | 作用 |
|
||
| --- | ---: | --- |
|
||
| `pages/redeem/RedeemPage.tsx` | 166 | 兑换页(同 CollectPage 骨架,按 status 渲染) |
|
||
| `utils/redeem.ts` | 82 | 纯函数:`isRedeemPath` / 金额时间格式化 / 状态→文案色调 |
|
||
| `types/redeem.ts` | 39 | 与后端 `RedeemOrderView` 对齐 |
|
||
| `services/redeem.ts` | 16 | `lookupRedeemOrder` / `confirmRedeemOrder` |
|
||
| `tests/redeem.test.ts` | 67 | 7 用例 |
|
||
| `pages/admin/platform/DouyinMashidaPanel.tsx` | 346 | 后台「抖音码速达」配置面板(进度/脱敏密钥/本地桩提示) |
|
||
| `components/admin/SecretFieldControl.tsx` | 158 | 脱敏密钥输入组件(从 `AdminPlatformShopsPage` 抽出,两侧共用) |
|
||
| `utils/douyin-mashida.ts` | 64 | 纯函数:本地地址判定 / 推送地址拼接 / 缺失项提示 |
|
||
| `types` + `services/admin/platform-config/douyin-mashida.ts` | 49 | 配置读写服务 |
|
||
| `tests/douyin-mashida.test.ts` | 86 | 6 用例 |
|
||
|
||
### 修改
|
||
|
||
| 文件 | 改了什么 |
|
||
| --- | --- |
|
||
| `apps/backend/src/app.ts` | 挂载 `/api/v1/open/douyin-mashida`(**必须在 `express.json` 之前**)+ `/api/v1/redeem` |
|
||
| `apps/backend/src/config/app-config-keys.ts` | 新增 key `douyin_mashida` |
|
||
| `apps/backend/src/routes/admin/platform-config.ts` | 注册后台配置路由(自动继承 admin 守卫) |
|
||
| `apps/backend/package.json` | 新增 `douyin-mashida:set-public-url` / `douyin-mashida:preflight` / `douyin-mashida:subscribe` |
|
||
| `apps/frontend/vite.config.ts` | dev server `allowedHosts` 放行常见隧道域名;**接上了此前从未被读取的 `VITE_ALLOWED_HOSTS`**(`.env`/compose 早就配了,白名单却是硬编码的) |
|
||
| `apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx` | 新增「抖音码速达」Tab;本地 `SecretFieldControl` 改为共享组件 import |
|
||
| `apps/frontend/src/App.tsx` | 新增 `isRedeemPath` 分支(早于 `isWorkerSite()`)+ `RedeemApplication` |
|
||
| `apps/frontend/src/styles/client.css` | `.redeem-page` / `.redeem-card` / `.redeem-order-input` |
|
||
|
||
---
|
||
|
||
## 四、配置清单
|
||
|
||
存在 `app_config_entries.douyin_mashida`(JSONB)。后台接口:`POST /api/v1/admin/platform-config/douyin-mashida`
|
||
|
||
| 字段 | 默认 | 说明 |
|
||
| --- | --- | --- |
|
||
| `enabled` | `true` | 关掉后推送接口返回 409 |
|
||
| `baseUrl` | `https://dy-openplatform-api.wanmahui.com` | 网关地址 |
|
||
| `uid` | 空 | 抖店商户号(必填) |
|
||
| `secret` | 空 | 用户秘钥(必填);**出参永不明文回显** |
|
||
| `certLink` | 空 | 卡券里的兑换链接,固定 `https://<域名>/redeem`(必填) |
|
||
| `messageType` | `2` | `order.message` 的类型:1-订单状态 / 2-订单发货 / 4-图片 / 5-视频 |
|
||
| `afterVerifyLink` | 空 | 兑换成功后发给买家的「去使用链接」 |
|
||
| `afterVerifyMessage` | `您的订单已兑换成功` | 通知正文 |
|
||
| `timeoutMs` | `10000` | 出网超时 |
|
||
| `handledTopics` | `trade_TradePaid` / `trade_TradeCanceled` / `refund_RefundSuccess` | 白名单外的 topic 只留痕 |
|
||
|
||
`GET` 会返回 `ready` 自查位(`enabled + uid + secret + certLink` 齐全才为 `true`)。
|
||
|
||
---
|
||
|
||
## 五、文档索引
|
||
|
||
| 文档 | 内容 | 状态 |
|
||
| --- | --- | --- |
|
||
| [README.md](README.md) | 抓取说明 + 目录 | ✅ 已入库 |
|
||
| [01-计费规则.md](01-计费规则.md) | 平台计费 | ✅ |
|
||
| [02-接口调用规范与签名.md](02-接口调用规范与签名.md) | 出网签名规则 | ✅ |
|
||
| [03-消息推送规范与签名.md](03-消息推送规范与签名.md) | 推送与验签 | ✅ |
|
||
| `接口API/`、`消息推送/`、`data/` | 全量抓取快照(46 个文件 / 488K) | ✅ |
|
||
| [04-结合本项目的深度分析.md](04-结合本项目的深度分析.md) | 深读平台后结合仓库的分析 | ✅ 已入库(历史分析) |
|
||
| [05-最小接入方案分析.md](05-最小接入方案分析.md) | **实施方案**(决策与取舍都在这) | ✅ 已入库(历史方案,已实施) |
|
||
| [06-待确认问题清单.md](06-待确认问题清单.md) | 问题清单(Q1~Q4 已答,其余边做边测) | ✅ 已入库(历史清单) |
|
||
| [07-最小接入实现说明.md](07-最小接入实现说明.md) | 实现说明 + 联调步骤 + 坑 | ✅ 已入库(现行) |
|
||
| [08-公网联调.md](08-公网联调.md) | **真实公网联调**(ngrok + 一键切地址 + 前置自检 + checklist + 坑) | ✅ 已入库(现行) |
|
||
| 本文 `00-总览.md` | 进度索引 | ✅ 已入库(现行) |
|
||
|
||
---
|
||
|
||
## 六、落地提交记录
|
||
|
||
| commit | 内容 |
|
||
| --- | --- |
|
||
| `5a73fb74` | 抖音券核销后台界面(列表/核销/冲正/同步/对账/作废) |
|
||
| `319e0822` | 领取消息只发给有自动发货领取单元的单,人工单不再假发货 |
|
||
| `4b081d88` | 抖音按 SKU 三层分类接入,对齐 `kuaishou-91` |
|
||
| `10464fea` | 抽离接单模板匹配引擎(来源适配器/引擎/价格收口) |
|
||
| `6850bc64` | 工单发布并核销支持抖音券(按来源分流) |
|
||
|
||
> 本文档与 04~12 均已入库;历史分析文档按状态标注,不再使用“仅本地/不入库”的口径。
|
||
|
||
---
|
||
|
||
## 七、上线步骤
|
||
|
||
1. **发版**:`bash deploy/ubuntu-deploy.sh`(启动时自动 up 迁移 `094`,建 `redeem_certificates` 表)
|
||
2. **配平台**:后台 `POST /api/v1/admin/platform-config/douyin-mashida` 填 `uid` / `secret` / `certLink=https://<域名>/redeem`;确认 `GET` 返回 `ready: true`
|
||
3. **配推送**:平台侧把消息地址设为 `https://<域名>/api/v1/open/douyin-mashida/message`,订阅 `trade_TradePaid`(建议加 `trade_TradeCanceled`、`refund_RefundSuccess`)
|
||
|
||
**Caddy 不用改**:已有 `try_files {path} /index.html`,`/redeem` 直接可用(这也是当初选路径式而非 `#` 的原因)。
|
||
|
||
---
|
||
|
||
## 八、缺口与待联调
|
||
|
||
### 没做的(按优先级)
|
||
|
||
| # | 缺口 | 影响 | 备注 |
|
||
| --- | --- | --- | --- |
|
||
| ~~1~~ | ~~后台前端配置面板~~ | **已完成**:`/admin/platform-shops?tab=douyinMashida` | — |
|
||
| ~~2~~ | ~~履约任务分支~~ | **已完成且已修正**:命中映射 → `affiliate_dash` 自动发货;未命中**不再写死** preferred、**不再兜底 `manual_dispatch`**(否则会出现「人工任务 + 工单」双载体) | `product-resolution-service.ts:286-350`(commit `4b081d88`) |
|
||
| ~~2b~~ | ~~**接单平台发单层(层②)**~~ | **已完成**:抖音按内部 SKU 三层分类(`snapshot.classification`),发单层有「载体唯一」硬守卫(命中 `affiliate_dash` 即跳过建工单);生产库接单模板 `platform` 已全部清空(354 条),抖音单从此可命中模板 | [10-三层分流设计.md](10-三层分流设计.md) / [11-接单模板待补清单.md](11-接单模板待补清单.md) |
|
||
| ~~2c~~ | ~~工单「发布并核销」只核销快手电子凭证~~ | **已完成**(2026-10-15):按来源分流,抖音工单核销 DYC 券(`redeem_certificates`),见 [§九点六](#九点六2026-10-14--10-15-增量) | `worker-platform/publish-douyin-voucher-service.ts` |
|
||
| 3 | 兑换后的实际交付物 | 当前只「上报核销 + 展示掩码券码 + 发链接」 | 等业务定(B2) |
|
||
| 4 | `order.searchList` 对账任务 | 消息丢了就漏单 | 二期 |
|
||
| ~~5~~ | ~~`cancelVerify` 后台按钮~~ | **已完成**:后台「抖音券核销」含核销/冲正/重新同步/对账/重发/作废(每个写操作带审计) | `routes/admin/douyin-mashida.ts`(commit `5a73fb74`) |
|
||
| 6 | 兑换页二次校验(手机后 4 位) | 知道订单号即可兑换 | 看业务是否接受(B1) |
|
||
| 7 | `message.config` 自动订阅 | 需平台侧手配 | 可选做「一键订阅」 |
|
||
| 8 | 隧道域名变化需手工同步平台侧 | `set-public-url` 会打印待配项;ngrok 免费版建议保留静态域名 | 固定域名后基本不是问题 |
|
||
| 9 | ngrok 免费版对浏览器插警示页 | 买家点 `cert_link` 会先过一页英文提示 | 正式环境用正式域名;联调期间点一下即可 |
|
||
| — | 工单里的抖音券前端不渲染 | **有意为之**(2026-10-15 用户口径):打手/客服**不需要看到券码**,快照只落 `material_json` 供追溯 | `material_json.douyinMashidaVoucher` |
|
||
|
||
### 联调要实测的点
|
||
|
||
1. `message_type=2` 发出去**会不会改变抖店订单的发货状态**(我们只想发链接)
|
||
2. 平台期望的**成功响应体**(当前 `{"code":0,"message":"success"}`)
|
||
3. 平台**重推策略**(失败我们返回 4xx,是否真重推)
|
||
4. `coupons.syncV2` 的 `order_id` 认**子订单号还是父订单号**(代码按子单优先、父单兜底,联调时看日志)
|
||
5. 同步成功后**买家在哪看到卡券**(决定用户能不能走到 `/redeem`)
|
||
6. `verifyV3` 重复核销的返回、能否 `cancelVerify` 冲正
|
||
7. `order.orderDetail` 的 `p_id`/`order_id` 实际是字符串还是数字
|
||
|
||
---
|
||
|
||
## 九、两个必须记住的坑
|
||
|
||
### 1)19 位订单号会被 `JSON.parse` 抹精度 ★
|
||
|
||
抖店订单号超过 JS 安全整数,`4837328765348567001` 经 `JSON.parse` 会变成 `...000`。
|
||
处理:`notify-service.ts:parseMessageData()` 解析后用正则从**原始 JSON 文本**把长数字原样抓回(字符串),
|
||
`applyRawBigIntegerOverrides` 覆盖 `p_id`/`order_id`/`s_id`/`s_ids`/`aftersale_id` 等。单测同时证明了"不修会错"和"修完精确"。
|
||
|
||
### 2)验签必须用 body 原文 → 路由挂在全局 parser 之前
|
||
|
||
`app.ts:42` 把 `/api/v1/open/douyin-mashida` 挂在 `express.json()` / `express.urlencoded()` **之前**,
|
||
路由内部用 `express.text({ type: () => true })` 自己收原文(并兼容 `data=<json>` 的表单形态)。
|
||
任何全局 body parser 都会重排/转义 JSON,签名原串就对不上。
|
||
|
||
---
|
||
|
||
## 九点五、真实平台实测结论(2026-10-09)
|
||
|
||
| 验证项 | 结果 |
|
||
| --- | --- |
|
||
| 出网签名(用真实 uid/secret 调 `order.orderDetail`) | ✅ `code=10000`,签名规则与文档一致 |
|
||
| 推送订阅 `message.config` | ✅ `code=10000 成功`(配置前平台一条都不推 —— 这是"下单没进 dev"的根因) |
|
||
| 进站推送(真密钥签名 → 公网地址 → 验签 → 建单) | ✅ `action=paid-order-upserted` |
|
||
| 19 位订单号精度 | ✅ `6930310175251856483` 精确保留 |
|
||
| `coupons.syncV2` 同步到真平台 | ✅ `sync_status=success` |
|
||
| 兑换页状态 | ✅ `redeemable`,展示真实商品名与真实支付时间 |
|
||
| **平台自动推送**(订阅后真单付款) | ✅ 6 秒到达,来源 IP `118.31.56.230`,端到端约 7 秒完成建单+发券+同步 |
|
||
| `pay_time` 字符串形态 | ⚠️ 实测发现并已修复(见 07 §4.8) |
|
||
| **核销(verifyV3)** | ✅ 真单核销成功,平台侧 `status=3 已使用 / verify_num=1` |
|
||
| **平台会改写券码** | ⚠️ 3 笔里 2 笔被平台改名 → 已修(核销改用平台侧券码 + 失败自愈,迁移 095) |
|
||
| `order.message` 发消息 | ✅ 兑换成功后已调用,无错误日志(成功日志默认被 integration 阈值 warn 挡住) |
|
||
| `messagetest` 自测入口 | ⚠️ 始终 `20006 data参数内容解析异常`,未探明其 data 要求;不影响真实链路 |
|
||
|
||
## 九点六、2026-10-14 / 10-15 增量
|
||
|
||
### 1)按 SKU 三层分流上线(`4b081d88`)
|
||
|
||
抖音与 `kuaishou-91` 同构,**只动抖音,kuaishou-91 不变**:
|
||
|
||
| 层 | 判据 | 载体 |
|
||
| --- | --- | --- |
|
||
| ① 自动发货 | 内部 SKU 命中 `AFFILIATE_DASH_SKU_MAPPING`(常量表 + 后台 `skuMapping` 增量) | `affiliate_dash` 履约任务(买家输 UID → 扫码绑定 → 自动核销) |
|
||
| ② 接单平台 | 命中接单模板(`work_product_rules`,按**内部 SKU**) | `work_orders` 工单 |
|
||
| ③ 人工 | 两者都不中 | **无载体**(无任务无工单)=待人工 |
|
||
|
||
- `sku_code` 恒为**内部 SKU**(不再被英文 sku 覆写);引流长标题只进 `snapshot.productTitle`,**永不参与匹配**。
|
||
- 未命中映射时**不设 preferred、强制覆盖 `unmatchedExecutorKey:''`**——否则会出现「人工任务 + 工单」双载体。
|
||
- 载体唯一守卫:`sync-worker-orders-service.ts:65` 命中 `classification.channel==='affiliate_dash'` 的订单项**不建工单**(reason `affiliate_dash_carrier_taken`)。
|
||
- 券 → 工单快照:`material_json.douyinMashidaVoucher`(对齐快手 `kuaishouIndustryVoucher`)。**前端有意不渲染**(打手/客服不需要看到券码)。
|
||
|
||
### 2)接单模板匹配引擎重构(`10464fea`,P0+P1)
|
||
|
||
按 [接单模板匹配引擎重构设计.md](../接单模板匹配引擎重构设计.md) 抽出 `services/product-match/`(适配器按**来源**划分:`kuaishou-91` / `pdd` / `douyin-mashida`,其中 `kuaishou-91` 同时覆盖 91 回调与快手 send-code 两条数据通道)。生产回放 6000 条订单项:`kuaishou-91`/PDD 决策 **0 差异**,抖音 5 条全为修正。旧 `work-product-match-mappers.ts` 变为门面,签名不变。
|
||
|
||
### 3)工单「发布并核销」支持抖音券(本次)
|
||
|
||
**问题**:抖音单走到接单平台后有工单,但「发布并核销」只查 `kuaishou_industry_vouchers`(抖音订单在那张表里永远是空的)→ **抖音券从未被核销**。三个入口(发布 / 指派 / 寄售履约)共用同一个函数,全部受影响。
|
||
|
||
**改法**:在唯一入口按**来源**分流,核销本身**不重复实现**。
|
||
|
||
```text
|
||
publishAdminWorkOrder / assignAdminWorkOrder / 寄售履约
|
||
└─ consumeIndustryVouchersBeforeWorkOrderPublish (publish-voucher-service.ts)
|
||
├─ 抖音(orders.provider === 'douyin_mashida')
|
||
│ └─ verifyDouyinMashidaCertificateForWorkOrderPublish (publish-douyin-voucher-service.ts)
|
||
│ ├─ 按 platform_order_id(= redeem_certificates.order_id,子单优先/父单兜底)查券
|
||
│ ├─ 委托 verifyCertificate(redeem/certificate-service.ts)→ coupons.verifyV3
|
||
│ │ (幂等 / invalid 拒核 / 未同步拒核 / 平台券码优先 / 失败对账自愈)
|
||
│ └─ 券快照 merge 进工单 material_json.douyinMashidaVoucher + 事件 publish_voucher_consumed
|
||
└─ 其它来源(kuaishou-91)
|
||
└─ 原快手电子凭证路径(listKuaishouIndustryVouchersByOid + consumeKuaishouIndustryVoucher)
|
||
```
|
||
|
||
| 文件 | 作用 |
|
||
| --- | --- |
|
||
| `worker-platform/publish-douyin-voucher-service.ts` | 抖音券核销分支(新增) |
|
||
| `worker-platform/publish-voucher-service.ts` | 顶部按来源分流(`isDouyinMashidaWorkOrder`,查 `orders.provider`) |
|
||
| `utils/voucher-consume.ts` | 前端失败提示改为「请到「抖音券核销」或快手电子凭证后台人工核销后再发货」 |
|
||
|
||
口径要点:
|
||
|
||
1. **查不到券 → 抛 409**(`work_order_publish_douyin_certificate_missing`)而不是静默成功——抖音口径是「任何订单都发券」,查不到说明发券环节没走完,需要运营去「抖音券核销」重发。
|
||
2. **券状态落库由 `verifyCertificate` 负责**,因此后台「抖音券核销」界面的状态、重复发布的幂等行为与买家兑换页完全一致(不会有「本地说没核销」的分叉)。
|
||
3. **回填工单是 best-effort**:失败只留 warn,不影响发布结果。
|
||
4. 单测用 `setOrderProviderResolverForTest` / `setDouyinVoucherDepsForTest` 注入(生产恒为 undefined,直连真实仓储与网关)。
|
||
|
||
**验证**(2026-10-15):后端 `npm run check` ✅ 740 例 0 失败(本次新增 13 例:核销成功回填/保留 material 其它字段/无券报错/真源报错转失败结果且不回填/已核销幂等/指派路径/无平台单号跳过 + 来源分流回归 3 例);前端 `npm run check` ✅ 143 例 0 失败。
|
||
|
||
### 4)其它
|
||
|
||
- `584b6f52`:问题单处置存在活跃拼单份额时,禁止取消 / 退回大厅 / 重开。
|
||
|
||
---
|
||
|
||
## 十、自检结果(2026-10-15 当时)
|
||
|
||
| 项 | 结果 |
|
||
| --- | --- |
|
||
| 后端 `npm run typecheck` | EXIT=0 |
|
||
| 新增单测(douyin-mashida + redeem) | **30 / 30 通过** |
|
||
| 后端 `npm run check`(format+sql+lint+typecheck+test+build) | EXIT=0,636 测试 / 633 通过 / 0 失败 |
|
||
| 前端 `npm run check` | EXIT=0,115 / 115 通过 |
|
||
| 迁移 `094` | 在本地 dev Postgres 临时库实测通过;重复 `(provider, order_id)` 插入返回 `INSERT 0 0`、重复券码触发唯一键 |
|