Files
order_site/docs/人工支出与备用金.md
yml2213 62763ddf0c feat(finance): 支持代录指定客服账户,并上线影子总账/对账/关账/快手平台退款
- 人工确认履约:管理员/财务/运营可传 fundAccountId / creditOwnerAdminUserId
  指定客服备用金/积分账户;客服仍强制本人账户
- 新增迁移 112:finance_accounts / journal_entries / journal_lines /
  periods / reconciliations / reconciliation_items,种子 11 个科目
- 影子总账:销售事实按日生成收入/退款/成本凭证,支持手工凭证、试算平衡;
  不接管钱包余额;总账同步接入定时任务与退款重算
- 期间关账/反关账:关账先补凭证并校验试算平衡,closed 期间禁止新增凭证
- 平台对账:JSON 账单导入,按 provider + platform_order_id 匹配订单/退款,
  标记 matched/amount_mismatch/missing_order/missing_refund/duplicate
- 平台退款:快手行业(91)同意退款 API 接入,成功后落 source=platform;
  抖音/拼多多返回 501,保留手工退款
- 新增后台「财务 → 总账与对账」页面(总账/期间/对账三个 Tab)
- 集成测试扩展到代录、总账平衡、关账阻断、手工凭证、对账匹配

验证:backend check 844(840 pass / 4 skip);frontend check 172 + build;
迁移 112 在 scratch 库全量执行并重跑幂等;本地生产副本已升级到 112,
销售事实同步 56 条凭证,借贷合计 1,885,351,864 = 1,885,351,864。
2026-10-26 13:32:47 +08:00

208 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 人工支出与客服备用金
> 状态:**现行(P0 已实施,待部署生产)**。
> 最后校准:2026-10-26(人工确认改单事务、subject_id 归一、配置自动匹配、人工缺成本入队列)
> **2026-10-24 变更**:独立「人工履约」页面已下线,订单列表/详情里的「人工录单」改为「确认履约」;`/finance/manual-order-cost*` 接口已删除。资金链路不变(扣积分/备用金 → 写 `manual_case` 快照)。
> 代码基线:`dev @ 43f402cf + 迁移 100/101/104/107`(已合并到 `dev`,**未推送、未部署生产**)
> **业务口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责人工录单的字段、流程、权限与备用金台账。
> 本文合并自原《人工支出登记落地方案-2026-10-17.md》与《客服备用金台账实施方案-2026-10-18.md》。
---
## 〇、只有一条链路:确认履约(原人工录单)
2026-10-23 口径调整:**公司人工支出登记单与审批报销状态机整链下线**。人工成本现在只有一条路径。
```text
订单列表/详情「确认履约」
→ 扣客服积分或备用金余额(写 fund_entries / platform_credit_entries)
→ 写 cost_snapshots(subject_type='manual_case',subject_id=order_item_id)
```
| 表 | 业务含义 | 状态 | 关键字段 |
| --- | --- | --- | --- |
| `manual_expenses` | **客服备用金支出**:客服从自己的备用金账户里充值/采购/赔偿 | 已实施(迁移 `100`) | `account_id` / `fund_entry_id` / `category` / `payee_*` / `order_item_id` / `evidence_json` |
| `cost_snapshots` | **人工成本的唯一落点** | 已实施(迁移 `104` + 完整性兜底 `111`) | `subject_type='manual_case'` / `subject_id=order_item_id` / `total_amount_fen` / `snapshot_stage='actual'`;资金来源写 `metadata_json.fundExpenseId / creditEntryId` |
结论:
1. **没有单独的登记单表**。`manual_expense_records` 与 `manual_expense_events` 已随迁移 `104` 归并下线。
2. **没有审批、没有报销**。订单页「人工录单」一次操作完成"扣钱 + 记成本",录入即计入毛利。
3. 客服侧收口的是"备用金余额与用途":钱从谁的备用金出、付给谁、有没有凭证,全在 `manual_expenses` + `fund_entries`。
4. 客服角色只能用本人备用金账户(后端越权直接 403);管理员/财务可代录到指定客服账户。
5. 余额不足直接拒绝,**不允许负余额**。
6. 历史人工支出找不回来,**直接放弃**:不补录、不做样本回填、不做均价估算。
---
## 一、核心口径
1. **人工成本逐单、按实际金额。** 不按小时、不按固定单量、不按固定单价。
2. **人工支出从客服备用金账户扣款。** 余额不足直接拒绝,不允许负余额。
3. **客服角色只能使用自己的备用金账户。** 后端越权直接 403;管理员/财务可代录到指定客服账户。
4. **历史人工支出找不回来,直接放弃。** 不补录、不做样本校准;历史缺口只计入 `cost_missing_count`,不阻塞上线。
5. **上线后每一笔人工单必须 100% 有成本记录**,否则不允许关账。
6. **录入必须"发生在垫付当天"才有监管价值**;事后补记只能算账,不能算监管。
7. **客服备用金是周转金台账,不是报销系统。** P0 只做"账户 + 流水 + 支出 + 对账",不做复杂审批和总账。
8. **人工录单一录到底**:没有登记单、没有审批、没有报销。钱与成本在同一次操作里落地。
---
## 二、客服备用金台账
### 2.1 落地结果
| 项 | 实际实现 |
| --- | --- |
| 数据库 | 迁移 `100_fund_accounts.sql`:`fund_accounts` / `fund_entries` / `manual_expenses` / `fund_reconciliations` |
| 显示名 | 迁移 `101_admin_display_name.sql`:`admin_users.display_name`,备用金 `owner_name` 由触发器同步 |
| 账户编号 | 迁移 `107_manual_fulfillment.sql`:`FA-<大写用户名>`(如 `FA-KF_1`、`FA-JIAJIALI`),重名追加 `-2`、`-3` |
| 后端 | `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 })` |
### 2.2 系统要回答的四个问题
1. 公司现在有多少钱在主管和客服手里?
2. 每个客服当前余额是多少?
3. 每一笔钱花到哪、为什么花、有没有凭证?
4. 系统余额和客服实际余额对不对得上?
### 2.3 账户
`fund_accounts` 一行 = 一个备用金账户,归属一个管理员(客服或主管)。`account_no` 必须是 `FA-<大写用户名>`,重名时按 `id` 顺序追加 `-2`、`-3`。
### 2.4 流水
`fund_entries` 是**只追加**的账户流水。每笔备用金变动写一行,正数=入账,负数=出账,带 `entry_type`(`issue` 发放 / `expense` 支出 / `reconcile` 对账调整 / `adjust` 人工调整)与 `balance_after` 快照。
### 2.5 支出
`manual_expenses` 记录一次具体支出:账户、金额(分)、分类(`purchase` 采购 / `compensation` 赔偿 / `freight` 运费 / `platform_recharge` 平台充值 / `other`)、收款方信息、凭证、关联的 `fund_entry_id`。
### 2.6 对账
`fund_reconciliations` 记录期初/期末账面余额、实际盘点余额与差异说明。差异必须由主管确认并留痕。
### 2.7 P0 未实现(仍在 P1)
批量发放/对账、导出、低余额提醒、大额支出主管确认;支出凭证目前只校验数组非空,未校验对象存储 key 是否真实存在。
> 原「异常队列」指人工支出登记单的审批异常队列,随登记链下线而取消。现在的人工异常统一为 `manual_cost_missing`:**首次创建人工履约配置之后**的订单项,只要还没有人工成本快照就进入队列;确认履约写入快照后自动 `resolved`。历史人工单不进入该队列。
---
## 三、确认履约
### 3.1 落点
- 服务:`apps/backend/src/services/finance/manual-fulfillment-service.ts`
- 接口:`apps/backend/src/routes/admin/finance.ts` 的 `POST /finance/manual-fulfillment/confirm`;待办 `GET /finance/manual-fulfillment/pending`
- 组件:`apps/frontend/src/components/admin/ManualFulfillmentConfirmModal.tsx`
- 订单入口:订单列表/详情「确认履约」
- 权限:`admin / operator / support`;客服只能使用本人备用金/积分账户;管理员/财务/运营可在确认时通过 `fundAccountId` / `creditOwnerAdminUserId` 指定客服账户代录
- 配置前置:商品与成本 → 人工 SKU 配置(`manual_fulfillment_configs`)
### 3.2 资金流与成本流(同一事务)
```text
BEGIN
锁定 order_items 行(FOR UPDATE OF oi)
→ 复查是否已有 manual_case 快照(有则 409,不扣款)
→ 按 SKU 自动匹配人工履约配置(配置晚于下单也能挂上)
→ 解析成本项/成本版本(版本缺失在扣款前失败)
→ 扣积分(platform_credit_entries)或扣备用金(fund_entries + manual_expenses)
→ 写 cost_snapshots(actual,subject_id = order_item_id)
COMMIT -- 任一步失败整体回滚
```
- 余额不足直接拒绝,**不允许负余额**;
- 重复点击/并发请求:订单项行锁 + `uq_cost_snapshots_manual_order_item` 唯一索引双重兜底,只有一个请求能成功扣款;
- 同一个订单项最多一条有效人工成本快照;不同订单项(包括现金与积分两条链路)不会因为 id 撞车互相覆盖。
### 3.3 成本快照
- `subject_type='manual_case'`、`subject_id=order_item_id`、`order_item_id` 同值;
- 金额:固定成本 = `credit_quantity × 平台换算率`;成本不固定 = 客服填写的实际金额;
- `metadata_json` 记录 `fundExpenseId / creditEntryId / subjectSource / platformKey / creditQuantity / costFen / paidBy` 等;
- `uq_cost_snapshots_manual_order_item` 保证一个订单项只有一条有效快照(迁移 `111`)。
### 3.4 录入字段
```text
order_item_id -- 订单项(必填;弹窗内锁定)
costFen -- 仅成本不固定的 SKU 必填,> 0(分)
payeeName -- 收款方(选填,默认「人工履约」)
note -- 备注(选填)
```
平台、消耗积分与固定成本都来自人工 SKU 配置;客服不选账户、不改平台。管理员/财务/运营代录时额外传 `fundAccountId`(现金)或 `creditOwnerAdminUserId`(积分),未指定则回退本人账户。
### 3.5 配置与监控
- 人工 SKU 的匹配键:`spec_json.skuNick → spec_json.outSkuSn → sku_code → sku_name`;没有前两级的历史单也能按后两级配置。
- 保存配置后立即回填该 SKU 最近 30 天的订单项;定时任务每 5 分钟对最近窗口内未匹配项兜底;更早历史需显式「重新匹配」。
- 未配置 SKU 的订单项进入 `manual_cost_missing`(`detail.configMissing=true`),配置后自动匹配;确认履约后异常自动收口。
---
## 四、订单页「确认履约」
- 弹窗只展示已配置履约渠道的人工项;固定成本零输入,成本不固定才要求填金额;
- 锁定订单与订单项,不允许改平台/账户;
- 确认即扣钱并写成本,没有审核/报销步骤;
- 若 SKU 还没配置,弹窗会提示先去「商品与成本 → 人工 SKU 配置」补配置;
- 重复确认会被拒绝,不会重复扣款。
---
## 五、每日必查与关账门槛
| 指标 | 目标 |
| --- | ---: |
| 人工订单项无资金流水(现金备用金支出 / 积分消耗流水) | 0 |
| 人工订单项无有效成本快照 | 0 |
| 现金人工成本金额与 manual_expenses 不一致 | 0 |
| `manual_cost_missing` open(上线后) | 0(历史人工单不计入) |
关账门槛:
- 存在上线后人工单无成本记录:**不允许关账**;
- 存在上线后人工单没有对应资金流水(现金或积分):**不允许关账**;
- 存在现金人工支出与备用金流水金额不一致:**不允许关账**。
核对 SQL 见 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) §八。
---
## 六、异常处理
- 发现人工单无成本:确认履约;扣款与写成本同事务,失败整体回滚,不会出现「扣了钱没记成本」。
- 备用金/积分不足:拒绝确认,由主管补拨/充值后再确认;不允许负余额。
- 人工 SKU 未配置:进入 `manual_cost_missing` 队列,配置后自动回填订单项,再确认即可。
- 确认后需要更正:当前不提供反审核;只能通过后续冲销/调整处理(P1),不要直接改库。
- 历史人工单:找不回来、**直接放弃**,不补录、不计入上线后监控缺口。
---
## 七、历史人工:直接放弃
历史人工支出找不回来,口径明确为**放弃**:
- 不补录、不回填 `cost_snapshots`;
- 不做样本抽取与均价估算;
- 不阻塞上线,历史缺口计入销售事实的 `cost_missing_count`;
- 只保证**上线后**的新单不丢:人工单必须有备用金支出与成本快照。
历史平台 53,901 项同理,只有拿到旧平台结算数据才允许按历史成本入账。
---
## 八、关联文档
- 业务口径: [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md)
- 数据模型: [成本中心与履约模型.md](成本中心与履约模型.md)
- 影子总账: [财务系统集成方案.md](财务系统集成方案.md)
- 订单读取与履约展示: [成本中心与履约模型.md](成本中心与履约模型.md) §六