base_data.py 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. """运输计划管理 - 基础数据 API(承运商、车辆、人员)"""
  2. from datetime import datetime
  3. from typing import Optional
  4. from fastapi import APIRouter, Depends, HTTPException, Query, status
  5. from sqlalchemy import case, func, or_
  6. from sqlalchemy.orm import Session
  7. from app.core.database import get_db
  8. from app.core.db_auth import DbUserContext, get_db_current_user
  9. from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
  10. from app.core.tenant_context import get_tenant_no
  11. from app.core.utils import generate_uuid
  12. from app.models.pl_models import (
  13. PlCarrier,
  14. PlPersonnel,
  15. PlVehicle,
  16. PlVehicleDriver,
  17. PlVehiclePersonnel,
  18. )
  19. from app.schemas.pl import (
  20. CarrierAuditReject,
  21. CarrierCreate,
  22. CarrierListResponse,
  23. CarrierRead,
  24. CarrierUpdate,
  25. carrier_to_read,
  26. CertRead,
  27. DriverCreate,
  28. DriverListResponse,
  29. DriverRead,
  30. DriverUpdate,
  31. PersonnelCreate,
  32. PersonnelDetailRead,
  33. PersonnelListResponse,
  34. PersonnelRead,
  35. PersonnelUpdate,
  36. VehicleCreate,
  37. VehicleDetailRead,
  38. VehicleDriverCreate,
  39. VehicleListResponse,
  40. VehicleRead,
  41. VehicleUpdate,
  42. )
  43. from app.services.pl_cert_service import (
  44. certs_to_read,
  45. list_owner_certs,
  46. list_owner_certs_batch,
  47. nearest_expire_date,
  48. save_owner_certs,
  49. )
  50. from app.services.pl_cert_required_service import (
  51. validate_personnel_required_certs,
  52. validate_vehicle_save_certs,
  53. )
  54. from app.services.pl_carrier_service import carrier_eligible_for_transport, normalize_carrier_urls
  55. from app.services.pl_personnel_service import (
  56. load_vehicle_personnel_rows,
  57. load_vehicle_personnel_summaries,
  58. save_vehicle_personnel,
  59. )
  60. from app.schemas.pl.paste_recognize import PasteCommitBody, PasteCommitResult
  61. from app.services.pl_paste_recognize_service import commit_paste_recognize
  62. router = APIRouter(prefix="", tags=["运输计划-基础数据"])
  63. # =============================================================================
  64. # 承运商 API
  65. # =============================================================================
  66. @router.get("/carrier", response_model=CarrierListResponse)
  67. def list_carriers(
  68. q: Optional[str] = Query(default=None, description="模糊搜索"),
  69. status: Optional[str] = Query(default=None, description="状态"),
  70. audit_status: Optional[str] = Query(default=None, alias="auditStatus", description="审核状态"),
  71. page: int = Query(default=1, ge=1),
  72. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  73. db: Session = Depends(get_db),
  74. user: DbUserContext = Depends(get_db_current_user),
  75. ) -> CarrierListResponse:
  76. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  77. query = db.query(PlCarrier).filter(
  78. PlCarrier.tenant_no == tenant_no,
  79. PlCarrier.del_flag == 0,
  80. )
  81. if status:
  82. query = query.filter(PlCarrier.status == status)
  83. if audit_status:
  84. query = query.filter(PlCarrier.audit_status == audit_status)
  85. if q:
  86. query = query.filter(
  87. or_(
  88. func.lower(PlCarrier.carrier_name).contains(q.lower()),
  89. func.lower(PlCarrier.contact_name).contains(q.lower()),
  90. )
  91. )
  92. # 承运商为物流主数据,列表不做 create_by 数据范围过滤
  93. query = query.order_by(
  94. case((PlCarrier.order_num.is_(None), 1), else_=0).asc(),
  95. PlCarrier.create_time.desc(),
  96. )
  97. total = query.count()
  98. offset = (page - 1) * page_size
  99. rows = query.offset(offset).limit(page_size).all()
  100. return CarrierListResponse(
  101. total=total,
  102. list=[carrier_to_read(row) for row in rows],
  103. )
  104. @router.post("/carrier", response_model=CarrierRead, status_code=status.HTTP_201_CREATED)
  105. def create_carrier(
  106. payload: CarrierCreate,
  107. db: Session = Depends(get_db),
  108. user: DbUserContext = Depends(get_db_current_user),
  109. ) -> CarrierRead:
  110. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  111. carrier = PlCarrier(
  112. id=generate_uuid(),
  113. tenant_no=tenant_no,
  114. carrier_name=payload.carrier_name,
  115. contact_name=payload.contact_name,
  116. contact_phone=payload.contact_phone,
  117. status=payload.status,
  118. business_license_urls=normalize_carrier_urls(payload.business_license_urls),
  119. road_transport_license_urls=normalize_carrier_urls(payload.road_transport_license_urls),
  120. transport_agreement_urls=normalize_carrier_urls(payload.transport_agreement_urls),
  121. transport_agreement_expire_date=payload.transport_agreement_expire_date,
  122. audit_status="pending",
  123. remark=payload.remark,
  124. create_by=user.id,
  125. create_dept_id=user.dept_id,
  126. )
  127. db.add(carrier)
  128. db.commit()
  129. db.refresh(carrier)
  130. return carrier_to_read(carrier)
  131. @router.get("/carrier/{id}", response_model=CarrierRead)
  132. def get_carrier(
  133. id: str,
  134. db: Session = Depends(get_db),
  135. user: DbUserContext = Depends(get_db_current_user),
  136. ) -> CarrierRead:
  137. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  138. carrier = _get_carrier(db, tenant_no, id, user)
  139. return carrier_to_read(carrier)
  140. @router.put("/carrier/{id}", response_model=CarrierRead)
  141. def update_carrier(
  142. id: str,
  143. payload: CarrierUpdate,
  144. db: Session = Depends(get_db),
  145. user: DbUserContext = Depends(get_db_current_user),
  146. ) -> CarrierRead:
  147. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  148. carrier = _get_carrier(db, tenant_no, id, user)
  149. payload_data = payload.model_dump(exclude_unset=True)
  150. for key in (
  151. "business_license_urls",
  152. "road_transport_license_urls",
  153. "transport_agreement_urls",
  154. ):
  155. if key in payload_data:
  156. payload_data[key] = normalize_carrier_urls(payload_data[key])
  157. for key, value in payload_data.items():
  158. setattr(carrier, key, value)
  159. carrier.update_by = user.id
  160. db.commit()
  161. db.refresh(carrier)
  162. return carrier_to_read(carrier)
  163. @router.delete("/carrier/{id}", status_code=status.HTTP_204_NO_CONTENT)
  164. def delete_carrier(
  165. id: str,
  166. db: Session = Depends(get_db),
  167. user: DbUserContext = Depends(get_db_current_user),
  168. ) -> None:
  169. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  170. carrier = _get_carrier(db, tenant_no, id, user)
  171. assert_row_deletable_by_creator(carrier, user_info_from_context(user))
  172. carrier.del_flag = 1
  173. carrier.update_by = user.id
  174. db.commit()
  175. @router.post("/carrier/{id}/approve", response_model=CarrierRead)
  176. def approve_carrier(
  177. id: str,
  178. db: Session = Depends(get_db),
  179. user: DbUserContext = Depends(get_db_current_user),
  180. ) -> CarrierRead:
  181. raise HTTPException(status_code=400, detail="承运商审核流程已停用")
  182. @router.post("/carrier/{id}/reject", response_model=CarrierRead)
  183. def reject_carrier(
  184. id: str,
  185. payload: CarrierAuditReject,
  186. db: Session = Depends(get_db),
  187. user: DbUserContext = Depends(get_db_current_user),
  188. ) -> CarrierRead:
  189. raise HTTPException(status_code=400, detail="承运商审核流程已停用")
  190. def _get_carrier(db: Session, tenant_no: str, carrier_id: str, user: DbUserContext) -> PlCarrier:
  191. carrier = (
  192. db.query(PlCarrier)
  193. .filter(
  194. PlCarrier.id == carrier_id,
  195. PlCarrier.tenant_no == tenant_no,
  196. PlCarrier.del_flag == 0,
  197. )
  198. .first()
  199. )
  200. if not carrier:
  201. raise HTTPException(status_code=404, detail="承运商不存在")
  202. return carrier
  203. # =============================================================================
  204. # 车辆 API
  205. # =============================================================================
  206. def _vehicle_to_read(
  207. vehicle: PlVehicle,
  208. summary: Optional[str] = None,
  209. default_driver_name: Optional[str] = None,
  210. *,
  211. cert_list: Optional[list[CertRead]] = None,
  212. ) -> VehicleRead:
  213. data = VehicleRead.model_validate(vehicle)
  214. if summary is not None:
  215. data.personnel_summary = summary
  216. if default_driver_name is not None:
  217. data.default_driver_name = default_driver_name
  218. if cert_list is not None:
  219. data.cert_list = cert_list
  220. return data
  221. def _vehicle_to_detail(
  222. db: Session, tenant_no: str, vehicle: PlVehicle
  223. ) -> VehicleDetailRead:
  224. summaries = load_vehicle_personnel_summaries(db, tenant_no, [vehicle.id])
  225. summary, default_name = summaries.get(vehicle.id, ("", None))
  226. personnel_list = load_vehicle_personnel_rows(db, tenant_no, vehicle.id)
  227. cert_list = certs_to_read(
  228. list_owner_certs(db, tenant_no, "vehicle", vehicle.id)
  229. )
  230. base = _vehicle_to_read(vehicle, summary, default_name)
  231. return VehicleDetailRead(
  232. **base.model_dump(by_alias=False, exclude={"cert_list"}),
  233. personnel_list=personnel_list,
  234. cert_list=cert_list,
  235. )
  236. @router.get("/vehicle", response_model=VehicleListResponse)
  237. def list_vehicles(
  238. q: Optional[str] = Query(default=None, description="模糊搜索车牌号"),
  239. vehicle_type: Optional[str] = Query(default=None, alias="vehicleType", description="车辆类型"),
  240. carrier_id: Optional[str] = Query(default=None, alias="carrierId", description="承运商ID"),
  241. vehicle_status: Optional[str] = Query(default=None, alias="vehicleStatus", description="车辆状态"),
  242. exclude_ineligible_carrier: Optional[bool] = Query(
  243. default=False,
  244. alias="excludeIneligibleCarrier",
  245. description="排除资质不全或协议过期的承运商车辆",
  246. ),
  247. page: int = Query(default=1, ge=1),
  248. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  249. db: Session = Depends(get_db),
  250. user: DbUserContext = Depends(get_db_current_user),
  251. ) -> VehicleListResponse:
  252. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  253. query = db.query(PlVehicle).filter(
  254. PlVehicle.tenant_no == tenant_no,
  255. PlVehicle.del_flag == 0,
  256. )
  257. if vehicle_type:
  258. query = query.filter(PlVehicle.vehicle_type == vehicle_type)
  259. if carrier_id:
  260. query = query.filter(PlVehicle.carrier_id == carrier_id)
  261. if vehicle_status:
  262. query = query.filter(PlVehicle.vehicle_status == vehicle_status)
  263. if q:
  264. query = query.filter(PlVehicle.license_plate.contains(q))
  265. # 车辆为主数据,列表不做 create_by 数据范围过滤
  266. query = query.order_by(
  267. case((PlVehicle.order_num.is_(None), 1), else_=0).asc(),
  268. PlVehicle.create_time.desc(),
  269. )
  270. if exclude_ineligible_carrier:
  271. vehicles = query.all()
  272. carrier_ids = {
  273. v.carrier_id for v in vehicles if v.vehicle_type == "carrier" and v.carrier_id
  274. }
  275. carrier_map: dict[str, PlCarrier] = {}
  276. if carrier_ids:
  277. carriers = (
  278. db.query(PlCarrier)
  279. .filter(
  280. PlCarrier.id.in_(carrier_ids),
  281. PlCarrier.tenant_no == tenant_no,
  282. PlCarrier.del_flag == 0,
  283. )
  284. .all()
  285. )
  286. carrier_map = {c.id: c for c in carriers}
  287. vehicles = [
  288. v
  289. for v in vehicles
  290. if v.vehicle_type != "carrier"
  291. or not v.carrier_id
  292. or (
  293. v.carrier_id in carrier_map
  294. and carrier_eligible_for_transport(carrier_map[v.carrier_id])
  295. )
  296. ]
  297. summaries = load_vehicle_personnel_summaries(
  298. db, tenant_no, [v.id for v in vehicles]
  299. )
  300. certs_by_owner = list_owner_certs_batch(
  301. db, tenant_no, "vehicle", [v.id for v in vehicles]
  302. )
  303. items = [
  304. _vehicle_to_read(
  305. v,
  306. *summaries.get(v.id, ("", None)),
  307. cert_list=certs_to_read(certs_by_owner.get(v.id, [])),
  308. )
  309. for v in vehicles
  310. ]
  311. return VehicleListResponse(total=len(items), list=items)
  312. total = query.count()
  313. offset = (page - 1) * page_size
  314. vehicles = query.offset(offset).limit(page_size).all()
  315. summaries = load_vehicle_personnel_summaries(
  316. db, tenant_no, [v.id for v in vehicles]
  317. )
  318. certs_by_owner = list_owner_certs_batch(
  319. db, tenant_no, "vehicle", [v.id for v in vehicles]
  320. )
  321. return VehicleListResponse(
  322. total=total,
  323. list=[
  324. _vehicle_to_read(
  325. v,
  326. *summaries.get(v.id, ("", None)),
  327. cert_list=certs_to_read(certs_by_owner.get(v.id, [])),
  328. )
  329. for v in vehicles
  330. ],
  331. )
  332. @router.post("/vehicle", response_model=VehicleDetailRead, status_code=status.HTTP_201_CREATED)
  333. def create_vehicle(
  334. payload: VehicleCreate,
  335. db: Session = Depends(get_db),
  336. user: DbUserContext = Depends(get_db_current_user),
  337. ) -> VehicleDetailRead:
  338. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  339. existing = (
  340. db.query(PlVehicle)
  341. .filter(
  342. PlVehicle.license_plate == payload.license_plate,
  343. PlVehicle.del_flag == 0,
  344. )
  345. .first()
  346. )
  347. if existing:
  348. raise HTTPException(status_code=400, detail="车牌号已存在")
  349. validate_vehicle_save_certs(
  350. db,
  351. tenant_no,
  352. vehicle_type=payload.vehicle_type,
  353. trailer_no=payload.trailer_no,
  354. cert_rows=payload.cert_list,
  355. personnel_rows=payload.personnel_list,
  356. )
  357. vehicle = PlVehicle(
  358. id=generate_uuid(),
  359. tenant_no=tenant_no,
  360. vehicle_type=payload.vehicle_type,
  361. carrier_id=payload.carrier_id,
  362. carrier_name=payload.carrier_name,
  363. license_plate=payload.license_plate,
  364. trailer_no=payload.trailer_no,
  365. load_capacity=payload.load_capacity,
  366. vehicle_status=payload.vehicle_status,
  367. remark=payload.remark,
  368. create_by=user.id,
  369. create_dept_id=user.dept_id,
  370. )
  371. db.add(vehicle)
  372. db.flush()
  373. try:
  374. save_vehicle_personnel(
  375. db,
  376. vehicle.id,
  377. payload.personnel_list,
  378. tenant_no,
  379. user_id=user.id,
  380. dept_id=user.dept_id,
  381. vehicle_type=payload.vehicle_type,
  382. )
  383. save_owner_certs(
  384. db,
  385. "vehicle",
  386. vehicle.id,
  387. payload.cert_list,
  388. tenant_no,
  389. user_id=user.id,
  390. dept_id=user.dept_id,
  391. )
  392. db.commit()
  393. except HTTPException:
  394. db.rollback()
  395. raise
  396. except Exception:
  397. db.rollback()
  398. raise
  399. db.refresh(vehicle)
  400. return _vehicle_to_detail(db, tenant_no, vehicle)
  401. @router.get("/vehicle/{id}", response_model=VehicleDetailRead)
  402. def get_vehicle(
  403. id: str,
  404. db: Session = Depends(get_db),
  405. user: DbUserContext = Depends(get_db_current_user),
  406. ) -> VehicleDetailRead:
  407. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  408. vehicle = _get_vehicle(db, tenant_no, id, user)
  409. return _vehicle_to_detail(db, tenant_no, vehicle)
  410. @router.put("/vehicle/{id}", response_model=VehicleDetailRead)
  411. def update_vehicle(
  412. id: str,
  413. payload: VehicleUpdate,
  414. db: Session = Depends(get_db),
  415. user: DbUserContext = Depends(get_db_current_user),
  416. ) -> VehicleDetailRead:
  417. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  418. vehicle = _get_vehicle(db, tenant_no, id, user)
  419. personnel_list = payload.personnel_list
  420. cert_list = payload.cert_list
  421. data = payload.model_dump(
  422. exclude_unset=True, exclude={"personnel_list", "cert_list"}
  423. )
  424. effective_vehicle_type = data.get("vehicle_type", vehicle.vehicle_type)
  425. effective_trailer_no = data.get("trailer_no", vehicle.trailer_no)
  426. effective_cert_rows = (
  427. cert_list
  428. if cert_list is not None
  429. else list_owner_certs(db, tenant_no, "vehicle", vehicle.id)
  430. )
  431. effective_personnel_rows = personnel_list
  432. if effective_personnel_rows is None:
  433. effective_personnel_rows = load_vehicle_personnel_rows(db, tenant_no, vehicle.id)
  434. validate_vehicle_save_certs(
  435. db,
  436. tenant_no,
  437. vehicle_type=effective_vehicle_type,
  438. trailer_no=effective_trailer_no,
  439. cert_rows=effective_cert_rows,
  440. personnel_rows=effective_personnel_rows,
  441. )
  442. for key, value in data.items():
  443. setattr(vehicle, key, value)
  444. vehicle.update_by = user.id
  445. try:
  446. if personnel_list is not None:
  447. save_vehicle_personnel(
  448. db,
  449. vehicle.id,
  450. personnel_list,
  451. tenant_no,
  452. user_id=user.id,
  453. dept_id=user.dept_id,
  454. vehicle_type=effective_vehicle_type,
  455. )
  456. if cert_list is not None:
  457. save_owner_certs(
  458. db,
  459. "vehicle",
  460. vehicle.id,
  461. cert_list,
  462. tenant_no,
  463. user_id=user.id,
  464. dept_id=user.dept_id,
  465. )
  466. db.commit()
  467. except HTTPException:
  468. db.rollback()
  469. raise
  470. except Exception:
  471. db.rollback()
  472. raise
  473. db.refresh(vehicle)
  474. return _vehicle_to_detail(db, tenant_no, vehicle)
  475. @router.delete("/vehicle/{id}", status_code=status.HTTP_204_NO_CONTENT)
  476. def delete_vehicle(
  477. id: str,
  478. db: Session = Depends(get_db),
  479. user: DbUserContext = Depends(get_db_current_user),
  480. ) -> None:
  481. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  482. vehicle = _get_vehicle(db, tenant_no, id, user)
  483. assert_row_deletable_by_creator(vehicle, user_info_from_context(user))
  484. vehicle.del_flag = 1
  485. vehicle.update_by = user.id
  486. db.commit()
  487. def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str, user: DbUserContext) -> PlVehicle:
  488. vehicle = (
  489. db.query(PlVehicle)
  490. .filter(
  491. PlVehicle.id == vehicle_id,
  492. PlVehicle.tenant_no == tenant_no,
  493. PlVehicle.del_flag == 0,
  494. )
  495. .first()
  496. )
  497. if not vehicle:
  498. raise HTTPException(status_code=404, detail="车辆不存在")
  499. return vehicle
  500. # =============================================================================
  501. # 人员 API
  502. # =============================================================================
  503. def _personnel_to_read(
  504. db: Session,
  505. tenant_no: str,
  506. personnel: PlPersonnel,
  507. *,
  508. cert_list: Optional[list[CertRead]] = None,
  509. ) -> PersonnelRead:
  510. nearest = nearest_expire_date(db, tenant_no, "personnel", personnel.id)
  511. data = PersonnelRead.model_validate(personnel)
  512. data.nearest_expire_date = nearest
  513. if cert_list is not None:
  514. data.cert_list = cert_list
  515. return data
  516. def _personnel_to_detail(
  517. db: Session, tenant_no: str, personnel: PlPersonnel
  518. ) -> PersonnelDetailRead:
  519. base = _personnel_to_read(db, tenant_no, personnel)
  520. cert_list = certs_to_read(
  521. list_owner_certs(db, tenant_no, "personnel", personnel.id)
  522. )
  523. return PersonnelDetailRead(
  524. **base.model_dump(by_alias=False, exclude={"cert_list"}),
  525. cert_list=cert_list,
  526. )
  527. @router.get("/personnel", response_model=PersonnelListResponse)
  528. def list_personnel(
  529. q: Optional[str] = Query(default=None, description="模糊搜索姓名/手机号"),
  530. status: Optional[str] = Query(default=None, description="状态"),
  531. personnel_affiliation: Optional[str] = Query(
  532. default=None, alias="personnelAffiliation", description="人员归属 self/carrier"
  533. ),
  534. page: int = Query(default=1, ge=1),
  535. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  536. db: Session = Depends(get_db),
  537. user: DbUserContext = Depends(get_db_current_user),
  538. ) -> PersonnelListResponse:
  539. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  540. query = db.query(PlPersonnel).filter(
  541. PlPersonnel.tenant_no == tenant_no,
  542. PlPersonnel.del_flag == 0,
  543. )
  544. if status:
  545. query = query.filter(PlPersonnel.status == status)
  546. if personnel_affiliation:
  547. query = query.filter(PlPersonnel.personnel_affiliation == personnel_affiliation)
  548. if q:
  549. query = query.filter(
  550. or_(
  551. func.lower(PlPersonnel.personnel_name).contains(q.lower()),
  552. PlPersonnel.phone_no.contains(q),
  553. )
  554. )
  555. # 人员为主数据,列表不做 create_by 数据范围过滤
  556. query = query.order_by(
  557. case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),
  558. PlPersonnel.create_time.desc(),
  559. )
  560. total = query.count()
  561. offset = (page - 1) * page_size
  562. rows = query.offset(offset).limit(page_size).all()
  563. personnel_ids = [p.id for p in rows]
  564. certs_by_owner = list_owner_certs_batch(db, tenant_no, "personnel", personnel_ids)
  565. return PersonnelListResponse(
  566. total=total,
  567. list=[
  568. _personnel_to_read(
  569. db,
  570. tenant_no,
  571. p,
  572. cert_list=certs_to_read(certs_by_owner.get(p.id, [])),
  573. )
  574. for p in rows
  575. ],
  576. )
  577. @router.post("/personnel", response_model=PersonnelDetailRead, status_code=status.HTTP_201_CREATED)
  578. def create_personnel(
  579. payload: PersonnelCreate,
  580. db: Session = Depends(get_db),
  581. user: DbUserContext = Depends(get_db_current_user),
  582. ) -> PersonnelDetailRead:
  583. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  584. validate_personnel_required_certs(
  585. payload.cert_list,
  586. payload.personnel_type,
  587. personnel_affiliation=payload.personnel_affiliation,
  588. subject_label=f"「{payload.personnel_name}」",
  589. )
  590. personnel = PlPersonnel(
  591. id=generate_uuid(),
  592. tenant_no=tenant_no,
  593. personnel_name=payload.personnel_name,
  594. phone_no=payload.phone_no,
  595. age=payload.age,
  596. id_card_no=payload.id_card_no,
  597. personnel_type=payload.personnel_type,
  598. personnel_affiliation=payload.personnel_affiliation,
  599. status=payload.status,
  600. remark=payload.remark,
  601. create_by=user.id,
  602. create_dept_id=user.dept_id,
  603. )
  604. db.add(personnel)
  605. db.flush()
  606. try:
  607. save_owner_certs(
  608. db,
  609. "personnel",
  610. personnel.id,
  611. payload.cert_list,
  612. tenant_no,
  613. user_id=user.id,
  614. dept_id=user.dept_id,
  615. )
  616. db.commit()
  617. except Exception:
  618. db.rollback()
  619. raise
  620. db.refresh(personnel)
  621. return _personnel_to_detail(db, tenant_no, personnel)
  622. @router.get("/personnel/{id}", response_model=PersonnelDetailRead)
  623. def get_personnel(
  624. id: str,
  625. db: Session = Depends(get_db),
  626. user: DbUserContext = Depends(get_db_current_user),
  627. ) -> PersonnelDetailRead:
  628. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  629. personnel = _get_personnel(db, tenant_no, id, user)
  630. return _personnel_to_detail(db, tenant_no, personnel)
  631. @router.put("/personnel/{id}", response_model=PersonnelDetailRead)
  632. def update_personnel(
  633. id: str,
  634. payload: PersonnelUpdate,
  635. db: Session = Depends(get_db),
  636. user: DbUserContext = Depends(get_db_current_user),
  637. ) -> PersonnelDetailRead:
  638. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  639. personnel = _get_personnel(db, tenant_no, id, user)
  640. cert_list = payload.cert_list
  641. data = payload.model_dump(exclude_unset=True, exclude={"cert_list"})
  642. effective_personnel_type = data.get("personnel_type", personnel.personnel_type)
  643. effective_personnel_affiliation = data.get(
  644. "personnel_affiliation", personnel.personnel_affiliation
  645. )
  646. effective_cert_rows = (
  647. cert_list
  648. if cert_list is not None
  649. else list_owner_certs(db, tenant_no, "personnel", personnel.id)
  650. )
  651. subject_name = data.get("personnel_name", personnel.personnel_name)
  652. validate_personnel_required_certs(
  653. effective_cert_rows,
  654. effective_personnel_type,
  655. personnel_affiliation=effective_personnel_affiliation,
  656. subject_label=f"「{subject_name}」",
  657. )
  658. for key, value in data.items():
  659. setattr(personnel, key, value)
  660. personnel.update_by = user.id
  661. try:
  662. if cert_list is not None:
  663. save_owner_certs(
  664. db,
  665. "personnel",
  666. personnel.id,
  667. cert_list,
  668. tenant_no,
  669. user_id=user.id,
  670. dept_id=user.dept_id,
  671. )
  672. db.commit()
  673. except Exception:
  674. db.rollback()
  675. raise
  676. db.refresh(personnel)
  677. return _personnel_to_detail(db, tenant_no, personnel)
  678. @router.delete("/personnel/{id}", status_code=status.HTTP_204_NO_CONTENT)
  679. def delete_personnel(
  680. id: str,
  681. db: Session = Depends(get_db),
  682. user: DbUserContext = Depends(get_db_current_user),
  683. ) -> None:
  684. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  685. personnel = _get_personnel(db, tenant_no, id, user)
  686. assert_row_deletable_by_creator(personnel, user_info_from_context(user))
  687. personnel.del_flag = 1
  688. personnel.update_by = user.id
  689. db.commit()
  690. def _get_personnel(db: Session, tenant_no: str, personnel_id: str, user: DbUserContext) -> PlPersonnel:
  691. personnel = (
  692. db.query(PlPersonnel)
  693. .filter(
  694. PlPersonnel.id == personnel_id,
  695. PlPersonnel.tenant_no == tenant_no,
  696. PlPersonnel.del_flag == 0,
  697. )
  698. .first()
  699. )
  700. if not personnel:
  701. raise HTTPException(status_code=404, detail="人员不存在")
  702. return personnel
  703. # =============================================================================
  704. # 司机 API(兼容别名,内部使用 pl_personnel)
  705. # =============================================================================
  706. def _personnel_to_driver_read(personnel: PlPersonnel) -> DriverRead:
  707. return DriverRead(
  708. id=personnel.id,
  709. driver_name=personnel.personnel_name,
  710. phone_no=personnel.phone_no,
  711. id_card_no=personnel.id_card_no,
  712. tenant_no=personnel.tenant_no,
  713. order_num=personnel.order_num,
  714. remark=personnel.remark,
  715. create_by=personnel.create_by,
  716. create_dept_id=personnel.create_dept_id,
  717. create_time=personnel.create_time,
  718. update_by=personnel.update_by,
  719. update_time=personnel.update_time,
  720. del_flag=personnel.del_flag,
  721. )
  722. @router.get("/driver", response_model=DriverListResponse)
  723. def list_drivers(
  724. q: Optional[str] = Query(default=None, description="模糊搜索姓名/手机号"),
  725. page: int = Query(default=1, ge=1),
  726. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  727. db: Session = Depends(get_db),
  728. user: DbUserContext = Depends(get_db_current_user),
  729. ) -> DriverListResponse:
  730. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  731. query = db.query(PlPersonnel).filter(
  732. PlPersonnel.tenant_no == tenant_no,
  733. PlPersonnel.del_flag == 0,
  734. )
  735. if q:
  736. query = query.filter(
  737. or_(
  738. func.lower(PlPersonnel.personnel_name).contains(q.lower()),
  739. PlPersonnel.phone_no.contains(q),
  740. )
  741. )
  742. # 司机列表为主数据选项,不做 create_by 数据范围过滤
  743. query = query.order_by(
  744. case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),
  745. PlPersonnel.create_time.desc(),
  746. )
  747. total = query.count()
  748. offset = (page - 1) * page_size
  749. rows = query.offset(offset).limit(page_size).all()
  750. return DriverListResponse(
  751. total=total,
  752. list=[_personnel_to_driver_read(p) for p in rows],
  753. )
  754. @router.post("/driver", response_model=DriverRead, status_code=status.HTTP_201_CREATED)
  755. def create_driver(
  756. payload: DriverCreate,
  757. db: Session = Depends(get_db),
  758. user: DbUserContext = Depends(get_db_current_user),
  759. ) -> DriverRead:
  760. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  761. personnel = PlPersonnel(
  762. id=generate_uuid(),
  763. tenant_no=tenant_no,
  764. personnel_name=payload.driver_name,
  765. phone_no=payload.phone_no,
  766. id_card_no=payload.id_card_no,
  767. status="1",
  768. remark=payload.remark,
  769. create_by=user.id,
  770. create_dept_id=user.dept_id,
  771. )
  772. db.add(personnel)
  773. db.commit()
  774. db.refresh(personnel)
  775. return _personnel_to_driver_read(personnel)
  776. @router.get("/driver/{id}", response_model=DriverRead)
  777. def get_driver(
  778. id: str,
  779. db: Session = Depends(get_db),
  780. user: DbUserContext = Depends(get_db_current_user),
  781. ) -> DriverRead:
  782. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  783. personnel = _get_personnel(db, tenant_no, id, user)
  784. return _personnel_to_driver_read(personnel)
  785. @router.put("/driver/{id}", response_model=DriverRead)
  786. def update_driver(
  787. id: str,
  788. payload: DriverUpdate,
  789. db: Session = Depends(get_db),
  790. user: DbUserContext = Depends(get_db_current_user),
  791. ) -> DriverRead:
  792. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  793. personnel = _get_personnel(db, tenant_no, id, user)
  794. if payload.driver_name is not None:
  795. personnel.personnel_name = payload.driver_name
  796. if payload.phone_no is not None:
  797. personnel.phone_no = payload.phone_no
  798. if payload.id_card_no is not None:
  799. personnel.id_card_no = payload.id_card_no
  800. if payload.remark is not None:
  801. personnel.remark = payload.remark
  802. personnel.update_by = user.id
  803. db.commit()
  804. db.refresh(personnel)
  805. return _personnel_to_driver_read(personnel)
  806. @router.delete("/driver/{id}", status_code=status.HTTP_204_NO_CONTENT)
  807. def delete_driver(
  808. id: str,
  809. db: Session = Depends(get_db),
  810. user: DbUserContext = Depends(get_db_current_user),
  811. ) -> None:
  812. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  813. personnel = _get_personnel(db, tenant_no, id, user)
  814. assert_row_deletable_by_creator(personnel, user_info_from_context(user))
  815. personnel.del_flag = 1
  816. personnel.update_by = user.id
  817. db.commit()
  818. # =============================================================================
  819. # 车辆-司机绑定 API(兼容,写入 pl_vehicle_personnel)
  820. # =============================================================================
  821. @router.post("/vehicle-driver", status_code=status.HTTP_201_CREATED)
  822. def bind_vehicle_driver(
  823. payload: VehicleDriverCreate,
  824. db: Session = Depends(get_db),
  825. user: DbUserContext = Depends(get_db_current_user),
  826. ) -> dict:
  827. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  828. _get_vehicle(db, tenant_no, payload.vehicle_id, user)
  829. _get_personnel(db, tenant_no, payload.driver_id, user)
  830. if payload.is_default == 1:
  831. db.query(PlVehiclePersonnel).filter(
  832. PlVehiclePersonnel.vehicle_id == payload.vehicle_id,
  833. PlVehiclePersonnel.role == "driver",
  834. PlVehiclePersonnel.del_flag == 0,
  835. ).update({"is_default": 0})
  836. vd = PlVehiclePersonnel(
  837. id=generate_uuid(),
  838. tenant_no=tenant_no,
  839. vehicle_id=payload.vehicle_id,
  840. personnel_id=payload.driver_id,
  841. role="driver",
  842. is_default=payload.is_default,
  843. create_by=user.id,
  844. create_dept_id=user.dept_id,
  845. )
  846. db.add(vd)
  847. db.commit()
  848. return {"id": vd.id}
  849. @router.delete("/vehicle-driver/{id}", status_code=status.HTTP_204_NO_CONTENT)
  850. def unbind_vehicle_driver(
  851. id: str,
  852. db: Session = Depends(get_db),
  853. user: DbUserContext = Depends(get_db_current_user),
  854. ) -> None:
  855. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  856. vd = (
  857. db.query(PlVehiclePersonnel)
  858. .filter(
  859. PlVehiclePersonnel.id == id,
  860. PlVehiclePersonnel.tenant_no == tenant_no,
  861. PlVehiclePersonnel.del_flag == 0,
  862. )
  863. .first()
  864. )
  865. if not vd:
  866. vd = (
  867. db.query(PlVehicleDriver)
  868. .filter(
  869. PlVehicleDriver.id == id,
  870. PlVehicleDriver.tenant_no == tenant_no,
  871. PlVehicleDriver.del_flag == 0,
  872. )
  873. .first()
  874. )
  875. if vd:
  876. assert_row_deletable_by_creator(vd, user_info_from_context(user))
  877. vd.del_flag = 1
  878. db.commit()
  879. @router.post("/paste-recognize/commit", response_model=PasteCommitResult)
  880. def paste_recognize_commit(
  881. payload: PasteCommitBody,
  882. db: Session = Depends(get_db),
  883. user: DbUserContext = Depends(get_db_current_user),
  884. ) -> PasteCommitResult:
  885. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  886. try:
  887. result = commit_paste_recognize(
  888. db,
  889. tenant_no,
  890. payload,
  891. user_id=user.id,
  892. dept_id=user.dept_id,
  893. )
  894. db.commit()
  895. return result
  896. except HTTPException:
  897. db.rollback()
  898. raise
  899. except Exception:
  900. db.rollback()
  901. raise