Files
order_site/docs/财务系统深度分析-2026-10-27.md
yml2213 9d5af359fa feat(finance): 完成财务控制、L1 钱包硬化与迁移收敛
- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度
- 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment
- 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单
- 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径
- 文档:同步迁移基线、合并评估、代码复核与上线说明
2026-10-27 16:08:16 +08:00

57 KiB
Raw Permalink Blame History

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 资金/账务全景

买家支付
  └─ 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:关账期间销售事实仍可重算,总账却冻结

证据:

触发路径:

  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:平台退款先调用平台,后落本地,且没有幂等键

证据:

触发路径:

  • 平台退款金额大于本地剩余可退金额 → 平台已同意退款,本地 createManualRefund 抛 409,账上没记;
  • 平台调用成功、本地插入成功,但接口超时/响应丢失 → 重试可能再插一条;
  • 平台退款单号重复提交 → 没有数据库层拦截。

影响:

  • 平台已退款、系统未退款:应收/收入虚高;
  • 重复退款:收入被多冲减;
  • 对账时平台有退款、系统无记录或金额不一致。

修复建议:

  • 引入退款状态机:requested → platform_approved → recorded → reconciled(或 outbox);
  • 在调用平台前先落“退款意图”,并做剩余可退金额预校验;
  • 对 (source, external_ref) 建唯一索引(external_ref <> '' 的部分唯一索引);
  • 平台调用使用平台退款单号做幂等键;本地失败要有补偿/重试任务,不能只抛错。

5.3 P0:全额退款撤销后,收入仍然是 0

证据:

影响:

  • 撤销全额退款后,订单仍是 refunded,销售事实仍然按全额退款冲减;
  • 成本异常在退款时被置为 ignored,撤销时没有重新打开;
  • 订单领取/核销等下游逻辑也可能继续把订单当作已退款。

修复建议:

  • voidOrderRefund 在同一事务内根据“剩余 confirmed 退款”重算 orders.pay_status(0 → paid,否则保持 refunded);
  • 撤销后重新打开被自动关闭的 cost_exceptions(status='open',清空 resolved_at);
  • 补一条集成测试:全额退款 → 撤销 → 销售事实恢复、总账恢复、异常恢复。

5.4 P0:关账没有门槛、没有锁,且可以重复关账

证据:

影响:

  • 可以带着大量 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:备用金对账使用“创建时的旧差额”调整“当前余额”

证据:

触发路径:

  1. 创建对账:系统余额 100,实际 90,差额 -10;
  2. 确认前发生一笔 20 元支出,余额变成 80;
  3. 确认时用 -10 调整当前余额 80 → 70;
  4. 实际盘点 90,账实仍然差 20。

影响:

  • 对账确认后余额仍不等于实际;
  • 差异被“调整”进了 adjustment 流水,但金额不对,后续更难查;
  • 如果创建与确认间隔很久,问题更严重。

修复建议:

  • 确认时锁账户并重新计算 actualBalanceFen - currentSystemBalanceFen;
  • 或者创建对账时就锁账户/冻结期间,禁止期间内再发生流水;
  • difference_fen 应作为“确认时点的差额”,而不是创建时的快照。

5.6 P1:权限矩阵与文档/实现不一致,且缺少职责分离

证据:

影响:

  • 文档承诺的“财务代录”实际不可用;
  • operator 可以跨客服代录备用金/积分(resolveFundAccountId / resolveCreditOwnerAdminUserId 只校验目标账户存在,不校验团队归属),是否符合内控需要明确;
  • 同一个 finance 用户可以改成本、做手工凭证、退款、关账、调积分,没有 maker-checker;
  • 无法把“制单/审核/关账”拆给不同人。

修复建议:

  • 明确能力矩阵,拆出 finance.post / finance.close / finance.reconcile / finance.cost_config;
  • 财务角色要么允许人工履约,要么在文档中删掉“财务代录”;
  • 关账、手工凭证、退款、积分调整至少两人复核(制单人与审核人不能同一人);
  • 代录接口增加目标客服的责任主管/团队范围校验(如果业务要求团队隔离)。

5.7 P1:cost_mode 字段在确认时没有生效

证据:

影响:

  • 保存逻辑目前按 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 不准确

证据:

影响:

  • 客服自助确认时,请求里没有 creditOwnerAdminUserId,metadata 为 null,但积分实际从该客服账户扣除;
  • 审计/对账按 metadata 找人会对不上,只能再查 platform_credit_entries。

修复建议:

  • 把解析后的 creditOwnerAdminUserId 传进快照写入;
  • 同时记录积分账户 ID/流水号,保证能从快照直接追溯到资金流水。

5.9 P1:幂等与审计缺口

问题 证据 影响
手工凭证重复 reference 静默复用旧凭证 ON CONFLICT DO NOTHING 后返回 existing:finance-ledger-service.ts:80–110 用户以为新建成功,实际未入账;审计 targetId 指向旧凭证
凭证金额小数被 Math.trunc 截断 finance-ledger-service.ts:503–504 1.9 分变 1 分,金额被静默修改
备用金拨付/发放/缴回无幂等键 admin-fund-service.ts:1248、1305、1431 双击/重试可能重复转账
积分充值/调整无幂等键 platform-credit-service.ts:165、219 重复充值
审计日志写入失败被吞 session.ts:156–165 财务操作成功但没有审计记录

修复建议:

  • 所有资金写命令接收 requestId/idempotencyKey,在 DB 建唯一键;
  • 审计写入与业务同事务,或写 outbox 后异步落库;财务操作不允许“审计失败但业务成功”;
  • 金额入参必须是整数分,拒绝小数;reference 重复应返回 409,而不是静默复用。

5.10 P1:平台对账的匹配粒度与批次幂等不足

证据:

影响:

  • 一单多品、平台按明细行出账时,销售行金额对不上;
  • 同一订单多笔退款且账单不带退款单号时,多行可能匹配到同一条退款,误报 matched;
  • 同一账单可以重复导入,形成多个批次,差异统计重复;
  • expected_fen 对 missing 行记 0,汇总差额不是真正的“全部行差额”。

修复建议:

  • 账单行增加订单项/平台明细号维度,按明细匹配;
  • 退款匹配必须要求平台退款单号,或至少要求金额+时间+订单项;
  • 对导入文件做 hash/批次唯一约束,重复导入拒绝或幂等;
  • missing 行也纳入 expected/actual 的差异计算,并区分“系统缺失”和“金额不符”。

5.11 P1:成本版本“追溯改价”就地 UPDATE,破坏版本历史

证据:

影响:

  • 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
  • 所有成本都进 5001,没有区分人工 5101:finance-ledger-service.ts:244–248
  • 科目表里的 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、:78

影响:

  • 上海时间每月 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)。

八、性能与运维

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 本次新增/修改的主要文件

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 只读检查负余额、负后余额、历史重复、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 凭证、试算平衡、关账、销售事实同步
sales-facts-service.ts 销售事实重算、毛利总览
order-refund-service.ts 手工/平台退款、撤销
manual-fulfillment-service.ts 人工履约确认、扣备用金/积分、写快照
finance-reconciliation-service.ts 平台账单导入、匹配、解决
admin-fund-service.ts 备用金账户、流水、支出、对账
admin-finance-service.ts 旧版收支明细
cost-snapshot-sync-service.ts 预估/实际成本快照同步
product-catalog-service.ts 商品、成本版本、追溯改价
platform-credit-service.ts 积分钱包
100_fund_accounts.sql 备用金表(历史基线)
103_finance_foundation.sql 商品、成本中心、销售事实、退款
104_finance_controls.sql 人工履约、总账/对账、财务控制、L1 钱包
105_work_order_consignment.sql 拼单、索引、寄卖对接客服

附录 B:上生产前建议执行的复核 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 三个平台的客服积分账户,互不通用
拼单 同一工单由多个打手按份额参与,按份额结算