fix: 售后回流口径统一 — 状态双字段同步、工序名归一、标签配色
产品存在 overall_status 与 status 两个状态字段,此前各写入点各写一份映射、 甚至只改 overall_status 不改 status,导致回流设备(product.status 停留在 OUTBOUND)污染看板统计口径。 - lifecycle.py: 把映射表收敛为单一来源 overall_to_product_status / sync_product_status;新增 normalize_after_sales_step,将售后设备沿用生产 阶段写法的历史工序名(测试/维修)折算到售后区独立工序名 - product_service.py: 删除本地 _OVERALL_TO_STATUS 副本,改用共享函数 - dashboard_service.py: WIP 矩阵补出 lifecycle_phase 列,活跃任务判定 (is_active) 提前到所有终结态判定之前,避免残留 OUTBOUND 被误判为完结 - scripts/fix_product_status.py: 历史数据修复脚本(一次性) - constants/task.ts: 售后工序标签由红色改紫色 —— 红色在本系统是「驳回/危险」 语义色,售后只是另一条流转支线,用红色会让操作员误以为设备报错
This commit is contained in:
1
backend/scripts/__init__.py
Normal file
1
backend/scripts/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""一次性运维脚本集合(按模块方式运行:python -m scripts.<name>)"""
|
||||
84
backend/scripts/fix_product_status.py
Normal file
84
backend/scripts/fix_product_status.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""一次性数据清洗 — 修复 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))
|
||||
Reference in New Issue
Block a user