diff --git a/apps/backend/src/db/migrations/113_finance_channel_cutover.sql b/apps/backend/src/db/migrations/113_finance_channel_cutover.sql index e49638b6..ba6abc50 100644 --- a/apps/backend/src/db/migrations/113_finance_channel_cutover.sql +++ b/apps/backend/src/db/migrations/113_finance_channel_cutover.sql @@ -1,29 +1,24 @@ -- 113_finance_channel_cutover.sql -- --- 财务渠道模型的一次性升级(合并原 113–118): +-- 财务渠道模型的一次性升级(合并原 113/114/115/117/118;116 已取消): -- 1. 订单项增加渠道/状态/来源三列,并从旧的 fulfillment_mode 回填; -- 2. 删除旧列及其索引; -- 3. 销售事实维度改为 channel; -- 4. 补齐财务管道增量索引。 -- -- 生产基线为 112,本迁移是断代升级的唯一一步;执行后立即跑 finance:cutover-v2。 --- 历史订单退款不进入新财务口径,因此不再引入 refund_adjustment 相关约束。 - +-- 历史订单退款不进入新财务口径,因此没有 refund_adjustment 相关约束。 -- --- 目标:把「渠道 / 状态 / 决策来源」从一列拆成三列,并把渠道决策从 +-- 目标:把「渠道 / 状态 / 决策来源」从旧的一列拆成三列,并把渠道决策从 -- 「有没有任务」反推改为「配置驱动」。 -- --- 生产前置: --- · 生产迁移停在 111(2026-10-30 20:32);本迁移为 113,编号未被占用。 --- · 生产数据:orders 438,740 / order_items 438,750 / 10.1 GB。 --- · order_items 上任何全表 UPDATE 约 34s,超过 15s 语句超时—— --- 所以本迁移**只做 DDL 与小表种子**,438k 行回填交给 --- `scripts/backfill-fulfillment-channel.ts`(分批、幂等、可续跑)。 +-- 数据量:生产约 44 万订单项;回填、删列、改名与建索引在同一事务内完成, +-- 42 万行副本实测约 50 秒。升级应在维护窗口执行。 -- -- 本迁移不做的事: --- · 不删除 fulfillment_mode(等代码切换完成后由 114 删除) --- · 不清派生数据(由断代脚本执行) --- · 不改历史订单(回填脚本按断代日归档) +-- · 不清派生数据(快照/事实/异常/总账由断代脚本清空); +-- · 不改订单业务状态(仅把 legacy_platform 旧值标为 archived, +-- 真正的断代归档由 finance:cutover-v2 执行)。 -- -- 与旧模型的关系: -- 旧:order_items.fulfillment_mode ∈ work_order / affiliate_dash / @@ -114,9 +109,9 @@ COMMENT ON COLUMN order_items.fulfillment_locked IS COMMENT ON COLUMN order_items.fulfillment_decided_by IS '最近一次渠道决策的后台用户;自动决策为空'; --- 旧列标记为废弃(114 删除)。保留是为了让过渡期代码仍可读历史值。 +-- 旧列先保留用于回填,本迁移末尾删除。 COMMENT ON COLUMN order_items.fulfillment_mode IS - '【已废弃,勿写入】旧履约方式四值枚举;读取请改用 fulfillment_channel + fulfillment_status。计划由 114 迁移删除'; + '【即将删除】旧履约方式四值枚举;本迁移末尾删除,读取请改用 fulfillment_channel + fulfillment_status'; -- --------------------------------------------------------------------------- -- 2. 新索引:按状态与渠道取数,全部走部分索引 @@ -152,17 +147,15 @@ COMMENT ON INDEX idx_order_items_unmapped_id IS COMMENT ON INDEX idx_order_items_channel_product IS '成本快照管道:渠道 + 商品维度覆盖索引'; --- 注意:旧索引 idx_order_items_fulfillment_pending_id(108)与 --- idx_order_items_fulfillment_product(110)在 114 里删除; --- 过渡期保留,避免旧代码路径失速。 +-- 旧索引(108/110 等)依赖 fulfillment_mode,统一在本迁移删除旧列前移除。 -- --------------------------------------------------------------------------- -- 3. 渠道规则表(L1):精确键规则,覆盖自动发货 / 人工 / 卡券 / 运营覆盖 -- --------------------------------------------------------------------------- -- 设计说明: --- · 接单平台(work_order)不走本表:它由接单模板 + 商品匹配引擎决定, --- 规则引擎的结论由 channel_assign 直接写成 channel=work_order。 +-- · 接单平台(work_order)默认由载体证据判定(存在已接工单/任务); +-- 本表也允许运营显式指定 work_order,用于无载体但需强制归属的场景。 -- · 本表只处理「精确键」类渠道:affiliate_dash / manual / -- third_party_voucher,以及运营对任意渠道的显式覆盖。 -- · provider / shop_id 为空串表示「不限」。 @@ -228,14 +221,14 @@ CREATE INDEX IF NOT EXISTS idx_sku_channel_rules_lookup INCLUDE (provider, shop_id, channel); COMMENT ON TABLE sku_channel_rules IS - 'L1 渠道路由规则:精确键(SKU / 内部 SKU / SKU 名称)→ 渠道;接单平台由模板引擎决定,不在此表'; + 'L1 渠道路由规则:精确键(SKU / 内部 SKU / SKU 名称)→ 渠道;接单平台由载体证据判定,不在此表'; COMMENT ON COLUMN sku_channel_rules.priority IS '越小越优先:运营指定 10 / 自动发货 60 / 人工 70;同优先级多渠道=冲突'; COMMENT ON COLUMN sku_channel_rules.source_ref_id IS '来源对象 id:affiliate=product_catalog.id / manual=manual_fulfillment_configs.id / 其余为空'; -- --------------------------------------------------------------------------- --- 4. 从现有配置生成初始规则(小表,836 行级,可在迁移内完成) +-- 4. 从现有配置生成初始规则(小表,几千行级,可在迁移内完成) -- --------------------------------------------------------------------------- -- 4.1 自动发货:商品目录里的 aff: 即渠道归属 @@ -280,7 +273,8 @@ FROM manual_fulfillment_configs cfg WHERE btrim(cfg.sku_key) <> '' ON CONFLICT (provider, shop_id, match_key_type, match_key, channel) DO NOTHING; --- 4.3 第三方卡券:不预置,运营在「渠道规则」页按 SKU 添加(source='voucher')。 +-- 4.3 第三方卡券:迁移不预置;新建 tpv: 商品时自动生成规则, +-- 也可在「卡券采购价」页点「批量生成渠道规则」补齐。 -- --------------------------------------------------------------------------- -- 5. 成本项:第三方卡券采购成本(仅用于统计,无载体) @@ -374,7 +368,7 @@ CREATE INDEX IF NOT EXISTS idx_cost_exception_items_item COMMENT ON TABLE cost_exceptions IS '成本/分类问题队列(SKU 级):scope_key 相同的订单项聚合为一条问题,明细见 cost_exception_items'; COMMENT ON COLUMN cost_exceptions.reason IS - 'classification_missing 待分类 / routing_conflict 规则冲突 / product_mapping_missing 未归一 / cost_missing 成本缺失 / manual_config_missing 人工 SKU 未配置'; + 'classification_missing 待分类 / routing_conflict 规则冲突 / product_mapping_missing 未归一 / cost_missing 成本缺失 / manual_config_missing 人工 SKU 未配置 / manual_cost_missing 人工未确认履约 / carrier_missing 缺少履约载体'; COMMENT ON COLUMN cost_exceptions.item_count IS '当前牵涉的订单项数(明细去重后的计数)'; @@ -408,8 +402,8 @@ COMMENT ON TABLE finance_watermarks IS -- 8. 销售事实:允许新渠道与「未分类」桶 -- --------------------------------------------------------------------------- --- 销售事实继续复用 fulfillment_mode 列承载渠道值(115 再改列名), --- 但必须允许第三方卡券、未分类与归档状态。 +-- 先沿用旧列名承载渠道值,本迁移末尾统一改名为 channel; +-- 这里先放宽约束,允许卡券、未分类与归档状态。 ALTER TABLE sales_daily_facts DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_fulfillment_mode; @@ -433,17 +427,10 @@ COMMENT ON TABLE manual_sku_registry IS '【派生数据】人工 SKU 扫描结果,可由 pending/manual 订单项重新生成;真源是 manual_fulfillment_configs'; -- --------------------------------------------------------------------------- --- 10. 完成 +-- 10. 回填渠道,并从旧列切换到新列 -- --------------------------------------------------------------------------- --- 迁移后仍需执行(按顺序): --- 1. scripts/backfill-fulfillment-channel.ts —— 438k 行分批回填 channel/status --- 2. scripts/finance-cutover-v2.ts —— 断代归档 + 派生数据清零 --- 3. scripts/rebuild-after-cutover.ts —— 10-31 起点数据重建 + 期初建账 - - --- =========================================================================== --- 从旧 fulfillment_mode 回填新三列(此后才能删除旧列) --- =========================================================================== +-- 回填完成后才能删除 fulfillment_mode;legacy_platform 统一归为 archived。 +-- 断代与派生数据清理仍由 finance:cutover-v2 执行。 UPDATE order_items SET fulfillment_channel = CASE @@ -468,21 +455,12 @@ SET fulfillment_status = 'pending', fulfillment_channel = NULL WHERE fulfillment_mode IS NULL AND fulfillment_status <> 'pending'; --- --- 强制新模型:彻底删除旧 `order_items.fulfillment_mode` 及其索引/约束。 --- --- 背景: --- · 113 已加入 fulfillment_channel + fulfillment_status + fulfillment_source; --- · 代码侧已全部改为读写新列,`fulfillment_mode` 不再有任何读写方; --- · 保留旧列只会让「新旧并存」继续存在,因此这一版直接删除。 --- --- 注意: --- 1. 本迁移只做 DDL,不搬运数据; --- 2. 升级时应在维护窗口内先跑迁移,紧接着执行 `npm run finance:cutover-v2` --- 完成断代归档与派生数据清理,避免中间态被业务读到; --- 3. 已被 113 标记为 archived 的历史行不受影响。 +-- --------------------------------------------------------------------------- +-- 11. 删除旧列与依赖索引(回填已完成) +-- --------------------------------------------------------------------------- +-- 代码侧已全部读写新列;删除旧列后不再存在新旧并存。 --- 旧版迁移曾加过「旧异常写入口」兼容触发器,这里一并清理,不再保留兼容层。 +-- 清理历史兼容触发器(如果曾创建过)。 DROP TRIGGER IF EXISTS trg_cost_exceptions_fill_scope_key ON cost_exceptions; DROP FUNCTION IF EXISTS cost_exceptions_fill_scope_key(); @@ -508,10 +486,10 @@ COMMENT ON COLUMN order_items.fulfillment_channel IS COMMENT ON COLUMN order_items.fulfillment_status IS '渠道状态:pending 待分类 / assigned 已确定 / archived 断代归档'; --- --- 命名统一:销售事实的维度列由 fulfillment_mode 改为 channel。 --- 值本身在 113 之后已经是渠道(含 unclassified),旧名字只会误导。 --- 唯一约束与索引会自动跟随列重命名,这里只额外修正索引名。 +-- --------------------------------------------------------------------------- +-- 12. 销售事实维度改名 channel +-- --------------------------------------------------------------------------- +-- 回填值已经是渠道(含 unclassified),这里只改列名并修正索引名。 ALTER TABLE sales_daily_facts RENAME COLUMN fulfillment_mode TO channel; @@ -522,8 +500,9 @@ ALTER INDEX IF EXISTS idx_sales_daily_facts_mode COMMENT ON COLUMN sales_daily_facts.channel IS '渠道维度:work_order / affiliate_dash / third_party_voucher / manual / unclassified'; --- --- 财务管道按订单项 id 增量扫描时的专用部分索引。 +-- --------------------------------------------------------------------------- +-- 13. 财务管道增量索引 +-- --------------------------------------------------------------------------- -- 42 万行实测:carrier_dispatch 的「人工配置待挂载」查询原本 10.9s(扫描 12 万人工单), -- 加上按 id 的部分索引后可走索引顺序扫描并提前 LIMIT 结束。