- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度 - 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment - 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单 - 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径 - 文档:同步迁移基线、合并评估、代码复核与上线说明
57 KiB
order_site 财务系统深度分析(2026-10-27)
分析对象:订单履约、成本中心、客服备用金/积分、销售事实、影子总账、期间关账与平台对账。 代码基线:
dev,HEAD0de0fd7b(迁移收敛到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 件事
- 关账期间销售事实仍可被重算,但总账会跳过已关账期间 → 报表与总账必然分叉。
- 平台退款先调用平台、后写本地,且本地没有
external_ref唯一约束 → 平台已退款、本地没记账,或重复记账。 - 全额退款撤销后
orders.pay_status不恢复 → 撤销了退款,收入仍然是 0。 - 关账不校验门槛、不加锁、可重复关账 → 带着缺失成本/未对账数据关账;并发写入可穿透。
- 备用金对账使用创建时的旧差额 → 对账确认后余额可能仍然不等于实际盘点。
- 总账“已过账不可改、冲正用反向分录”没有实现 → 按日
DELETE + 重建,审计链断裂。 - 权限矩阵与文档不一致:
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:后台 APIapps/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 的关键顺序:
- 先按 SKU 兜底匹配
manual_fulfillment_configs; SELECT ... FOR UPDATE OF oi锁订单项;- 复查是否已有有效
manual_case快照; - 解析成本项/成本版本;
- 现金路径:
writeFundExpense扣备用金并写manual_expenses;积分路径:consumePlatformCreditWithClient扣积分并写流水; - 写
cost_snapshots(actual,subject_id = order_item_id); - 收口
manual_cost_missing异常; - 全部在同一个
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- 定时任务每 5 分钟对最近 30 天重算:cost-finance-sync-job.ts:47–55
- 退款提交后立即重算原销售日:order-refund-service.ts:301–305
- 总账同步遇到已关账期间直接
skipped:finance-ledger-service.ts:197–205
触发路径:
- 9 月关账;
- 10 月发生一笔 9 月订单的退款;
createManualRefund重算 9 月某天的sales_daily_facts;- 总账同步看到 9 月已关账,跳过;
- 9 月销售毛利报表已经变化,9 月总账仍是关账时点数字。
影响:
- 报表与总账永久分叉;
- 关账失去意义;
- 无法向审计解释“关账后收入为什么变了”。
修复建议:
- 方案 A(推荐):已关账期间的退款/成本变化不直接改原期间事实,改为在当前开放期间生成“以前年度/以前期间调整”事实或总账调整凭证;
- 方案 B:关账时把
sales_daily_facts快照化(按期间冻结),后续变更只写调整表; - 无论哪种,
rebuildSalesDailyFacts必须检查finance_periods.status,不能静默重写已关账期间。
5.2 P0:平台退款先调用平台,后落本地,且没有幂等键
证据:
- 平台调用在第 522 行,本地
createManualRefund在第 529 行:order-refund-service.ts:522–541 - 可退金额校验在
createManualRefund内部(第 188 行),也就是平台调用之后:order-refund-service.ts:181–193 order_refunds只有refund_no唯一,没有(source, external_ref)唯一约束: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 - 撤销只更新
order_refunds.status='voided',没有恢复orders.pay_status:order-refund-service.ts:354–366 - 销售事实在没有 confirmed 退款时,仍会把
pay_status='refunded'当作全额退款:sales-facts-service.ts:70–79
影响:
- 撤销全额退款后,订单仍是
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–448assertPeriodOpen是普通SELECT,没有FOR UPDATE或 advisory lock:finance-ledger-service.ts:537–548- 文档明确要求“人工单无成本/未扣备用金/金额不一致不允许关账”:财务与履约口径总纲:126–130
- 文档说“关账前会先补齐该期间凭证,并校验试算平衡”:财务上线初始化与验收:283
影响:
- 可以带着大量
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 - 确认时读的是存储的
difference_fen,加到当前余额:admin-fund-service.ts:2087、admin-fund-service.ts:2115–2126
触发路径:
- 创建对账:系统余额 100,实际 90,差额 -10;
- 确认前发生一笔 20 元支出,余额变成 80;
- 确认时用 -10 调整当前余额 80 → 70;
- 实际盘点 90,账实仍然差 20。
影响:
- 对账确认后余额仍不等于实际;
- 差异被“调整”进了 adjustment 流水,但金额不对,后续更难查;
- 如果创建与确认间隔很久,问题更严重。
修复建议:
- 确认时锁账户并重新计算
actualBalanceFen - currentSystemBalanceFen; - 或者创建对账时就锁账户/冻结期间,禁止期间内再发生流水;
difference_fen应作为“确认时点的差额”,而不是创建时的快照。
5.6 P1:权限矩阵与文档/实现不一致,且缺少职责分离
证据:
- 人工履约路由只允许
admin / operator / support,不包含 finance:finance.ts:107–126 - 但服务层
PRIVILEGED_ROLES明确包含 finance,说明代码作者预期 finance 可以代录:manual-fulfillment-service.ts:493–500 - 文档说“管理员/财务可代录到指定客服账户”:人工支出与备用金.md:32
- 同一篇文档又说“管理员/财务/运营可代录”:人工支出与备用金.md:104
finance.config被用于成本配置、手工凭证、退款、关账、积分调整等所有写操作;finance.review、finance.export基本未使用:072_admin_finance_role.sql:11–14、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 - 确认时读取了
cfg.cost_mode,但resolveManualFulfillmentCost只接收creditQuantity/unitPriceFen/providedCostFen,没有接收costMode:manual-fulfillment-service.ts:285–298、manual-fulfillment-service.ts:670–697
影响:
- 保存逻辑目前按
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
影响:
- 客服自助确认时,请求里没有
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:平台对账的匹配粒度与批次幂等不足
证据:
- 销售行用
orders.total_amount作为 expected,而不是订单项/账单行:finance-reconciliation-service.ts:292–298 - 退款行没有
external_ref时取该订单最新一条 confirmed 退款:finance-reconciliation-service.ts:301–318 finance_reconciliations没有(provider, period_from, period_to, source)唯一约束: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 - 自动发货价格同样有就地更新:affiliate-product-service.ts:348–362
- 文档明确“改价只新增版本;不覆盖历史”:成本中心与履约模型.md:232、:265
approved_by_admin_user_id被写成当前操作人,等于自批:product-catalog-service.ts:1404
影响:
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 内控问题
- 职责分离不足:
finance.config同时允许配置成本、做手工凭证、退款、关账、调积分;没有制单/审核分离。 - 关账无审批:关账/反关账只要求
finance.config,没有二次确认/原因/审批。 - 审计可丢失:
recordAdminAudit捕获异常后只logWarn;财务操作可以成功而无审计。 - 审计非不可变:
admin_audit_logs没有防篡改/哈希链;应用可删改。 - 代录范围宽:
operator可以指定任意客服的备用金/积分账户(resolveFundAccountId/resolveCreditOwnerAdminUserId只校验存在性),是否符合“主管只能管自己团队”的内控需要业务确认。 - 前端操作缺少确认:关账/反关账是单按钮直接 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 本次新增/修改的主要文件
- 财务基础:103_finance_foundation.sql(商品/成本/销售事实/退款)
- 财务控制与 L1:104_finance_controls.sql(人工履约/总账/对账/控制/钱包硬化)
- 拼单与寄卖:105_work_order_consignment.sql
- 总账:finance-ledger-service.ts
- 销售事实:sales-facts-service.ts
- 退款:order-refund-service.ts
- 备用金/积分:admin-fund-service.ts、platform-credit-service.ts
- 成本版本:product-catalog-service.ts、affiliate-product-service.ts
- 平台对账:finance-reconciliation-service.ts
- 权限:finance.ts、finance-ledger.ts、orders.ts、kuaishou-industry.ts
- 前端:App.tsx、AdminFinanceLedgerPage.tsx、AdminOrderDetailPage.tsx、AdminSalesProfitPage.tsx
- 回归测试: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 只读检查负余额、负后余额、历史重复、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 仍未完成(按路线图继续)
- 已关账期间的“当前期间调整”:目前是“冻结 + 禁止直接退款/撤销”,还没有自动生成“以前期间调整”事实/凭证;跨期退款需先反关账。
- 成本版本同时间/上游基线导入的更正:相同生效时间没有可插入的时间区间,仍会有一次就地 UPDATE;上游价格基线导入的部分路径也仍可能就地更正。需要在 schema 上支持同时间多版本或引入版本变更审计表。
- 总账维度与完整覆盖:仍只覆盖销售/成本,钱包/押金/提现/寄卖/平台费未入账;凭证行还没有 provider/shop/product 维度。
- 审计与业务同事务:审计失败仍只记录告警,未做到“审计失败则财务操作回滚”。
- maker-checker:手工凭证、关账、退款、积分调整仍未强制双人复核。
- 前端 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 三个平台的客服积分账户,互不通用 |
| 拼单 | 同一工单由多个打手按份额参与,按份额结算 |