18 KiB
抖音码速达 · 三层分流设计(皮肤自动发货 / 接单平台 / 人工)
状态:已实施(抖音侧,2026-10-14);后续项见 §8 未决问题。最后校准:2026-10-18(
main @ 7542d547;仅基线更新,业务逻辑未变)。 依据:生产库只读实测(接单模板 662 条)+ 全链路代码走查 关联:09-皮肤领取流程.md、11-接单模板待补清单.md
一、要解决的问题
抖音现在只有两条路:
- 内部 SKU 命中
AFFILIATE_DASH_SKU_MAPPING→affiliate_dash皮肤自动发货; - 其余 → 一律
manual_dispatch(人工)。
而 kuaishou-91 来源(91 回调 + 快手 send-code 两条数据通道)是三条路:皮肤自动发货 / 接单平台自动发单(按接单模板建工单给打手) / 人工兜底。本次目标:让抖音复用同一套分类,把中间那层补上。
二、kuaishou-91 现在的三层机制(代码事实)
flowchart TD
A["订单项(upsertOrderFromSource)"] --> B["① 履约路由<br/>resolveFulfillmentRoute"]
A --> E["② 接单平台发单<br/>syncWorkerOrdersForSourceOrder(autoOnly)"]
B -->|"映射命中 / 规则命中 affiliate_dash"| C["executor=affiliate_dash<br/>皮肤自动发货"]
B -->|"规则指向 / 兜底"| D["executor=manual_dispatch"]
E -->|"接单模板命中且 auto_create"| F["建工单 → 打手接单"]
E -->|"未命中"| G["不建工单"]
D --> H["③ 人工处理"]
G --> H
层① 履约路由(决定「谁发货」)
| 事实 | 位置 |
|---|---|
可路由执行器只有两个:affiliate_dash / manual_dispatch |
apps/backend/src/services/fulfillment/routing-config-service.ts:76-81 |
| 判定顺序:指定匹配(preferred)→ 商品路由规则(商品名 / 内部SKU)→ 全局优先级 → 未命中兜底 | 同上 :144-253 |
| preferred 一旦可用就直接返回,规则层不再执行 | 同上 :168-182 |
manual_dispatch 不需要候选即可被选中(天然兜底) |
同上 :320-328 |
| 配置入口(后台):「履约配置」页 → 商品路由规则 | apps/frontend/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx:47-50 |
生产库实测(2026-10-14):rules: [](商品路由规则一条都没配)、unmatchedExecutorKey: ""、defaultExecutorPriority: ["affiliate_dash"] |
app_config_entries.fulfillment_routing |
因为 unmatchedExecutorKey 是空串,91 未命中映射时不建履约任务(isConfigured=false,订单停在「待配置」) |
apps/backend/src/services/fulfillment/product-resolution-service.ts:217 |
生产库验证:有工单的 91 订单 2000 个样本中,with_tasks = 0 → 工单单没有履约任务 |
work_orders × fulfillment_tasks 只读核对 |
层② 接单平台发单(决定「要不要找打手」)
| 事实 | 位置 |
|---|---|
建单后无条件对所有 provider 调用发单同步(autoOnly: true) |
apps/backend/src/services/order/order-service.ts:211、:241 |
发单主流程:逐订单项匹配模板 → 命中且 auto_create → 建工单 |
apps/backend/src/services/worker-platform/sync-worker-orders-service.ts:37、:119-226 |
匹配有两条路径:① 商品绑定(work_product_rule_mappings)② 旧规则(rule.product_name / rule.sku_code 对 item.sku_name / item.sku_code) |
apps/backend/src/services/worker-platform/work-product-match-mappers.ts:100-107 |
路径①要求 seller_ids_json 命中(DB CHECK 强制非空数组)→ 抖音没有 sellerId,这条路径永不命中 |
同上 :171-185;约束 work_product_rule_mappings_seller_ids_json_check |
| 路径②是「无 sellerId 上下文」时唯一可用的匹配 | 同上 :238-344 |
| provider/platform 过滤:规则留空=通配;只有 PDD 有「复用快手模板」豁免 | 同上 :346-372 |
| 配置入口(后台):「接单平台」页 → 接单模板(provider 下拉已含「抖音码速达」) | apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx、apps/frontend/src/utils/admin-options.ts:12 |
层③ 人工
manual_dispatch 执行器把任务置 MANUAL_REVIEW 并通知(apps/backend/src/services/fulfillment/executors/manual-executor.ts:16-45)。
关键结论:层① 与层② 是同一个单子的两种执行载体(业务上严格互斥)
⚠️ 先纠正一个容易搞错的关系:91 回调、快手 send-code、工单不是三条独立的线,本质是一个单子。前两者是同一个 kuaishou-91 来源的两条数据通道,工单只是这个单子的执行载体之一。
sync-work-orders-from-send-code.ts:5-6 的注释就是权威口径:「91 回调是订单骨架权威源,快手 send-code 是数据补全源与兜底源」。
一个单子的构成(生产库实测):
| 组成 | 说明 | 生产库 |
|---|---|---|
orders + order_items |
订单骨架(91 回调权威 + 快手 send-code 补全/兜底) | 329,260 |
kuaishou_industry_vouchers |
买家侧快手电子凭证(按 oid 关联) |
329,219(≈100%) |
| 执行载体(二选一) | 工单(打手交付) 或 fulfillment_tasks(affiliate_dash 自动发货) |
两者交集 0 |
| 凭证归属 | 工单自带凭证:work_orders.material_json.kuaishouIndustryVoucher[](voucherCode / token / status) |
— |
- 所以「工单」不是独立于订单的第三条线,而是这个单子的执行载体之一;
- 代码层没有「二选一」的守卫(
syncWorkerOrdersForSourceOrder独立运行),业务上的严格互斥由「商品集合 + 凭证归属」保证(实测 0 / 329,260); - 抖音改造要对齐的是这个模型:一个单子 = 骨架 + 买家侧凭证 + 一个执行载体(抖音的凭证就是 DYC 券 +
coupons.syncV2)。
三、生产库实测:模板现状与抖音实际结果
3.1 接单模板(662 条,全部 enabled / auto_create=true)
| 维度 | 实测 |
|---|---|
provider |
全部为空(662/662) |
platform |
kuaishou 354 条、空 308 条(不是全部留空) |
shop_id |
全部为空(662/662)→ 不锁店铺 |
match_json |
只有 4 条非空(全是 kuaishou) |
sku_code |
只有 6 条非空(全是 kuaishou) |
| 商品绑定 | work_product_rule_mappings 662 条(1 模板 1 绑定),但绑定要求 sellerId |
| 商品名覆盖 | 含「和平精英」的模板 0 条 |
结论:模板匹配实际只靠 product_name。 而抖音未命中皮肤映射的商品,其 sku_name 是引流长标题,product_name contains 基本匹配不上。
3.2 抖音单实际结果(生产库,全量 11 笔)
| 指标 | 值 |
|---|---|
orders(provider=douyin_mashida) |
11 |
order_items |
11 |
work_product_match_logs.match_status = unmatched |
11(全部) |
| 已建工单 | 0 |
逐笔现状(含分层归属):
| 落库 sku_code | 落库 sku_name | 笔数 | 归属 |
|---|---|---|---|
lucky_coin_x2 |
幸运币2个 | 3 | 层① 自动发货 ✅ |
suit_romantic_destiny |
套装-浪漫天命 | 2 | 层① 自动发货 ✅ |
套装-牛仔情诗 |
和平精英皮肤小店入口返场集市新皮肤出售牛仔情诗 | 2 | 层③ 人工(无模板) |
套装-狂战萌兔 |
和平精英皮肤小店入口出售返场吃鸡皮肤狂战萌兔 | 2 | 层③ 人工(无模板) |
浪漫天命 |
和平精英皮肤小店入口出售返场吃鸡皮肤浪漫天命 | 1 | 层③ 人工(映射表写法不一致) |
突击兵密钥5个 |
和平精英指挥官密钥出售隐藏款密钥突击兵密钥碎梦浮光礼盒系列 | 1 | 层③ 人工(本可命中模板,被 platform 挡住) |
模拟「放行 platform=kuaishou 模板」:11 笔中只有 1 笔能命中 —— 突击兵密钥5个 → rule 21「突击兵」(kuaishou / contains)。其余 10 笔在 662 个模板里没有任何对应商品,必须新建模板。
3.3 抖音落库口径(决定「用什么键去匹配」)
resolveOrderItemForFulfillment 在 selectedExecutorKey === affiliate_dash 时会把 skuCode 换成英文 sku:
| 商品类型 | 落库 sku_code |
落库 sku_name |
|---|---|---|
| 命中皮肤映射(层①) | 英文 sku(lucky_coin_x2) |
中文内部 SKU(幸运币2个) |
| 未命中映射(层②/③) | 内部 SKU(套装-牛仔情诗) |
引流长标题 |
由此得出一条硬性设计结论:层② 的匹配键只能是「内部 SKU」,即未命中映射那批的 item.sku_code(item.sku_name 是引流标题,不可作为匹配键)。
四、目标设计
4.1 判定顺序
订单项(provider=douyin_mashida)
├─ ① 内部 SKU 命中 AFFILIATE_DASH_SKU_MAPPING(含后台增量映射)
│ → preferred = affiliate_dash(皮肤自动发货,现状不变)
│ → 且【不参与接单平台发单】(防重复交付,见 4.3)
├─ ② 否则交给 resolveFulfillmentRoute(不再写死 preferred)
│ → 商品路由规则(商品名 / 内部SKU)→ 全局优先级 → 兜底
│ → 命中 affiliate_dash 时才自动发货;否则 executor=manual_dispatch
├─ ③ 接单平台发单(独立层,与 ①② 并行判断)
│ → 接单模板命中且 auto_create → 建工单给打手
└─ ④ 以上都不中 → 人工(MANUAL_REVIEW)
要点:层①/② 决定「谁发货」,层③ 决定「要不要找打手」,与 91 保持同一套语义。
4.2 与 91 对齐的两处
- 抖音不再是「硬二分」,而是参与同一套履约路由;
- 抖音的皮肤映射支持后台增量(与 91 一致),新增商品不必改代码。
4.3 防重复交付(新增硬校验)
91 靠「商品集合不重叠」侥幸避免「既建工单又自动发货」。抖音商品命名混乱(同一商品出现「套装-浪漫天命」与「浪漫天命」两种写法),必须加显式校验:
该订单项命中皮肤映射,或已存在
executor_key='affiliate_dash'的履约任务 → 跳过建工单。
判据实现位置建议:sync-worker-orders-service.ts 的订单项循环内、createWorkOrder 之前。
判定方式:matchAffiliateDashSku(sku_name) || matchAffiliateDashSku(sku_code)(命中映射的商品,中文内部 SKU 在 sku_name 上),或直接查该 order_item_id 的 affiliate_dash 任务是否存在。
4.4 开关与灰度
建议在抖音配置(app_config_entries.douyin_mashida)新增布尔项:
| 字段 | 默认 | 作用 |
|---|---|---|
workerDispatchEnabled |
false | 抖音单是否参与接单平台发单(层③)。关闭时行为与现在完全一致 |
理由:层③ 会真实派出工单、产生打手赏金,属于业务口径变化,必须可灰度、可一键回退。
4.5 与 91 的第三处差异:抖音现在会「强行建人工任务」
| 维度 | 91(现状) | 抖音(现状) |
|---|---|---|
| 未命中映射时 | unmatchedExecutorKey="" → 不建履约任务,订单停在「待配置」;若接单模板命中则建工单 |
分支写死 preferredExecutorKey=manual_dispatch → 必建 manual_dispatch 任务(manual_review) |
| 生产数据 | 有工单的 91 订单 2000 个样本:with_tasks = 0(纯工单,无任务) |
11 笔:5 笔 affiliate_dash(3 link_generated / 2 redeemed)+ 6 笔 manual_dispatch / manual_review |
→ 放行接单平台发单后,抖音会出现**「人工任务 + 工单」并存**,与 91 的「纯工单」语义不一致。要么接受(人工复核多一步),要么把抖音未命中时的兜底改成「不建任务、订单进待配置」。已列入 §八 未决问题。
五、改动清单(文件级)
✅ 实施状态(2026-10-14):第 1、2、3、4 项已落地并测试通过;2026-10-17 复核后端全量 740 例(737 pass / 3 skipped)。第 7、8、9、10 项待实施。详见 12-按SKU分类改造总设计.md「§1.1 已实施 / §1.2 待实施」。
- 第 4 项的实际落法:抖音未命中时不再兜底
manual_dispatch(覆盖unmatchedExecutorKey),因此「未命中 = 无执行载体」,由发单层先判接单模板 → 命中即工单、都不中即待人工;载体唯一由snapshot.classification.channel === 'affiliate_dash'守卫。
| # | 文件 | 改动 | 风险 |
|---|---|---|---|
| 1 | apps/backend/src/services/fulfillment/product-resolution-service.ts:255-317 |
抖音分支去掉写死的 preferredExecutorKey:映射命中才置 affiliate_dash,未命中交给规则/优先级/兜底(候选保留 affiliate_dash + manual_dispatch) |
低。默认行为不变(无规则时仍回落人工) |
| 2 | 同上 :323-329 |
matchDouyinMashidaAffiliateDashSku 改为「常量表 + mergeAffiliateDashSkuMapping(config.skuMapping)」,与 91 对齐(可选英文透传) |
低 |
| 3 | apps/backend/src/services/worker-platform/work-product-match-mappers.ts:346-372 |
matchesRuleProviderPlatform 对抖音放行(对齐 PDD 的复用豁免),使抖音可命中 platform=kuaishou 的 354 条模板 |
中:会真实触发发单,需与开关同时上 |
| 4 | apps/backend/src/services/worker-platform/sync-worker-orders-service.ts(循环内) |
① 抖音 workerDispatchEnabled=false 直接跳过;② 命中皮肤映射 / 已有 affiliate_dash 任务 → 跳过建工单 |
低(纯守卫) |
| 5 | apps/backend/src/services/platforms/douyin-mashida/source-config-service.ts |
新增 workerDispatchEnabled 字段 + normalize + 默认 false |
低 |
| 6 | apps/backend/src/services/admin/platform-config/douyin-mashida-service.ts + apps/frontend/src/types/admin/platform-config/douyin-mashida.ts |
配置出参 / 类型透出 | 低 |
| 7 | apps/frontend/src/pages/admin/platform/DouyinMashidaPanel.tsx |
新增「参与接单平台发单」开关 + 说明文案 | 低 |
| 8 | 文档 | 本文件 + 11-接单模板待补清单.md |
— |
只做 1、2、3、4 的守卫部分(不加开关)也能跑,但不建议:层③ 一旦放行会立即对存量抖音单重跑匹配。
六、风险与对策
| 风险 | 说明 | 对策 |
|---|---|---|
| 误派单 | 模板用 contains 短名(如「突击兵」)会命中引流长标题 |
优先按 sku_code(内部SKU,exact)配模板;短名模板改成 exact |
| 重复交付 | 模板命中已自动发货商品 → 同时建工单 + 发货任务 | 4.3 的硬校验(必须做) |
| 存量单被补发 | 放行后历史 unmatched 的抖音单可能被重跑匹配建单 | 开关默认关;放行前后用只读 SQL 对比工单数 |
| 打手侧口径不符 | 模板的材料字段(requirement_json.fields)是快手/PDD 口径,抖音单未必有对应素材 |
新建抖音模板时单独确认字段 |
| 引流标题不可靠 | sku_name 是运营随手写的标题(同一商品两种写法已实测) |
统一以 sku_code(内部SKU)为准,并把命名规范回推到抖音侧商品配置 |
七、验证方法
7.1 只读 SQL(生产库核对,勿写)
-- 1) 抖音单是否已发单
SELECT count(*) AS douyin_work_orders
FROM work_orders w JOIN orders o ON o.id = w.order_id
WHERE o.provider = 'douyin_mashida';
-- 2) 抖音单匹配结论分布
SELECT l.match_status, count(*)
FROM work_product_match_logs l JOIN orders o ON o.id = l.order_id
WHERE o.provider = 'douyin_mashida'
GROUP BY 1;
-- 3) 模板平台分布(确认放行范围)
SELECT platform, count(*) FROM work_product_rules WHERE enabled GROUP BY 1;
-- 4) 模拟某个内部 SKU 放行后能命中哪些模板(:sku 换成抖音内部 SKU)
SELECT id, platform, product_name, match_type
FROM work_product_rules
WHERE enabled AND (
(match_type = 'exact' AND :sku = product_name) OR
(match_type <> 'exact' AND :sku LIKE '%' || product_name || '%')
);
7.2 单测范围
| 用例 | 期望 |
|---|---|
| 抖音内部 SKU 命中映射 + 未命中映射 两种 | selectedExecutorKey 分别为 affiliate_dash / manual_dispatch(无规则时) |
抖音商品路由规则(internal_sku 维度)命中 |
规则生效(现在被 preferred 短路,改后应生效) |
抖音单 + workerDispatchEnabled=false |
不建工单 |
| 抖音单 + 命中皮肤映射 | 不建工单(防重复交付) |
| 抖音单 + 命中模板 + 开关开 | 建工单,match_status=matched |
八、未决问题
- 放行范围:抖音是否允许直接命中
platform=kuaishou的 354 条模板?还是要求运营复制一份platform留空/抖音的模板(数据更干净,但要人工维护 662→增量)? - 奖励与材料:抖音模板的接单金额、材料字段(
requirement_json)是否与快手同口径?打手端是否需要展示买家留言/收件人(抖音单的 buyer 信息在detail.buyer_words/post_receiver)? - 映射命中但准备失败:命中皮肤映射但
affiliate_dash下单失败(余额不足等)时,是否允许回落层③发单?还是只进人工? - 商品命名规范:抖音侧内部 SKU 命名(「套装-浪漫天命」vs「浪漫天命」)是否统一?这是映射漏配的根因之一。
- 存量补单:放行后是否要对历史 11 笔(尤其 6 笔人工单)重跑匹配?建议只对新单生效,历史单人工处理。
- 抖音未命中时还要不要建
manual_dispatch任务(见 §4.5)?91 是「不建任务、订单进待配置」,抖音目前是「一定建人工任务」。 - 接单模板的 platform 限制已清空(2026-10-14 生产库执行,见
backups/douyin-platform-clear-20261014/):354 条原platform=kuaishou已改为留空 → 抖音单现在就能命中这些模板(新单生效,历史单不回溯)。是否接受「用清空 platform 代替代码放行」这一取舍,需确认。
已执行变更记录:354 条
work_product_rules.platform由kuaishou清空(事务内 UPDATE,updated_rows=354,事后platform <> ''计数为 0)。原值备份在backups/douyin-platform-clear-20261014/。