Files
order_site/docs/财务系统集成方案.md
T
yml2213 7bacb7d5eb docs: 重构项目文档并补充三块载体、成本中心与人工支出方案
合并本轮全部文档改动(原 5 个 docs 提交压成 1 个)。

口径与索引:
- 新增 docs/README.md 文档总索引:三来源口径(91 + 快手 send-code = kuaishou-91)、
  「一个订单 = 骨架 + 买家凭证 + 一个执行载体」、成本只改一处、文档状态机
- 过期阶段文档移入 docs/archive/2026-10-08/(保留内容,不删除)

分析报告:
- docs/项目全量分析报告-2026-10-17.md:架构、五条业务链路、P0/P1/P2 风险与改进路线
- docs/载体判定与成本中心落地-复核与草案-2026-10-17.md:生产只读克隆量化、上一轮结论复核与 4 处修正

方案文档:
- docs/成本中心与三块履约模型.md:三块执行载体、标准商品、成本版本与快照、人工支出登记
- docs/财务系统集成方案.md:复式记账模型、分录表、分期落地
- docs/影子总账说明.md:影子期与转正期的边界、对账与转正标准

SQL 草案(docs/drafts/,未接入迁移):
- 载体判定与人工块量化.sql:量化查询、carrier_type 判定与回填、守恒校验
- 成本中心与销售事实-DDL草案.sql:product_catalog / channel_product_mappings /
  cost_items / cost_versions / cost_snapshots / cost_exceptions / sales_daily_facts
- 人工支出登记-DDL草案.sql:manual_fulfillment_cases / manual_expense_records / manual_expense_events

业务口径确认(2026-10-17):
- 收入 = 用户实付(orders.total_amount),与快手 paymentFen 数据核验 100% 一致
- 打手报酬 = 支出/成本(最终提现打款),按总额法
- 人工块以「支出可追溯」为先,SLA 监管延后
- 退款/仲裁暂未接入,先预留字段与反向冲正关系

三份草案已在一次性沙箱库按依赖顺序零错误应用(10 张新表、27 个 CHECK 约束)。
2026-10-17 22:03:03 +08:00

21 KiB
Raw Blame History

财务系统集成方案

状态:方案(待实施) 代码基线:main @ 6850bc64 目标:在不破坏现有抢单、押金、结算、寄卖、提现链路的前提下,为 order_site 增加一套可对账、可关账、可追溯的财务系统。 结论先说:不要直接在 worker_wallets / worker_wallet_ledgers 上继续加字段。 现有表是业务钱包与流水,不是会计总账;正确做法是新增复式记账底座,让现有钱包成为它的“余额投影”。

如果只想知道“影子总账”是什么、为什么先跑影子账:先读 影子总账说明.md。


一、现有财务能力盘点

1.1 已有表

表 作用 现状
worker_wallets 打手资金汇总 available_amount / frozen_deposit_amount / pending_unfreeze_amount / frozen_withdraw_amount / total_credited_amount / total_settled_amount
worker_wallet_ledgers 打手资金流水 ledger_type / amount / balance_after / frozen_after / audit_status / payload_json
worker_deposit_unfreezes 押金/报酬延迟解冻队列 到点后由调度任务入账
worker_finance_requests 充值/提现申请 request_type / status / amount / frozen_amount
worker_withdrawal_accounts 提现账户 多渠道、二维码、黑名单
worker_after_sales_cases / worker_after_sales_debts 售后追责与欠款 欠款可从后续报酬抵扣
worker_wallet_ledgers.payload_json 寄卖发布押金等扩展信息 kind / consignmentSubmissionId 等
consignment_submissions / consignment_inventories 寄卖发布押金与货款 押金冻结/释放/没收、供货货款结算

1.2 已有后台财务页

apps/backend/src/services/admin/admin-finance-service.ts 目前用一条大 UNION ALL 把订单收款、退款、打手报酬、提现、充值、售后追缴拼成“财务事件”,输出日汇总与流水。

这不是总账:

  • 没有借贷方向,无法验证每笔业务是否平衡。
  • 没有会计科目、期间、关账。
  • 没有平台侧账单与对账差异。
  • 没有订单级收入/成本/毛利。
  • 没有业务事件幂等键,重放可能重复统计。
  • 余额是可变字段,无法从流水独立重算并校验。

1.3 现有资金动作(必须全部纳入财务)

业务动作 代码入口 财务性质
订单支付 services/order/order-service.ts 收入/预收 + 平台应收
订单退款 订单状态回流、售后 冲减收入/应收
工单验收结算 repositories/worker-platform/work-order-settlement-repo.ts 履约成本 + 应付打手
抢单冻结押金 work-order-grab-repo.ts 打手余额 → 押金负债
验收释放押金 work-order-settlement-repo.ts 押金负债 → 打手余额
取消/超时没收押金 work-order-cancel-repo.ts 押金负债 → 违约金收入
拼单份额结算 work-order-settlement-repo.ts 按份额拆分成本/押金
售后扣款/欠款抵扣 after-sales-repo.ts 冲减应付打手 / 其他应收
充值审核通过 worker-finance-request-service.ts 银行存款 → 打手余额
提现申请/审核 同上 打手余额 → 银行存款
寄卖发布押金 consignment-deposit-repo.ts 打手余额 → 押金负债
寄卖货款结算 consignment-deposit-repo.ts 采购成本/存货 → 应付卖家
平台服务费/通道费 目前未单独建模 销售费用/财务费用

1.4 三块履约与成本中心的前置关系

财务总账不是孤立存在的,它需要先知道“这笔钱属于哪个商品、哪个店铺、哪个执行载体、用了哪个成本版本”:

块 载体 成本来源 财务侧
自动发货 affiliate_dash 上游固定 cost_amount → cost_versions 采购成本
接单平台 work_orders 模板价格 → cost_versions / 工单实际报酬快照 履约成本 + 押金负债
人工客服 manual_fulfillment_cases 按单/按小时人工成本 人工成本 + 履约完成

因此建议顺序是:

商品主数据 + 渠道SKU映射
  -> 成本中心(cost_items / cost_versions / cost_snapshots)
  -> 销售事实(sales_daily_facts)
  -> 影子总账(finance_*)
  -> 对账 / 转正

详细设计见 成本中心与三块履约模型.md。


二、建设原则

  1. 复式记账:每笔业务至少两行分录,借方合计 = 贷方合计;不平不允许提交。
  2. 不可变:已过账分录禁止 UPDATE / DELETE;冲正用反向分录。
  3. 业务事件幂等:每个业务动作有唯一 business_event_id,重复投递只入账一次。
  4. 金额只用整数分:数据库 BIGINT,禁止浮点。
  5. 钱包是投影:worker_wallets 的余额必须能由总账分录重算出来;差异告警。
  6. 同一事务:业务状态变更与会计分录在同一数据库事务内提交,避免“货发了、账没记”。
  7. 期间可关账:关账后禁止补记;必须用调整分录并留原因。
  8. 对账优先:先做影子账 + 对账,再切换钱包读模型,不做一次性大爆炸迁移。

三、目标架构

flowchart LR
  BIZ["业务动作<br/>订单/工单/寄卖/提现/售后"] --> OUTBOX["finance_business_events<br/>幂等事件"]
  OUTBOX --> POST["finance-posting-service<br/>借贷规则"]
  POST --> JE["finance_journal_entries"]
  JE --> JL["finance_journal_lines"]
  JL --> ACC["finance_accounts<br/>科目表"]
  JL --> PROJ["余额投影<br/>worker_wallets / 平台应收应付"]
  EXT["平台账单 / 支付流水"] --> REC["finance_reconciliations"]
  PROJ --> REC
  REC --> REPORT["试算平衡 / 利润表 / 订单损益"]

3.1 科目表(建议)

编码 科目 类型 正常方向
1001 银行存款 资产 借
1002 支付通道待结算 资产 借
1101 应收账款-平台 资产 借
1201 预付账款-供货平台 资产 借
1301 其他应收-打手欠款 资产 借
2001 应付打手报酬 负债 贷
2002 应付卖家货款 负债 贷
2003 打手押金 负债 贷
2004 打手可提现余额 负债 贷
2101 应付平台服务费 负债 贷
2201 预收账款 负债 贷
3001 本年利润 权益 贷
4001 商品销售收入 收入 贷
4101 平台服务费收入 收入 贷
4201 违约金/没收押金收入 收入 贷
5001 商品采购成本 成本 借
5101 履约成本-打手报酬 成本 借
5201 支付通道费 费用 借
5301 售后损失 费用 借
5401 平台服务费 费用 借

科目可按实际业务裁剪,但“资产/负债/权益/收入/成本费用”五类与借贷方向必须先定死。

3.2 核心表

-- 科目
CREATE TABLE finance_accounts (
  id BIGSERIAL PRIMARY KEY,
  account_code TEXT NOT NULL UNIQUE,
  account_name TEXT NOT NULL,
  account_type TEXT NOT NULL CHECK (account_type IN ('asset','liability','equity','revenue','expense')),
  normal_side TEXT NOT NULL CHECK (normal_side IN ('debit','credit')),
  parent_id BIGINT REFERENCES finance_accounts(id),
  status TEXT NOT NULL DEFAULT 'active',
  created_at TIMESTAMPTZ NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL
);

-- 业务事件收件箱:幂等闸门
CREATE TABLE finance_business_events (
  id BIGSERIAL PRIMARY KEY,
  business_event_id TEXT NOT NULL UNIQUE,
  event_type TEXT NOT NULL,
  source_type TEXT NOT NULL,
  source_id TEXT NOT NULL,
  occurred_at TIMESTAMPTZ NOT NULL,
  payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
  status TEXT NOT NULL DEFAULT 'received'
    CHECK (status IN ('received','posted','failed','ignored')),
  attempts INTEGER NOT NULL DEFAULT 0,
  last_error TEXT NOT NULL DEFAULT '',
  created_at TIMESTAMPTZ NOT NULL,
  processed_at TIMESTAMPTZ
);

-- 结算单(打手报酬 / 卖家货款 / 平台服务费)
CREATE TABLE finance_settlements (
  id BIGSERIAL PRIMARY KEY,
  settlement_no TEXT NOT NULL UNIQUE,
  counterparty_type TEXT NOT NULL CHECK (counterparty_type IN ('worker','supplier','platform','other')),
  counterparty_id TEXT NOT NULL DEFAULT '',
  period_key TEXT NOT NULL,
  amount_fen BIGINT NOT NULL DEFAULT 0,
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending','approved','paid','cancelled')),
  approved_at TIMESTAMPTZ,
  paid_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL
);

CREATE TABLE finance_settlement_items (
  id BIGSERIAL PRIMARY KEY,
  settlement_id BIGINT NOT NULL REFERENCES finance_settlements(id) ON DELETE CASCADE,
  source_type TEXT NOT NULL,
  source_id TEXT NOT NULL,
  amount_fen BIGINT NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL
);

-- 凭证头
CREATE TABLE finance_journal_entries (
  id BIGSERIAL PRIMARY KEY,
  entry_no TEXT NOT NULL UNIQUE,
  business_event_id TEXT NOT NULL UNIQUE REFERENCES finance_business_events(business_event_id),
  entry_type TEXT NOT NULL,
  occurred_at TIMESTAMPTZ NOT NULL,
  posted_at TIMESTAMPTZ NOT NULL,
  period_key TEXT NOT NULL,
  currency TEXT NOT NULL DEFAULT 'CNY',
  memo TEXT NOT NULL DEFAULT '',
  status TEXT NOT NULL DEFAULT 'posted' CHECK (status IN ('draft','posted','reversed')),
  reversal_of_id BIGINT REFERENCES finance_journal_entries(id),
  created_by TEXT NOT NULL DEFAULT 'system',
  created_at TIMESTAMPTZ NOT NULL
);

-- 凭证行
CREATE TABLE finance_journal_lines (
  id BIGSERIAL PRIMARY KEY,
  entry_id BIGINT NOT NULL REFERENCES finance_journal_entries(id) ON DELETE RESTRICT,
  line_no INTEGER NOT NULL,
  account_id BIGINT NOT NULL REFERENCES finance_accounts(id),
  debit_fen BIGINT NOT NULL DEFAULT 0,
  credit_fen BIGINT NOT NULL DEFAULT 0,
  order_id BIGINT,
  work_order_id BIGINT,
  worker_id BIGINT,
  shop_id TEXT NOT NULL DEFAULT '',
  provider TEXT NOT NULL DEFAULT '',
  settlement_id BIGINT REFERENCES finance_settlements(id),
  memo TEXT NOT NULL DEFAULT '',
  dimensions_json JSONB NOT NULL DEFAULT '{}'::jsonb,
  CHECK (debit_fen >= 0 AND credit_fen >= 0),
  CHECK ((debit_fen > 0) <> (credit_fen > 0)),
  UNIQUE (entry_id, line_no)
);

-- 会计期间
CREATE TABLE finance_periods (
  period_key TEXT PRIMARY KEY,          -- 2026-10
  starts_at TIMESTAMPTZ NOT NULL,
  ends_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','closed')),
  closed_at TIMESTAMPTZ,
  closed_by TEXT NOT NULL DEFAULT ''
);

-- 对账批次 / 明细
CREATE TABLE finance_reconciliations (
  id BIGSERIAL PRIMARY KEY,
  channel TEXT NOT NULL,                -- 91 / kuaishou / pdd / douyin / affiliate_dash / alipay
  period_key TEXT NOT NULL,
  expected_fen BIGINT NOT NULL DEFAULT 0,
  actual_fen BIGINT NOT NULL DEFAULT 0,
  diff_fen BIGINT NOT NULL DEFAULT 0,
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending','matched','diff','resolved')),
  created_at TIMESTAMPTZ NOT NULL,
  resolved_at TIMESTAMPTZ
);

CREATE TABLE finance_reconciliation_items (
  id BIGSERIAL PRIMARY KEY,
  reconciliation_id BIGINT NOT NULL REFERENCES finance_reconciliations(id) ON DELETE CASCADE,
  source_type TEXT NOT NULL,
  source_id TEXT NOT NULL,
  external_no TEXT NOT NULL DEFAULT '',
  expected_fen BIGINT NOT NULL DEFAULT 0,
  actual_fen BIGINT NOT NULL DEFAULT 0,
  diff_fen BIGINT NOT NULL DEFAULT 0,
  resolution TEXT NOT NULL DEFAULT '',
  created_at TIMESTAMPTZ NOT NULL
);

3.3 业务事件 → 分录规则(建议)

冲正不修改原凭证:新建一条 business_event_id = <原ID>:reverse 的反向凭证,并让 reversal_of_id 指向原凭证;原凭证保持 posted 不变。

事件 借方 贷方 备注
订单支付 1002 支付通道待结算 / 1101 应收平台 2201 预收账款 金额 = 用户实付(已确认);确认时点待拍板
履约完成/验收 2201 预收账款 4001 商品销售收入 或按交付时点确认
工单验收结算 5101 履约成本 2001 应付打手报酬 按份额逐笔
打手报酬转可提现 2001 应付打手报酬 2004 打手可提现余额 与现有 reward_settlement 对齐
抢单冻结押金 2004 打手可提现余额 2003 打手押金 不产生损益
释放押金 2003 打手押金 2004 打手可提现余额 不产生损益
没收押金 2003 打手押金 4201 违约金收入 需原因与审计
提现审核通过 2004 打手可提现余额 1001 银行存款 先冻结后扣减
充值审核通过 1001 银行存款 2004 打手可提现余额 需支付流水号
售后扣款 2001/2004 1301 其他应收-打手欠款 欠款不足转 worker_after_sales_debts
寄卖发布押金 2004 打手可提现余额 2003 打手押金 与普通押金分维度
寄卖验收货款 5001 商品采购成本 2002 应付卖家货款 货款金额取 `cost_price_fen
支付卖家货款 2002 应付卖家货款 1001 银行存款 需结算单
平台服务费 5401 平台服务费 2101 应付平台服务费 从平台账单导入
人工支出登记通过(垫付) 5101 履约成本-人工(按 cost_item_id 映射科目) 2005 其他应付-垫付人 付款项目/订单号/付款人必填;见 人工支出登记草案
人工垫付报销 2005 其他应付-垫付人 1001 银行存款 需凭证与审批留痕
退款冲正(预留) 反向凭证 reversal_of_id 指向原凭证 — 退款/仲裁暂未接入,先预留不实现

已确认(2026-10-17):收入金额口径 = 用户实付;打手报酬 = 支出(总额法);退款/仲裁先预留;人工支出以登记表为入口。 仍待拍板:收入确认时点(付款 / 发货 / 验收 / 售后期结束),它直接影响利润表与预收账款。


四、与现有代码的接入方式

4.1 唯一记账入口

新增 apps/backend/src/services/finance/finance-posting-service.ts:

postBusinessEvent({
  businessEventId,   // 业务唯一键,如 work_order:123:settle
  eventType,         // work_order_settled
  sourceType,        // work_order
  sourceId,          // 123
  occurredAt,
  lines: [...],      // 借贷行
})

约束:

  • 在业务事务内调用;使用同一个 PoolClient。
  • finance_business_events 先 INSERT ... ON CONFLICT DO NOTHING。
  • 已存在则直接返回,不重复入账。
  • 校验借贷平衡;不平抛错并回滚业务事务。
  • 写入 finance_journal_entries / finance_journal_lines。
  • 更新钱包投影(或由投影任务异步更新,但同一事务更安全)。

4.2 需要改造的写入点

位置 改造
services/order/order-service.ts 订单支付/退款发事件
repositories/worker-platform/work-order-settlement-repo.ts 验收结算、押金释放、报酬入账发事件
work-order-grab-repo.ts 抢单冻结押金发事件
work-order-cancel-repo.ts 取消/超时没收押金发事件
after-sales-repo.ts 售后扣款/欠款抵扣发事件
worker-finance-request-service.ts 充值/提现审核发事件
consignment-deposit-repo.ts 发布押金、货款结算发事件
admin-finance-service.ts 改为读总账/试算平衡,不再用 UNION ALL 拼报表

4.3 钱包投影一致性

第一阶段保留现有钱包更新逻辑,同时写总账;第二阶段再切换:

  1. 影子期:业务照旧改钱包,同时写总账;每日比较“钱包余额”与“总账重算余额”。
  2. 校验期:差异为 0 连续 N 天后,后台展示“总账余额”为准。
  3. 切换期:钱包读接口改为读投影;旧字段保留一个版本用于回滚。
  4. 清理期:删除重复的余额更新代码,只保留总账 + 投影。

五、后台页面与 API

5.1 页面

页面 内容
财务总览 收入、成本、毛利、押金负债、可提现余额、待结算
凭证簿 按期间/事件/科目/订单筛选;查看借贷行
科目表 科目树、启用/停用、期初余额
结算中心 打手报酬、卖家货款、平台服务费结算单
对账中心 平台账单导入、差异明细、处理状态
期间关账 月末检查、关账、反关账(需高权限)
订单损益 单笔订单收入、采购成本、打手报酬、退款、毛利
试算平衡 / 利润表 按期间导出

5.2 API

建议新增前缀 /api/v1/admin/finance:

GET  /accounts
POST /accounts
GET  /journal?period=&eventType=&orderId=&workOrderId=
GET  /journal/:id
GET  /trial-balance?period=
GET  /profit-loss?period=
GET  /order-profit/:orderId
GET  /settlements
POST /settlements/:id/approve
GET  /reconciliations
POST /reconciliations/import
POST /reconciliations/:id/resolve
GET  /periods
POST /periods/:key/close

权限建议新增:

  • finance.post:手工分录
  • finance.close:关账
  • finance.reconcile:对账处理
  • 现有 finance.view / review / config / export 继续使用

所有写操作必须写 admin_audit_logs;关账、手工分录、冲正必须填原因。


六、分期落地

P0:影子总账(1–2 周)

前置:先完成商品主数据、渠道 SKU 映射、成本中心与销售事实(见 成本中心与三块履约模型.md)。否则影子总账只能记“总额”,无法按店铺/皮肤/成本版本出毛利。

  • 建 finance_accounts / business_events / journal_entries / journal_lines / periods。
  • 先接 4 类高频事件:订单支付、工单结算、押金冻结/释放/没收、提现。
  • 写总账但不改现有钱包逻辑。
  • 每日对账:钱包余额 vs 总账余额。
  • 补金额守恒与幂等测试。

P1:寄卖、售后与人工成本(1–2 周)

  • 接寄卖发布押金、货款结算、售后欠款/抵扣。
  • 接人工 case 的人工成本快照(按单/按小时)。
  • 建 finance_settlements / finance_settlement_items。
  • 后台增加凭证簿与试算平衡。

P2:平台对账(2–3 周)

  • 接 91 / 快手 / PDD / 抖音 / affiliate-dash 账单或导出。
  • 建 finance_reconciliations / finance_reconciliation_items。
  • 差异必须能定位到订单/工单/提现单。
  • 对账差异不自动改账,必须人工确认后生成调整分录。

P3:报表与关账(2 周)

  • 利润表、订单损益、现金流量简表。
  • 期间关账、反关账、期初余额。
  • 导出与权限审计。

P4:钱包切换(1 周,需灰度)

  • 钱包读模型切到总账投影。
  • 旧字段保留一个版本;差异告警。
  • 回滚方案:关闭投影读开关,回到旧钱包字段。

七、必须补的测试

  1. 每笔凭证借方合计 = 贷方合计。
  2. 同一 business_event_id 重复投递只入账一次。
  3. 冲正后原凭证 + 反向凭证净额为 0。
  4. 钱包余额 = 总账重算余额。
  5. 抢单冻结 / 验收释放 / 取消没收的押金守恒。
  6. 拼单多份额:份额报酬 + 押金 + 欠款抵扣 = 钱包变化。
  7. 提现:申请冻结、审核通过、拒绝解冻三条路径。
  8. 寄卖:发布押金、驳回释放、验收货款、撤销处置。
  9. 售后:扣款、欠款、后续报酬抵扣。
  10. 关账后禁止补记;调整分录必须留原因。
  11. 对账:平台金额、订单金额、钱包金额三方差异可定位。

八、需要业务拍板的问题

2026-10-17 已确认 4 条口径,明细见 成本中心与三块履约模型.md §11。

# 问题 状态 影响
1 收入金额口径 ✅ 已确认:收入 = 用户实付(orders.total_amount),与快手 paymentFen 数据核验 100% 一致 收入基数
2 收入确认时点:付款 / 发货 / 验收 / 售后期结束? ⏳ 待定(金额口径已定) 决定利润表与预收账款
3 平台结算周期与账单口径(91、快手、PDD、抖音、affiliate-dash) ⏳ 待定 决定对账与应收
4 打手报酬是“成本”还是“代收代付”? ✅ 已确认:支出/成本(最终提现打款),按总额法 收入总额法/净额法
5 寄卖货款的采购成本确认时点 ⏳ 待定 决定存货/成本
6 提现通道费、平台服务费由谁承担 ⏳ 待定 决定费用科目与结算
7 退款、拒付、仲裁的冲减规则 ✅ 已确认:暂未接入,先预留(字段 + reversal_of_id + 事件类型,不实现逻辑) 决定售后与收入冲回
8 人工支出如何入账 ✅ 已确认:以「人工支出登记」为入口(付款项目/金额/订单号/付款人/凭证),SLA 监管延后 决定人工成本科目与总账事件
9 是否需要发票/税务 ⏳ 待定 决定发票表与税额
10 是否多币种 ⏳ 待定(当前 CNY) 若否则需汇率与汇兑损益

九、一句话结论

财务系统应作为“总账层”接入,而不是继续扩展业务钱包。 先把商品主数据、成本中心、人工 case 与销售事实补齐,再以影子总账 + 幂等业务事件 + 每日对账落地;确认钱包与总账长期一致后,再切换余额读模型。这样既能保护现有抢单/押金/寄卖链路,也能为平台结算、订单损益和关账留出正确底座。