Files
order_site/docs/财务上线初始化与验收-2026-10-26.md
yml2213 08318ef8d9 feat(deploy): 财务上线数据阶段编排与运维加固
- 新增 deploy/post-deploy.sh:回填 → 业务配置 → 同步 → 验收四阶段编排,支持 --only/--dry-run/窗口参数
- 新增 tools 镜像与 scripts/verify-finance-launch.sql(15 项上线验收)
- 验收口径:成本闸门二选一、要求已过账凭证、备用金支出关联、成本覆盖 WARN
- 加固:参数校验、工具镜像每次刷新、docker run 兜底超时、迁移前缀探测、验收失败退出码 2
- 工具镜像预建 /app/data,消除日志 EACCES
- 回填后与 restore-db 恢复后自动 VACUUM (ANALYZE),修复订单列表分页 COUNT 全表扫描(41 万单实测 27s → 0.17s)
- 文档:41 万单同步用 7 天窗 + 180s 超时;回填/恢复后刷新统计
2026-10-28 11:52:42 +08:00

336 lines
15 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.
# 财务/成本上线初始化与验收(2026-10-26)
> 状态:现行操作手册。适用于 `dev` 合并后的财务/成本模块上线;**历史人工成本不补录**。
> 代码基线:迁移 `103`–`105`(本手册写于人工成本完整性与配置自动匹配修复之后)。
> 数据基线:生产 dump `2026-10-22`(迁移 `101`)。历史人工支出找不回来,只保证**首次配置人工履约之后**的新单可记录、可监控。
---
## 〇、上线范围与边界
本次上线的是「备用金 + 人工成本 + 成本中心 + 销售事实 + 影子总账 P0」模块:
| 能力 | 本次是否上线 |
| --- | --- |
| 客服备用金账户/流水/支出/对账 | 是(数据需上线后初始化) |
| 人工 SKU 配置 + 确认履约(扣积分/备用金 + 写 actual 快照) | 是(配置需上线后设置) |
| 商品目录、渠道映射、成本版本、成本快照、销售事实 | 是 |
| 手工退款登记(冲减收入、关闭成本异常) | 是 |
| 影子总账(科目/凭证/试算平衡)、期间关账、平台对账 | 是(迁移 `104`;对账当前为手工账单导入) |
| 平台退款 API | **部分**:快手行业(91)已接入;抖音/拼多多仍手工登记 |
明确不做:
- 历史人工支出不补录、不按均价估算;
- 历史人工单的 `cost_missing` 不进入上线后监控口径;
- 平台自动拉单、全平台退款 API、总账接管钱包余额暂不做;
- 人工成本监控从**第一次创建人工履约配置的时间**开始计算(`manual_fulfillment_configs.created_at`)。
---
## 一、上线顺序(必须按序)
### 1. 备份 + 迁移
```bash
# 生产机
cd /home/order_site
bash deploy/ubuntu-deploy.sh --backup --dry-run # 先看计划
bash deploy/ubuntu-deploy.sh --backup # 迁移前 pg_dump + OSS,再重建/重启
```
后端启动会自动执行 `102`–`105`。确认:
```sql
SELECT name FROM schema_migrations ORDER BY id DESC LIMIT 5;
-- 期望包含:111_work_order_share_cancel_reason_comment / 110_work_order_hall_and_shop_indexes / 109_work_order_share_participation / 108_finance_cost_integrity
```
> 迁移 `109`/`110`/`111` 都是增量、幂等;`111` 会给 `orders(375k)` 建表达式索引,实测写阻塞约 200–300ms。
### 2. 初始化客服资金(备用金 + 积分)
上线后由财务/主管在后台完成:
1. **责任主管账户**:钱包管理 → 主管账户,先确认至少有一个可用主管账户(补建客服账户时必须选择责任主管)。
2. **备用金账户**:钱包管理 → 客服账户 → 「补建客服账户」(或调用 `POST /api/v1/admin/fund/accounts/ensure-agents`,需传 `responsibleSupervisorAccountId`);可直接填期初余额,也可建完再发放。
3. **拨付备用金**:钱包管理 → 对应账户 → 「发放」,把周转金拨给客服;余额不足时人工确认会直接拒绝。
4. **积分充值**:商品与成本 → 积分钱包 → 选择客服/平台 → 「发放」(南航/乐玩 1 积分 = 1 分,ROCK 1 积分 = 1 元,以 `fulfillment_profiles.config_json.rechargeUnitPriceFen` 为准)。
5. 要求:客服只能使用**本人**备用金账户和积分账户;管理员/财务/运营代录时可在确认弹窗选择**指定客服**的备用金/积分账户,未选择则回退本人账户。
校验:
```sql
-- 每个客服都应有 agent 账户;余额按实际拨付为准
SELECT au.username, fa.account_no, fa.balance_fen, fa.status
FROM admin_users au
LEFT JOIN fund_accounts fa
ON fa.owner_admin_user_id = au.id AND fa.account_type = 'agent'
WHERE au.role = 'support'
ORDER BY au.username;
-- 三平台积分账户余额
SELECT pca.platform_key, au.username, pca.balance_quantity
FROM platform_credit_accounts pca
JOIN admin_users au ON au.id = pca.owner_admin_user_id
ORDER BY pca.platform_key, au.username;
```
### 2.5 商品初始化/回填只处理最近 30 天
入口:商品与成本 → 标准商品。接单平台/人工客服 Tab 顶部有日期范围,默认最近 30 天:
- 「从接单模板初始化」只使用范围内工单证据;范围外模板不再先建商品;
- 「按工单回填商品」只回填范围内订单项;
- 「归一人工商品」只归一范围内订单项;
- 自动发货的同步/回填固定最近 30 天。
原因:更早历史 SKU 噪声大,且人工历史成本本身不可追,重新带出会污染渠道映射和商品目录。需要调整时在页面改日期范围,不要默认全量。
### 3. 扫描并配置人工 SKU
入口:商品与成本 → 人工 SKU 配置。
1. 选择日期范围(建议上线前 30 天)→ 「扫描」;没有 `spec_json.skuNick/outSkuSn` 的单会按 `sku_code → sku_name` 兜底,不再漏扫。
2. 逐项配置:平台(南航/乐玩/ROCK/备用金现金)+ 消耗积分(0 = 成本不固定,客服确认时手填)+ 启用。
3. **保存后会自动重新匹配该 SKU 最近 30 天的订单项**,不需要再点「重新匹配」;定时任务每 5 分钟会对最近窗口内未匹配人工项做一次兜底回填;更早历史不自动回填。
4. 未配置的 SKU 会进入成本异常队列(`manual_cost_missing`,`detail.configMissing=true`),按队列把配置补齐即可。
批量检查缺口:
```sql
-- 已配置 / 未配置的人工 SKU(按首次配置之后的订单项)
SELECT
COUNT(*) FILTER (WHERE oi.fulfillment_config_id IS NULL) AS unconfigured_items,
COUNT(*) AS manual_items
FROM order_items oi
WHERE oi.fulfillment_mode = 'manual'
AND oi.created_at >= (SELECT MIN(created_at) FROM manual_fulfillment_configs);
-- 需要补配置的 SKU(按订单量排序)
SELECT
COALESCE(NULLIF(oi.spec_json->>'skuNick',''), NULLIF(oi.spec_json->>'outSkuSn',''),
NULLIF(oi.sku_code,''), NULLIF(oi.sku_name,'')) AS sku_key,
MAX(oi.sku_name) AS sku_name,
COUNT(*) AS items
FROM order_items oi
WHERE oi.fulfillment_mode = 'manual'
AND oi.fulfillment_config_id IS NULL
AND oi.created_at >= (SELECT MIN(created_at) FROM manual_fulfillment_configs)
GROUP BY 1
ORDER BY items DESC;
```
> 配置是全量生效的:保存后历史未匹配项也会被回填(只填空值),但历史成本仍不会补录。
### 4. 确认人工履约(日常)
订单列表/详情 → 「确认履约」:
- 固定成本 SKU:零输入,直接扣对应积分/备用金;
- 成本不固定 SKU:填写本次实际成本(分);
- 扣款、写 `manual_expenses`/积分流水、写 `manual_case` 成本快照在**同一个事务**里;
- 重复点击/并发请求只能成功一次(订单项行锁 + 唯一索引兜底),失败不会重复扣款;
- 成功后可重复确认会被拒绝。
监控:
```sql
-- 上线后人工项:已配置但未确认(客服待办)
SELECT COUNT(*) AS pending_confirmed
FROM order_items oi
WHERE oi.fulfillment_mode = 'manual'
AND oi.fulfillment_config_id IS NOT NULL
AND oi.created_at >= :launch_at
AND NOT EXISTS (
SELECT 1 FROM cost_snapshots cs
WHERE cs.subject_type = 'manual_case'
AND cs.order_item_id = oi.id
AND cs.superseded_at IS NULL
);
-- 人工成本缺失异常(队列)
SELECT status, COUNT(*) FROM cost_exceptions
WHERE reason = 'manual_cost_missing'
GROUP BY status;
```
`manual_cost_missing` 只在「首次配置之后」生成;确认履约写入快照后自动置为 `resolved`。
### 5. 历史回填(退款功能上线前必须跑 `revenue:backfill`)
**存量订单要能退款,必须先回填订单项收入。** 迁移 `103` 只加列不回填;`revenue_basis='unknown'` 的订单项 `revenue_amount_fen=0`,订单详情「退款登记」会因可退金额为 0 而置灰,手工退款也会失败。
```bash
npm run revenue:backfill -- --dry-run # 待回填量,应为 0
npm run revenue:backfill # 分批回填;中断可重跑
npm run fulfillment:backfill # 只填 NULL(可选,报表用)
npm run finance:sync -- --mode=all --from=2026-07-10 --to=2026-10-22
```
> 回填完成后必须刷新统计信息,否则订单列表的分页总数会被规划成 `orders` 全表扫描:
>
> ```sql
> VACUUM (ANALYZE) order_items;
> VACUUM (ANALYZE) orders;
> ```
>
> `bash deploy/post-deploy.sh --only=backfill` 已自动执行这一步;手工回填请补跑。
验证:
```sql
SELECT COUNT(*) FROM order_items WHERE revenue_basis = 'unknown'; -- 退款上线前应为 0
```
长区间按 30 天窗口分段执行(默认 `DATABASE_STATEMENT_TIMEOUT_MS=15000`)。
> 41 万单规模的实测口径:30 天窗口的单条语句会超过 60 秒,被 statement timeout 打断。
> 这种体量请用 **7 天窗口 + 180 秒超时**:
>
> ```bash
> DATABASE_STATEMENT_TIMEOUT_MS=180000 bash deploy/post-deploy.sh \
> --only=sync --from=2026-09-27 --to=2026-10-27 --window-days=7
> ```
注意:人工历史成本按既定口径**不补录**,历史人工毛利仍不可用。
---
## 二、上线验收 SQL(替换 `:launch_at` 为实际上线时间)
```sql
-- 1. 履约方式覆盖:四项相加 = order_items 总数,NULL = 0
SELECT COALESCE(fulfillment_mode, '(null)') AS mode, COUNT(*)
FROM order_items GROUP BY 1 ORDER BY 2 DESC;
-- 2. 上线后人工项必须有履约配置(允许配置跟进期)
SELECT COUNT(*) FROM order_items
WHERE fulfillment_mode = 'manual'
AND created_at >= :launch_at
AND fulfillment_config_id IS NULL;
-- 3. 上线后人工项必须有成本快照(关账门槛)
SELECT COUNT(*) FROM order_items oi
WHERE oi.fulfillment_mode = 'manual'
AND oi.created_at >= :launch_at
AND NOT EXISTS (
SELECT 1 FROM cost_snapshots cs
WHERE cs.subject_type = 'manual_case'
AND cs.order_item_id = oi.id
AND cs.superseded_at IS NULL
);
-- 4. 人工成本快照金额 = 对应备用金支出(仅现金路径)
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.created_at >= :launch_at
AND cs.total_amount_fen <> me.amount_fen;
-- 5. 一个订单项最多一条有效人工快照(应为 0)
SELECT order_item_id, COUNT(*)
FROM cost_snapshots
WHERE subject_type = 'manual_case' AND superseded_at IS NULL
GROUP BY 1 HAVING COUNT(*) > 1;
-- 6. 销售事实与实时聚合对平(同一日期区间)
-- 净收入 = Σ订单项收入 − 已确认退款;成本 = Σ有效快照
WITH r AS (SELECT MIN(stat_date) a, MAX(stat_date) b FROM sales_daily_facts)
SELECT
(SELECT SUM(net_revenue_fen) FROM sales_daily_facts) AS facts_revenue,
(SELECT SUM(cost_amount_fen) FROM sales_daily_facts) AS facts_cost;
-- 与独立聚合对比,允许 0 差异。
-- 7. 人工成本异常:上线后应能被确认动作收口
SELECT reason, status, COUNT(*) FROM cost_exceptions
WHERE created_at >= :launch_at
GROUP BY 1,2 ORDER BY 3 DESC;
-- 7.1 存量订单收入回填(退款功能上线前应为 0)
SELECT COUNT(*) AS unknown_revenue_items
FROM order_items WHERE revenue_basis = 'unknown';
-- 8. 总账借贷平衡(应为差额 0)
SELECT
COALESCE(SUM(debit_fen), 0) AS debit_fen,
COALESCE(SUM(credit_fen), 0) AS credit_fen,
COALESCE(SUM(debit_fen), 0) - COALESCE(SUM(credit_fen), 0) AS diff_fen
FROM finance_journal_lines l
JOIN finance_journal_entries e ON e.id = l.entry_id AND e.status = 'posted'
WHERE e.occurred_at >= :launch_at;
-- 9. 已关账期间不应再有新凭证
SELECT e.period_key, COUNT(*)
FROM finance_journal_entries e
JOIN finance_periods p ON p.period_key = e.period_key AND p.status = 'closed'
WHERE e.created_at >= :launch_at
GROUP BY 1;
```
---
## 三、常见问题
| 现象 | 原因 | 处理 |
| --- | --- | --- |
| 确认时报「还没有配置履约渠道」 | SKU 未配置,或配置的 `sku_key` 与订单项不匹配 | 在人工 SKU 配置里按兜底键配置;保存后自动回填 |
| 确认时报「当前账号还没有备用金账户」 | 该客服未补建账户 | 钱包管理 → 补建客服账户 |
| 确认时报「积分不足」 | 未充值/充值到别的客服 | 积分钱包 → 给本人账户发放 |
| 确认成功但列表仍有「待确认」 | 前端缓存 | 刷新;后端以快照为准 |
| `manual_cost_missing` 不减少 | 未确认履约,或确认失败回滚 | 看订单详情确认;异常只能由快照自动收口 |
| 订单详情「退款登记」置灰 | 订单项 `revenue_amount_fen=0`,通常是存量订单未跑收入回填 | 执行 `npm run revenue:backfill`;刷新后「可退」应大于 0 |
---
## 四、总账、期间关账与平台对账(迁移 108)
入口:后台「财务 → 总账与对账」(`/admin/finance/ledger`)。
### 4.1 影子总账
- 销售事实按日同步:收入(借 1101 / 贷 4001)、退款(借 4101 / 贷 1101)、成本(借 5001 / 贷 2001);
- **未关账期间按日整体重建**:退款/收入回填导致销售事实变化时,旧日凭证会被替换;已关账期间保持不动;
- 财务可手工录入调整凭证(借贷科目 + 金额 + 摘要),必须借贷平衡;
- 试算平衡按日期区间展示;总账不接管 `worker_wallets` / `fund_accounts` 余额;
- 定时任务 `cost-finance-sync` 每 5 分钟同步最近窗口;也可手动「同步销售事实到总账」。
### 4.2 期间关账
- 期间按 `YYYY-MM`;关账前会先补齐该期间凭证,并校验试算平衡;
- 关账后该期间禁止新增凭证;反关账需 `finance.config` 权限并写审计;
- 关账不修改历史凭证,只是阻断新写入。
### 4.3 平台对账
- 支持粘贴 JSON 账单导入(销售/退款行),按 `provider + platform_order_id` 匹配订单/退款;
- 明细状态:`matched / amount_mismatch / missing_order / missing_refund / duplicate`;
- 差异批次可标记解决(记录备注);平台自动拉单后续复用同一匹配器。
账单行示例:
```json
[
{ "platformOrderId": "123456", "entryType": "sale", "amountFen": 990 },
{ "platformOrderId": "123456", "entryType": "refund", "amountFen": 100, "externalRef": "RF-1" }
]
```
### 4.4 平台退款 API
- 快手行业(91):`POST /api/v1/admin/kuaishou-industry/orders/:orderId/refunds/approve-and-record`,传 `platformRefundId`、`orderItemId`、`amountFen`;
先调用快手「同意退款」,成功后再落 `order_refunds.source='platform'` 并冲减销售事实;
- 抖音/拼多多:暂无退款 API,返回 501,仍用「手工退款登记」;
- 平台退款必须传订单项;参数校验在调用平台之前完成,避免平台已同意、本地记账失败。
---
## 五、已知边界(不要对外承诺)
1. 总账是**影子账**:销售事实按日凭证 + 手工凭证;尚未覆盖每一笔钱包/寄卖/提现业务,也没有接管余额;「收支明细」仍是业务事件拼装。
2. 平台对账当前是**手工账单 JSON 导入**,未做平台自动拉单;平台退款 API 仅接入快手(91),抖音/拼多多仍手工登记。
3. 平台服务费/支付通道费/寄卖货款成本确认时点仍待定;相关成本项配置为空时不参与毛利。
4. 历史人工成本、历史平台成本、历史未归一商品不补齐;历史区间毛利只能作为参考。
5. 管理员/财务代录已支持指定客服备用金/积分账户;客服只能使用本人账户。