base_data.py 33 KB

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