"""一次性数据清洗 — 修复 Product.status 与 overall_status 脱节的历史存量 背景 ---- app/core/lifecycle.py 记录了历史缺陷:早期 receive_task / transfer_task 只改 overall_status、不改 status,导致部分设备的 status 残留为 'pending' / 'OUTBOUND' 等旧值。 现状(重要) ---------- 所有 overall_status 的写入点**都已补上 sync_product_status()**: task_service.py:387 / :691 / :1007、product_service.py:512、 webhooks.py:84 / :256、product_finalize_service.py:107 因此新数据不会再脱节,本脚本只处理历史存量。 影响面 ------ 前端列表筛选走的是 macro_status(由 overall_status 实时派生,见 product_service._resolve_macro_status),所以这批脏数据**基本不影响页面展示**。 但 _mark_after_sales_if_reactivated 等逻辑会读 product.status 判断"是否出库回流", 脏值可能引发误判 —— 故仍需清洗。 用法 ---- python -m scripts.fix_product_status # 干跑:只打印待修复清单 python -m scripts.fix_product_status --apply # 确认后真正写库 """ from __future__ import annotations import argparse import asyncio from sqlalchemy import select from app.core.database import AsyncSessionLocal from app.core.lifecycle import overall_to_product_status from app.models.product import Product async def main(apply: bool) -> None: async with AsyncSessionLocal() as db: products = ( await db.execute(select(Product).order_by(Product.serial_number)) ).scalars().all() fixes: list[tuple[Product, str, str]] = [] for p in products: expected = overall_to_product_status(p.overall_status) current = (p.status or "").strip() # 大小写不敏感比较:历史数据里存在小写 'pending' if current.upper() == expected: continue fixes.append((p, current or "(空)", expected)) print(f"扫描 {len(products)} 台设备,需修复 {len(fixes)} 台\n") if fixes: header = f"{'序列号':<18}{'overall_status':<16}{'当前 status':<14}→ 目标 status" print(header) print("-" * len(header) * 2) for p, cur, exp in fixes: overall = p.overall_status or "(NULL)" phase = p.lifecycle_phase or "-" print(f"{p.serial_number:<18}{overall:<16}{cur:<14}→ {exp} [{phase}]") else: print("所有设备的 status 均已与 overall_status 对齐。") if not apply: print("\n[干跑] 未写库。确认清单无误后,加 --apply 执行。") return if not fixes: return for p, _cur, exp in fixes: p.status = exp await db.commit() print(f"\n[已提交] {len(fixes)} 台设备的 status 已对齐到 overall_status。") if __name__ == "__main__": parser = argparse.ArgumentParser(description="对齐 Product.status 与 overall_status 的历史脏数据") parser.add_argument("--apply", action="store_true", help="真正写库(默认仅干跑)") asyncio.run(main(parser.parse_args().apply))