pl_paste_recognize_service.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. trailer_no = (draft.trailer_no or "").strip()
  177. if not trailer_no:
  178. raise HTTPException(status_code=400, detail="挂车号不能为空")
  179. target.trailer_no = trailer_no
  180. target.load_capacity = draft.load_capacity
  181. target.carrier_id = carrier_id
  182. target.carrier_name = carrier_name
  183. target.vehicle_type = "carrier"
  184. target.update_by = user_id
  185. return target.id
  186. trailer_no = (draft.trailer_no or "").strip()
  187. if not trailer_no:
  188. raise HTTPException(status_code=400, detail="挂车号不能为空")
  189. vehicle = PlVehicle(
  190. id=generate_uuid(),
  191. tenant_no=tenant_no,
  192. vehicle_type="carrier",
  193. carrier_id=carrier_id,
  194. carrier_name=carrier_name,
  195. license_plate=plate,
  196. trailer_no=trailer_no,
  197. load_capacity=draft.load_capacity,
  198. vehicle_status="available",
  199. create_by=user_id,
  200. create_dept_id=dept_id,
  201. )
  202. db.add(vehicle)
  203. db.flush()
  204. return vehicle.id
  205. def commit_paste_recognize(
  206. db: Session,
  207. tenant_no: str,
  208. body: PasteCommitBody,
  209. *,
  210. user_id: str,
  211. dept_id: str,
  212. ) -> PasteCommitResult:
  213. cmap = _conflict_map(body.conflicts or [])
  214. for item in cmap.values():
  215. if item.action == "cancel":
  216. raise HTTPException(status_code=400, detail="已取消")
  217. need_vehicle = body.mode == "vehicle_bundle" or (
  218. body.mode == "personnel_first" and body.create_vehicle
  219. )
  220. if need_vehicle and body.vehicle is None:
  221. raise HTTPException(status_code=400, detail="缺少车辆信息")
  222. driver_id: Optional[str] = None
  223. escort_id: Optional[str] = None
  224. vehicle_id: Optional[str] = None
  225. if body.driver is not None:
  226. driver_id = _resolve_personnel(
  227. db,
  228. tenant_no,
  229. key="驾驶员",
  230. draft=body.driver,
  231. conflict=cmap.get("driver"),
  232. user_id=user_id,
  233. dept_id=dept_id,
  234. )
  235. if body.escort is not None:
  236. escort_id = _resolve_personnel(
  237. db,
  238. tenant_no,
  239. key="押运员",
  240. draft=body.escort,
  241. conflict=cmap.get("escort"),
  242. user_id=user_id,
  243. dept_id=dept_id,
  244. )
  245. if need_vehicle and body.vehicle is not None:
  246. vehicle_id = _resolve_vehicle(
  247. db,
  248. tenant_no,
  249. draft=body.vehicle,
  250. conflict=cmap.get("vehicle"),
  251. user_id=user_id,
  252. dept_id=dept_id,
  253. )
  254. rows: list[VehiclePersonnelRow] = []
  255. if driver_id and body.driver is not None:
  256. rows.append(
  257. VehiclePersonnelRow(
  258. personnelId=driver_id,
  259. personnelName=body.driver.personnel_name.strip(),
  260. phoneNo=(body.driver.phone_no or "").strip() or None,
  261. role="driver",
  262. isDefault=1,
  263. )
  264. )
  265. if escort_id and body.escort is not None:
  266. rows.append(
  267. VehiclePersonnelRow(
  268. personnelId=escort_id,
  269. personnelName=body.escort.personnel_name.strip(),
  270. phoneNo=(body.escort.phone_no or "").strip() or None,
  271. role="escort",
  272. isDefault=0,
  273. )
  274. )
  275. if rows:
  276. save_vehicle_personnel(
  277. db,
  278. vehicle_id,
  279. rows,
  280. tenant_no,
  281. user_id=user_id,
  282. dept_id=dept_id,
  283. vehicle_type="carrier",
  284. )
  285. if body.mode == "personnel_first" and not need_vehicle:
  286. if not driver_id and not escort_id:
  287. raise HTTPException(status_code=400, detail="请至少识别一名人员")
  288. if body.mode == "vehicle_bundle" and not vehicle_id:
  289. raise HTTPException(status_code=400, detail="车辆创建失败")
  290. return PasteCommitResult(
  291. vehicleId=vehicle_id,
  292. driverId=driver_id,
  293. escortId=escort_id,
  294. )