base_data.py 33 KB

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