base_data.py 32 KB

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