pl_paste_recognize_service.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. """运输计划 - 三方粘贴识别批量提交"""
  2. from __future__ import annotations
  3. from typing import Optional
  4. from fastapi import HTTPException
  5. from sqlalchemy.orm import Session
  6. from app.core.utils import generate_uuid
  7. from app.models.pl_models import PlCarrier, PlPersonnel, PlVehicle
  8. from app.schemas.pl.base_data import VehiclePersonnelRow
  9. from app.schemas.pl.paste_recognize import (
  10. ConflictAction,
  11. PasteCommitBody,
  12. PasteCommitResult,
  13. PastePersonnelIn,
  14. PasteVehicleIn,
  15. )
  16. from app.services.pl_personnel_service import save_vehicle_personnel
  17. def _conflict_map(conflicts: list[ConflictAction]) -> dict[str, ConflictAction]:
  18. result: dict[str, ConflictAction] = {}
  19. for item in conflicts:
  20. key = (item.key or "").strip()
  21. if not key:
  22. continue
  23. if key in result:
  24. raise HTTPException(status_code=400, detail=f"冲突项「{key}」重复")
  25. result[key] = item
  26. return result
  27. def _find_personnel_by_id_card(
  28. db: Session, tenant_no: str, id_card_no: Optional[str]
  29. ) -> Optional[PlPersonnel]:
  30. card = (id_card_no or "").strip().upper()
  31. if not card:
  32. return None
  33. return (
  34. db.query(PlPersonnel)
  35. .filter(
  36. PlPersonnel.tenant_no == tenant_no,
  37. PlPersonnel.id_card_no == card,
  38. PlPersonnel.del_flag == 0,
  39. )
  40. .first()
  41. )
  42. def _find_vehicle_by_plate(
  43. db: Session, tenant_no: str, license_plate: str
  44. ) -> Optional[PlVehicle]:
  45. plate = (license_plate or "").strip()
  46. if not plate:
  47. return None
  48. return (
  49. db.query(PlVehicle)
  50. .filter(
  51. PlVehicle.tenant_no == tenant_no,
  52. PlVehicle.license_plate == plate,
  53. PlVehicle.del_flag == 0,
  54. )
  55. .first()
  56. )
  57. def _get_personnel(
  58. db: Session, tenant_no: str, personnel_id: str
  59. ) -> PlPersonnel:
  60. row = (
  61. db.query(PlPersonnel)
  62. .filter(
  63. PlPersonnel.id == personnel_id,
  64. PlPersonnel.tenant_no == tenant_no,
  65. PlPersonnel.del_flag == 0,
  66. )
  67. .first()
  68. )
  69. if not row:
  70. raise HTTPException(status_code=400, detail=f"人员不存在:{personnel_id}")
  71. return row
  72. def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str) -> PlVehicle:
  73. row = (
  74. db.query(PlVehicle)
  75. .filter(
  76. PlVehicle.id == vehicle_id,
  77. PlVehicle.tenant_no == tenant_no,
  78. PlVehicle.del_flag == 0,
  79. )
  80. .first()
  81. )
  82. if not row:
  83. raise HTTPException(status_code=400, detail=f"车辆不存在:{vehicle_id}")
  84. return row
  85. def _resolve_personnel(
  86. db: Session,
  87. tenant_no: str,
  88. *,
  89. key: str,
  90. draft: PastePersonnelIn,
  91. conflict: Optional[ConflictAction],
  92. user_id: str,
  93. dept_id: str,
  94. ) -> str:
  95. name = (draft.personnel_name or "").strip()
  96. if not name:
  97. raise HTTPException(status_code=400, detail=f"{key}姓名不能为空")
  98. existing = _find_personnel_by_id_card(db, tenant_no, draft.id_card_no)
  99. if existing and conflict is None:
  100. raise HTTPException(
  101. status_code=400,
  102. detail=f"人员「{name}」身份证已存在,请先选择复用/覆盖/取消",
  103. )
  104. if conflict is not None:
  105. if conflict.action == "cancel":
  106. raise HTTPException(status_code=400, detail="已取消")
  107. target = _get_personnel(db, tenant_no, conflict.existing_id)
  108. if conflict.action == "reuse":
  109. return target.id
  110. # overwrite
  111. target.personnel_name = name
  112. target.phone_no = (draft.phone_no or "").strip() or None
  113. card = (draft.id_card_no or "").strip().upper() or None
  114. if card:
  115. target.id_card_no = card
  116. target.personnel_type = draft.personnel_type
  117. target.personnel_affiliation = "carrier"
  118. target.update_by = user_id
  119. return target.id
  120. personnel = PlPersonnel(
  121. id=generate_uuid(),
  122. tenant_no=tenant_no,
  123. personnel_name=name,
  124. phone_no=(draft.phone_no or "").strip() or None,
  125. id_card_no=(draft.id_card_no or "").strip().upper() or None,
  126. personnel_type=draft.personnel_type,
  127. personnel_affiliation="carrier",
  128. status="1",
  129. create_by=user_id,
  130. create_dept_id=dept_id,
  131. )
  132. db.add(personnel)
  133. db.flush()
  134. return personnel.id
  135. def _resolve_vehicle(
  136. db: Session,
  137. tenant_no: str,
  138. *,
  139. draft: PasteVehicleIn,
  140. conflict: Optional[ConflictAction],
  141. user_id: str,
  142. dept_id: str,
  143. ) -> str:
  144. plate = (draft.license_plate or "").strip()
  145. if not plate:
  146. raise HTTPException(status_code=400, detail="车牌号不能为空")
  147. carrier_id = (draft.carrier_id or "").strip() or None
  148. carrier_name = (draft.carrier_name or "").strip() or None
  149. if carrier_id:
  150. carrier = (
  151. db.query(PlCarrier)
  152. .filter(
  153. PlCarrier.id == carrier_id,
  154. PlCarrier.tenant_no == tenant_no,
  155. PlCarrier.del_flag == 0,
  156. )
  157. .first()
  158. )
  159. if not carrier:
  160. raise HTTPException(status_code=400, detail="承运商不存在")
  161. carrier_name = carrier_name or carrier.carrier_name
  162. else:
  163. carrier_name = None
  164. existing = _find_vehicle_by_plate(db, tenant_no, plate)
  165. if existing and conflict is None:
  166. raise HTTPException(
  167. status_code=400,
  168. detail=f"车牌「{plate}」已存在,请先选择复用/覆盖/取消",
  169. )
  170. if conflict is not None:
  171. if conflict.action == "cancel":
  172. raise HTTPException(status_code=400, detail="已取消")
  173. target = _get_vehicle(db, tenant_no, conflict.existing_id)
  174. if conflict.action == "reuse":
  175. return target.id
  176. target.trailer_no = (draft.trailer_no or "").strip() or None
  177. target.load_capacity = draft.load_capacity
  178. target.carrier_id = carrier_id
  179. target.carrier_name = carrier_name
  180. target.vehicle_type = "carrier"
  181. target.update_by = user_id
  182. return target.id
  183. vehicle = PlVehicle(
  184. id=generate_uuid(),
  185. tenant_no=tenant_no,
  186. vehicle_type="carrier",
  187. carrier_id=carrier_id,
  188. carrier_name=carrier_name,
  189. license_plate=plate,
  190. trailer_no=(draft.trailer_no or "").strip() or None,
  191. load_capacity=draft.load_capacity,
  192. vehicle_status="available",
  193. create_by=user_id,
  194. create_dept_id=dept_id,
  195. )
  196. db.add(vehicle)
  197. db.flush()
  198. return vehicle.id
  199. def commit_paste_recognize(
  200. db: Session,
  201. tenant_no: str,
  202. body: PasteCommitBody,
  203. *,
  204. user_id: str,
  205. dept_id: str,
  206. ) -> PasteCommitResult:
  207. cmap = _conflict_map(body.conflicts or [])
  208. for item in cmap.values():
  209. if item.action == "cancel":
  210. raise HTTPException(status_code=400, detail="已取消")
  211. need_vehicle = body.mode == "vehicle_bundle" or (
  212. body.mode == "personnel_first" and body.create_vehicle
  213. )
  214. if need_vehicle and body.vehicle is None:
  215. raise HTTPException(status_code=400, detail="缺少车辆信息")
  216. driver_id: Optional[str] = None
  217. escort_id: Optional[str] = None
  218. vehicle_id: Optional[str] = None
  219. if body.driver is not None:
  220. driver_id = _resolve_personnel(
  221. db,
  222. tenant_no,
  223. key="驾驶员",
  224. draft=body.driver,
  225. conflict=cmap.get("driver"),
  226. user_id=user_id,
  227. dept_id=dept_id,
  228. )
  229. if body.escort is not None:
  230. escort_id = _resolve_personnel(
  231. db,
  232. tenant_no,
  233. key="押运员",
  234. draft=body.escort,
  235. conflict=cmap.get("escort"),
  236. user_id=user_id,
  237. dept_id=dept_id,
  238. )
  239. if need_vehicle and body.vehicle is not None:
  240. vehicle_id = _resolve_vehicle(
  241. db,
  242. tenant_no,
  243. draft=body.vehicle,
  244. conflict=cmap.get("vehicle"),
  245. user_id=user_id,
  246. dept_id=dept_id,
  247. )
  248. rows: list[VehiclePersonnelRow] = []
  249. if driver_id and body.driver is not None:
  250. rows.append(
  251. VehiclePersonnelRow(
  252. personnelId=driver_id,
  253. personnelName=body.driver.personnel_name.strip(),
  254. phoneNo=(body.driver.phone_no or "").strip() or None,
  255. role="driver",
  256. isDefault=1,
  257. )
  258. )
  259. if escort_id and body.escort is not None:
  260. rows.append(
  261. VehiclePersonnelRow(
  262. personnelId=escort_id,
  263. personnelName=body.escort.personnel_name.strip(),
  264. phoneNo=(body.escort.phone_no or "").strip() or None,
  265. role="escort",
  266. isDefault=0,
  267. )
  268. )
  269. if rows:
  270. save_vehicle_personnel(
  271. db,
  272. vehicle_id,
  273. rows,
  274. tenant_no,
  275. user_id=user_id,
  276. dept_id=dept_id,
  277. vehicle_type="carrier",
  278. )
  279. if body.mode == "personnel_first" and not need_vehicle:
  280. if not driver_id and not escort_id:
  281. raise HTTPException(status_code=400, detail="请至少识别一名人员")
  282. if body.mode == "vehicle_bundle" and not vehicle_id:
  283. raise HTTPException(status_code=400, detail="车辆创建失败")
  284. return PasteCommitResult(
  285. vehicleId=vehicle_id,
  286. driverId=driver_id,
  287. escortId=escort_id,
  288. )