合并本轮全部文档改动(原 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 约束)。
23 KiB
抖音码速达 · 总览(进度 / 代码 / 配置 / 上线)
更新:2026-10-15 状态:最小接入已跑通,且按 SKU 的三层分流已上线(真实网关签名验证 / 真实推送订阅 / 真实 orderDetail 建单 / 真实 coupons.syncV2 同步成功 / 工单发布顺带核销抖音券),两端自检全绿 本文是这套内容的入口,具体细节在 04~12,最新增量见 §九点六。
一、一句话现状
按 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、分层实测 10-三层分流设计.md、运营动作 11-接单模板待补清单.md。
只走真实公网联调(离线模拟已按要求删除):
ngrok http 80起隧道拿公网地址douyin-mashida:set-public-url一键把certLink切过去 + 打印平台侧待配项 + 自检隧道douyin-mashida:subscribe订阅推送(message.config)—— 不做这步平台一条消息都不会推douyin-mashida:preflight前置自检(配置就绪度 / 真实网关连通性 / 公网回调可达 / 域名一致性)
详见 08-公网联调.md。
⚠️ localhost 永远收不到平台推送、也发不出买家能打开的链接 —— 必须挂隧道。
二、闭环与落点
① 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 | 抓取说明 + 目录 | ✅ 已入库 |
| 01-计费规则.md | 平台计费 | ✅ |
| 02-接口调用规范与签名.md | 出网签名规则 | ✅ |
| 03-消息推送规范与签名.md | 推送与验签 | ✅ |
接口API/、消息推送/、data/ |
全量抓取快照(46 个文件 / 488K) | ✅ |
| 04-结合本项目的深度分析.md | 深读平台后结合仓库的分析 | ✅ 已入库(历史分析) |
| 05-最小接入方案分析.md | 实施方案(决策与取舍都在这) | ✅ 已入库(历史方案,已实施) |
| 06-待确认问题清单.md | 问题清单(Q1~Q4 已答,其余边做边测) | ✅ 已入库(历史清单) |
| 07-最小接入实现说明.md | 实现说明 + 联调步骤 + 坑 | ✅ 已入库(现行) |
| 08-公网联调.md | 真实公网联调(ngrok + 一键切地址 + 前置自检 + checklist + 坑) | ✅ 已入库(现行) |
本文 00-总览.md |
进度索引 | ✅ 已入库(现行) |
六、落地提交记录
| commit | 内容 |
|---|---|
5a73fb74 |
抖音券核销后台界面(列表/核销/冲正/同步/对账/作废) |
319e0822 |
领取消息只发给有自动发货领取单元的单,人工单不再假发货 |
4b081d88 |
抖音按 SKU 三层分类接入,对齐 kuaishou-91 |
10464fea |
抽离接单模板匹配引擎(来源适配器/引擎/价格收口) |
6850bc64 |
工单发布并核销支持抖音券(按来源分流) |
本文档与 04~12 均已入库;历史分析文档按状态标注,不再使用“仅本地/不入库”的口径。
七、上线步骤
- 发版:
bash deploy/ubuntu-deploy.sh(启动时自动 up 迁移094,建redeem_certificates表) - 配平台:后台
POST /api/v1/admin/platform-config/douyin-mashida填uid/secret/certLink=https://<域名>/redeem;确认GET返回ready: true - 配推送:平台侧把消息地址设为
https://<域名>/api/v1/open/douyin-mashida/message,订阅trade_TradePaid(建议加trade_TradeCanceled、refund_RefundSuccess)
Caddy 不用改:已有 try_files {path} /index.html,/redeem 直接可用(这也是当初选路径式而非 # 的原因)。
八、缺口与待联调
没做的(按优先级)
| # | 缺口 | 影响 | 备注 |
|---|---|---|---|
已完成:/admin/platform-shops?tab=douyinMashida |
— | ||
已完成且已修正:命中映射 → affiliate_dash 自动发货;未命中不再写死 preferred、不再兜底 manual_dispatch(否则会出现「人工任务 + 工单」双载体) |
product-resolution-service.ts:286-350(commit 4b081d88) |
||
已完成:抖音按内部 SKU 三层分类(snapshot.classification),发单层有「载体唯一」硬守卫(命中 affiliate_dash 即跳过建工单);生产库接单模板 platform 已全部清空(354 条),抖音单从此可命中模板 |
10-三层分流设计.md / 11-接单模板待补清单.md | ||
已完成(2026-10-15):按来源分流,抖音工单核销 DYC 券(redeem_certificates),见 §九点六 |
worker-platform/publish-douyin-voucher-service.ts |
||
| 3 | 兑换后的实际交付物 | 当前只「上报核销 + 展示掩码券码 + 发链接」 | 等业务定(B2) |
| 4 | order.searchList 对账任务 |
消息丢了就漏单 | 二期 |
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 |
联调要实测的点
message_type=2发出去会不会改变抖店订单的发货状态(我们只想发链接)- 平台期望的成功响应体(当前
{"code":0,"message":"success"}) - 平台重推策略(失败我们返回 4xx,是否真重推)
coupons.syncV2的order_id认子订单号还是父订单号(代码按子单优先、父单兜底,联调时看日志)- 同步成功后买家在哪看到卡券(决定用户能不能走到
/redeem) verifyV3重复核销的返回、能否cancelVerify冲正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'的订单项不建工单(reasonaffiliate_dash_carrier_taken)。 - 券 → 工单快照:
material_json.douyinMashidaVoucher(对齐快手kuaishouIndustryVoucher)。前端有意不渲染(打手/客服不需要看到券码)。
2)接单模板匹配引擎重构(10464fea,P0+P1)
按 接单模板匹配引擎重构设计.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(抖音订单在那张表里永远是空的)→ 抖音券从未被核销。三个入口(发布 / 指派 / 寄售履约)共用同一个函数,全部受影响。
改法:在唯一入口按来源分流,核销本身不重复实现。
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 |
前端失败提示改为「请到「抖音券核销」或快手电子凭证后台人工核销后再发货」 |
口径要点:
- 查不到券 → 抛 409(
work_order_publish_douyin_certificate_missing)而不是静默成功——抖音口径是「任何订单都发券」,查不到说明发券环节没走完,需要运营去「抖音券核销」重发。 - 券状态落库由
verifyCertificate负责,因此后台「抖音券核销」界面的状态、重复发布的幂等行为与买家兑换页完全一致(不会有「本地说没核销」的分叉)。 - 回填工单是 best-effort:失败只留 warn,不影响发布结果。
- 单测用
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、重复券码触发唯一键 |