云机环境 · 非验收证据。全部结论下方附真实终端输出。
目的:发生产 SOP 要求「含 migration 的发版必须先真库排练(同版本库+存量行,真跑升级验存量未动)」。
| 项 | 结果 |
|---|---|
| 9 个迁移从 main 版本升到 test 版本 | ✅ 全部跑通,退出码 0 |
| 存量数据是否被动 | ✅ 一行没动(5 张表前后计数完全一致) |
| 新表 | ✅ 4 张全部建成 |
| 新字段 | ✅ 9/9 全部到位 |
| 破坏性操作(升级段) | ✅ 零 —— 无 drop_table / drop_column |
| 🔴 存量守门 | ✅ 实测生效,且失败时整体回滚 |
生产结算停在 phase66_000_zero_guarantee(settle-prod 最后部署 08-10),
test 在 phase71_000_support_qrcodes,中间隔 9 个迁移。
搭法:复制本地真库 → 退回 phase66 → 再升回 head。带真实存量行,最接近生产。
① 核心迁移链从零跑不通
先试过建空库从头跑,失败:
Running upgrade 0c5960f2e981 -> 002 ... -> 003
psycopg2.errors.DuplicateTable: relation "wechat_work_config" already exists
第 003 号要建的表,前面 0c5960f2e981 已经建过。生产不受影响(库是一路长起来的、不会重放),
但新环境永远建不起来。独立问题,已记录。
② 结算迁移无法从当前数据回退
asyncpg.exceptions.NotNullViolationError:
column "voucher_id" of relation "payment_confirmations" contains null values
微信支付新流程产生的到账记录本来就没有凭证号,回退时要把该列改回 NOT NULL 就撞上。
处理:在一次性复制库上删掉这 2074 行(生产 phase66 年代不存在这类记录),对齐后回退成功。
⚠️ 含义:结算迁移一旦上线就基本回不去了,发版前要有这个心理准备。
phase70_000_drop_pending_amount 里写了道硬守门:
def _assert_no_pending_amount_rows() -> None:
remaining = bind.execute(sa.text(
f"select count(*) from {TABLE} where status = '待确认金额'")).scalar_one()
if remaining:
raise RuntimeError(f"still {remaining} order(s) in status '待确认金额'. ...")
负面测试:往预演库塞 1 张「待确认金额」单(amount_due 为空,还原真实坏单),跑升级:
Running upgrade phase69_000_review_guard -> phase70_000_drop_pending_amt, 废「待确认金额」状态
RuntimeError: still 1 order(s) in status '待确认金额'. 请先在平台把这些在途单人工跑完或取消...
--- 退出码 1 ---
--- 拦下后版本: phase66_000_zero_guarantee ---
🔴 一张坏单就把整批升级拦下,并且版本退回起点、前 8 个迁移一并回滚,无中间态。
⇒ 风险性质是「发版发不出去」,不是「数据被弄坏」。 失败是安全的。
✅ 发版前必须在生产库跑这一条:
SELECT count(*) FROM store_service_orders WHERE status = '待确认金额';
-- 必须 = 0,否则整批结算迁移会中止
Running upgrade phase66_000_zero_guarantee -> phase67_000_service_prices, 价格管理
Running upgrade phase67_000_service_prices -> phase67_001_change_clock_ts, 改价历史时间戳
Running upgrade phase67_001_change_clock_ts -> phase68_000_payment_orders, 微信支付订单表
Running upgrade phase68_000_payment_orders -> phase68_001_refund_offset, 退款冲销
Running upgrade phase68_001_refund_offset -> phase68_002_signup_wx, 签约档案存微信身份
Running upgrade phase68_002_signup_wx -> phase68_003_store_closed, 机构关闭留痕
Running upgrade phase68_003_store_closed -> phase69_000_review_guard, 复核链接手机号验证
Running upgrade phase69_000_review_guard -> phase70_000_drop_pending_amt, 废「待确认金额」
Running upgrade phase70_000_drop_pending_amt -> phase71_000_support_qrcodes, 客服二维码
--- 退出码 0 ---
--- 升级后版本: phase71_000_support_qrcodes ---
BEFORE 服务订单 3407 | 到账确认 4203 | 签约档案 24861 | 收益快照 4245 | 渠道 41307
AFTER 服务订单 3407 | 到账确认 4203 | 签约档案 24861 | 收益快照 4245 | 渠道 41307
payment_orders / service_price_changes / service_prices / support_qrcodes
payer_openid payer_unionid payer_nickname payer_avatar
closed_reason closed_at closed_by
review_link_failed_attempts review_link_locked_until
CHECK (status = ANY (ARRAY['待付款','待确认到账','待开通','已完成','已驳回','已取消','开通失败']))
按 upgrade() 段单独解析(先前混着 downgrade() 段看会误判):
| 迁移 | 升级段动作 |
|---|---|
| phase67_000 | create_table ×2, create_index ×1 |
| phase67_001 | execute(ALTER 默认值) |
| phase68_000 | create_table ×1, add_column ×1, alter_column ×2, 约束/索引 ×5 |
| phase68_001 | drop_constraint ×3(约束,非数据), create_check_constraint ×1, create_index ×1 |
| phase68_002 | add_column ×4, create_index ×1 |
| phase68_003 | add_column ×3 |
| phase69_000 | add_column ×2 |
| phase70_000 | 换 CHECK 约束 + 换索引(名字吓人,实际不删列不删表) |
| phase71_000 | create_table ×1 |
全部 9 个升级段:drop_table 0 次、drop_column 0 次。
预演库已删除,临时 worktree 已移除,本地真库未被触碰(仍 phase71_000_support_qrcodes)。
云机环境 · 非验收证据。