Files
order_site/docs/抖音-码速达/12-按SKU分类改造总设计.md
T

28 KiB
Raw Blame History

抖音码速达 · 按 SKU 分类改造总设计(对齐 kuaishou-91 三层)

状态:抖音侧已实施(2026-10-14);P2 / P3 后续项未实施。最后校准:2026-10-18(main @ 7542d547;仅基线更新,业务逻辑未变)。 依据:全链路代码走查 + 生产库只读实测(模板 662 条 / kuaishou-91 订单 32.9 万 / 抖音 11 笔) 前置:10-三层分流设计.md、11-接单模板待补清单.md 前提:抖音侧实现可自由重构(无历史兼容包袱);91 回调与快手 send-code 是同一个 kuaishou-91 来源的两条数据通道,不拆成两个来源


一、目标与结论摘要

目标:抖音与 kuaishou-91 用同一套「按 SKU 分类」逻辑,产出三种结果:

层 触发条件 产物
① 皮肤/道具自动发货 SKU 命中 affiliate_dash 映射表 affiliate_dash 履约任务 + 领取链接
② 接单平台发单 SKU 命中接单模板(work_product_rules) 工单(打手抢单),不建履约任务
③ 人工 以上都不命中 不建任务、不建工单,进「待人工」队列

⚠️ 这三层不是三条独立业务线,而是同一个单子选择哪种执行载体(见 §2.1「一个单子的构成」):一个单子 = 订单骨架 + 买家侧凭证 + 一个执行载体。

结论(需要改的不是「加一层」,而是三件事):

  1. 分类键不稳定:现在 SKU 命中自动发货后,order_items.sku_code 会被覆写成英文 sku,导致「按 SKU 分类」失去稳定依据 → 必须确立规范化的 canonical SKU 并保证不被覆写;
  2. 抖音绕过了分类框架:抖音分支写死 preferredExecutorKey,既不认履约路由规则,也未命中时强制建 manual_dispatch 人工任务(91 是「不建任务」);
  3. 人工层没有载体:91 的「待配置队列」链路实际已成死代码(见 §3.4),人工单只是「裸单」;抖音连裸单标记都没有 → 需要一个显式的「待人工」状态/队列。

1.1 已实施(2026-10-14,抖音侧)

# 改动 位置
1 抖音 sku_code 恒为内部 SKU(不再被英文 sku 覆写);英文 sku 落 snapshot.affiliateDash.sku;引流标题落 snapshot.productTitle apps/backend/src/services/fulfillment/product-resolution-service.ts(resolveOrderItemForFulfillment)
2 抖音分级判定重写:命中映射 → affiliate_dash;未命中 → 不设 preferred、且不兜底 manual_dispatch → 无执行载体 同上(resolveDouyinMashidaItemCandidate)
3 映射来源改为「权威常量表 + 后台 skuMapping 增量」(新增商品不必发版) 同上(matchDouyinMashidaAffiliateDashSku)
4 写 snapshot.classification = { channel, canonicalSku, matchMode } 同上
5 载体唯一守卫:分类为 affiliate_dash 的订单项不再建工单 apps/backend/src/services/worker-platform/sync-worker-orders-service.ts
6 券 cert_link 按基址形态决定(/h5 一律按订单号拼;其它形态有 token 用 token、否则回退 certLink)——不再依赖是否命中自动发货 apps/backend/src/services/redeem/certificate-service.ts
7 抖音券写入工单 material_json.douyinMashidaVoucher(对齐快手把凭证挂工单):① 发券后回填;② 建工单时券已存在则直接带上(覆盖「补模板后重新匹配」) 同上 + apps/backend/src/services/worker-platform/sync-worker-orders-service.ts

验证(2026-10-14 当时):npm run typecheck 通过;后端全量测试 717 例 / 714 通过 / 0 失败 / 3 跳过;本次新增 16 例(分类判定、sku_code 不覆写、券链接 5 例、券回填 4 例、载体守卫 3 例、91 回归)。2026-10-17 复核为 740 项(737 pass / 3 skipped)。

1.2 业务口径(2026-10-14 确认)与后续

项 决定 落法
A 「待人工」队列 / 后台筛选 测试阶段不做,人工直接处理 未命中单 = 无任务无工单,维持现状
B 券的发放 任何订单都要发券,与是否命中无关 发券逻辑不动(所有抖音单都发券 + syncV2);cert_link 也解耦命中判据(见 §1.1 第 6 项)
C 券 → 工单 写入工单 material_json.douyinMashidaVoucher 发券后回填 + 建单时带入,两条路径都覆盖

仍需运营/产品:抖音内部 SKU 命名规范 + 按 SKU 补接单模板(见 11-接单模板待补清单.md)。


二、现状全链路梳理

2.1 kuaishou-91 全链路

flowchart TD
  A["91 下单 API(验签+参数校验)"] --> B["buildOpen91SourceEvent<br/>provider=91kaquan / platform=kuaishou<br/>skuCode=中文商品名(productNo 去 ----店铺 后缀,后续会被覆写)"]
  B --> C["upsertOrderFromSource"]
  C --> D["resolveOrderItemForFulfillment<br/>分类"]
  D -->|"命中 AFFILIATE_DASH_SKU_MAPPING"| E["isConfigured=true → 建 affiliate_dash 任务<br/>requires_claim → claim token → 领取页"]
  D -->|"未命中(rules 空 + unmatched 空串)"| F["isConfigured=false → 不建任何任务"]
  C --> G["syncWorkerOrdersForSourceOrder(autoOnly)<br/>按 work_product_rules 匹配"]
  G -->|命中且 auto_create| H["建工单 → 打手抢单"]
  G -->|未命中| I["不建工单"]
  F --> J["有买家侧凭证但无执行载体<br/>(无工单无任务),order_status=paid"]
环节 事实 位置
进单 createOpen91Order:验签 → 号码校验 → buildOpen91SourceEvent apps/backend/src/services/open-91/order-create-service.ts:26、apps/backend/src/services/platforms/ninetyone/order-service.ts:28
SKU 口径 分类键 = 商品名(productNo 去 ----<shopId> 后缀);sku_code 有三种形态、不可作分类键(见 §3.3) ninetyone/order-service.ts:62、parseOpen91ProductNo:91
分类 规则表为空、unmatchedExecutorKey=''、优先级 [affiliate_dash] → 只有映射命中才有 executor app_config_entries.fulfillment_routing(生产库实测)
任务 仅 configuredOrderItems(即命中映射的项)建任务;affiliate_dash 走 requires_claim=true services/order/order-service.ts:233-236、services/order/delivery-task-service.ts:55
发单 无条件对全部订单项跑接单模板匹配(autoOnly:true) services/order/order-service.ts:211、:241
电子凭证门禁 用 oid=platform_order_id 查快手电子凭证;未确认 → 暂停建任务,但工单照建 services/fulfillment/order-fulfillment-readiness-service.ts:17
领取 仅 ① 层有 claim token → 领取页(输 UID → 绑定 → 发货) services/claim/claim-service.ts:152
后台 91 平台配置页有「待配置」列表 + 重试/标记失败 services/platforms/ninetyone/order-service.ts:199(retryOpen91Order)、:222(failOpen91Order)

生产库实测(2026-10-14):

指标 数量
91 订单 329,260
└ 有快手电子凭证 329,219(≈100%)
└ affiliate_dash 任务 46,103(另 kuaishou_ct_assisted 46,894 / kuaishou_feifei 7,007)
└ 接单平台工单 126,537
└ 工单 ∩ 任务 0(严格互斥)
└ 既无任务也无工单 102,674(8 月 46,329 / 9 月 37,713 / 10 月 18,631;近 10 天约 1,000/天)
后者的 order_status 102,648 = paid,25 = pending_config,1 = manual_failed
后者的快照 isConfigured 近 10 天 10,208 项全部 false

一个单子的构成(关键认知)

91 回调与快手 send-code 是同一个 kuaishou-91 来源的两条数据通道,工单不是独立的第三条线——sync-work-orders-from-send-code.ts:5-6 注释即权威口径:「91 回调是订单骨架权威源,快手 send-code 是数据补全源与兜底源」。两者共享同一店铺键空间与同一套匹配上下文,不是两个来源。

组成 说明
orders + order_items 订单骨架(91 回调权威 + 快手 send-code 补全/兜底)
kuaishou_industry_vouchers 买家侧快手电子凭证(按 oid 关联;≈100% 订单都有)
执行载体(二选一,实测交集 0) 工单(打手交付) 或 fulfillment_tasks(affiliate_dash 自动发货)
凭证归属 工单自带凭证:work_orders.material_json.kuaishouIndustryVoucher[](voucherCode / token / status / 有效期)

→ 因此那 102,674 笔的准确说法是:有买家侧凭证,但既没派打手、也没走自动发货的裸单(需要显式人工队列,见 §4.3),而不是「另一条业务线」。

2.2 抖音 全链路(现状)

flowchart TD
  A["trade_TradePaid 推送"] --> B["验签 + webhook_events 幂等"]
  B --> C["order.orderDetail 拉详情"]
  C --> D["buildDouyinMashidaSourceEvent<br/>skuCode=内部SKU(code) / skuName=引流长标题"]
  D --> E["upsertOrderFromSource"]
  E --> F["resolveOrderItemForFulfillment 抖音分支"]
  F -->|"命中常量映射"| G["affiliate_dash 任务<br/>(skuCode 被覆写为英文 sku)"]
  F -->|"未命中"| H["**强制** manual_dispatch 任务<br/>manual_review(无人工队列)"]
  E --> I["syncWorkerOrdersForSourceOrder(autoOnly)"]
  I --> J["实测 11 笔全 unmatched → 0 工单"]
  E --> K["每个订单项都发 DYC 券 + coupons.syncV2"]
  G --> L["付款后私信领取链接(仅①层)"]
  H --> M["券 cert_link 兜底指 /h5"]
环节 事实 位置
进单 推送 → orderDetail 补齐 → upsertOrderFromSource services/platforms/douyin-mashida/notify-service.ts:112-229
SKU 口径 skuCode = 内部 SKU(code/out_sku_id/product_id),skuName = 引流长标题 douyin-mashida/order-mapper.ts:151-165
分类 抖音分支写死 preferredExecutorKey:命中映射→affiliate_dash,否则→manual_dispatch;候选只有这两个 services/fulfillment/product-resolution-service.ts:255-317
映射来源 只查代码常量表 AFFILIATE_DASH_SKU_MAPPING(不走后台增量、无英文透传) 同上 :323-329
sku_code 污染 选中 affiliate_dash 后 skuCode 被替换为英文 sku,skuName 被替换为中文内部 SKU 同上 :89-102
发单 与其他 provider 同样调用,但被 platform 过滤挡住 work-product-match-mappers.ts:346-372(已于 2026-10-14 清空 platform,解除)
券码 所有订单项都发券 + syncV2;cert_link 优先 claim 链接、否则 certLink(含 /h5 捷径) services/redeem/certificate-service.ts:61、:137-180
核销 领取完成 best-effort verifyV3;/redeem 支持手动输订单号核销 services/claim/kuaishou-cloud-claim-service.ts:529、services/redeem/redeem-service.ts:53

生产库实测(11 笔):命中映射 5 笔(3 link_generated / 2 redeemed,均为 lucky_coin_x2、suit_romantic_destiny)+ 未命中 6 笔(manual_dispatch / manual_review);接单模板匹配 11 笔全 unmatched、0 工单。

2.3 逐项差异对照

维度 kuaishou-91 抖音(现状) 改造方向
分类键 中文商品名(productNo) 内部 SKU(code) 统一为规范化 canonical SKU
sku_code 落库 命中映射后被覆写成英文 同左(未命中时才是内部 SKU) 不再覆写,英文 sku 只落 snapshot
分类框架 走 resolveFulfillmentRoute(规则+优先级+unmatched) 写死 preferred,绕过框架 复用同一框架
未命中兜底 不建任务(裸单) 强制建 manual_dispatch 任务 显式「待人工」,不建任务
接单平台 命中 126,537 工单 0(已清空 platform,待新单验证) 按 SKU 对齐商品名/编码
人工队列 有 UI 但链路已成死代码 无 新增显式状态 + 队列 + 重试
券码 同一个单子的买家侧快手电子凭证(挂在任务/工单上,≈100% 订单都有) 自有 redeem_certificates + syncV2(等价于抖音版买家侧凭证) 按分类决定是否发券 / 链接形态
领取链接 命中映射的才有 同左(人工单已修不再误发) 保持
电子凭证门禁 有(发码回调未确认先暂停) 无(抖音无此概念) 不适用

三、生产库关键事实(改造的前提)

3.1 接单模板(662 条)

维度 实测
provider 全部为空(通配)
platform 原 354 条 kuaishou,已于 2026-10-14 清空(现 662 条全通配)
shop_id 全部为空
sku_code 仅 6 条有值(全是原 kuaishou 批次)
match_json 仅 4 条有值
商品绑定 662 条(1:1),但 seller_ids_json 有非空 CHECK → 抖音无 sellerId → 这条路径对抖音永远不可用

→ 抖音的模板匹配只能走「规则自身的 sku_code / product_name」(work-product-match-mappers.ts:303-343)。

3.2 履约路由配置(生产库)

{ "rules": [], "enabled": true,
  "executors": { "affiliate_dash": {"enabled": true}, "manual_dispatch": {"enabled": true} },
  "unmatchedExecutorKey": "", "defaultExecutorPriority": ["affiliate_dash"] }

→ 91 未命中时选不出执行器(isConfigured=false)→ 不建任务。抖音若照抄此配置语义,就天然等于「未命中不建任务」。

3.3 落库口径(决定匹配键)

91(生产库实测,329,248 个订单项) —— sku_code 有三种形态,不可作为分类键:

商品类型 sku_code sku_name
命中映射(①层) 英文 sku(suit_romantic_destiny,如 330 笔) 中文商品名(套装-浪漫天命)
接口直接进单(走工单) 91 productNo(中文商品名) 中文商品名
被快手 send-code 补全后 平台数字 ID(189873094508762,占 244,322/329,248 = 74%) 中文商品名(1个精致荣耀专属礼盒)

抖音:

商品类型 sku_code sku_name
命中映射(①层) 英文 sku(lucky_coin_x2) 中文内部 SKU(幸运币2个)
未命中(②③层) 内部 SKU(套装-牛仔情诗) 引流长标题(和平精英皮肤小店入口…)

→ 结论:

  • 91 的分类键只能是「商品名」(sku_name / productNo)——这正是 662 条接单模板全部用 product_name 匹配的原因;
  • 抖音的分类键是「内部 SKU」(未命中映射时落在 sku_code 上);
  • 所以 canonical SKU 必须按来源分别取,不能一律取 sku_code;抖音「命中后被覆写」的问题必须修掉。

3.4 91 的「待配置队列」是死代码(重要)

  • 类型里有 UpsertOrderIgnoredResult(order-service.ts:63-69),但运行时从不返回 ignored: true(全仓库仅出现在该类型定义处);
  • createOpen91Order 的 if (result.ignored) → upsertOpen91PendingOrder()(services/open-91/order-create-service.ts:63)因此永不触发;
  • 生产库仅 25 笔 pending_config(全部 2026-08-08~08-11,早期版本产物);
  • 结论:91 未命中的订单现在既不建任务、也不进队列,只是 paid 状态的裸单(102,674 笔)。

→ 抖音改造时不要复用这套死链路,应新建显式的人工状态与队列。


四、目标模型:按 SKU 的三层分类

4.1 统一分类键(canonical SKU)

canonicalSku(按来源分别取,禁止一律取 sku_code)
  91   :商品名 —— sku_name / productNo(如「1个精英荣耀专属礼盒」「套装-浪漫天命」)
  抖音 :内部 SKU —— sku_order_list[].code(如「幸运币2个」「套装-牛仔情诗」)
  归一化:NFKC → 去所有空白 → 小写(复用 normalizeInternalSkuMatchKey)

落库约定(核心改动):

字段 约定
order_items.sku_code 仅抖音:恒等于 canonicalSku(不再被执行器覆写);91 保持现状(sku_code 三种形态见 §3.3,其分类键取 sku_name)
order_items.sku_name 可读商品名(抖音取内部 SKU;引流标题另存 snapshot.productTitle)
order_items.item_snapshot_json.classification 新增:{ channel, canonicalSku, matchedRuleId, matchReason, decidedAt }
item_snapshot_json.affiliateDash.sku 英文 sku 只落这里(+ resolvedSkuCode)

4.2 判定顺序(抖音与 91 共用)

输入:orderItem(canonicalSku, 商品名)

① 皮肤/道具自动发货
   canonicalSku 命中 AFFILIATE_DASH_SKU_MAPPING(常量表 + 后台 skuMapping 增量)
   → executor=affiliate_dash;requires_claim;建 claim token;付款后发领取链接
   → 明确标记:不再参与接单平台发单(防重复交付)

② 接单平台发单
   canonicalSku / 商品名 命中 work_product_rules(enabled + auto_create)
   → 建工单;**不建履约任务**;不发领取链接
   (是否发券/券链接形态见 §4.4)

③ 人工
   都不命中
   → isConfigured=false(不建任务);也不建工单
   → 标记 classification.channel='manual',进「待人工」队列(§4.3)

与 91 的对应关系:①②③ 完全同构;差别只在 ③ 由「裸单」升级为「显式待人工」。

4.3 人工层(③)的载体

设计点 方案
订单状态 不污染 order_status(保持真实 paid),用 order_items.snapshot.classification.channel='manual' 表达
队列判据 「无履约任务 且 无工单」= 待人工(可直接 SQL 筛,见 §7.3)
后台入口 订单列表/抖音配置页新增筛选「待人工」+ 单笔「重新匹配」按钮(复用 POST /api/v1/admin/worker-platform/orders/reprocess-matches)
重试语义 运营补好映射/模板后,重跑分类即可建任务或工单
通知 复用 notifyOpen91PendingConfig 类似机制给运营/群机器人告警(可选)

若产品希望与 91 的 pending_config 字面对齐,也可用 order_status='pending_config',但会污染真实支付状态,不推荐。

4.4 券码与领取页按分类分流(需产品决策)

层 是否发券给平台 cert_link 领取页
① 自动发货 是(现状) claim token 链接(现状) 输 UID → 绑定 → 发货
② 接单平台 决策项 建议:不发券或指「人工/客服」页 不做领取交互
③ 人工 决策项 同上 现状已显示「人工处理中,请等待客服」

现状问题:②③ 也会发券且 cert_link 走 /h5(certificate-service.ts:151 的 /h5 捷径),买家点进去看到「人工处理中」,属误导。


五、改造方案(抖音侧,可自由重构)

5.1 数据层:canonical SKU 不再被覆写

  • resolveOrderItemForFulfillment(product-resolution-service.ts:69-137):skuCode 不再随执行器替换;英文 sku 只写 snapshot.affiliateDash.sku 与 snapshot.resolvedSkuCode。
  • 只对抖音生效(91 现状不动,避免影响 32.9 万历史与线上逻辑):在函数内按 isDouyinMashidaOrder(provider, platform) 分支处理。
  • 影响面需同步检查:planner.buildFulfillmentTaskContext(用 skuCode/skuName)、工单 productName(用 sku_name)、redeem-service.buildSummary、后台展示。

5.2 分类判定:抖音接入统一框架

改写 resolveDouyinMashidaItemCandidate(product-resolution-service.ts:255-317):

// 伪代码
const canonicalSku = 内部SKU
const dashSku = matchAffiliateDashSku(mergeAffiliateDashSkuMapping(config.skuMapping), canonicalSku)
if (dashSku) {
  → candidates=[{affiliate_dash, available:true}], preferred=affiliate_dash
} else {
  → candidates=[{affiliate_dash, available:false}],**不设 preferred**
     // 让 resolveFulfillmentRoute 走规则/优先级/unmatched(当前配置下 unmatched='' → 不选执行器)
}
// 分类结果写 snapshot.classification

要点:

  • 不再写死 manual_dispatch → 未命中=不建任务(与 91 一致);
  • 顺带修好「后台履约路由规则对抖音无效」的问题(规则含 internal_sku 维度);
  • 映射查找改为「常量表 + 后台增量」(对齐 91,新增商品不必发版)。

5.3 接单平台层对齐

事项 做法
platform 限制 已执行:清空 354 条 platform=kuaishou(2026-10-14),抖音现在可命中
匹配键 运营按 sku_code = 内部 SKU + match_type=exact 建模板(见 11-接单模板待补清单.md)
provider 限制 已全为空(通配),无需改
代码侧 建议在 matchesRuleProviderPlatform 增加注释/白名单说明「抖音依赖 platform 留空」,避免后人又填回去
防重复交付(必做) 命中映射的订单项 → 跳过建工单(sync-worker-orders-service.ts 循环内加守卫,判据:matchAffiliateDashSku(sku_code) 或已存在 executor_key='affiliate_dash' 的任务)

5.4 人工层落地

  • order_items.snapshot.classification.channel='manual'(判定时写入);
  • 后台订单列表加筛选项「待人工(无任务无工单)」;
  • 单笔/批量「重新匹配」入口(复用现有 reprocess-matches);
  • 可选:运营告警(同 notifyOpen91PendingConfig)。

5.5 券码与领取页

  • ① 层:保持现状(有 token 才发领取链接、完成时自动核销);
  • ②③ 层:按 §4.4 的产品决策实施(建议不发券 or 链接指人工页),并去掉 certificate-service.ts:151 的 /h5 捷径(它是历史遗留,会让人工单拿到领取页链接)。

5.6 可观测

  • 分类结果落 snapshot.classification;
  • 日志统一前缀 [douyin-mashida/classify],含 canonicalSku / channel / matchedRuleId / reason;
  • 后台「匹配诊断」页已能看工单侧未命中原因,补充抖音分类结果展示。

5.7 存量数据

  • 不回溯历史 sku_code(已落库的英文 sku 保留);
  • 存量 11 笔抖音单(6 笔人工)人工处理;
  • 放行后只对新单生效。

六、改动清单(文件级)

# 文件 改动 风险
1 services/fulfillment/product-resolution-service.ts:69-137 抖音:skuCode 不再被覆写(canonical SKU 恒为内部 SKU) 中(需同步检查展示/任务/工单)
2 services/fulfillment/product-resolution-service.ts:255-317 抖音分类改写:不写死 preferred;未命中不选执行器;写 snapshot.classification 低
3 services/fulfillment/product-resolution-service.ts:323-329 抖音映射查找加后台增量(mergeAffiliateDashSkuMapping) 低
4 services/worker-platform/sync-worker-orders-service.ts(循环内) 防重复交付守卫:命中映射/已有 affiliate_dash 任务 → 跳过建工单 低(纯守卫)
5 services/worker-platform/work-product-match-mappers.ts:346-372 注释/白名单明确「抖音依赖 platform 留空」;可选按 sku_code 提升匹配优先级 低
6 services/order/order-service.ts 分类结果透传(classification 写入 snapshot 已完成在 #1/#2);确认未配置时不再返回误导性的 ignored 语义 低
7 services/platforms/douyin-mashida/source-config-service.ts 新增开关:workerDispatchEnabled(默认 false)、claimMessageEnabled 已有 低
8 services/redeem/certificate-service.ts:137-180 去掉 /h5 捷径;按 classification.channel 决定 cert_link(或是否发券) 中(影响买家链接)
9 后台(AdminOrdersPage / 抖音配置面板) 「待人工」筛选 + 重新匹配入口 + 分类结果展示 低
10 后台(AdminPlatformFulfillmentPage) 履约路由规则说明补充「抖音按内部 SKU 匹配」 低
11 文档 本文件 + 更新 10/11 号文档 —

七、测试计划

7.1 单测(新增/修改)

用例 期望
抖音 SKU 命中映射 sku_code = 内部 SKU(不被覆写);snapshot.affiliateDash.sku = 英文 sku
抖音 SKU 未命中映射 isConfigured=false → 不建任务(无 manual_dispatch)
抖音 SKU 命中接单模板 建工单,且不建履约任务
抖音 SKU 命中映射 + 同时命中模板 不建工单(防重复交付)
抖音履约路由规则(internal_sku)命中 规则生效(现在被 preferred 短路)
workerDispatchEnabled=false 不建工单
91 回归 现有行为完全不变(sku_code 覆写保留)

7.2 端到端(联调/灰度)

用 scripts/douyin-mashida-push.ts 造三类 SKU 的付款消息 → 校验:任务/工单/券/私信 四类产物与分类一致。

7.3 只读核对 SQL

-- 分类分布(改造后新单)
SELECT oi.item_snapshot_json::jsonb->'classification'->>'channel' AS channel, count(*)
FROM order_items oi JOIN orders o ON o.id = oi.order_id
WHERE o.provider = 'douyin_mashida' GROUP BY 1;

-- 待人工队列(无任务无工单)——91 现状 102,674 笔
SELECT count(*) FROM orders o
WHERE o.provider = 'douyin_mashida'
  AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_id = o.id)
  AND NOT EXISTS (SELECT 1 FROM work_orders w WHERE w.order_id = o.id);

-- 防重复交付自查(应为 0)
SELECT count(*) FROM work_orders w
JOIN orders o ON o.id = w.order_id
JOIN fulfillment_tasks ft ON ft.order_id = o.id AND ft.executor_key = 'affiliate_dash'
WHERE o.provider = 'douyin_mashida';

八、灰度与上线

  1. 代码合并但不打开开关:新分类逻辑上线,workerDispatchEnabled=false → 抖音行为 = 「命中映射自动发货 / 否则待人工」,不建工单;
  2. 验证人工层:确认未命中单不再产生 manual_dispatch 任务,且能进「待人工」筛选;
  3. 打开发单开关(小批商品先配模板)→ 观察工单量与匹配诊断;
  4. 全量放开:补齐抖音商品模板(见 11 号文档);
  5. 任一步异常:关开关即可回到第 1 步状态。

九、未决问题(需产品/运营确认)

  1. ②③ 层是否发券给抖音平台?(现状:全发;若不发会影响买家在抖店看到的卡券入口)
  2. ②③ 层的 cert_link 指向:/redeem(买家自助兑换)还是「客服/人工」页?
  3. 人工层表达:显式 classification.channel='manual'(推荐)还是沿用 91 的 order_status='pending_config'?
  4. 91 侧是否同步整改:91 的 102,674 笔裸单 + 死掉的待配置链路要不要一起修(本次设计对 91 只做「不改」,但问题客观存在)?
  5. 打手材料字段口径:抖音模板的 requirement_json 需交什么素材(买家留言 buyer_words?UID?);
  6. 抖音内部 SKU 命名规范:同一商品出现过「套装-浪漫天命」与「浪漫天命」两种写法,需回推抖音侧统一。

十、附:运营配置清单(配合 §5.3)

  1. 接单模板按 sku_code = 抖音内部 SKU + 精确匹配 新建(现 662 条里只有 6 条配了 sku_code);
  2. 已自动发货的商品(幸运币2个、套装-浪漫天命 等)不要建模板;
  3. 平台/来源一律留空(现已全空,勿再填);
  4. 「适用商品绑定」对抖音无效(缺 sellerId),勿依赖;
  5. 详细逐商品清单见 11-接单模板待补清单.md。