base_data.py 32 KB

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