Files
order_site/docs/接单模板匹配引擎重构设计.md
T
yml2213 a3e2bdb7c4 feat(finance): 人工成本单事务/幂等 + 配置自动匹配 + 缺成本监控
- 确认履约改为单事务:锁订单项 → 复查快照 → 扣积分/备用金 → 写成本快照;
  快照失败整体回滚,不再出现「钱扣了、成本没记」
- manual_case 的 subject_id 统一为 order_item_id,新增订单项级唯一索引(迁移 111),
  消除 manual_expenses.id / platform_credit_entries.id 跨表撞车
- 人工 SKU 匹配增加 sku_code / sku_name 兜底;保存配置立即回填最近 30 天,
  定时任务按窗口 rematch,确认时再按 SKU 兜底匹配
- 新增 manual_cost_missing 异常队列:从首次人工配置起算,确认履约立即收口
- 补单元测试与真实 PostgreSQL 集成测试(并发、重复、强制快照失败回滚、双资金链路)
- 校准财务文档并新增上线初始化/验收手册(历史人工不补录,只保证配置后的新单)

验证:backend typecheck/lint/format/check:sql/test 843(839 pass / 4 skip);
frontend typecheck/lint/test 172;迁移 111 在 scratch 库全量执行 + 重跑幂等。
2026-10-26 13:04:51 +08:00

25 KiB
Raw Blame History

接单模板匹配引擎重构设计

目标读者:后端 / 后台运营。状态:P0 + P1 已实施;P2 / P3 待办。最后校准:2026-10-23(代码基线 dev(合并 dev1023 后)+ 迁移 103–111;匹配业务逻辑未变)。

一句话:把「订单信息 → 接单模板(价格/拼单/要求)」的匹配,从散落的多个消费点、平台判断和"店铺必填"的绑定表里,收敛为一个平台无关的引擎:匹配条件完全由订单信息表达,店铺只是可选条件之一,新增来源只加一个适配器 + 一组配置。

来源口径(2026-10-17 确认):全系统只有三个匹配来源——kuaishou-91、pdd、douyin-mashida。其中 91 回调与快手 send-code 是同一个 kuaishou-91 来源的两条数据通道:91 是订单骨架权威源,快手 send-code 是补全/兜底源;它们共享店铺键空间,不是两个来源,也不要拆成两个适配器。


1. 现状盘点(全部为生产实测 / 代码实证)

1.1 当前链路

graph LR
  A["kuaishou-91<br/>91 回调(骨架)+ 快手 send-code(补全)"] --> ORD[("orders / order_items")]
  C["PDD 同步"] --> ORD
  D["抖音订单"] --> ORD
  ORD --> CTX["匹配上下文构造<br/>resolveKuaishouWorkProductMatchContext"]
  CTX --> ENG["打分<br/>work-product-match-mappers"]
  CFG[("work_product_rules<br/>+ work_product_rule_mappings")] --> ENG
  ENG --> WO["work_orders:价格 / 拼单 / 时限 / 要求字段"]
  ENG --> LOG[("work_product_match_logs")]
  ADM["后台:匹配测试 / 重匹配"] --> ENG
  OTH["寄卖建议价 / 模板改价同步大厅"] -.-> CFG

1.2 四个匹配消费点(不是四个来源)

这里的“四个”是代码里曾经各自实现匹配的消费点,不是订单来源数量。订单来源始终只有 kuaishou-91 / pdd / douyin-mashida 三个。

消费者 位置 现在怎么匹配 问题
发单建工单 services/worker-platform/sync-worker-orders-service.ts:48-260 resolveMatchingProductRuleDecision + 数量/价格/拼单各自解析 触发点分散、不重算
后台自测 / 重匹配 services/worker-platform/admin-kuaishou-match-service.ts:301-370 自己拼一套 order/item payload,isPdd 靠 sellerId.includes('pdd') 猜(:310-315) 与生产路径两套推导
履约 / 自动发货 services/fulfillment/product-resolution-service.ts:207/246/484-494 isOpen91KuaishouOrder / isDouyinMashidaOrder + 路由规则 + affiliate_dash SKU 映射 与模板匹配互不知晓
周边 寄卖建议价 consignment-matching-service.ts:622;模板改价同步大厅 admin-worker-catalog-service.ts:513-545 各自再算一遍规格/数量/价格 同口径三处实现

1.3 匹配条件当前怎么表达(根因)

  1. 模板行自带匹配字段:work_product_rules.provider / platform / shop_id / sku_code / product_name / match_type / match_json。→ "模板名"本身就是匹配键,于是出现"名字越长分越高"(work-product-match-mappers.ts:336-342)。
  2. 商品绑定表要求店铺非空:work_product_rule_mappings 有 CHECK jsonb_array_length(seller_ids_json) > 0(迁移 034),且打分函数第一关就是店铺命中(:179-185)。→ 不配店铺就无法使用"商品绑定"这一层,而它恰恰是唯一表达 SKU 精确/数量系列的地方。
  3. 上下文只有一个平台实现:resolveKuaishouWorkProductMatchContext 只读 snapshot.kuaishouSendCode(:147-169)。其它平台靠别名兜(out_sku_sn / externalSkuName / goodsId / spec,:241-257、303-316)。
  4. 兜底算法会被静默启用::103 useLegacyFallback = !context.sellerId || 开关,后台 legacyFallbackEnabled=false(生产实测值)也拦不住。
  5. 匹配入口三处触发且不重算:syncWorkerOrdersForSourceOrder 被 91 回调(platforms/ninetyone/order-service.ts:187)、订单通用入口(order/order-service.ts:211/241)、快手 send-code(sync-work-orders-from-send-code.ts:141/224)分别调用;服务内发现已有工单即 already_exists 跳过(sync-worker-orders-service.ts:97)→ 先到先匹配,后到的补全数据不会纠正错误结果。注意:91 与快手 send-code 属于同一个 kuaishou-91 来源,这里是同一来源的两条到达通道,不是两个来源。

1.4 平台耦合清单

位置 耦合内容 后果
work-product-match-mappers.ts:360-371 跨平台豁免只给 PDD(provider=pdd / platform=pdd|kuaishou) 新平台要么被挡、要么误命中
work-product-match-mappers.ts:103 缺 sellerId 就打开旧兜底 后台开关失效
work-product-match-mappers.ts:306-316 匹配文本含 snapshot.externalSkuName(抖音=引流标题) 引流词命中错误模板
admin-kuaishou-match-service.ts:310-319 isPdd 靠字符串猜 后台自测与生产不一致
product-resolution-service.ts:484-494 isOpen91KuaishouOrder / isDouyinMashidaOrder 履约侧第二套平台判断
services/platforms/ 无平台注册表(无 index) 新平台接入靠人肉对齐

1.5 订单信息实际有什么(生产实测 2026-10-14)

下表的 provider=91kaquan 是订单通道标识;匹配层仍统一归到 sourceKey=kuaishou-91。不要把 provider 当成匹配来源数量。

订单 provider 订单项数 有 shop_id 店铺数 有 kuaishouSendCode
91kaquan 329,677 100% 8 252,375(76.6%)
pdd 4,443 100% 4 0
douyin_mashida 17 100% 1 0

店铺键是三套命名空间(互不相通):

来源 shop_id 样例 说明
91kaquan 4661340439 141242642 91kaquan 快手 sellerId;91kaquan 是兜底值(21,044 单);历史脏:同店 shop_name 有「向日葵游戏服务店 / 91卡券 / 快手店铺-4661340439」三种写法
pdd xiangrikui mutashuyu PDD 店铺 code
douyin_mashida 305760014 抖音店铺 id(当前 0 条绑定含它)

其它可用键(按来源):

来源 平台商品ID 平台SKU ID 商家外部SKU编码 规格/内部SKU文本 商品标题 数量
kuaishou-91 snapshot.productNo — externalSkuCode 有 send-code 时=skuNick(真实规格名);否则=sku_name(内部商品名) kuaishouSendCode.itemTitle order_items.quantity / 规格文本
pdd goods_id — out_sku_sn spec goods_name goods_number
douyin-mashida productId skuId / outSkuId code(内部 SKU) code(内部 SKU) productName(就是引流标题) item_num(每件=1,数量在 SKU 文本里)

结论:"不绑店铺也能匹配"在数据上是成立的——每个来源都有 ≥1 个稳定商品键(91 商品名 / PDD out_sku_sn / 抖音 内部 SKU);店铺只是"更精确的加分条件"。


2. 目标模型

2.1 分层

graph LR
  S["来源适配器<br/>kuaishou-91 / pdd / douyin-mashida"] --> M["ProductMatchContext<br/>平台无关标准字段"]
  M --> E["引擎(纯函数)<br/>条件打分 + 决策"]
  CFG[("模板 work_product_rules<br/>+ 匹配条目 work_product_match_entries")] --> E
  E --> P["WorkOrderPlan<br/>rule / entry / 数量 / 单价 / 总额 / 拼单 / 理由"]
  P --> C1["发单建工单"]
  P --> C2["后台试匹配 & 重匹配"]
  P --> C3["大厅改价 / 寄卖建议价"]
  DEJ["来源数据补全(快手 send-code 回填等)"] --> S

三层职责单一:适配器只做字段归一、引擎只做打分决策(无 DB、无平台判断)、价格/拼单只有一处实现。

2.2 ProductMatchContext(平台无关)

type ProductMatchContext = {
  sourceKey: 'kuaishou-91' | 'pdd' | 'douyin-mashida' | 'unknown' // 来源(适配器),不是 provider
  provider: string
  platform: string // 仅展示/日志用,不参与匹配
  shopKeys: string[] // 命名空间化店铺键:kuaishou:4661340439 / douyin:305760014 / pdd:xiangrikui;空数组=该来源无店铺上下文
  hasSellerContext: boolean // 是否有卖家上下文(快手 send-code 卖家 ID)
  item: {
    itemId: string // 平台商品 ID
    relItemId: string // 关联商品 ID(快手 relItemId / 拼多多 goods_id)
    skuId: string // 平台 SKU ID
    relSkuId: string // 关联 SKU ID
    skuCodes: string[] // 编码候选(外部 SKU 编码 / 平台编码)
    skuNick: string // SKU 规格文本(单值,结构化条件用)
    skuTexts: string[] // SKU 文本候选(文本条件用,顺序=可信度);不含引流标题
    itemTitle: string // 商品标题(默认不参与,需条件显式声明;抖音恒为空)
    promoTitle: string // 引流标题:只展示、永不参与匹配
  }
  quantity: number // 已归一(规格文本 > 订单项数量 > 1)
  raw: JsonObject // 仅写日志
}

硬性不变式:

  • 匹配只读 item.* 与 quantity;promoTitle / raw 永不进打分。
  • 每个来源必须至少提供一个稳定商品键;缺失时引擎返回 unmatched + 原因(不允许静默降级到别家算法)。
  • 店铺、来源都是可选作用域:配置了就必须命中(过滤),不配=不限;两者都不配也能匹配。

2.3 匹配条目 MatchEntry(替代"店铺必填的绑定表")

一个模板可挂 0..N 条匹配条目;每条条件都可选,[] / 空串 = 不限;不含任何条件的条目 = 通配(对应现在的 product_default,必须显式创建)。

字段 含义 可空
sourceKeys 适用来源(kuaishou-91 / pdd / douyin-mashida) ✅ 空 = 全部来源
shopKeys 适用店铺(命名空间键,多选) ✅ 空 = 不限店铺(本次重点)
itemIds / relItemIds 平台商品 ID / 关联商品 ID(多值 OR) ✅
skuIds / relSkuIds 平台 SKU ID / 关联 SKU ID(多值 OR) ✅
skuText / skuNick + 匹配方式 规格文本:exact / contains / series(剥数量,兼容密钥·秘钥)/ series-exact ✅
itemTitle + 匹配方式 商品标题:off / exact / contains(默认 off) ✅
qtyMin / qtyMax 数量区间(0/0 = 不限),用于"5 个以上另一档价" ✅
baseScore 条目基础分(条件强度层级,由编译/模板推导,非人工填写) ❌
priority 同模板内排序;越小越优先(沿用 sort_order ASC 语义) ❌

决策已定:不做「平台」维度,平台差异由「来源」表达(kuaishou-91 / pdd / douyin-mashida),避免同一件事两处可配。

2.4 打分与决策(替换"模板名越长分越高")

条件分两类:作用域(来源/店铺,配了就必须命中)与判定条件(all 全中、any 至少一个、bonus 命中加分)。判定条件的权重按"确定性"排序:

条件 命中得分
relSkuId / skuId 精确 900
skuText / skuNick 精确 800
数量系列 series / series-exact 600
skuCode 精确(模板编码命中) 500
skuText / skuNick 包含(旧"模板名"路径) 400 + 具体度(min(80, 模板名长度),复刻历史"更长更具体"的排序语义)
relItemId / itemId 精确 300
itemTitle 命中 200(仅在条件显式配置时参与)
数量区间命中 不满足即淘汰;满足不加分

条目基础分(baseScore)表达层级:SKU 精确映射 1000 > 数量系列映射 800 > 商品默认映射 500 > 结构化条件 300 > 纯文本条件 0。 即:映射类条件永远压过纯文本条件(对应旧实现里 sku_exact(10000) > sku_series(8000) > product_default(5000) > 模板名路径(≤290) 的层级关系)。

决策:最高分唯一 → matched;并列最高 → ambiguous(不猜,记日志待人工/补配置);无候选 → unmatched。

unmatched / ambiguous 必须落诊断:每条候选差在哪一维(source_mismatch / shop_mismatch / quantity_below_min / any_not_matched:<字段:模式:值>),后台直接可读。

2.5 数量、价格、拼单(单一实现)

quantity = 规格文本数量系列(「指挥官密钥5个」→ 5)
         > 订单项数量字段(order_items.quantity)
         > 1
价格     = 模板.unit_price_fen > 0 ? 单价 × quantity : 固定 reward_amount
拼单     = 模板.sharing_enabled && quantity > 1 && 验收模式非 friend_gift
         ;auto_from_order 时使用订单 quantity 作为份数

注意:规格文本优先于数量字段是抖音的硬要求(订单项 item_num 恒为 1,数量写在 SKU 名里), 因此不能反过来"显式字段优先",否则「指挥官密钥5个」会按 1 个计价。

以上合并现 work-order-pricing.ts,作为唯一实现,被发单/后台改价/寄卖建议价共用。

2.6 触发点收敛

  • 匹配只在来源数据补全之后执行一次;补全(快手 send-code 回填、抖音详情补齐)不得触发第二次写单据。
  • 幂等:同一 order_item 已有工单则跳过(保留现行为),但后台"重匹配"必须能整单重跑(删除/更新由显式操作触发)。
  • 建议入口:order_items 落库后统一发一条"匹配任务"(事件/队列表或同步调用),发单、日志、诊断共用同一结果。

3. 数据模型改造

3.1 新表 work_product_match_entries

CREATE TABLE IF NOT EXISTS work_product_match_entries (
  id            BIGSERIAL PRIMARY KEY,
  rule_id       BIGINT NOT NULL REFERENCES work_product_rules(id) ON DELETE CASCADE,
  enabled       BOOLEAN NOT NULL DEFAULT true,
  priority      INTEGER NOT NULL DEFAULT 100,
  source_keys   JSONB NOT NULL DEFAULT '[]'::jsonb,   -- 空 = 全部来源
  shop_keys     JSONB NOT NULL DEFAULT '[]'::jsonb,   -- 空 = 不限店铺(命名空间键)
  item_id       TEXT NOT NULL DEFAULT '',
  out_item_id   TEXT NOT NULL DEFAULT '',
  sku_id        TEXT NOT NULL DEFAULT '',
  out_sku_id    TEXT NOT NULL DEFAULT '',
  sku_text      TEXT NOT NULL DEFAULT '',
  sku_match_mode TEXT NOT NULL DEFAULT 'contains'
    CHECK (sku_match_mode IN ('exact', 'contains', 'series')),
  title_text    TEXT NOT NULL DEFAULT '',
  title_match_mode TEXT NOT NULL DEFAULT 'off'
    CHECK (title_match_mode IN ('off', 'exact', 'contains')),
  qty_min       INTEGER NOT NULL DEFAULT 0,
  qty_max       INTEGER NOT NULL DEFAULT 0,
  is_wildcard   BOOLEAN NOT NULL DEFAULT false,  -- 通配条目(无条件匹配全部),需显式勾选
  created_at    TIMESTAMPTZ NOT NULL,
  updated_at    TIMESTAMPTZ NOT NULL,
  -- 要么显式声明通配,要么至少有一个判定维度,避免"空条件条目"意外抢单
  CHECK (
    is_wildcard
    OR source_keys <> '[]'::jsonb OR shop_keys <> '[]'::jsonb
    OR out_sku_id <> '' OR sku_id <> '' OR sku_text <> ''
    OR out_item_id <> '' OR item_id <> '' OR title_text <> ''
    OR qty_min > 0 OR qty_max > 0
  )
);
CREATE INDEX IF NOT EXISTS work_product_match_entries_lookup_idx
  ON work_product_match_entries (enabled, priority, id);

3.2 兼容与迁移(不动存量数据语义)

  1. 模板行字段保留只读:product_name / match_type / sku_code / shop_id / provider / platform 保留,迁移脚本为每个启用模板生成一条兼容条目:
    • product_name 非空 → 条目 A:skuText = product_name, skuMatchMode = match_type(exact|contains);
    • sku_code 非空 → 条目 B:outSkuId = sku_code(现在只有 6 条模板配了)。
  2. 4 条商品绑定 → 条目:seller_ids_json → shopKeys(补 kuaishou: 前缀)、mapping_type=sku_exact → outSkuId/skuText + exact、sku_series → skuText + series + sku_match_mode、rel_item_id → itemId、item_title → titleText(contains)。
  3. work_product_rule_mappings 双写期:新写入写新表,旧表保留一个版本用于回滚对比,确认稳定后下线。
  4. 匹配日志扩展:work_product_match_logs 增加 source_key / shop_key / candidates_json(逐维得分) / decision_reason(现有列保留)。
  5. 不改 work_orders 结构:匹配产物(rule_id + 价格/拼单)落库口径不变。

4. 代码结构

apps/backend/src/services/product-match/
  types.ts          # ProductMatchContext / MatchEntry / MatchDecision / WorkOrderPlan
  context.ts        # 组装上下文(读 order_items 标准字段 + 快照别名),不含平台分支
  adapters/
    kuaishou-91.ts  # 91 回调 + 快手 send-code 两路数据合成同一上下文(共享店铺键空间)
    pdd.ts
    douyin-mashida.ts # skuText=内部SKU;promoTitle=引流标题(永不参与匹配)
    registry.ts     # sourceKey → adapter
  engine.ts         # 条件打分 + 决策 + 诊断(纯函数,无 DB / 无平台判断)
  pricing.ts        # 数量 / 单价 / 总额 / 拼单(收编 work-order-pricing.ts)
  index.ts          # 对外唯一入口:resolveWorkOrderPlan(order, item, { rules, entries })

对外只有一个函数出口;发单、后台自测、重匹配、寄卖建议价、大厅改价全部调用它。新增来源 = adapters/<source>.ts + registry 注册 + fixtures,引擎不动。


5. 后台 UI 调整(运营视角)

  • 模板表单拆成两块:① 模板(价格/时间/要求字段/验收模式/拼单)、② 匹配条件(可加多条条目)。模板名称降级为纯展示名,不再参与匹配。
  • 匹配条目表单:来源 / 平台 / 店铺(多选,可留空)/ 商品ID / SKU编码 / SKU文本 + 匹配方式(精确·包含·数量系列)/ 标题匹配(默认关闭)/ 数量区间 / 优先级。
  • "商品绑定"列表页改为"匹配条目"列表:显示条件摘要 + 命中统计。
  • 保留并强化"试匹配":粘贴订单信息 → 选来源 → 输出决策 + 每个候选的逐维得分 + 为什么没命中(替代现在只给模板名与拒绝原因的诊断)。
  • 匹配日志增加来源/店铺/决策原因筛选。

6. 分期落地与风险护栏

期 内容 影响面 预估
P0 兜底开关尊重后台配置;抖音适配层:skuText 用内部 SKU、shop 用 order.shop_id、itemTitle 标为 promoTitle 仅抖音 0.5~1 天
P1 抽 product-match/(context + adapters + engine + pricing),行为等价迁移;补每来源 golden fixtures 全来源,行为等价 2~3 天
P2 后台自测/重匹配、寄卖建议价、大厅改价统一走 resolveWorkOrderPlan;匹配触发点收敛到"补全后一次" 全来源 1~2 天
P3 新表 + 迁移脚本 + 后台 UI(可选条件/店铺可选/条目化)+ 旧表下线 含数据迁移 2~3 天

护栏:

  • 91 冻结:91 走 legacy 兼容条目 + golden fixtures 全量回归(关键样本:8 家店、有/无 send-code 两类单、数量系列 1/2/5/10 个)。
  • 逐来源灰度:新引擎先只对 douyin-mashida 生效,pdd 次之,91 最后;每步都能一键回退到旧路径。
  • 不猜:并列最高一律 ambiguous,配合后台"未命中/歧义"清单驱动补配置(避免再出现"悄悄选了错模板")。
  • 数据零改动:P0/P1 不发 migration;P3 的迁移只新增表与条目,不动 work_orders / 历史匹配。

7. 验收标准

  1. 每来源一组 fixtures:给定订单信息(快照 + 商品字段)→ 期望 {status, ruleId, quantity, rewardAmount, sharing};91 的 30+ 组历史样本结果与现网一致。
  2. 抖音「指挥官密钥1个 / 5个」:命中 9.5 元/个模板、数量 5 时自动开 5 份拼单(当前为 0.85 元/个、拼单关)。
  3. 店铺缺失场景:清空 shopKeys 的条目仍能命中;shop 为 null 时不产生 ambiguous。
  4. 引流标题场景:商品标题含其它商品词时,只要条目未开启标题匹配,结果不受影响。
  5. 后台"试匹配"输出与生产建单结果一致(当前两者是两套推导,这是必测项)。
  6. 新增来源演练:只加一个 adapter + 配置即可跑通(以 fixtures 覆盖)。

8. 实施进展

8.1 已完成(P0 + P1,2026-10-14)

新增模块 apps/backend/src/services/product-match/:

文件 职责
types.ts ProductMatchContext / ProductMatchEntry / ProductMatchDecision
normalize.ts 文本/标识归一、数量解析、数量系列(含密钥·秘钥等价)
adapters/{kuaishou-91,pdd,douyin-mashida,registry}.ts 三个来源适配器 + 注册表(平台差异只在这里)
context.ts 上下文构建 + 来源文本兜底策略 + 旧投影兼容
compile.ts 旧「模板 + 商品绑定」→ 匹配条目(唯一的历史差异落点)
engine.ts 条件打分 + 决策(不猜)+ 逐维诊断
pricing.ts 数量 / 单价 / 拼单唯一实现(work-order-pricing.ts 改为转发)
product-match.test.ts 每来源 fixtures(含抖音误命中回归)

worker-platform/work-product-match-mappers.ts 改为门面:函数签名不变,内部调用新引擎(发单服务、后台匹配测试/重匹配、既有 19 个匹配用例全部无需改动)。

8.2 已验证

  • 生产数据回放(6000 条近 21 天订单项):91kaquan 5845 条、pdd 138 条决策 100% 一致(matched/unmatched 分布与命中模板 id 全部相同)。
  • 抖音 5 条命中变化,全部是修正:
    • 指挥官密钥1个 / 5个(订单 334081 / 334084 / 334088):碎梦浮光珍宝礼盒 0.85 元/个、拼单关 → 指挥官密钥 9.5 元/个、拼单随订单开 5 份;
    • 突击兵密钥5个(333380 / 333932):由引流标题误命中的 指挥官密钥 → 突击兵(与快手侧同商品的判定一致)。
  • npm run check(2026-10-14 当时):format / sql guard / lint / typecheck / 727 用例(0 失败)/ build 全绿;2026-10-17 复核后端 740 项(737 pass / 3 skipped)、前端 143 项。

8.3 待办(P2 / P3)

  • P2:后台「试匹配 / 重匹配」与发单共用同一入口(目前已共用匹配内核,派生字段仍是两套);寄卖建议价、大厅改价改走 pricing.ts;匹配触发点收敛到"来源数据补全后一次",去重不再依赖"已有工单就跳过"。
  • P3:work_product_match_entries 建表 + 旧配置迁移(§3.2)+ 后台条目化 UI;之后删除 compile.ts 的兼容编译与 legacyFallbackEnabled 配置项。
  • 存量:抖音误命中的 3 张工单(132001 / 132003 / 132004,均 pending_material、无打手接单)在新代码上线后用后台「重匹配」重跑。

附录 A:来源 → 上下文映射

来源 sourceKey shopKeys itemId / relItemId skuId / relSkuId skuCodes skuNick / skuTexts itemTitle promoTitle
91 + 快手 kuaishou-91 kuaishou:<send-code sellerId>、kuaishou:<order.shop_id> sendCode.itemId / sendCode.relItemId sendCode.skuId / sendCode.relSkuId sku_code、externalSkuCode、out_sku_sn、externalItemId 有 send-code:skuNick;文本候选沿用旧列表 sendCode.itemTitle productTitle
PDD pdd pdd:<order.shop_id> goods_id / goods_id — / out_sku_sn 同上(旧列表) out_sku_sn / 旧文本列表 goods_name —
抖音 douyin-mashida douyin:<order.shop_id> productId / outProductId skuId / outSkuId sku_code、externalSkuCode、outSkuId、outProductId sku_code(内部 SKU);文本候选仅 sku_code(缺失时 sku_name) 恒为空 productName(=引流标题)
未知来源 unknown <provider>:<order.shop_id> 快照字段 快照字段 sku_code sku_code / sku_name 快照标题 productTitle

附录 B:旧字段 → 新概念

旧 新
work_product_rules.product_name + match_type 匹配条目 skuText + skuMatchMode
work_product_rules.sku_code 匹配条目 outSkuId
work_product_rules.provider / platform / shop_id 匹配条目 sourceKeys / shopKeys(全部可空;旧的 provider/platform 残留值一律忽略)
work_product_rule_mappings.seller_ids_json 匹配条目 shopKeys(命名空间键,可空)
mapping_type = product_default / sku_exact / sku_series 条目条件组合(无 SKU 条件 / outSkuId 或 skuText exact / skuText series)
sku_match_mode = fuzzy / exact skuMatchMode = contains / exact(+ series 独立成档)
legacyFallbackEnabled 删除(不再有第二套算法)