# 影子总账到底是什么意思
> 状态:**现行说明(未实施)**
> 最后校准:2026-10-23
> 代码基线:`dev @ 43f402cf + 迁移 `103`–`107`(已合并到 `dev`,**未推送、未部署生产**)
> 配套方案:[财务系统集成方案.md](财务系统集成方案.md)
> **总口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。**
> 前置:统一履约事实、商品主数据、渠道映射、成本中心与销售事实;实施顺序以总纲第六节为准。
> 一句话:**影子总账是一套“跟着真实业务跑、但暂时不接管余额”的复式会计账。** 它先把每一笔业务翻译成借贷分录,和现有钱包/订单/提现数据逐日对账;确认长期对得平之后,再让它成为正式总账。
>
> 边界:影子总账解决“钱怎么记、怎么对”,不负责“商品是什么、成本是多少、人工谁处理”。商品与成本见 [成本中心与履约模型.md](成本中心与履约模型.md),人工支出见 [人工支出与备用金.md](人工支出与备用金.md)。
> 当前进度:客服备用金、商品与成本中心、成本快照、销售事实、人工录单已实施;`finance_*` 表、分录、对账、转正均未实施。人工成本现在只有一条链路:人工录单 → 扣客服备用金 → 写成本快照,无登记单/审批/报销;历史人工支出找不回来、直接放弃,上线后必须全覆盖。
---
## 一、为什么叫“影子”
“影子”不是指数据是假的,而是指它的**地位**:
- 现有 `worker_wallets` 仍然是运行时余额的权威来源;
- 现有 `worker_wallet_ledgers` 仍然继续记录打手资金动作;
- 影子总账在后台**并行**记录同一批业务事件,但暂时不决定用户能提现多少、能接多少单;
- 它像影子一样跟着真实系统走,用来验证“如果我们用会计方式记账,能不能和现有系统对得上”。
```text
真实业务
├─ 现有链路:改订单 / 改工单 / 改钱包 / 改提现单 ← 仍然权威
└─ 影子链路:业务事件 → 复式分录 → 总账余额 ← 只观察、不接管
```
当影子账连续对平、业务口径确认后,才进入“转正”阶段:总账成为权威,钱包变成总账的余额投影。
---
## 二、它和现在的钱包流水有什么区别
| 维度 | 现在的 `worker_wallet_ledgers` | 影子总账 |
| --- | --- | --- |
| 记账方式 | 单边流水:`+50` / `-20` | 复式分录:每笔有借方和贷方,必须平衡 |
| 覆盖范围 | 主要是打手资金 | 订单收入、成本、打手报酬、押金、提现、寄卖货款、平台费 |
| 余额 | `worker_wallets` 直接维护可变余额 | 由分录汇总/投影得出,可独立重算 |
| 目的 | 让打手看到余额与流水 | 让财务能出报表、对账、关账、算毛利 |
| 可修改性 | 余额可更新,流水是业务记录 | 已过账分录不可改,只能反向冲正 |
| 维度 | 主要按 worker / work_order | 同时按 order / work_order / worker / shop / provider / settlement |
| 失败影响 | 直接影响打手余额 | 影子期不影响业务;转正后与业务同事务 |
### 最关键的区别:单边 vs 复式
假设打手被冻结 20 元押金:
- 现在只记一条:`frozen_deposit +20`,`available -20`;
- 复式账要记两条:
- 借:打手可提现余额 20
- 贷:打手押金负债 20
这样财务才能回答:这 20 元不是“消失了”,而是从“可提现负债”重分类成了“押金负债”。押金退回到哪、没收进哪个收入科目,也都有依据。
---
## 三、一个具体例子
假设一笔订单:
- 买家支付 100 元;
- 工单验收,打手报酬 50 元;
- 打手抢单时冻结押金 20 元;
- 后来提现 10 元。
### 1. 买家支付 100 元
```text
借:1002 支付通道待结算 100
贷:2201 预收账款 100
```
此时钱包不变。这笔钱还不是收入,而是预收/待结算。
### 2. 工单验收,确认收入与履约成本
```text
借:2201 预收账款 100
贷:4001 商品销售收入 100
借:5101 履约成本-打手报酬 50
贷:2001 应付打手报酬 50
```
此时钱包仍不一定变,因为报酬可能还在待解冻期。
### 3. 报酬转为可提现
```text
借:2001 应付打手报酬 50
贷:2004 打手可提现余额 50
```
此时钱包 `available_amount +50`。
### 4. 抢单冻结押金 20 元
```text
借:2004 打手可提现余额 20
贷:2003 打手押金 20
```
钱包:`available -20`,`frozen_deposit +20`。这只是负债内部重分类,不是费用。
### 5. 提现 10 元
```text
借:2004 打手可提现余额 10
贷:1001 银行存款 10
```
钱包:`available -10`。
### 6. 最终核对
```text
钱包 available = 50 - 20 - 10 = 20
钱包 frozen_deposit = 20
```
总账中对应的负债也应分别是 20 和 20;如果对不上,就说明有事件漏记、重复记或金额映射错了。
---
## 四、影子总账在本项目怎么跑
```mermaid
flowchart LR
A["业务事务
订单/工单/提现/寄卖/售后"] --> B["finance_business_events
幂等事件"]
B --> C["finance-posting-service
借贷规则"]
C --> D["finance_journal_entries"]
D --> E["finance_journal_lines"]
E --> F["科目余额 / 钱包投影"]
G["现有 worker_wallets / ledgers"] --> H["每日对账"]
F --> H
I["平台账单 / 支付流水"] --> H
H --> J["差异清单
finance_reconciliation_items"]
```
### 1. 业务事件先落“收件箱”
每个业务动作生成唯一事件:
```text
order:123:paid
work_order:456:settled
work_order:456:deposit_freeze
withdrawal:789:approved
consignment:abc:payout
```
写入 `finance_business_events`,`business_event_id` 唯一。重复投递直接忽略,保证幂等。
### 2. 按规则生成借贷分录
`finance-posting-service` 只做一件事:把事件翻译成分录。
```text
事件:work_order:456:settled
分录:
借 5101 履约成本 50
贷 2001 应付打手报酬 50
维度:orderId / workOrderId / workerId / shopId / provider
```
规则必须集中管理,不能散落在各个业务 service 里。
### 3. 写凭证与分录
- `finance_journal_entries`:凭证头(事件、时间、期间、状态)。
- `finance_journal_lines`:借贷行(科目、金额、维度)。
- 已过账凭证不可 UPDATE / DELETE;冲正用反向凭证。
### 4. 生成余额投影
有两种方式:
- **实时投影**:分录写入后同步更新 `finance_account_balances` / `worker_wallets`;
- **定时重算**:按期间从分录汇总,生成余额快照。
影子期建议两者都做:实时投影用于展示,定时重算用于校验。
### 5. 每日对账
至少做四组对账:
| 对账 | 左 | 右 |
| --- | --- | --- |
| 钱包余额 | `worker_wallets.available_amount` | 总账中打手可提现余额 |
| 押金余额 | `worker_wallets.frozen_deposit_amount` | 总账中打手押金负债 |
| 待解冻 | `worker_wallets.pending_unfreeze_amount` | 总账中待解冻明细 |
| 平台应收 | 平台账单/结算单 | 总账中应收账款 |
差异写入 `finance_reconciliation_items`,必须能定位到具体订单、工单、提现单或事件。
---
## 五、影子期和转正期的区别
这是最容易混淆的地方。
### 5.1 影子期(观察)
- 现有钱包仍是权威;
- 影子总账写入失败,**不应阻断业务**;
- 失败事件进入重试/死信队列;
- 每日对账发现漏记、重复、金额不一致;
- 目标是把“事件覆盖率”和“对账差异”压到 0。
因此影子期可以做成:
```text
业务事务提交
→ 尝试写影子总账(best-effort)
→ 失败只记录 finance_business_events.status=failed
→ 定时任务重试
```
### 5.2 转正期(接管)
- 总账成为权威;
- 分录写入与业务状态变更必须在**同一个数据库事务**;
- 分录写失败 → 业务回滚;
- 钱包余额从总账投影读取;
- 旧钱包字段保留一个版本用于回滚。
```text
业务事务开始
→ 改业务状态
→ 写总账事件 + 分录
→ 更新钱包投影
→ 提交
```
**影子期允许“账慢一点”,转正期不允许“账少一笔”。**
---
## 六、历史数据怎么处理
不建议一上来把 32 万+ 历史订单全部重放。更稳妥的做法是:
### 方案 A:期初余额 + 未来流水(推荐)
1. 选一个切换日(例如 2026-11-01)。
2. 把切换日各科目余额作为“期初分录”导入:
- 银行存款
- 平台应收
- 应付打手
- 打手押金
- 打手可提现余额
- 应付卖家货款
3. 切换日之后的所有业务走影子总账。
4. 历史报表继续由旧系统/旧查询提供,或后续再做历史回放。
优点:风险低、上线快、不会因为历史脏数据卡住。
缺点:历史订单级损益需要另行回放。
### 方案 B:有限回放(用于验证)
选最近 1–3 个月做回放:
- 从 `orders`、`work_orders`、`worker_wallet_ledgers`、`worker_finance_requests`、`consignment_*` 重建事件;
- 与现有钱包/报表逐日比对;
- 验证 posting 规则,不要求覆盖全部历史。
### 方案 C:全量回放(不推荐先行)
历史数据可能存在:
- 已删除/改写的业务记录;
- 旧口径押金;
- 多渠道补单;
- 手工调账没有事件来源。
全量回放很容易得到“永远对不平”的结果,反而拖慢项目。
---
## 七、转正标准
建议至少满足:
1. 所有核心事件类型已接入:订单支付/退款、工单结算、押金冻结/释放/没收、提现、充值、寄卖押金/货款、售后扣款/欠款。
2. 连续 30 天:
- 借贷不平凭证数 = 0;
- 钱包余额与总账重算差异 = 0;
- 押金/待解冻/提现冻结差异 = 0;
- 平台应收差异可解释;
- 事件漏记率低于约定阈值(例如 0.1%)。
3. 财务/运营完成抽样核对与签字。
4. 有明确回滚开关:关闭总账投影读,回到旧钱包字段。
5. 关账、冲正、手工分录权限与审计已就绪。
---
## 八、常见误区
| 误区 | 实际 |
| --- | --- |
| 影子总账 = 数据库备份 | 不是。备份是数据副本,影子总账是另一套会计模型 |
| 有 `worker_wallet_ledgers` 就是总账 | 不是。它是单边业务流水,不能出借贷平衡报表 |
| 先把钱包表改成借贷 | 风险极高。应先并行建账,再切换读模型 |
| 影子账可以先随便改 | 不行。已过账分录必须不可变,否则无法审计 |
| 对账就是总账 | 不是。对账是校验手段,总账是记账体系 |
| 历史数据必须全部重算 | 不一定。期初余额 + 未来流水通常更稳 |
| 影子期 Ledger 写失败要回滚业务 | 影子期不应阻断业务;转正期才必须同事务 |
---
## 九、在本项目落地的最小步骤
> 表结构、API 与分期细节见 [财务系统集成方案.md](财务系统集成方案.md) §三–§六;全局顺序以 [README.md](README.md) §三 为准。
1. **先完成前置**:商品主数据、渠道 SKU 映射、成本中心、销售事实。
2. **建总账表**:`finance_accounts`、`finance_business_events`、`finance_journal_entries`、`finance_journal_lines`、`finance_periods`、`finance_reconciliations`。
3. **先接 4 类高频事件**:订单支付、工单结算、押金冻结/释放/没收、提现;影子期只观察,不阻断业务。
4. **日对账 + 差异清单**:钱包余额 vs 总账余额;先只报差异,不自动改账。
5. **连续对平后转正**:总账成为权威,钱包改为投影;保留回滚开关。
---
## 十、一句话总结
**影子总账就是“先用会计的方式把业务再记一遍,但不让它管钱;等它和现有钱包、平台账单长期对得上,再让它转正”。** 它的价值不是多一张表,而是提前暴露“钱从哪里来、到哪里去、哪一笔没记、哪一笔记错了”,避免财务系统上线后才发现历史资金链路对不平。