diff --git a/apps/backend/src/db/migrations/113_fulfillment_channel_split.sql b/apps/backend/src/db/migrations/113_finance_channel_cutover.sql similarity index 76% rename from apps/backend/src/db/migrations/113_fulfillment_channel_split.sql rename to apps/backend/src/db/migrations/113_finance_channel_cutover.sql index d6aa5fd7..e49638b6 100644 --- a/apps/backend/src/db/migrations/113_fulfillment_channel_split.sql +++ b/apps/backend/src/db/migrations/113_finance_channel_cutover.sql @@ -1,4 +1,14 @@ --- 113_fulfillment_channel_split.sql +-- 113_finance_channel_cutover.sql +-- +-- 财务渠道模型的一次性升级(合并原 113–118): +-- 1. 订单项增加渠道/状态/来源三列,并从旧的 fulfillment_mode 回填; +-- 2. 删除旧列及其索引; +-- 3. 销售事实维度改为 channel; +-- 4. 补齐财务管道增量索引。 +-- +-- 生产基线为 112,本迁移是断代升级的唯一一步;执行后立即跑 finance:cutover-v2。 +-- 历史订单退款不进入新财务口径,因此不再引入 refund_adjustment 相关约束。 + -- -- 目标:把「渠道 / 状态 / 决策来源」从一列拆成三列,并把渠道决策从 -- 「有没有任务」反推改为「配置驱动」。 @@ -429,3 +439,127 @@ COMMENT ON TABLE manual_sku_registry IS -- 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 回填新三列(此后才能删除旧列) +-- =========================================================================== +UPDATE order_items +SET + fulfillment_channel = CASE + WHEN fulfillment_mode = 'legacy_platform' THEN NULL + ELSE fulfillment_mode + END, + fulfillment_status = CASE + WHEN fulfillment_mode = 'legacy_platform' THEN 'archived' + ELSE 'assigned' + END, + fulfillment_source = CASE + WHEN fulfillment_mode = 'legacy_platform' THEN 'cutover_archive' + ELSE 'carrier_evidence' + END, + fulfillment_decided_at = NOW(), + updated_at = NOW() +WHERE fulfillment_mode IS NOT NULL + AND fulfillment_channel IS NULL; + +UPDATE order_items +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 的历史行不受影响。 + +-- 旧版迁移曾加过「旧异常写入口」兼容触发器,这里一并清理,不再保留兼容层。 +DROP TRIGGER IF EXISTS trg_cost_exceptions_fill_scope_key ON cost_exceptions; +DROP FUNCTION IF EXISTS cost_exceptions_fill_scope_key(); + +ALTER TABLE order_items + DROP CONSTRAINT IF EXISTS chk_order_items_fulfillment_mode; + +-- 依赖旧列的部分索引必须先删,否则 DROP COLUMN 会被拒绝。 +DROP INDEX IF EXISTS idx_order_items_fulfillment_mode; +DROP INDEX IF EXISTS idx_order_items_fulfillment_pending; +DROP INDEX IF EXISTS idx_order_items_fulfillment_id; +DROP INDEX IF EXISTS idx_order_items_fulfillment_pending_id; +DROP INDEX IF EXISTS idx_order_items_fulfillment_product; +DROP INDEX IF EXISTS idx_order_items_manual_pending_cover; +DROP INDEX IF EXISTS idx_order_items_manual_sku_key_stored; +DROP INDEX IF EXISTS idx_order_items_manual_internal_sku; +DROP INDEX IF EXISTS idx_order_items_fulfillment_config; + +ALTER TABLE order_items + DROP COLUMN IF EXISTS fulfillment_mode; + +COMMENT ON COLUMN order_items.fulfillment_channel IS + '履约渠道(唯一真源):work_order / affiliate_dash / third_party_voucher / manual;pending 时为空'; +COMMENT ON COLUMN order_items.fulfillment_status IS + '渠道状态:pending 待分类 / assigned 已确定 / archived 断代归档'; + +-- +-- 命名统一:销售事实的维度列由 fulfillment_mode 改为 channel。 +-- 值本身在 113 之后已经是渠道(含 unclassified),旧名字只会误导。 +-- 唯一约束与索引会自动跟随列重命名,这里只额外修正索引名。 + +ALTER TABLE sales_daily_facts + RENAME COLUMN fulfillment_mode TO channel; + +ALTER INDEX IF EXISTS idx_sales_daily_facts_mode + RENAME TO idx_sales_daily_facts_channel; + +COMMENT ON COLUMN sales_daily_facts.channel IS + '渠道维度:work_order / affiliate_dash / third_party_voucher / manual / unclassified'; + +-- +-- 财务管道按订单项 id 增量扫描时的专用部分索引。 +-- 42 万行实测:carrier_dispatch 的「人工配置待挂载」查询原本 10.9s(扫描 12 万人工单), +-- 加上按 id 的部分索引后可走索引顺序扫描并提前 LIMIT 结束。 + +-- carrier_dispatch:人工渠道待挂配置 +CREATE INDEX IF NOT EXISTS idx_order_items_manual_pending_config_id + ON order_items (id) + WHERE fulfillment_status = 'assigned' + AND fulfillment_channel = 'manual' + AND fulfillment_config_id IS NULL; + +-- carrier_dispatch:已配置但未确认履约(人工成本未记录) +CREATE INDEX IF NOT EXISTS idx_order_items_manual_configured_id + ON order_items (id) + WHERE fulfillment_status = 'assigned' + AND fulfillment_channel = 'manual' + AND fulfillment_config_id IS NOT NULL; + +-- carrier_dispatch:自动发货/接单平台缺少履约载体 +CREATE INDEX IF NOT EXISTS idx_order_items_carrier_check_id + ON order_items (id) + WHERE fulfillment_status = 'assigned' + AND fulfillment_channel IN ('affiliate_dash', 'work_order'); + +-- cost_snapshot:按 id 顺序扫描需要核算成本的订单项 +CREATE INDEX IF NOT EXISTS idx_order_items_cost_ready_id + ON order_items (id) + WHERE fulfillment_status = 'assigned' + AND fulfillment_channel IN ( + 'affiliate_dash', 'third_party_voucher', 'work_order', 'manual' + ); + +-- +-- carrier_dispatch 按「人工 SKU 配置」反查订单项时,先按 manual_sku_key 命中再按 id 排序; +-- 42 万行实测原查询 3.3s,补 (manual_sku_key, id) 部分索引后可走位图索引 + 小结果集排序。 + +CREATE INDEX IF NOT EXISTS idx_order_items_manual_sku_key_id + ON order_items (manual_sku_key, id) + WHERE fulfillment_status = 'assigned' + AND fulfillment_channel = 'manual'; diff --git a/apps/backend/src/db/migrations/114_drop_legacy_fulfillment_mode.sql b/apps/backend/src/db/migrations/114_drop_legacy_fulfillment_mode.sql deleted file mode 100644 index d654017b..00000000 --- a/apps/backend/src/db/migrations/114_drop_legacy_fulfillment_mode.sql +++ /dev/null @@ -1,40 +0,0 @@ --- 114_drop_legacy_fulfillment_mode.sql --- --- 强制新模型:彻底删除旧 `order_items.fulfillment_mode` 及其索引/约束。 --- --- 背景: --- · 113 已加入 fulfillment_channel + fulfillment_status + fulfillment_source; --- · 代码侧已全部改为读写新列,`fulfillment_mode` 不再有任何读写方; --- · 保留旧列只会让「新旧并存」继续存在,因此这一版直接删除。 --- --- 注意: --- 1. 本迁移只做 DDL,不搬运数据; --- 2. 升级时应在维护窗口内先跑迁移,紧接着执行 `npm run finance:cutover-v2` --- 完成断代归档与派生数据清理,避免中间态被业务读到; --- 3. 已被 113 标记为 archived 的历史行不受影响。 - --- 旧版迁移曾加过「旧异常写入口」兼容触发器,这里一并清理,不再保留兼容层。 -DROP TRIGGER IF EXISTS trg_cost_exceptions_fill_scope_key ON cost_exceptions; -DROP FUNCTION IF EXISTS cost_exceptions_fill_scope_key(); - -ALTER TABLE order_items - DROP CONSTRAINT IF EXISTS chk_order_items_fulfillment_mode; - --- 依赖旧列的部分索引必须先删,否则 DROP COLUMN 会被拒绝。 -DROP INDEX IF EXISTS idx_order_items_fulfillment_mode; -DROP INDEX IF EXISTS idx_order_items_fulfillment_pending; -DROP INDEX IF EXISTS idx_order_items_fulfillment_id; -DROP INDEX IF EXISTS idx_order_items_fulfillment_pending_id; -DROP INDEX IF EXISTS idx_order_items_fulfillment_product; -DROP INDEX IF EXISTS idx_order_items_manual_pending_cover; -DROP INDEX IF EXISTS idx_order_items_manual_sku_key_stored; -DROP INDEX IF EXISTS idx_order_items_manual_internal_sku; -DROP INDEX IF EXISTS idx_order_items_fulfillment_config; - -ALTER TABLE order_items - DROP COLUMN IF EXISTS fulfillment_mode; - -COMMENT ON COLUMN order_items.fulfillment_channel IS - '履约渠道(唯一真源):work_order / affiliate_dash / third_party_voucher / manual;pending 时为空'; -COMMENT ON COLUMN order_items.fulfillment_status IS - '渠道状态:pending 待分类 / assigned 已确定 / archived 断代归档'; diff --git a/apps/backend/src/db/migrations/115_rename_facts_channel.sql b/apps/backend/src/db/migrations/115_rename_facts_channel.sql deleted file mode 100644 index e43e5f4f..00000000 --- a/apps/backend/src/db/migrations/115_rename_facts_channel.sql +++ /dev/null @@ -1,14 +0,0 @@ --- 115_rename_facts_channel.sql --- --- 命名统一:销售事实的维度列由 fulfillment_mode 改为 channel。 --- 值本身在 113 之后已经是渠道(含 unclassified),旧名字只会误导。 --- 唯一约束与索引会自动跟随列重命名,这里只额外修正索引名。 - -ALTER TABLE sales_daily_facts - RENAME COLUMN fulfillment_mode TO channel; - -ALTER INDEX IF EXISTS idx_sales_daily_facts_mode - RENAME TO idx_sales_daily_facts_channel; - -COMMENT ON COLUMN sales_daily_facts.channel IS - '渠道维度:work_order / affiliate_dash / third_party_voucher / manual / unclassified'; diff --git a/apps/backend/src/db/migrations/116_refund_adjustment_facts.sql b/apps/backend/src/db/migrations/116_refund_adjustment_facts.sql deleted file mode 100644 index 9e916f68..00000000 --- a/apps/backend/src/db/migrations/116_refund_adjustment_facts.sql +++ /dev/null @@ -1,39 +0,0 @@ --- 116_refund_adjustment_facts.sql --- --- 断代后历史订单仍会退款:这些订单项已 archived、不参与正常销售事实, --- 需要在「退款发生日」记一笔冲减,否则退款在财务体系里完全丢失。 --- 这里新增事实渠道值 refund_adjustment 承载这类冲减行。 - -ALTER TABLE sales_daily_facts - DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_fulfillment_mode; -ALTER TABLE sales_daily_facts - DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_channel; - -ALTER TABLE sales_daily_facts - ADD CONSTRAINT chk_sales_daily_facts_channel - CHECK ( - channel IN ( - 'work_order', 'affiliate_dash', 'third_party_voucher', - 'manual', 'unclassified', 'archived', 'refund_adjustment' - ) - ); - -COMMENT ON COLUMN sales_daily_facts.channel IS - '渠道维度:work_order / affiliate_dash / third_party_voucher / manual / unclassified / archived / refund_adjustment'; - --- 退款冲减行允许净收入为负(其余渠道仍要求非负)。 -ALTER TABLE sales_daily_facts - DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_amounts; - -ALTER TABLE sales_daily_facts - ADD CONSTRAINT chk_sales_daily_facts_amounts - CHECK ( - order_count >= 0 - AND item_count >= 0 - AND quantity >= 0 - AND gross_revenue_fen >= 0 - AND refund_amount_fen >= 0 - AND (net_revenue_fen >= 0 OR channel = 'refund_adjustment') - AND cost_amount_fen >= 0 - AND cost_missing_count >= 0 - ); diff --git a/apps/backend/src/db/migrations/117_fulfillment_pipeline_indexes.sql b/apps/backend/src/db/migrations/117_fulfillment_pipeline_indexes.sql deleted file mode 100644 index efc9820f..00000000 --- a/apps/backend/src/db/migrations/117_fulfillment_pipeline_indexes.sql +++ /dev/null @@ -1,33 +0,0 @@ --- 117_fulfillment_pipeline_indexes.sql --- --- 财务管道按订单项 id 增量扫描时的专用部分索引。 --- 42 万行实测:carrier_dispatch 的「人工配置待挂载」查询原本 10.9s(扫描 12 万人工单), --- 加上按 id 的部分索引后可走索引顺序扫描并提前 LIMIT 结束。 - --- carrier_dispatch:人工渠道待挂配置 -CREATE INDEX IF NOT EXISTS idx_order_items_manual_pending_config_id - ON order_items (id) - WHERE fulfillment_status = 'assigned' - AND fulfillment_channel = 'manual' - AND fulfillment_config_id IS NULL; - --- carrier_dispatch:已配置但未确认履约(人工成本未记录) -CREATE INDEX IF NOT EXISTS idx_order_items_manual_configured_id - ON order_items (id) - WHERE fulfillment_status = 'assigned' - AND fulfillment_channel = 'manual' - AND fulfillment_config_id IS NOT NULL; - --- carrier_dispatch:自动发货/接单平台缺少履约载体 -CREATE INDEX IF NOT EXISTS idx_order_items_carrier_check_id - ON order_items (id) - WHERE fulfillment_status = 'assigned' - AND fulfillment_channel IN ('affiliate_dash', 'work_order'); - --- cost_snapshot:按 id 顺序扫描需要核算成本的订单项 -CREATE INDEX IF NOT EXISTS idx_order_items_cost_ready_id - ON order_items (id) - WHERE fulfillment_status = 'assigned' - AND fulfillment_channel IN ( - 'affiliate_dash', 'third_party_voucher', 'work_order', 'manual' - ); diff --git a/apps/backend/src/db/migrations/118_order_items_manual_sku_index.sql b/apps/backend/src/db/migrations/118_order_items_manual_sku_index.sql deleted file mode 100644 index e63ee322..00000000 --- a/apps/backend/src/db/migrations/118_order_items_manual_sku_index.sql +++ /dev/null @@ -1,9 +0,0 @@ --- 118_order_items_manual_sku_index.sql --- --- carrier_dispatch 按「人工 SKU 配置」反查订单项时,先按 manual_sku_key 命中再按 id 排序; --- 42 万行实测原查询 3.3s,补 (manual_sku_key, id) 部分索引后可走位图索引 + 小结果集排序。 - -CREATE INDEX IF NOT EXISTS idx_order_items_manual_sku_key_id - ON order_items (manual_sku_key, id) - WHERE fulfillment_status = 'assigned' - AND fulfillment_channel = 'manual'; diff --git a/docs/README.md b/docs/README.md index 5146db02..3a7410eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -97,7 +97,7 @@ ## 三、统一实施顺序 ```text -0. 统一履约事实(渠道 + 成本模型正交) 已完成(迁移 113–116) +0. 统一履约事实(渠道 + 成本模型正交) 已完成(迁移 113) 1. 商品主数据 + 渠道 SKU 映射 已完成 P0(仅自动发货/接单平台) 2. 成本中心 + 销售事实 已完成 P0 2.5 人工成本链路收敛(确认履约 → 扣积分/备用金 → 写快照) 已完成 P0(迁移 104) diff --git a/docs/dev合并main评估-2026-10-27.md b/docs/dev合并main评估-2026-10-27.md index 252d5fe2..82707b07 100644 --- a/docs/dev合并main评估-2026-10-27.md +++ b/docs/dev合并main评估-2026-10-27.md @@ -1,7 +1,7 @@ # dev 合并 main 评估(2026-10-27) -> **历史文档(2026-10-31 前口径)**:文中 `fulfillment_mode` / `legacy_platform` 已随迁移 113–116 删除; +> **历史文档(2026-10-31 前口径)**:文中 `fulfillment_mode` / `legacy_platform` 已随迁移 113 删除; > 现行口径为 `order_items.fulfillment_channel + fulfillment_status`,销售事实维度为 `sales_daily_facts.channel`。 > 本文保留用于追溯当时的决策,不再作为实现依据。 > 评估对象:`main`(2fb7aae1)与 `dev`(0de0fd7b + 当前未提交的财务 P0/P1 改动)。 diff --git a/docs/成本中心API契约.md b/docs/成本中心API契约.md index a365ab95..7062d381 100644 --- a/docs/成本中心API契约.md +++ b/docs/成本中心API契约.md @@ -1,7 +1,7 @@ # 成本中心 API 契约 > 状态:**现行**。后端服务与前端页面都必须严格按此实现,不得各自发挥。 -> 最后校准:2026-10-31(履约渠道模型,迁移 113–116) +> 最后校准:2026-10-31(履约渠道模型,迁移 113) > 代码基线:`main` > 详细业务口径见 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md);数据模型见 [成本中心与履约模型.md](成本中心与履约模型.md)。 diff --git a/docs/成本中心与履约模型.md b/docs/成本中心与履约模型.md index 7f1cb286..233357dc 100644 --- a/docs/成本中心与履约模型.md +++ b/docs/成本中心与履约模型.md @@ -1,7 +1,7 @@ # 成本中心与履约模型 > 状态:**现行**。2026-10-31 起履约事实改为「渠道 + 状态」两列,旧 `fulfillment_channel` 已删除。 -> 最后校准:2026-10-31(迁移 113–116) +> 最后校准:2026-10-31(迁移 113) > 代码基线:`main` > 数据基线:2026-10-29 生产 dump;历史数字仅作迁移参照。 > 上线初始化/验收见 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md)。 diff --git a/docs/财务与履约口径总纲-2026-10-23.md b/docs/财务与履约口径总纲-2026-10-23.md index 4c6205dd..eb5aba37 100644 --- a/docs/财务与履约口径总纲-2026-10-23.md +++ b/docs/财务与履约口径总纲-2026-10-23.md @@ -1,7 +1,7 @@ # 财务与履约口径总纲(2026-10-23 重写版) -> **历史文档(2026-10-31 前口径)**:文中 `fulfillment_mode` / `legacy_platform` 已随迁移 113–116 删除; +> **历史文档(2026-10-31 前口径)**:文中 `fulfillment_mode` / `legacy_platform` 已随迁移 113 删除; > 现行口径为 `order_items.fulfillment_channel + fulfillment_status`,销售事实维度为 `sales_daily_facts.channel`。 > 本文保留用于追溯当时的决策,不再作为实现依据。 > 状态:**已归档**。现行口径见《成本中心与履约模型》(2026-10-31 校准)与《成本中心API契约》。 diff --git a/docs/财务流程总览-2026-11-01.md b/docs/财务流程总览-2026-11-01.md index 03ad352e..fc22e8ba 100644 --- a/docs/财务流程总览-2026-11-01.md +++ b/docs/财务流程总览-2026-11-01.md @@ -2,7 +2,7 @@ > 状态:**现行**。断代日 `2026-10-31`:历史订单全部归档,财务只统计断代之后的数据。 > 历史订单的退款不进入本系统财务口径,由业务侧自行处理。 -> 代码基线:`main`(迁移 112–118)。 +> 代码基线:`main`(迁移 112–113)。 ## 一、范围与原则 @@ -131,6 +131,21 @@ 3. 写入断代记录,重复执行会被拦截; 4. 之后只处理新订单,无需任何历史回填。 +### 11.1 升级步骤(生产 112 → 113) + +原 113–118 已合并为一条 `113_finance_channel_cutover`(含渠道回填、删旧列、事实列改名、管道索引): + +```bash +# 1. 备份 +# 2. 迁移(42 万行实测约 50 秒,期间 order_items 会短暂持锁) +npm run db:migrate +# 3. 断代:归档历史 + 清空派生数据 +npm run finance:cutover-v2 -- --execute --confirm=2026-10-31 +# 4. 部署代码,启动后台 +``` + +迁移内部已包含 `fulfillment_mode → fulfillment_channel/status` 回填,不再需要单独的回填脚本。 + ## 十二、运维 - 后台「履约渠道 → 管道状态」查看水位、最近成功、错误; diff --git a/docs/财务系统深度分析-2026-10-27.md b/docs/财务系统深度分析-2026-10-27.md index 0488cc3e..fa4e5148 100644 --- a/docs/财务系统深度分析-2026-10-27.md +++ b/docs/财务系统深度分析-2026-10-27.md @@ -1,7 +1,7 @@ # order_site 财务系统深度分析(2026-10-27) -> **历史文档(2026-10-31 前口径)**:文中 `fulfillment_mode` / `legacy_platform` 已随迁移 113–116 删除; +> **历史文档(2026-10-31 前口径)**:文中 `fulfillment_mode` / `legacy_platform` 已随迁移 113 删除; > 现行口径为 `order_items.fulfillment_channel + fulfillment_status`,销售事实维度为 `sales_daily_facts.channel`。 > 本文保留用于追溯当时的决策,不再作为实现依据。 > 分析对象:订单履约、成本中心、客服备用金/积分、销售事实、影子总账、期间关账与平台对账。 diff --git a/docs/财务系统集成方案.md b/docs/财务系统集成方案.md index ebc5bb06..58a52fd0 100644 --- a/docs/财务系统集成方案.md +++ b/docs/财务系统集成方案.md @@ -20,7 +20,7 @@ | 商品主数据 / 渠道 SKU 映射 / 成本中心 | **已实施 P0(`dev` 分支)**,迁移 `103/104`;标准商品 745 个,只覆盖自动发货 38 + 接单平台 707 | | `sales_daily_facts` / 订单级毛利 | **已实施 P0(`dev` 分支)**,迁移 `105`;94 天收入 ¥2,650.87 万、成本 ¥950.27 万 | | 人工成本录入(确认履约 → 扣积分/备用金 → 写快照) | **已实施 P0(`dev` 分支)**,迁移 `104/107`;2026-10-24 人工履约页下线、入口收敛到订单页;**无登记单、无审批、无报销** | -| 履约渠道统一 `fulfillment_channel + fulfillment_status` | **已实施(2026-10-31,迁移 113–116)**;旧 `fulfillment_mode` 已删除,订单/成本/事实统一读渠道 | +| 履约渠道统一 `fulfillment_channel + fulfillment_status` | **已实施(2026-10-31,迁移 113)**;旧 `fulfillment_mode` 已删除,订单/成本/事实统一读渠道 | | `finance_accounts` / `finance_journal_entries` / `finance_journal_lines` | **已实施 P0(迁移 108)** | | `finance_periods` / `finance_reconciliations` / `finance_reconciliation_items` | **已实施 P0(迁移 108)** | | `finance_business_events` / `finance_settlements` | 未实施(当前用条目唯一键幂等) |