docs(finance): 现行财务文档收敛为 5 篇,履约口径只定义一次

原 9 篇 3,647 行,履约分类口径在总纲、模型文档、交接总结里重复三遍,
且仍留着"三块履约""按小时人工成本"等已作废表述。本次收敛为 5 篇
1,778 行:

- 财务与履约口径总纲-2026-10-23.md(300 行):唯一口径来源
  七条硬口径、三个正交维度、真实履约构成、成本模型规则、人工全覆盖、
  实施顺序、进度、验收 SQL、防回归清单
- 成本中心与履约模型.md(471 行):数据模型、迁移 103–107、履约回填、
  销售事实、本地库原地改造、索引与性能
- 成本中心API契约.md(270 行):字段替换总表、枚举、前后端模块边界
- 人工支出与备用金.md(218 行):人工支出登记字段/状态机/权限 +
  客服备用金台账
- 财务系统集成方案.md:复式总账层(未实施)

旧的 4 篇(成本中心与三块履约模型、人工支出登记落地方案、
客服备用金台账实施方案、财务履约改造交接总结)移入
docs/archive/2026-10-23/ 并加归档提示。

履约分类判定 SQL 只在总纲保留一份,其它文档改为链接。
This commit is contained in:
yml2213
2026-10-23 16:06:40 +08:00
parent 63ae32d18c
commit 43f402cf72
15 changed files with 1103 additions and 337 deletions
@@ -1,10 +1,10 @@
# 载体判定与成本中心落地:复核与草案(2026-10-17 第二轮)
> 状态:**历史(2026-10-17 第二轮复核,已归档)**;基线:`main @ ccd5ed2a`;代码未改动。
> 说明:本文的现行结论已吸收进 [成本中心与三块履约模型.md](../../成本中心与三块履约模型.md) 与 [财务系统集成方案.md](../../财务系统集成方案.md);本文件只用于追溯 2026-10-17 的数据量化与 DDL 验证过程,不再作为实现依据。
> 说明:本文的现行结论已吸收进 [成本中心与三块履约模型.md](../../成本中心与履约模型.md) 与 [财务系统集成方案.md](../../财务系统集成方案.md);本文件只用于追溯 2026-10-17 的数据量化与 DDL 验证过程,不再作为实现依据。
> 数据来源:**本地只读克隆**(生产快照,`orders` 316,597 行,数据截至 2026-10-14 14:52),全程只读 SELECT,未连生产、未写入任何业务库。
> 金额单位:**分**(`orders.total_amount` / `work_orders.reward_amount` / `source_order_amount_fen` 均为分)。
> 上游文档:[成本中心与三块履约模型.md](../../成本中心与三块履约模型.md)、[项目全量分析报告-2026-10-17.md](../../项目全量分析报告-2026-10-17.md)。
> 上游文档:[成本中心与三块履约模型.md](../../成本中心与履约模型.md)、[项目全量分析报告-2026-10-17.md](../../项目全量分析报告-2026-10-17.md)。
本轮任务是把上一轮「只分析」的结论**复核到底、量化到数、落成可执行草案**。三件事:① 提交并推送待提交文档;② 收敛本机生产备份暴露面;③ 用只读 SQL 量化载体分布与人工块,并产出载体判定 + 成本中心两份 SQL 草案。
@@ -0,0 +1,323 @@
# 人工支出登记落地方案(2026-10-18 修订)
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**已实施 P0(`dev` 分支,迁移 111;订单页人工录单、备用金扣款为 2026-10-22/23 增量)**
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> **总口径以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准。**
> 依据:业务口径「人工块当前不以 SLA 监管为目标,先把流程搞清楚:哪个客服处理了哪些东西、付了多少钱、什么项目、关联哪个订单」。
> 数据来源:本地生产快照克隆(数据截至 2026-10-14)。
> 重要:**客服备用金支出已经实施**(`manual_expenses`);**公司人工支出登记**(`manual_expense_records`)也已实施,两张表通过 `fund_expense_id` 关联,不是同一张表。
> 重要:人工成本**逐单、按实际金额**录入,入口在订单界面;**不按小时,也不按固定单量/固定单价**。
> 重要:**人工支出实际上都走客服备用金**——登记时必须选择备用金账户,系统在同一事务内扣减余额、写 `fund_entries` 与 `manual_expenses`,再回填 `manual_expense_records.fund_expense_id`。
> 重要:**客服角色只能使用自己的备用金账户**(默认自动带入,不能选择别人);后端越权直接 403。
> 重要:**历史人工支出不补录**,只取少量样本做校准;**上线后每一笔人工单必须 100% 有成本记录**,否则不允许关账。
> 相关文档:[成本中心与履约方式模型.md](../../成本中心与履约模型.md) §8 · [财务系统集成方案.md](../../财务系统集成方案.md) §八 · [客服备用金台账实施方案](../../人工支出与备用金.md) ·
> DDL 草案 [drafts/2026-10-17-人工支出登记-DDL草案.sql](../../drafts/2026-10-17-人工支出登记-DDL草案.sql)
---
## 〇、先分清两张表
| 表 | 业务含义 | 当前状态 | 关键字段 |
| --- | --- | --- | --- |
| `manual_expenses` | **客服备用金支出**:客服从自己的备用金账户里充值/采购/赔偿 | **已实施** | `account_id` / `fund_entry_id` / `category` / `payee_*` / `evidence_json` |
| `manual_expense_records` | **公司人工支出/垫付登记**:按订单/成本项记录公司实际付出去的钱 | **已实施 P0(迁移 111)** | `order_id` / `order_item_id` / `cost_item_id` / `paid_by_admin_user_id` / `status` / `cost_snapshot_id` |
结论:
1. 客服侧已经收口的是“备用金余额与用途”,解决“客服手里有多少钱、这笔钱花到哪”。
2. **公司口径的实际支出/垫付已落地**:后台「人工支出」页(`/admin/manual-expenses`)支持登记、审批、驳回、报销;通过后按成本项写 `cost_snapshots`,没有有效成本版本时进入异常队列。
3. 订单界面已接入「人工录单」:订单列表对人工单显示按钮、订单详情始终可录,弹窗锁定订单号、选择**备用金账户**并逐单填写**实际金额**,不存在小时单价或固定单量计价。
4. 两张表通过 `fund_expense_id` 关联,且该字段由系统写入:人工支出先从客服备用金账户扣款并生成 `manual_expenses`,再写 `manual_expense_records`。
5. **不要**把 `manual_expenses` 直接改名为 `manual_expense_records`,也不要让两张表共用索引/约束名。
实现落点(2026-10-23):
- 迁移:`apps/backend/src/db/migrations/111_manual_expense_records.sql`
- 服务:`apps/backend/src/services/finance/manual-expense-service.ts`;备用金扣款唯一写入口 `admin-fund-service.ts` 的 `writeFundExpense`
- 接口:`apps/backend/src/routes/admin/finance.ts` 的 `/finance/manual-expenses*`;订单页专用 `POST /finance/manual-order-costs`
- 页面:`apps/frontend/src/pages/admin/AdminManualExpensePage.tsx`,入口 `/admin/manual-expenses`
- 订单入口:订单列表/详情「人工录单」打开 `ManualOrderCostModal`,锁定订单号、选择订单项与备用金账户、填写本单实际金额;支持深链 `/admin/orders/:orderId?manualCost=1`
- 权限:查看 `finance.view` / `fund.view`,登记 `fund.expense` / `finance.config`,审核 `finance.review`
- 资金流:登记时锁定 `fund_accounts` 行 → 校验余额 → 扣减 → 写支出流水 → 写 `manual_expenses` → 写 `manual_expense_records.fund_expense_id`,全程同一事务;余额不足直接拒绝
- 成本快照:通过后以 `subject_type='manual_case'`、`subject_id=expense.id` 写 `actual` 快照;同一订单项多笔支出可累加,不会互相覆盖
---
## 一、结论先行
1. **订单里的数据足够预填 90%**:店铺、商品、规格、数量、用户实付、支付时间、平台单号、履约标签、状态,后台订单列表/详情已经全部具备。客服实际只需要补 **4 件事**:渠道/项目、金额、收款方、凭证(外加默认带出的「谁、什么时候」)。
2. **原有财务页没有“支出”这一侧**:7 个账目类别全部是收款或打手侧资金。现在公司人工支出已有独立「人工支出」页;客服备用金支出仍在 `fund` 模块,两者通过 `fund_expense_id` 桥接。
3. **落地成本低、风险小**:不需要改订单主表,不需要新角色体系(现有 `admin/finance/operator/support` 四角色 + `finance.*` 权限即可覆盖),只需新增「公司人工支出登记」表 + 订单侧入口 + 财务侧新类别。
4. **登记必须“发生在垫付当天”**才有监管价值;事后补记只能算账,不能算监管。
5. **本轮只补公司口径**:客服备用金台账已完成,不重复做一套客服余额系统;通过 `fund_expense_id` 关联即可。
---
## 二、现状盘点(逐字段,来自代码而非猜测)
### 2.1 订单已经带了什么(可直接预填)
订单列表行数据由 `mapAdminOrderListItem` 组装(`apps/backend/src/services/admin/admin-order-read-helpers.ts:17`),字段与截图中看到的元素一一对应:
| 界面元素(截图) | 数据字段 | 来源 | 支出登记用途 |
| --- | --- | --- | --- |
| 快手 / 91卡券 | `provider` / `platform` | 订单表 | 自动带出,决定默认渠道选项 |
| 2629001926827116 | `platformOrderId` | 订单表 | **对账主键**,必带 |
| 小鱼儿游戏服务店 | `shopName` / `shopId` | 订单表 | 统计维度(店铺) |
| 和平精英…雪山精灵兔 | `itemTitle` / `items[]` | 订单项 + 快照 | 商品/皮肤维度 |
| 指挥客密钥1个、(2进1)暗夜风云/蓝调溯影 | `skuNick` / `skuCode` | 订单项 | 规格维度(注意口径问题见 §7.4) |
| 数量 1 · 关联工单 1 | `totalQuantity` / `workOrderCount` | 订单项 / 工单 | 数量与载体提示 |
| ¥ 10.30 CNY | `totalAmount` / `totalAmountFen` | 订单表 | **用户实付(= 收入)**,用于算毛利 |
| 2026-10-17 22:24:21 | `paidAt` / `createdAt` | 订单表 | 支出时间不能早于支付时间 |
| 接单平台 / ⚡自动发货 / 🎫核销码 | `fulfillmentMode` + `voucherState` | 派生 | 履约方式与核销码必须分开显示;核销码不参与判定 |
| 已支付 | `payStatus` | 订单表 | 只有已支付才允许登记支出 |
| 待准备资源 / 未开始 | `resourceStatus` / `customerStatus` | 任务/工单派生 | 提示当前进度 |
| (未在列表显示)操作人 | 后台会话 `userId` / `username` / `role` | `admin_sessions` | 默认「登记人」 |
补充事实:克隆库里后台账号 **29 个,其中 `support`(客服)20 个**,`finance`(财务)角色独立存在 —— 登记人群与审批人是现成的。
> 说明:截图上的标签文案在代码里是「待准备资源」(`resourceStatus = pending_prepare`)、「未开始」(`customerStatus = not_started`),见 `apps/frontend/src/components/admin/StatusTag.tsx:39,48`。
### 2.2 订单里**没有**的(必须人工补,这也是登记表的全部价值)
| # | 缺失信息 | 为什么订单里不可能有 |
| --- | --- | --- |
| 1 | **实际走哪个渠道/找谁履约** | 订单只记录**平台来源**(快手/PDD/抖音),不记录我方线下找的上游、打手、代购 |
| 2 | **实际付了多少钱** | 订单只有**用户付给我们的钱**,没有**我们付出去的钱** |
| 3 | **收款方**(微信号/支付宝/银行卡/账号/昵称) | 完全在系统外 |
| 4 | **凭证**(转账截图、聊天记录、订单截图) | 完全在系统外 |
| 5 | **实际垫付人** | 可能与登录账号不同(主管代付、他人代垫) |
| 6 | **备注/原因**(为什么赔、加急、补差价) | 无结构可依 |
### 2.3 现有财务流水的类别缺口(关键发现)
`AdminFinancePage.tsx:39` 的账目类别:
```text
订单收款(order_paid) · 订单退款(order_refund) · 打手报酬(worker_reward) · 打手提现(withdraw_paid)
打手充值(recharge) · 打手资金回收(worker_recovery) · 售后追缴退还(after_sales_refund)
```
**没有任何一类描述“我方公司支出”**。后端 `admin-finance-service.ts` 的 `allowedCategories` 同样只有这 7 类,且方向判定只把 `order_paid / recharge / worker_recovery` 视为流入。客服备用金页已有自己的 `manual_expenses` 明细,但那是 fund 口径,不会自动进入财务页;即使今天记了公司人工支出,财务页也没有地方落。
**结论**:人工支出登记必须同时补「财务流水类别」,否则只是把数据存进了另一张看不见的表。
---
## 三、登记模型:以订单为入口,人工只做最小输入
### 3.1 字段级分工(自动带出 vs 人工填写)
| 字段 | 谁提供 | 必填 | 说明 |
| --- | --- | --- | --- |
| 订单号 / 订单项 | 自动(订单) | 是(无订单支出除外) | 从订单详情/列表发起时锁定 |
| **客服备用金账户** | **人工选 / 客服自动** | **是** | 决定这笔钱从哪个客服的备用金扣;客服角色锁定自己的账户,管理员/财务可选择。同一事务内扣余额、写流水与 `manual_expenses` |
| 平台 / 店铺 / 商品 / 规格 / 数量 | 自动(订单) | 是 | 只读展示,避免手抄错 |
| 用户实付 | 自动(订单) | 是 | 只读,用于毛利 |
| 支付时间 | 自动(订单) | 是 | 校验支出时间不早于它 |
| **付款项目(渠道/成本项)** | **人工选** | **是** | 对应 `cost_items`,例如「上游拿货-A 渠道」「代练打手」「平台充值」;没有可选项时走"新建项目"审批流 |
| **支出金额** | **人工填** | **是** | 单位分;> 阈值触发二级审批 |
| **收款方类型 + 名称** | **人工填** | **是** | 打手 / 上游 / 客户 / 平台 / 其他 |
| **收款账号** | 人工填 | 否 | 微信/支付宝/银行卡号,用于事后核对 |
| **付款方式** | 人工选 | 是 | 微信 / 支付宝 / 银行 / 现金 / 平台余额 |
| **付款时间** | 人工填 | 是 | 默认当前时间,可回填(当天) |
| **凭证** | **人工传** | **是** | 转账截图/聊天记录,支持多张 |
| 登记人 + 登记时间 | 自动(会话) | — | 默认登录账号,可改(代付场景) |
| 备注 / 原因 | 人工填 | 否 | 赔偿、差价、加急等必须写 |
| 报销状态 | 流程 | — | 待审 / 已通过 / 已驳回 / 已报销 |
### 3.2 两种登记形态都要支持
| 形态 | 场景 | 订单号 | 占比预估 |
| --- | --- | --- | --- |
| **有订单支出** | 客服看到订单 → 去别的渠道拿货/找打手 → 登记 | 必填 | 绝大多数 |
| **无订单支出** | 平台充值、批量拿货、客户赔偿、运费、办公 | 允许为空 | 少数但必须支持,否则这部分钱永远查不到 |
无订单支出**必须单独可见**(财务页筛选项 + 异常队列统计),不能混进订单支出里被平均掉。
> **人工成本计价口径(2026-10-22 确认)**:人工单**逐单、按实际金额**登记,**不按小时、不按固定单量/固定单价**——每一单的实际付款都可能不同。
> 订单号是必填前置条件;同一订单项的多笔实际支出逐笔累加,未录入即保持 `cost_missing`,不按均价自动补齐。
> **资金来源(2026-10-23 确认)**:人工支出实际上都是客服备用金出的,因此登记时**必须选择备用金账户**;公司直付不再作为默认路径。**客服只能用自己的账户**,默认自动带入;后端对越权账户返回 403。
### 3.3 一对多关系(必须允许)
- **一个订单项可以有多笔支出**:渠道费 + 加急费 + 补差价 → 逐笔登记,汇总到订单损益。
- **一笔转账覆盖多个订单**(例如一次拿货 5 单)→ 建议拆成 5 条明细,用 `expense_batch_no` 归组(可选字段),避免把成本摊错订单。
- **实际支出 vs 标准成本**:`cost_snapshots` 记的是**标准成本**(成本中心算出来的),人工支出记的是**实际发生**。二者要能对比,差异就是"人工议价空间/异常"的信号 —— 这是后续做成本优化的关键数据,不要在实现时用其中一个覆盖另一个。
### 3.4 上线后人工全覆盖(硬要求)
- 人工判定:已支付订单项没有工单、没有自动发货任务、没有历史平台任务;**有没有快手核销码不影响判定**。
- 每一笔人工单必须有:处理人、实际金额、费用项目、收款方、付款方式、备用金账户、`fund_expense_id`、凭证或备注、成本快照。
- 新人工单必须在 **24 小时内**录入成本;超过 24 小时进入异常队列。
- 每日检查:人工单无成本记录 = 0;人工支出无 `fund_expense_id` = 0;人工支出与备用金流水金额不一致 = 0。
- 存在上述缺口时**不允许关账**。
- 历史人工支出不补录,只在报表中标记“历史未采集”,不纳入上线后的监控缺口。
---
## 四、交互落地(三个入口,一套数据)
### 4.1 订单列表(就是截图这一页)— 已落地
- 行操作已加「人工录单」按钮(与「详情」「H5」并列),只在该订单存在 `manual` 项时显示(`manualItemCount > 0`);点击跳转 `/admin/orders/:id?manualCost=1` 并自动打开录单弹窗。
- 本单已登记支出合计列尚未做;后续可加,让主管扫一眼看出哪些单垫了钱。
- 筛选条件「有/无支出登记」「支出待审批」尚未做,是后续主管抽查与财务对账的入口。
### 4.2 订单详情页 — 已落地「人工录单」弹窗
`AdminOrderDetailPage.tsx` 在操作区提供「人工录单」按钮,弹窗内锁定订单号、选择订单项(单订单项自动带出)、填写本单实际金额,可当场生效或提交财务审核。
订单详情后续可加一张汇总卡片(当前已能在弹窗内录入,卡片汇总尚未做):
```text
┌ 人工支出 ────────────────────────────────┐
│ 合计 ¥XX.XX(已通过) / 待审 ¥XX.XX │
│ ┌────────┬──────┬────────┬──────┬──────┐ │
│ │项目 │金额 │收款方 │付款人 │状态 │ │
│ ├────────┼──────┼────────┼──────┼──────┤ │
│ │上游拿货 │¥8.50 │微信-xx │客服A │待审 │ │
│ └────────┴──────┴────────┴──────┴──────┘ │
│ [+ 人工录单] │
│ 毛利预览:收入 ¥10.30 − 标准成本 ¥0 − 支出 ¥8.50 = ¥1.80 │
└──────────────────────────────────────────┘
```
「毛利预览」是这张卡片真正的管理价值:**一眼看到这单是不是亏的**。
### 4.3 财务页
- 账目类别新增:`人工支出(manual_expense)`、`人工垫付报销(manual_expense_reimburse)`。
- 新增「支出登记」页签:按 状态 / 客服 / 店铺 / 项目 / 日期 筛选,支持批量审批。
- 汇总口径:**谁付了多少钱**(按登记人)、**钱花在哪**(按项目/店铺)、**还有多少没报销**。
### 4.4 时间要求(监管的生命线)
**垫付当天登记**。建议规则:
- 超过 24 小时补登记 → 标记「迟登记」,进异常队列;
- 超过 7 天 → 必须主管审批才能登记;
- 未登记但有订单证据(如工单备注提到垫付)→ 系统提示补登。
理由:事后补记的金额与项目失真最快,而"当天登记"是可执行、可考核的硬约束。
---
## 五、状态机与权限(复用现有体系,不新增角色)
```text
draft ──提交──> submitted ──审批──> approved ──报销──> reimbursed
│
└──驳回──> rejected ──修改后重提──> submitted
(预留)approved/reimbursed ──退款或仲裁──> reversed(反向登记,不改原记录)
```
| 动作 | 谁可以 | 现有能力 |
| --- | --- | --- |
| 提交登记 | 客服 / 运营 / 管理员 | `requireAdminRoles(['admin','operator','support'])`(`routes/admin/orders.ts:14` 已是这个组合) |
| 审批 / 驳回 | 财务(或管理员) | 角色 `finance`;权限点 `finance.review` 已存在 |
| 维护付款项目 | 财务 | `finance.config` 已存在 |
| 查看支出 | 财务 / 管理员 | `finance.view` 已存在 |
| 导出 | 财务 | `finance.export` 已存在 |
> 注意:**订单路由目前只做角色校验,没有权限点校验**(`routes/admin/orders.ts` 全篇没有 `requirePermission`)。新增支出接口时建议按财务口径加权限点,避免客服看到全公司支出。
**审计双留痕**(现有约定即可满足):
1. 路由层 `createJsonHandler({ audit })` → 写 `admin_audit_logs`(`admin-audit-service.ts:11`);
2. 业务层写 `manual_expense_events`(只追加,记录状态流转与操作人)。
**不可变约束**:`approved` 之后禁止 UPDATE 金额/项目/收款方;要改只能反向登记(`reversal_of_id`),这是为退款/仲裁预留的,也是财务能信这张表的前提。
---
## 六、校验与异常队列(不默认 0、不静默通过)
| # | 规则 | 不通过时 |
| --- | --- | --- |
| 1 | 金额 > 0 | 拒绝提交 |
| 2 | 单笔 ≥ ¥5,000(阈值待定) | 需管理员二级审批 |
| 3 | 凭证必传(至少 1 张) | 拒绝提交 |
| 4 | 订单必须存在且 `pay_status = paid` | 拒绝;无订单支出走独立形态 |
| 5 | 付款项目必选 | 允许暂存但**不得 approved**;进「未归类」队列 |
| 6 | 同订单 + 同项目 + 同金额 + 同收款方,24h 内重复 | 提示疑似重复,需填原因才可提交 |
| 7 | 付款时间不得晚于当前、不得早于订单支付时间 | 拒绝 |
| 8 | 迟登记(> 24h) | 允许但标记,进异常队列 |
| 9 | 收款方为空/无意义(如「微信」) | 提示补全 |
**异常队列必须在财务页可见**:未归类数、迟登记数、无订单支出占比、待审超期数。否则又会变成"看不见的账"。
---
## 七、与已有系统的衔接
### 7.1 载体判定(人工块显性化)
人工支出登记本身就是「这单是人工在做」的强证据。回填规则补充一条:**存在 approved 人工支出的订单项,其 `carrier_type` 判定为 `manual`**(与任务/工单存在性并列,冲突时以现有优先级为准并记异常)。
### 7.2 成本中心
- `cost_item_id` 即"付款项目",与 `cost_items`(`cost_type = 'manual_service'` 或 `'auto_purchase'`)打通;
- `approved` 时写 `cost_snapshots`(`source = 'manual_input'`),与自动发货/工单的标准成本并列进入订单损益;
- 匹配不到项目 → `cost_exceptions`,不默认 0。
### 7.3 影子总账(分录)
```text
approved(垫付) 借:5102 履约成本-人工客服 贷:2005 其他应付-垫付人
reimbursed(报销) 借:2005 其他应付-垫付人 贷:1001 银行存款
reversed(预留) 反向登记 + 反向凭证,原凭证不动
```
幂等事件键:`manual_expense:{expense_no}:approved|reimbursed|reversed`。
### 7.4 报表口径的两个坑(已在别处确认,实施时必须遵守)
1. **数量**:`order_items.quantity` 不可信(316,585 项里 316,578 项 = 1,而名称带「N个」的有 104,710 项,按名称解析总量是它的 **7.3 倍**)→ 支出若要按件摊,必须用 `normalized_quantity`。
2. **商品**:同一商品名对应多个 `sku_code`(「苏尔南钻石100颗」47 个码)→ 必须经 `channel_product_mappings` 归一到 `canonical_product_id`。
---
## 八、在全局路线中的位置
> 全局顺序以 [README.md](../../README.md) §三 为准。本文只负责“公司人工支出登记”,不重复商品、成本中心、销售事实和影子总账的完整设计。
| 本文内部阶段 | 内容 | 前置 |
| --- | --- | --- |
| **A. 登记表 + 入口** | `manual_expense_records` / `manual_expense_events`、订单详情「登记支出」卡片、订单列表入口与标签 | 成本中心基础表已建;`cost_item_id` 可为空,但空值进异常队列 |
| **B. 审批 + 财务页** | 状态机、审批/驳回/报销、财务页新类别、「支出登记」页签、异常队列 | A 完成 |
| **C. 成本快照 + 毛利** | `approved` 写 `cost_snapshots`,订单损益预览 | 成本中心 `cost_versions` / `cost_snapshots` 可用 |
| **D. 总账入账** | `approved` / `reimbursed` / `reversed` 生成影子总账事件 | 影子总账已落地 |
**客服备用金支出已经完成 A 的一部分(记录钱和用途),但没有审批、成本项、成本快照和总账事件。** 因此本轮不是重做客服台账,而是把公司口径补上并桥接。
---
## 九、待业务确认(实施前需拍板)
| # | 问题 | 建议默认 |
| --- | --- | --- |
| 1 | 审批人是谁? | 财务(`finance` 角色)审批,管理员兜底 |
| 2 | 单笔二级审批阈值? | ¥5,000 起需管理员 |
| 3 | 区分「垫付」与「公司直付」吗? | **不再区分**:人工支出一律从客服备用金账户扣款;公司直付不是默认路径 |
| 4 | 必须支持无订单支出吗? | **必须**(平台充值/赔偿/运费无处可记) |
| 5 | 允许一单多笔支出吗? | **必须** |
| 6 | 凭证必须吗? | **必须**(且支持多张) |
| 7 | 迟登记如何处理? | 24h 内正常,超时标记,超 7 天需审批 |
| 8 | 是否与现有 7 类财务流水合并展示? | 合并展示、分类筛选 |
| 9 | 人工支出与备用金支出的关系? | **人工支出登记时自动生成备用金支出并扣款**;不是从备用金页反向带入 |
| 10 | 实际付款人必须等于登录人吗? | 允许代付;实际付款人必填,默认登录人,便于按人日结对账 |
| 11 | `cost_item_id` 为空能否提交? | 可暂存/提交,但不得 `approved`;进“未归类”异常队列 |
| 12 | 人工成本按单还是按小时? | **已确认(2026-10-22):逐单按实际金额**,不按小时、不按固定单量/固定单价;工时只作可选分析字段 |
| 13 | 历史人工支出是否补录? | **不补录**,只取少量样本校准;上线后人工单必须 100% 有成本记录 |
| 14 | 电子凭证是否作为分类? | **不作为履约/成本分类**;只是快手核销码状态 |
---
## 十、一句话小结
**订单里已经有"卖了什么、卖了多少钱、什么时候卖"的全部事实;缺的只是"我们为了这单付出去多少钱、付给谁、谁垫的、有没有凭证"。把这一件事做成当天可填、必附凭证、不可篡改、能进财务页的登记表,人工块就从"系统盲区"变成"可查账的成本项"。**
@@ -0,0 +1,476 @@
# 客服备用金台账实施方案(2026-10-18)
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**已实施 P0**(迁移 100/101/113;代码已上线到代码库,未部署生产)。
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> **总口径以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准。**
> 背景:两位主管向客服发放备用金,客服用于充值、采购、赔偿等;目前全部走人工,无法监控每笔资金用途和每个客服余额。
> 结论:这不是报销系统,而是**客服备用金(周转金)台账**。P0 已完成“账户 + 流水 + 支出 + 对账”,不做复杂审批和总账。
---
## 〇、实际落地结果
| 项 | 实际实现 |
| --- | --- |
| 数据库 | `100_fund_accounts.sql`:`fund_accounts` / `fund_entries` / `manual_expenses` / `fund_reconciliations` |
| 显示名 | `101_admin_display_name.sql`:`admin_users.display_name`,备用金 `owner_name` 由触发器同步 |
| 后端 | `admin-fund-service.ts`、`routes/admin/fund.ts`;金额全部用分,写操作使用事务与行锁 |
| 前端 | `AdminFundPage.tsx`、`FundManagementView.tsx`、`FundSupportView.tsx`;管理端与客服端分流 |
| 权限 | `fund.view / fund.issue / fund.expense / fund.reconcile / fund.adjust / fund.export` |
| 审计 | 所有写接口接入 `createJsonHandler({ audit })` |
| 测试 | 最近全量校验(`5d1ba115`):后端 765 项,762 通过 / 3 跳过;前端 143 项 + build 通过 |
**与本文原方案的差异:**
1. 实际表名 `manual_expenses` 是**客服备用金支出明细**,不是后来财务方案里的公司人工支出表 `manual_expense_records`。
2. P0 未实现异常队列、批量发放/对账、导出、低余额提醒、大额支出主管确认;这些仍在 P1。
3. 客服账户姓名使用 `owner_name` 快照 + `admin_users.display_name` 触发器同步;历史审计 payload 不重写。
4. 支出凭证目前只校验数组非空,未校验对象存储 key 是否真实存在。
**后续衔接:**
- 财务侧公司人工支出登记使用 `manual_expense_records`,通过 `fund_expense_id` 关联本表的 `manual_expenses.id`。
- 新人工支出登记会**先扣客服备用金**、写 `fund_entries` 与 `manual_expenses`,再写 `manual_expense_records`,全程同一事务。
- 客服角色只能使用本人备用金账户;管理员/财务可代录到指定客服账户。
- 历史人工支出不补录,只取少量样本校准;上线后每一笔人工单必须有关联的备用金支出与成本快照。
- 备用金账户编号统一为 `FA-<大写用户名>`(如 `FA-KF_1`、`FA-JIAJIALI`),重名追加 `-2`、`-3`。
---
## 一、目标与非目标
### 1.1 目标
系统必须能回答四个问题:
1. 公司现在有多少钱在主管和客服手里?
2. 每个客服当前余额是多少?
3. 每一笔钱花到哪、为什么花、有没有凭证?
4. 系统余额和客服实际余额对不对得上?
### 1.2 非目标(P0 不做)
- 不做垫付/直付的复杂资金性质区分;
- 不做多级审批流;
- 不做成本中心、订单毛利、复式总账(这是备用金 P0 当时的边界;成本中心与销售事实已在后续迁移 `103–112` 完成);
- 不做平台账户独立钱包(当前口径:充值后立即用于订单/消耗,直接记支出);
- 不做多币种、发票、税务。
---
## 二、业务模型
```mermaid
flowchart LR
C["公司资金"] -->|拨付| S["主管资金池<br/>2 位主管"]
S -->|发放| A["客服备用金账户<br/>每人一个余额"]
A -->|支出| P["平台 / 供应商 / 客户"]
A -->|缴回| S
P -->|付款截图| E["支出记录"]
```
### 2.1 五类流水,缺一不可
| 流水类型 | 钱的方向 | 对客服余额 | 是否算支出 | 关键字段 |
| --- | --- | --- | --- | --- |
| 期初 | 上线初始化 | 增加 | 否 | 金额、主管确认 |
| 发放 | 主管 → 客服 | 增加 | 否 | 金额、主管、客服、时间、凭证 |
| 支出 | 客服 → 平台/供应商/客户 | 减少 | 是 | 金额、用途、收款方、平台账号、订单号(可选)、付款截图 |
| 缴回 | 客服 → 主管 | 减少 | 否 | 金额、主管、客服、时间、凭证 |
| 调整 | 主管/财务 → 客服 | 增加或减少 | 视原因 | 差异金额、原因、审批人、凭证 |
> 只有“支出”没有“发放/缴回/期初”,余额永远算不出来。
### 2.2 余额公式
```text
客服余额 = 期初 + 累计发放 + 累计调整 - 累计支出 - 累计缴回
主管池余额 = 期初 + 公司拨付 + 客服缴回 - 发放给客服 - 调整
```
余额只能由流水产生,不允许直接手工修改。
---
## 三、账户设计
### 3.1 主管资金池
- 两位主管各一个账户;
- 公司给主管拨付、主管发给客服、客服缴回,都记入主管池;
- 每个客服指定一个责任主管;
- 发放可以两位主管都操作,但每笔必须记录发放人;
- 对账由责任主管确认,避免“两个人都以为对方对过了”。
### 3.2 客服备用金账户
- 每个客服一个账户;
- 客服只能看到自己的余额和流水;
- 主管能看到自己名下客服的余额和流水;
- 财务/管理员能看到全部;
- 余额不足时不能继续支出,先由主管补拨。
### 3.3 平台账户
当前口径为 A:充值后立即用于订单/消耗,因此不单独建平台钱包。支出记录中保留“平台名称 + 平台账号”,用于追溯钱充到了哪里。
如果以后出现“充值后平台还有余额、以后继续用”的情况,再增加平台账户层,不混入客服余额。
---
## 四、数据模型
P0 四张表即可。
### 4.1 `fund_accounts` 资金账户
| 字段 | 说明 |
| --- | --- |
| id | 主键 |
| account_no | 账户编号,唯一;格式 `FA-<大写用户名>`(如 `FA-KF_2`、`FA-JIAJIALI`),重名追加 `-2`、`-3`;迁移 `113` 已回填历史随机编号 |
| account_type | `supervisor` / `agent` |
| owner_admin_user_id | 主管或客服的后台用户 ID |
| owner_name | 姓名快照 |
| responsible_supervisor_id | 客服的责任主管;主管账户为空 |
| balance_fen | 当前余额,分 |
| status | `active` / `frozen` / `closed` |
| created_at / updated_at | 时间 |
约束:
- 同一用户同一类型只能有一个账户;
- 余额不得为负;
- 冻结账户不能发放、不能支出,但允许缴回和调整。
### 4.2 `fund_entries` 资金流水
| 字段 | 说明 |
| --- | --- |
| id | 主键 |
| entry_no | 流水号,唯一 |
| account_id | 所属账户 |
| entry_type | `opening` / `funding` / `issue` / `expense` / `return` / `adjustment` |
| amount_fen | 有符号金额,正数流入、负数流出 |
| balance_after_fen | 记账后余额快照 |
| transfer_no | 转账批次号;发放/缴回的两条流水共用 |
| counterparty_account_id | 对方账户;支出时为空 |
| expense_id | 支出明细 ID;仅 `expense` 使用 |
| reconciliation_id | 对账调整关联 |
| occurred_at | 资金实际发生时间 |
| created_by_admin_user_id | 操作人 |
| evidence_json | 凭证文件 key、备注 |
| note | 说明 |
| created_at | 创建时间 |
约束:
- `amount_fen <> 0`;
- 余额只能通过流水变化;
- 发放、缴回必须同时写两条流水:主管负数、客服正数,或反过来;
- 支出必须同时写一条客服负数流水和一条 `manual_expenses`;
- 已记账流水不可修改、不可删除;更正走调整或反向流水。
### 4.3 `manual_expenses` 支出明细
| 字段 | 说明 |
| --- | --- |
| id | 主键 |
| expense_no | 支出编号,唯一 |
| account_id | 客服备用金账户 |
| amount_fen | 支出金额,分 |
| category | `platform_recharge` / `purchase` / `compensation` / `freight` / `other` |
| payee_type | `platform` / `supplier` / `customer` / `other` |
| payee_name | 收款方名称 |
| payee_account | 收款账号 |
| platform_name | 充值平台名称 |
| platform_account | 平台账号 |
| order_id | 关联订单,可空 |
| order_item_id | 关联订单项,可空 |
| paid_at | 付款时间 |
| evidence_json | 付款截图、凭证 |
| note | 用途/原因 |
| fund_entry_id | 对应资金流水,唯一 |
| created_by_admin_user_id | 登记人 |
| created_at / updated_at | 时间 |
约束:
- 金额 > 0;
- 用途、收款方、付款时间、凭证必填;
- 订单号可选;赔偿类建议关联订单;
- 同一客服 + 同金额 + 同收款方 24 小时内重复,提示疑似重复;
- 付款时间不能晚于当前时间;超过 24 小时补记标记“迟登记”。
### 4.4 `fund_reconciliations` 对账
| 字段 | 说明 |
| --- | --- |
| id | 主键 |
| account_id | 客服或主管账户 |
| period_start / period_end | 对账区间 |
| system_balance_fen | 系统余额 |
| actual_balance_fen | 实际余额 |
| difference_fen | 差异 |
| status | `pending` / `confirmed` / `discrepancy` |
| reason | 差异原因 |
| evidence_json | 盘点/截图 |
| confirmed_by_admin_user_id | 确认人 |
| confirmed_at | 确认时间 |
| created_at | 创建时间 |
规则:
- 差异为 0 才能直接确认;
- 差异不为 0 必须填写原因,并走调整流水;
- 调整必须由主管/财务/管理员操作,不能由客服自行改余额。
---
## 五、业务流程
### 5.1 上线初始化
1. 建立两位主管账户;
2. 建立每个客服账户,指定责任主管;
3. 主管确认每个客服的期初余额;
4. 期初余额写 `opening` 流水,备注“上线期初余额”;
5. 期初确认后,所有余额变化必须走流水。
### 5.2 公司 → 主管
管理员记录“拨付”,主管池增加。若公司层暂不管理,至少给主管池录期初余额。
### 5.3 主管 → 客服
主管选择客服、填写金额、付款方式、时间、凭证,提交后:
- 主管池减少;
- 客服余额增加;
- 生成两条 `issue` 流水,共用一个 `transfer_no`。
### 5.4 客服支出
客服填写金额、用途、收款方/平台、平台账号、订单号(可选)、付款时间、截图、备注,提交后:
- 客服余额减少;
- 生成一条 `expense` 流水;
- 生成一条 `manual_expenses` 明细。
人工支出登记使用同一条资金流:先扣客服余额、写 `fund_entries` 与 `manual_expenses`,再写 `manual_expense_records.fund_expense_id`。客服角色默认并强制使用本人账户。
### 5.5 客服缴回
客服或主管发起缴回,主管确认后:
- 客服余额减少;
- 主管池增加;
- 生成两条 `return` 流水。
### 5.6 对账
1. 系统显示当前余额;
2. 主管/客服盘点实际现金、微信、支付宝等余额;
3. 输入实际余额,系统计算差异;
4. 差异为 0 直接确认;不为 0 填写原因并走调整;
5. 调整后重新对平。
建议频率:每笔流水当天记,每天生成余额快照,每周主管与客服对一次账。
---
## 六、权限设计
新增权限点:
- `fund.view`:查看账户和流水;
- `fund.issue`:发放/缴回;
- `fund.expense`:登记支出;
- `fund.reconcile`:发起/确认对账;
- `fund.adjust`:调整余额;
- `fund.export`:导出。
角色建议:
| 角色 | 权限 |
| --- | --- |
| 管理员 | 全部 |
| 财务 | 查看全部、对账、调整、导出 |
| 主管(operator 或新增 `fund_manager`) | 查看名下客服、发放、缴回、对账 |
| 客服(support) | 登记自己的支出、查看自己的余额和流水 |
服务层还要做归属校验:
- 客服只能操作自己的账户;
- 主管只能操作自己名下客服;
- 发放人不能给自己发放;
- 调整必须由主管/财务/管理员操作;
- 所有写操作写审计日志。
---
## 七、页面设计
### 7.1 管理端“备用金”页
建议五个页签:
1. **总览**:公司总备用金、主管池合计、客服余额合计、本月支出、待对账人数、异常笔数。
2. **客服余额**:客服、责任主管、当前余额、本月发放、本月支出、本月缴回、最后交易、最后对账、状态。
3. **主管资金池**:主管、池余额、本月拨入、本月发放、本月收回、名下客服数、差异。
4. **支出明细**:时间、客服、金额、用途、收款方/平台、订单号、凭证、状态;支持筛选。
5. **对账与异常**:负余额、无凭证、长期未动、疑似重复、对账差异、超限额。
### 7.2 客服端“我的备用金”
- 当前余额;
- 发放/支出/缴回流水;
- “登记支出”按钮;
- “申请补拨”按钮;
- “申请缴回”按钮;
- 对账状态。
### 7.3 订单详情(可选)
订单详情增加“本单相关支出”小卡片,展示已关联的支出合计和明细。订单号不是必填,所以这个卡片只对已关联订单的支出生效。
---
## 八、接口设计
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/admin/fund/overview` | 总览 |
| GET | `/admin/fund/accounts` | 账户列表 |
| GET | `/admin/fund/accounts/:id` | 账户详情 + 流水 |
| POST | `/admin/fund/accounts/:id/funding` | 公司 → 主管拨付 |
| POST | `/admin/fund/accounts/:id/issue` | 主管 → 客服发放 |
| POST | `/admin/fund/accounts/:id/return` | 客服 → 主管缴回 |
| POST | `/admin/fund/accounts/:id/expenses` | 登记支出 |
| GET | `/admin/fund/expenses` | 支出明细 |
| GET | `/admin/fund/expenses/:id` | 支出详情 |
| POST | `/admin/fund/reconciliations` | 发起对账 |
| POST | `/admin/fund/reconciliations/:id/confirm` | 确认对账 |
| POST | `/admin/fund/reconciliations/:id/adjust` | 差异调整 |
实现要求:
- 所有金额用“分”存储,前端按元展示;
- 发放、缴回、支出必须在数据库事务中完成;
- 账户行使用 `SELECT ... FOR UPDATE`,避免并发导致余额错乱;
- 所有写接口使用现有 `createJsonHandler({ audit })` 写审计日志;
- 所有写接口做权限点和归属校验。
---
## 九、校验与异常
| 规则 | 处理 |
| --- | --- |
| 金额 <= 0 | 拒绝 |
| 余额不足 | 拒绝,提示先补拨 |
| 账户冻结 | 禁止发放/支出,允许缴回/调整 |
| 支出无凭证 | 拒绝 |
| 发放/缴回无凭证 | 允许备注+交接说明,但标记待补 |
| 收款方为空 | 拒绝 |
| 付款时间晚于当前 | 拒绝 |
| 付款时间早于当天 | 允许,标记迟登记 |
| 同客服+同金额+同收款方 24h 内重复 | 提示,需备注原因 |
| 对账差异未填原因 | 拒绝确认 |
| 调整无审批人 | 拒绝 |
异常队列:
- 负余额;
- 无凭证;
- 超过 7 天未对账;
- 余额长期不动;
- 单笔/单日支出异常;
- 同一凭证被多笔复用;
- 对账差异。
---
## 十、分期落地
### P0(先能记、能看、能对)
- 四张表 + 索引;
- 主管池、客服账户;
- 期初、发放、支出、缴回、调整五类流水;
- 支出明细 + 凭证上传;
- 总览、客服余额、主管池、支出明细、对账页面;
- 权限点 + 审计;
- 上线期初余额;
- 每周对账。
### P1(更好用)
- 异常队列和提醒;
- 余额上限、低余额提醒;
- 批量发放/批量对账;
- 导出;
- 大额支出主管确认;
- 移动端快速登记。
### P2(算得更清)
- 订单关联支出汇总;
- 支出分类与成本中心打通;
- 单均人工成本、店铺/客服维度报表。
### P3(财务总账)
- 复式记账;
- 与订单、钱包、提现、人工支出统一对账;
- 期间关账。
---
## 十一、上线步骤
1. 业务确认:两位主管、客服名单、责任主管、支出分类、期初余额截止时间。
2. 开发:迁移、后端台账服务、接口、前端页面、权限。
3. 初始化:建立账户,录入期初余额,主管逐人确认。
4. 试点:选 2 位主管 + 2~3 位客服,跑 1 周。
5. 校准:根据试点调整字段、分类、对账频率。
6. 全量:全部客服上线,关闭线下 Excel/微信群记账。
7. 运营:每周对账,每月复盘余额和支出异常。
---
## 十二、测试与验收
### 12.1 必测
- 发放:主管池减少、客服余额增加,两条流水金额相反;
- 支出:客服余额减少,支出明细与流水一一对应;
- 缴回:客服余额减少、主管池增加;
- 余额不足:拒绝支出;
- 并发发放/支出:余额不超扣、不出现负数;
- 权限:客服不能看别人,主管不能看非名下客服,客服不能调整;
- 对账差异:必须填原因并生成调整流水;
- 审计:所有写操作有日志。
### 12.2 验收标准
- 每个客服的当前余额可查;
- 每笔支出可追溯到人、金额、用途、收款方、时间、凭证;
- 主管池余额与发放/缴回流水对得上;
- 系统余额与客服实际余额的差异有记录、有原因、有审批;
- 不存在无流水导致的余额变化;
- 不存在负余额(除非明确开启允许负余额)。
---
## 十三、待确认
1. 两位主管的钱是公司资金,还是主管个人先垫?P0 先按公司资金池记,个人垫资在备注中说明。
2. 客服账号是否都是现有 `admin_users` 的 `support` 角色?如果是,直接绑定 `owner_admin_user_id`。
3. 客服余额是“一个总余额”,还是需要分微信/支付宝/现金多个钱包?P0 建议一个总余额 + 流水记付款方式。
4. 期初余额的截止时间和确认人。
5. 支出分类是否就用:平台充值、采购/拿货、客户赔偿、运费、其他。
6. 对账频率:每周一次是否足够,是否需要每日快照。
7. 大额支出是否需要主管事前确认;阈值多少。
@@ -0,0 +1,661 @@
# 成本中心与履约方式模型(文件名保留)
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**现行(P0 已实施,履约方式已统一)**。商品主数据、成本中心、成本快照、销售事实、人工支出、`fulfillment_mode` 均已落地;人工商品归一与全覆盖仍待实施。
> 最后校准:2026-10-23
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`
> 数据基线:本地生产快照克隆(数据截至 2026-10-14)
> **财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责商品、成本、销售事实的数据模型。
> 文件名保留是为了历史链接稳定,正文不再使用“三块”作为分类依据。
> 关联:
> - [项目全量分析报告-2026-10-17.md](../../项目全量分析报告-2026-10-17.md)
> - [影子总账说明.md](../../影子总账说明.md)
> - [财务系统集成方案.md](../../财务系统集成方案.md)
> - [人工支出登记落地方案-2026-10-17.md](../../人工支出与备用金.md)
---
## 〇、当前进度与边界
| 能力 | 状态 |
| --- | --- |
| 客服备用金支出(`manual_expenses`) | **已实施**;只解决客服备用金余额与用途,不等于公司人工支出登记 |
| 商品主数据 / 渠道 SKU 映射 | **已实施 P0**;迁移 `103` + 匹配键修正 `106`,后台「商品与成本」页可批量初始化、维护与回填历史订单项 |
| `cost_items` / `cost_versions` / `cost_snapshots` / `cost_exceptions` | **已实施 P0**;迁移 `104` + 修正 `105`,`107` 增加商品载体,`108` 增加业务时间与预估/实际替代 |
| 销售事实 / 订单毛利 | **已实施 P0**;迁移 `109`,`sales_daily_facts` + 后台「销售毛利」页,未归一商品单独成行 |
| 公司人工支出登记(`manual_expense_records`) | **已实施 P0**;迁移 `111`,支持登记/审批/驳回/报销;轻量人工 case 尚未实施 |
| 影子总账 | 未实施;必须在本文件的成本与销售事实之后 |
### 0.1 本期已实现边界
已实现:
- `product_catalog`、`channel_product_mappings`,以及 `order_items.canonical_product_id` / `normalized_quantity` / `revenue_amount_fen` / `revenue_basis`;
- 按 `provider + shop_id + platform_sku_code` 建映射,空 `shop_id` 为来源级映射、具体店铺优先;`provider` 必须与 `orders.provider` 一致(91 为 `91kaquan`,不是匹配层的 `kuaishou-91`,服务层会归一别名);
- 映射回填、SKU 文本数量回填与订单项收入固化(一单一品归整单,多品按等额拆分并标 `equal_split`);
- `cost_items` / `cost_versions` / `cost_snapshots` / `cost_exceptions`;
- 成本版本优先级 `rule > product > shop > provider > global`,按 `paid_at` / `accepted_at` 等业务时间解析;
- 订单项成本预览、预估快照、工单验收实际快照、缺失成本异常;
- `sync-cost-finance` 脚本与定时任务:批量回填快照并重算销售事实;
- `sales_daily_facts` 与后台「销售毛利」页,未归一商品单独成行,成本缺失显式告警;
- 后台「商品与成本」页、`finance.view` / `finance.config` 权限与审计。
本期不做:
- 订单进单时自动写 `canonical_product_id` / `normalized_quantity`(目前用后台回填);
- 上游 `cost_amount` 实时自动同步为 `cost_versions`(目前用价格导入脚本);
- 人工 case 在业务事务内自动生成成本快照;
- 退款按日冲减与退款金额字段;
- 影子总账。
---
## 一、先纠正口径:履约方式不是订单来源,也不是核销码
同一个来源的订单,可能走自动发货、接单平台、历史平台或人工。电子凭证只是快手核销码,不参与履约方式判定。
| 履约方式 | 实现 | 成本模型 | 监管重点 |
| --- | --- | --- | --- |
| `affiliate_dash` | affiliate-dash 自动履约任务 | 上游 SKU 固定成本版本 | 上游成本、店铺/日期/皮肤销量 |
| `work_order` | `work_orders` + 拼单份额 | 模板规则版本 / 工单实际报酬 | 发单价格、打手提现、售后追缴 |
| `legacy_platform` | `kuaishou_ct_assisted` / `kuaishou_feifei` 历史任务 | 历史成本未采集则显式标记 | 仅历史报表,不再新增 |
| `manual` | 无工单、无自动发货任务、无历史平台任务,人工处理 | 逐单实际金额(不按工时/单量) | 处理人、实际支出、备用金、凭证 |
硬不变式:
```text
一个订单项 = 订单骨架 + 可选核销码 + 一个履约方式
履约方式 ∈ { affiliate_dash, work_order, legacy_platform, manual }
voucher_state 是附加状态,不参与履约方式判定
```
当前真实数据:work_order 118,053 / affiliate_dash 41,676 / legacy_platform 53,901 / manual 102,955;四项相加 = 316,585。
> 人工 102,955 项中,11,349 项已有 `canonical_product_id`,但全部指向 `tpl:*` 接单模板商品——只说明 SKU 命中过模板,**不代表有工单**;成本模型仍按 `manual`。真正未归一的人工项是 91,606 项。
---
## 二、生产数据证据(2026-10-17 只读)
### 2.1 最近 30 天无载体订单
“无载体”定义:已支付订单项,既没有 `fulfillment_tasks`,也没有 `work_orders`。
| 来源 | 无载体订单数 | 无载体金额 | 说明 |
| --- | ---: | ---: | --- |
| 91 | **42,391** | 约 ¥624 万 | 几乎全部 `snapshot.isConfigured=false` |
| PDD | **2,258** | 约 ¥39 万 | 几乎全部 `isConfigured=false` |
| 抖音 | **8** | 约 ¥595 | 仅 8 单未命中,抖音分流已基本收口 |
补充:
- 2026-09 无载体订单项约 **37,708**;2026-08 约 **46,322**。这不是历史遗留,是持续发生。
- `manual_dispatch` 任务全库只有 **6 条**,全部为 `manual_review`。
- 这些无载体订单的 `claim_status` 全部为 `active`,没有完成信号。
- 已验收工单对应的订单,`orders.order_status` 仍然是 `paid`:**订单主状态不能代表履约完成**。
结论:**人工块不是少量兜底,而是每天上千单、金额很大的主业务块,但基本没有进入系统任务与状态机。**
### 2.2 接单成本占比
最近 30 天已验收工单:
| 来源 | 工单数 | 打手报酬 | 订单金额口径 | 报酬/订单金额 |
| --- | ---: | ---: | ---: | ---: |
| 91 | 60,902 | ¥235.9 万 | ¥274.4 万 | 86.0% |
| PDD | 1,289 | ¥20.3 万 | ¥25.4 万 | 80.0% |
| 抖音 | 478 | ¥2.14 万 | ¥2.21 万 | 96.5% |
**注意:91 的 `orders.total_amount` 来自 `maxAmount`(91 给的可接受金额/结算口径),PDD 来自 `order_amount`,抖音来自 `pay_amount`。三者不是同一财务口径,不能直接相加当收入。**
### 2.3 SKU 口径问题
最近 30 天,同一个商品名会对应多个 `sku_code`:
- 「指挥官密钥1个」:**27 个** `sku_code`
- 「1个精致荣耀专属礼盒」:10 个
- 「苏尔南钻石100颗」:35 个
因此:
- 不能直接按 `sku_code` 做“什么皮肤卖了多少”;
- 必须先建 `product_catalog` + `channel_product_mappings`,把平台 SKU 归一到 `canonical_product_id`;
- 数量也不能只读 `order_items.quantity`,要从 SKU 文本解析(如「指挥官密钥5个」)。
### 2.4 当前订单结构
最近 30 天,三个来源的订单基本都是 **1 个订单 1 个订单项**。这让第一版销售统计可以做简化,但模型必须按“订单项”设计,不能把“一单一品”写死。
---
## 三、统一商品维度:先解决“什么皮肤”
### 3.1 商品主数据
```text
product_catalog
id
canonical_sku -- 内部标准编码,统计/成本/报表唯一口径
product_name -- 展示名
category -- 皮肤/礼包/密钥/改名卡/其他
game -- 和平精英等
carrier_type -- 默认履约载体:affiliate_dash / work_order / manual / shared
status
attributes_json
```
`carrier_type`(迁移 `107_product_catalog_carrier.sql`)是后台「商品与成本中心」的分区维度:自动发货 / 接单平台 / 人工 / 共用。回填口径(2026-10-22 修正):只把**真正走 affiliate-dash 执行器**的商品算作 `affiliate_dash`——以 `fulfillment_tasks.executor_key='affiliate_dash'` 的履约证据为准,按 `context_json.skuCode` 建标准商品(`aff:<skuCode>`,生产快照 41,676 条任务 / 28 个 SKU);接单模板初始化出来的 `tpl:*` 商品全部是 `work_order`(662 个)。**渠道映射只代表平台 SKU 能归一到标准商品,不代表自动发货**。自动发货是单一上游渠道,订单项直接取最新 `fulfillment_tasks.context_json.skuCode` 归一为 `aff:<skuCode>`,**不写 `channel_product_mappings`**;页面「回填自动发货 SKU」会回填 `order_items.canonical_product_id` 并自动重算受影响日期的销售事实。人工/共用由后台按需手工调整;新增自动发货 SKU 可在页面点「同步自动发货商品」补建。
上游自动发货商品价格用 `apps/backend/scripts/sync-affiliate-dash-prices.ts` 批量导入(价格清单见 `apps/backend/scripts/data/affiliate-dash-prices.json`):按 `aff:<skuCode>` 补建/更新商品,并给指定成本项生成 `scope=product` 的按件版本;金额取上游 `cost_amount`(分),价格变化时先停用旧版本再插入新版本,历史快照不变。首次导入历史基线用 `--effective-from=2026-07-10`,脚本会把「金额相同但生效时间更晚」的版本回填到该日期。示例:`npx tsx scripts/sync-affiliate-dash-prices.ts --cost-item-id=5 --effective-from=2026-07-10`。
接单平台与人工客服各有一个系统成本项(迁移 `110_system_cost_items.sql`):
- `sys_work_order_reward`:662 个接单模板各生成一条规则级成本版本(634 条固定发单金额 + 28 条按件发单单价),全局 0 元版本只作「按工单实际结算」的占位。已验收工单按 `work_orders.reward_amount` 写 `actual`;未验收但已确定报酬的工单先按同一金额写 `estimated`(metadata `source=work_order_reward_committed`),验收后升级为 `actual`;取消工单不计成本。本地全量已回填 95,870 条 `actual` + 21,217 条 `estimated`,其中 111,831 条快照已关联到对应模板成本版本。
- `sys_manual_service`:人工支出登记通过后按登记金额写 `actual` 快照;成本版本先为 `draft` 占位,不参与预估,但允许人工支出使用实际金额入账。财务填写正式按单单价后可将版本改为 `active`。
### 3.2 渠道 SKU 映射
```text
channel_product_mappings
id
provider -- 落库值:91kaquan / pdd / douyin_mashida
shop_id -- 空串 = 来源级;具体店铺优先
platform_sku_code -- 平台原始 sku_code / productNo / code
platform_sku_name
match_type -- sku_code / sku_code_name / sku_code_series / sku_name_series
platform_sku_name_key -- 归一后的精确名称或数量系列键
canonical_product_id
quantity_multiplier -- 数量已在归一数量中体现,系列 SKU 固定为 1
confidence -- exact / rule / manual
source -- manual / import / rule
enabled
```
匹配优先级:
```text
店铺 + SKU 编码
> 来源 + SKU 编码
> 店铺 + SKU 编码 + 精确名称
> 来源 + SKU 编码 + 精确名称
> 店铺 + SKU 编码 + 名称系列
> 来源 + SKU 编码 + 名称系列
> 店铺 + 名称系列
> 来源 + 名称系列
```
匹配不到的商品进入 `product_mapping_missing` 异常队列,**不能直接拿原始 `sku_code` 当报表维度**。
#### 3.2.1 接单模板批量初始化(2026-10-18 修正)
662 个接单模板不允许逐个手工建商品。后台「从接单模板初始化」按以下规则批量生成:
1. **每个启用模板生成 1 个标准商品**,内部编码 `tpl:<rule_key>`;暂时没有历史工单的模板也先建商品。
2. **91 与快手 send-code 的历史工单直接命中模板**,以 `work_orders.product_rule_id` 作为映射的第一手证据,不再只看原始 `sku_code`。
3. 同一 `provider + shop_id + sku_code` 只命中一个模板时,生成 `sku_code` 精确映射。
4. 同一编码被多个模板复用时,继续用“编码 + 归一名称系列”和“编码 + 精确名称”消歧;只有唯一归属才生成映射。
5. 同一编码 + 同一名称在历史上仍命中多个模板时,**不选最新、不按次数猜**;跳过并列入冲突清单,前端在初始化预览中展示候选模板与涉及订单项数。
6. 同一来源 + 店铺内名称系列唯一时,额外生成 `sku_name_series` 兜底,供后续出现新编码时使用;它永远排在编码类映射之后。
7. 数量系列(1个/2个/5个/10个,秘钥/密钥等等价写法)归为同一个标准商品;数量写入 `order_items.normalized_quantity`,不能把“5个”再写成 `quantity_multiplier=5`,否则会重复乘算。
8. 重跑初始化时只清理 `source='import'` 且备注为“接单模板 … 自动初始化”的映射,并重置由这些映射带出的订单项 `canonical_product_id`;人工映射、成本版本、快照和真实订单/工单不删除。
本地生产快照验证结果(2026-10-18):
- 启用模板 662 个,自动生成/复用标准商品 662 个;
- 历史工单证据 112,451 个订单项;
- 分层映射 4,311 条,其中编码精确 2,302、编码 + 精确名称 325、编码 + 名称系列 160、名称系列兜底 1,524;
- 可唯一归一 86,533 个有证据订单项;另有 25,918 个订单项属于 32 组真实冲突,保持未映射;
- 全库实际归一 103,143 个订单项(包含可由名称系列兜底命中的无工单订单项)。
### 3.3 店铺维度
`shop_id` 是稳定键;`shop_name` 会变。报表以 `shop_id` 为准,展示名取最新或按时间快照。
---
## 四、统一成本中心
### 4.1 当前成本散落点
| 成本 | 当前来源 | 问题 |
| --- | --- | --- |
| 自动发货采购成本 | `affiliate-dash` 商品接口 `cost_amount` | 已落库:订单项按 `aff:<skuCode>` 归一,写 `estimated` 快照并在销售事实/成本版本页展示(2026-10-22) |
| 工单报酬 | `work_product_rules.reward_amount` / `unit_price_fen` / `sharing_unit_reward` | 价格与匹配规则混在一张表,改价无版本 |
| 工单实际成本 | `work_orders.reward_amount`、`share_reward` | 实际值有了,但没有统一成本项/版本 |
| 人工成本 | 订单界面「人工录单」→ `manual_expense_records` + `sys_manual_service` 快照 | 已落地逐单实际金额;**不按小时、不按固定单量/固定单价**;未录入的订单项留在成本缺失队列 |
| 平台费/支付费/售后损失 | 分散或未建模 | 无法算真实毛利 |
### 4.2 成本类型
| 类型 | 说明 | 典型单位 |
| --- | --- | --- |
| `auto_purchase` | 自动发货上游采购成本 | 按件 |
| `work_order_reward` | 接单平台打手报酬 | 按件/按单/按份 |
| `manual_service` | 人工客服成本 | 逐单实际金额(不按工时) |
| `platform_fee` | 平台服务费/佣金 | 按单/按比例 |
| `payment_fee` | 支付通道费 | 按单/按比例 |
| `after_sales_loss` | 售后损失/赔付 | 按单 |
| `shipping_other` | 其他履约费用 | 按单 |
### 4.3 成本表
```text
cost_items
id
cost_key
cost_type
name
unit -- per_item / per_order / per_hour / percent
currency
status
cost_versions
id
cost_item_id
amount_fen -- 固定金额;percent 类型用 rate_bps
rate_bps
effective_from
effective_to
scope_type -- global / provider / shop / product / rule
provider
shop_id
canonical_product_id
work_product_rule_id
source -- manual / upstream_sync / import
status -- draft / active / retired
change_reason
created_by
approved_by
```
### 4.4 成本快照
```text
cost_snapshots
id
order_id
order_item_id
work_order_id
manual_case_id
cost_item_id
cost_version_id
cost_type
quantity
unit_amount_fen
total_amount_fen
snapshot_stage -- estimated / actual / adjustment
resolved_at
metadata_json
```
规则:
- 改价只新增 `cost_versions`,不覆盖历史;
- 订单/工单/人工单创建或完成时写 `cost_snapshot`;
- 匹配不到成本时写 `cost_missing`,**不能默认 0**;
- 历史成本永远从快照读,不从当前成本表倒推。
### 4.5 “只改一个地方”的过渡方案
目标状态:
- 金额只在 `cost_versions` 改;
- `work_product_rules` 只保留匹配条件 + 计价方式 + `cost_item_id`;
- `channel_product_mappings` 引用自动发货 `cost_item_id`;
- 人工成本引用 `manual_service` 成本项;
- 业务表只保存 `cost_snapshot_id`。
过渡:
1. 给 `work_product_rules` 增加可空 `cost_item_id`;
2. 有 `cost_item_id` 走成本中心;
3. 没有则回退旧 `reward_amount/unit_price_fen`,标记 `legacy_cost_source`;
4. 659 条规则逐步迁移;
5. 全部迁移后旧字段变只读/删除。
这样不会一次性打断发单,又能逐步做到“成本改动只在一个地方”。
---
## 五、销售事实与店铺日报
### 5.1 销售事实表
```text
sales_daily_facts
stat_date -- Asia/Shanghai,按 paid_at 归属
provider
shop_id
canonical_product_id -- 允许为空:未归一桶
carrier_type -- affiliate_dash / work_order / manual / shared / unknown
order_count
item_count
quantity -- 归一化件数(SKU 文本解析后)
gross_revenue_fen
refund_amount_fen
net_revenue_fen
cost_amount_fen
cost_missing_count
cost_stage -- missing / partial / estimated / actual
gross_profit_fen
```
### 5.2 三个必须统一的字段
1. **商品**:`canonical_product_id`,不是原始 `sku_code`;为空表示未归一,报表单独成行,不能丢。
2. **数量**:`normalized_quantity`,不是简单的 `order_items.quantity`。
3. **收入**:`revenue_amount_fen` + `revenue_basis`,不是直接 `orders.total_amount`。一单一品归整单;多品当前按等额拆分,余数给最后一项。
收入口径(2026-10-22 已确认):
| 来源 | 当前字段 | 口径 |
| --- | --- | --- |
| 91 | `orders.total_amount` | 用户实付,已与快手 `paymentFen` 核验一致 |
| PDD | `orders.total_amount` | 用户实付 |
| 抖音 | `orders.total_amount` | 用户实付 |
### 5.3 退款日期
现在只有 `pay_status=refunded` 和 `updated_at`,不足以做按日净销售。`sales_daily_facts` 已预留 `refund_amount_fen`,但退款逻辑仍未接入;后续补 `refunded_at` / `refund_amount_fen` 或退款事件表。
### 5.4 统计实现
当前是增量重算表,不是物化视图:
```text
orders + order_items + channel_product_mappings + cost_snapshots
-> sales_daily_facts(按日期范围 DELETE + INSERT)
```
后台入口:`/admin/sales-profit`。回填脚本:`npm run finance:sync -- --days=14 --mode=all`;定时任务 `cost-finance-sync` 每 5 分钟处理最近 7 天。成本缺失会写入 `cost_missing_count`,毛利在补齐成本前只作参考。
数据校验:本地 2026-07-10 ~ 2026-10-14 共 316,597 个已支付订单,其中 12 单没有订单项(¥2,480.60),无法归集到商品/成本,销售事实会排除并在页面单独提示;其余 316,585 个订单项收入与订单总额完全对平。
全量回填后(2026-07-10 ~ 2026-10-10):接单平台 95,870 条 `actual`(¥3,773,401.32)+ 21,217 条 `estimated`(¥754,912.45);自动发货 41,676 条 `estimated`(¥4,974,059.00)。自动发货与接单平台收入归一率均为 100%。未归一 145,507 项 / ¥1,608.33 万按正确履约口径拆分为:**人工未归一 91,606 项 / ¥1,219.20 万**,历史平台 53,901 项 / ¥389.12 万。人工全量是 102,955 项,另外 11,349 项已归一但全部挂 `tpl:*` 商品,不代表有工单;不能把 91,606 当成人工全量。接单平台仍有 966 个订单项没有成本快照:601 个已取消工单 + 365 个零报酬未验收工单,保留在成本异常队列等业务确认,不自动记 0。
---
## 六、自动发货块
```text
订单项 -> 最新 fulfillment_task.context_json.skuCode
-> product_catalog.canonical_sku = 'aff:<skuCode>'
-> 上游商品成本 cost_amount -> cost_snapshot
-> 履约完成 -> sales_daily_facts
```
要点:
- **不建渠道映射**:自动发货只有 affiliate-dash 一个上游渠道,订单项直接从履约任务 SKU 归一;`channel_product_mappings` 只服务接单平台。
- 回填入口:后台「自动发货 SKU」→「回填自动发货 SKU」,接口 `POST /api/v1/admin/cost-center/affiliate-products/backfill`。按 `order_item_id` 取最新任务 SKU,只更新有变化的订单项,完成后自动重算受影响日期的销售事实;返回 `updatedCount / skippedCount / remainingCount`。
- 列表的「履约任务 / 最近履约」实时取自 `fulfillment_tasks`,不读会被价格导入覆盖的 `product_catalog.attributes_json`。
- 上游商品接口已有 `price_amount` / `cost_amount`,价格由 `sync-affiliate-dash-prices.ts` 落到成本中心。
- `price_amount` 是售价/积分,`cost_amount` 是成本,不能混用。
- 成本变化生成新版本;变动超阈值告警/审批。
- 店铺/日期/皮肤统计必须走 `canonical_product_id`,不能按原始 `sku_code`。
- 统计件数要解析 SKU 文本中的数量。
实测(本地 2026-10-22):41,676 个自动发货订单项全部回填,未归一收入 ¥5,089,712.26 → ¥0.00;采购成本 ¥4,974,059.00(毛利率 2.27%)。剩余未归一 145,507 项 / ¥1,608.33 万,其中**人工未归一 91,606 项 / ¥1,219.20 万**,历史平台 53,901 项 / ¥389.12 万;人工全量为 102,955 项。人工需要逐单实际成本,历史平台需要历史结算口径。
---
## 七、接单平台块
```text
订单项 -> 命中接单模板 -> work_order
-> 创建/发布/抢单/验收 -> 工单报酬 -> cost_snapshot
-> 押金冻结/释放/没收 -> 影子总账(负债,不直接进成本)
-> 提现/售后/欠款 -> 影子总账
```
要点:
- 工单报酬是主成本;押金是负债,不是成本。
- 模板改价必须生成 `cost_versions`,不能直接改历史工单。
- 工单实际结算金额写入 `cost_snapshot(actual)`;未验收但已确定报酬的工单先写 `actual` 以外的 `estimated`,验收后升级。
- 商品归一入口:后台「接单平台」→「按工单回填商品」,接口 `POST /api/v1/admin/cost-center/work-order-products/backfill`。优先按工单真实使用的 `work_orders.product_rule_id` 指向 `tpl:<rule_key>`;仍无法归一的 341 个长尾订单项,再按 `provider + shop_id + sku_code + normalized_sku_name` 自动补 45 个 `wo:` 标准商品和 115 条 `sku_code_name` 渠道映射。两次回填后接单平台归一率 100%。
- 成本版本入口:后台「接单平台」→「成本版本」→「同步模板发单金额」,接口 `POST /api/v1/admin/cost-center/work-order-template-costs/sync`。662 个模板各生成一条规则级版本(634 固定金额 + 28 按件单价),页面同时显示该模板的「用户实付均价」,用于对比用户付款与发单金额;全局 0 元版本只作实际结算占位。
- 成本异常:966 个订单项没有成本快照(601 个已取消工单 + 365 个零报酬未验收工单),保留在异常队列;取消/零报酬不等于 0 成本,不自动写 0。
- 删除模板:历史成本版本会脱钩并停用,`cost_snapshots` 继续引用原版本,保证历史毛利可追溯;不会因为外键把模板卡死。
- 影子总账负责收入/成本/押金/提现/售后的复式记录;成本中心负责“金额是多少、用了哪个版本”。
本地实测(2026-10-22):接单平台 118,053 个订单项全部归一;未归一收入 ¥775,641.19(14.54%)→ ¥0.00。成本 95,870 条 `actual`(¥3,773,401.32)+ 21,217 条 `estimated`(¥754,912.45),合计 ¥4,528,313.77;销售收入 ¥5,335,734.45,毛利率 15.13%。成本版本从 39 条增至 701 条(38 自动发货商品 + 662 模板规则 + 1 全局占位)。
生产数据提示:91/PDD/抖音已验收工单的报酬/订单金额分别约 86%/80%/96.5%。在收入口径未确认前,这个比例只能作为量级参考,不能直接当毛利率。
---
## 八、人工客服块(当前最需要补的)
> **口径确认(2026-10-17)**:业务明确人工块当前**不以 SLA 监管为目标**,第一优先级是
> **人工支出可追溯** ——「主要是把流程搞清楚:哪个人处理了哪些东西、付了多少钱。现在人工完全监管不到,
> 很多地方需要人工付款,也不知道付款项目、金额、订单号」。
> 因此本章顺序调整为:**① 人工支出登记 → ② 处理人可见(轻量 case) → ③ SLA 与监管指标(延后)**。
> DDL 见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](../../drafts/2026-10-17-人工支出登记-DDL草案.sql)。
>
> **2026-10-22 进度**:客服备用金支出已实施(`fund.manual_expenses`),解决“客服手里有多少钱、这笔钱花到哪”;
> 公司口径的人工支出登记(`manual_expense_records`)也已实施(迁移 `111`),并在订单界面提供「人工录单」逐单录入实际成本;
> 后者通过 `fund_expense_id` 关联前者,不重复做客服余额系统。轻量人工 case 仍未实施。
### 8.1 问题本质
不是“客服效率低”,而是**人工单没有进入系统状态机**,且**人工付款完全没有台账**:
- 没有 case;
- 没有处理人;
- 没有开始/完成时间;
- 没有交付证据;
- 没有人工成本;
- 订单完成状态无法回写;
- **付款项目、金额、关联订单号、付款人、凭证全部缺失** —— 这是当前最直接的损失点。
### 8.2 载体判定
建议在订单项上增加显式字段:
```text
carrier_type: affiliate_dash / work_order / manual
carrier_status: pending / active / completed / failed / cancelled
carrier_decided_at
carrier_reason
```
判定:
1. 命中自动发货 -> `affiliate_dash`
2. 命中接单模板 -> `work_order`
3. 都没有 -> `manual`
4. 宽限期后仍无载体 -> 自动创建人工 case
### 8.3 人工 case 模型
> 落地版见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](../../drafts/2026-10-17-人工支出登记-DDL草案.sql):已去掉 `due_at`/优先级等 SLA 字段,先只保留「谁在处理哪个订单项 + 结果 + 证据 + 支出登记」。下表为完整形态(阶段③)。
```text
manual_fulfillment_cases
id
case_no
order_id
order_item_id
provider
shop_id
canonical_product_id
quantity
priority
status
assignee_admin_user_id
assigned_at
due_at
started_at
completed_at
outcome -- delivered / refunded / failed / cancelled
delivery_reference
evidence_json
cost_snapshot_id
notes
manual_case_events
id
case_id
event_type
actor_type
actor_id
payload_json
created_at
```
状态:
```text
pending_assign -> assigned -> processing -> waiting_customer
-> pending_review -> completed / failed / cancelled
```
### 8.4 监管规则(阶段③,延后)
- 每个无载体订单必须有 case;
- 完成必须填 outcome + 交付凭证/参考号;
- 失败必须填原因并决定重试/退款/升级;
- 高价值订单主管复核;
- 超时自动升级;
- 所有状态变更写 `manual_case_events`,不可改;
- 客服不能私下改价/退款,必须走审批。
### 8.5 人工成本(已落地:逐单、按实际金额)
> **业务口径(2026-10-22 确认)**:人工成本**不能按小时,也不能按固定单量/固定单价**——每一单的实际付款都不一样。
> 必须**先关联订单号,再逐单录入这一单实际付出去的钱**。
> **资金来源(2026-10-23 确认)**:人工支出实际上都走客服备用金,登记时必须选择备用金账户;**客服角色只能用自己的账户(默认自动带入),管理员/财务可代录到指定客服账户**。
- 入口在订单界面:订单列表对人工单显示「人工录单」,订单详情始终有「人工录单」;弹窗锁定订单号、选择**备用金账户**,订单项多于 1 个时由人工选择。
- 接口 `POST /api/v1/admin/finance/manual-order-costs`:同一事务内扣减备用金余额、写 `fund_entries` 与 `manual_expenses`,再写 `manual_expense_records`(成本项固定 `sys_manual_service`,`fund_expense_id` 必填),金额即本单实际支出;余额不足直接拒绝。
- 有审核权限(`admin` / `finance` / `finance.review`)且选择立即生效时,当场审批并写 `actual` 快照;否则留在 `submitted`,由财务在「人工支出」页审核。
- 快照以 `subject_type='manual_case'`、`subject_id=expense.id` 写入并携带 `order_item_id`;销售事实按 `cost_snapshots.order_item_id` 汇总,同一订单项的多笔支出(渠道费 + 加急费 + 补差价)累加。
- 工时只作为后续可选的分析字段,不参与成本计算;不设「按单固定单价」。
- **历史人工支出不补录**,只取少量样本做校准;历史缺口在报表中标记“历史未采集”,不计入上线后的监控缺口。
- **上线后人工单必须 100% 有成本记录**:人工单无支出、支出无 `fund_expense_id`、支出与备用金流水不一致,均不允许关账。
- 未录入成本的订单项保持 `cost_missing`,进入成本异常队列,不默认记 0。
### 8.6 监管指标(阶段③,延后)
- 待处理量、平均等待、最长挂起;
- 客服处理量、平均处理时长、完成率;
- 退款率、投诉率、返工率;
- 单均人工成本;
- 无凭证完成率;
- 同一买家/客服异常重复;
- 高价值订单抽样复核。
### 8.7 与现有任务系统的关系
现有 `manual_dispatch` 任务只有 6 条,且缺少 assignee/SLA/证据/成本字段。建议:
- 新人工 case 作为主模型;
- 如果存在对应 `manual_dispatch` 任务,完成时桥接现有完成接口;
- 不把所有人工作业强行塞进 `fulfillment_tasks`。
---
## 九、履约方式统一数据流
```mermaid
flowchart LR
O["订单项"] --> C["统一履约判定 fulfillment_mode"]
C -->|有自动任务| A["affiliate_dash"]
C -->|有工单| W["work_order"]
C -->|历史 ct/feifei| L["legacy_platform"]
C -->|都没有| M["manual"]
A --> CS["cost_snapshots"]
W --> CS
L --> CS
M --> CS
CS --> F["sales_daily_facts / 毛利"]
A --> GL["影子总账"]
W --> GL
L --> GL
M --> GL
P["product_catalog + channel_product_mappings"] --> F
V["cost_items + cost_versions"] --> CS
Q["voucher_state(可选核销码)"] -.不参与判定.-> C
```
---
## 十、分期落地
### P0:商品与销售事实(先能做报表)——已实施
- `product_catalog`(迁移 `103/107`)
- `channel_product_mappings`(迁移 `103/106`)
- `sales_daily_facts`(迁移 `109`,日期范围重算表)
- 数量归一、收入固化;退款日期字段仍待补
### P1:成本中心影子期——已实施运行层
- `cost_items` / `cost_versions` / `cost_snapshots`(迁移 `104/105/108`)
- 自动发货接固定成本与价格导入
- 接单平台:系统成本项 `sys_work_order_reward` 在工单验收后按 `reward_amount` 直接写实际快照
- 人工客服:系统成本项 `sys_manual_service` + 人工支出登记通过后写实际快照;正式单价版本待财务填写
- 缺失成本进异常队列
### P2:人工全覆盖与轻量处理记录——支出登记已完成,轻量 case 未实施
- `fulfillment_mode` 已统一(迁移 `114`):人工只在无工单、无自动任务、无历史平台任务时出现
- 无载体自动建轻量 case(谁在处理哪个订单项)
- **公司人工支出登记**:付款项目 / 金额 / 订单号 / 付款人 / 收款人 / 付款方式 / 凭证 / 审批
- 登记时强制从客服备用金扣款,并写 `fund_expense_id`;客服只能使用本人账户
- **历史人工支出不补录**,只取少量样本校准;上线后人工单必须 100% 有成本记录
- 人工支出进成本快照与影子总账(借:履约成本-人工,贷:备用金/其他应付)
- 延后(阶段③):SLA、超时升级、主管复核阈值、监管指标
### P3:影子总账
- 订单收入、工单成本、押金、提现、寄卖、售后
- 日对账、差异清单
- 连续对平后转正
---
## 十一、业务口径确认结果(2026-10-17)
### 11.1 已确认
| # | 问题 | 结论 | 依据 |
| --- | --- | --- | --- |
| 1 | 91 的 `orders.total_amount` 是收入还是成本上限? | **收入**:`total_amount` 就是用户付款金额,与快手发码配合,计入收入 | 业务确认;数据核验:91 已支付 312,569 笔中 235,271 笔含快手 `paymentFen`,其中 235,268 笔与 `total_amount` **完全一致(100.00%)**,仅 3 笔差 ¥1.80~2.00 |
| 2 | 打手报酬是成本还是代收代付? | **支出/成本**:工单结算后打手会提现,我方实际付款给他们 → 按**总额法**(收入记用户实付全额,报酬记履约成本) | 业务确认 |
| 3 | 人工块的目标是什么? | **先做流程可见与支出可追溯**(谁处理了哪些、付了多少钱、什么项目、关联订单号、凭证),SLA/监管指标延后 | 业务确认 |
| 4 | 退款/仲裁怎么冲减? | **暂未接入,先预留**:字段、反向冲正关系与事件类型先留好,本轮不实现逻辑 | 业务确认 |
| 5 | 收入确认时点? | **2026-10-22 确认:先按 `paid_at` 归属销售日**;成本在快照生成后回填到同一销售日,后续再评估验收/售后期口径 | 业务确认 |
| 6 | 未归一商品怎么进报表? | **2026-10-22 确认:允许 `canonical_product_id` 为空,单独成“未归一”行**,保证总收入与订单总额对平 | 业务确认 |
| 7 | 人工成本怎么归集? | **逐单、按实际金额**:关联订单号录入本单实际付款,**不按小时、不按固定单量/固定单价**;同一订单项多笔可累加 | 业务确认(2026-10-22) |
| 8 | 电子凭证算不算履约类型? | **不算**:电子凭证只是快手核销码,订单仍按工单/自动任务/历史平台/人工判定 | 业务确认(2026-10-23) |
| 9 | 历史人工支出是否补录? | **不补录**,只取少量样本校准;上线后人工单必须全覆盖 | 业务确认(2026-10-23) |
| 10 | 历史 ct_assisted/feifei 怎么处理? | **单列 `legacy_platform`**,不混入人工;历史成本未采集则显式标记 | 业务确认(2026-10-23) |
### 11.2 仍待确认
1. 自动发货的 `cost_amount` 是否稳定、是否含平台积分/汇率?
2. 平台服务费/支付通道费由谁承担、结算周期与账单口径?
3. 寄卖货款的采购成本确认时点?
---
## 十二、分析基线与克隆现状(2026-10-17 更新)
生产只读聚合(`SET default_transaction_read_only = on`)已完成,无写入、无改表、无改数据。
**现有基线**:本机已存在可反复使用的**生产快照克隆**(本地 docker `order_site-postgres-1`,`orders` 316,597 行,数据截至 2026-10-14),
因此**后续 SQL 迭代一律打本地克隆,不必再连生产**;需要更新时再取新快照。
本机备份目录已收敛:`backups/` 及子目录 `700`、两份 dump `600`,并删除了一个残留临时 dump(137 MB)。
如需长期保留留档,建议只保留一份最新 dump,并确认磁盘加密。
---
## 十三、一句话结论
**自动发货解决“固定成本 + 店铺/日期/皮肤销量”,接单平台解决“变动成本 + 打手结算”,历史平台只做历史报表,人工解决“逐单实际支出 + 处理人可见 + 备用金扣款”;四者共用一套商品主数据、成本中心和销售事实表,成本改价只发生在 `cost_versions` 一个地方。电子凭证只是核销码,不是分类。**
@@ -0,0 +1,140 @@
# 财务/履约人工块修复总结(交接用)
> **归档说明(2026-10-23):** 本文是重写前的版本,仅作历史追溯,**不得作为实现依据**。结论已并入现行文档,见 [文档索引](../README.md)。
> 状态:**交接摘要**。详细口径、迁移 SQL、代码清单与验收查询以 [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md) 为准。
> 代码基线:`dev @ 5d1ba115 + 迁移 103–114`。
> 数据基线:本地生产快照克隆,数据截至 2026-10-14。
> 分支:`dev` 领先 `main` 20+ 个提交;迁移 `103–114` 未部署生产;本轮只做本地提交,暂不推送。
---
## 一、这次到底修了什么
1. **人工成本改为逐单、按实际金额**:订单页「人工录单」直接关联订单项,不再按小时、固定单价或固定单量估算。
2. **人工支出改为从客服备用金扣款**:登记时同一事务扣余额、写 `fund_entries`、写 `manual_expenses`、回填 `manual_expense_records.fund_expense_id`。
3. **客服只能用自己的备用金账户**:客服角色默认自动带入本人账户,传别人账户后端直接 403;管理员/财务可代录。
4. **备用金账户编号可读**:统一为 `FA-<大写用户名>`,重名追加 `-2/-3`;迁移 `113` 已回填本地历史账户。
5. **订单页只展示平台判断 SKU**:人工录单弹窗不再混入内部规格文本。
6. **履约方式统一落库**:新增 `order_items.fulfillment_mode`,订单页、成本快照、销售事实只读这一个字段。
7. **核销码与履约方式彻底拆开**:快手凭证/抖音券只作为 `voucherState` 展示,不再把 91 人工单判成 `voucher_only`。
8. **历史平台成本异常归位**:53,901 条 open 异常从 `manual` 修正为 `legacy_platform`,不再污染人工账单。
## 二、关键事实(先记住这几个数)
| 履约方式 | 订单项 | 收入 | 当前成本 |
| --- | ---: | ---: | --- |
| `work_order` 接单平台 | 118,053 | ¥533.57 万 | 缺 966 项 |
| `affiliate_dash` 自动发货 | 41,676 | ¥508.97 万 | 完整 |
| `legacy_platform` 历史平台 | 53,901 | ¥389.12 万 | 历史未采集 |
| `manual` 人工 | 102,955 | ¥1,219.20 万 | 仅 1 条 ¥320 |
| **合计** | **316,585** | **¥2,650.87 万** | 成本 ¥950.27 万 |
两个容易写错的数字:
- **人工全量 = 102,955 项**;
- **人工未归一 = 91,606 项**(另外 11,349 项挂在 `tpl:*` 模板商品,只表示 SKU 命中,不代表有工单)。
## 三、电子凭证的正确位置
快手电子凭证只是**核销码**,不是履约方式,也不是成本类型。
真实数据:91/快手人工单 100,618 项,**全部有凭证记录**(`CONSUMED` 87,310 / `DESTROYED` 13,247 / `UNUSED` 61),但它们仍然是人工单。
旧代码的根因(已在 P0 删除):
```text
vouchers.length > 0 || provider === '91kaquan' -> voucher_only
provider === 'douyin_mashida' -> 自动发货
```
这两条兜底会把人工单从订单页隐藏。现在只读 `order_items.fulfillment_mode`,核销码单独返回 `voucherState`。本地联调:带已核销快手凭证的 91 人工单返回 `manual + consumed`,不再变成 `voucher_only`。
## 四、现在不要做什么
- 不要补录历史人工支出:历史只取 20–30 单/来源做区间校准,不写 `cost_snapshots`。
- 不要用均价、工时或固定单价补齐人工成本。
- 不要把 `manual_expenses` 改名或复用为 `manual_expense_records`。
- 不要用 `product_catalog.carrier_type` 反推履约方式。
- 不要在人工商品归一前重算历史总账。
- 不要把 `legacy_platform` 混回人工。
- 不要再用 `voucher_only` 或 provider 兜底判履约;P0 已统一到 `fulfillment_mode`。
## 五、下一步只做 P1:人工商品归一
完整口径与验收见 [总纲 §八、§九](../../财务与履约口径总纲-2026-10-23.md#八p0-实施记录统一履约事实)。
P0 已完成(本地):
1. 迁移 `114`:`fulfillment_mode` / `fulfillment_decided_at` / `fulfillment_reason` + 索引;
2. 同时放宽 `product_catalog`、`cost_items`、`cost_snapshots`、`sales_daily_facts` 的 `carrier_type` CHECK;
3. 316,585 项回填完成:`work_order 118,053 / affiliate_dash 41,676 / legacy_platform 53,901 / manual 102,955`;
4. 53,901 条 open 成本异常由 `manual` 修正为 `legacy_platform`;
5. 订单页、成本快照、销售事实全部只读 `fulfillment_mode`;核销码改为独立状态;
6. 定时任务每 5 分钟兜底归集,失败不阻断下单。
P1 下一步:
1. 人工单按 `spec.skuId` 建 `man:<skuId>` 商品,PDD 用 `sku_code/sku_name` 兜底;
2. 复核 11,349 个仍挂 `tpl:*` 的人工订单项,确认是保留商品身份还是改挂 `man:*`;
3. 人工录单保持逐单实际金额,24 小时内录入;
4. 验收:人工单无成本记录 = 0(上线后区间)。
## 六、上线后运营(硬要求)
- 人工单支付后 **24 小时内**必须录成本;
- 客服只能用自己的备用金账户,余额不足直接拒绝;
- 每日 10:00 检查:
- 超过 24 小时仍未分类的订单项 = 0;
- 人工单无成本 = 0;
- 人工支出无 `fund_expense_id` = 0;
- 人工支出与备用金流水金额不一致 = 0;
- 上述任一不为 0,**不允许关账**;
- 历史人工单标记 `historical_missing`,不计入上线后缺口。
## 七、验证命令
```bash
# 后端
cd apps/backend && npm run check
# 前端
cd apps/frontend && npm run check
# 文档相对链接(在仓库根目录)
python3 - <<'PY'
import os,re
files=[]
for base,_,names in os.walk('docs'):
for n in names:
if n.endswith('.md'): files.append(os.path.join(base,n))
missing=[]
for f in files:
text=open(f,encoding='utf-8').read()
for m in re.finditer(r'(?<!\!)\[[^\]]*\]\(([^)]+)\)',text):
link=m.group(1).strip()
if link.startswith(('http://','https://','mailto:','#')): continue
p=link.split('#')[0]
if p and not os.path.exists(os.path.normpath(os.path.join(os.path.dirname(f),p))):
missing.append((f,link))
print('missing',len(missing))
for x in missing: print(x)
PY
```
## 八、文档阅读顺序
1. [财务与履约口径总纲-2026-10-23.md](../../财务与履约口径总纲-2026-10-23.md):唯一口径 + P0 执行清单。
2. [成本中心与履约方式模型.md](../../成本中心与履约模型.md):商品、成本版本、快照、销售事实。
3. [人工支出登记落地方案-2026-10-17.md](../../人工支出与备用金.md):人工录单字段、审批、权限。
4. [客服备用金台账实施方案-2026-10-18.md](../../人工支出与备用金.md):备用金账户与流水。
5. [财务系统集成方案.md](../../财务系统集成方案.md) / [影子总账说明.md](../../影子总账说明.md):后续总账层。
6. [README.md](../../README.md):文档索引与分支状态。
## 九、交接注意
- 生产部署脚本 `deploy/ubuntu-deploy.sh` 不会自动 `git pull`;部署前必须先拉代码。
- 生产域名变更要同步 DCAuth `appRegistry`,否则客户端会报“不允许访问”。
- 成本映射的 `provider` 用 `orders.provider = 91kaquan`,不是匹配层的 `kuaishou-91`。
- 显式单号检索必须忽略排他筛选,这是前后端共同约定。
- Git 操作必须先获得用户确认;当前只保留本地提交,不推送、不部署。