base_data.py 31 KB

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