Files
order_site/docs/抖音-码速达/10-三层分流设计.md
T
yml2213 10464fea89 refactor(product-match): 抽离接单模板匹配引擎(来源适配器/引擎/价格收口 + 后台按订单项试匹配)
新增 services/product-match:来源适配器(kuaishou-91 合成 91+快手 / pdd / douyin-mashida)+ 平台无关匹配上下文 + 条件打分引擎(并列最高=ambiguous 不猜、逐维诊断)+ 价格数量拼单唯一实现。

- 商店键命名空间化(kuaishou:/pdd:/douyin:),店铺为可选作用域:留空即可按订单信息匹配
- 抖音匹配文本只取内部 SKU,引流标题只进 promoTitle,itemTitle 恒空(修复 0.85 元误命中碎梦浮光珍宝礼盒、拼单未开)
- work-product-match-mappers 改为门面(签名不变),work-order-pricing 改为转发
- 新增后台「按订单项试匹配」接口与面板:走发单同一套链路,返回模板/数量/单价/总额/拼单
- 生产样本回放:91/PDD 6000 条决策 0 差异;抖音 5 条全为修正

设计见 docs/接单模板匹配引擎重构设计.md
2026-10-14 15:58:21 +08:00

18 KiB
Raw Blame History

抖音码速达 · 三层分流设计(皮肤自动发货 / 接单平台 / 人工)

状态:设计稿,未实施(2026-10-14) 依据:生产库只读实测(接单模板 662 条)+ 全链路代码走查 关联:09-皮肤领取流程.md、11-接单模板待补清单.md


一、要解决的问题

抖音现在只有两条路:

  1. 内部 SKU 命中 AFFILIATE_DASH_SKU_MAPPING → affiliate_dash 皮肤自动发货;
  2. 其余 → 一律 manual_dispatch(人工)。

而 91/快手 是三条路:皮肤自动发货 / 接单平台自动发单(按接单模板建工单给打手) / 人工兜底。本次目标:让抖音复用同一套分类,把中间那层补上。


二、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 单、快手、工单不是三条独立的线,本质是一个单子。

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 对齐的两处

  1. 抖音不再是「硬二分」,而是参与同一套履约路由;
  2. 抖音的皮肤映射支持后台增量(与 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 项已落地并测试通过(类型检查 + 后端全量 712 例);第 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

八、未决问题

  1. 放行范围:抖音是否允许直接命中 platform=kuaishou 的 354 条模板?还是要求运营复制一份 platform 留空/抖音的模板(数据更干净,但要人工维护 662→增量)?
  2. 奖励与材料:抖音模板的接单金额、材料字段(requirement_json)是否与快手同口径?打手端是否需要展示买家留言/收件人(抖音单的 buyer 信息在 detail.buyer_words / post_receiver)?
  3. 映射命中但准备失败:命中皮肤映射但 affiliate_dash 下单失败(余额不足等)时,是否允许回落层③发单?还是只进人工?
  4. 商品命名规范:抖音侧内部 SKU 命名(「套装-浪漫天命」vs「浪漫天命」)是否统一?这是映射漏配的根因之一。
  5. 存量补单:放行后是否要对历史 11 笔(尤其 6 笔人工单)重跑匹配?建议只对新单生效,历史单人工处理。
  6. 抖音未命中时还要不要建 manual_dispatch 任务(见 §4.5)?91 是「不建任务、订单进待配置」,抖音目前是「一定建人工任务」。
  7. 接单模板的 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/。