"""运输计划 - 三方粘贴识别批量提交""" from __future__ import annotations from typing import Optional from fastapi import HTTPException from sqlalchemy.orm import Session from app.core.utils import generate_uuid from app.models.pl_models import PlCarrier, PlPersonnel, PlVehicle from app.schemas.pl.base_data import VehiclePersonnelRow from app.schemas.pl.paste_recognize import ( ConflictAction, PasteCommitBody, PasteCommitResult, PastePersonnelIn, PasteVehicleIn, ) from app.services.pl_personnel_service import save_vehicle_personnel def _conflict_map(conflicts: list[ConflictAction]) -> dict[str, ConflictAction]: result: dict[str, ConflictAction] = {} for item in conflicts: key = (item.key or "").strip() if not key: continue if key in result: raise HTTPException(status_code=400, detail=f"冲突项「{key}」重复") result[key] = item return result def _find_personnel_by_id_card( db: Session, tenant_no: str, id_card_no: Optional[str] ) -> Optional[PlPersonnel]: card = (id_card_no or "").strip().upper() if not card: return None return ( db.query(PlPersonnel) .filter( PlPersonnel.tenant_no == tenant_no, PlPersonnel.id_card_no == card, PlPersonnel.del_flag == 0, ) .first() ) def _find_vehicle_by_plate( db: Session, tenant_no: str, license_plate: str ) -> Optional[PlVehicle]: plate = (license_plate or "").strip() if not plate: return None return ( db.query(PlVehicle) .filter( PlVehicle.tenant_no == tenant_no, PlVehicle.license_plate == plate, PlVehicle.del_flag == 0, ) .first() ) def _get_personnel( db: Session, tenant_no: str, personnel_id: str ) -> PlPersonnel: row = ( db.query(PlPersonnel) .filter( PlPersonnel.id == personnel_id, PlPersonnel.tenant_no == tenant_no, PlPersonnel.del_flag == 0, ) .first() ) if not row: raise HTTPException(status_code=400, detail=f"人员不存在:{personnel_id}") return row def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str) -> PlVehicle: row = ( db.query(PlVehicle) .filter( PlVehicle.id == vehicle_id, PlVehicle.tenant_no == tenant_no, PlVehicle.del_flag == 0, ) .first() ) if not row: raise HTTPException(status_code=400, detail=f"车辆不存在:{vehicle_id}") return row def _resolve_personnel( db: Session, tenant_no: str, *, key: str, draft: PastePersonnelIn, conflict: Optional[ConflictAction], user_id: str, dept_id: str, ) -> str: name = (draft.personnel_name or "").strip() if not name: raise HTTPException(status_code=400, detail=f"{key}姓名不能为空") existing = _find_personnel_by_id_card(db, tenant_no, draft.id_card_no) if existing and conflict is None: raise HTTPException( status_code=400, detail=f"人员「{name}」身份证已存在,请先选择复用/覆盖/取消", ) if conflict is not None: if conflict.action == "cancel": raise HTTPException(status_code=400, detail="已取消") target = _get_personnel(db, tenant_no, conflict.existing_id) if conflict.action == "reuse": return target.id # overwrite target.personnel_name = name target.phone_no = (draft.phone_no or "").strip() or None card = (draft.id_card_no or "").strip().upper() or None if card: target.id_card_no = card target.personnel_type = draft.personnel_type target.personnel_affiliation = "carrier" target.update_by = user_id return target.id personnel = PlPersonnel( id=generate_uuid(), tenant_no=tenant_no, personnel_name=name, phone_no=(draft.phone_no or "").strip() or None, id_card_no=(draft.id_card_no or "").strip().upper() or None, personnel_type=draft.personnel_type, personnel_affiliation="carrier", status="1", create_by=user_id, create_dept_id=dept_id, ) db.add(personnel) db.flush() return personnel.id def _resolve_vehicle( db: Session, tenant_no: str, *, draft: PasteVehicleIn, conflict: Optional[ConflictAction], user_id: str, dept_id: str, ) -> str: plate = (draft.license_plate or "").strip() if not plate: raise HTTPException(status_code=400, detail="车牌号不能为空") carrier_id = (draft.carrier_id or "").strip() if not carrier_id: raise HTTPException(status_code=400, detail="请选择承运商") carrier = ( db.query(PlCarrier) .filter( PlCarrier.id == carrier_id, PlCarrier.tenant_no == tenant_no, PlCarrier.del_flag == 0, ) .first() ) if not carrier: raise HTTPException(status_code=400, detail="承运商不存在") carrier_name = (draft.carrier_name or "").strip() or carrier.carrier_name existing = _find_vehicle_by_plate(db, tenant_no, plate) if existing and conflict is None: raise HTTPException( status_code=400, detail=f"车牌「{plate}」已存在,请先选择复用/覆盖/取消", ) if conflict is not None: if conflict.action == "cancel": raise HTTPException(status_code=400, detail="已取消") target = _get_vehicle(db, tenant_no, conflict.existing_id) if conflict.action == "reuse": return target.id target.trailer_no = (draft.trailer_no or "").strip() or None target.load_capacity = draft.load_capacity target.carrier_id = carrier_id target.carrier_name = carrier_name target.vehicle_type = "carrier" target.update_by = user_id return target.id vehicle = PlVehicle( id=generate_uuid(), tenant_no=tenant_no, vehicle_type="carrier", carrier_id=carrier_id, carrier_name=carrier_name, license_plate=plate, trailer_no=(draft.trailer_no or "").strip() or None, load_capacity=draft.load_capacity, vehicle_status="available", create_by=user_id, create_dept_id=dept_id, ) db.add(vehicle) db.flush() return vehicle.id def commit_paste_recognize( db: Session, tenant_no: str, body: PasteCommitBody, *, user_id: str, dept_id: str, ) -> PasteCommitResult: cmap = _conflict_map(body.conflicts or []) for item in cmap.values(): if item.action == "cancel": raise HTTPException(status_code=400, detail="已取消") need_vehicle = body.mode == "vehicle_bundle" or ( body.mode == "personnel_first" and body.create_vehicle ) if need_vehicle and body.vehicle is None: raise HTTPException(status_code=400, detail="缺少车辆信息") driver_id: Optional[str] = None escort_id: Optional[str] = None vehicle_id: Optional[str] = None if body.driver is not None: driver_id = _resolve_personnel( db, tenant_no, key="驾驶员", draft=body.driver, conflict=cmap.get("driver"), user_id=user_id, dept_id=dept_id, ) if body.escort is not None: escort_id = _resolve_personnel( db, tenant_no, key="押运员", draft=body.escort, conflict=cmap.get("escort"), user_id=user_id, dept_id=dept_id, ) if need_vehicle and body.vehicle is not None: vehicle_id = _resolve_vehicle( db, tenant_no, draft=body.vehicle, conflict=cmap.get("vehicle"), user_id=user_id, dept_id=dept_id, ) rows: list[VehiclePersonnelRow] = [] if driver_id and body.driver is not None: rows.append( VehiclePersonnelRow( personnelId=driver_id, personnelName=body.driver.personnel_name.strip(), phoneNo=(body.driver.phone_no or "").strip() or None, role="driver", isDefault=1, ) ) if escort_id and body.escort is not None: rows.append( VehiclePersonnelRow( personnelId=escort_id, personnelName=body.escort.personnel_name.strip(), phoneNo=(body.escort.phone_no or "").strip() or None, role="escort", isDefault=0, ) ) if rows: save_vehicle_personnel( db, vehicle_id, rows, tenant_no, user_id=user_id, dept_id=dept_id, vehicle_type="carrier", ) if body.mode == "personnel_first" and not need_vehicle: if not driver_id and not escort_id: raise HTTPException(status_code=400, detail="请至少识别一名人员") if body.mode == "vehicle_bundle" and not vehicle_id: raise HTTPException(status_code=400, detail="车辆创建失败") return PasteCommitResult( vehicleId=vehicle_id, driverId=driver_id, escortId=escort_id, )