89 lines
3.2 KiB
Python
89 lines
3.2 KiB
Python
"""数据订正:`profile_snapshots.current_customer_id` 与 `is_current` 对齐。
|
||||
|
|
|
|||
|
|
**默认 dry-run**,加 `--apply` 才提交。幂等,可重复运行。
|
|||
|
|
|
|||
|
|
### 背景
|
|||
|
|
|
|||
|
|
`current_customer_id` 是**普通可空列 + 唯一键** `uk_profile_snapshot_current`(不是生成列),
|
|||
|
|
契约是「当前版本写客户 ID、历史版本写 NULL」。但写入方曾把它误当生成列,
|
|||
|
|
两处路径都未赋值(`ProfileAssemblyService._write_snapshot`、
|
|||
|
|
`CustomerProfileCandidateService._write_profile_snapshot`),造成两类脏数据:
|
|||
|
|
|
|||
|
|
1. 历史版本 `is_current=0` 却**仍留着** `current_customer_id`;
|
|||
|
|
2. 当前版本 `is_current=1` 却是 NULL。
|
|||
|
|
|
|||
|
|
后果:唯一键形同虚设(多个 NULL 不冲突)⇒「每个客户最多一条 current」失效;
|
|||
|
|
而且 (1) 的值一旦与 (2) 补上的值相同就会**撞唯一键**。
|
|||
|
|
|
|||
|
|
### 顺序要紧
|
|||
|
|
|
|||
|
|
先清 (1) 再补 (2):反过来的话,第 2 步写入时旧值还在,会直接撞键。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
.\\.venv\\Scripts\\python.exe tools\\fix_profile_snapshot_current.py # 先看
|
|||
|
|
.\\.venv\\Scripts\\python.exe tools\\fix_profile_snapshot_current.py --apply # 再改
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
from sqlalchemy import text
|
|||
|
|
|
|||
|
|
from app.infrastructure.db import engine
|
|||
|
|
|
|||
|
|
#: (1) 历史版本不该持有该列 → 清空。必须先做。
|
|||
|
|
_CLEAR_STALE = text("""
|
|||
|
|
UPDATE profile_snapshots
|
|||
|
|
SET current_customer_id = NULL
|
|||
|
|
WHERE is_current = 0 AND current_customer_id IS NOT NULL
|
|||
|
|
""")
|
|||
|
|
|
|||
|
|
#: (2) 当前版本必须持有该列 → 补上。放在 (1) 之后,避免撞唯一键。
|
|||
|
|
_FILL_CURRENT = text("""
|
|||
|
|
UPDATE profile_snapshots
|
|||
|
|
SET current_customer_id = customer_id
|
|||
|
|
WHERE is_current = 1 AND current_customer_id IS NULL
|
|||
|
|
""")
|
|||
|
|
|
|||
|
|
_COUNT_STALE = text(
|
|||
|
|
"SELECT COUNT(*) FROM profile_snapshots "
|
|||
|
|
"WHERE is_current = 0 AND current_customer_id IS NOT NULL"
|
|||
|
|
)
|
|||
|
|
_COUNT_MISSING = text(
|
|||
|
|
"SELECT COUNT(*) FROM profile_snapshots "
|
|||
|
|
"WHERE is_current = 1 AND current_customer_id IS NULL"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def main(apply: bool) -> int:
|
|||
|
|
async with engine.connect() as conn:
|
|||
|
|
stale = await conn.scalar(_COUNT_STALE)
|
|||
|
|
missing = await conn.scalar(_COUNT_MISSING)
|
|||
|
|
print(f"待清空(is_current=0 却留着值): {stale}")
|
|||
|
|
print(f"待补写(is_current=1 却是 NULL): {missing}")
|
|||
|
|
|
|||
|
|
if not apply:
|
|||
|
|
print("dry-run:未提交。确认无误后加 --apply 再运行。")
|
|||
|
|
await conn.rollback()
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
cleared = (await conn.execute(_CLEAR_STALE)).rowcount
|
|||
|
|
filled = (await conn.execute(_FILL_CURRENT)).rowcount
|
|||
|
|
await conn.commit()
|
|||
|
|
print(f"已清空 {cleared} 行、已补写 {filled} 行")
|
|||
|
|
|
|||
|
|
# 复核:两类异常都应归零
|
|||
|
|
print("复核 待清空:", await conn.scalar(_COUNT_STALE))
|
|||
|
|
print("复核 待补写:", await conn.scalar(_COUNT_MISSING))
|
|||
|
|
print("\n各客户的当前快照:")
|
|||
|
|
for row in (await conn.execute(text(
|
|||
|
|
"SELECT customer_id, version, is_current, current_customer_id "
|
|||
|
|
"FROM profile_snapshots ORDER BY customer_id, version"
|
|||
|
|
))).mappings().all():
|
|||
|
|
print(" ", dict(row))
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(asyncio.run(main("--apply" in sys.argv)))
|