feat(finance): 完成财务控制、L1 钱包硬化与迁移收敛
- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度 - 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment - 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单 - 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径 - 文档:同步迁移基线、合并评估、代码复核与上线说明
This commit is contained in:
+14
-10
@@ -1,7 +1,7 @@
|
||||
# order_site 文档索引
|
||||
|
||||
> 最后校准:2026-10-26,代码基线 `dev`(合并 `dev1023` + 人工成本完整性修复,迁移基线收敛为 `103`–`111`;**未部署生产**)。
|
||||
> 数据基线:生产 dump `backups/postgres/order_site-20261022-092601.dump`(数据 2026-07-10 ~ 2026-10-22);测试/本地旧编号已到 115,生产按收敛后的 101 → 111 路径执行。历史全量数字仅作基线,**上线验收以 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 为准**。
|
||||
> 最后校准:2026-10-27,代码基线 `dev`(合并 `dev1023` + 人工成本完整性修复 + 财务 P0 控制,迁移基线收敛为 `103`–`105`;**未部署生产**)。
|
||||
> 数据基线:生产 dump `backups/postgres/order_site-20261022-092601.dump`(数据 2026-07-10 ~ 2026-10-22);本地已校准到 105(103 财务基础、104 财务控制/L1、105 拼单/寄卖客服),生产按收敛后的 102 → 105 路径执行。历史全量数字仅作基线,**上线验收以 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 为准**。
|
||||
> 本文是 `docs/` 的唯一入口。**财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;数据模型看 [成本中心与履约模型.md](成本中心与履约模型.md),接口看 [成本中心API契约.md](成本中心API契约.md)。
|
||||
> 新增文档必须在下表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
| 名称 | 业务含义 | 当前状态 |
|
||||
| --- | --- | --- |
|
||||
| `manual_expenses` | **客服备用金支出明细**:客服从自己的备用金里充值/采购/赔偿,减少客服余额 | 已实施(迁移 100);确认履约的现金支出也写这里 |
|
||||
| `cost_snapshots` | **人工成本的唯一落点**:`subject_type='manual_case'`,`subject_id=order_item_id`(资金来源写 metadata) | 已实施(迁移 104 + 111) |
|
||||
| `cost_snapshots` | **人工成本的唯一落点**:`subject_type='manual_case'`,`subject_id=order_item_id`(资金来源写 metadata) | 已实施(迁移 104) |
|
||||
|
||||
**没有独立的登记单,也没有审批与报销环节。** `manual_expense_records` / `manual_expense_events` 已随迁移 `104` 归并下线(这两张表在生产 102 上从未创建)。后续做财务系统时,人工成本的钱与账都从这条链路走,不要再另造一张人工支出表。
|
||||
|
||||
@@ -79,28 +79,28 @@
|
||||
| 客服备用金台账 | **已实施 P0** | 迁移 `100/101/107`;账户、流水、支出、对账;编号 `FA-<用户名>`;客服只能使用本人账户 |
|
||||
| 后台用户中文显示名 | **已实施** | `admin_users.display_name`,备用金姓名触发器同步;审计、登录记录、任务处理人优先显示中文名 |
|
||||
| 商品主数据 + 渠道 SKU 映射 | **已实施 P0/P1** | 2,017 个标准商品、4,897 条映射:`tpl:` 710、`man:` 1,195、`aff:` 49、`wo:` 63;订单项已按履约证据/人工内部 SKU 归一 |
|
||||
| 成本中心 | **已实施 P0** | 迁移 `104`;710 个规则级版本 + 2 个全局占位版本;工单成本快照 151,939 条,其中 actual 129,430、estimated 22,509 |
|
||||
| 销售事实 / 订单毛利 | **已实施 P0** | 迁移 `105`;375,447 项、收入 ¥3,143.61 万、成本 ¥572.95 万、毛利 ¥2,570.66 万;`sales_daily_facts.fulfillment_mode` |
|
||||
| 成本中心 | **已实施 P0** | 迁移 `103`;710 个规则级版本 + 2 个全局占位版本;工单成本快照 151,939 条,其中 actual 129,430、estimated 22,509 |
|
||||
| 销售事实 / 订单毛利 | **已实施 P0** | 迁移 `103`;375,447 项、收入 ¥3,143.61 万、成本 ¥572.95 万、毛利 ¥2,570.66 万;`sales_daily_facts.fulfillment_mode` |
|
||||
| 人工成本录入(确认履约) | **已实施 P0(2026-10-26 单事务 + 防重复)** | 确认即扣积分/备用金并写 `actual` 快照;钱与成本同事务、订单项级唯一;无登记单/审批/报销 |
|
||||
| 人工 SKU 配置自动匹配 | **已实施(2026-10-26)** | 保存配置立即回填 + 定时 rematch + 确认时按 SKU 兜底匹配 |
|
||||
| 人工缺成本监控 | **已实施(2026-10-26)** | `manual_cost_missing`:首次人工配置之后的订单项未确认即入队,确认后自动收口 |
|
||||
| 履约方式统一 | **已实施 P0** | 迁移 `105`;375,447 项全部回填:接单平台 153,148、人工 115,194、历史平台 53,901、自动发货 53,204 |
|
||||
| 履约方式统一 | **已实施 P0** | 迁移 `103`;375,447 项全部回填:接单平台 153,148、人工 115,194、历史平台 53,901、自动发货 53,204 |
|
||||
| 人工商品归一 | **已实施 P1(本次快照恢复执行)** | 人工 115,194 项已归一为 1,195 个 `man:<内部SKU>` 商品;历史人工支出仍不补录 |
|
||||
| 影子总账 | **已实施 P0(2026-10-26)** | `finance_accounts/journal_entries/journal_lines/periods`;销售事实按日凭证 + 手工凭证 + 试算平衡;不接管钱包余额 |
|
||||
| 成本异常归档 | **已实施** | 迁移 `108`;已处理异常超 60 天搬入 `cost_exceptions_archive`,open 行不归档;`COST_EXCEPTION_RETENTION_DAYS` 可调 |
|
||||
| 平台对账 / 期间关账 | **已实施 P0** | 平台账单 JSON 导入匹配、差异明细、标记解决;关账/反关账;见 `/admin/finance/ledger` |
|
||||
| 平台退款 API | **部分实施** | 快手行业(91)同意退款已接入;抖音/拼多多暂未接 API,仍手工登记 |
|
||||
|
||||
> 分支状态(2026-10-26):`dev` 已合并 `dev1023` 与人工成本完整性修复,迁移基线收敛为 `103`–`111`(原 108–115 的财务/性能补丁合并进 108);临时库从 001 全量执行与从 101 升级均待验收。**未推送、未部署生产**;生产口径仍以 `main` 为准。上线前按 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 初始化资金与人工 SKU。
|
||||
> 分支状态(2026-10-27):`dev` 已合并 `dev1023` 与人工成本完整性修复,迁移收敛为 `103`–`105`(103 财务基础、104 财务控制/L1、105 拼单/寄卖客服);已验证 001→105 与 102→105 两条升级路径。**未推送、未部署生产**;生产口径仍以 `main` 为准。上线前按 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 初始化资金与人工 SKU。
|
||||
> 财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。
|
||||
|
||||
## 三、统一实施顺序
|
||||
|
||||
```text
|
||||
0. 统一履约事实(fulfillment_mode + 成本模型正交) 已完成 P0(迁移 105)
|
||||
0. 统一履约事实(fulfillment_mode + 成本模型正交) 已完成 P0(迁移 103)
|
||||
1. 商品主数据 + 渠道 SKU 映射 已完成 P0(仅自动发货/接单平台)
|
||||
2. 成本中心 + 销售事实 已完成 P0
|
||||
2.5 人工成本链路收敛(确认履约 → 扣积分/备用金 → 写快照) 已完成 P0(迁移 104/107/111)
|
||||
2.5 人工成本链路收敛(确认履约 → 扣积分/备用金 → 写快照) 已完成 P0(迁移 104)
|
||||
3. 人工商品归一与上线后人工成本全覆盖 已完成代码;生产数据需上线后配置 SKU(历史不补录)
|
||||
4. 退款冲减与异常队列 已完成手工退款 + 快手平台退款;人工缺成本已入队
|
||||
5. 影子总账(销售事实 + 手工凭证) 已完成 P0(迁移 108)
|
||||
@@ -129,11 +129,15 @@
|
||||
| 文档 | 状态 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) | **现行唯一口径** | 履约方式、成本模型、人工全覆盖、历史策略、实施顺序、运营日检与验收 SQL |
|
||||
| [成本中心与履约模型.md](成本中心与履约模型.md) | 现行 | 商品、成本版本、快照、异常、销售事实的数据模型;迁移 `103`–`111`;履约回填;迁移基线与本地库同步;索引与性能 |
|
||||
| [成本中心与履约模型.md](成本中心与履约模型.md) | 现行 | 商品、成本版本、快照、异常、销售事实的数据模型;迁移 `103`–`105`;履约回填;迁移基线与本地库同步;索引与性能 |
|
||||
| [成本中心API契约.md](成本中心API契约.md) | 现行 | 成本中心接口字段替换总表、枚举、模块边界(后端 5 模块 + 前端 4 个固定 Tab) |
|
||||
| [人工支出与备用金.md](人工支出与备用金.md) | 现行(P0 已实施) | 确认履约流程/权限、客服备用金台账、本人账户与编号规则 |
|
||||
| [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) | **现行上线手册** | 迁移后资金/人工 SKU 初始化、验收 SQL、日常监控 |
|
||||
| [财务系统集成方案.md](财务系统集成方案.md) | 现行方案(影子总账 P0 已实施) | 复式记账、总账表、对账、关账;P1/P2 平台自动对账待接 |
|
||||
| [财务系统深度分析-2026-10-27.md](财务系统深度分析-2026-10-27.md) | 参考(一次性审计,不改变现行口径) | 2026-10-27 代码/数据/权限审计、风险清单与上线前修复路线 |
|
||||
| [dev合并main评估-2026-10-27.md](dev合并main评估-2026-10-27.md) | 参考(合并评估) | dev 领先 main 的改动、迁移收敛、生产规模升级实测与发布检查清单 |
|
||||
| [财务代码审计复核-2026-10-27.md](财务代码审计复核-2026-10-27.md) | 参考(代码复核) | 对“旧报表重复扣减、总账 DELETE、钱包 BIGINT、死锁”等结论的逐条校正与补充 |
|
||||
| [打手重复奖励核对-2026-10-27.md](打手重复奖励核对-2026-10-27.md) | 待人工核对 | 8 组疑似重复奖励、16 条流水、多计 ¥196.20;含 84886 寄卖误报说明 |
|
||||
| [影子总账说明.md](影子总账说明.md) | 现行说明(P0 已实施) | 影子总账是什么、和钱包流水的区别、对账与转正标准 |
|
||||
| [项目全量分析报告-2026-10-17.md](项目全量分析报告-2026-10-17.md) | 历史参考 | 2026-10-17 架构与风险;财务口径以总纲为准 |
|
||||
| [接单模板匹配引擎重构设计.md](接单模板匹配引擎重构设计.md) | 现行(P0/P1 已实施) | `kuaishou-91 / pdd / douyin-mashida` 三来源匹配引擎 |
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# dev 合并 main 评估(2026-10-27)
|
||||
|
||||
> 评估对象:`main`(2fb7aae1)与 `dev`(0de0fd7b + 当前未提交的财务 P0/P1 改动)。
|
||||
> 生产基线:生产库当前 `schema_migrations=102`;验证用 dump 恢复到 `101` 后再补 `102`。
|
||||
> 结论:**代码可以合并;生产发布还需要执行一组一次性数据初始化/回填,不能只合并就上线。**
|
||||
|
||||
---
|
||||
|
||||
## 一、分支关系
|
||||
|
||||
```text
|
||||
2fb7aae1 main
|
||||
│
|
||||
└─ 99 commits ──► 0de0fd7b dev(本地另有未提交的财务 P0/P1 改动)
|
||||
```
|
||||
|
||||
- `main` 是 `dev` 的祖先:`git rev-list --left-right --count main...dev` = `0 99`。
|
||||
- 没有反向提交,合并是 **fast-forward**,不会产生代码冲突。
|
||||
- `dev` 相对 `main`:286 个文件、约 40,828 行新增、2,142 行删除。
|
||||
- 改动集中在:财务/成本中心、人工履约、客服备用金/积分、影子总账、退款、拼单多次参与、后台财务导航、部署配置与文档。
|
||||
|
||||
## 二、迁移现状
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `main` 迁移文件 | 102 个(001–102) |
|
||||
| `dev` 迁移文件 | **105 个(001–105)** |
|
||||
| 生产数据库 | 已到 `102` |
|
||||
| 生产升级路径 | `102 → 105`,共 **3 个迁移文件** |
|
||||
| 新增迁移文件 | 103–105,共 3 个 |
|
||||
|
||||
### 本轮收敛结果
|
||||
|
||||
- [103_finance_foundation.sql](../apps/backend/src/db/migrations/103_finance_foundation.sql):商品目录、成本中心、销售事实、退款单
|
||||
- [104_finance_controls.sql](../apps/backend/src/db/migrations/104_finance_controls.sql):人工履约、影子总账、对账/关账、财务控制、L1 钱包硬化
|
||||
- [105_work_order_consignment.sql](../apps/backend/src/db/migrations/105_work_order_consignment.sql):拼单多次参与、大厅索引、撤份额注释、寄卖对接客服
|
||||
|
||||
原来的 103–114 共 12 个文件已删除,内容按领域合并进以上 3 个迁移;不再保留 110/111/112/113/114 这类几行的补丁文件。
|
||||
|
||||
### 为什么是 3 个而不是 1 个
|
||||
|
||||
- 103 是财务基础表,104 依赖 103 的商品/成本表,105 是独立的工单/寄卖域;
|
||||
- 单文件会超过 2000 行,审查和排障成本高;
|
||||
- 按领域拆成 3 个,生产升级仍是 3 个文件,比原来的 13 个大幅减少。
|
||||
|
||||
> 当前是测试环境,允许重写未上线迁移;生产库只到 102,不依赖旧 103–114 的迁移记录。
|
||||
|
||||
## 三、生产规模升级验证
|
||||
|
||||
用生产 dump 恢复到模拟库,先补到 `102`,再执行 `103 → 105`:
|
||||
|
||||
| 检查 | 结果 |
|
||||
| --- | --- |
|
||||
| 迁移执行 | 103–105 全部成功,退出码 0 |
|
||||
| 迁移总耗时 | 约 **26 秒**(单事务;本地 Docker,含大表改 BIGINT) |
|
||||
| 订单 / 订单项 | 375,459 / 375,447,数据未丢 |
|
||||
| 迁移后 `revenue_basis='unknown'` | 375,447(符合预期,需要回填) |
|
||||
| 迁移后 `fulfillment_mode IS NULL` | 375,447(符合预期,需要回填) |
|
||||
| 财务事实 | `sales_daily_facts=0`、`cost_snapshots=0`、`finance_journal_entries=0`(等待初始化/同步) |
|
||||
| 备用金账户 | 0(生产 102 没有备用金数据) |
|
||||
| 拼单份额 | 41,383 行,迁移成功 |
|
||||
| 触发器等对象 | 104 的凭证/备用金/幂等/对账对象、钱包 CHECK/event_key/审计视图均存在;105 的寄卖客服表/索引存在 |
|
||||
|
||||
### 回填实测
|
||||
|
||||
| 脚本 | 结果 |
|
||||
| --- | --- |
|
||||
| `npm run revenue:backfill` | 375,447 行全部回填,76 批,约 **83 秒** |
|
||||
| `npm run fulfillment:backfill` | 375,447 行全部回填,76 批,约 **90 秒**;计数与文档一致(work_order 153,148 / manual 115,194 / legacy 53,901 / affiliate 53,204) |
|
||||
| `npm run finance:sync -- --mode=facts --from=2026-10-01 --to=2026-10-22` | 489 行事实,约 2.6 秒;成本为 0,因为商品目录尚未初始化,符合当前阶段 |
|
||||
|
||||
> 回填脚本并发执行会互相争 `order_items` 行锁,实测出现 1–3 秒/批的慢查询。
|
||||
> 生产应**串行执行**,并避开高峰。
|
||||
|
||||
## 四、合并/发布前必须处理
|
||||
|
||||
| # | 事项 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 提交当前未提交改动 | 当前 P0/P1 改动仍在工作区,先提交并跑 `check`,否则不能合并 |
|
||||
| 2 | 迁移编号统一 | 已收敛为 103–105;旧 103–114 文件删除,生产只差 103–105 |
|
||||
| 3 | 生产备份 | 部署脚本 `--backup`;迁移是单事务,失败可整体回滚 |
|
||||
| 4 | 收入回填 | 迁移只给警告;不跑 `revenue:backfill` 会导致退款按钮置灰、销售事实收入为 0 |
|
||||
| 5 | 履约方式回填 | 不跑 `fulfillment:backfill` 会导致历史订单项 `fulfillment_mode=NULL`,订单/成本页口径缺失 |
|
||||
| 6 | 商品目录/成本版本初始化 | 生产模拟迁移后 `product_catalog=0`;必须先初始化商品、模板/自动发货/人工 SKU,才能生成成本快照 |
|
||||
| 7 | 客服资金初始化 | 备用金账户、积分账户按上线手册初始化;当前生产 101 没有资金数据 |
|
||||
| 8 | 财务历史同步 | 初始化后跑 `finance:sync --mode=all`(建议 30 天窗口分段),生成成本快照/销售事实/总账 |
|
||||
| 9 | 发布窗口 | 迁移约 26 秒单事务锁(含大表改 BIGINT);建议低峰执行,观察 `orders/order_items/worker_wallet_ledgers` 锁等待 |
|
||||
| 10 | 文档空白 | 已顺手清理 2 个历史文档尾部空行;提交后 `git diff --check main...dev` 应通过 |
|
||||
| 11 | 历史重复结算 | 生产数据审计视图发现 8 组疑似重复 `reward_settlement`(另 1 组 84886 为寄卖货款误报),多计约 ¥196.20;需财务确认,不自动扣回 |
|
||||
|
||||
## 五、建议的合并顺序
|
||||
|
||||
```bash
|
||||
# 1. 提交当前 P0/P1
|
||||
git add apps/backend/src/db/migrations/103_finance_foundation.sql \
|
||||
apps/backend/src/db/migrations/104_finance_controls.sql \
|
||||
apps/backend/src/db/migrations/105_work_order_consignment.sql \
|
||||
apps/backend/src/services apps/backend/src/routes apps/frontend/src docs
|
||||
git commit -m "feat(finance): 财务 P0/P1 控制与迁移收敛"
|
||||
|
||||
# 2. 全量检查
|
||||
cd apps/backend && npm run check
|
||||
cd ../frontend && npm run check
|
||||
|
||||
# 3. fast-forward 合并
|
||||
git checkout main
|
||||
git merge --ff-only dev
|
||||
git push origin main
|
||||
```
|
||||
|
||||
> 注意:以上是“代码合并”。生产发布还要执行第四节的 3–8;不要只 `docker compose up` 就开放财务页面。
|
||||
|
||||
## 六、结论
|
||||
|
||||
- **分支合并:可以。** `main` 是 `dev` 祖先,fast-forward 无冲突;代码与测试通过。
|
||||
- **生产发布:暂不建议“合并即上线”。** 迁移本身在生产规模数据上验证通过(102→105,约 26 秒),
|
||||
但收入/履约回填、商品目录/成本/人工 SKU/备用金初始化、财务历史同步必须按顺序完成,
|
||||
否则财务页面会显示空数据或毛利严重失真。
|
||||
- **迁移文件优化:103–114 已收敛为 103–105 三个领域迁移。** 生产库只到 102,升级路径只有 3 个文件;后续按“一次发布一个迁移主题”控制增量,未发布迁移在合并前 squash。
|
||||
@@ -426,4 +426,3 @@ FROM (
|
||||
|
||||
- 后台 `mapWorkOrderSharingWorkers` 仍按打手汇总一行(用于列表角标),明细操作走份额表,属设计保留。
|
||||
- 5 项后端既有测试失败:`backup-db.sh` 在容器内缺 `/deploy` 文件;3 项为 CRLF 下源码字符串守卫断言;1 项 `http-cache` 路由守卫。均未触及本次改动文件,建议后续统一把源码守卫改成忽略行尾。
|
||||
|
||||
|
||||
+23
-43
@@ -1,8 +1,8 @@
|
||||
# 成本中心与履约模型
|
||||
|
||||
> 状态:**现行**。商品主数据、成本中心、成本快照、销售事实、履约事实均已落地。
|
||||
> 最后校准:2026-10-26
|
||||
> 代码基线:`dev`(合并 dev1023 + 人工成本完整性修复,迁移基线收敛为 `103`–`111`)
|
||||
> 最后校准: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) 为准**;本文只负责数据模型、迁移与运维。
|
||||
@@ -35,35 +35,19 @@ orders ──┬── order_items ──── fulfillment_mode (唯一履约
|
||||
|
||||
## 一、迁移基线:10 个收敛为 5 个
|
||||
|
||||
原 `dev` 分支上,商品、成本中心、人工履约用 10 个**历史编号**迁移(`103`–`111`,1,275 行)分批落地,字段反复摇摆。其中:
|
||||
|
||||
- 原 `108`(下线人工支出登记)与原 `109`(隔离历史快照主体)在「生产 102 → 当前」的唯一升级路径上是**空操作**;
|
||||
- 原 `111` 是给 `104` 自身种子打的补丁(draft 占位改 active 占位);
|
||||
- 原 `112` 的 `resolution_note` 列本就属于异常表。
|
||||
|
||||
2026-10-25 收敛为 5 个(合计 1,151 行):
|
||||
2026-10-27 最终收敛为 3 个领域迁移(合计约 2,177 行):
|
||||
|
||||
| 迁移 | 内容 | 行数 |
|
||||
| --- | --- | ---: |
|
||||
| `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`(含 `resolution_note`)+ 订单项收入固化 + 系统成本项 + 遗留占位清理 | 503 |
|
||||
| `105_sales_daily_facts.sql` | `order_items.fulfillment_mode` 三列 + `sales_daily_facts` | 166 |
|
||||
| `106_order_refunds.sql` | `order_refunds` 退款表 | 38 |
|
||||
| `107_manual_fulfillment.sql` | 人工 SKU 登记/配置 + 三平台积分钱包 + 备用金账户编号可读化 | 292 |
|
||||
| `103_finance_foundation.sql` | 商品目录、渠道映射、成本项/版本/快照/异常、订单项收入与履约方式、销售事实、退款单 | 861 |
|
||||
| `104_finance_controls.sql` | 人工 SKU/配置、备用金/积分、影子总账、对账/关账、财务控制、L1 钱包硬化 | 1,119 |
|
||||
| `105_work_order_consignment.sql` | 拼单多次参与、大厅/店铺索引、撤份额注释、寄卖对接客服 | 197 |
|
||||
|
||||
后续增量(不参与「10 合 5」的收敛):
|
||||
原来 103–114 的 12 个补丁文件已删除;生产当前在 102,只需执行 103–105。
|
||||
|
||||
| 迁移 | 内容 |
|
||||
| --- | --- |
|
||||
| `108_cost_exception_archive.sql` | 成本异常 60 天归档 |
|
||||
| `109_work_order_share_participation.sql` | 拼单多次参与份额模型(与本模块无关,同批上线) |
|
||||
| `110_work_order_hall_and_shop_indexes.sql` | 大厅置顶/店铺筛选索引(与本模块无关) |
|
||||
| `111_work_order_share_cancel_reason_comment.sql` | 拼单取消原因注释(与本模块无关) |
|
||||
| `108_finance_cost_integrity.sql` | 成本异常归档 + 人工成本单事务兜底(订单项级唯一、`subject_id=order_item_id`、SKU 索引)+ 财务总账/期间/对账 + 商品页/模板查询索引(合并原 108/111/112/113/114) |
|
||||
**合并原则**:同一批上线的结构性 DDL 按领域合并为 `103`(商品/成本/销售/退款)、`104`(人工/总账/控制)、`105`(拼单/寄卖);不再按单表或几行补丁拆文件。
|
||||
|
||||
**合并原则**:同一批上线的结构性 DDL 才合并;数据回填与业务口径注释跟着所属表走。`103` 保持独立(商品主数据是成本与销售事实的共同上游),`106` 保持独立(退款是独立业务线)。
|
||||
|
||||
**人工成本快照主体(2026-10-26 定稿)**:`manual_case` 的 `subject_id` 统一为 `order_item_id`,不再使用 `manual_expenses.id` / `platform_credit_entries.id`(两张表各自 BIGSERIAL,必然撞 id)。资金来源只放 `metadata_json.fundExpenseId / creditEntryId`。迁移 `111` 增加订单项级唯一索引 `uq_cost_snapshots_manual_order_item`,一个订单项最多一条有效人工快照;`subject_id` 为正数的历史行归一到订单项,负数历史孤儿快照按迁移 `104` 约定保留、不参与新链路。
|
||||
**人工成本快照主体(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 字段替换对照
|
||||
|
||||
@@ -268,7 +252,7 @@ resolved_at, superseded_at, created_at
|
||||
- 匹配不到成本时写 `cost_missing` 异常,**不能默认 0**;
|
||||
- 履约方式不存快照里,需要时 join `order_items.fulfillment_mode`;
|
||||
- 人工单的快照主体是 `subject_type='manual_case'` + `subject_id=order_item_id`;资金流水 id 只写 `metadata_json`,**没有独立的登记单表**;
|
||||
- 负数 `subject_id` 表示历史孤儿快照(迁移 `104` 约定),迁移 `112` 保留不动,不参与新链路;
|
||||
- 负数 `subject_id` 表示历史孤儿快照(迁移 `103` 约定),迁移 `104` 保留不动,不参与新链路;
|
||||
- 一个订单项最多一条有效人工快照(`uq_cost_snapshots_manual_order_item`)。
|
||||
|
||||
### 3.5 `cost_exceptions`
|
||||
@@ -289,7 +273,7 @@ created_at, resolved_at, resolved_by
|
||||
|
||||
## 四、订单项收入固化
|
||||
|
||||
`order_items.revenue_amount_fen` / `revenue_basis` 在迁移 `104` 里固化:
|
||||
`order_items.revenue_amount_fen` / `revenue_basis` 在迁移 `103` 里固化:
|
||||
|
||||
| `revenue_basis` | 含义 |
|
||||
| --- | --- |
|
||||
@@ -430,26 +414,22 @@ WHERE oi.id = d.id
|
||||
|
||||
## 八、迁移基线与本地库同步(2026-10-27 收敛)
|
||||
|
||||
生产尚未执行过 `103` 及之后的迁移。2026-10-27 已把原来的 `108/111/112/113/114` 合并为 `108_finance_cost_integrity`,原 `115` 注释迁移改为 `111`;生产升级统一走 **101 → 111**,不再出现 108–115 的补丁链。
|
||||
生产数据库当前在 **102**。dev 已把原 103–114 收敛为 3 个领域迁移:
|
||||
|
||||
1. **全新升级**:从生产 dump 恢复到 `101`,直接 `npm run db:migrate` 升到 `111`;
|
||||
2. **已跑过旧 112–115 的本地/测试库**:schema 已等价,不要删 103–111 重跑;执行一次迁移历史校准:
|
||||
| 迁移 | 内容 |
|
||||
| --- | --- |
|
||||
| `103_finance_foundation.sql` | 商品目录、成本中心、成本项/版本/快照、销售事实、退款单 |
|
||||
| `104_finance_controls.sql` | 人工履约、备用金/积分、影子总账、对账/关账、财务控制、L1 钱包硬化 |
|
||||
| `105_work_order_consignment.sql` | 拼单多次参与、大厅/店铺索引、撤份额注释、寄卖对接客服 |
|
||||
|
||||
```bash
|
||||
docker exec -i order_site-postgres-1 psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
|
||||
-v ON_ERROR_STOP=1 < apps/backend/scripts/reconcile-migration-history.sql
|
||||
npm run db:migrate
|
||||
```
|
||||
- 生产升级:**102 → 103 → 104 → 105**,只执行 3 个迁移文件。
|
||||
- 全新库:`001 → 105` 一次跑完。
|
||||
- 已跑过旧 103–114 的测试库:**直接重建**,或把 `schema_migrations` 清到 102 后重新执行 103–105;旧的逐补丁校准脚本已删除。
|
||||
- 生产库只到 102,不依赖旧 103–114 的迁移记录。
|
||||
|
||||
脚本只把 `schema_migrations` 的旧 `108–115` 行重建为新的 `108–111`,不重复执行 DDL;若库仍停留在旧 103–107,直接正常迁移即可。
|
||||
### 8.1 收入口径回填(迁移 103 不做全表更新)
|
||||
|
||||
2026-10-27 实测:旧 115 库与新 111 库 `pg_dump --schema-only` **完全一致**(仅 pg_dump 随机 token/版本注释不同);新库 001→111 与 101→111 两条路径均通过。
|
||||
|
||||
旧的逐列改造脚本 `scripts/rewrite-local-db-2026-10-23.sql` 已随迁移合并作废,移入 [archive/2026-10-23/](archive/2026-10-23/);只在追溯当时 `carrier_type → cost_model` / `fulfillment_mode` 的改造矩阵时查阅,**不要在新环境执行**。
|
||||
|
||||
### 8.1 收入口径回填(迁移 104 不做全表更新)
|
||||
|
||||
`order_items.revenue_amount_fen` / `revenue_basis` 在迁移 `104` 里**只建列,不刷历史数据**:
|
||||
`order_items.revenue_amount_fen` / `revenue_basis` 在迁移 `103` 里**只建列,不刷历史数据**:
|
||||
375,447 行的全表 `UPDATE` 实测约 **34 秒**,超过默认语句超时(`DATABASE_STATEMENT_TIMEOUT_MS=15s`),
|
||||
放进单事务迁移会拖住启动甚至直接失败。
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# 打手重复奖励核对清单(2026-10-27)
|
||||
|
||||
> 数据源:本地生产快照 `order_site`(与 `backups/postgres/order_site-20261022-092601.dump` 模拟库一致)。
|
||||
> 审计视图:`v_worker_wallet_duplicate_rewards`(迁移 104,已排除寄卖货款并按 `worker_id` 分组)。
|
||||
> **结论:原始按“工单 + 流水类型”分组得到 9 组;排除寄卖货款后,实际疑似重复 8 组。**
|
||||
> 处理原则:**不自动扣回、不删除流水**;由财务逐组核对后走人工调整。
|
||||
|
||||
---
|
||||
|
||||
## 一、汇总
|
||||
|
||||
| 指标 | 数值 |
|
||||
| --- | --- |
|
||||
| 原始同工单奖励流水组 | 9 组 |
|
||||
| 其中寄卖货款误报 | 1 组(工单 84886) |
|
||||
| 实际疑似重复 | **8 组** |
|
||||
| 涉及流水 | 16 条 |
|
||||
| 两组流水合计 | **¥392.40** |
|
||||
| 重复多计(按每组一半估算) | **¥196.20** |
|
||||
| 涉及打手 | 7 人 |
|
||||
| 工单状态 | 全部 `accepted` |
|
||||
|
||||
> 金额单位:分;下表同时给出元。`多计` 按“两条金额相同、保留一条”估算,最终以财务核对为准。
|
||||
|
||||
---
|
||||
|
||||
## 二、疑似重复明细(8 组)
|
||||
|
||||
| # | 工单号 | 工单 ID | 打手 ID | 打手账号 | 流水 ID | 单条金额 | 条数 | 合计 | 多计 | 时间差 | 当前可用余额 |
|
||||
| --- | --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| 1 | WO68ae5cc00012 | 41430 | 7 | 18622832733 | 26738 / 26739 | ¥12.60 | 2 | ¥25.20 | ¥12.60 | 0.218s | ¥22,817.40 |
|
||||
| 2 | WOf9a256424094 | 67580 | 28 | 19391334110 | 55324 / 55325 | ¥47.50 | 2 | ¥95.00 | ¥47.50 | 3.487s | ¥329.04 |
|
||||
| 3 | WO1ef60bfd41f3 | 70556 | 60 | 18700402637 | 58633 / 58634 | ¥0.20 | 2 | ¥0.40 | ¥0.20 | 3.808s | ¥1,397.00 |
|
||||
| 4 | WO26092719382700 | 90262 | 21 | 17386836758 | 85908 / 85909 | ¥6.50 | 2 | ¥13.00 | ¥6.50 | 0.595s | ¥104.50 |
|
||||
| 5 | WO26100215392303 | 100509 | 37 | 13779907018 | 99520 / 99522 | ¥12.80 | 2 | ¥25.60 | ¥12.80 | 0.773s | ¥0.48 |
|
||||
| 6 | WO26100312143075 | 102885 | 51 | 14718183196 | 102228 / 102229 | ¥100.00 | 2 | ¥200.00 | ¥100.00 | 3.400s | ¥1,887.60 |
|
||||
| 7 | WO26100316285341 | 103640 | 21 | 17386836758 | 103021 / 103022 | ¥1.60 | 2 | ¥3.20 | ¥1.60 | 3.376s | ¥104.50 |
|
||||
| 8 | WO26101115455274 | 125710 | 1365 | 15669767683 | 130982 / 130983 | ¥15.00 | 2 | ¥30.00 | ¥15.00 | 0.614s | ¥4,407.18 |
|
||||
|
||||
---
|
||||
|
||||
## 三、逐条流水明细(16 条)
|
||||
|
||||
| 流水 ID | 工单 ID | 打手 ID | 金额 | 余额后 | 冻结后 | 备注 | payload | 创建时间 |
|
||||
| ---: | ---: | ---: | ---: | ---: | ---: | --- | --- | --- |
|
||||
| 26738 | 41430 | 7 | 1260 | 1515880 | 0 | 验收通过结算报酬 | `{}` | 2026-08-26 19:15:09.638 |
|
||||
| 26739 | 41430 | 7 | 1260 | 1517140 | 0 | 验收通过结算报酬 | `{}` | 2026-08-26 19:15:09.856 |
|
||||
| 55324 | 67580 | 28 | 4750 | 124321 | 6000 | 验收通过结算报酬 | `{}` | 2026-09-03 21:05:22.924 |
|
||||
| 55325 | 67580 | 28 | 4750 | 129071 | 6000 | 验收通过结算报酬 | `{}` | 2026-09-03 21:05:26.411 |
|
||||
| 58633 | 70556 | 60 | 20 | 76280 | 0 | 验收通过结算报酬 | `{}` | 2026-09-04 21:18:52.540 |
|
||||
| 58634 | 70556 | 60 | 20 | 76300 | 0 | 验收通过结算报酬 | `{}` | 2026-09-04 21:18:56.348 |
|
||||
| 85908 | 90262 | 21 | 650 | 221195 | 0 | 验收通过结算报酬 | `{}` | 2026-09-27 19:50:13.522 |
|
||||
| 85909 | 90262 | 21 | 650 | 221845 | 0 | 验收通过结算报酬 | `{}` | 2026-09-27 19:50:14.117 |
|
||||
| 99520 | 100509 | 37 | 1280 | 57160 | 12000 | 验收通过结算报酬 | `{}` | 2026-10-02 15:52:18.017 |
|
||||
| 99522 | 100509 | 37 | 1280 | 58440 | 12000 | 验收通过结算报酬 | `{}` | 2026-10-02 15:52:18.790 |
|
||||
| 102228 | 102885 | 51 | 10000 | 331210 | 0 | 验收通过结算报酬 | `{}` | 2026-10-03 12:29:32.277 |
|
||||
| 102229 | 102885 | 51 | 10000 | 341210 | 0 | 验收通过结算报酬 | `{}` | 2026-10-03 12:29:35.677 |
|
||||
| 103021 | 103640 | 21 | 160 | 74193 | 0 | 验收通过结算报酬 | `{}` | 2026-10-03 16:48:16.927 |
|
||||
| 103022 | 103640 | 21 | 160 | 74353 | 0 | 验收通过结算报酬 | `{}` | 2026-10-03 16:48:20.303 |
|
||||
| 130982 | 125710 | 1365 | 1500 | 469690 | 0 | 验收通过结算报酬 | `{}` | 2026-10-11 17:17:22.873 |
|
||||
| 130983 | 125710 | 1365 | 1500 | 471190 | 0 | 验收通过结算报酬 | `{}` | 2026-10-11 17:17:23.487 |
|
||||
|
||||
> 16 条流水的 `payload_json` 都为空、备注都是“验收通过结算报酬”,且同一打手、同一工单、时间差 0.2–3.8 秒,重复特征明显。
|
||||
|
||||
---
|
||||
|
||||
## 四、误报排除:工单 84886
|
||||
|
||||
原始 9 组里包含工单 84886,但它不是重复奖励:
|
||||
|
||||
| 流水 ID | 打手 ID | 金额 | 备注 | payload |
|
||||
| ---: | ---: | ---: | --- | --- |
|
||||
| 87769 | 25 | 13000 | 验收通过结算报酬 | `{}` |
|
||||
| 87770 | 37 | 13000 | 寄卖货款结算 #84886 | `{"inventoryId":159,"workOrderId":84886,"costPriceFen":13000,"submissionNo":"SUB26092515093529"}` |
|
||||
|
||||
- 两条流水属于**不同打手**;
|
||||
- 一条是工单验收奖励,一条是寄卖货款结算;
|
||||
- 金额相同只是巧合,不能按重复扣回。
|
||||
|
||||
因此审计视图已修正为:
|
||||
|
||||
- 按 `worker_id + work_order_id + ledger_type + share_id` 分组;
|
||||
- 排除 `payload_json ? 'inventoryId'` 的寄卖货款;
|
||||
- 结果从 9 组降为 **8 组**。
|
||||
|
||||
---
|
||||
|
||||
## 五、人工核对建议
|
||||
|
||||
每组按以下顺序确认:
|
||||
|
||||
1. 查工单 `accepted` 时间、验收人、`work_order_shares` 是否有同打手多份额;
|
||||
2. 确认两条流水是否都是“验收通过结算报酬”,且没有对应的冲正/扣回流水;
|
||||
3. 对比 `worker_wallets.total_settled_amount` 与全部结算流水,确认多计已进入余额;
|
||||
4. 确认打手当前余额是否足够扣回;不足时登记欠款,不要直接把余额改成负数;
|
||||
5. 处理方式建议:
|
||||
- 余额足够:人工新增负数调整流水(`ledger_type` 建议 `manual_adjustment`,备注“重复结算追回 + 工单号”);
|
||||
- 余额不足:登记 `worker_after_sales_debts` 或线下追缴,后续结算抵扣;
|
||||
- 不删除原流水,不直接改 `worker_wallets` 历史余额。
|
||||
|
||||
---
|
||||
|
||||
## 六、复核 SQL
|
||||
|
||||
```sql
|
||||
-- 当前疑似重复(已排除寄卖货款、按打手分组)
|
||||
SELECT *
|
||||
FROM v_worker_wallet_duplicate_rewards
|
||||
ORDER BY ledger_count DESC, related_work_order_id;
|
||||
|
||||
-- 指定工单的全部奖励流水
|
||||
SELECT id, worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
note, payload_json, created_at
|
||||
FROM worker_wallet_ledgers
|
||||
WHERE related_work_order_id = 41430
|
||||
AND ledger_type IN ('reward_settlement', 'sharing_reward', 'reward_pending_unfreeze')
|
||||
ORDER BY id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、结论
|
||||
|
||||
- 需要人工核对的疑似重复:**8 组,16 条流水,合计 ¥392.40,多计约 ¥196.20**;
|
||||
- 工单 84886 是寄卖货款 + 工单奖励,**不是重复**,不要扣回;
|
||||
- 历史重复不自动修复,避免误伤正常寄卖/多份额结算;
|
||||
- 迁移 104 已为未来奖励流水加 `event_key` 唯一约束,新重复会被数据库直接拒绝。
|
||||
@@ -1,7 +1,7 @@
|
||||
# 财务/成本上线初始化与验收(2026-10-26)
|
||||
|
||||
> 状态:现行操作手册。适用于 `dev` 合并后的财务/成本模块上线;**历史人工成本不补录**。
|
||||
> 代码基线:迁移 `103`–`111`(本手册写于人工成本完整性与配置自动匹配修复之后)。
|
||||
> 代码基线:迁移 `103`–`105`(本手册写于人工成本完整性与配置自动匹配修复之后)。
|
||||
> 数据基线:生产 dump `2026-10-22`(迁移 `101`)。历史人工支出找不回来,只保证**首次配置人工履约之后**的新单可记录、可监控。
|
||||
|
||||
---
|
||||
@@ -16,7 +16,7 @@
|
||||
| 人工 SKU 配置 + 确认履约(扣积分/备用金 + 写 actual 快照) | 是(配置需上线后设置) |
|
||||
| 商品目录、渠道映射、成本版本、成本快照、销售事实 | 是 |
|
||||
| 手工退款登记(冲减收入、关闭成本异常) | 是 |
|
||||
| 影子总账(科目/凭证/试算平衡)、期间关账、平台对账 | 是(迁移 `112`;对账当前为手工账单导入) |
|
||||
| 影子总账(科目/凭证/试算平衡)、期间关账、平台对账 | 是(迁移 `104`;对账当前为手工账单导入) |
|
||||
| 平台退款 API | **部分**:快手行业(91)已接入;抖音/拼多多仍手工登记 |
|
||||
|
||||
明确不做:
|
||||
@@ -38,7 +38,7 @@ bash deploy/ubuntu-deploy.sh --backup --dry-run # 先看计划
|
||||
bash deploy/ubuntu-deploy.sh --backup # 迁移前 pg_dump + OSS,再重建/重启
|
||||
```
|
||||
|
||||
后端启动会自动执行 `102`–`111`。确认:
|
||||
后端启动会自动执行 `102`–`105`。确认:
|
||||
|
||||
```sql
|
||||
SELECT name FROM schema_migrations ORDER BY id DESC LIMIT 5;
|
||||
@@ -158,7 +158,7 @@ GROUP BY status;
|
||||
|
||||
### 5. 历史回填(退款功能上线前必须跑 `revenue:backfill`)
|
||||
|
||||
**存量订单要能退款,必须先回填订单项收入。** 迁移 `104` 只加列不回填;`revenue_basis='unknown'` 的订单项 `revenue_amount_fen=0`,订单详情「退款登记」会因可退金额为 0 而置灰,手工退款也会失败。
|
||||
**存量订单要能退款,必须先回填订单项收入。** 迁移 `103` 只加列不回填;`revenue_basis='unknown'` 的订单项 `revenue_amount_fen=0`,订单详情「退款登记」会因可退金额为 0 而置灰,手工退款也会失败。
|
||||
|
||||
```bash
|
||||
npm run revenue:backfill -- --dry-run # 待回填量,应为 0
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# 对《财务系统深度审计》的代码级复核(2026-10-27)
|
||||
|
||||
> 复核对象:用户给出的财务系统审计结论。
|
||||
> 复核方式:逐条对照当前 `dev` 代码、迁移 `001–105`、本机数据库与生产 dump 模拟升级。
|
||||
> 总评:**架构判断(双轨期、旧报表口径混编、历史人工成本缺失)方向正确;但有 4 处结论需要纠正,2 处需要降级为“风险假设”,1 处已经是旧代码状态。**
|
||||
|
||||
---
|
||||
|
||||
## 一、需要纠正的结论
|
||||
|
||||
### 1. “打手报酬 + 提现会被重复扣减 100%” —— 不成立
|
||||
|
||||
旧报表的净现金流函数实际是:
|
||||
|
||||
```ts
|
||||
// apps/backend/src/services/admin/admin-finance-service.ts:323-338
|
||||
return (
|
||||
row.paidAmount +
|
||||
row.rechargeAmount -
|
||||
row.refundAmount -
|
||||
row.withdrawAmount -
|
||||
row.afterSalesRefundAmount +
|
||||
row.workerRecoveryAmount
|
||||
)
|
||||
```
|
||||
|
||||
**`workerRewardAmount` 没有进入 `netCashFlow`。** 代码没有把“打手报酬”和“提现”同时减掉,因此不存在“100% 双重扣减”。
|
||||
|
||||
真正的问题是**口径混编**:
|
||||
|
||||
- “打手报酬”是权责发生制的成本列;
|
||||
- “提现”是收付实现制的现金流出;
|
||||
- 页面把它们并排展示,但净现金流只减提现,用户如果把两列相加会误读;
|
||||
- 旧页面既不是完整利润表,也不是完整现金流量表。
|
||||
|
||||
**修正结论:不是算术 bug,是报表语义/口径设计问题。**
|
||||
|
||||
### 2. “总账同步 DELETE 重建,违背不可变性” —— 当前代码已修复
|
||||
|
||||
用户引用的 `finance-ledger-service.ts#L251-L265` 是改造前实现。当前代码已经:
|
||||
|
||||
- 不再 `DELETE FROM finance_journal_entries`;
|
||||
- 按日计算收入/退款/成本的 delta,追加 `sales_*_adjustment` 凭证;
|
||||
- 迁移 `104` 增加 `trg_finance_journal_immutable`,有分录的已过账凭证禁止删除;
|
||||
- 迁移 `104` 增加 `trg_finance_journal_balanced`,事务提交时校验整张凭证借贷平衡。
|
||||
|
||||
**修正结论:该隐患在 2026-10-27 的 P0 改造中已关闭;剩余问题是“按日汇总凭证”仍然粒度较粗,不是“物理删除”。**
|
||||
|
||||
### 3. “所有金额都是 BIGINT,底层约束极其严密” —— 不成立,L1 钱包反而很弱
|
||||
|
||||
当前金额类型并不统一:
|
||||
|
||||
| 表/列 | 类型 | 上限 |
|
||||
| --- | --- | --- |
|
||||
| `orders.total_amount` | `INTEGER` | ¥21,474,836.47 |
|
||||
| `worker_wallets.*` 六个余额列 | `INTEGER` | 同上 |
|
||||
| `worker_wallet_ledgers.amount/balance_after` | `INTEGER` | 同上 |
|
||||
| `worker_finance_requests.amount/frozen_amount` | `INTEGER` | 同上 |
|
||||
| `fund_*`、`cost_*`、`finance_*` | `BIGINT` | 远大于业务规模 |
|
||||
|
||||
证据:
|
||||
|
||||
- [001_init.sql:108](../apps/backend/src/db/migrations/001_init.sql#L108) `orders.total_amount INTEGER`
|
||||
- [004_worker_order_platform.sql:31-53](../apps/backend/src/db/migrations/004_worker_order_platform.sql#L31-L53) 钱包/流水均为 `INTEGER`
|
||||
|
||||
而且 `worker_wallets` / `worker_wallet_ledgers` / `worker_finance_requests` **没有任何 CHECK 约束**:
|
||||
|
||||
- 没有 `available_amount >= 0`、`frozen_deposit_amount >= 0` 等余额非负约束;
|
||||
- 没有“余额 = 流水汇总”的触发器;
|
||||
- `worker_wallet_ledgers` 没有 `(related_work_order_id, ledger_type, shareId)` 业务唯一键,结算重试/重复调用没有数据库级幂等。
|
||||
|
||||
**修正结论:新财务表约束较强;真正掌管钱的 L1 钱包约束很弱,且金额是 int4,不是 BIGINT。**
|
||||
|
||||
### 4. “退款撤销生成红字冲正” —— 只有净额调整,没有凭证级 reversal
|
||||
|
||||
当前 `voidOrderRefund` 的实际动作是:
|
||||
|
||||
1. 把 `order_refunds.status` 置为 `voided`;
|
||||
2. 重算 `orders.pay_status`(本轮 P0 修复);
|
||||
3. 重算销售事实;
|
||||
4. 总账按事实差异追加 `sales_refund_adjustment`。
|
||||
|
||||
它**没有**:
|
||||
|
||||
- `reversal_of_id` 指向原退款凭证;
|
||||
- 一张与原凭证一一对应的红字凭证;
|
||||
- 对原退款凭证的状态标记。
|
||||
|
||||
**修正结论:效果上是冲减,审计上不是“红字冲正”;应描述为“软删除 + 事实重算 + 差额调整凭证”。**
|
||||
|
||||
### 5. “打手结算/提现/售后必然死锁” —— 是风险假设,不是已证实结论
|
||||
|
||||
当前主要锁顺序基本一致:
|
||||
|
||||
- 工单结算:`work_orders` → `work_order_shares` → `worker_wallets`
|
||||
- 售后:`work_orders` → `work_order_shares` → `worker_after_sales_cases/debts` → `worker_wallets`
|
||||
- 提现:`worker_finance_requests` → `worker_wallets`
|
||||
|
||||
共同热点是 `worker_wallets`,但代码里没有看到“先锁钱包、再回头锁工单/份额”的反向路径;因此不能直接断言“必然死锁”。
|
||||
|
||||
**修正结论:这是高并发热点与潜在死锁风险,需要压测 + 死锁重试/监控,不是已证实的生产故障。**
|
||||
|
||||
### 6. “6 状态可变模型” —— 实际是 6 个余额桶,不是数据库状态机
|
||||
|
||||
`worker_wallets` 是 6 个金额字段,不是 6 个状态:
|
||||
|
||||
- `available_amount`
|
||||
- `frozen_deposit_amount`
|
||||
- `pending_unfreeze_amount`
|
||||
- `frozen_withdraw_amount`
|
||||
- `total_credited_amount`
|
||||
- `total_settled_amount`
|
||||
|
||||
状态迁移完全由应用层代码维护,数据库没有状态枚举/非负约束/守恒触发器。
|
||||
|
||||
**修正结论:应称为“6 个可变余额桶”,不是严格状态机。**
|
||||
|
||||
---
|
||||
|
||||
## 二、判断正确、但需要补充的部分
|
||||
|
||||
### 1. 历史人工成本缺失导致“假毛利” —— 正确
|
||||
|
||||
- 人工订单项:**115,194**;
|
||||
- 人工收入:**¥14,470,149.94**(1,447.01 万);
|
||||
- 历史人工成本未补录,`cost_snapshots` 没有对应 `manual_case`,销售事实里 `cost_missing_count > 0`、成本按 0 计;
|
||||
- 前端 `AdminSalesProfitPage` 虽然有“有 N 个订单项没有成本快照,当前毛利偏高”的 warning,但报表仍可导出/展示;
|
||||
- 关账门槛只覆盖“首次人工配置之后”的人工单,历史区间不会被自动拦截。
|
||||
|
||||
**建议:全生命周期报表必须默认按 `cost_stage` 过滤或显著标注;历史区间单独出“成本未采集”版本,不能与上线后区间混在一起。**
|
||||
|
||||
### 2. 影子总账是“日汇总粗粒度账” —— 正确
|
||||
|
||||
当前总账仍按 `sales_daily_facts` 的“日期 × 来源 × 店铺 × 商品 × 履约方式”聚合成每日总收入/总退款/总成本,再生成凭证。它没有:
|
||||
|
||||
- 单笔订单/工单级凭证;
|
||||
- provider/shop/product 维度;
|
||||
- 钱包、押金、提现、寄卖、平台费凭证。
|
||||
|
||||
**建议:先把总账定位为“销售/成本影子账”,不要对外称为完整复式总账;转正前补齐事件级过账。**
|
||||
|
||||
### 3. 人工履约事务/幂等做得较好 —— 正确
|
||||
|
||||
- `SELECT ... FOR UPDATE OF oi` 锁订单项;
|
||||
- `uq_cost_snapshots_manual_order_item` 唯一索引;
|
||||
- 备用金/积分扣减与快照同事务;
|
||||
- 失败整体回滚。
|
||||
|
||||
这部分用户判断准确。
|
||||
|
||||
---
|
||||
|
||||
## 三、用户审计漏掉的关键问题
|
||||
|
||||
### 1. L1 钱包没有数据库级余额约束
|
||||
|
||||
`worker_wallets` 没有 `CHECK`,也没有“余额 = Σ流水”的约束。应用层一旦有 bug/并发/手工 SQL,余额可以直接变负或与流水脱节。新财务表有 `fund_accounts` 的余额触发器,但**没有覆盖 `worker_wallets`**。
|
||||
|
||||
### 2. 打手结算流水没有业务幂等键
|
||||
|
||||
`worker_wallet_ledgers` 只有 `(worker_id, created_at)` 索引;没有 `(related_work_order_id, ledger_type, shareId)` 唯一键。工单状态机通常能挡住重复结算,但数据库层没有兜底。
|
||||
|
||||
### 3. `orders.total_amount` 是 int4
|
||||
|
||||
单笔订单上限 ¥21,474,836.47。当前业务量远低于上限,但和“全部 BIGINT”的表述不符,未来大额订单/多币种会踩边界。
|
||||
|
||||
### 4. 旧报表不是“利润表”,也不是“现金流量表”
|
||||
|
||||
它把订单收款、退款、打手报酬、提现、充值、售后追缴拼在一起;`netCashFlow` 只减现金流出,不减打手报酬(这是对的),但页面没有把“成本/现金流/资产负债”拆开。建议保留为“运营收支流水”,另建利润表/现金流量表,而不是继续给它加字段。
|
||||
|
||||
### 5. 迁移收敛后仍有“生产数据初始化”断层
|
||||
|
||||
生产 dump 升级到 `105` 成功,但:
|
||||
|
||||
- `product_catalog=0`;
|
||||
- `cost_snapshots=0`;
|
||||
- `sales_daily_facts=0`;
|
||||
- `fund_accounts=0`;
|
||||
- 375,447 个订单项需要 revenue/fulfillment 回填。
|
||||
|
||||
**代码合并 ≠ 财务可用。** 发布必须按上线手册初始化。
|
||||
|
||||
---
|
||||
|
||||
## 四、对用户路线图的校准
|
||||
|
||||
| 用户步骤 | 复核结论 |
|
||||
| --- | --- |
|
||||
| 第一步:废弃旧报表 | **方向对,但不要直接删**;先把“现金流/运营流水”和“损益”拆成两个页面,旧页保留为现金流辅助 |
|
||||
| 第二步:事件级实时过账 | **正确**;本轮已完成“停止删除重建 + 追加调整凭证 + 数据库触发器”,但尚未做到每笔业务同事务过账 |
|
||||
| 第三步:平台账单 API | **正确**;当前仍是手工 JSON 导入,快手退款 API 仅部分接入 |
|
||||
| 第四步:钱包转总账投影 | **正确但最远**;先补 L1 钱包约束/幂等,再谈投影 |
|
||||
|
||||
---
|
||||
|
||||
## 五、一句话结论
|
||||
|
||||
**用户的“双轨期”判断是对的,旧报表口径混编、历史人工成本缺失、影子账粒度粗也都是真问题;但“重复扣减 100%”“全部 BIGINT/约束严密”“DELETE 重建”“必然死锁”这几条与当前代码不符。真正更危险的是:掌管钱的 `worker_wallets` 仍然是 int4、无 CHECK、无流水幂等,而新财务表的约束并没有覆盖它。**
|
||||
|
||||
---
|
||||
|
||||
## 六、后续补全记录(2026-10-27)
|
||||
|
||||
针对本复核指出的 L1 钱包问题,已补迁移 `104_finance_controls.sql`(含 L1 钱包硬化;生产 dump `102 → 105` 实测见合并评估):
|
||||
|
||||
- `orders.total_amount`、`worker_wallets.*`、`worker_wallet_ledgers.amount/balance_after/frozen_after`、`worker_finance_requests.amount/frozen_amount`、工单/份额/售后/寄卖金额全部 **int4 → BIGINT**;
|
||||
- `worker_wallets` 六个余额、流水后余额、提现申请、订单/工单/份额/售后/寄卖金额加 **非负 CHECK**;
|
||||
- `worker_wallet_ledgers.event_key` + 触发器 + 唯一索引:工单 + 份额 + 流水类型,防未来奖励重复结算;
|
||||
- 新增只读视图 `v_worker_wallet_duplicate_rewards` 与审计脚本 `scripts/audit-worker-wallet.sql`;
|
||||
- 生产模拟数据发现 **8 组疑似重复 `reward_settlement`,合计 ¥392.40、重复多计约 ¥196.20**;另有 1 组(84886)是寄卖货款与工单奖励,属误报。未自动扣回,需财务确认。
|
||||
|
||||
因此,本复核中“L1 钱包是当前最大短板”的结论仍然成立,但已经从“无约束”变成“有 BIGINT、有 CHECK、有未来幂等、有历史审计视图”。
|
||||
@@ -0,0 +1,917 @@
|
||||
# order_site 财务系统深度分析(2026-10-27)
|
||||
|
||||
> 分析对象:订单履约、成本中心、客服备用金/积分、销售事实、影子总账、期间关账与平台对账。
|
||||
> 代码基线:`dev`,HEAD `0de0fd7b`(迁移收敛到 `101–111`)。
|
||||
> 文档基线:`docs/README.md` 标注的现行口径(2026-10-26)。
|
||||
> 验证环境:本机 Docker PostgreSQL(开发库,**不是生产库**);生产尚未部署,按仓库文档口径仍以 `main` 为生产基线。
|
||||
>
|
||||
> 一句话结论:**业务闭环已经能做;作为“财务系统”还不能直接关账、对外出报表或承担审计。**
|
||||
> 核心问题不是“少一张表”,而是**事实层、总账层、资金层三套数据没有形成强一致与可校验的勾稽关系**。
|
||||
>
|
||||
> **2026-10-27 更新:P0 代码改进已落地,见 §十一;本报告前文的“问题/建议”保留为审计基线。**
|
||||
|
||||
---
|
||||
|
||||
## 一、结论先行
|
||||
|
||||
| 维度 | 判断 | 依据 |
|
||||
| --- | --- | --- |
|
||||
| 订单 → 履约 → 成本 | 基本闭环 | `fulfillment_mode` + `cost_snapshots` + `cost_exceptions` 已打通 |
|
||||
| 人工履约资金闭环 | 做得较好 | 单事务、订单项行锁、唯一索引、失败回滚均有实现与集成测试 |
|
||||
| 客服备用金 | 可用,但内控不足 | 余额/流水/支出/对账齐全;对账差额存在过期风险;缺少幂等键 |
|
||||
| 退款 | 手工路径可用,平台路径有硬缺口 | 平台退款先调用平台、后落本地;全额退款撤销不恢复收入 |
|
||||
| 销售事实 | 可算毛利,但可信度依赖成本覆盖 | 成本缺失时毛利被高估;关账期间仍可重算 |
|
||||
| 影子总账 | **只是“销售/成本影子账”** | 11 个科目中仅 4 个有发生额;钱包、押金、提现、寄卖、平台费均未入账 |
|
||||
| 期间关账 | **不建议生产使用** | 无门槛、无锁、可重复关账;已关账期间事实仍可变 |
|
||||
| 权限/审计 | 粒度过粗 | `finance.config` 一权多用;无 maker-checker;审计失败被吞 |
|
||||
| 测试 | 覆盖面尚可 | 后端 866 项(862 pass / 4 skip),前端 181 pass;财务集成测试默认跳过 |
|
||||
|
||||
### 上生产前必须先解决的 7 件事
|
||||
|
||||
1. **关账期间销售事实仍可被重算**,但总账会跳过已关账期间 → 报表与总账必然分叉。
|
||||
2. **平台退款先调用平台、后写本地**,且本地没有 `external_ref` 唯一约束 → 平台已退款、本地没记账,或重复记账。
|
||||
3. **全额退款撤销后 `orders.pay_status` 不恢复** → 撤销了退款,收入仍然是 0。
|
||||
4. **关账不校验门槛、不加锁、可重复关账** → 带着缺失成本/未对账数据关账;并发写入可穿透。
|
||||
5. **备用金对账使用创建时的旧差额** → 对账确认后余额可能仍然不等于实际盘点。
|
||||
6. **总账“已过账不可改、冲正用反向分录”没有实现** → 按日 `DELETE + 重建`,审计链断裂。
|
||||
7. **权限矩阵与文档不一致**:`finance` 被人工履约路由排除,`operator` 反而可以跨客服代录;`finance.config` 同时管成本、凭证、退款、关账、积分。
|
||||
|
||||
---
|
||||
|
||||
## 二、系统全景
|
||||
|
||||
### 2.1 技术栈与部署
|
||||
|
||||
| 层 | 技术 |
|
||||
| --- | --- |
|
||||
| 前端 | React 19、Vite 8、Ant Design 6、React Router 7、TanStack Query、TypeScript |
|
||||
| 后端 | Node.js、Express 5、TypeScript、PostgreSQL、node-pg-migrate |
|
||||
| 网关 | Caddy |
|
||||
| 运行 | Docker Compose(开发 / 生产分离) |
|
||||
|
||||
财务相关代码主要集中在:
|
||||
|
||||
- `apps/backend/src/services/finance/`:总账、销售事实、退款、人工履约
|
||||
- `apps/backend/src/services/cost-center/`:商品目录、成本版本、成本快照、成本异常、积分钱包
|
||||
- `apps/backend/src/services/admin/admin-fund-service.ts`:客服备用金
|
||||
- `apps/backend/src/services/admin/admin-finance-service.ts`:旧版“收支明细”报表
|
||||
- `apps/backend/src/routes/admin/finance*.ts`、`fund.ts`、`cost-center*.ts`:后台 API
|
||||
- `apps/backend/src/db/migrations/100–108`:资金、成本、销售事实、退款、人工履约、总账
|
||||
- `apps/frontend/src/pages/admin/AdminFinancePage.tsx`、`AdminFinanceLedgerPage.tsx`、`AdminSalesProfitPage.tsx`:财务前端
|
||||
|
||||
### 2.2 资金/账务全景
|
||||
|
||||
```text
|
||||
买家支付
|
||||
└─ orders / order_items
|
||||
├─ fulfillment_mode = work_order ──► work_orders ──► reward_amount
|
||||
├─ fulfillment_mode = affiliate_dash ──► affiliate 采购成本
|
||||
├─ fulfillment_mode = legacy_platform ──► 历史成本缺口
|
||||
└─ fulfillment_mode = manual ──► 客服确认履约
|
||||
├─ 备用金 fund_accounts / fund_entries / manual_expenses
|
||||
└─ 积分 platform_credit_accounts / entries
|
||||
│
|
||||
▼
|
||||
cost_snapshots(estimated / actual)
|
||||
│
|
||||
▼
|
||||
sales_daily_facts(按日、按商品、按履约方式)
|
||||
│
|
||||
▼
|
||||
finance_journal_entries / lines(影子总账)
|
||||
│
|
||||
▼
|
||||
finance_periods(关账) + finance_reconciliations(平台对账)
|
||||
|
||||
另一条并行链路:
|
||||
orders / order_refunds / worker_wallet_ledgers / worker_finance_requests
|
||||
└─ admin-finance-service.ts(旧版 UNION ALL 收支明细)
|
||||
```
|
||||
|
||||
**关键事实:上面两条链路不是同一套口径。** 旧版“收支明细”按业务事件拼装,新版“销售毛利”按销售事实,总账又只读销售事实。三套数字在页面上并存,但没有强制对账。
|
||||
|
||||
### 2.3 三套“财务视图”的定位
|
||||
|
||||
| 视图 | 入口 | 口径 | 现状 |
|
||||
| --- | --- | --- | --- |
|
||||
| 收支明细 | `/admin/finance` | 订单收款、退款、打手报酬、提现、充值、售后追缴的现金流/事件汇总 | 旧版 `UNION ALL`,不是复式账 |
|
||||
| 销售毛利 | `/admin/finance/sales-profit` | 权责发生制:订单项收入 − 退款 − 成本快照 | 依赖成本覆盖率;成本缺失时毛利不可用 |
|
||||
| 总账与对账 | `/admin/finance/ledger` | 销售事实按日生成收入/退款/成本凭证 | 仅覆盖销售/成本;不接管钱包/备用金余额 |
|
||||
|
||||
---
|
||||
|
||||
## 三、数据模型与不变量
|
||||
|
||||
### 3.1 核心表
|
||||
|
||||
| 域 | 表 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| 交易 | `orders` / `order_items` | 订单骨架、订单项、`revenue_amount_fen`、`fulfillment_mode` |
|
||||
| 退款 | `order_refunds` | 一条 = 一个订单项的一次退款;整单退款用 `batch_no` 分组 |
|
||||
| 商品 | `product_catalog` / `channel_product_mappings` | 标准商品、渠道 SKU 映射 |
|
||||
| 成本定义 | `cost_items` / `cost_versions` | 成本模型、计价方式、按生效时间的版本 |
|
||||
| 成本事实 | `cost_snapshots` | 订单项/工单/人工单实际使用的成本快照 |
|
||||
| 成本异常 | `cost_exceptions` / `cost_exceptions_archive` | 缺成本、缺映射队列与归档 |
|
||||
| 销售事实 | `sales_daily_facts` | 日期 × 来源 × 店铺 × 商品 × 履约方式的收入/退款/成本/毛利 |
|
||||
| 人工配置 | `manual_sku_registry` / `manual_fulfillment_configs` | 内部 SKU → 渠道 + 消耗积分 + 成本模式 |
|
||||
| 客服资金 | `fund_accounts` / `fund_entries` / `manual_expenses` / `fund_reconciliations` | 备用金账户、流水、支出、对账 |
|
||||
| 客服积分 | `platform_credit_accounts` / `platform_credit_entries` | 南航/乐玩/ROCK 三平台积分钱包 |
|
||||
| 总账 | `finance_accounts` / `finance_journal_entries` / `finance_journal_lines` | 科目、凭证头、借贷行 |
|
||||
| 期间/对账 | `finance_periods` / `finance_reconciliations` / `finance_reconciliation_items` | 关账、平台账单导入与差异明细 |
|
||||
|
||||
### 3.2 关键不变量:哪些是数据库强约束,哪些只是应用层约定
|
||||
|
||||
| 不变量 | 实现 | 强度 |
|
||||
| --- | --- | --- |
|
||||
| 单张凭证借贷平衡 | `normalizeLines` 应用层校验 | **弱**:无数据库触发器/延迟约束,直接 SQL 可写入不平衡凭证 |
|
||||
| 已过账凭证不可修改/删除 | 文档要求;代码按日删除重建 | **未实现**:没有触发器,也没有反向凭证 API |
|
||||
| 一个订单项最多一条有效人工成本快照 | `uq_cost_snapshots_manual_order_item` 部分唯一索引 | **强** |
|
||||
| 一个订单项同一成本项最多一条有效快照 | `uq_cost_snapshots_active_subject_item` 部分唯一索引 | **强** |
|
||||
| 备用金余额 = 期初 + Σ 流水 | 应用层 `fund_accounts.balance_fen` 维护 | **弱**:无触发器/对账约束 |
|
||||
| 备用金支出必须对应一条流水 | `manual_expenses.fund_entry_id` 唯一 + 同事务写入 | **较强** |
|
||||
| 一个订单项最多一条人工支出 | 仅普通索引 `idx_manual_expenses_order_item` | **弱**:可插入多条 |
|
||||
| 退款金额 ≤ 订单项可退金额 | `createManualRefund` 应用层校验 | **弱**:并发下依赖订单行锁,平台路径存在绕过 |
|
||||
| 平台退款外部单号唯一 | 无唯一索引 | **未实现** |
|
||||
| 销售事实毛利 = 净收入 − 成本 | `chk_sales_daily_facts_profit` | **强** |
|
||||
| 净收入 ≥ 0 | `chk_sales_daily_facts_amounts` | **强**(但会掩盖超额退款,见 5.9) |
|
||||
| 会计期间关账后禁止新增凭证 | `assertPeriodOpen` 普通 SELECT | **弱**:无锁,存在并发穿透 |
|
||||
|
||||
这张表是后面所有问题的总根源:**系统把大量“财务不变量”放在了应用层,而应用层又没有全部事务化/加锁。**
|
||||
|
||||
---
|
||||
|
||||
## 四、关键链路拆解
|
||||
|
||||
### 4.1 订单收入固化
|
||||
|
||||
- `order_items.revenue_amount_fen` 在迁移 `104` 增加,回填脚本 `revenue:backfill` 分批执行。
|
||||
- 当前规则:一单一品归整单;多品按等额拆分,`revenue_basis` 标记 `order_total / equal_split / platform_item`。
|
||||
- `sales_daily_facts` 的销售日取 `COALESCE(orders.paid_at, orders.created_at)` 的 `Asia/Shanghai` 日期;退款会回写到**原销售日**,不是退款发生日。
|
||||
|
||||
**评价:**
|
||||
|
||||
- 金额用 `BIGINT` 分,方向正确。
|
||||
- 多品等额拆分不是实际成交价,商品级毛利会失真;当前本地库恰好全部是一单一品,所以暂时没有暴露,但这是未来必须补的口径。
|
||||
- 退款回写原销售日,是后面“关账期间事实仍可变”的根源。
|
||||
|
||||
### 4.2 成本解析与快照
|
||||
|
||||
- 成本项 `cost_items.cost_model`:`affiliate_purchase` / `work_order_reward` / `manual_actual` / `platform_fee` / `payment_fee` / `after_sales_loss` / `other`。
|
||||
- 成本版本按 `effective_from/effective_to` 和范围优先级 `rule > product > shop > provider > global` 解析。
|
||||
- 预估快照由 `syncEstimatedOrderItemCostSnapshots` 生成;工单验收后由 `syncActualWorkOrderCostSnapshots` 升级为 `actual`;人工确认时直接写 `actual`。
|
||||
- 匹配不到成本不默认 0,进入 `cost_exceptions`;销售事实记 `cost_missing_count`。
|
||||
|
||||
**评价:**
|
||||
|
||||
- “不默认 0”是正确的财务设计。
|
||||
- `cost_snapshots` 冻结金额,历史订单不会因为改价而变化,这是对的。
|
||||
- 但 `cost_versions` 的“追溯改价”会**就地 UPDATE 当前 active 版本**(见 5.11),版本历史并不真正不可变。
|
||||
- 预估快照一旦写入不会因版本变化刷新;对“支付时点成本”来说是合理的,但对“当前成本估算”来说会过期,需要在报表上明确口径。
|
||||
|
||||
### 4.3 人工履约资金闭环(实现最好的一段)
|
||||
|
||||
`confirmManualFulfillment` 的关键顺序:
|
||||
|
||||
1. 先按 SKU 兜底匹配 `manual_fulfillment_configs`;
|
||||
2. `SELECT ... FOR UPDATE OF oi` 锁订单项;
|
||||
3. 复查是否已有有效 `manual_case` 快照;
|
||||
4. 解析成本项/成本版本;
|
||||
5. 现金路径:`writeFundExpense` 扣备用金并写 `manual_expenses`;积分路径:`consumePlatformCreditWithClient` 扣积分并写流水;
|
||||
6. 写 `cost_snapshots`(`actual`,`subject_id = order_item_id`);
|
||||
7. 收口 `manual_cost_missing` 异常;
|
||||
8. 全部在同一个 `withTransaction` 中。
|
||||
|
||||
**优点:**
|
||||
|
||||
- 钱与成本同事务,失败整体回滚;
|
||||
- 订单项行锁 + 部分唯一索引,双击/并发只能成功一次;
|
||||
- 集成测试覆盖了重复确认、并发确认、快照失败回滚。
|
||||
|
||||
**待改进:**
|
||||
|
||||
- `cost_mode` 字段在确认时没有被读取,实际只按 `credit_quantity > 0` 判断固定/可变(见 5.7);
|
||||
- 快照 metadata 记录的是请求里的 `creditOwnerAdminUserId`,不是解析后的实际扣款人(见 5.8);
|
||||
- `finance` 角色被路由排除,无法执行文档所说的“财务代录”(见 5.6)。
|
||||
|
||||
### 4.4 退款
|
||||
|
||||
- 手工退款:`createManualRefund` 锁订单行、按订单项计算剩余可退、插入 `order_refunds`、全额退完置 `orders.pay_status='refunded'`、关闭成本异常;提交后重算销售事实并同步总账。
|
||||
- 平台退款(快手 91):`approveAndRecordKuaishouRefund` 先调用平台“同意退款”,成功后再调用 `createManualRefund` 落本地。
|
||||
- 撤销:`voidOrderRefund` 把退款行置 `voided`,重算销售事实与总账。
|
||||
|
||||
**问题集中在平台路径和撤销路径,见 5.2、5.3。**
|
||||
|
||||
### 4.5 销售事实 → 总账
|
||||
|
||||
`syncFinanceLedgerFromFacts` 按日聚合 `sales_daily_facts`:
|
||||
|
||||
- 收入:借 `1101` 应收平台 / 贷 `4001` 商品销售收入
|
||||
- 退款:借 `4101` 退款冲减 / 贷 `1101` 应收平台
|
||||
- 成本:借 `5001` 履约成本 / 贷 `2001` 应付履约款
|
||||
|
||||
幂等键:`event_type + ref_type + ref_id`,其中 `ref_id = 日期`。
|
||||
|
||||
**评价:**
|
||||
|
||||
- 能生成一张“借贷平衡”的日汇总账;
|
||||
- 但只使用 `1101/2001/4001/4101/5001` 五个科目,`1001/1002/1201/2005/3001/5101` 从未使用;
|
||||
- 没有现金收款、没有应付结算、没有备用金/积分资产、没有钱包/押金/提现;
|
||||
- 因此它不是完整复式账,而是**收入/成本权责发生制的影子账**。
|
||||
|
||||
### 4.6 备用金
|
||||
|
||||
- `fund_accounts`:主管池 / 客服账户;`balance_fen` 由应用层维护。
|
||||
- `fund_entries`:`opening/funding/issue/expense/return/adjustment`,只追加。
|
||||
- `manual_expenses`:客服支出明细,必须关联一条 `fund_entries`。
|
||||
- 发放/缴回会写主管与客服两条相反流水,`transfer_no` 关联,设计正确。
|
||||
- 对账:`fund_reconciliations` 记录系统余额、实际余额、差额。
|
||||
|
||||
**优点:**
|
||||
|
||||
- 写路径统一收口到 `writeFundExpense`;
|
||||
- 发放/缴回/支出都先 `SELECT ... FOR UPDATE` 锁账户;
|
||||
- `balance_fen >= 0`、`amount_fen <> 0` 等约束齐全。
|
||||
|
||||
**问题:**
|
||||
|
||||
- 对账差额在创建时计算,确认时直接拿来调整当前余额(见 5.5);
|
||||
- 发放/缴回/拨付/积分充值没有幂等键(见 5.9);
|
||||
- 账户余额没有数据库级“余额 = Σ流水”约束。
|
||||
|
||||
### 4.7 平台对账
|
||||
|
||||
- 手工粘贴 JSON 账单,按 `provider + platform_order_id` 匹配订单/退款。
|
||||
- 明细状态:`matched / amount_mismatch / missing_order / missing_refund / duplicate`。
|
||||
- 批次可标记 `resolved`。
|
||||
|
||||
**问题:**
|
||||
|
||||
- 销售行按 `orders.total_amount` 匹配,不按订单项;
|
||||
- 退款行没有 `external_ref` 时取该订单“最新一条退款”,多笔退款会重复匹配同一条;
|
||||
- 批次没有唯一约束,重复导入同一账单不会被拦截(见 5.10)。
|
||||
|
||||
---
|
||||
|
||||
## 五、正确性审计:按严重度排列
|
||||
|
||||
> 说明:以下“P0”指上生产前必须修,否则可能造成账实不符、资金损失或无法审计;“P1”指会显著降低报表/内控可信度;“P2”指体验、性能或边界问题。
|
||||
|
||||
### 5.1 P0:关账期间销售事实仍可重算,总账却冻结
|
||||
|
||||
**证据:**
|
||||
|
||||
- `rebuildSalesDailyFacts` 直接 `DELETE FROM sales_daily_facts WHERE stat_date BETWEEN ...`,没有任何 `finance_periods` 检查:[sales-facts-service.ts:47–57](../apps/backend/src/services/finance/sales-facts-service.ts#L47-L57)
|
||||
- 定时任务每 5 分钟对最近 30 天重算:[cost-finance-sync-job.ts:47–55](../apps/backend/src/services/scheduler/cost-finance-sync-job.ts#L47-L55)
|
||||
- 退款提交后立即重算原销售日:[order-refund-service.ts:301–305](../apps/backend/src/services/finance/order-refund-service.ts#L301-L305)
|
||||
- 总账同步遇到已关账期间直接 `skipped`:[finance-ledger-service.ts:197–205](../apps/backend/src/services/finance/finance-ledger-service.ts#L197-L205)
|
||||
|
||||
**触发路径:**
|
||||
|
||||
1. 9 月关账;
|
||||
2. 10 月发生一笔 9 月订单的退款;
|
||||
3. `createManualRefund` 重算 9 月某天的 `sales_daily_facts`;
|
||||
4. 总账同步看到 9 月已关账,跳过;
|
||||
5. 9 月销售毛利报表已经变化,9 月总账仍是关账时点数字。
|
||||
|
||||
**影响:**
|
||||
|
||||
- 报表与总账永久分叉;
|
||||
- 关账失去意义;
|
||||
- 无法向审计解释“关账后收入为什么变了”。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 方案 A(推荐):已关账期间的退款/成本变化不直接改原期间事实,改为在**当前开放期间**生成“以前年度/以前期间调整”事实或总账调整凭证;
|
||||
- 方案 B:关账时把 `sales_daily_facts` 快照化(按期间冻结),后续变更只写调整表;
|
||||
- 无论哪种,`rebuildSalesDailyFacts` 必须检查 `finance_periods.status`,不能静默重写已关账期间。
|
||||
|
||||
### 5.2 P0:平台退款先调用平台,后落本地,且没有幂等键
|
||||
|
||||
**证据:**
|
||||
|
||||
- 平台调用在第 522 行,本地 `createManualRefund` 在第 529 行:[order-refund-service.ts:522–541](../apps/backend/src/services/finance/order-refund-service.ts#L522-L541)
|
||||
- 可退金额校验在 `createManualRefund` 内部(第 188 行),也就是平台调用之后:[order-refund-service.ts:181–193](../apps/backend/src/services/finance/order-refund-service.ts#L181-L193)
|
||||
- `order_refunds` 只有 `refund_no` 唯一,没有 `(source, external_ref)` 唯一约束:[103_finance_foundation.sql](../apps/backend/src/db/migrations/103_finance_foundation.sql)
|
||||
|
||||
**触发路径:**
|
||||
|
||||
- 平台退款金额大于本地剩余可退金额 → 平台已同意退款,本地 `createManualRefund` 抛 409,账上没记;
|
||||
- 平台调用成功、本地插入成功,但接口超时/响应丢失 → 重试可能再插一条;
|
||||
- 平台退款单号重复提交 → 没有数据库层拦截。
|
||||
|
||||
**影响:**
|
||||
|
||||
- 平台已退款、系统未退款:应收/收入虚高;
|
||||
- 重复退款:收入被多冲减;
|
||||
- 对账时平台有退款、系统无记录或金额不一致。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 引入退款状态机:`requested → platform_approved → recorded → reconciled`(或 outbox);
|
||||
- 在调用平台前先落“退款意图”,并做剩余可退金额预校验;
|
||||
- 对 `(source, external_ref)` 建唯一索引(`external_ref <> ''` 的部分唯一索引);
|
||||
- 平台调用使用平台退款单号做幂等键;本地失败要有补偿/重试任务,不能只抛错。
|
||||
|
||||
### 5.3 P0:全额退款撤销后,收入仍然是 0
|
||||
|
||||
**证据:**
|
||||
|
||||
- 全额退款时把订单置为 `refunded`:[order-refund-service.ts:240–263](../apps/backend/src/services/finance/order-refund-service.ts#L240-L263)
|
||||
- 撤销只更新 `order_refunds.status='voided'`,没有恢复 `orders.pay_status`:[order-refund-service.ts:354–366](../apps/backend/src/services/finance/order-refund-service.ts#L354-L366)
|
||||
- 销售事实在没有 confirmed 退款时,仍会把 `pay_status='refunded'` 当作全额退款:[sales-facts-service.ts:70–79](../apps/backend/src/services/finance/sales-facts-service.ts#L70-L79)
|
||||
|
||||
**影响:**
|
||||
|
||||
- 撤销全额退款后,订单仍是 `refunded`,销售事实仍然按全额退款冲减;
|
||||
- 成本异常在退款时被置为 `ignored`,撤销时没有重新打开;
|
||||
- 订单领取/核销等下游逻辑也可能继续把订单当作已退款。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- `voidOrderRefund` 在同一事务内根据“剩余 confirmed 退款”重算 `orders.pay_status`(0 → `paid`,否则保持 `refunded`);
|
||||
- 撤销后重新打开被自动关闭的 `cost_exceptions`(`status='open'`,清空 `resolved_at`);
|
||||
- 补一条集成测试:全额退款 → 撤销 → 销售事实恢复、总账恢复、异常恢复。
|
||||
|
||||
### 5.4 P0:关账没有门槛、没有锁,且可以重复关账
|
||||
|
||||
**证据:**
|
||||
|
||||
- `closeFinancePeriod` 只是先同步、再算试算平衡,然后直接 upsert 为 `closed`;没有 `if (!trial.balanced) throw`,也没有检查 `cost_missing_count`、人工成本、资金流水、对账差异:[finance-ledger-service.ts:413–448](../apps/backend/src/services/finance/finance-ledger-service.ts#L413-L448)
|
||||
- `assertPeriodOpen` 是普通 `SELECT`,没有 `FOR UPDATE` 或 advisory lock:[finance-ledger-service.ts:537–548](../apps/backend/src/services/finance/finance-ledger-service.ts#L537-L548)
|
||||
- 文档明确要求“人工单无成本/未扣备用金/金额不一致不允许关账”:[财务与履约口径总纲:126–130](./财务与履约口径总纲-2026-10-23.md#L126-L130)
|
||||
- 文档说“关账前会先补齐该期间凭证,并校验试算平衡”:[财务上线初始化与验收:283](./财务上线初始化与验收-2026-10-26.md#L283)
|
||||
|
||||
**影响:**
|
||||
|
||||
- 可以带着大量 `cost_missing` 关账,毛利失真;
|
||||
- 可以带着未对账的平台差异关账;
|
||||
- 并发请求可以在关账检查与提交之间写入凭证;
|
||||
- 重复关账会覆盖 `closed_at` 与 `summary_json`,关账审计被改写。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 关账放在一个事务里,先对 `finance_periods` 行加锁(或 `pg_advisory_xact_lock`),再检查门槛,再写 `closed`;
|
||||
- 门槛至少包括:试算平衡、`cost_missing_count=0`、人工单无成本=0、人工支出与流水差异=0、未解决对账差异=0;
|
||||
- 已关账期间再次关账应拒绝或幂等返回,不得覆盖原 `closed_at`;
|
||||
- 关账/反关账必须要求填写原因,并写审计。
|
||||
|
||||
### 5.5 P0:备用金对账使用“创建时的旧差额”调整“当前余额”
|
||||
|
||||
**证据:**
|
||||
|
||||
- 创建对账时保存 `system_balance_fen` 与 `difference_fen`:[admin-fund-service.ts:2001–2031](../apps/backend/src/services/admin/admin-fund-service.ts#L2001-L2031)
|
||||
- 确认时读的是存储的 `difference_fen`,加到**当前**余额:[admin-fund-service.ts:2087](../apps/backend/src/services/admin/admin-fund-service.ts#L2087)、[admin-fund-service.ts:2115–2126](../apps/backend/src/services/admin/admin-fund-service.ts#L2115-L2126)
|
||||
|
||||
**触发路径:**
|
||||
|
||||
1. 创建对账:系统余额 100,实际 90,差额 -10;
|
||||
2. 确认前发生一笔 20 元支出,余额变成 80;
|
||||
3. 确认时用 -10 调整当前余额 80 → 70;
|
||||
4. 实际盘点 90,账实仍然差 20。
|
||||
|
||||
**影响:**
|
||||
|
||||
- 对账确认后余额仍不等于实际;
|
||||
- 差异被“调整”进了 adjustment 流水,但金额不对,后续更难查;
|
||||
- 如果创建与确认间隔很久,问题更严重。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 确认时锁账户并重新计算 `actualBalanceFen - currentSystemBalanceFen`;
|
||||
- 或者创建对账时就锁账户/冻结期间,禁止期间内再发生流水;
|
||||
- `difference_fen` 应作为“确认时点的差额”,而不是创建时的快照。
|
||||
|
||||
### 5.6 P1:权限矩阵与文档/实现不一致,且缺少职责分离
|
||||
|
||||
**证据:**
|
||||
|
||||
- 人工履约路由只允许 `admin / operator / support`,**不包含 finance**:[finance.ts:107–126](../apps/backend/src/routes/admin/finance.ts#L107-L126)
|
||||
- 但服务层 `PRIVILEGED_ROLES` 明确包含 finance,说明代码作者预期 finance 可以代录:[manual-fulfillment-service.ts:493–500](../apps/backend/src/services/finance/manual-fulfillment-service.ts#L493-L500)
|
||||
- 文档说“管理员/财务可代录到指定客服账户”:[人工支出与备用金.md:32](./人工支出与备用金.md#L32)
|
||||
- 同一篇文档又说“管理员/财务/运营可代录”:[人工支出与备用金.md:104](./人工支出与备用金.md#L104)
|
||||
- `finance.config` 被用于成本配置、手工凭证、退款、关账、积分调整等所有写操作;`finance.review`、`finance.export` 基本未使用:[072_admin_finance_role.sql:11–14](../apps/backend/src/db/migrations/072_admin_finance_role.sql#L11-L14)、[103_finance_foundation.sql](../apps/backend/src/db/migrations/103_finance_foundation.sql)
|
||||
|
||||
**影响:**
|
||||
|
||||
- 文档承诺的“财务代录”实际不可用;
|
||||
- `operator` 可以跨客服代录备用金/积分(`resolveFundAccountId` / `resolveCreditOwnerAdminUserId` 只校验目标账户存在,不校验团队归属),是否符合内控需要明确;
|
||||
- 同一个 `finance` 用户可以改成本、做手工凭证、退款、关账、调积分,没有 maker-checker;
|
||||
- 无法把“制单/审核/关账”拆给不同人。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 明确能力矩阵,拆出 `finance.post` / `finance.close` / `finance.reconcile` / `finance.cost_config`;
|
||||
- 财务角色要么允许人工履约,要么在文档中删掉“财务代录”;
|
||||
- 关账、手工凭证、退款、积分调整至少两人复核(制单人与审核人不能同一人);
|
||||
- 代录接口增加目标客服的责任主管/团队范围校验(如果业务要求团队隔离)。
|
||||
|
||||
### 5.7 P1:`cost_mode` 字段在确认时没有生效
|
||||
|
||||
**证据:**
|
||||
|
||||
- 配置表有 `cost_mode`(`fixed/variable`):[104_finance_controls.sql](../apps/backend/src/db/migrations/104_finance_controls.sql)
|
||||
- 确认时读取了 `cfg.cost_mode`,但 `resolveManualFulfillmentCost` 只接收 `creditQuantity/unitPriceFen/providedCostFen`,没有接收 `costMode`:[manual-fulfillment-service.ts:285–298](../apps/backend/src/services/finance/manual-fulfillment-service.ts#L285-L298)、[manual-fulfillment-service.ts:670–697](../apps/backend/src/services/finance/manual-fulfillment-service.ts#L670-L697)
|
||||
|
||||
**影响:**
|
||||
|
||||
- 保存逻辑目前按 `creditQuantity > 0` 自动派生 `cost_mode`,所以正常 UI 路径一致;
|
||||
- 但 DB 导入、历史数据、手工改库可以造出 `cost_mode='variable' + credit_quantity>0`,系统仍按固定成本扣款;
|
||||
- 字段语义是“装饰性”的,容易误导配置人员。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 要么删除 `cost_mode`,只保留 `credit_quantity`(0 = 可变);
|
||||
- 要么在确认时真正读取 `cost_mode`,并校验“fixed 必须有 credit_quantity>0,variable 必须为 0”;
|
||||
- 增加一条配置一致性约束/检查 SQL。
|
||||
|
||||
### 5.8 P1:人工快照的扣款人 metadata 不准确
|
||||
|
||||
**证据:**
|
||||
|
||||
- 快照 metadata 的 `creditOwnerAdminUserId` 取的是请求参数,而不是 `resolveCreditOwnerAdminUserId` 的返回值:[manual-fulfillment-service.ts:398–408](../apps/backend/src/services/finance/manual-fulfillment-service.ts#L398-L408)
|
||||
|
||||
**影响:**
|
||||
|
||||
- 客服自助确认时,请求里没有 `creditOwnerAdminUserId`,metadata 为 `null`,但积分实际从该客服账户扣除;
|
||||
- 审计/对账按 metadata 找人会对不上,只能再查 `platform_credit_entries`。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 把解析后的 `creditOwnerAdminUserId` 传进快照写入;
|
||||
- 同时记录积分账户 ID/流水号,保证能从快照直接追溯到资金流水。
|
||||
|
||||
### 5.9 P1:幂等与审计缺口
|
||||
|
||||
| 问题 | 证据 | 影响 |
|
||||
| --- | --- | --- |
|
||||
| 手工凭证重复 `reference` 静默复用旧凭证 | `ON CONFLICT DO NOTHING` 后返回 existing:[finance-ledger-service.ts:80–110](../apps/backend/src/services/finance/finance-ledger-service.ts#L80-L110) | 用户以为新建成功,实际未入账;审计 targetId 指向旧凭证 |
|
||||
| 凭证金额小数被 `Math.trunc` 截断 | [finance-ledger-service.ts:503–504](../apps/backend/src/services/finance/finance-ledger-service.ts#L503-L504) | 1.9 分变 1 分,金额被静默修改 |
|
||||
| 备用金拨付/发放/缴回无幂等键 | [admin-fund-service.ts:1248](../apps/backend/src/services/admin/admin-fund-service.ts#L1248)、[1305](../apps/backend/src/services/admin/admin-fund-service.ts#L1305)、[1431](../apps/backend/src/services/admin/admin-fund-service.ts#L1431) | 双击/重试可能重复转账 |
|
||||
| 积分充值/调整无幂等键 | [platform-credit-service.ts:165](../apps/backend/src/services/cost-center/platform-credit-service.ts#L165)、[219](../apps/backend/src/services/cost-center/platform-credit-service.ts#L219) | 重复充值 |
|
||||
| 审计日志写入失败被吞 | [session.ts:156–165](../apps/backend/src/routes/admin/session.ts#L156-L165) | 财务操作成功但没有审计记录 |
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 所有资金写命令接收 `requestId/idempotencyKey`,在 DB 建唯一键;
|
||||
- 审计写入与业务同事务,或写 outbox 后异步落库;财务操作不允许“审计失败但业务成功”;
|
||||
- 金额入参必须是整数分,拒绝小数;`reference` 重复应返回 409,而不是静默复用。
|
||||
|
||||
### 5.10 P1:平台对账的匹配粒度与批次幂等不足
|
||||
|
||||
**证据:**
|
||||
|
||||
- 销售行用 `orders.total_amount` 作为 expected,而不是订单项/账单行:[finance-reconciliation-service.ts:292–298](../apps/backend/src/services/finance/finance-reconciliation-service.ts#L292-L298)
|
||||
- 退款行没有 `external_ref` 时取该订单最新一条 confirmed 退款:[finance-reconciliation-service.ts:301–318](../apps/backend/src/services/finance/finance-reconciliation-service.ts#L301-L318)
|
||||
- `finance_reconciliations` 没有 `(provider, period_from, period_to, source)` 唯一约束:[104_finance_controls.sql](../apps/backend/src/db/migrations/104_finance_controls.sql)
|
||||
|
||||
**影响:**
|
||||
|
||||
- 一单多品、平台按明细行出账时,销售行金额对不上;
|
||||
- 同一订单多笔退款且账单不带退款单号时,多行可能匹配到同一条退款,误报 matched;
|
||||
- 同一账单可以重复导入,形成多个批次,差异统计重复;
|
||||
- `expected_fen` 对 missing 行记 0,汇总差额不是真正的“全部行差额”。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 账单行增加订单项/平台明细号维度,按明细匹配;
|
||||
- 退款匹配必须要求平台退款单号,或至少要求金额+时间+订单项;
|
||||
- 对导入文件做 hash/批次唯一约束,重复导入拒绝或幂等;
|
||||
- missing 行也纳入 expected/actual 的差异计算,并区分“系统缺失”和“金额不符”。
|
||||
|
||||
### 5.11 P1:成本版本“追溯改价”就地 UPDATE,破坏版本历史
|
||||
|
||||
**证据:**
|
||||
|
||||
- 追溯改价时直接 `UPDATE cost_versions SET amount_fen=..., effective_from=..., created_by=..., approved_by=...`:[product-catalog-service.ts:1397–1415](../apps/backend/src/services/cost-center/product-catalog-service.ts#L1397-L1415)
|
||||
- 自动发货价格同样有就地更新:[affiliate-product-service.ts:348–362](../apps/backend/src/services/cost-center/affiliate-product-service.ts#L348-L362)
|
||||
- 文档明确“改价只新增版本;不覆盖历史”:[成本中心与履约模型.md:232](./成本中心与履约模型.md#L232)、[:265](./成本中心与履约模型.md#L265)
|
||||
- `approved_by_admin_user_id` 被写成当前操作人,等于自批:[product-catalog-service.ts:1404](../apps/backend/src/services/cost-center/product-catalog-service.ts#L1404)
|
||||
|
||||
**影响:**
|
||||
|
||||
- `cost_snapshots` 的历史金额仍保留,但 `cost_version_id` 指向的版本内容已经变了;
|
||||
- 想从版本表还原“当时生效的价格”会得到错误结果;
|
||||
- 审计无法区分“当时定价”与“后来改价”;
|
||||
- 自批使 `approved_by` 字段失去意义。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 追溯改价也插入新版本(更早的 `effective_from`),旧版本保留并设置 `effective_to`;
|
||||
- 如果必须修正错误版本,保留原行并新增“更正版本”,不要 UPDATE 金额;
|
||||
- `approved_by` 必须与 `created_by` 不同,或至少记录审批时间/原因。
|
||||
|
||||
### 5.12 P1:总账只是“销售/成本影子账”,不能当完整复式账
|
||||
|
||||
**证据:**
|
||||
|
||||
- 总账只生成三类凭证:收入、退款、成本:[finance-ledger-service.ts:212–249](../apps/backend/src/services/finance/finance-ledger-service.ts#L212-L249)
|
||||
- 所有成本都进 `5001`,没有区分人工 `5101`:[finance-ledger-service.ts:244–248](../apps/backend/src/services/finance/finance-ledger-service.ts#L244-L248)
|
||||
- 科目表里的 `1001/1002/1201/2005/3001/5101` 从未被写入(本机库查询确认)
|
||||
- `finance_journal_lines.dimensions_json` 在销售凭证中始终是 `{}`,没有 provider/shop/product/order 维度
|
||||
|
||||
**影响:**
|
||||
|
||||
- 试算平衡只能证明“每张凭证自身借贷相等”,不能证明账务完整;
|
||||
- 资产负债表科目(银行、备用金、积分预付、应付)没有发生额,无法与钱包/资金对账;
|
||||
- 无法输出店铺/商品/订单级损益;
|
||||
- 不能把“总账”作为正式账套使用。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 明确命名:当前是“销售/成本影子账”,不要对外称完整总账;
|
||||
- 若要转正,补齐钱包、押金、提现、充值、寄卖、售后、平台费、备用金/积分的完整事件与结算分录;
|
||||
- 给销售凭证写入 `provider/shopId/productId/orderId/fulfillmentMode` 维度;
|
||||
- 增加“总账 vs 销售事实 vs 钱包/备用金”的日对账。
|
||||
|
||||
### 5.13 P1:三套报表口径并存,缺少对账
|
||||
|
||||
| 口径 | 位置 | 差异点 |
|
||||
| --- | --- | --- |
|
||||
| 旧收支明细 | `admin-finance-service.ts` | 订单收款、退款、打手报酬、提现、充值、售后追缴的 `UNION ALL`;按事件日期 |
|
||||
| 销售毛利 | `sales-facts-service.ts` | 权责发生制;按 `paid_at` 归属;退款回写原销售日 |
|
||||
| 影子总账 | `finance-ledger-service.ts` | 只读销售事实;按日总额 |
|
||||
|
||||
**影响:**
|
||||
|
||||
- 同一时间段,“收款/退款/打手报酬”在三个页面可能不同;
|
||||
- 财务/运营无法判断哪个是权威口径;
|
||||
- 对外报表没有唯一来源。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 明确唯一权威口径(建议:销售事实 + 总账,旧报表只做现金流辅助);
|
||||
- 在页面上标注口径与“不可直接比较”;
|
||||
- 增加日对账:旧报表事件 vs 销售事实 vs 总账。
|
||||
|
||||
### 5.14 P2:默认日期使用 UTC,月初边界会取错月份
|
||||
|
||||
**证据:**
|
||||
|
||||
- 试算平衡和总账同步路由用 `new Date().toISOString().slice(0,10)` 取“今天”:[finance-ledger.ts:48](../apps/backend/src/routes/admin/finance-ledger.ts#L48)、[:78](../apps/backend/src/routes/admin/finance-ledger.ts#L78)
|
||||
|
||||
**影响:**
|
||||
|
||||
- 上海时间每月 1 日 00:00–07:59,UTC 还是上个月最后一天;
|
||||
- 默认期间会变成上个月,用户看到的试算平衡/同步范围不对。
|
||||
|
||||
**修复建议:**
|
||||
|
||||
- 统一用 `formatShanghaiDate(new Date())`,不要用 UTC 日期。
|
||||
|
||||
### 5.15 P2:金额精度与 JS Number
|
||||
|
||||
- 数据库用 `BIGINT` 分,正确;但服务层大量 `Number(bigint)` 再回写。
|
||||
- 当前规模(十亿分级别)远低于 `Number.MAX_SAFE_INTEGER`,暂时安全。
|
||||
- 但 `Math.trunc` 接受小数、`Number` 转换没有上限校验;未来接入更大金额/多币种时需要改成字符串/整数运算。
|
||||
|
||||
---
|
||||
|
||||
## 六、数据质量现状(本机开发库,非生产)
|
||||
|
||||
> 以下数字来自本机 Docker `order_site-postgres-1` 的只读查询(2026-10-27),**不是生产库**。列在这里是为了说明:在“未按上线手册初始化”的默认状态下,财务页面会呈现什么,以及为什么关账前必须做数据门禁。
|
||||
|
||||
| 指标 | 数值 | 解读 |
|
||||
| --- | ---: | --- |
|
||||
| 订单 / 订单项 | 375,459 / 375,447 | 与文档基线量级一致 |
|
||||
| 销售事实行 / 日期范围 | 5,381 / 2026-09-25–2026-10-22 | 只有 28 天,不是全历史 |
|
||||
| 未归一订单项 | 340,225 / 375,447 = **90.6%** | 商品级毛利基本不可用 |
|
||||
| `work_order` 未归一 | 153,148 / 153,148 = **100%** | 接单平台商品维度全缺 |
|
||||
| `affiliate_dash` 未归一 | 53,204 / 53,204 = **100%** | 自动发货商品维度全缺 |
|
||||
| `legacy_platform` 未归一 | 53,901 / 53,901 = **100%** | 历史平台商品维度全缺 |
|
||||
| `manual` 未归一 | 79,972 / 115,194 = **69.4%** | 人工也只有三成挂了商品 |
|
||||
| 标准商品 / 渠道映射 | 542 / **0** | 本机库只有 `man:` 商品,没有渠道映射 |
|
||||
| 成本快照 | 72,297,全部 `work_order_reward` | 没有人工/自动发货成本快照 |
|
||||
| 缺成本订单项 | 63,764 / 146,338 = **43.6%** | 这些项成本按 0 计 |
|
||||
| 缺成本对应收入 | ¥8,730,818.99 / ¥13,230,754.79 = **66.0%** | 毛利被严重高估 |
|
||||
| 成本异常 | 146,338(111,116 open) | 主要是 `product_mapping_missing` |
|
||||
| 人工 SKU 配置 / 登记 | 0 / 0 | 人工链路未初始化 |
|
||||
| 备用金账户 / 支出 | 0 / 0 | 资金链路未初始化 |
|
||||
| 总账凭证 / 分录 | 56 / 112 | 只有 2026-09、2026-10 两个月 |
|
||||
| 总账不平衡凭证 | 0 | 应用层校验有效 |
|
||||
| 已使用科目 | 4 个(1101/2001/4001/5001) | 1001/1002/1201/2005/3001/5101 全为 0 |
|
||||
| 期间 / 对账批次 | 0 / 0 | 尚未关账、未导入账单 |
|
||||
|
||||
**结论:**
|
||||
|
||||
- 当前库的“毛利 ¥1,047.91 万”不可用于决策:66% 的收入没有成本,真实毛利会显著更低;
|
||||
- 商品级分析只能覆盖约 9.4% 的订单项;
|
||||
- 这不是代码必然结果,而是“初始化/回填未完成”的结果;但系统没有在页面上阻止用户把这种毛利当真实毛利(只给了 warning)。
|
||||
|
||||
---
|
||||
|
||||
## 七、权限、审计与内控
|
||||
|
||||
### 7.1 角色/能力矩阵(按代码)
|
||||
|
||||
| 能力 | admin | finance | operator | support |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 查看总账/试算平衡 | ✅ | ✅ | ✅(有 `finance.view`) | ❌ |
|
||||
| 手工凭证 / 关账 / 反关账 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 成本配置 / 成本版本 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 手工退款 / 撤销退款 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 快手平台退款 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 人工履约待办 / 确认 | ✅ | ❌(路由排除) | ✅ | ✅ |
|
||||
| 备用金查看 | ✅ 全部 | ✅ 全部 | ✅(主管范围) | ✅ 本人 |
|
||||
| 备用金发放 | ✅ | ❌ | ✅ | ❌ |
|
||||
| 备用金缴回 | ✅ | ❌ | ✅ | ✅(本人) |
|
||||
| 备用金支出 | ✅ | ❌ | ❌ | ✅(本人) |
|
||||
| 备用金对账 | ✅ | ✅ | ✅ | ❌ |
|
||||
| 备用金差异调整 | ✅ | ✅ | ❌ | ❌ |
|
||||
| 积分充值/调整 | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
### 7.2 内控问题
|
||||
|
||||
1. **职责分离不足**:`finance.config` 同时允许配置成本、做手工凭证、退款、关账、调积分;没有制单/审核分离。
|
||||
2. **关账无审批**:关账/反关账只要求 `finance.config`,没有二次确认/原因/审批。
|
||||
3. **审计可丢失**:`recordAdminAudit` 捕获异常后只 `logWarn`;财务操作可以成功而无审计。
|
||||
4. **审计非不可变**:`admin_audit_logs` 没有防篡改/哈希链;应用可删改。
|
||||
5. **代录范围宽**:`operator` 可以指定任意客服的备用金/积分账户(`resolveFundAccountId` / `resolveCreditOwnerAdminUserId` 只校验存在性),是否符合“主管只能管自己团队”的内控需要业务确认。
|
||||
6. **前端操作缺少确认**:关账/反关账是单按钮直接 mutation,没有二次确认;对账“标记解决”固定写“后台确认差异已处理”,没有让用户填写原因([AdminFinanceLedgerPage.tsx:216](../apps/frontend/src/pages/admin/AdminFinanceLedgerPage.tsx#L216))。
|
||||
|
||||
---
|
||||
|
||||
## 八、性能与运维
|
||||
|
||||
### 8.1 当前性能设计
|
||||
|
||||
- 财务同步任务 `cost-finance-sync` 默认每 5 分钟跑最近 30 天:
|
||||
- 履约方式兜底
|
||||
- 人工商品归一
|
||||
- 人工配置 rematch
|
||||
- 预估成本快照
|
||||
- 工单实际成本快照
|
||||
- 销售事实重算
|
||||
- 总账同步
|
||||
- `sales_daily_facts` 重建是“删 30 天 + 重插 30 天”,目前 5,381 行,成本可接受;但订单量增长后会变重。
|
||||
- 总账同步按天循环,每天至少 1 次 period 查询 + 1 个事务 + 0–3 条凭证;30 天约 30 个事务。
|
||||
- 迁移 108 增加了 `idx_orders_basis_date`、`idx_order_items_order_sku_cover` 等索引,方向正确。
|
||||
|
||||
### 8.2 风险
|
||||
|
||||
- 每 5 分钟全量重算 30 天,会与在线退款/人工确认争锁;`sales_daily_facts` 的 `DELETE` 会锁范围行。
|
||||
- 没有历史分区;`finance_journal_*`、`sales_daily_facts` 未来会持续增长。
|
||||
- 没有“同步失败告警/重试队列”:定时任务失败只会在日志里,下一次再试;退款后的即时同步失败不会自动补偿。
|
||||
- 数据保留/归档目前只覆盖 `cost_exceptions`,不覆盖 `finance_journal_*`、`sales_daily_facts`。
|
||||
|
||||
### 8.3 建议
|
||||
|
||||
- 销售事实改为按日增量重算(只重算有变化的日期),而不是固定 30 天全量;
|
||||
- 总账同步与事实重算放在同一任务内做“日期级”幂等,并记录 `last_success_at`;
|
||||
- 关键任务失败接入告警(至少 `logError` + 监控);
|
||||
- 对 `finance_journal_entries/lines`、`sales_daily_facts` 规划按月分区或归档。
|
||||
|
||||
---
|
||||
|
||||
## 九、测试与验证
|
||||
|
||||
### 9.1 本次实际执行
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `cd apps/backend && npx tsc -p tsconfig.json --noEmit` | 通过 |
|
||||
| `cd apps/backend && npm run lint:check` | 通过 |
|
||||
| `cd apps/backend && npm test` | 853 tests,849 pass,4 skipped,0 fail |
|
||||
| `cd apps/frontend && npm test` | 181 tests,181 pass,0 fail |
|
||||
| 财务定向单测(15 项) | 全部通过 |
|
||||
|
||||
> 4 个 skipped 中包含需要真实 PostgreSQL 的 `manual-fulfillment.integration.test.ts`;默认不设置 `MANUAL_FULFILLMENT_TEST_DATABASE_URL` 时会跳过。
|
||||
|
||||
### 9.2 已覆盖
|
||||
|
||||
- 人工履约:固定成本、可变成本、重复确认、并发确认、快照失败回滚、代录指定账户;
|
||||
- 总账:销售事实同步、借贷平衡、手工凭证、不平衡拒绝、关账阻断、对账匹配;
|
||||
- 备用金/积分:单事务扣款、余额不足、异常收口(集成测试中部分覆盖)。
|
||||
|
||||
### 9.3 未覆盖(建议补)
|
||||
|
||||
- 已关账期间发生退款/人工确认 → 事实与总账分叉;
|
||||
- 平台退款金额超过剩余可退 → 平台已调用、本地失败;
|
||||
- 全额退款撤销 → 收入/异常恢复;
|
||||
- 备用金对账创建后发生流水 → 确认差额;
|
||||
- 重复导入同一平台账单;
|
||||
- 权限矩阵(finance 被排除、operator 代录范围);
|
||||
- 审计写入失败时财务操作是否应回滚;
|
||||
- 手工凭证重复 `reference`;
|
||||
- 金额小数输入。
|
||||
|
||||
---
|
||||
|
||||
## 十、改进路线图
|
||||
|
||||
### P0:上生产前(建议 1–2 周)
|
||||
|
||||
| # | 动作 | 验收标准 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 关账期间事实冻结/调整机制 | 已关账期间的 `sales_daily_facts` 不再被重算;退款/成本变化走当前期间调整 |
|
||||
| 2 | 退款状态机 + 幂等 | `(source, external_ref)` 唯一;平台调用前预校验;失败可补偿 |
|
||||
| 3 | 全额退款撤销修复 | 撤销后 `pay_status` 恢复、事实恢复、异常重开 |
|
||||
| 4 | 关账门槛 + 锁 | 不满足门槛无法关账;并发写入被阻断;重复关账不覆盖审计 |
|
||||
| 5 | 备用金对账差额重算 | 确认时按当前余额重算,账实相等 |
|
||||
| 6 | 数据库硬约束 | 凭证借贷平衡触发器、已过账不可删改、`fund_accounts` 余额校验、`manual_expenses(order_item_id)` 唯一 |
|
||||
| 7 | 权限/审计 | 拆分 `finance.post/close/reconcile`;关键操作双人复核;审计与业务同事务 |
|
||||
|
||||
### P1:2–4 周
|
||||
|
||||
| # | 动作 | 验收标准 |
|
||||
| --- | --- | --- |
|
||||
| 8 | 对账按订单项/平台明细匹配 | 多品订单、多笔退款可正确匹配 |
|
||||
| 9 | 统一报表口径 | 旧收支明细、销售毛利、总账有明确主口径与日对账 |
|
||||
| 10 | 成本版本 append-only | 追溯改价也新增版本;`approved_by` 不再自批 |
|
||||
| 11 | 总账维度 | 凭证行写 provider/shop/product/order;可出订单/商品损益 |
|
||||
| 12 | 数据质量门禁 | 上线范围 `cost_missing=0`、人工单 100% 有快照/流水、未归一占比可解释 |
|
||||
| 13 | 幂等键 | 发放/缴回/拨付/积分充值/手工凭证全部支持 requestId |
|
||||
|
||||
### P2:后续
|
||||
|
||||
- 完整总账:钱包、押金、提现、充值、寄卖、售后、平台服务费;
|
||||
- 反冲/red reversal 工作流;
|
||||
- 利润表、资产负债表、现金流量简表;
|
||||
- 发票/税务/多币种;
|
||||
- 总账/事实表分区与归档;
|
||||
- 前端关账二次确认、对账差异原因必填。
|
||||
|
||||
---
|
||||
|
||||
## 十一、本次已实施改进(2026-10-27)
|
||||
|
||||
本轮按路线图先落 P0,并继续 P1 的 requestId 幂等、成本版本 append-only 与平台对账维度改造。
|
||||
|
||||
### 11.1 已完成
|
||||
|
||||
| # | 改进 | 关键实现 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 关账期间销售事实冻结 | `rebuildSalesDailyFacts` 跳过 `finance_periods.status='closed'` 的月份,返回 `skippedClosedPeriods`;退款/撤销/平台退款在已关账期间直接 409 | 本地库临时关闭 2026-09 后重算,9 月 `computed_at` 不变,10 月正常重算 |
|
||||
| 2 | 总账改为追加调整,不再删除重建 | `syncFinanceLedgerFromFacts` 按日算 delta,使用 `sales_*_adjustment` 凭证;按日 advisory lock 防并发重复 | 新代码中不再有 `DELETE FROM finance_journal_entries`;现有 10 月数据同步 delta=0 |
|
||||
| 3 | 凭证不可删改 + 借贷平衡硬约束 | 迁移 `104`:`trg_finance_journal_balanced`、`trg_finance_journal_immutable` | 手工插入不平衡凭证被数据库拒绝;删除有分录的已过账凭证被拒绝 |
|
||||
| 3.1 | 备用金余额勾稽硬约束 | 迁移 `104`:`trg_fund_entries_balance`、`trg_fund_accounts_balance`,按账户校验 `balance_fen = Σ fund_entries.amount_fen` | 余额 100 但无流水的账户被数据库拒绝;期初+流水一致时通过 |
|
||||
| 4 | 平台退款先校验、先落意图 | `finance_refund_intents` 状态机;`approveAndRecordKuaishouRefund` 在调用平台前校验可退金额;`order_refunds(source, external_ref)` 唯一 | 金额超可退时在调用平台前返回 409;集成测试通过 |
|
||||
| 5 | 全额退款撤销恢复收入 | `voidOrderRefund` 按剩余 confirmed 退款重算 `orders.pay_status`,并重新打开被自动关闭的成本异常 | 空库集成脚本:退款后 `refunded/0`,撤销后 `paid/5000` |
|
||||
| 6 | 关账门槛 + 期间锁 | `assertPeriodCloseable` 校验人工缺成本、人工无资金流水、人工金额不一致、未解决对账差异、试算平衡;关账与凭证共用 `finance_period` advisory lock;重复关账 409 | 集成测试先被“人工未确认成本”拦住,补齐后才允许关账 |
|
||||
| 7 | 备用金对账差额按当前余额重算 | `confirmAdminFundReconciliation` 在锁内用 `actual - current balance` 计算差额,并回写 `system_balance_fen/difference_fen` | 静态回归测试覆盖 |
|
||||
| 8 | 权限拆分 + finance 纳入人工履约 | 新增 `finance.post / finance.close / finance.reconcile`;人工履约路由与订单查看路由加入 `finance`;前端按新权限显示 | 迁移 `104` + 路由/前端类型检查通过 |
|
||||
| 9 | 凭证重复 reference 不再静默复用 | `postJournalEntry` 默认重复幂等键抛 409;金额必须是整数分 | 类型检查与全量测试通过 |
|
||||
| 10 | 关账/反关账二次确认 | 前端 `Popconfirm` 明确提示关账后不可写、会校验门槛 | 前端类型检查通过 |
|
||||
|
||||
### 11.2 本次新增/修改的主要文件
|
||||
|
||||
- 财务基础:[103_finance_foundation.sql](../apps/backend/src/db/migrations/103_finance_foundation.sql)(商品/成本/销售事实/退款)
|
||||
- 财务控制与 L1:[104_finance_controls.sql](../apps/backend/src/db/migrations/104_finance_controls.sql)(人工履约/总账/对账/控制/钱包硬化)
|
||||
- 拼单与寄卖:[105_work_order_consignment.sql](../apps/backend/src/db/migrations/105_work_order_consignment.sql)
|
||||
- 总账:[finance-ledger-service.ts](../apps/backend/src/services/finance/finance-ledger-service.ts)
|
||||
- 销售事实:[sales-facts-service.ts](../apps/backend/src/services/finance/sales-facts-service.ts)
|
||||
- 退款:[order-refund-service.ts](../apps/backend/src/services/finance/order-refund-service.ts)
|
||||
- 备用金/积分:[admin-fund-service.ts](../apps/backend/src/services/admin/admin-fund-service.ts)、[platform-credit-service.ts](../apps/backend/src/services/cost-center/platform-credit-service.ts)
|
||||
- 成本版本:[product-catalog-service.ts](../apps/backend/src/services/cost-center/product-catalog-service.ts)、[affiliate-product-service.ts](../apps/backend/src/services/cost-center/affiliate-product-service.ts)
|
||||
- 平台对账:[finance-reconciliation-service.ts](../apps/backend/src/services/finance/finance-reconciliation-service.ts)
|
||||
- 权限:[finance.ts](../apps/backend/src/routes/admin/finance.ts)、[finance-ledger.ts](../apps/backend/src/routes/admin/finance-ledger.ts)、[orders.ts](../apps/backend/src/routes/admin/orders.ts)、[kuaishou-industry.ts](../apps/backend/src/routes/admin/kuaishou-industry.ts)
|
||||
- 前端:[App.tsx](../apps/frontend/src/App.tsx)、[AdminFinanceLedgerPage.tsx](../apps/frontend/src/pages/admin/AdminFinanceLedgerPage.tsx)、[AdminOrderDetailPage.tsx](../apps/frontend/src/pages/admin/AdminOrderDetailPage.tsx)、[AdminSalesProfitPage.tsx](../apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx)
|
||||
- 回归测试:[finance-controls.test.ts](../apps/backend/src/services/finance/finance-controls.test.ts)
|
||||
|
||||
### 11.3 验证结果
|
||||
|
||||
| 检查 | 结果 |
|
||||
| --- | --- |
|
||||
| 后端类型检查 | 通过 |
|
||||
| 后端 Lint / Prettier | 通过 |
|
||||
| 后端全量测试 | 866 项,862 pass,4 skipped |
|
||||
| 前端类型检查 / 测试 | 通过;181 pass |
|
||||
| 空库全量迁移 `001 → 105` | 通过 |
|
||||
| 人工履约集成测试(空库) | 通过 |
|
||||
| 数据库触发器手测 | 不平衡凭证、删除已过账凭证、余额与流水不一致均被拒绝 |
|
||||
| 销售事实关账冻结手测 | 9 月 `computed_at` 不变,10 月正常重算 |
|
||||
| P1 资金 requestId 手测 | 同 requestId 第二次返回 409,余额只增加一次 |
|
||||
| P1 积分 requestId 手测 | 同 requestId 第二次返回 409,积分只增加一次 |
|
||||
| P1 成本版本 append-only 手测 | 旧版本 retired 且金额不变,新版本 active 从追溯日生效 |
|
||||
| P1 对账订单项匹配手测 | 一单两品两行全部 matched,`order_item_id` 正确落库 |
|
||||
|
||||
### 11.3.1 L1 打手钱包硬化(迁移 104)
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| 金额类型 | `orders.total_amount`、`worker_wallets.*`、`worker_wallet_ledgers.amount/balance_after/frozen_after`、`worker_finance_requests.amount/frozen_amount`、工单/份额/售后/寄卖金额全部 int4 → **BIGINT** |
|
||||
| 非负约束 | `worker_wallets` 六个余额、流水后余额、提现申请、订单/工单/份额/售后/寄卖金额加 CHECK |
|
||||
| 奖励幂等 | `worker_wallet_ledgers.event_key` + 触发器 + 唯一索引:工单+份额+流水类型,防未来重复结算 |
|
||||
| 历史重复 | 新增只读视图 `v_worker_wallet_duplicate_rewards`;排除寄卖货款后为 **8 组**疑似重复 `reward_settlement`(16 条流水,合计 ¥392.40),多计约 **¥196.20**,不自动扣回,需财务确认 |
|
||||
| 审计脚本 | [audit-worker-wallet.sql](../apps/backend/scripts/audit-worker-wallet.sql) 只读检查负余额、负后余额、历史重复、event_key 缺失 |
|
||||
|
||||
### 11.4 P1 本轮已完成
|
||||
|
||||
| # | 改进 | 关键实现 | 验证 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 资金/积分 requestId 幂等 | 迁移 `104` 给 `fund_entries` / `platform_credit_entries` 加 `request_id` + 账户级唯一索引;重复请求 409,事务回滚 | 同 requestId 发放/充值第二次 409,余额只变一次 |
|
||||
| 2 | 成本版本追溯改价 append-only | 追溯改价不再 UPDATE 金额,而是退休旧版本 + 新增从追溯日生效的版本;旧版本行保留 | 旧版本 `retired` 且金额不变,新版本 `active` 从追溯日生效 |
|
||||
| 2.1 | 人工确认关账保护 | 已关账期间的订单项确认直接 409,避免“扣了钱但销售事实/总账没记” | 代码路径 + 回归测试覆盖 |
|
||||
| 3 | 平台对账订单项/明细维度 | 迁移 `104` 给 `finance_reconciliation_items` 加 `platform_item_id/order_item_id`;账单行可用 `orderItemId` 按订单项收入匹配;退款无外部单号时至少要求金额一致 | 一单两品两行全部 matched,`order_item_id` 正确落库 |
|
||||
| 4 | 对账重复行判定 | 销售/退款重复键加入平台明细号/订单项/金额,避免一单多品被误判 duplicate | 两行同订单不同订单项不再互判重复 |
|
||||
|
||||
### 11.5 仍未完成(按路线图继续)
|
||||
|
||||
1. **已关账期间的“当前期间调整”**:目前是“冻结 + 禁止直接退款/撤销”,还没有自动生成“以前期间调整”事实/凭证;跨期退款需先反关账。
|
||||
2. **成本版本同时间/上游基线导入的更正**:相同生效时间没有可插入的时间区间,仍会有一次就地 UPDATE;上游价格基线导入的部分路径也仍可能就地更正。需要在 schema 上支持同时间多版本或引入版本变更审计表。
|
||||
3. **总账维度与完整覆盖**:仍只覆盖销售/成本,钱包/押金/提现/寄卖/平台费未入账;凭证行还没有 provider/shop/product 维度。
|
||||
4. **审计与业务同事务**:审计失败仍只记录告警,未做到“审计失败则财务操作回滚”。
|
||||
5. **maker-checker**:手工凭证、关账、退款、积分调整仍未强制双人复核。
|
||||
6. **前端 requestId 复用**:后端已支持 requestId,前端尚未在“同一用户意图重试”时复用同一个 key。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:关键文件索引
|
||||
|
||||
| 文件 | 作用 |
|
||||
| --- | --- |
|
||||
| [finance-ledger-service.ts](../apps/backend/src/services/finance/finance-ledger-service.ts) | 凭证、试算平衡、关账、销售事实同步 |
|
||||
| [sales-facts-service.ts](../apps/backend/src/services/finance/sales-facts-service.ts) | 销售事实重算、毛利总览 |
|
||||
| [order-refund-service.ts](../apps/backend/src/services/finance/order-refund-service.ts) | 手工/平台退款、撤销 |
|
||||
| [manual-fulfillment-service.ts](../apps/backend/src/services/finance/manual-fulfillment-service.ts) | 人工履约确认、扣备用金/积分、写快照 |
|
||||
| [finance-reconciliation-service.ts](../apps/backend/src/services/finance/finance-reconciliation-service.ts) | 平台账单导入、匹配、解决 |
|
||||
| [admin-fund-service.ts](../apps/backend/src/services/admin/admin-fund-service.ts) | 备用金账户、流水、支出、对账 |
|
||||
| [admin-finance-service.ts](../apps/backend/src/services/admin/admin-finance-service.ts) | 旧版收支明细 |
|
||||
| [cost-snapshot-sync-service.ts](../apps/backend/src/services/cost-center/cost-snapshot-sync-service.ts) | 预估/实际成本快照同步 |
|
||||
| [product-catalog-service.ts](../apps/backend/src/services/cost-center/product-catalog-service.ts) | 商品、成本版本、追溯改价 |
|
||||
| [platform-credit-service.ts](../apps/backend/src/services/cost-center/platform-credit-service.ts) | 积分钱包 |
|
||||
| [100_fund_accounts.sql](../apps/backend/src/db/migrations/100_fund_accounts.sql) | 备用金表(历史基线) |
|
||||
| [103_finance_foundation.sql](../apps/backend/src/db/migrations/103_finance_foundation.sql) | 商品、成本中心、销售事实、退款 |
|
||||
| [104_finance_controls.sql](../apps/backend/src/db/migrations/104_finance_controls.sql) | 人工履约、总账/对账、财务控制、L1 钱包 |
|
||||
| [105_work_order_consignment.sql](../apps/backend/src/db/migrations/105_work_order_consignment.sql) | 拼单、索引、寄卖对接客服 |
|
||||
|
||||
## 附录 B:上生产前建议执行的复核 SQL
|
||||
|
||||
```sql
|
||||
-- 1. 凭证是否借贷平衡(应为 0 行)
|
||||
SELECT entry_id
|
||||
FROM finance_journal_lines
|
||||
GROUP BY entry_id
|
||||
HAVING SUM(debit_fen) <> SUM(credit_fen);
|
||||
|
||||
-- 2. 备用金余额是否等于流水合计(应为 0 行)
|
||||
SELECT a.id, a.account_no, a.balance_fen, COALESCE(SUM(e.amount_fen), 0) AS entry_sum
|
||||
FROM fund_accounts a
|
||||
LEFT JOIN fund_entries e ON e.account_id = a.id
|
||||
GROUP BY a.id, a.account_no, a.balance_fen
|
||||
HAVING a.balance_fen <> COALESCE(SUM(e.amount_fen), 0);
|
||||
|
||||
-- 3. 人工现金成本与备用金支出是否一致(应为 0 行)
|
||||
SELECT cs.id, me.expense_no, me.amount_fen, cs.total_amount_fen
|
||||
FROM cost_snapshots cs
|
||||
JOIN manual_expenses me ON me.order_item_id = cs.order_item_id
|
||||
WHERE cs.subject_type = 'manual_case'
|
||||
AND cs.superseded_at IS NULL
|
||||
AND cs.total_amount_fen <> me.amount_fen;
|
||||
|
||||
-- 4. 关账后是否还在改销售事实(应为 0 行)
|
||||
SELECT f.stat_date, f.computed_at, p.closed_at
|
||||
FROM sales_daily_facts f
|
||||
JOIN finance_periods p ON p.period_key = to_char(f.stat_date, 'YYYY-MM')
|
||||
WHERE p.status = 'closed'
|
||||
AND f.computed_at > p.closed_at;
|
||||
|
||||
-- 5. 平台退款是否重复(应为 0 行)
|
||||
SELECT source, external_ref, COUNT(*)
|
||||
FROM order_refunds
|
||||
WHERE external_ref <> ''
|
||||
GROUP BY source, external_ref
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 6. 同一订单项是否有多条人工支出(应为 0 行)
|
||||
SELECT order_item_id, COUNT(*)
|
||||
FROM manual_expenses
|
||||
WHERE order_item_id IS NOT NULL
|
||||
GROUP BY order_item_id
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 7. 成本缺失与未归一规模(关账门禁输入)
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE cost_missing_count > 0) AS rows_with_missing_cost,
|
||||
SUM(cost_missing_count) AS missing_items,
|
||||
SUM(gross_revenue_fen) FILTER (WHERE cost_stage = 'missing') AS missing_revenue_fen
|
||||
FROM sales_daily_facts;
|
||||
|
||||
-- 8. 总账 vs 销售事实按日收入差异(应为 0 行)
|
||||
WITH ledger AS (
|
||||
SELECT e.period_key, e.ref_id::date AS stat_date,
|
||||
SUM(l.credit_fen) FILTER (WHERE l.account_code = '4001') AS ledger_revenue_fen
|
||||
FROM finance_journal_entries e
|
||||
JOIN finance_journal_lines l ON l.entry_id = e.id
|
||||
WHERE e.event_type = 'sales_revenue'
|
||||
GROUP BY e.period_key, e.ref_id
|
||||
)
|
||||
SELECT f.stat_date, f.gross_revenue_fen, l.ledger_revenue_fen
|
||||
FROM sales_daily_facts f
|
||||
FULL JOIN ledger l ON l.stat_date = f.stat_date
|
||||
WHERE COALESCE(f.gross_revenue_fen, 0) <> COALESCE(l.ledger_revenue_fen, 0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 C:术语
|
||||
|
||||
| 术语 | 含义 |
|
||||
| --- | --- |
|
||||
| 履约方式 `fulfillment_mode` | 谁执行:`work_order` / `affiliate_dash` / `legacy_platform` / `manual` |
|
||||
| 成本模型 `cost_model` | 成本怎么算:采购 / 打手报酬 / 人工实际 / 平台费 / 支付费 / 售后损失 / 其他 |
|
||||
| 销售事实 `sales_daily_facts` | 按日、来源、店铺、商品、履约方式聚合的收入/退款/成本/毛利 |
|
||||
| 成本快照 `cost_snapshots` | 某个订单项/工单/人工单实际使用的成本版本与金额,历史不回写 |
|
||||
| 影子总账 | 并行记录复式分录、但不接管钱包余额的账 |
|
||||
| 备用金 | 公司拨给主管、再由主管发给客服的周转现金 |
|
||||
| 积分钱包 | 南航/乐玩/ROCK 三个平台的客服积分账户,互不通用 |
|
||||
| 拼单 | 同一工单由多个打手按份额参与,按份额结算 |
|
||||
@@ -873,4 +873,3 @@ flowchart LR
|
||||
## 九、需要你确认的问题
|
||||
|
||||
见对话中的提问(不在文档内固化,确认后我再补「口径确认」章节)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user