# 成本中心与履约模型 > 状态:**现行**。商品主数据、成本中心、成本快照、销售事实、履约事实均已落地。 > 最后校准:2026-10-27 > 代码基线:`dev`(合并 dev1023 + 人工成本完整性修复,迁移基线收敛为 `103`–`105`) > 数据基线:生产 dump `order_site-20261022-092601.dump`(2026-07-10 ~ 2026-10-22);本地只跑了近 30 天增量,历史数字仅作基线。 > 上线初始化/验收见 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md)。 > **业务口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责数据模型、迁移与运维。 > 接口字段见 [成本中心API契约.md](成本中心API契约.md)。 --- ## 〇、模型总览 ```text orders ──┬── order_items ──── fulfillment_mode (唯一履约事实) │ ├── canonical_product_id ── product_catalog │ ├── normalized_quantity / normalized_sku_name / normalized_sku_series │ └── revenue_amount_fen / revenue_basis │ ├── cost_snapshots ──── cost_versions ──── cost_items (cost_model) ├── cost_exceptions (order_item_id → order_items.fulfillment_mode) ├── sales_daily_facts (fulfillment_mode) └── manual_expenses (客服备用金支出) ── subject_id ── cost_snapshots ``` 四条不变式: 1. **履约方式只有一处存**:`order_items.fulfillment_mode`。快照、异常、销售事实要展示履约方式时 join `order_items`。 2. **商品来源只由 `canonical_sku` 前缀表达**,商品表不存载体列。 3. **成本模型只表达"怎么算"**:`cost_items.cost_model` 与履约方式正交,不复用同一字段。 4. **成本金额只在一处改**:`cost_versions`;业务表只保存快照,历史成本永不回写。 --- ## 一、迁移基线:10 个收敛为 5 个 2026-10-27 最终收敛为 3 个领域迁移(合计约 2,177 行): | 迁移 | 内容 | 行数 | | --- | --- | ---: | | `103_finance_foundation.sql` | 商品目录、渠道映射、成本项/版本/快照/异常、订单项收入与履约方式、销售事实、退款单 | 861 | | `104_finance_controls.sql` | 人工 SKU/配置、备用金/积分、影子总账、对账/关账、财务控制、L1 钱包硬化 | 1,119 | | `105_work_order_consignment.sql` | 拼单多次参与、大厅/店铺索引、撤份额注释、寄卖对接客服 | 197 | 原来 103–114 的 12 个补丁文件已删除;生产当前在 102,只需执行 103–105。 **合并原则**:同一批上线的结构性 DDL 按领域合并为 `103`(商品/成本/销售/退款)、`104`(人工/总账/控制)、`105`(拼单/寄卖);不再按单表或几行补丁拆文件。 **人工成本快照主体(2026-10-26 定稿)**:`manual_case` 的 `subject_id` 统一为 `order_item_id`,不再使用 `manual_expenses.id` / `platform_credit_entries.id`(两张表各自 BIGSERIAL,必然撞 id)。资金来源只放 `metadata_json.fundExpenseId / creditEntryId`。迁移 `104` 增加订单项级唯一索引 `uq_cost_snapshots_manual_order_item`,一个订单项最多一条有效人工快照;`subject_id` 为正数的历史行归一到订单项,负数历史孤儿快照按迁移 `103` 约定保留、不参与新链路。 ### 1.1 字段替换对照 | 旧字段 | 新字段 | 出现位置 | | --- | --- | --- | | `cost_items.carrier_type` | `cost_items.cost_model` | DB / 成本项接口 | | `cost_items.cost_type` | (并入 `cost_model`) | DB / 成本项接口 | | `cost_snapshots.carrier_type` | (删除) | DB | | `cost_exceptions.carrier_type` | (删除) | DB | | `product_catalog.carrier_type` | (删除,改由 `canonical_sku` 前缀推导) | DB / 商品接口 | | `sales_daily_facts.carrier_type` | `sales_daily_facts.fulfillment_mode` | DB / 销售事实接口 | | `work_product_rules.legacy_cost_source` | (删除) | DB | | `cost_versions.cost_item_carrier_type` | `cost_versions.cost_item_cost_model` | 成本版本接口 | | `pricing_mode: per_hour / fixed_daily` | (删除,只剩三种) | 成本项接口 | | `manual_expense_records` / `manual_expense_events` | (删除,人工成本只走确认履约) | DB | ### 1.2 成本模型映射 | 旧 `cost_type` | 新 `cost_model` | | --- | --- | | `auto_purchase` | `affiliate_purchase` | | `work_order_reward` | `work_order_reward` | | `manual_service` | `manual_actual` | | `platform_fee` | `platform_fee` | | `payment_fee` | `payment_fee` | | `after_sales_loss` | `after_sales_loss` | | `shipping_other` / `other` | `other` | ### 1.3 商品来源前缀 | 前缀 | 来源 | 对应履约方式(仅参考,不作推导依据) | | --- | --- | --- | | `aff:` | 自动发货 | `affiliate_dash` | | `tpl:` | 接单模板 | `work_order` | | `wo:` | 历史工单证据 | `work_order` | | `man:` | 人工 | `manual` | | 其他 | 未分类 | 不得推断 | 本地现状:`aff:` 38 个、`tpl:` 662 个、`wo:` 45 个,`man:` 0 个(P1 待建)。 --- ## 二、商品主数据 ### 2.1 `product_catalog` ```text id canonical_sku -- 内部稳定编码;前缀表达来源 canonical_name category unit_name -- 件/个/颗等,配合 normalized_quantity status -- active / archived attributes_json ``` `canonical_sku` 是内部稳定编码,创建后不随平台改名。`attributes_json` 会被价格导入覆盖,**不能作为统计源**。 ### 2.2 `channel_product_mappings` ```text provider -- 落库值:91kaquan / pdd / douyin_mashida shop_id -- 空串 = 来源级;具体店铺优先 platform_sku_code platform_sku_name platform_sku_name_key -- 归一后的精确名称或数量系列键 match_type -- sku_code / sku_code_name / sku_code_series / sku_name_series canonical_product_id quantity_multiplier -- 数量已在归一数量中体现,系列 SKU 固定为 1 confidence -- exact / rule / manual source -- manual / import / rule enabled ``` 唯一键 = `provider + shop_id + platform_sku_code + match_type + platform_sku_name_key`。91 在同一店铺复用 `sku_code`,只有加入名称键才能消歧。 匹配优先级: ```text 店铺 + SKU 编码 > 来源 + SKU 编码 > 店铺 + SKU 编码 + 精确名称 > 来源 + SKU 编码 + 精确名称 > 店铺 + SKU 编码 + 名称系列 > 来源 + SKU 编码 + 名称系列 > 店铺 + 名称系列 > 来源 + 名称系列 ``` 匹配不到的商品进入 `product_mapping_missing` 异常队列,**不能直接拿原始 `sku_code` 当报表维度**。 ### 2.3 `order_items` 的商品与数量字段 ```text canonical_product_id -- 归一到标准商品 normalized_quantity -- 从 SKU 名称解析并固化;未解析时为 NULL normalized_sku_name -- NFKC、小写、去空白后的名称 normalized_sku_series -- 剥离数量并归一同义字后的名称系列 ``` **`normalized_quantity` 不预填。** 预填会把"5 个"类 SKU 永久锁死成 1,必须由回填脚本按 SKU 名称解析后写入。`quantity` 长期几乎恒为 1,禁止直接拿它当销量。 ### 2.4 接单模板批量初始化 接单模板不允许逐个手工建商品。后台「从接单模板全量同步商品与映射」按以下规则批量生成: 1. **商品全量同步,不依赖订单**:所有启用模板都建/刷新标准商品,内部编码 `tpl:`;模板改名、改分类、改发单金额都会刷新到商品上。新模板还没有工单也能在「商品与成本中心」直接定价。停用模板默认不建商品,勾选后可一并建成「归档」商品。 2. **渠道映射仍只按工单证据建**:默认最近 30 天,可在页面日期筛选里调整;范围内没有工单的模板只建商品、不建映射——没有订单就没有平台 SKU 可映射。 3. **范围内的工单直接命中模板**,以 `work_orders.product_rule_id` 作为映射的第一手证据,不只看原始 `sku_code`。 4. 同一 `provider + shop_id + sku_code` 只命中一个模板时,生成 `sku_code` 精确映射。 5. 编码被多个模板复用时,继续用"编码 + 归一名称系列"和"编码 + 精确名称"消歧;只有唯一归属才生成映射。 6. 同一编码 + 同一名称仍命中多个模板时,**不选最新、不按次数猜**;跳过并列入冲突清单。 7. 来源 + 店铺内名称系列唯一时,额外生成 `sku_name_series` 兜底,永远排在编码类映射之后。 8. 数量系列(1个/2个/5个/10个,秘钥/密钥等等价写法)归为同一标准商品;数量写入 `normalized_quantity`,**不能把"5个"再写成 `quantity_multiplier=5`**,否则会重复乘算。 9. 重跑初始化只清理**所选时间范围内**由 `source='import'`、备注为"接单模板 … 自动初始化"的映射带出的订单项 `canonical_product_id`,并删除范围内使用到的导入映射;范围外的人工映射、成本版本、快照和真实订单/工单、已建商品都不删除。 本地验证结果(2026-10-18):启用模板 662 个;分层映射 4,426 条(编码精确 2,302、编码 + 精确名称 440、编码 + 名称系列 160、名称系列兜底 1,524);可唯一归一 86,533 个有证据订单项,另有 25,918 个订单项属于 32 组真实冲突,保持未映射。 全量同步改版后(2026-10-29,本地库预览 09-30 ~ 10-29):模板 710 个全部启用、范围内有工单证据 388 个;商品新建 704 个 / 复用 6 个,候选映射 3,198 条。之前被跳过的 322 个无单模板,现在也会有商品(可定价),只是暂时没有渠道映射。 ### 2.5 自动发货商品 自动发货只有 affiliate-dash 一个上游渠道,SKU 固定归一为 `aff:`,**不写 `channel_product_mappings`**。渠道映射只代表"平台 SKU 能归一到标准商品",不代表自动发货。 上游价格用 `apps/backend/scripts/sync-affiliate-dash-prices.ts` 批量导入(清单见 `apps/backend/scripts/data/affiliate-dash-prices.json`,38 条):按 `aff:` 补建/更新商品,并给 `sys_affiliate_purchase` 成本项生成 `scope=product` 的按件版本。金额变化时先停用旧版本再插入新版本,历史快照不变。首次导入历史基线用 `--effective-from=2026-07-10`。 --- ## 三、成本中心 ### 3.1 成本模型与适用履约方式 | `cost_model` | 适用履约方式 | 计价方式 | 成本来源 | | --- | --- | --- | --- | | `affiliate_purchase` | `affiliate_dash` | `per_item` | 上游 SKU 固定成本版本 | | `work_order_reward` | `work_order` | `per_order` | 模板规则级版本 / 工单实际报酬 | | `manual_actual` | `manual` | `per_order` | 订单页逐单录入的实际支出 | | `platform_fee` | 全部 | `per_order` / `percent_of_revenue` | 未配置 | | `payment_fee` | 全部 | `per_order` / `percent_of_revenue` | 未配置 | | `after_sales_loss` | 全部 | `per_order` | 未配置 | | `other` | 全部 | `per_order` | 未配置 | 后端判定见 `cost-center-shared.ts` 的 `costModelScopeSql()`;通用模型对所有履约方式生效,专属模型只对对应履约方式生效。 ### 3.2 `cost_items` / `cost_versions` ```text cost_items id, code, name cost_model -- 见 §3.1 pricing_mode -- per_item / per_order / percent_of_revenue unit_name, currency, status, note cost_versions id, cost_item_id scope_type -- global / provider / shop / product / rule provider, shop_id canonical_product_id, work_product_rule_id amount_fen -- 固定金额;percent_of_revenue 时用 rate_bps rate_bps -- 万分比,10000 = 100% effective_from, effective_to source -- manual / upstream_sync / import status -- draft / active / retired change_reason, created_by, approved_by ``` 范围优先级:`rule > product > shop > provider > global`。成本版本按**业务发生时间**(`paid_at` / `accepted_at`)解析,不是回填时的 `now()`。 改价只新增版本;历史订单走 `cost_snapshots`,不受影响。 ### 3.3 系统内置成本项 | 编码 | 成本模型 | 说明 | | --- | --- | --- | | `sys_work_order_reward` | `work_order_reward` | 662 个接单模板各一条规则级版本;全局 0 元版本只作"按工单实际结算"的占位 | | `sys_affiliate_purchase` | `affiliate_purchase` | 上游 SKU 固定成本版本,38 个商品级 | | `sys_manual_service` | `manual_actual` | 订单页确认履约写入 `actual` 快照;版本为 0 元占位 | 工单报酬的三种快照: - 工单**验收**后按 `work_orders.reward_amount` 写 `actual`(本地 95,870 条); - 未验收但已确定报酬的工单先按同一金额写 `estimated`(metadata `source=work_order_reward_committed`,本地 21,217 条); - 取消工单不计成本。 ### 3.4 `cost_snapshots` ```text id subject_type -- order_item / work_order / manual_case subject_id, order_id, order_item_id, work_order_id cost_item_id, cost_version_id, canonical_product_id quantity, unit_amount_fen, total_amount_fen snapshot_stage -- estimated / actual / adjustment source -- resolved / legacy_rule / manual_input / missing metadata_json cost_basis_at -- 解析成本版本时用的业务时间 resolved_at, superseded_at, created_at ``` 规则: - 改价只新增 `cost_versions`,不覆盖历史; - 同一主体 + 同一成本项只允许一条"当前有效"快照(`uq_cost_snapshots_active_subject_item`); - 预估快照代表支付时点的成本,**不因后续改价重写**;只有实际成本允许替代当前有效行; - 匹配不到成本时写 `cost_missing` 异常,**不能默认 0**; - 履约方式不存快照里,需要时 join `order_items.fulfillment_mode`; - 人工单的快照主体是 `subject_type='manual_case'` + `subject_id=order_item_id`;资金流水 id 只写 `metadata_json`,**没有独立的登记单表**; - 负数 `subject_id` 表示历史孤儿快照(迁移 `103` 约定),迁移 `104` 保留不动,不参与新链路; - 一个订单项最多一条有效人工快照(`uq_cost_snapshots_manual_order_item`)。 ### 3.5 `cost_exceptions` ```text id subject_type, subject_id order_item_id, work_order_id reason -- product_mapping_missing / cost_missing / manual_cost_missing status -- open / resolved / ignored detail_json created_at, resolved_at, resolved_by ``` `open` 数量必须在后台暴露。按履约方式筛选时用 `EXISTS`/`IN` 半连接 `order_items.fulfillment_mode`(本地 248,878 条,`IN` 半连接 + `idx_order_items_fulfillment_id` 索引约 170ms)。 --- ## 四、订单项收入固化 `order_items.revenue_amount_fen` / `revenue_basis` 在迁移 `103` 里固化: | `revenue_basis` | 含义 | | --- | --- | | `order_total` | 一单一品,订单实付全部归该订单项 | | `equal_split` | 多订单项按等额拆分,余数给最后一项 | | `platform_item` | 有平台明细时替换 | | `manual` / `unknown` | 未定 | 本地全量 316,585 项都是 `order_total`(当前一单一品),但模型按"订单项"设计,不把"一单一品"写死。 收入按 `orders.paid_at` 归属销售日(Asia/Shanghai),没有 `paid_at` 时退回 `created_at`。 --- ## 五、销售事实 ### 5.1 `sales_daily_facts` ```text stat_date -- Asia/Shanghai,按 paid_at 归属 provider, shop_id canonical_product_id -- 允许为空:未归一桶 fulfillment_mode -- work_order / affiliate_dash / legacy_platform / manual order_count, item_count, quantity gross_revenue_fen, refund_amount_fen, net_revenue_fen cost_amount_fen, cost_missing_count, cost_stage gross_profit_fen computed_at ``` 口径: - 收入 = `order_items.revenue_amount_fen`,不直接拿整单金额; - 成本来自 `cost_snapshots` 当前有效行;没有快照的订单项计入 `cost_missing_count`; - 未归一商品允许 `canonical_product_id` 为空,单独成"未归一"行,保证总收入对平; - `cost_stage`:`missing`(全部无快照)/ `partial`(部分无)/ `estimated`(全部预估)/ `actual`(全部实际); - `cost_missing_count > 0` 时该行毛利不直接用于决策; - 退款暂未接入,`refund_amount_fen` 先按 `pay_status='refunded'` 预留(整单全额冲减,无部分退款)。 ### 5.2 本地重算结果(2026-10-23) | 履约方式 | 事实行 | 订单项 | 收入 | 成本 | 缺失成本 | | --- | ---: | ---: | ---: | ---: | ---: | | `work_order` | 9,585 | 118,053 | ¥533.57 万 | ¥452.83 万 | 966 | | `affiliate_dash` | 3,163 | 41,676 | ¥508.97 万 | ¥497.41 万 | 0 | | `manual` | 1,849 | 102,955 | ¥1,219.20 万 | ¥0.32 万 | 102,954 | | `legacy_platform` | 201 | 53,901 | ¥389.12 万 | ¥0 | 53,901 | --- ## 六、履约事实 ### 6.1 `order_items.fulfillment_mode` ```text fulfillment_mode -- work_order / affiliate_dash / legacy_platform / manual;允许 NULL fulfillment_decided_at -- 最后一次判定时间 fulfillment_reason -- 判定来源:order_sync / reconcile / backfill ``` 判定顺序固定:**工单 → 自动发货任务 → 历史平台任务 → 人工**。当前库中三种载体没有重叠,顺序不影响结果;后续出现重叠必须告警,不能静默覆盖。 决策服务在 `services/fulfillment/fulfillment-mode-service.ts`: - `classifyFulfillmentMode()` —— 纯函数,供单测与文档对照; - `decideOrderItemsFulfillmentModes()` —— 订单同步后批量落库; - `reconcilePendingFulfillmentModes()` —— 24 小时兜底归集; - `getFulfillmentModeCounts()` —— 计数核对。 ### 6.2 回填 回填是**一次性脚本**,不塞进迁移:31 万行 `UPDATE` 放进迁移会拖慢部署,也无法在中途失败后按批重试。 ```bash npm run fulfillment:backfill ``` 只填 `NULL`,可重复执行。判定 SQL: ```sql WITH decided AS ( SELECT oi.id, CASE WHEN EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) THEN 'work_order' WHEN EXISTS ( SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash' ) THEN 'affiliate_dash' WHEN EXISTS ( SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key IN ('kuaishou_ct_assisted', 'kuaishou_feifei') ) THEN 'legacy_platform' ELSE 'manual' END AS fulfillment_mode FROM order_items oi ) UPDATE order_items oi SET fulfillment_mode = d.fulfillment_mode, fulfillment_decided_at = now(), fulfillment_reason = 'backfill_2026_10_23' FROM decided d WHERE oi.id = d.id AND oi.fulfillment_mode IS NULL; ``` > 上述计数是 2026-10-23 历史回填的结果,仅作迁移正确性参照;当前本地生产副本只跑了近 30 天增量,**不要用这些数字判断上线是否完成**。 > 本地验收(2026-10-23 重写后重跑):`work_order` 118,053 / `manual` 102,955 / `legacy_platform` 53,901 / `affiliate_dash` 41,676,四项相加 = 316,585,`NULL = 0`。 ### 6.3 决策边界(写进代码注释与测试) 1. `fulfillment_mode` 允许短暂为 `NULL`;订单项创建时通常还没有工单/任务,不能强制非空。 2. 上线后新订单项超过 **24 小时**仍为 `NULL`,进入异常队列;不是要求在创建瞬间就有值。 3. 模式可以因业务改派而变化,但每次变化必须写原因与操作人,禁止静默 `UPDATE`。 4. **商品来源不能反推履约方式。** 当前 11,349 个人工项指向 `tpl:*` 商品,就是典型反例。 5. `voucher_state` 只做展示与核销流程,不参与任何成本/履约判断。 6. 三种载体同时命中的订单项必须先告警人工复核;当前本地为 0。 --- ## 七、同步脚本与调度 | 命令 | 作用 | | --- | --- | | `npm run fulfillment:backfill` | 履约方式分批回填(只填 NULL) | | `npm run finance:sync -- --mode=all` | 预估快照 + 实际快照 + 销售事实重算 | | `npm run finance:sync -- --mode=estimated` | 只算预估快照并写缺失成本异常 | | `npm run finance:sync -- --mode=actual` | 只把工单快照升级为实际金额 | | `npm run finance:sync -- --mode=facts` | 只重算销售事实 | 长历史重算要分窗口:单次 `rebuildSalesDailyFacts` 超过约 400 天会被语句超时(`DATABASE_STATEMENT_TIMEOUT_MS`,默认 15 秒)打断。本地 2026-07-10 ~ 2026-10-23 分 4 段重算,正常。 定时任务 `services/scheduler/cost-finance-sync-job.ts` 每 5 分钟先归集待判定的订单项,再算成本与销售事实;默认只处理最近 7 天(可在 `scheduled_jobs` 配置里调大,测试/小机器不建议超过 30 天,避免单条语句超时)。 --- ## 八、迁移基线与本地库同步(2026-10-27 收敛) 生产数据库当前在 **102**。dev 已把原 103–114 收敛为 3 个领域迁移: | 迁移 | 内容 | | --- | --- | | `103_finance_foundation.sql` | 商品目录、成本中心、成本项/版本/快照、销售事实、退款单 | | `104_finance_controls.sql` | 人工履约、备用金/积分、影子总账、对账/关账、财务控制、L1 钱包硬化 | | `105_work_order_consignment.sql` | 拼单多次参与、大厅/店铺索引、撤份额注释、寄卖对接客服 | - 生产升级:**102 → 103 → 104 → 105**,只执行 3 个迁移文件。 - 全新库:`001 → 105` 一次跑完。 - 已跑过旧 103–114 的测试库:**直接重建**,或把 `schema_migrations` 清到 102 后重新执行 103–105;旧的逐补丁校准脚本已删除。 - 生产库只到 102,不依赖旧 103–114 的迁移记录。 ### 8.1 收入口径回填(迁移 103 不做全表更新) `order_items.revenue_amount_fen` / `revenue_basis` 在迁移 `103` 里**只建列,不刷历史数据**: 375,447 行的全表 `UPDATE` 实测约 **34 秒**,超过默认语句超时(`DATABASE_STATEMENT_TIMEOUT_MS=15s`), 放进单事务迁移会拖住启动甚至直接失败。 历史行由脚本分批回填(幂等、可续跑): ```bash npm run revenue:backfill -- --dry-run # 统计待回填量并抽样预览 npm run revenue:backfill # 默认每批 5000 行 npm run revenue:backfill -- --batch-size=2000 --max-batches=100 ``` 服务启动时会做一次带索引的计数:若仍有 `revenue_basis='unknown'` 的订单项,会打印告警并提示执行上面的命令。 **不执行回填的后果是销售事实收入偏低**(订单项收入按 0 计),不会报错,所以部署后必须确认该计数为 0。 `check-sql-guard.ts` 改为以 `main` 为基线校验迁移字节:**已发布的迁移不可修改**,未合并分支上的新迁移仍可重写;基线可用 `MIGRATION_BASELINE_REF` 覆盖,找不到基线时跳过该项检查。 --- ## 九、索引与性能 | 索引 | 用途 | | --- | --- | | `idx_order_items_fulfillment_mode (fulfillment_mode, order_id)` | 按履约方式筛订单项 | | `idx_order_items_fulfillment_id (fulfillment_mode, id)` | 异常按履约方式半连接 | | `idx_order_items_canonical_product` | 按标准商品查订单项 | | `idx_order_items_normalized_sku_lookup` | 按归一名称/系列匹配映射 | | `idx_channel_product_mappings_match_lookup` | 映射查找 | | `uq_channel_product_mappings_match_key` | 映射唯一键 | | `idx_cost_versions_lookup / product / rule` | 成本版本解析 | | `uq_cost_snapshots_active_subject_item` | 每个主体 + 成本项只允许一条有效快照 | | `idx_cost_exceptions_status_created` | 异常队列列表 | | `idx_sales_daily_facts_date / shop / product / mode` | 销售事实各维度 | | `idx_work_orders_rule_cover` | 成本版本页按接单规则聚合 | | `idx_order_items_revenue_cover` | 收入聚合 | | `idx_work_categories_name` | 商品目录按分类名 lateral join | | `idx_cost_exceptions_archivable` | 归档任务扫描「已处理且超期」的异常(部分索引,`status <> 'open'`) | ### 9.1 成本异常归档(60 天) `cost_exceptions` 只增不减:本地快照 260,253 行(resolved 151,939 / open 108,314)。已处理的行只是审计痕迹,继续留在热表会拖慢按状态+时间的分页,并让唯一索引持续膨胀。 | 项 | 规则 | | --- | --- | | 归档对象 | `status <> 'open'` 且 `COALESCE(resolved_at, created_at)` 超过保留期 | | open 行 | **永不归档**——它是待办队列,超期也要留在页面上 | | 保留期 | `COST_EXCEPTION_RETENTION_DAYS`,默认 **60** 天 | | 落点 | `cost_exceptions_archive`(保留原 id 与时间字段,加 `archived_at`) | | 执行 | `data-retention` 每日任务;每批 1000 行,`DELETE ... RETURNING` + `INSERT` 同语句,单批原子 | | 手动 | `npm run cost-exceptions:archive -- --dry-run` / `-- --days=90` | | 一次性清理 | `npm run cost-exceptions:archive -- --days=0`(不按时间过滤,归档全部已处理行;open 仍不归档) | 归档不删除审计痕迹,但热表查询与页面上不会再出现这些历史行;需要追溯时查归档表。 > **2026-10-25 已执行一次性清理(历史库)**:`--days=0` 口径归档 151,939 条 resolved 异常(152 批,3.9 秒)。该数字只作历史记录。 > 热表 260,253 → 108,314 行(全部为 open),`VACUUM FULL` 后体积 155 MB → 51 MB(heap 38 / idx 13), > 归档表 61 MB;主键重叠校验 0 条。生产环境不要在业务时段跑 `VACUUM FULL`,日常由 autovacuum 复用空间即可。 --- ## 十、关联文档 - 业务口径: [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) - 接口契约: [成本中心API契约.md](成本中心API契约.md) - 人工支出与备用金: [人工支出与备用金.md](人工支出与备用金.md) - 影子总账: [财务系统集成方案.md](财务系统集成方案.md) - 运维: [数据库SQL约束与运维.md](数据库SQL约束与运维.md)、[部署启动与数据库重建.md](部署启动与数据库重建.md)