- 新增 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 超时;回填/恢复后刷新统计
15 KiB
财务/成本上线初始化与验收(2026-10-26)
状态:现行操作手册。适用于
dev合并后的财务/成本模块上线;历史人工成本不补录。 代码基线:迁移103–105(本手册写于人工成本完整性与配置自动匹配修复之后)。 数据基线:生产 dump2026-10-22(迁移101)。历史人工支出找不回来,只保证首次配置人工履约之后的新单可记录、可监控。
〇、上线范围与边界
本次上线的是「备用金 + 人工成本 + 成本中心 + 销售事实 + 影子总账 P0」模块:
| 能力 | 本次是否上线 |
|---|---|
| 客服备用金账户/流水/支出/对账 | 是(数据需上线后初始化) |
| 人工 SKU 配置 + 确认履约(扣积分/备用金 + 写 actual 快照) | 是(配置需上线后设置) |
| 商品目录、渠道映射、成本版本、成本快照、销售事实 | 是 |
| 手工退款登记(冲减收入、关闭成本异常) | 是 |
| 影子总账(科目/凭证/试算平衡)、期间关账、平台对账 | 是(迁移 104;对账当前为手工账单导入) |
| 平台退款 API | 部分:快手行业(91)已接入;抖音/拼多多仍手工登记 |
明确不做:
- 历史人工支出不补录、不按均价估算;
- 历史人工单的
cost_missing不进入上线后监控口径; - 平台自动拉单、全平台退款 API、总账接管钱包余额暂不做;
- 人工成本监控从第一次创建人工履约配置的时间开始计算(
manual_fulfillment_configs.created_at)。
一、上线顺序(必须按序)
1. 备份 + 迁移
# 生产机
cd /home/order_site
bash deploy/ubuntu-deploy.sh --backup --dry-run # 先看计划
bash deploy/ubuntu-deploy.sh --backup # 迁移前 pg_dump + OSS,再重建/重启
后端启动会自动执行 102–105。确认:
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. 初始化客服资金(备用金 + 积分)
上线后由财务/主管在后台完成:
- 责任主管账户:钱包管理 → 主管账户,先确认至少有一个可用主管账户(补建客服账户时必须选择责任主管)。
- 备用金账户:钱包管理 → 客服账户 → 「补建客服账户」(或调用
POST /api/v1/admin/fund/accounts/ensure-agents,需传responsibleSupervisorAccountId);可直接填期初余额,也可建完再发放。 - 拨付备用金:钱包管理 → 对应账户 → 「发放」,把周转金拨给客服;余额不足时人工确认会直接拒绝。
- 积分充值:商品与成本 → 积分钱包 → 选择客服/平台 → 「发放」(南航/乐玩 1 积分 = 1 分,ROCK 1 积分 = 1 元,以
fulfillment_profiles.config_json.rechargeUnitPriceFen为准)。 - 要求:客服只能使用本人备用金账户和积分账户;管理员/财务/运营代录时可在确认弹窗选择指定客服的备用金/积分账户,未选择则回退本人账户。
校验:
-- 每个客服都应有 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 配置。
- 选择日期范围(建议上线前 30 天)→ 「扫描」;没有
spec_json.skuNick/outSkuSn的单会按sku_code → sku_name兜底,不再漏扫。 - 逐项配置:平台(南航/乐玩/ROCK/备用金现金)+ 消耗积分(0 = 成本不固定,客服确认时手填)+ 启用。
- 保存后会自动重新匹配该 SKU 最近 30 天的订单项,不需要再点「重新匹配」;定时任务每 5 分钟会对最近窗口内未匹配人工项做一次兜底回填;更早历史不自动回填。
- 未配置的 SKU 会进入成本异常队列(
manual_cost_missing,detail.configMissing=true),按队列把配置补齐即可。
批量检查缺口:
-- 已配置 / 未配置的人工 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成本快照在同一个事务里; - 重复点击/并发请求只能成功一次(订单项行锁 + 唯一索引兜底),失败不会重复扣款;
- 成功后可重复确认会被拒绝。
监控:
-- 上线后人工项:已配置但未确认(客服待办)
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 而置灰,手工退款也会失败。
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全表扫描:VACUUM (ANALYZE) order_items; VACUUM (ANALYZE) orders;
bash deploy/post-deploy.sh --only=backfill已自动执行这一步;手工回填请补跑。
验证:
SELECT COUNT(*) FROM order_items WHERE revenue_basis = 'unknown'; -- 退款上线前应为 0
长区间按 30 天窗口分段执行(默认 DATABASE_STATEMENT_TIMEOUT_MS=15000)。
41 万单规模的实测口径:30 天窗口的单条语句会超过 60 秒,被 statement timeout 打断。 这种体量请用 7 天窗口 + 180 秒超时:
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 为实际上线时间)
-- 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; - 差异批次可标记解决(记录备注);平台自动拉单后续复用同一匹配器。
账单行示例:
[
{ "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,仍用「手工退款登记」;
- 平台退款必须传订单项;参数校验在调用平台之前完成,避免平台已同意、本地记账失败。
五、已知边界(不要对外承诺)
- 总账是影子账:销售事实按日凭证 + 手工凭证;尚未覆盖每一笔钱包/寄卖/提现业务,也没有接管余额;「收支明细」仍是业务事件拼装。
- 平台对账当前是手工账单 JSON 导入,未做平台自动拉单;平台退款 API 仅接入快手(91),抖音/拼多多仍手工登记。
- 平台服务费/支付通道费/寄卖货款成本确认时点仍待定;相关成本项配置为空时不参与毛利。
- 历史人工成本、历史平台成本、历史未归一商品不补齐;历史区间毛利只能作为参考。
- 管理员/财务代录已支持指定客服备用金/积分账户;客服只能使用本人账户。