Files
yml2213 7bacb7d5eb docs: 重构项目文档并补充三块载体、成本中心与人工支出方案
合并本轮全部文档改动(原 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 约束)。
2026-10-17 22:03:03 +08:00

23 KiB
Raw Permalink Blame History

抖音码速达 · 总览(进度 / 代码 / 配置 / 上线)

更新: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。

只走真实公网联调(离线模拟已按要求删除):

  1. ngrok http 80 起隧道拿公网地址
  2. douyin-mashida:set-public-url 一键把 certLink 切过去 + 打印平台侧待配项 + 自检隧道
  3. douyin-mashida:subscribe 订阅推送(message.config)—— 不做这步平台一条消息都不会推
  4. 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 均已入库;历史分析文档按状态标注,不再使用“仅本地/不入库”的口径。


七、上线步骤

  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 / 11-接单模板待补清单.md
2c 工单「发布并核销」只核销快手电子凭证 已完成(2026-10-15):按来源分流,抖音工单核销 DYC 券(redeem_certificates),见 §九点六 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 抽出 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 前端失败提示改为「请到「抖音券核销」或快手电子凭证后台人工核销后再发货」

口径要点:

  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、重复券码触发唯一键