docs(finance): 现行财务文档收敛为 5 篇,履约口径只定义一次

原 9 篇 3,647 行,履约分类口径在总纲、模型文档、交接总结里重复三遍,
且仍留着"三块履约""按小时人工成本"等已作废表述。本次收敛为 5 篇
1,778 行:

- 财务与履约口径总纲-2026-10-23.md(300 行):唯一口径来源
  七条硬口径、三个正交维度、真实履约构成、成本模型规则、人工全覆盖、
  实施顺序、进度、验收 SQL、防回归清单
- 成本中心与履约模型.md(471 行):数据模型、迁移 103–107、履约回填、
  销售事实、本地库原地改造、索引与性能
- 成本中心API契约.md(270 行):字段替换总表、枚举、前后端模块边界
- 人工支出与备用金.md(218 行):人工支出登记字段/状态机/权限 +
  客服备用金台账
- 财务系统集成方案.md:复式总账层(未实施)

旧的 4 篇(成本中心与三块履约模型、人工支出登记落地方案、
客服备用金台账实施方案、财务履约改造交接总结)移入
docs/archive/2026-10-23/ 并加归档提示。

履约分类判定 SQL 只在总纲保留一份,其它文档改为链接。
This commit is contained in:
yml2213
2026-10-23 16:06:40 +08:00
parent 63ae32d18c
commit 43f402cf72
15 changed files with 1103 additions and 337 deletions
+21 -21
View File
@@ -1,7 +1,7 @@
# order_site 文档索引
> 最后校准:2026-10-23,代码基线 `dev @ 5d1ba115 + 迁移 103–114`。
> 本文是 `docs/` 的唯一入口。**财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;接手改造先读 [财务履约改造交接总结-2026-10-23.md](财务履约改造交接总结-2026-10-23.md),本文只做索引与速查。
> 最后校准:2026-10-23,代码基线 `feat/finance-rewrite @ 7557de6d + 迁移 103–107`。
> 本文是 `docs/` 的唯一入口。**财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;数据模型看 [成本中心与履约模型.md](成本中心与履约模型.md),接口看 [成本中心API契约.md](成本中心API契约.md)。
> 新增文档必须在下表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。
## 一、先记住六条口径
@@ -48,7 +48,7 @@
| 名称 | 业务含义 | 当前状态 |
| --- | --- | --- |
| `manual_expenses` | **客服备用金支出明细**:客服从自己的备用金里充值/采购/赔偿,减少客服余额 | 已实施(迁移 100) |
| `manual_expense_records` | **公司人工支出/垫付登记**:按订单/成本项记录公司实际付出去的钱、付款人、收款人、凭证、审批 | 已实施 P0(迁移 111) |
| `manual_expense_records` | **公司人工支出/垫付登记**:按订单/成本项记录公司实际付出去的钱、付款人、收款人、凭证、审批 | 已实施 P0(迁移 106) |
后续做财务系统时,**不要把 `manual_expenses` 直接改名或复用为 `manual_expense_records`**。两者业务含义、审批链和报表口径不同,必须通过 `fund_expense_id` 关联;新人工支出登记会先扣备用金,再写人工支出记录。
@@ -71,24 +71,24 @@
| 模块 | 状态 | 事实 |
| --- | --- | --- |
| 客服备用金台账 | **已实施 P0** | 迁移 `100/101/113`;账户、流水、支出、对账;编号 `FA-<用户名>`;客服只能使用本人账户 |
| 客服备用金台账 | **已实施 P0** | 迁移 `100/101/106`;账户、流水、支出、对账;编号 `FA-<用户名>`;客服只能使用本人账户 |
| 后台用户中文显示名 | **已实施** | `admin_users.display_name`,备用金姓名触发器同步;审计、登录记录、任务处理人优先显示中文名 |
| 商品主数据 + 渠道 SKU 映射 | **已实施 P0(dev 分支)** | 745 个标准商品、4,426 条映射;只覆盖自动发货 38 个 + 接单平台 707 个。人工 11,349 项虽已归一但全部挂 `tpl:*` 模板商品,需在 P1 复核;人工/历史平台专用商品尚未建立 |
| 成本中心 | **已实施 P0(dev 分支)** | 迁移 `104/105/107/108/110/112`;38 个商品级版本 + 662 个规则级版本;人工走 `sys_manual_service` 实际快照 |
| 销售事实 / 订单毛利 | **已实施 P0(dev 分支)** | 迁移 `109`;94 天事实收入 ¥2,650.87 万、成本 ¥950.27 万;四种履约方式已统一读 `fulfillment_mode` |
| 公司人工支出登记 | **已实施 P0(dev 分支)** | 迁移 `111`;登记时强制扣客服备用金并写 `fund_expense_id`;客服只能使用本人账户 |
| 履约方式统一 | **已实施 P0(dev 分支)** | 迁移 `114`;316,585 项已回填;订单页、成本快照、销售事实只读 `fulfillment_mode`;人工项带核销码仍显示人工 |
| 商品主数据 + 渠道 SKU 映射 | **已实施 P0** | 745 个标准商品、4,426 条映射;只覆盖自动发货 38 个 + 接单平台 707 个。人工 11,349 项虽已归一但全部挂 `tpl:*` 模板商品,需在 P1 复核;人工专用商品尚未建立 |
| 成本中心 | **已实施 P0** | 迁移 `104`;`cost_items.cost_model` 取代 `cost_type + carrier_type`;38 个商品级版本 + 662 个规则级版本;人工走 `sys_manual_service` 实际快照 |
| 销售事实 / 订单毛利 | **已实施 P0** | 迁移 `105`;94 天事实收入 ¥2,650.87 万、成本 ¥950.27 万;`sales_daily_facts.fulfillment_mode` |
| 公司人工支出登记 | **已实施 P0** | 迁移 `106`;登记时强制扣客服备用金并写 `fund_expense_id`;客服只能使用本人账户 |
| 履约方式统一 | **已实施 P0** | 迁移 `107`;316,585 项已回填,四项计数与总纲 §二 一致;订单页、成本快照、销售事实只读 `fulfillment_mode` |
| 人工商品归一 | **未实施(P1)** | 人工单按 `spec.skuId` 建 `man:<skuId>`;历史 11,349 项 `tpl:*` 商品待复核 |
| 影子总账 | 未实施 | 现有财务页仍是 `UNION ALL` 业务事件拼装,不是复式总账 |
| 退款冲减 / 平台对账 / 期间关账 | 未实施 | 仅方案 |
> 分支状态(2026-10-23):`dev` 领先 `main` 20+ 个提交,迁移 `103–114` 尚未合并、未部署生产;生产口径仍以 `main` 为准。
> 分支状态(2026-10-23):重写分支 `feat/finance-rewrite` 把原 `103–114` 十二个迁移合并为 `103–107` 五个;本地库已按新 schema 原地改造并重跑回填,**尚未合并到 `dev`、未部署生产**;生产口径仍以 `main` 为准。
> 财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。
## 三、统一实施顺序
```text
0. 统一履约事实(fulfillment_mode + 成本模型) 已完成 P0(迁移 114)
0. 统一履约事实(fulfillment_mode + 成本模型正交) 已完成 P0(迁移 107)
1. 商品主数据 + 渠道 SKU 映射 已完成 P0(仅自动发货/接单平台)
2. 成本中心 + 销售事实 已完成 P0
3. 人工商品归一与上线后人工成本全覆盖 未实施(下一阶段)
@@ -97,7 +97,7 @@
6. 平台对账、报表、期间关账 未实施
```
**第 0 步已完成。** 迁移、回填、代码改造与验收结果见 [总纲 §八](财务与履约口径总纲-2026-10-23.md#八p0-实施记录统一履约事实)。下一步只做第 3 步人工商品归一,不要同时改退款或总账。
**第 0 步已完成。** 迁移、回填、代码改造与验收结果见 [成本中心与履约模型](成本中心与履约模型.md) §六、§八。下一步只做第 3 步人工商品归一,不要同时改退款或总账。
## 四、当前必须拍板
@@ -118,14 +118,13 @@
| 文档 | 状态 | 用途 |
| --- | --- | --- |
| [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) | **现行唯一口径** | 履约方式、成本模型、人工全覆盖、历史策略、P0 迁移/回填/代码清单、运营日检与验收 SQL |
| [财务履约改造交接总结-2026-10-23.md](财务履约改造交接总结-2026-10-23.md) | **交接摘要** | 本次改了什么、关键数字、下一步 P0、运营硬要求、验证命令与阅读顺序 |
| [项目全量分析报告-2026-10-17.md](项目全量分析报告-2026-10-17.md) | 历史参考 | 2026-10-17 架构与风险;财务口径以总纲为准 |
| [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) | **现行唯一口径** | 履约方式、成本模型、人工全覆盖、历史策略、实施顺序、运营日检与验收 SQL |
| [成本中心与履约模型.md](成本中心与履约模型.md) | 现行 | 商品、成本版本、快照、异常、销售事实的数据模型;迁移 `103–107`;履约回填;本地库原地改造;索引与性能 |
| [成本中心API契约.md](成本中心API契约.md) | 现行 | 成本中心接口字段替换总表、枚举、模块边界(后端 5 模块 + 前端 8 面板) |
| [人工支出与备用金.md](人工支出与备用金.md) | 现行(P0 已实施) | 人工支出登记字段/状态机/权限、客服备用金台账、本人账户与编号规则 |
| [财务系统集成方案.md](财务系统集成方案.md) | 现行方案(总账未实施) | 复式记账、总账表、对账、关账 |
| [成本中心与履约方式模型.md](成本中心与三块履约模型.md) | 现行(文件名保留) | 商品、成本快照、销售事实;履约分类以总纲为准 |
| [人工支出登记落地方案-2026-10-17.md](人工支出登记落地方案-2026-10-17.md) | 现行(P0 已实施) | 人工支出字段、入口、状态机、权限、备用金衔接 |
| [影子总账说明.md](影子总账说明.md) | 现行说明(未实施) | 影子总账是什么、和钱包流水的区别、对账与转正标准 |
| [客服备用金台账实施方案-2026-10-18.md](客服备用金台账实施方案-2026-10-18.md) | **已实施 P0** | 备用金账户、流水、支出、对账、本人账户与编号规则 |
| [项目全量分析报告-2026-10-17.md](项目全量分析报告-2026-10-17.md) | 历史参考 | 2026-10-17 架构与风险;财务口径以总纲为准 |
| [接单模板匹配引擎重构设计.md](接单模板匹配引擎重构设计.md) | 现行(P0/P1 已实施) | `kuaishou-91 / pdd / douyin-mashida` 三来源匹配引擎 |
| [履约配置/多发货平台与电子凭证整体链路.md](履约配置/多发货平台与电子凭证整体链路.md) | 现行 | 进单、履约、领取、电子凭证的整体链路 |
| [affiliate-dash-发货平台/affiliate-dash对接.md](affiliate-dash-发货平台/affiliate-dash对接.md) | 现行 | affiliate-dash 自动履约接口与字段 |
@@ -143,9 +142,9 @@
| 文件 | 用途 | 接入前必须处理 |
| --- | --- | --- |
| [drafts/2026-10-17-载体判定与人工块量化.sql](drafts/2026-10-17-载体判定与人工块量化.sql) | 载体分布/人工块量化查询、`carrier_type` 判定与回填 | 只读查询可直接用;DDL 部分按当前迁移号顺延,不占用 100/101 |
| [drafts/2026-10-17-成本中心与销售事实-DDL草案.sql](drafts/2026-10-17-成本中心与销售事实-DDL草案.sql) | `product_catalog`、`channel_product_mappings`、`cost_items/versions/snapshots/exceptions`、`sales_daily_facts` | 商品、成本、销售事实均已落地(`103–109`);系统成本项在 `110`;草案仅作设计追溯,不再作为建表依据 |
| [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql) | `manual_fulfillment_cases`、`manual_expense_records`、`manual_expense_events` | 人工支出登记已落地为 `111`;轻量人工 case 仍为草案 |
| [drafts/2026-10-17-载体判定与人工块量化.sql](drafts/2026-10-17-载体判定与人工块量化.sql) | 载体分布/人工块量化查询、`carrier_type` 判定与回填 | **已过时**:`carrier_type` 已从商品/成本/快照/异常表删除;只读查询里的口径结论已吸收进总纲 §二 |
| [drafts/2026-10-17-成本中心与销售事实-DDL草案.sql](drafts/2026-10-17-成本中心与销售事实-DDL草案.sql) | `product_catalog`、`channel_product_mappings`、`cost_items/versions/snapshots/exceptions`、`sales_daily_facts` | 已落地为 `103`–`105`;草案里的 `carrier_type` 已被 `cost_model` / `fulfillment_mode` 取代,仅作设计追溯 |
| [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql) | `manual_fulfillment_cases`、`manual_expense_records`、`manual_expense_events` | 人工支出登记已落地为 `106`;轻量人工 case 仍为草案 |
## 六、厂商协议与外部资料
@@ -165,6 +164,7 @@
| --- | --- |
| [archive/README.md](archive/README.md) | 2026-10-08 及更早的阶段分析、寄卖方案、性能快照、一次性迁移检查脚本;2026-10-17 复核文档 |
| [archive/2026-10-17/载体判定与成本中心落地-复核与草案.md](archive/2026-10-17/载体判定与成本中心落地-复核与草案.md) | 2026-10-17 第二轮复核与数据量化;结论已吸收进现行成本中心文档,仅作历史追溯 |
| [archive/2026-10-23/](archive/2026-10-23/) | 2026-10-23 重写前的四篇:`成本中心与三块履约模型`、`人工支出登记落地方案`、`客服备用金台账实施方案`、`财务履约改造交接总结`;结论已分别并入现行 5 篇文档 |
## 八、维护规则
@@ -1,10 +1,10 @@
# 载体判定与成本中心落地:复核与草案(2026-10-17 第二轮)
> 状态:**历史(2026-10-17 第二轮复核,已归档)**;基线:`main @ ccd5ed2a`;代码未改动。
> 说明:本文的现行结论已吸收进 [成本中心与三块履约模型.md](../../成本中心与三块履约模型.md) 与 [财务系统集成方案.md](../../财务系统集成方案.md);本文件只用于追溯 2026-10-17 的数据量化与 DDL 验证过程,不再作为实现依据。
> 说明:本文的现行结论已吸收进 [成本中心与三块履约模型.md](../../成本中心与履约模型.md) 与 [财务系统集成方案.md](../../财务系统集成方案.md);本文件只用于追溯 2026-10-17 的数据量化与 DDL 验证过程,不再作为实现依据。
> 数据来源:**本地只读克隆**(生产快照,`orders` 316,597 行,数据截至 2026-10-14 14:52),全程只读 SELECT,未连生产、未写入任何业务库。
> 金额单位:**分**(`orders.total_amount` / `work_orders.reward_amount` / `source_order_amount_fen` 均为分)。
> 上游文档:[成本中心与三块履约模型.md](../../成本中心与三块履约模型.md)、[项目全量分析报告-2026-10-17.md](../../项目全量分析报告-2026-10-17.md)。
> 上游文档:[成本中心与三块履约模型.md](../../成本中心与履约模型.md)、[项目全量分析报告-2026-10-17.md](../../项目全量分析报告-2026-10-17.md)。
本轮任务是把上一轮「只分析」的结论**复核到底、量化到数、落成可执行草案**。三件事:① 提交并推送待提交文档;② 收敛本机生产备份暴露面;③ 用只读 SQL 量化载体分布与人工块,并产出载体判定 + 成本中心两份 SQL 草案。
@@ -1,9 +1,11 @@
# 人工支出登记落地方案(2026-10-18 修订)
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**已实施 P0(`dev` 分支,迁移 111;订单页人工录单、备用金扣款为 2026-10-22/23 增量)**
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> **总口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。**
> **总口径以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准。**
> 依据:业务口径「人工块当前不以 SLA 监管为目标,先把流程搞清楚:哪个客服处理了哪些东西、付了多少钱、什么项目、关联哪个订单」。
> 数据来源:本地生产快照克隆(数据截至 2026-10-14)。
> 重要:**客服备用金支出已经实施**(`manual_expenses`);**公司人工支出登记**(`manual_expense_records`)也已实施,两张表通过 `fund_expense_id` 关联,不是同一张表。
@@ -11,8 +13,8 @@
> 重要:**人工支出实际上都走客服备用金**——登记时必须选择备用金账户,系统在同一事务内扣减余额、写 `fund_entries` 与 `manual_expenses`,再回填 `manual_expense_records.fund_expense_id`。
> 重要:**客服角色只能使用自己的备用金账户**(默认自动带入,不能选择别人);后端越权直接 403。
> 重要:**历史人工支出不补录**,只取少量样本做校准;**上线后每一笔人工单必须 100% 有成本记录**,否则不允许关账。
> 相关文档:[成本中心与履约方式模型.md](成本中心与三块履约模型.md) §8 · [财务系统集成方案.md](财务系统集成方案.md) §八 · [客服备用金台账实施方案](客服备用金台账实施方案-2026-10-18.md) ·
> DDL 草案 [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql)
> 相关文档:[成本中心与履约方式模型.md](../../成本中心与履约模型.md) §8 · [财务系统集成方案.md](../../财务系统集成方案.md) §八 · [客服备用金台账实施方案](../../人工支出与备用金.md) ·
> DDL 草案 [drafts/2026-10-17-人工支出登记-DDL草案.sql](../../drafts/2026-10-17-人工支出登记-DDL草案.sql)
---
@@ -282,7 +284,7 @@ reversed(预留) 反向登记 + 反向凭证,原凭证不动
## 八、在全局路线中的位置
> 全局顺序以 [README.md](README.md) §三 为准。本文只负责“公司人工支出登记”,不重复商品、成本中心、销售事实和影子总账的完整设计。
> 全局顺序以 [README.md](../../README.md) §三 为准。本文只负责“公司人工支出登记”,不重复商品、成本中心、销售事实和影子总账的完整设计。
| 本文内部阶段 | 内容 | 前置 |
| --- | --- | --- |
@@ -1,9 +1,11 @@
# 客服备用金台账实施方案(2026-10-18)
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**已实施 P0**(迁移 100/101/113;代码已上线到代码库,未部署生产)。
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> **总口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。**
> **总口径以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准。**
> 背景:两位主管向客服发放备用金,客服用于充值、采购、赔偿等;目前全部走人工,无法监控每笔资金用途和每个客服余额。
> 结论:这不是报销系统,而是**客服备用金(周转金)台账**。P0 已完成“账户 + 流水 + 支出 + 对账”,不做复杂审批和总账。
@@ -1,16 +1,18 @@
# 成本中心与履约方式模型(文件名保留)
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**现行(P0 已实施,履约方式已统一)**。商品主数据、成本中心、成本快照、销售事实、人工支出、`fulfillment_mode` 均已落地;人工商品归一与全覆盖仍待实施。
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> 数据基线:本地生产快照克隆(数据截至 2026-10-14)
> **财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责商品、成本、销售事实的数据模型。
> **财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责商品、成本、销售事实的数据模型。
> 文件名保留是为了历史链接稳定,正文不再使用“三块”作为分类依据。
> 关联:
> - [项目全量分析报告-2026-10-17.md](项目全量分析报告-2026-10-17.md)
> - [影子总账说明.md](影子总账说明.md)
> - [财务系统集成方案.md](财务系统集成方案.md)
> - [人工支出登记落地方案-2026-10-17.md](人工支出登记落地方案-2026-10-17.md)
> - [项目全量分析报告-2026-10-17.md](../../项目全量分析报告-2026-10-17.md)
> - [影子总账说明.md](../../影子总账说明.md)
> - [财务系统集成方案.md](../../财务系统集成方案.md)
> - [人工支出登记落地方案-2026-10-17.md](../../人工支出与备用金.md)
---
@@ -429,7 +431,7 @@ orders + order_items + channel_product_mappings + cost_snapshots
> **人工支出可追溯** ——「主要是把流程搞清楚:哪个人处理了哪些东西、付了多少钱。现在人工完全监管不到,
> 很多地方需要人工付款,也不知道付款项目、金额、订单号」。
> 因此本章顺序调整为:**① 人工支出登记 → ② 处理人可见(轻量 case) → ③ SLA 与监管指标(延后)**。
> DDL 见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql)。
> DDL 见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](../../drafts/2026-10-17-人工支出登记-DDL草案.sql)。
>
> **2026-10-22 进度**:客服备用金支出已实施(`fund.manual_expenses`),解决“客服手里有多少钱、这笔钱花到哪”;
> 公司口径的人工支出登记(`manual_expense_records`)也已实施(迁移 `111`),并在订单界面提供「人工录单」逐单录入实际成本;
@@ -467,7 +469,7 @@ carrier_reason
### 8.3 人工 case 模型
> 落地版见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql):已去掉 `due_at`/优先级等 SLA 字段,先只保留「谁在处理哪个订单项 + 结果 + 证据 + 支出登记」。下表为完整形态(阶段③)。
> 落地版见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](../../drafts/2026-10-17-人工支出登记-DDL草案.sql):已去掉 `due_at`/优先级等 SLA 字段,先只保留「谁在处理哪个订单项 + 结果 + 证据 + 支出登记」。下表为完整形态(阶段③)。
```text
manual_fulfillment_cases
@@ -1,6 +1,8 @@
# 财务/履约人工块修复总结(交接用)
> 状态:**交接摘要**。详细口径、迁移 SQL、代码清单与验收查询以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**交接摘要**。详细口径、迁移 SQL、代码清单与验收查询以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准。
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`。
> 数据基线:本地生产快照克隆,数据截至 2026-10-14。
> 分支:`dev` 领先 `main` 20+ 个提交;迁移 `103–114` 未部署生产;本轮只做本地提交,暂不推送。
@@ -60,7 +62,7 @@ provider === 'douyin_mashida' -> 自动发货
## 五、下一步只做 P1:人工商品归一
完整口径与验收见 [总纲 §八、§九](财务与履约口径总纲-2026-10-23.md#八p0-实施记录统一履约事实)。
完整口径与验收见 [总纲 §八、§九](../../财务与履约口径总纲-2026-10-23.md#八p0-实施记录统一履约事实)。
P0 已完成(本地):
@@ -122,12 +124,12 @@ PY
## 八、文档阅读顺序
1. [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md):唯一口径 + P0 执行清单。
2. [成本中心与履约方式模型.md](成本中心与三块履约模型.md):商品、成本版本、快照、销售事实。
3. [人工支出登记落地方案-2026-10-17.md](人工支出登记落地方案-2026-10-17.md):人工录单字段、审批、权限。
4. [客服备用金台账实施方案-2026-10-18.md](客服备用金台账实施方案-2026-10-18.md):备用金账户与流水。
5. [财务系统集成方案.md](财务系统集成方案.md) / [影子总账说明.md](影子总账说明.md):后续总账层。
6. [README.md](README.md):文档索引与分支状态。
1. [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md):唯一口径 + P0 执行清单。
2. [成本中心与履约方式模型.md](../../成本中心与履约模型.md):商品、成本版本、快照、销售事实。
3. [人工支出登记落地方案-2026-10-17.md](../../人工支出与备用金.md):人工录单字段、审批、权限。
4. [客服备用金台账实施方案-2026-10-18.md](../../人工支出与备用金.md):备用金账户与流水。
5. [财务系统集成方案.md](../../财务系统集成方案.md) / [影子总账说明.md](../../影子总账说明.md):后续总账层。
6. [README.md](../../README.md):文档索引与分支状态。
## 九、交接注意
+218
View File
@@ -0,0 +1,218 @@
# 人工支出与客服备用金
> 状态:**现行(P0 已实施,未部署生产)**。
> 最后校准:2026-10-23
> 代码基线:`feat/finance-rewrite @ 7557de6d + 迁移 100/101/106`
> **业务口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责人工支出的字段、流程、权限与备用金台账。
> 本文合并自原《人工支出登记落地方案-2026-10-17.md》与《客服备用金台账实施方案-2026-10-18.md》。
---
## 〇、先分清两张表
| 表 | 业务含义 | 状态 | 关键字段 |
| --- | --- | --- | --- |
| `manual_expenses` | **客服备用金支出**:客服从自己的备用金账户里充值/采购/赔偿 | 已实施(迁移 `100`) | `account_id` / `fund_entry_id` / `category` / `payee_*` / `evidence_json` |
| `manual_expense_records` | **公司人工支出登记**:按订单/成本项记录公司实际付出去的钱 | 已实施 P0(迁移 `106`) | `order_id` / `order_item_id` / `cost_item_id` / `paid_by_admin_user_id` / `status` / `cost_snapshot_id` |
结论:
1. 客服侧已收口的是"备用金余额与用途",解决"客服手里有多少钱、这笔钱花到哪"。
2. **公司口径的实际支出已落地**:后台「人工支出」页(`/admin/manual-expenses`)支持登记、审批、驳回、报销;通过后按成本项写 `cost_snapshots`,没有有效成本版本时进入异常队列。
3. 订单界面已接入「人工录单」:订单列表对人工单显示按钮、订单详情始终可录,弹窗锁定订单号、选择**备用金账户**并逐单填写**实际金额**,不存在小时单价或固定单量计价。
4. 两张表通过 `fund_expense_id` 关联,且该字段由系统写入:人工支出先从客服备用金账户扣款并生成 `manual_expenses`,再写 `manual_expense_records`。
5. **不要**把 `manual_expenses` 直接改名为 `manual_expense_records`,也不要让两张表共用索引/约束名。
---
## 一、核心口径
1. **人工成本逐单、按实际金额。** 不按小时、不按固定单量、不按固定单价。
2. **人工支出从客服备用金账户扣款。** 余额不足直接拒绝,不允许负余额。
3. **客服角色只能使用自己的备用金账户。** 后端越权直接 403;管理员/财务可代录到指定客服账户。
4. **历史人工支出不补录。** 只取少量样本做校准;历史缺口标记 `historical_missing`,不阻塞上线。
5. **上线后每一笔人工单必须 100% 有成本记录**,否则不允许关账。
6. **登记必须"发生在垫付当天"才有监管价值**;事后补记只能算账,不能算监管。
7. **客服备用金是周转金台账,不是报销系统。** P0 只做"账户 + 流水 + 支出 + 对账",不做复杂审批和总账。
---
## 二、客服备用金台账
### 2.1 落地结果
| 项 | 实际实现 |
| --- | --- |
| 数据库 | 迁移 `100_fund_accounts.sql`:`fund_accounts` / `fund_entries` / `manual_expenses` / `fund_reconciliations` |
| 显示名 | 迁移 `101_admin_display_name.sql`:`admin_users.display_name`,备用金 `owner_name` 由触发器同步 |
| 账户编号 | 迁移 `106_manual_expense_records.sql`:`FA-<大写用户名>`(如 `FA-KF_1`、`FA-JIAJIALI`),重名追加 `-2`、`-3` |
| 后端 | `admin-fund-service.ts`、`routes/admin/fund.ts`;金额全部用分,写操作使用事务与行锁 |
| 前端 | `AdminFundPage.tsx`、`FundManagementView.tsx`、`FundSupportView.tsx`;管理端与客服端分流 |
| 权限 | `fund.view / fund.issue / fund.expense / fund.reconcile / fund.adjust / fund.export` |
| 审计 | 所有写接口接入 `createJsonHandler({ audit })` |
### 2.2 系统要回答的四个问题
1. 公司现在有多少钱在主管和客服手里?
2. 每个客服当前余额是多少?
3. 每一笔钱花到哪、为什么花、有没有凭证?
4. 系统余额和客服实际余额对不对得上?
### 2.3 账户
`fund_accounts` 一行 = 一个备用金账户,归属一个管理员(客服或主管)。`account_no` 必须是 `FA-<大写用户名>`,重名时按 `id` 顺序追加 `-2`、`-3`。
### 2.4 流水
`fund_entries` 是**只追加**的账户流水。每笔备用金变动写一行,正数=入账,负数=出账,带 `entry_type`(`issue` 发放 / `expense` 支出 / `reconcile` 对账调整 / `adjust` 人工调整)与 `balance_after` 快照。
### 2.5 支出
`manual_expenses` 记录一次具体支出:账户、金额(分)、分类(`purchase` 采购 / `compensation` 赔偿 / `freight` 运费 / `platform_recharge` 平台充值 / `other`)、收款方信息、凭证、关联的 `fund_entry_id`。
### 2.6 对账
`fund_reconciliations` 记录期初/期末账面余额、实际盘点余额与差异说明。差异必须由主管确认并留痕。
### 2.7 P0 未实现(仍在 P1)
异常队列、批量发放/对账、导出、低余额提醒、大额支出主管确认;支出凭证目前只校验数组非空,未校验对象存储 key 是否真实存在。
---
## 三、公司人工支出登记
### 3.1 落点
- 迁移:`apps/backend/src/db/migrations/106_manual_expense_records.sql`
- 服务:`apps/backend/src/services/finance/manual-expense-service.ts`;备用金扣款唯一写入口 `admin-fund-service.ts` 的 `writeFundExpense`
- 接口:`apps/backend/src/routes/admin/finance.ts` 的 `/finance/manual-expenses*`;订单页专用 `POST /finance/manual-order-costs`
- 页面:`apps/frontend/src/pages/admin/AdminManualExpensePage.tsx`,入口 `/admin/manual-expenses`
- 订单入口:订单列表/详情「人工录单」打开 `ManualOrderCostModal`,锁定订单号、选择订单项与备用金账户、填写本单实际金额;支持深链 `/admin/orders/:orderId?manualCost=1`
- 权限:查看 `finance.view` / `fund.view`,登记 `fund.expense` / `finance.config`,审核 `finance.review`
### 3.2 资金流(同一事务)
```text
锁定 fund_accounts 行
→ 校验余额
→ 扣减余额
→ 写 fund_entries
→ 写 manual_expenses
→ 写 manual_expense_records.fund_expense_id
```
余额不足直接拒绝,**不允许负余额**。
### 3.3 成本快照
通过后以 `subject_type='manual_case'`、`subject_id=expense.id` 写 `actual` 快照,金额 = 登记的实际金额。同一订单项多笔支出可累加,不会互相覆盖。
没有匹配到有效成本版本时,写 `manual_expense_cost_missing` 异常(`status='open'`),不允许静默记 0。
### 3.4 状态机
```text
draft ──> submitted ──> approved ──> reimbursed
│
└──> rejected
```
| 状态 | 含义 | 可执行动作 |
| --- | --- | --- |
| `draft` | 草稿 | 编辑、提交 |
| `submitted` | 待审批 | 审批通过 / 驳回 |
| `approved` | 已通过 | 标记已报销;此时写 `actual` 成本快照 |
| `rejected` | 已驳回 | 需填 `reject_reason` |
| `reimbursed` | 已报销 | 终态 |
| `reversed` | 已冲正 | 预留,关联 `reversal_of_id` |
状态变更只追加到 `manual_expense_events`,不更新、不删除历史事件。
### 3.5 字段
```text
manual_expense_records
expense_no -- 唯一编号
order_id, order_item_id, work_order_id
fund_expense_id -- 关联 manual_expenses.id;系统写入
cost_item_id -- 付款项目 -> cost_items.cost_model
expense_category -- goods_purchase / compensation / refund_advance /
-- service_fee / freight / recharge / other
amount_fen -- 实际金额(分),> 0
currency
paid_by_admin_user_id, paid_by_name
payee_type, payee_name, payee_account
payment_method
paid_at
status, approved_by, approved_at, rejected_by, rejected_at, reject_reason
reimbursed_by, reimbursed_at
evidence_json -- 凭证:支付截图对象 key、流水号、聊天记录 key
cost_snapshot_id
reversal_of_id
expense_batch_no, note
```
### 3.6 凭证
`evidence_json` **至少留一项**可追溯凭证(对象存储 key、流水号或聊天记录 key),否则拒绝提交。
---
## 四、订单页「人工录单」
订单里的数据足够预填 90%:店铺、商品、规格、数量、用户实付、支付时间、平台单号、履约标签、状态,后台订单列表/详情已经全部具备。客服实际只需要补 **4 件事**:渠道/项目、金额、收款方、凭证(外加默认带出的「谁、什么时候」)。
弹窗规则:
- 锁定订单号,不允许改;
- 必须选择备用金账户(客服角色自动带入本人账户且不可改);
- 金额按本单实际金额填写,**没有小时单价或固定单量**;
- 付款时间不能晚于当前时间,也不能早于订单支付时间。
---
## 五、每日必查与关账门槛
| 指标 | 目标 |
| --- | ---: |
| 人工订单项无人工支出记录 | 0 |
| 人工支出无 `fund_expense_id`(上线后) | 0 |
| 人工支出无成本快照 | 0 |
| 人工支出金额与备用金流水不一致 | 0 |
| 成本异常 open 中人工项 | 0(历史标记除外) |
关账门槛:
- 存在人工单无成本记录:**不允许关账**;
- 存在人工支出未关联备用金:**不允许关账**;
- 存在人工支出与备用金流水金额不一致:**不允许关账**。
核对 SQL 见 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) §八。
---
## 六、异常处理
- 发现人工单无成本:当天补录;补录仍走备用金扣款,不允许直接改余额。
- 备用金余额不足:拒绝登记,由财务充值后再录;不允许负余额。
- 同一订单项多笔支出:允许累加(渠道费、加急费、补差价),但每笔都必须有独立备用金流水。
- 历史人工单:标记 `historical_missing`,不补录、不计入上线后缺口。
---
## 七、历史样本校准(只做参考)
- 每个来源抽 20–30 单,覆盖低价/中价/高价;
- 只用于估算历史人工成本区间与毛利率,**不写 `cost_snapshots`**;
- 样本结论单独记录,禁止回填成逐单成本;
- 历史平台 53,901 项同理,只有拿到旧平台结算数据才允许按历史成本入账。
---
## 八、关联文档
- 业务口径: [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md)
- 数据模型: [成本中心与履约模型.md](成本中心与履约模型.md)
- 影子总账: [财务系统集成方案.md](财务系统集成方案.md)
- 订单读取与履约展示: [成本中心与履约模型.md](成本中心与履约模型.md) §六
@@ -40,7 +40,7 @@ kuaishou-cloud / kuaishou-feifei 通道已下线,affiliate-dash 负责真实
当前真实数据:work_order 118,053 / affiliate_dash 41,676 / legacy_platform 53,901 / manual 102,955,合计 316,585。
详细设计见 [../财务与履约口径总纲-2026-10-23.md](../财务与履约口径总纲-2026-10-23.md) 与 [../成本中心与履约方式模型.md](../成本中心与三块履约模型.md)。
详细设计见 [../财务与履约口径总纲-2026-10-23.md](../财务与履约口径总纲-2026-10-23.md) 与 [../成本中心与履约模型.md](../成本中心与履约模型.md)。
## 2. 目标链路
+2 -2
View File
@@ -2,13 +2,13 @@
> 状态:**现行说明(未实施)**
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> 代码基线:`feat/finance-rewrite @ 7557de6d + 迁移 103–107`
> 配套方案:[财务系统集成方案.md](财务系统集成方案.md)
> **总口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。**
> 前置:统一履约事实、商品主数据、渠道映射、成本中心与销售事实;实施顺序以总纲第六节为准。
> 一句话:**影子总账是一套“跟着真实业务跑、但暂时不接管余额”的复式会计账。** 它先把每一笔业务翻译成借贷分录,和现有钱包/订单/提现数据逐日对账;确认长期对得平之后,再让它成为正式总账。
>
> 边界:影子总账解决“钱怎么记、怎么对”,不负责“商品是什么、成本是多少、人工谁处理”。商品与成本见 [成本中心与履约方式模型.md](成本中心与三块履约模型.md),人工履约见同一文档第八节。
> 边界:影子总账解决“钱怎么记、怎么对”,不负责“商品是什么、成本是多少、人工谁处理”。商品与成本见 [成本中心与履约模型.md](成本中心与履约模型.md),人工支出见 [人工支出与备用金.md](人工支出与备用金.md)。
> 当前进度:客服备用金、商品与成本中心、成本快照、销售事实、公司人工支出登记已实施;`finance_*` 表、分录、对账、转正均未实施。人工支出现在强制走客服备用金;历史人工支出不补录,上线后必须全覆盖。
+270
View File
@@ -0,0 +1,270 @@
# 成本中心 API 契约
> 状态:**现行**。后端服务与前端页面都必须严格按此实现,不得各自发挥。
> 最后校准:2026-10-23
> 代码基线:`feat/finance-rewrite @ 7557de6d + 迁移 103–107`
> 详细业务口径见 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md);数据模型见 [成本中心与履约模型.md](成本中心与履约模型.md)。
## 一、字段替换总表
| 旧字段 | 新字段 | 出现位置 |
| --- | --- | --- |
| `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 |
## 二、枚举映射
### 2.1 `cost_model`(取代 `cost_type` + `carrier_type`)
成本模型与履约方式正交:专属模型只对对应履约方式生效,通用模型对所有履约方式生效。
| 新值 | 含义 | 取代旧的 |
| --- | --- | --- |
| `affiliate_purchase` | 自动发货上游采购成本 | `cost_type=auto_purchase` |
| `work_order_reward` | 接单平台打手报酬 | `cost_type=work_order_reward` |
| `manual_actual` | 人工客服实际支出 | `cost_type=manual_service` |
| `platform_fee` | 平台服务费 | `cost_type=platform_fee` |
| `payment_fee` | 支付通道费 | `cost_type=payment_fee` |
| `after_sales_loss` | 售后损失 | `cost_type=after_sales_loss` |
| `other` | 其他 | `cost_type=shipping_other` / `cost_type=other` |
### 2.2 `pricing_mode`(精简为 3 个)
`per_item` / `per_order` / `percent_of_revenue`
(删除 `per_hour`、`fixed_daily`——人工成本逐单按实际金额,不按工时)
### 2.3 `fulfillment_mode`(履约方式,唯一真源)
`work_order` / `affiliate_dash` / `legacy_platform` / `manual`
## 三、成本项接口
```
POST /api/v1/admin/cost-center/cost-items
GET /api/v1/admin/cost-center/cost-items
PATCH /api/v1/admin/cost-center/cost-items/:id
DELETE /api/v1/admin/cost-center/cost-items/:id
```
请求体(创建/更新):
```jsonc
{
"code": "可选;留空则按 costModel 自动生成",
"name": "必填",
"costModel": "affiliate_purchase | work_order_reward | manual_actual | platform_fee | payment_fee | after_sales_loss | other",
"pricingMode": "per_item | per_order | percent_of_revenue",
"unitName": "件",
"currency": "CNY",
"status": "active | archived",
"note": ""
}
```
**不再接受 `costType` 与 `carrierType`。**
响应项(`CostItem`):
```jsonc
{
"id": 10,
"code": "sys_affiliate_purchase",
"name": "自动发货上游采购成本",
"costModel": "affiliate_purchase",
"pricingMode": "per_item",
"unitName": "件",
"currency": "CNY",
"status": "active",
"note": "",
"snapshotCount": 0,
"createdAt": "...",
"updatedAt": "..."
}
```
## 四、商品接口
商品**不再返回 `carrierType`**。前端若要按来源分组,用 `canonicalSku` 前缀:
| 前缀 | 来源 | 建议展示名 |
| --- | --- | --- |
| `aff:` | 自动发货 | 自动发货 |
| `tpl:` | 接单模板 | 接单平台 |
| `wo:` | 工单证据 | 接单平台 |
| `man:` | 人工 | 人工 |
| 其他 | 未分类 | 未分类 |
`GET /api/v1/admin/cost-center/products` 的 `carrierType` 查询参数**保留**,
但语义改为「按 `canonical_sku` 前缀筛选」,取值 `affiliate_dash | work_order | manual`。
后端实现方式:
```sql
AND (
($n = 'affiliate_dash' AND p.canonical_sku LIKE 'aff:%')
OR ($n = 'work_order' AND (p.canonical_sku LIKE 'tpl:%' OR p.canonical_sku LIKE 'wo:%'))
OR ($n = 'manual' AND p.canonical_sku LIKE 'man:%')
)
```
## 五、成本版本接口
路径不变,字段不变:
```jsonc
{
"id": 1,
"costItemId": 10,
"costItemCode": "sys_affiliate_purchase",
"costItemName": "自动发货上游采购成本",
"costModel": "affiliate_purchase",
"pricingMode": "per_item",
"scopeType": "product | rule | shop | provider | global",
"provider": "", "shopId": "",
"canonicalProductId": 3, "canonicalSku": "aff:SKU-1", "canonicalName": "...",
"workProductRuleId": null, "workProductRuleName": "", "workProductRuleKey": "",
"ruleRewardAmountFen": 0, "ruleUnitPriceFen": 0,
"revenueItemCount": 0, "revenueFen": 0, "revenueAvgFen": 0,
"rewardAvgFen": 0, "rewardFen": 0, "amountKind": "unit_price | fixed",
"isSystemPlaceholder": false,
"amountFen": 1000, "rateBps": 0,
"effectiveFrom": "...", "effectiveTo": null,
"source": "manual | upstream_sync | import",
"status": "draft | active | retired",
"changeReason": "",
"createdByAdminUserId": null, "approvedByAdminUserId": null,
"createdAt": "...", "updatedAt": "..."
}
```
`costItemCarrierType` 字段**删除**,改为 `costItemCostModel`。
## 六、成本异常接口
路径不变。请求参数 `carrierType` **删除**;响应 `carrierType` **删除**,改为 `fulfillmentMode`。
若前端要按履约方式筛选异常,用参数 `fulfillmentMode`:
```jsonc
// 请求
{ "status": "open", "subjectType": "order_item", "fulfillmentMode": "manual" }
```
后端实现:join `order_items oi ON oi.id = ce.order_item_id` 并过滤 `oi.fulfillment_mode`。
响应项:
```jsonc
{
"id": 1,
"subjectType": "order_item",
"subjectId": 316571,
"orderItemId": 316571,
"workOrderId": null,
"fulfillmentMode": "manual",
"reason": "cost_missing",
"status": "open",
"detail": {},
"createdAt": "...", "resolvedAt": null, "resolvedByAdminUserId": null
}
```
## 七、销售事实接口
`GET /api/v1/admin/sales-profit` 的分组维度字段名从 `carrierType` 改为 `fulfillmentMode`:
```jsonc
{
"fulfillmentMode": "manual",
"itemCount": 102955,
"grossRevenueFen": 1219202470,
"costAmountFen": 32000,
"costMissingCount": 102954,
"grossProfitFen": -1219200470
}
```
筛选参数同样从 `carrierType` 改为 `fulfillmentMode`,取值四种履约方式。
## 八、成本中心概览接口
`GET /api/v1/admin/cost-center/overview` 的统计改为按来源前缀与成本模型分组:
```jsonc
{
"productCount": 745,
"mappingCount": 4426,
"costItemCount": 3,
"activeVersionCount": 701,
"openExceptionCount": 157822,
"unmappedOrderItemCount": 145507,
"productSourceCounts": {
"affiliate_dash": 38,
"work_order": 707,
"manual": 0,
"uncategorized": 0
},
"costModelCounts": {
"affiliate_purchase": 1,
"work_order_reward": 1,
"manual_actual": 1
}
}
```
`GET /api/v1/admin/cost-center/options` 返回的 `products[].source` 同样是来源前缀推导值,
`costItems[].costModel` 取代原 `carrierType`。
## 九、后端模块边界(已完成拆分)
原 `apps/backend/src/services/admin/admin-cost-center-service.ts`(4,248 行)已拆为:
| 新文件 | 行数 | 负责的导出 |
| --- | ---: | --- |
| `services/cost-center/cost-center-shared.ts` | ~800 | 类型、常量、归一化函数、`mapXxx` 映射函数、provider 归一、`costModelScopeSql` |
| `services/cost-center/cost-center-overview-service.ts` | ~150 | 总览统计与选项 |
| `services/cost-center/product-catalog-service.ts` | ~1,000 | 商品与渠道映射的 CRUD、回填、选项 |
| `services/cost-center/template-bootstrap-service.ts` | ~1,380 | 接单模板引导、自动发货商品同步、工单证据回填、模板成本版本同步、`buildTemplateMappingPlan` |
| `services/cost-center/cost-definition-service.ts` | ~1,240 | 成本项 / 成本版本 / 成本异常 / 成本预览 / 成本快照写入 |
原文件**已删除**,路由 `apps/backend/src/routes/admin/cost-center.ts` 从新模块导入。
`routes/admin/worker-platform/config.ts` 的模板成本同步也从新模块导入。
## 十、前端模块边界(已完成拆分)
原 `apps/frontend/src/pages/admin/AdminCostCenterPage.tsx`(2,887 行)已拆为:
| 新文件 | 内容 |
| --- | --- |
| `pages/admin/cost-center/CostCenterShell.tsx` | 页面骨架、顶部来源切换(全部/自动发货/接单平台/人工)、Tab 定义 |
| `pages/admin/cost-center/shared.ts` | 常量、类型、标签函数 |
| `pages/admin/cost-center/CostCenterContext.ts` | 页面上下文(`useCostCenter`) |
| `pages/admin/cost-center/ProductCatalogPanel.tsx` | 标准商品 Tab |
| `pages/admin/cost-center/ChannelMappingPanel.tsx` | 渠道映射 Tab |
| `pages/admin/cost-center/AffiliateSkuPanel.tsx` | 自动发货 SKU Tab |
| `pages/admin/cost-center/CostItemPanel.tsx` | 成本项 Tab |
| `pages/admin/cost-center/CostVersionPanel.tsx` | 成本版本 Tab |
| `pages/admin/cost-center/CostExceptionPanel.tsx` | 成本异常 Tab |
| `pages/admin/cost-center/CostPreviewPanel.tsx` | 成本预览 Tab |
`AdminCostCenterPage.tsx` 只保留 `export default` 转发到 `CostCenterShell`(18 行)。
类型文件 `apps/frontend/src/types/admin/cost-center.ts` 已按本契约改名:
```ts
export type FulfillmentMode = 'work_order' | 'affiliate_dash' | 'legacy_platform' | 'manual'
export type ProductSource = 'affiliate_dash' | 'work_order' | 'manual' | ''
export type CostModel =
| 'affiliate_purchase'
| 'work_order_reward'
| 'manual_actual'
| 'platform_fee'
| 'payment_fee'
| 'after_sales_loss'
| 'other'
```
`CostCarrierType` 已删除。
+471
View File
@@ -0,0 +1,471 @@
# 成本中心与履约模型
> 状态:**现行**。商品主数据、成本中心、成本快照、销售事实、履约事实均已落地。
> 最后校准:2026-10-23
> 代码基线:`feat/finance-rewrite @ 7557de6d + 迁移 103–107`
> 数据基线:本地生产快照克隆(数据截至 2026-10-14),已按新 schema 原地改造
> **业务口径以 [财务与履约口径总纲-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_expense_records ── fund_expense_id ── manual_expenses
```
四条不变式:
1. **履约方式只有一处存**:`order_items.fulfillment_mode`。快照、异常、销售事实要展示履约方式时 join `order_items`。
2. **商品来源只由 `canonical_sku` 前缀表达**,商品表不存载体列。
3. **成本模型只表达"怎么算"**:`cost_items.cost_model` 与履约方式正交,不复用同一字段。
4. **成本金额只在一处改**:`cost_versions`;业务表只保存快照,历史成本永不回写。
---
## 一、迁移基线:12 个合并为 5 个
原 `dev` 分支上,商品与成本中心用 12 个迁移(`103`–`114`,1,087 行)分批落地,字段反复摇摆。2026-10-23 重写为 5 个:
| 迁移 | 内容 | 行数 |
| --- | --- | ---: |
| `103_product_catalog.sql` | `product_catalog`(无载体列)+ `channel_product_mappings`(含匹配键)+ `order_items` 商品/归一字段 | 152 |
| `104_cost_center.sql` | `cost_items`(`cost_model`)+ `cost_versions` + `cost_snapshots`(无载体列)+ `cost_exceptions`(无载体列)+ 订单项收入固化 + 系统成本项 | 476 |
| `105_sales_daily_facts.sql` | `sales_daily_facts`(`fulfillment_mode`) | 104 |
| `106_manual_expense_records.sql` | `manual_expense_records` + `manual_expense_events` + 备用金账户编号可读化 | 201 |
| `107_order_item_fulfillment_mode.sql` | `order_items.fulfillment_mode` 三列 | 48 |
### 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` | (删除,只剩三种) | 成本项接口 |
### 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 接单模板批量初始化
662 个接单模板不允许逐个手工建商品。后台「从接单模板初始化」按以下规则批量生成:
1. **每个启用模板生成 1 个标准商品**,内部编码 `tpl:<rule_key>`;暂无历史工单的模板也先建商品。
2. **历史工单直接命中模板**,以 `work_orders.product_rule_id` 作为映射的第一手证据,不只看原始 `sku_code`。
3. 同一 `provider + shop_id + sku_code` 只命中一个模板时,生成 `sku_code` 精确映射。
4. 编码被多个模板复用时,继续用"编码 + 归一名称系列"和"编码 + 精确名称"消歧;只有唯一归属才生成映射。
5. 同一编码 + 同一名称仍命中多个模板时,**不选最新、不按次数猜**;跳过并列入冲突清单。
6. 来源 + 店铺内名称系列唯一时,额外生成 `sku_name_series` 兜底,永远排在编码类映射之后。
7. 数量系列(1个/2个/5个/10个,秘钥/密钥等等价写法)归为同一标准商品;数量写入 `normalized_quantity`,**不能把"5个"再写成 `quantity_multiplier=5`**,否则会重复乘算。
8. 重跑初始化时只清理 `source='import'` 且备注为"接单模板 … 自动初始化"的映射,并重置由这些映射带出的订单项 `canonical_product_id`;人工映射、成本版本、快照和真实订单/工单不删除。
本地验证结果(2026-10-18):启用模板 662 个;分层映射 4,426 条(编码精确 2,302、编码 + 精确名称 440、编码 + 名称系列 160、名称系列兜底 1,524);可唯一归一 86,533 个有证据订单项,另有 25,918 个订单项属于 32 组真实冲突,保持未映射。
### 2.5 自动发货商品
自动发货只有 affiliate-dash 一个上游渠道,SKU 固定归一为 `aff:<skuCode>`,**不写 `channel_product_mappings`**。渠道映射只代表"平台 SKU 能归一到标准商品",不代表自动发货。
上游价格用 `apps/backend/scripts/sync-affiliate-dash-prices.ts` 批量导入(清单见 `apps/backend/scripts/data/affiliate-dash-prices.json`,38 条):按 `aff:<skuCode>` 补建/更新商品,并给 `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` 快照;版本先为 `draft` 占位 |
工单报酬的三种快照:
- 工单**验收**后按 `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`。
### 3.5 `cost_exceptions`
```text
id
subject_type, subject_id
order_item_id, work_order_id
reason -- product_mapping_missing / cost_missing / manual_expense_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` 在迁移 `104` 里固化:
| `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 重写后重跑):`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 分钟先归集待判定的订单项,再算成本与销售事实。
---
## 八、本地库原地改造(2026-10-23 已执行)
生产尚未执行过 `103–114`,本地库需要从旧 schema 改到新 schema。改造脚本:`scripts/rewrite-local-db-2026-10-23.sql`。
改造矩阵(列级 `ALTER`,不重建表、不动源表):
| 表 | 改造 |
| --- | --- |
| `product_catalog` | 删 `carrier_type` |
| `cost_items` | `cost_type` + `carrier_type` → `cost_model`;`auto_purchase` 编码统一为 `sys_affiliate_purchase` |
| `cost_snapshots` | 删 `carrier_type`、删未使用的 `manual_case_id` |
| `cost_exceptions` | 删 `carrier_type` |
| `sales_daily_facts` | `carrier_type` → `fulfillment_mode`(改名) |
| `manual_expense_records` | `cost_item_id` 改 `NOT NULL` |
| `work_product_rules` | 删 `legacy_cost_source` |
| `order_items` | **不动**(履约事实、收入固化、商品指向全部保留) |
执行顺序:
```bash
# 1. 备份(已完成)
# backups/postgres/order_site_pre_rewrite_20261023.dump
# 2. 删旧迁移记录 + 列级改造
docker exec -i order_site-postgres-1 psql -U postgres -d order_site \
-v ON_ERROR_STOP=1 < scripts/rewrite-local-db-2026-10-23.sql
# 3. 应用新迁移 103–107
npm run db:migrate
# 4. 重跑履约回填(幂等)
npm run fulfillment:backfill
# 5. 分窗口重算销售事实
npm run finance:sync -- --mode=facts --from=2026-07-10 --to=2026-08-08
# ...按 30 天窗口依次执行
```
改造后数据量核对:商品 745、映射 4,426、成本项 3、成本版本 702、快照 158,764、异常 248,878、销售事实 14,798、人工支出 1、订单项 316,585 —— 全部与改造前一致。
`check-sql-guard.ts` 对 `103`–`114` 段迁移放开字节校验(这一组在分支内整体重写);`001`–`102` 仍必须保持字节不变。
---
## 九、索引与性能
| 索引 | 用途 |
| --- | --- |
| `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 |
---
## 十、关联文档
- 业务口径: [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md)
- 接口契约: [成本中心API契约.md](成本中心API契约.md)
- 人工支出与备用金: [人工支出与备用金.md](人工支出与备用金.md)
- 影子总账: [财务系统集成方案.md](财务系统集成方案.md)
- 运维: [数据库SQL约束与运维.md](数据库SQL约束与运维.md)、[部署启动与数据库重建.md](部署启动与数据库重建.md)
+1 -1
View File
@@ -1,6 +1,6 @@
# 接单模板匹配引擎重构设计
> 目标读者:后端 / 后台运营。状态:**P0 + P1 已实施;P2 / P3 待办**。最后校准:2026-10-23(代码基线 `dev @ 5d1ba115 + 迁移 103–114`;匹配业务逻辑未变)。
> 目标读者:后端 / 后台运营。状态:**P0 + P1 已实施;P2 / P3 待办**。最后校准:2026-10-23(代码基线 `feat/finance-rewrite @ 7557de6d + 迁移 103–107`;匹配业务逻辑未变)。
>
> 一句话:把「订单信息 → 接单模板(价格/拼单/要求)」的匹配,从散落的多个消费点、平台判断和"店铺必填"的绑定表里,收敛为**一个平台无关的引擎**:匹配条件完全由**订单信息**表达,**店铺只是可选条件之一**,新增来源只加一个适配器 + 一组配置。
>
+84 -285
View File
@@ -1,38 +1,42 @@
# 财务与履约口径总纲(2026-10-23)
# 财务与履约口径总纲(2026-10-23 重写版)
> 状态:**现行唯一口径**。本文是财务、成本、履约分类的总入口;其他文档与本文件冲突时,以本文为准。
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`。历史文档中的旧基线、旧三块模型、按小时人工成本等表述全部作废。
> 最后校准:2026-10-23
> 代码基线:`feat/finance-rewrite @ 7557de6d + 迁移 103–107`
> 数据基线:本地生产快照克隆(数据截至 2026-10-14),已按新 schema 原地改造
> 适用范围:订单接入、履约判定、商品归一、成本快照、销售事实、人工支出、备用金、后续总账与对账。
---
## 〇、六条硬口径
## 〇、七条硬口径
1. **电子凭证不是履约方式,也不是成本类型。** 快手电子凭证只是核销码/兑换码,抖音 DYC 券(`redeem_certificates`)同理;一个有核销码的 91/快手或抖音订单,仍可能走自动发货、接单平台或人工。
2. **旧平台通道已经下线。** `kuaishou_ct_assisted`、`kuaishou_feifei` 只保留历史数据,报表中统一标记为 `legacy_platform`,不得混入人工。
3. **人工的定义只有一个:** 已支付订单项,没有工单、没有自动发货任务、没有历史平台任务,即为人工;有没有核销码不影响判定。
4. **人工成本逐单、按实际金额。** 不按小时、不按固定单量、不按固定单价;必须从客服备用金账户扣款,客服只能使用本人账户。
5. **历史人工支出不补录。** 只取少量样本做校准参考;历史缺口统一标记为“历史未采集”,不阻塞上线。
5. **历史人工支出不补录。** 只取少量样本做校准参考;历史缺口统一标记为"历史未采集",不阻塞上线。
6. **上线后人工账单必须 100% 可监控。** 每一笔人工单必须有处理人、实际金额、备用金支出与成本快照;缺失即进入异常队列,不允许关账。
7. **履约方式只有一个真源。** `order_items.fulfillment_mode` 是唯一事实来源,商品表、成本项表、快照表、异常表、销售事实表都不再各自存一份"载体"。
---
## 一、履约方式与成本模型必须分开
## 一、三个正交维度
当前系统把“怎么交付”和“成本怎么算”混在同一个 `carrier_type` 里,导致同一订单在订单页、成本快照、销售事实中出现不同分类。正确模型拆成三个正交维度:
系统过去把"怎么交付"和"成本怎么算"混在同一个 `carrier_type` 里,导致同一订单在订单页、成本快照、销售事实中出现不同分类。正确模型拆成三个正交维度:
| 维度 | 取值 | 回答的问题 |
| --- | --- | --- |
| `fulfillment_mode` | `work_order` / `affiliate_dash` / `legacy_platform` / `manual` | 这单最终由谁执行 |
| `voucher_state` | 无 / 已发码 / 已核销 / 已销毁 | 有没有快手核销码、走到哪一步 |
| `cost_model` | `work_order_reward` / `affiliate_purchase` / `manual_actual` / `legacy_cost` / `none` | 这单成本怎么算 |
| 维度 | 取值 | 回答的问题 | 存储位置 |
| --- | --- | --- | --- |
| `fulfillment_mode` | `work_order` / `affiliate_dash` / `legacy_platform` / `manual` | 这单最终由谁执行 | `order_items.fulfillment_mode`(唯一真源) |
| `voucher_state` | 无 / 已发码 / 已核销 / 已销毁 | 有没有核销码、走到哪一步 | 订单读取时单独计算 |
| `cost_model` | `affiliate_purchase` / `work_order_reward` / `manual_actual` / `platform_fee` / `payment_fee` / `after_sales_loss` / `other` | 这单成本怎么算 | `cost_items.cost_model` |
硬规则:
- `voucher_state` **不参与** `fulfillment_mode` 判定;
- `fulfillment_mode` 必须在履约决策时写入 `order_items`,不能每次查询临时推断;
- 成本快照、销售事实、后台订单页只读同一份 `fulfillment_mode`;
- 一个订单项只能有一个 `fulfillment_mode`,核销码可以有多个状态事件。
- 一个订单项只能有一个 `fulfillment_mode`,核销码可以有多个状态事件;
- **商品来源不由履约方式推导**:商品的来源由 `canonical_sku` 前缀表达(`aff:` / `tpl:` / `wo:` / `man:`),不能反推履约方式。
---
@@ -46,7 +50,7 @@
| **人工 `manual`** | **102,955** | **¥1,219.20 万** | **仅 1 条 ¥320,历史基本未采集** |
| 合计 | 316,585 | ¥2,650.87 万 | — |
> **数字口径必须先分清“全量”和“未归一”。** 人工全量是 102,955 项;其中 11,349 项已有 `canonical_product_id`,但这 11,349 项全部指向 `tpl:*` 接单模板商品,只说明 SKU 命中过模板,**不代表它们有工单或走过接单平台**。真正未归一的是 91,606 项。历史平台 53,901 项全部未归一。两者相加才是报表里的“未归一 145,507 项”。
> **数字口径必须先分清"全量"和"未归一"。** 人工全量是 102,955 项;其中 11,349 项已有 `canonical_product_id`,但这 11,349 项全部指向 `tpl:*` 接单模板商品,只说明 SKU 命中过模板,**不代表它们有工单或走过接单平台**。真正未归一的是 91,606 项。历史平台 53,901 项全部未归一。两者相加才是报表里的"未归一 145,507 项"。
人工 102,955 项的来源与核销码事实:
@@ -56,7 +60,7 @@
| 拼多多 | 2,335 | ¥41.74 万 | 无快手凭证 |
| 抖音 | 2 | ¥62.90 | 无快手凭证 |
这张表直接说明:**91 人工单全部有核销码,但仍然是人工。** 把“有核销码”当成 `voucher_only` 或自动发货,是订单页漏掉人工块的直接原因。
这张表直接说明:**91 人工单全部有核销码,但仍然是人工。** 把"有核销码"当成 `voucher_only` 或自动发货,是订单页漏掉人工块的直接原因。
归一潜力:人工 102,955 项中 **100,529 项有 `spec_json.skuId`(97.6%)**,共 3,561 个 SKU;剩余 2,426 项主要来自拼多多,可用 `sku_code/sku_name` 建立映射。人工商品归一后,商品编码统一用 `man:<skuId>`;不得因为复用了 `tpl:*` 商品就把履约方式改成 `work_order`。
@@ -64,23 +68,23 @@
---
## 三、成本中心现行规则
## 三、成本模型规则
| 成本模型 | 适用履约方式 | 成本来源 | 现状 |
| --- | --- | --- | --- |
| `affiliate_purchase` | 自动发货 | 上游 SKU 固定成本版本 | 38 个商品,全部有 active 版本 |
| `work_order_reward` | 接单平台 | 模板规则级版本 / 工单实际报酬 | 662 个规则版本,966 项异常 |
| `manual_actual` | 人工 | 订单页逐单录入的实际支出 | 已实现,历史未采集 |
| `legacy_cost` | 历史平台 | 旧平台结算数据(如可得) | 未采集,历史单列 |
| `none` | 无成本或平台承担 | 业务确认后显式标记 | 暂不默认 |
| `platform_fee` / `payment_fee` / `after_sales_loss` / `other` | 全部履约方式 | 通用成本项 | 未配置 |
硬规则:
- 未确认成本时保持 `cost_missing`,**绝不默认记 0**;
- 人工成本只走 `sys_manual_service`,通过 `manual_expense_records` 写 `actual` 快照;
- 自动发货、接单平台继续使用现有成本版本与规则版本;
- 电子凭证不建成本项,也不建“凭证载体”成本;
- 历史平台成本单独标记 `legacy_cost`,不要混入人工成本。
- 电子凭证不建成本项,也不建"凭证载体"成本;
- 计价方式只保留三种:`per_item`(按件)、`per_order`(按单)、`percent_of_revenue`(按收入比例)。**已删除 `per_hour` / `fixed_daily`**——人工成本逐单按实际金额,不按工时;
- 历史平台成本单独标记,不混入人工成本。
---
@@ -133,7 +137,7 @@
## 五、历史数据处理
- 历史人工支出**不补录**,只允许抽取样本做成本校准;
- 历史人工单在销售事实中保留收入,成本标记为 `historical_missing`,不计入“上线后监控缺口”;
- 历史人工单在销售事实中保留收入,成本标记为 `historical_missing`,不计入"上线后监控缺口";
- 历史平台 `legacy_platform` 单独成行,成本未采集的标记 `legacy_missing`;
- 自动发货、接单平台历史成本不回写、不重算版本,沿用现有快照。
@@ -141,36 +145,16 @@
## 六、实施顺序
### P0:统一履约事实(先做)
| 阶段 | 内容 | 状态 |
| --- | --- | --- |
| P0 | 统一履约事实(`fulfillment_mode`)+ 成本模型正交 | **已完成(本地,未部署生产)** |
| P1 | 人工商品归一(`man:<skuId>`)+ 上线后人工成本全覆盖 | 未实施 |
| P2 | 退款冲减与异常闭环 | 未实施 |
| P3 | 影子总账与对账 | 未实施 |
> 执行细节、迁移 SQL、回填脚本与代码清单见 §八;本节只保留顺序。
P0 的具体迁移、回填、代码清单与验收 SQL 见 [成本中心与履约模型](成本中心与履约模型.md);接口契约见 [成本中心API契约](成本中心API契约.md)。
1. `order_items` 增加 `fulfillment_mode`、`fulfillment_decided_at`、`fulfillment_reason`;
2. 按第四节顺序回填 316,585 个订单项;
3. 后台、成本快照、销售事实统一读取该字段;
4. 移除 `provider === '91kaquan' -> voucher_only` 的兜底;
5. 验收:work_order 118,053 / affiliate_dash 41,676 / legacy_platform 53,901 / manual 102,955 四项相加等于总数。
### P1:人工商品与成本入口
1. 人工单按 `spec.skuId` 建 `man:<skuId>` 商品,PDD 用 `sku_code/sku_name` 兜底;
2. 订单页人工录单保持逐单实际金额;
3. 上线后新人工单必须在 24 小时内录入成本;
4. 验收:人工单无成本记录 = 0。
### P2:退款与异常闭环
1. 订单增加退款字段;
2. 销售事实接入退款金额与成本冲减;
3. 人工/接单平台异常队列按履约方式拆分;
4. 验收:退款订单收入、成本、毛利可对平。
### P3:影子总账与对账
1. 建 `finance_accounts / finance_business_events / finance_journal_*`;
2. 先接订单支付、工单结算、押金、提现、人工支出五类事件;
3. 每日对账:钱包余额 vs 总账余额;
4. 验收:试算平衡、钱包差异为 0,再谈关账与报表。
> 下一步只做 P1,不要同时改退款或总账。
---
@@ -181,246 +165,23 @@
| 订单页人工录单,按单实际金额 | **已实施** | `74b62f34` |
| 人工支出从客服备用金扣款 | **已实施** | `2aeb789e` |
| 客服只能使用本人备用金账户 | **已实施** | `a7f60414` |
| 备用金账户编号 `FA-<用户名>` | **已实施** | `fac34c84` + 迁移 `113` |
| 备用金账户编号 `FA-<用户名>` | **已实施** | `fac34c84` + 迁移 `106` |
| 订单页只展示平台判断 SKU | **已实施** | `c38cd8ca`、`cd9bb141` |
| `order_items.fulfillment_mode` 持久化 | **已实施** | 迁移 `114` + 统一决策服务 |
| 订单页 / 成本快照 / 销售事实统一读字段 | **已实施** | `admin-read-service`、`sales-facts-service`、`cost-snapshot-sync-service` |
| 本地 316,585 项回填 | **已实施** | `fulfillment:backfill`,四项计数与 §8.2 一致 |
| 已有成本异常载体同步 | **已实施** | 53,901 条由 `manual` 修正为 `legacy_platform` |
| 24 小时兜底归集 | **已实施** | `cost_finance_sync` 每 5 分钟先归集 |
| 财务与履约文档统一口径 | **本轮完成** | 本文件 + 交接总结 + 关联文档 |
| `order_items.fulfillment_mode` 持久化 | **已实施** | 迁移 `107` + 统一决策服务 |
| 订单页 / 成本快照 / 销售事实统一读字段 | **已实施** | `admin-order-read-helpers`、`sales-facts-service`、`cost-snapshot-sync-service` |
| 本地 316,585 项回填 | **已实施** | `fulfillment:backfill`,四项计数与 §二 一致 |
| 成本模型与履约方式正交(去 `carrier_type`) | **已实施(本地库已改造)** | 迁移 `103–107` 重写;`cost_items.cost_model` |
| 12 个迁移合并为 5 个 | **已实施** | `103_product_catalog` ~ `107_order_item_fulfillment_mode` |
| 人工商品归一 `man:<skuId>` | **未实施** | P1 |
| 退款冲减、异常闭环 | **未实施** | 本文 §六 P2 |
| 影子总账、对账、关账 | **未实施** | 本文 §六 P3 |
| 退款冲减、异常闭环 | **未实施** | §六 P2 |
| 影子总账、对账、关账 | **未实施** | §六 P3 |
> 分支事实:以上实现均在 `dev`;迁移 `103–114` 尚未部署生产。
> 下一阶段只做 P1:人工商品归一 + 上线后人工成本全覆盖,不要同时改退款或总账。
> 分支事实:以上实现均在 `feat/finance-rewrite`;迁移 `103–107` 尚未合并到 `dev`、未部署生产。
> 生产口径仍以 `main` 为准。
---
## 八、P0 实施记录:统一履约事实
> 状态:**已实施(本地 `dev`,未部署生产)**。迁移 `114` 已执行,316,585 项已回填,四项计数与 §8.2 一致。
### 8.1 迁移 `114_order_item_fulfillment_mode.sql`
四张表的 `carrier_type` 约束原先都不接受 `legacy_platform`;只加 `order_items` 字段会在写销售事实/成本快照时直接失败。迁移 `114` 已同时放宽:
- `product_catalog.chk_product_catalog_carrier_type`
- `cost_items.chk_cost_items_carrier_type`
- `cost_snapshots.chk_cost_snapshots_carrier_type`
- `sales_daily_facts.chk_sales_daily_facts_carrier`(保留 `unknown`)
```sql
ALTER TABLE order_items
ADD COLUMN IF NOT EXISTS fulfillment_mode text,
ADD COLUMN IF NOT EXISTS fulfillment_decided_at timestamptz,
ADD COLUMN IF NOT EXISTS fulfillment_reason text;
ALTER TABLE order_items
ADD CONSTRAINT chk_order_items_fulfillment_mode
CHECK (
fulfillment_mode IS NULL
OR fulfillment_mode IN ('work_order', 'affiliate_dash', 'legacy_platform', 'manual')
);
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_mode
ON order_items (fulfillment_mode, order_id);
CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_pending
ON order_items (order_id)
WHERE fulfillment_mode IS NULL;
ALTER TABLE product_catalog DROP CONSTRAINT IF EXISTS chk_product_catalog_carrier_type;
ALTER TABLE product_catalog ADD CONSTRAINT chk_product_catalog_carrier_type
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared'));
ALTER TABLE cost_items DROP CONSTRAINT IF EXISTS chk_cost_items_carrier_type;
ALTER TABLE cost_items ADD CONSTRAINT chk_cost_items_carrier_type
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared'));
ALTER TABLE cost_snapshots DROP CONSTRAINT IF EXISTS chk_cost_snapshots_carrier_type;
ALTER TABLE cost_snapshots ADD CONSTRAINT chk_cost_snapshots_carrier_type
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared'));
ALTER TABLE sales_daily_facts DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_carrier;
ALTER TABLE sales_daily_facts ADD CONSTRAINT chk_sales_daily_facts_carrier
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy_platform', 'manual', 'shared', 'unknown'));
```
### 8.2 回填(只填 NULL,可重复执行)
> 迁移 `114` 只做 DDL;回填单独执行,不要塞进启动迁移。31 万行 `UPDATE` 放进迁移会拖慢部署,也无法在中途失败后按批重试。建议写成一次性脚本,每 5,000 行提交一次。
判定顺序固定:**工单 → 自动发货任务 → 历史平台任务 → 人工**。当前库中三种载体没有重叠,顺序不影响结果;后续出现重叠必须告警,不能静默覆盖。
```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-07-10 ~ 2026-10-14):
| fulfillment_mode | 订单项 | 收入 | 无成本项 |
| --- | ---: | ---: | ---: |
| `work_order` | 118,053 | ¥533.57 万 | 966 |
| `affiliate_dash` | 41,676 | ¥508.97 万 | 0 |
| `legacy_platform` | 53,901 | ¥389.12 万 | 53,901 |
| `manual` | 102,955 | ¥1,219.20 万 | 102,954 |
| **合计** | **316,585** | **¥2,650.87 万** | **157,821** |
> 以上 SQL 已在本地库实际执行:定时任务先归集 20,000 项,回填脚本再处理 296,585 项,合计 316,585,`NULL = 0`;脚本耗时约 86 秒。随后同步 53,901 条 open 成本异常的 `carrier_type`(manual → legacy_platform),全表检查耗时约 5.4 秒。
生产执行顺序:
```bash
# 1. 部署并等待迁移 114 完成(只做 DDL)
npm run db:migrate:status
# 2. 分批回填 + 同步已有成本异常载体
npm run fulfillment:backfill
# 3. 核对四项计数与 NULL=0,再触发财务重算
npm run finance:sync -- --from=<上线前起始日> --to=<今天> --mode=all
```
### 8.3 代码改造记录
| 文件 | 改造结果 |
| --- | --- |
| `apps/backend/src/db/migrations/114_order_item_fulfillment_mode.sql` | 新增三列 + 两个索引;放宽四张表的 `carrier_type` CHECK |
| `apps/backend/src/services/fulfillment/fulfillment-mode-service.ts` | 统一判定与批量归集;`decideOrderItemsFulfillmentModes()`、`reconcilePendingFulfillmentModes()`、`syncOpenCostExceptionCarrierTypes()` |
| `apps/backend/scripts/backfill-fulfillment-mode.ts` | 分批回填 + 同步已有成本异常载体;npm 命令 `fulfillment:backfill` |
| `apps/backend/src/services/admin/admin-order-read-helpers.ts` | 只读 `fulfillment_mode`;删除 voucher/provider 兜底;核销码单独返回 `voucherState` |
| `apps/backend/src/services/finance/sales-facts-service.ts` | 直接读 `COALESCE(oi.fulfillment_mode, 'manual')`,不再复制判定 |
| `apps/backend/src/services/cost-center/cost-snapshot-sync-service.ts` | 三处临时判定全部替换为 `fulfillment_mode` |
| `apps/backend/src/services/scheduler/cost-finance-sync-job.ts` | 每 5 分钟先归集待判定订单项,再算成本与销售事实 |
| `apps/backend/src/services/worker-platform/sync-worker-orders-service.ts` | 订单同步结束后批量落履约方式;失败不阻断下单 |
| 前端订单列表 / 详情 / 类型 / 显示工具 | 四种模式 + `mixed/pending`;核销码独立标签;人工按钮只认 `manualItemCount > 0` |
### 8.4 决策边界(写进代码注释与测试)
1. `fulfillment_mode` 允许短暂为 `NULL`;订单项创建时通常还没有工单/任务,不能强制非空。
2. 上线后新订单项超过 **24 小时**仍为 `NULL`,进入异常队列;不是要求在创建瞬间就有值。
3. 模式可以因业务改派而变化,但每次变化必须写原因与操作人(审计日志或后续事件表),禁止静默 `UPDATE`。
4. `product_catalog.carrier_type` 只描述商品来源,**不能反推履约方式**。当前 11,349 个人工项指向 `tpl:*` 商品,就是典型反例。
5. `voucher_state` 只做展示与核销流程,不参与任何成本/履约判断。
6. 三种载体同时命中的订单项必须先告警人工复核;当前本地为 0。
### 8.5 完成情况
- 迁移 `114` 已在本地库执行,字段、索引、四处 CHECK 均已生效;
- 回填后 §8.2 四项数字完全一致,`NULL = 0`;
- 订单页、成本快照、销售事实已统一读 `fulfillment_mode`;
- 本地联调:带已核销快手凭证的 91 人工单返回 `manual` + `voucherState=consumed`,`canClaimH5=false`;
- 销售事实重算后四种载体收入/成本与 §8.2 完全一致;
- 后端 `npm run check` 通过(765 项:762 通过 / 3 跳过);前端 `npm run check` 通过(143 项测试 + build);
- 生产仍需按 §8.2 顺序执行迁移、回填、财务重算,并连续观察 7 天日检。
---
## 九、上线后运营规则(人工账单必须 100% 可监控)
### 9.1 角色与时限
| 动作 | 角色 | 时限 | 系统要求 |
| --- | --- | --- | --- |
| 人工单录成本 | 客服本人 | 支付后 24 小时内 | 自动带入本人备用金账户;逐单实际金额 |
| 代录 | 管理员/财务 | 同上 | 必须明确选择客服备用金账户 |
| 日检 | 客服主管 | 每日 10:00 | 未分类、无成本、无备用金关联均为 0 |
| 周对账 | 财务 | 每周一 | 人工支出 vs 备用金流水金额一致 |
| 月结 | 财务负责人 | 每月 3 日前 | 上线后区间零缺口才允许关账 |
### 9.2 每日必查
1. 上线日之后支付的订单项,超过 24 小时仍无 `fulfillment_mode`:**0**。
2. `manual` 订单项无有效成本快照:**0**。
3. `manual_expense_records.fund_expense_id IS NULL`(上线后):**0**。
4. `manual_expense_records.amount_fen <> manual_expenses.amount_fen`:**0**。
5. 人工项仍挂 `tpl:*` 商品的数量:P1 前允许存在,P1 后应改为 `man:*` 或经业务确认保留。
### 9.3 异常处理
- 发现人工单无成本:当天补录;补录仍走备用金扣款,不允许直接改余额。
- 备用金余额不足:拒绝登记,由财务充值后再录;不允许负余额。
- 同一订单项多笔支出:允许累加(渠道费、加急费、补差价),但每笔都必须有独立备用金流水。
- 历史人工单:标记 `historical_missing`,不补录、不计入上线后缺口。
### 9.4 历史样本校准(只做参考)
- 每个来源抽 20–30 单,覆盖低价/中价/高价;
- 只用于估算历史人工成本区间与毛利率,不写 `cost_snapshots`;
- 样本结论单独记录,禁止回填成逐单成本;
- 历史平台 53,901 项同理,只有拿到旧平台结算数据才允许按 `legacy_cost` 入账。
---
## 十、常见错误(防回归)
| # | 错误 | 后果 | 正确做法 |
| --- | --- | --- | --- |
| 1 | 把“有快手核销码”当成 `voucher_only` | 91 人工单全部从订单页消失 | 核销码只做状态展示 |
| 2 | 按 `provider=91kaquan/douyin_mashida` 直接判自动发货 | 人工单被藏进自动发货 | 只看工单/任务/历史任务 |
| 3 | 用 `product_catalog.carrier_type` 反推履约方式 | 11,349 个人工项被误认为接单平台 | 只读 `order_items.fulfillment_mode` |
| 4 | 只加 `order_items` 字段,不改四张表的 CHECK | 写 `legacy_platform` 时运行时报错 | 按 §8.1 同时放宽 |
| 5 | 只回填已支付历史单,不做增量兜底 | 新单长期停在 `NULL` | 24 小时定时归集 + 异常队列 |
| 6 | 把“人工 91,606”当成“人工全量” | 与 102,955 对不上,报表互相矛盾 | 写清“全量 102,955 / 未归一 91,606” |
| 7 | 把 `manual_expenses` 当公司人工支出表 | 两套语义混用,无法对账 | 用 `manual_expense_records` + `fund_expense_id` |
| 8 | 用均价、工时、固定单价补历史人工 | 违背逐单实际金额,毛利失真 | 历史不补录,只做样本区间 |
| 9 | 只改订单页,不改成本/销售事实 | 三处口径继续分叉 | 同一字段、同一决策入口 |
| 10 | 在旧文档里保留“三块/按小时/公司直付”旧结论 | 下次开发又按旧口径实现 | 旧文只留追溯标记,现行结论只写本文件 |
---
## 十一、文档地图与维护规则
| 文档 | 唯一职责 | 状态 |
| --- | --- | --- |
| **本文件** | 履约/成本/人工口径、实施顺序、验收 SQL | **现行唯一口径** |
| [成本中心与履约方式模型.md](成本中心与三块履约模型.md) | 商品、成本版本、快照、销售事实的数据模型 | 现行(文件名保留) |
| [人工支出登记落地方案-2026-10-17.md](人工支出登记落地方案-2026-10-17.md) | 人工支出字段、入口、审批、权限 | 现行(P0 已实施) |
| [客服备用金台账实施方案-2026-10-18.md](客服备用金台账实施方案-2026-10-18.md) | 备用金账户、流水、支出、编号 | 现行(P0 已实施) |
| [财务系统集成方案.md](财务系统集成方案.md) | `finance_*` 复式总账层 | 方案(未实施) |
| [影子总账说明.md](影子总账说明.md) | 影子总账概念、对账、转正 | 说明(未实施) |
| [履约配置/多发货平台与电子凭证整体链路.md](履约配置/多发货平台与电子凭证整体链路.md) | 进单、履约、领取、核销链路 | 现行 |
| [README.md](README.md) | 文档索引、分支与进度 | 现行入口 |
维护规则:
1. 履约分类只在本文定义;其他文档只链接,不复制判定 SQL。
2. 任何数字必须注明口径:全量 / 已归一 / 未归一 / 上线后。
3. 每次迁移后更新本文的代码基线、数据基线、§七 进度和 §8.2 数字。
4. 新文档必须登记到 `README.md`;过期文档移入 `docs/archive/`,不得在现行文档里保留冲突结论。
5. 不再使用“三块履约”作为分类名;`成本中心与三块履约模型.md` 只是历史文件名。
6. 代码评审必须检查:订单页、`sales-facts-service`、`cost-snapshot-sync-service` 是否都读同一字段。
---
## 十二、上线验收查询
## 八、上线验收查询
> 以下 SQL 在 `fulfillment_mode` 落地后使用;`DATE '2026-11-01'` 替换为实际上线日。
@@ -466,10 +227,9 @@ WHERE r.amount_fen <> me.amount_fen
-- 6. 人工单未归一商品:P1 完成后应为 0
SELECT COUNT(*)
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
WHERE oi.fulfillment_mode = 'manual'
AND oi.canonical_product_id IS NULL
AND COALESCE(o.paid_at, o.created_at) >= DATE '2026-11-01';
AND oi.created_at >= DATE '2026-11-01';
-- 7. 核销码不得改变履约方式:带快手凭证的 91 人工单仍应为 manual
SELECT oi.fulfillment_mode, COUNT(*)
@@ -497,5 +257,44 @@ SELECT COUNT(*)
FROM order_items oi
JOIN product_catalog pc ON pc.id = oi.canonical_product_id
WHERE oi.fulfillment_mode = 'manual'
AND pc.carrier_type = 'work_order';
AND pc.canonical_sku LIKE 'tpl:%';
```
---
## 九、常见错误(防回归)
| # | 错误 | 后果 | 正确做法 |
| --- | --- | --- | --- |
| 1 | 把"有快手核销码"当成 `voucher_only` | 91 人工单全部从订单页消失 | 核销码只做状态展示 |
| 2 | 按 `provider=91kaquan/douyin_mashida` 直接判自动发货 | 人工单被藏进自动发货 | 只看工单/任务/历史任务 |
| 3 | 用 `product_catalog` 的来源反推履约方式 | 11,349 个人工项被误认为接单平台 | 只读 `order_items.fulfillment_mode` |
| 4 | 在商品/成本项/快照/异常四张表各存一份载体 | 四处口径分叉 | 载体只存 `order_items.fulfillment_mode` |
| 5 | 只回填已支付历史单,不做增量兜底 | 新单长期停在 `NULL` | 24 小时定时归集 + 异常队列 |
| 6 | 把"人工 91,606"当成"人工全量" | 与 102,955 对不上,报表互相矛盾 | 写清"全量 102,955 / 未归一 91,606" |
| 7 | 把 `manual_expenses` 当公司人工支出表 | 两套语义混用,无法对账 | 用 `manual_expense_records` + `fund_expense_id` |
| 8 | 用均价、工时、固定单价补历史人工 | 违背逐单实际金额,毛利失真 | 历史不补录,只做样本区间 |
| 9 | 只改订单页,不改成本/销售事实 | 三处口径继续分叉 | 同一字段、同一决策入口 |
| 10 | 在旧文档里保留"三块/按小时/公司直付"旧结论 | 下次开发又按旧口径实现 | 旧文只留追溯标记,现行结论只写本文件 |
---
## 十、文档地图与维护规则
| 文档 | 唯一职责 | 状态 |
| --- | --- | --- |
| **本文件** | 履约/成本/人工口径、实施顺序、验收 SQL | **现行唯一口径** |
| [成本中心与履约模型.md](成本中心与履约模型.md) | 商品、成本版本、快照、销售事实的数据模型与迁移 | 现行 |
| [成本中心API契约.md](成本中心API契约.md) | 成本中心后端/前端接口字段与模块边界 | 现行 |
| [人工支出与备用金.md](人工支出与备用金.md) | 人工支出登记、客服备用金台账、审批与权限 | 现行(P0 已实施) |
| [财务系统集成方案.md](财务系统集成方案.md) | `finance_*` 复式总账层(未实施) | 方案 |
| [README.md](README.md) | 文档索引、分支与进度 | 现行入口 |
维护规则:
1. 履约分类只在本文定义;其他文档只链接,不复制判定 SQL。
2. 任何数字必须注明口径:全量 / 已归一 / 未归一 / 上线后。
3. 每次迁移后更新本文的代码基线、数据基线、§七 进度和 §二 数字。
4. 新文档必须登记到 `README.md`;过期文档移入 `docs/archive/`,不得在现行文档里保留冲突结论。
5. 不再使用"三块履约"作为分类名。
6. 代码评审必须检查:订单页、`sales-facts-service`、`cost-snapshot-sync-service` 是否都读同一字段。
+2 -2
View File
@@ -2,7 +2,7 @@
> 状态:**现行方案(总账层未实施)**。本文只描述 `finance_*` 总账层;当前总账表尚未建立,现有财务页仍是 `UNION ALL` 业务事件拼装。
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> 代码基线:`feat/finance-rewrite @ 7557de6d + 迁移 103–107`
> **履约方式、成本模型、人工全覆盖口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文不重复分类设计。
> 目标:在**不破坏现有抢单、押金、结算、寄卖、提现链路**的前提下,增加可对账、可关账、可追溯的复式记账底座。
> 结论先说:**不要直接在 `worker_wallets` / `worker_wallet_ledgers` 上继续加字段。** 现有表是业务钱包与流水,不是会计总账;正确做法是新增复式记账底座,让现有钱包成为它的“余额投影”。
@@ -98,7 +98,7 @@
-> 对账 / 转正
```
详细设计见 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 与 [成本中心与履约方式模型.md](成本中心与三块履约模型.md)。
详细设计见 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 与 [成本中心与履约模型.md](成本中心与履约模型.md)。
---
+3 -3
View File
@@ -161,7 +161,7 @@ pending_material → unassigned → open → in_progress
- 同一个商品名对应多个 `sku_code`:「指挥官密钥1个」27 个、「苏尔南钻石100颗」35 个,不能直接按 `sku_code` 做销量统计。
- 当前 30 天订单基本是一单一品,但模型必须按订单项设计。
详细设计见 [成本中心与履约方式模型.md](成本中心与三块履约模型.md)。
详细设计见 [成本中心与履约模型.md](成本中心与履约模型.md)。
---
@@ -250,7 +250,7 @@ pending_material → unassigned → open → in_progress
- 匹配引擎:P0 + P1 已实施,P2/P3 待办。
- 抖音三层分流:抖音侧已实施。
6. 校正过期测试数:抖音总览中的 636/115 改为 2026-10-17 复核的 740/143。
7. 2026-10-17 增补:新增 [成本中心与履约方式模型.md](成本中心与三块履约模型.md),把自动发货/接单平台/人工客服三块、统一成本中心、销售事实与人工 case 纳入现行设计;本报告同步补入生产只读数据与 C1~C5 风险。
7. 2026-10-17 增补:新增 [成本中心与履约模型.md](成本中心与履约模型.md),把自动发货/接单平台/人工客服三块、统一成本中心、销售事实与人工 case 纳入现行设计;本报告同步补入生产只读数据与 C1~C5 风险。
仍保留为“设计/待办”的内容:
@@ -279,7 +279,7 @@ pending_material → unassigned → open → in_progress
5. 余额是可变字段,缺少“由流水重算”的校验。
6. 资金事件缺少统一的 `business_event_id` 幂等键。
建议:先建**影子总账 + 对账**,不要一开始替换钱包。成本金额与商品维度单独由成本中心负责,人工块由人工 case 负责;三者关系见 [成本中心与履约方式模型.md](成本中心与三块履约模型.md),总账细节见 [财务系统集成方案.md](财务系统集成方案.md) 与 [影子总账说明.md](影子总账说明.md)。
建议:先建**影子总账 + 对账**,不要一开始替换钱包。成本金额与商品维度单独由成本中心负责,人工块由人工 case 负责;三者关系见 [成本中心与履约模型.md](成本中心与履约模型.md),总账细节见 [财务系统集成方案.md](财务系统集成方案.md) 与 [影子总账说明.md](影子总账说明.md)。
---