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 超时;回填/恢复后刷新统计
This commit is contained in:
yml2213
2026-10-28 11:52:42 +08:00
parent 2ddc26091c
commit 08318ef8d9
8 changed files with 977 additions and 8 deletions
+311
View File
@@ -0,0 +1,311 @@
-- 财务上线验收(只读)
--
-- 用途:部署 + 数据初始化完成后,一次跑完所有闸门,输出 PASS / FAIL 清单。
-- 不写任何业务表;结果只落在会话级临时表 _launch_checks。
--
-- 用法:
-- # 1) 通过编排脚本(推荐,自动带上 launch_at)
-- bash deploy/post-deploy.sh --only=verify --launch-at='2026-10-27 18:00:00+08'
--
-- # 2) 手工执行(变量必须传,否则用脚本内的兜底时间)
-- docker compose exec -T postgres sh -lc \
-- 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-- -v ON_ERROR_STOP=1 -v launch_at="2026-10-27 18:00:00+08" -f -' \
-- < scripts/verify-finance-launch.sql
--
-- 退出码:0 = 无 FAIL(WARN 不阻断);3 = 存在 FAIL(末尾 RAISE EXCEPTION)。
-- 前置条件:迁移 103–105 已应用;否则先跑 deploy/ubuntu-deploy.sh。
\set ON_ERROR_STOP on
\if :{?launch_at}
\else
-- 与 post-deploy.sh 一致:默认取本次执行时间(容器 TZ=Asia/Shanghai)。
\set launch_at `date '+%Y-%m-%d %H:%M:%S%z'`
\endif
\echo ''
\echo '==================== 财务上线验收 ===================='
\echo '上线时间(launch_at): ' :launch_at
\echo ''
-- 建表与 15 条检查的 INSERT 不输出到终端(错误仍会进 stderr)。
\o /dev/null
SET client_min_messages = warning;
DROP TABLE IF EXISTS _launch_checks;
CREATE TEMP TABLE _launch_checks (
seq int,
id text,
title text,
actual text,
expected text,
status text,
hint text
);
-- 01 迁移基线 --------------------------------------------------------------
INSERT INTO _launch_checks
SELECT 1, '01', '迁移基线 103–105 已应用',
v::text, '3',
CASE WHEN v = 3 THEN 'PASS' ELSE 'FAIL' END,
'后端未完成迁移:docker compose logs backend | grep -i migration'
FROM (
-- 按版本前缀逐个判断,避免后续 105_xxx_hotfix 之类增量迁移把计数顶成 4。
SELECT (EXISTS (SELECT 1 FROM schema_migrations WHERE COALESCE(filename, name) ~ '^103_'))::int
+ (EXISTS (SELECT 1 FROM schema_migrations WHERE COALESCE(filename, name) ~ '^104_'))::int
+ (EXISTS (SELECT 1 FROM schema_migrations WHERE COALESCE(filename, name) ~ '^105_'))::int AS v
) s;
-- 02 存量订单项收入回填 ----------------------------------------------------
INSERT INTO _launch_checks
SELECT 2, '02', '存量订单项收入已回填(revenue_basis)',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'跑 npm run revenue:backfill(deploy/post-deploy.sh 会自动执行);未回填时退款按钮置灰'
FROM (
SELECT count(*) AS v FROM order_items WHERE revenue_basis = 'unknown'
) s;
-- 03 履约方式回填 ----------------------------------------------------------
INSERT INTO _launch_checks
SELECT 3, '03', '履约方式已回填(fulfillment_mode)',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'跑 npm run fulfillment:backfill;NULL 会让订单/成本页口径缺失'
FROM (
SELECT count(*) AS v FROM order_items WHERE fulfillment_mode IS NULL
) s;
-- 04 履约方式取值合法 ------------------------------------------------------
INSERT INTO _launch_checks
SELECT 4, '04', '履约方式取值合法',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'存在未知 fulfillment_mode,检查回填脚本与历史脏数据'
FROM (
SELECT count(*) AS v
FROM order_items
WHERE fulfillment_mode IS NOT NULL
AND fulfillment_mode NOT IN ('work_order', 'manual', 'legacy_platform', 'affiliate_dash')
) s;
-- 05 商品目录 --------------------------------------------------------------
INSERT INTO _launch_checks
SELECT 5, '05', '商品目录已初始化(product_catalog)',
v::text, '> 0',
CASE WHEN v > 0 THEN 'PASS' ELSE 'FAIL' END,
'商品与成本 → 标准商品 →「从接单模板初始化」/「按工单回填商品」/「归一人工商品」'
FROM (SELECT count(*) AS v FROM product_catalog) s;
-- 06 有效成本 --------------------------------------------------------------
INSERT INTO _launch_checks
SELECT 6, '06', '有效成本已生成(版本定价或正金额快照)',
'版本 ' || priced_versions::text || ' 个 / 正金额快照 ' || positive_snapshots::text || ' 条',
'至少一项 > 0',
CASE WHEN priced_versions > 0 OR positive_snapshots > 0 THEN 'PASS' ELSE 'FAIL' END,
'迁移只建 0 元占位版本;工单奖励/实际成本会写入快照。两项都为 0 时:先配置成本版本,或确认工单奖励已回写'
FROM (
SELECT
(SELECT count(*) FROM cost_versions
WHERE status = 'active' AND (amount_fen > 0 OR rate_bps > 0)) AS priced_versions,
(SELECT count(*) FROM cost_snapshots
WHERE superseded_at IS NULL AND total_amount_fen > 0) AS positive_snapshots
) s;
-- 07 客服备用金账户 --------------------------------------------------------
INSERT INTO _launch_checks
SELECT 7, '07', '客服备用金账户已补建',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'钱包管理 → 客服账户 →「补建客服账户」(POST /admin/fund/accounts/ensure-agents)'
FROM (
SELECT count(*) AS v
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'
AND fa.id IS NULL
) s;
-- 08 上线后人工项必须有履约配置 -------------------------------------------
INSERT INTO _launch_checks
SELECT 8, '08', '上线后人工项已匹配履约配置',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'商品与成本 → 人工 SKU 配置 →「扫描」并逐项配置;保存后会自动回填近 30 天'
FROM (
SELECT count(*) AS v
FROM order_items oi
WHERE oi.fulfillment_mode = 'manual'
AND oi.created_at >= :'launch_at'::timestamptz
AND oi.fulfillment_config_id IS NULL
) s;
-- 09 上线后人工项必须有成本快照(关账门槛) -------------------------------
INSERT INTO _launch_checks
SELECT 9, '09', '上线后人工项均已确认履约(关账门槛)',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'待办量:有未确认人工单就会 FAIL,不宜当日常健康检查;订单列表/详情 →「确认履约」后写 manual_case 快照并扣备用金/积分'
FROM (
SELECT count(*) AS v
FROM order_items oi
WHERE oi.fulfillment_mode = 'manual'
AND oi.created_at >= :'launch_at'::timestamptz
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
)
) s;
-- 10 人工快照唯一性 --------------------------------------------------------
INSERT INTO _launch_checks
SELECT 10, '10', '一个订单项最多一条有效人工快照',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'出现重复快照说明并发确认未被幂等拦截,需排查 manual_case 写入路径'
FROM (
SELECT count(*) AS v
FROM (
SELECT order_item_id
FROM cost_snapshots
WHERE subject_type = 'manual_case' AND superseded_at IS NULL
GROUP BY 1
HAVING count(*) > 1
) dup
) s;
-- 11 快照金额 = 备用金支出(现金路径) ------------------------------------
INSERT INTO _launch_checks
SELECT 11, '11', '人工快照金额与备用金支出一致',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'金额不一致通常是确认事务部分失败;查 cost_snapshots / manual_expenses / fund_entries 三处'
FROM (
SELECT count(*) AS v
FROM cost_snapshots cs
-- 只核对现金路径:metadata.fundExpenseId 精确定位备用金支出,
-- 避免同一订单项有多条 manual_expenses(如另外登记的人工支出)时行放大误报。
JOIN manual_expenses me
ON me.id = NULLIF(cs.metadata_json->>'fundExpenseId', '')::bigint
WHERE cs.subject_type = 'manual_case'
AND cs.superseded_at IS NULL
AND cs.created_at >= :'launch_at'::timestamptz
AND cs.total_amount_fen <> me.amount_fen
) s;
-- 12 总账借贷平衡 ----------------------------------------------------------
INSERT INTO _launch_checks
SELECT 12, '12', '总账借贷平衡(全部已过账凭证)',
v::text || '(已过账凭证 ' || posted::text || ' 张)', '借贷为 0 且至少有 1 张凭证',
CASE WHEN v = 0 AND (posted > 0 OR facts = 0) THEN 'PASS' ELSE 'FAIL' END,
'财务 → 总账与对账 →「同步销售事实到总账」;有销售事实但 0 张凭证同样视为未完成(空表求和恒为 0,不能直接判 PASS)'
FROM (
SELECT
COALESCE(abs(sum(l.debit_fen) - sum(l.credit_fen)), 0)::bigint AS v,
count(DISTINCT e.id) AS posted,
(SELECT count(*) FROM sales_daily_facts) AS facts
FROM finance_journal_lines l
JOIN finance_journal_entries e ON e.id = l.entry_id AND e.status = 'posted'
) s;
-- 13 已关账期间不应再产生新凭证 -------------------------------------------
INSERT INTO _launch_checks
SELECT 13, '13', '已关账期间无新增凭证',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'FAIL' END,
'检查关账后是否有人重跑同步;以 closed_at 为基准,正常关账前生成的凭证不算违规'
FROM (
SELECT count(*) AS v
FROM finance_journal_entries e
JOIN finance_periods p
ON p.period_key = e.period_key
AND p.status = 'closed'
WHERE p.closed_at IS NOT NULL
AND e.created_at > p.closed_at
) s;
-- 14 销售事实已生成 --------------------------------------------------------
INSERT INTO _launch_checks
SELECT 14, '14', '销售事实已生成(sales_daily_facts)',
v::text, '> 0',
CASE WHEN v > 0 THEN 'PASS' ELSE 'FAIL' END,
'跑 finance:sync --mode=facts(post-deploy.sh 会按窗口执行);定时任务只覆盖最近 7 天'
FROM (SELECT count(*) AS v FROM sales_daily_facts) s;
-- 15 近 7 天成本覆盖(仅提示,不阻断上线) ---------------------------------
-- 说明:历史人工成本按既定口径不补录,这里只看非人工;自动发货/接单平台
-- 的成本缺口会直接影响毛利,必须在上线前处理,但不用它单独卡住验收。
INSERT INTO _launch_checks
SELECT 15, '15', '近 7 天非人工销售事实成本覆盖(仅提示)',
v::text, '0',
CASE WHEN v = 0 THEN 'PASS' ELSE 'WARN' END,
'存在成本缺失的非人工销售事实:先归一商品并配置成本版本;历史人工成本按既定口径不补录,故不计入'
FROM (
SELECT count(*) AS v
FROM sales_daily_facts
WHERE stat_date >= current_date - 6
AND fulfillment_mode <> 'manual'
AND cost_missing_count > 0
) s;
\o
\echo '【闸门清单】'
SELECT seq AS "#",
status,
id,
title AS "检查项",
actual AS "实际",
expected AS "期望"
FROM _launch_checks
ORDER BY seq;
\echo ''
\echo '【未通过 / 提示项的处理建议】'
SELECT id,
status AS "状态",
title AS "检查项",
hint AS "处理建议"
FROM _launch_checks
WHERE status <> 'PASS'
ORDER BY seq;
\echo ''
\echo '【汇总】'
SELECT count(*) FILTER (WHERE status = 'PASS') AS "通过",
count(*) FILTER (WHERE status = 'FAIL') AS "未通过",
count(*) FILTER (WHERE status = 'WARN') AS "提示",
count(*) AS "合计"
FROM _launch_checks;
\echo ''
\echo '【数据规模参考】'
SELECT (SELECT count(*) FROM orders) AS "订单",
(SELECT count(*) FROM order_items) AS "订单项",
(SELECT count(*) FROM product_catalog) AS "商品",
(SELECT count(*) FROM cost_snapshots WHERE superseded_at IS NULL) AS "有效成本快照",
(SELECT count(*) FROM sales_daily_facts) AS "销售事实行",
(SELECT count(*) FROM finance_journal_entries) AS "总账凭证",
(SELECT count(*) FROM cost_exceptions WHERE status = 'open') AS "未处理成本异常",
(SELECT count(*) FROM order_items WHERE revenue_basis = 'unknown') AS "待回填收入项";
DO $$
DECLARE
failed_count int;
failed_list text;
BEGIN
SELECT count(*), string_agg(id || ' ' || title, ';' ORDER BY seq)
INTO failed_count, failed_list
FROM _launch_checks
WHERE status = 'FAIL';
IF failed_count > 0 THEN
RAISE EXCEPTION '财务上线验收未通过(% 项):%', failed_count, failed_list;
END IF;
RAISE NOTICE '财务上线验收全部通过';
END $$;