base_data.py 32 KB

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