chore(pl): add script to remount orphaned transport dibang stop links.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4e3c2a9ffb
commit
45ed795501
2 files changed
+354
No files matched your search
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
修复 pl_transport_db_link 悬空的 stop_id / plan_product_id。
|
||||
|
||||
段主键 upsert 上线前,计划保存会软删重建送货段,导致 link 仍指向旧 stop/product,
|
||||
详情「送货信息段次」过滤不到地磅单。
|
||||
|
||||
用法(在 backend 目录、已配置 DATABASE_URL / .env):
|
||||
python scripts/fix_pl_transport_db_link_stop_ids.py --plan-no PO20260904024
|
||||
python scripts/fix_pl_transport_db_link_stop_ids.py --plan-no PO20260904024 --apply
|
||||
python scripts/fix_pl_transport_db_link_stop_ids.py --apply # 全租户扫描
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.models.pl_models import ( # noqa: E402
|
||||
PlPlan,
|
||||
PlPlanDelivery,
|
||||
PlPlanLoad,
|
||||
PlPlanProduct,
|
||||
PlTransportDbLink,
|
||||
)
|
||||
|
||||
DEFAULT_TENANT = "shanxia2.0"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FixProposal:
|
||||
link_id: str
|
||||
plan_id: str
|
||||
plan_no: str
|
||||
old_stop_id: Optional[str]
|
||||
new_stop_id: Optional[str]
|
||||
old_plan_product_id: str
|
||||
new_plan_product_id: Optional[str]
|
||||
reason: str
|
||||
can_apply: bool
|
||||
|
||||
|
||||
def _engine_from_env() -> Engine:
|
||||
url = os.environ.get("DATABASE_URL")
|
||||
if not url:
|
||||
from app.core.config import settings
|
||||
|
||||
url = settings.DATABASE_URL
|
||||
return create_engine(url, pool_pre_ping=True)
|
||||
|
||||
|
||||
def _stop_alive(db: Session, *, stop_type: Optional[str], stop_id: Optional[str], tenant_no: str) -> bool:
|
||||
if not stop_id:
|
||||
return True
|
||||
if stop_type == "load":
|
||||
return (
|
||||
db.query(PlPlanLoad.id)
|
||||
.filter(PlPlanLoad.id == stop_id, PlPlanLoad.tenant_no == tenant_no, PlPlanLoad.del_flag == 0)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
if stop_type == "delivery":
|
||||
return (
|
||||
db.query(PlPlanDelivery.id)
|
||||
.filter(
|
||||
PlPlanDelivery.id == stop_id,
|
||||
PlPlanDelivery.tenant_no == tenant_no,
|
||||
PlPlanDelivery.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _product_alive(db: Session, plan_product_id: str) -> bool:
|
||||
if not plan_product_id:
|
||||
return False
|
||||
return (
|
||||
db.query(PlPlanProduct.id)
|
||||
.filter(PlPlanProduct.id == plan_product_id, PlPlanProduct.del_flag == 0)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def _resolve_delivery_stop_id(
|
||||
db: Session,
|
||||
*,
|
||||
plan_id: str,
|
||||
tenant_no: str,
|
||||
old_stop_id: Optional[str],
|
||||
) -> tuple[Optional[str], str]:
|
||||
active = (
|
||||
db.query(PlPlanDelivery)
|
||||
.filter(
|
||||
PlPlanDelivery.plan_id == plan_id,
|
||||
PlPlanDelivery.tenant_no == tenant_no,
|
||||
PlPlanDelivery.del_flag == 0,
|
||||
)
|
||||
.order_by(PlPlanDelivery.seq.asc(), PlPlanDelivery.create_time.asc())
|
||||
.all()
|
||||
)
|
||||
if not active:
|
||||
return None, "无有效送货段可改挂"
|
||||
if len(active) == 1:
|
||||
return active[0].id, "仅一条有效送货段,唯一匹配"
|
||||
|
||||
old_name = None
|
||||
if old_stop_id:
|
||||
old = db.query(PlPlanDelivery).filter(PlPlanDelivery.id == old_stop_id).first()
|
||||
if old:
|
||||
old_name = (old.customer_name or "").strip()
|
||||
if old_name:
|
||||
matched = [d for d in active if (d.customer_name or "").strip() == old_name]
|
||||
if len(matched) == 1:
|
||||
return matched[0].id, f"按客户名匹配: {old_name}"
|
||||
if len(matched) > 1:
|
||||
return None, f"同客户名送货段不唯一: {old_name}"
|
||||
return None, "多条送货段且无法唯一匹配客户名"
|
||||
|
||||
|
||||
def _resolve_load_stop_id(
|
||||
db: Session,
|
||||
*,
|
||||
plan_id: str,
|
||||
tenant_no: str,
|
||||
) -> tuple[Optional[str], str]:
|
||||
active = (
|
||||
db.query(PlPlanLoad)
|
||||
.filter(
|
||||
PlPlanLoad.plan_id == plan_id,
|
||||
PlPlanLoad.tenant_no == tenant_no,
|
||||
PlPlanLoad.del_flag == 0,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if not active:
|
||||
return None, "无有效装货段可改挂"
|
||||
if len(active) == 1:
|
||||
return active[0].id, "仅一条有效装货段,唯一匹配"
|
||||
return None, "多条装货段无法自动改挂"
|
||||
|
||||
|
||||
def _resolve_plan_product_id(
|
||||
db: Session,
|
||||
*,
|
||||
plan_id: str,
|
||||
tenant_no: str,
|
||||
old_plan_product_id: str,
|
||||
) -> tuple[Optional[str], str]:
|
||||
if _product_alive(db, old_plan_product_id):
|
||||
return old_plan_product_id, "计划产品仍有效"
|
||||
old = (
|
||||
db.query(PlPlanProduct)
|
||||
.filter(PlPlanProduct.id == old_plan_product_id, PlPlanProduct.plan_id == plan_id)
|
||||
.first()
|
||||
)
|
||||
crm_product_id = (old.product_id if old else "") or ""
|
||||
if not crm_product_id:
|
||||
return None, "旧计划产品无 product_id,无法改挂"
|
||||
active = (
|
||||
db.query(PlPlanProduct)
|
||||
.filter(
|
||||
PlPlanProduct.plan_id == plan_id,
|
||||
PlPlanProduct.tenant_no == tenant_no,
|
||||
PlPlanProduct.del_flag == 0,
|
||||
PlPlanProduct.product_id == crm_product_id,
|
||||
)
|
||||
.order_by(PlPlanProduct.order_num.asc(), PlPlanProduct.create_time.asc())
|
||||
.all()
|
||||
)
|
||||
if len(active) == 1:
|
||||
return active[0].id, f"按 CRM product_id 匹配: {crm_product_id}"
|
||||
if not active:
|
||||
return None, f"无有效计划产品匹配 product_id={crm_product_id}"
|
||||
return None, f"多条计划产品匹配 product_id={crm_product_id}"
|
||||
|
||||
|
||||
def collect_proposals(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
plan_no: Optional[str],
|
||||
) -> list[FixProposal]:
|
||||
q = (
|
||||
db.query(PlTransportDbLink, PlPlan)
|
||||
.join(PlPlan, PlPlan.id == PlTransportDbLink.plan_id)
|
||||
.filter(
|
||||
PlTransportDbLink.tenant_no == tenant_no,
|
||||
PlTransportDbLink.del_flag == 0,
|
||||
PlPlan.tenant_no == tenant_no,
|
||||
PlPlan.del_flag == 0,
|
||||
)
|
||||
)
|
||||
if plan_no:
|
||||
q = q.filter(PlPlan.plan_no == plan_no)
|
||||
|
||||
proposals: list[FixProposal] = []
|
||||
for link, plan in q.all():
|
||||
stop_type = (link.stop_type or "").strip() or None
|
||||
old_stop_id = (link.stop_id or "").strip() or None
|
||||
old_pp = (link.plan_product_id or "").strip()
|
||||
|
||||
reasons: list[str] = []
|
||||
new_stop_id = old_stop_id
|
||||
stop_ok = True
|
||||
if not _stop_alive(db, stop_type=stop_type, stop_id=old_stop_id, tenant_no=tenant_no):
|
||||
if stop_type == "delivery":
|
||||
new_stop_id, stop_reason = _resolve_delivery_stop_id(
|
||||
db, plan_id=plan.id, tenant_no=tenant_no, old_stop_id=old_stop_id
|
||||
)
|
||||
elif stop_type == "load":
|
||||
new_stop_id, stop_reason = _resolve_load_stop_id(
|
||||
db, plan_id=plan.id, tenant_no=tenant_no
|
||||
)
|
||||
else:
|
||||
new_stop_id, stop_reason = None, f"未知 stop_type={stop_type}"
|
||||
reasons.append(f"stop: {stop_reason}")
|
||||
stop_ok = bool(new_stop_id)
|
||||
|
||||
new_pp = old_pp
|
||||
product_ok = True
|
||||
if not _product_alive(db, old_pp):
|
||||
new_pp, pp_reason = _resolve_plan_product_id(
|
||||
db, plan_id=plan.id, tenant_no=tenant_no, old_plan_product_id=old_pp
|
||||
)
|
||||
reasons.append(f"product: {pp_reason}")
|
||||
product_ok = bool(new_pp)
|
||||
|
||||
if not reasons:
|
||||
continue
|
||||
|
||||
proposals.append(
|
||||
FixProposal(
|
||||
link_id=link.id,
|
||||
plan_id=plan.id,
|
||||
plan_no=plan.plan_no or "",
|
||||
old_stop_id=old_stop_id,
|
||||
new_stop_id=new_stop_id,
|
||||
old_plan_product_id=old_pp,
|
||||
new_plan_product_id=new_pp,
|
||||
reason="; ".join(reasons),
|
||||
can_apply=stop_ok and product_ok,
|
||||
)
|
||||
)
|
||||
return proposals
|
||||
|
||||
|
||||
def apply_proposals(db: Session, proposals: list[FixProposal]) -> int:
|
||||
applied = 0
|
||||
now = datetime.now()
|
||||
for p in proposals:
|
||||
if not p.can_apply:
|
||||
print(f" SKIP link={p.link_id} plan={p.plan_no}: {p.reason}")
|
||||
continue
|
||||
link = (
|
||||
db.query(PlTransportDbLink)
|
||||
.filter(PlTransportDbLink.id == p.link_id, PlTransportDbLink.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
if not link:
|
||||
continue
|
||||
changed = False
|
||||
if p.new_stop_id and p.new_stop_id != (link.stop_id or ""):
|
||||
link.stop_id = p.new_stop_id
|
||||
changed = True
|
||||
if p.new_plan_product_id and p.new_plan_product_id != (link.plan_product_id or ""):
|
||||
link.plan_product_id = p.new_plan_product_id
|
||||
changed = True
|
||||
if changed:
|
||||
link.update_time = now
|
||||
applied += 1
|
||||
print(
|
||||
f" APPLY link={p.link_id} plan={p.plan_no}: "
|
||||
f"stop_id {p.old_stop_id} → {p.new_stop_id}; "
|
||||
f"plan_product_id {p.old_plan_product_id} → {p.new_plan_product_id}"
|
||||
)
|
||||
return applied
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="修复悬空的运输地磅 link stop/product id")
|
||||
parser.add_argument("--apply", action="store_true", help="实际写库(默认 dry-run)")
|
||||
parser.add_argument("--plan-no", default=None, help="仅处理指定计划编号")
|
||||
parser.add_argument("--tenant", default=DEFAULT_TENANT, help="租户编号")
|
||||
args = parser.parse_args()
|
||||
|
||||
engine = _engine_from_env()
|
||||
parsed = urlparse(str(engine.url))
|
||||
print(f"目标库: {parsed.path.lstrip('/')} @ {parsed.hostname}:{parsed.port or 3306}")
|
||||
print(f"租户: {args.tenant}")
|
||||
print(f"模式: {'APPLY' if args.apply else 'DRY-RUN'}")
|
||||
if args.plan_no:
|
||||
print(f"计划: {args.plan_no}")
|
||||
print("---")
|
||||
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
proposals = collect_proposals(db, tenant_no=args.tenant, plan_no=args.plan_no)
|
||||
if not proposals:
|
||||
print("无可修复的悬空 link。")
|
||||
return
|
||||
for p in proposals:
|
||||
flag = "OK" if p.can_apply else "NEED-MANUAL"
|
||||
print(
|
||||
f"[{flag}] link={p.link_id} plan={p.plan_no} ({p.plan_id})\n"
|
||||
f" stop_id {p.old_stop_id} → {p.new_stop_id}\n"
|
||||
f" plan_product_id {p.old_plan_product_id} → {p.new_plan_product_id}\n"
|
||||
f" reason: {p.reason}"
|
||||
)
|
||||
print("---")
|
||||
if not args.apply:
|
||||
print(f"共 {len(proposals)} 条候选。加 --apply 写库。")
|
||||
return
|
||||
n = apply_proposals(db, proposals)
|
||||
db.commit()
|
||||
print(f"已更新 {n} 条 link。")
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user