base_data.py 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804
  1. """运输计划管理 - 基础数据 API(承运商、车辆、人员)"""
  2. from datetime import datetime
  3. from typing import Any, List, Optional
  4. from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
  5. from pydantic import BaseModel, ConfigDict, Field
  6. from sqlalchemy import case, func, or_
  7. from sqlalchemy.orm import Session
  8. from app.utils.excel_export import (
  9. EXPORT_MAX_ROWS,
  10. ExportColumn,
  11. ExportValidationError,
  12. export_rows_to_xlsx_response,
  13. filter_export_columns,
  14. )
  15. from app.core.database import get_db
  16. from app.core.db_auth import DbUserContext, get_db_current_user
  17. from app.core.delete_guard import assert_row_deletable_by_creator, user_info_from_context
  18. from app.core.tenant_context import get_tenant_no
  19. from app.core.utils import generate_uuid
  20. from app.models.pl_models import (
  21. PlCarrier,
  22. PlPersonnel,
  23. PlVehicle,
  24. PlVehicleDriver,
  25. PlVehiclePersonnel,
  26. )
  27. from app.schemas.pl import (
  28. CarrierAuditReject,
  29. CarrierCreate,
  30. CarrierListResponse,
  31. CarrierRead,
  32. CarrierUpdate,
  33. carrier_to_read,
  34. CertRead,
  35. DriverCreate,
  36. DriverListResponse,
  37. DriverRead,
  38. DriverUpdate,
  39. PersonnelCreate,
  40. PersonnelDetailRead,
  41. PersonnelListResponse,
  42. PersonnelRead,
  43. PersonnelUpdate,
  44. VehicleCreate,
  45. VehicleDetailRead,
  46. VehicleDriverCreate,
  47. VehicleListResponse,
  48. VehicleRead,
  49. VehicleUpdate,
  50. )
  51. from app.services.pl_cert_service import (
  52. certs_to_read,
  53. list_owner_certs,
  54. list_owner_certs_batch,
  55. nearest_expire_date,
  56. save_owner_certs,
  57. )
  58. from app.services.pl_cert_required_service import (
  59. validate_personnel_required_certs,
  60. validate_vehicle_save_certs,
  61. )
  62. from app.services.pl_carrier_service import carrier_eligible_for_transport, normalize_carrier_urls
  63. from app.services.pl_personnel_service import (
  64. load_vehicle_personnel_rows,
  65. load_vehicle_personnel_summaries,
  66. save_vehicle_personnel,
  67. )
  68. from app.services.pl_personnel_user_bind import assert_sys_user_bind_available
  69. from app.schemas.pl.paste_recognize import PasteCommitBody, PasteCommitResult
  70. from app.services.pl_paste_recognize_service import commit_paste_recognize
  71. router = APIRouter(prefix="", tags=["运输计划-基础数据"])
  72. def _merge_query(request: Request, **known) -> dict:
  73. out = {k: v for k, v in known.items() if v is not None and v != ""}
  74. for k, v in request.query_params.multi_items():
  75. if k in ("page", "pageSize"):
  76. continue
  77. if k not in out and v not in (None, ""):
  78. out[k] = v
  79. return out
  80. def _param_get(params: dict, *keys: str) -> Any:
  81. for key in keys:
  82. if key in params and params[key] not in (None, ""):
  83. return params[key]
  84. return None
  85. def _body_to_params(body: BaseModel) -> dict:
  86. data = body.model_dump(by_alias=True, exclude_none=True)
  87. for key in ("ids", "columns"):
  88. data.pop(key, None)
  89. extra = getattr(body, "model_extra", None) or {}
  90. for key, value in extra.items():
  91. if key not in data and value not in (None, ""):
  92. data[key] = value
  93. return data
  94. def _carrier_column_filter_kwargs(params: dict) -> dict:
  95. return dict(
  96. carrier_name=_param_get(params, "carrierName", "carrier_name"),
  97. carrier_name_in=_param_get(params, "carrierNameIn", "carrier_name_in"),
  98. contact_name=_param_get(params, "contactName", "contact_name"),
  99. contact_name_in=_param_get(params, "contactNameIn", "contact_name_in"),
  100. contact_phone=_param_get(params, "contactPhone", "contact_phone"),
  101. contact_phone_in=_param_get(params, "contactPhoneIn", "contact_phone_in"),
  102. status=_param_get(params, "status"),
  103. status_in=_param_get(params, "statusIn", "status_in"),
  104. remark=_param_get(params, "remark"),
  105. transport_agreement_expire_date_from=_param_get(
  106. params,
  107. "transportAgreementExpireDateFrom",
  108. "transport_agreement_expire_date_from",
  109. ),
  110. transport_agreement_expire_date_to=_param_get(
  111. params,
  112. "transportAgreementExpireDateTo",
  113. "transport_agreement_expire_date_to",
  114. ),
  115. )
  116. def _vehicle_column_filter_kwargs(params: dict) -> dict:
  117. return dict(
  118. license_plate=_param_get(params, "licensePlate", "license_plate"),
  119. license_plate_in=_param_get(params, "licensePlateIn", "license_plate_in"),
  120. vehicle_status=_param_get(params, "vehicleStatus", "vehicle_status"),
  121. vehicle_status_in=_param_get(params, "vehicleStatusIn", "vehicle_status_in"),
  122. carrier_name=_param_get(params, "carrierName", "carrier_name"),
  123. remark=_param_get(params, "remark"),
  124. load_capacity_op=_param_get(params, "loadCapacityOp", "load_capacity_op"),
  125. load_capacity=_param_get(params, "loadCapacity", "load_capacity"),
  126. load_capacity_min=_param_get(params, "loadCapacityMin", "load_capacity_min"),
  127. load_capacity_max=_param_get(params, "loadCapacityMax", "load_capacity_max"),
  128. )
  129. def _personnel_column_filter_kwargs(params: dict) -> dict:
  130. return dict(
  131. personnel_name=_param_get(params, "personnelName", "personnel_name"),
  132. personnel_name_in=_param_get(params, "personnelNameIn", "personnel_name_in"),
  133. phone_no=_param_get(params, "phoneNo", "phone_no"),
  134. phone_no_in=_param_get(params, "phoneNoIn", "phone_no_in"),
  135. status=_param_get(params, "status"),
  136. status_in=_param_get(params, "statusIn", "status_in"),
  137. personnel_type=_param_get(params, "personnelType", "personnel_type"),
  138. personnel_type_in=_param_get(params, "personnelTypeIn", "personnel_type_in"),
  139. id_card_no=_param_get(params, "idCardNo", "id_card_no"),
  140. remark=_param_get(params, "remark"),
  141. sys_user_name=_param_get(params, "sysUserName", "sys_user_name"),
  142. age_op=_param_get(params, "ageOp", "age_op"),
  143. age=_param_get(params, "age"),
  144. age_min=_param_get(params, "ageMin", "age_min"),
  145. age_max=_param_get(params, "ageMax", "age_max"),
  146. )
  147. # =============================================================================
  148. # 承运商 API
  149. # =============================================================================
  150. class CarrierColumnFacetBody(BaseModel):
  151. """承运商列头可选值(去重;可附带列表上下文)"""
  152. model_config = ConfigDict(extra="allow")
  153. field: str = Field(..., description="carrierName | contactName | contactPhone")
  154. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  155. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  156. _CARRIER_FACET_COLUMNS = {
  157. "carrierName": PlCarrier.carrier_name,
  158. "contactName": PlCarrier.contact_name,
  159. "contactPhone": PlCarrier.contact_phone,
  160. }
  161. @router.post("/carrier/column-facet-values")
  162. def carrier_column_facet_values(
  163. body: CarrierColumnFacetBody,
  164. db: Session = Depends(get_db),
  165. user: DbUserContext = Depends(get_db_current_user),
  166. ):
  167. """承运商列头可选值:在列表上下文(排除本列)下去重。"""
  168. from app.services.column_facet import list_column_facet_values
  169. from app.services.pl.carrier_list_filters import build_carrier_column_filters
  170. from app.services.pl.facet_context import strip_simple_facet_self_params
  171. column = _CARRIER_FACET_COLUMNS.get(body.field)
  172. if column is None:
  173. raise HTTPException(status_code=400, detail="不支持的列")
  174. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  175. params = strip_simple_facet_self_params(_body_to_params(body), body.field)
  176. extra_filters = build_carrier_column_filters(
  177. **_carrier_column_filter_kwargs(params)
  178. )
  179. audit = _param_get(params, "auditStatus", "audit_status")
  180. if audit:
  181. extra_filters.append(PlCarrier.audit_status == audit)
  182. qv = _param_get(params, "q")
  183. if qv:
  184. extra_filters.append(
  185. or_(
  186. func.lower(PlCarrier.carrier_name).contains(str(qv).lower()),
  187. func.lower(PlCarrier.contact_name).contains(str(qv).lower()),
  188. PlCarrier.contact_phone.contains(str(qv)),
  189. )
  190. )
  191. rows = list_column_facet_values(
  192. db,
  193. PlCarrier,
  194. column,
  195. tenant_no=tenant_no,
  196. keyword=body.keyword,
  197. limit=body.limit if body.limit is not None else 500,
  198. extra_filters=extra_filters or None,
  199. )
  200. return {"list": rows}
  201. class VehicleColumnFacetBody(BaseModel):
  202. """车辆列头可选值(去重;可附带列表上下文)"""
  203. model_config = ConfigDict(extra="allow")
  204. field: str = Field(..., description="licensePlate")
  205. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  206. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  207. _VEHICLE_FACET_COLUMNS = {
  208. "licensePlate": PlVehicle.license_plate,
  209. }
  210. @router.post("/vehicle/column-facet-values")
  211. def vehicle_column_facet_values(
  212. body: VehicleColumnFacetBody,
  213. db: Session = Depends(get_db),
  214. user: DbUserContext = Depends(get_db_current_user),
  215. ):
  216. """车辆列头可选值:在列表上下文(排除本列)下去重(含 vehicleType)。"""
  217. from app.services.column_facet import list_column_facet_values
  218. from app.services.pl.facet_context import strip_simple_facet_self_params
  219. from app.services.pl.vehicle_list_filters import build_vehicle_column_filters
  220. column = _VEHICLE_FACET_COLUMNS.get(body.field)
  221. if column is None:
  222. raise HTTPException(status_code=400, detail="不支持的列")
  223. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  224. params = strip_simple_facet_self_params(_body_to_params(body), body.field)
  225. extra_filters = build_vehicle_column_filters(
  226. **_vehicle_column_filter_kwargs(params)
  227. )
  228. vt = _param_get(params, "vehicleType", "vehicle_type")
  229. if vt:
  230. extra_filters.append(PlVehicle.vehicle_type == vt)
  231. cid = _param_get(params, "carrierId", "carrier_id")
  232. if cid:
  233. extra_filters.append(PlVehicle.carrier_id == cid)
  234. qv = _param_get(params, "q")
  235. if qv:
  236. extra_filters.append(PlVehicle.license_plate.contains(str(qv)))
  237. mine_flag = _param_get(params, "mineVehicles", "mine_vehicles")
  238. if mine_flag is True or (
  239. mine_flag is not None
  240. and str(mine_flag).strip().lower() in {"1", "true", "yes", "y"}
  241. ):
  242. from app.services.pl_vehicle_mine_vehicles import (
  243. license_plates_for_user_personnel,
  244. should_restrict_mine_vehicles,
  245. )
  246. if should_restrict_mine_vehicles(
  247. db, tenant_no, user.id, mine_vehicles=True
  248. ):
  249. plates = license_plates_for_user_personnel(db, tenant_no, user.id)
  250. if not plates:
  251. return {"list": []}
  252. extra_filters.append(PlVehicle.license_plate.in_(list(plates)))
  253. rows = list_column_facet_values(
  254. db,
  255. PlVehicle,
  256. column,
  257. tenant_no=tenant_no,
  258. keyword=body.keyword,
  259. limit=body.limit if body.limit is not None else 500,
  260. extra_filters=extra_filters or None,
  261. )
  262. return {"list": rows}
  263. class PersonnelColumnFacetBody(BaseModel):
  264. """人员列头可选值(去重;可附带列表上下文)"""
  265. model_config = ConfigDict(extra="allow")
  266. field: str = Field(..., description="personnelName | phoneNo")
  267. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  268. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  269. _PERSONNEL_FACET_COLUMNS = {
  270. "personnelName": PlPersonnel.personnel_name,
  271. "phoneNo": PlPersonnel.phone_no,
  272. }
  273. @router.post("/personnel/column-facet-values")
  274. def personnel_column_facet_values(
  275. body: PersonnelColumnFacetBody,
  276. db: Session = Depends(get_db),
  277. user: DbUserContext = Depends(get_db_current_user),
  278. ):
  279. """人员列头可选值:在列表上下文(排除本列)下去重(含 personnelAffiliation)。"""
  280. from app.services.column_facet import list_column_facet_values
  281. from app.services.pl.facet_context import strip_simple_facet_self_params
  282. from app.services.pl.personnel_list_filters import build_personnel_column_filters
  283. column = _PERSONNEL_FACET_COLUMNS.get(body.field)
  284. if column is None:
  285. raise HTTPException(status_code=400, detail="不支持的列")
  286. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  287. params = strip_simple_facet_self_params(_body_to_params(body), body.field)
  288. extra_filters = build_personnel_column_filters(
  289. **_personnel_column_filter_kwargs(params)
  290. )
  291. affiliation = _param_get(params, "personnelAffiliation", "personnel_affiliation")
  292. if affiliation:
  293. extra_filters.append(PlPersonnel.personnel_affiliation == affiliation)
  294. qv = _param_get(params, "q")
  295. if qv:
  296. extra_filters.append(
  297. or_(
  298. func.lower(PlPersonnel.personnel_name).contains(str(qv).lower()),
  299. PlPersonnel.phone_no.contains(str(qv)),
  300. )
  301. )
  302. rows = list_column_facet_values(
  303. db,
  304. PlPersonnel,
  305. column,
  306. tenant_no=tenant_no,
  307. keyword=body.keyword,
  308. limit=body.limit if body.limit is not None else 500,
  309. extra_filters=extra_filters or None,
  310. )
  311. return {"list": rows}
  312. @router.get("/carrier", response_model=CarrierListResponse)
  313. def list_carriers(
  314. request: Request,
  315. q: Optional[str] = Query(default=None, description="模糊搜索"),
  316. status: Optional[str] = Query(default=None, description="状态"),
  317. audit_status: Optional[str] = Query(default=None, alias="auditStatus", description="审核状态"),
  318. page: int = Query(default=1, ge=1),
  319. page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
  320. db: Session = Depends(get_db),
  321. user: DbUserContext = Depends(get_db_current_user),
  322. ) -> CarrierListResponse:
  323. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  324. params = _merge_query(request, q=q, status=status, auditStatus=audit_status)
  325. query = db.query(PlCarrier).filter(
  326. PlCarrier.tenant_no == tenant_no,
  327. PlCarrier.del_flag == 0,
  328. )
  329. audit = _param_get(params, "auditStatus", "audit_status")
  330. if audit:
  331. query = query.filter(PlCarrier.audit_status == audit)
  332. qv = _param_get(params, "q")
  333. if qv:
  334. query = query.filter(
  335. or_(
  336. func.lower(PlCarrier.carrier_name).contains(str(qv).lower()),
  337. func.lower(PlCarrier.contact_name).contains(str(qv).lower()),
  338. PlCarrier.contact_phone.contains(str(qv)),
  339. )
  340. )
  341. from app.services.pl.carrier_list_filters import build_carrier_column_filters
  342. column_filters = build_carrier_column_filters(
  343. **_carrier_column_filter_kwargs(params)
  344. )
  345. if column_filters:
  346. query = query.filter(*column_filters)
  347. # 承运商为物流主数据,列表不做 create_by 数据范围过滤
  348. query = query.order_by(
  349. case((PlCarrier.order_num.is_(None), 1), else_=0).asc(),
  350. PlCarrier.create_time.desc(),
  351. )
  352. total = query.count()
  353. offset = (page - 1) * page_size
  354. rows = query.offset(offset).limit(page_size).all()
  355. return CarrierListResponse(
  356. total=total,
  357. list=[carrier_to_read(row) for row in rows],
  358. )
  359. CARRIER_EXPORT_COLUMNS = [
  360. ExportColumn(title="承运商名称", field="carrierName"),
  361. ExportColumn(title="联系人", field="contactName"),
  362. ExportColumn(title="联系电话", field="contactPhone"),
  363. ExportColumn(title="合作状态", field="statusLabel"),
  364. ExportColumn(title="资质提示", field="qualificationHint"),
  365. ExportColumn(title="协议有效期", field="transportAgreementExpireDate"),
  366. ExportColumn(title="备注", field="remark"),
  367. ]
  368. class CarrierExportBody(BaseModel):
  369. q: Optional[str] = None
  370. status: Optional[str] = None
  371. audit_status: Optional[str] = Field(default=None, alias="auditStatus")
  372. ids: Optional[List[str]] = None
  373. columns: Optional[List[str]] = None
  374. model_config = {"populate_by_name": True, "extra": "allow"}
  375. @router.post("/carrier/export")
  376. def export_carriers(
  377. body: CarrierExportBody,
  378. db: Session = Depends(get_db),
  379. user: DbUserContext = Depends(get_db_current_user),
  380. ):
  381. """导出承运商列表(当前筛选全量,或 ids 已选)。"""
  382. try:
  383. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  384. params = _body_to_params(body)
  385. query = db.query(PlCarrier).filter(
  386. PlCarrier.tenant_no == tenant_no,
  387. PlCarrier.del_flag == 0,
  388. )
  389. audit = _param_get(params, "auditStatus", "audit_status")
  390. if audit:
  391. query = query.filter(PlCarrier.audit_status == audit)
  392. qv = _param_get(params, "q")
  393. if qv:
  394. query = query.filter(
  395. or_(
  396. func.lower(PlCarrier.carrier_name).contains(str(qv).lower()),
  397. func.lower(PlCarrier.contact_name).contains(str(qv).lower()),
  398. PlCarrier.contact_phone.contains(str(qv)),
  399. )
  400. )
  401. from app.services.pl.carrier_list_filters import build_carrier_column_filters
  402. column_filters = build_carrier_column_filters(
  403. **_carrier_column_filter_kwargs(params)
  404. )
  405. if column_filters:
  406. query = query.filter(*column_filters)
  407. if body.ids:
  408. query = query.filter(PlCarrier.id.in_(body.ids))
  409. query = query.order_by(
  410. case((PlCarrier.order_num.is_(None), 1), else_=0).asc(),
  411. PlCarrier.create_time.desc(),
  412. )
  413. rows = query.limit(EXPORT_MAX_ROWS + 1).all()
  414. items = []
  415. for row in rows:
  416. data = carrier_to_read(row).model_dump(by_alias=True)
  417. data["statusLabel"] = "合作中" if str(data.get("status")) == "1" else "已终止"
  418. if data.get("transportAgreementExpired"):
  419. data["qualificationHint"] = "协议已过期"
  420. elif data.get("qualificationComplete") is False:
  421. data["qualificationHint"] = "资质不全"
  422. else:
  423. data["qualificationHint"] = "资质齐全"
  424. items.append(data)
  425. requested = None
  426. if body.columns:
  427. requested = []
  428. for key in body.columns:
  429. if key in (
  430. "qualificationAttachments",
  431. "qualificationAttachment",
  432. "photoUrls",
  433. ):
  434. continue
  435. if key == "status":
  436. requested.append("statusLabel")
  437. elif key == "qualificationHint":
  438. requested.append("qualificationHint")
  439. else:
  440. requested.append(key)
  441. cols = filter_export_columns(CARRIER_EXPORT_COLUMNS, requested)
  442. date_str = datetime.now().strftime("%Y%m%d")
  443. return export_rows_to_xlsx_response(
  444. db,
  445. tenant_no=tenant_no,
  446. sheet_title="承运商列表",
  447. columns=cols,
  448. rows=items,
  449. filename_stem=f"承运商列表_{date_str}",
  450. )
  451. except ExportValidationError as e:
  452. raise HTTPException(status_code=400, detail=str(e)) from e
  453. @router.post("/carrier", response_model=CarrierRead, status_code=status.HTTP_201_CREATED)
  454. def create_carrier(
  455. payload: CarrierCreate,
  456. db: Session = Depends(get_db),
  457. user: DbUserContext = Depends(get_db_current_user),
  458. ) -> CarrierRead:
  459. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  460. carrier = PlCarrier(
  461. id=generate_uuid(),
  462. tenant_no=tenant_no,
  463. carrier_name=payload.carrier_name,
  464. contact_name=payload.contact_name,
  465. contact_phone=payload.contact_phone,
  466. status=payload.status,
  467. business_license_urls=normalize_carrier_urls(payload.business_license_urls),
  468. road_transport_license_urls=normalize_carrier_urls(payload.road_transport_license_urls),
  469. transport_agreement_urls=normalize_carrier_urls(payload.transport_agreement_urls),
  470. transport_agreement_expire_date=payload.transport_agreement_expire_date,
  471. audit_status="pending",
  472. remark=payload.remark,
  473. create_by=user.id,
  474. create_dept_id=user.dept_id,
  475. )
  476. db.add(carrier)
  477. db.commit()
  478. db.refresh(carrier)
  479. return carrier_to_read(carrier)
  480. @router.get("/carrier/{id}", response_model=CarrierRead)
  481. def get_carrier(
  482. id: str,
  483. db: Session = Depends(get_db),
  484. user: DbUserContext = Depends(get_db_current_user),
  485. ) -> CarrierRead:
  486. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  487. carrier = _get_carrier(db, tenant_no, id, user)
  488. return carrier_to_read(carrier)
  489. @router.put("/carrier/{id}", response_model=CarrierRead)
  490. def update_carrier(
  491. id: str,
  492. payload: CarrierUpdate,
  493. db: Session = Depends(get_db),
  494. user: DbUserContext = Depends(get_db_current_user),
  495. ) -> CarrierRead:
  496. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  497. carrier = _get_carrier(db, tenant_no, id, user)
  498. payload_data = payload.model_dump(exclude_unset=True)
  499. for key in (
  500. "business_license_urls",
  501. "road_transport_license_urls",
  502. "transport_agreement_urls",
  503. ):
  504. if key in payload_data:
  505. payload_data[key] = normalize_carrier_urls(payload_data[key])
  506. for key, value in payload_data.items():
  507. setattr(carrier, key, value)
  508. carrier.update_by = user.id
  509. db.commit()
  510. db.refresh(carrier)
  511. return carrier_to_read(carrier)
  512. @router.delete("/carrier/{id}", status_code=status.HTTP_204_NO_CONTENT)
  513. def delete_carrier(
  514. id: str,
  515. db: Session = Depends(get_db),
  516. user: DbUserContext = Depends(get_db_current_user),
  517. ) -> None:
  518. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  519. carrier = _get_carrier(db, tenant_no, id, user)
  520. assert_row_deletable_by_creator(carrier, user_info_from_context(user))
  521. carrier.del_flag = 1
  522. carrier.update_by = user.id
  523. db.commit()
  524. @router.post("/carrier/{id}/approve", response_model=CarrierRead)
  525. def approve_carrier(
  526. id: str,
  527. db: Session = Depends(get_db),
  528. user: DbUserContext = Depends(get_db_current_user),
  529. ) -> CarrierRead:
  530. raise HTTPException(status_code=400, detail="承运商审核流程已停用")
  531. @router.post("/carrier/{id}/reject", response_model=CarrierRead)
  532. def reject_carrier(
  533. id: str,
  534. payload: CarrierAuditReject,
  535. db: Session = Depends(get_db),
  536. user: DbUserContext = Depends(get_db_current_user),
  537. ) -> CarrierRead:
  538. raise HTTPException(status_code=400, detail="承运商审核流程已停用")
  539. def _get_carrier(db: Session, tenant_no: str, carrier_id: str, user: DbUserContext) -> PlCarrier:
  540. carrier = (
  541. db.query(PlCarrier)
  542. .filter(
  543. PlCarrier.id == carrier_id,
  544. PlCarrier.tenant_no == tenant_no,
  545. PlCarrier.del_flag == 0,
  546. )
  547. .first()
  548. )
  549. if not carrier:
  550. raise HTTPException(status_code=404, detail="承运商不存在")
  551. return carrier
  552. # =============================================================================
  553. # 车辆 API
  554. # =============================================================================
  555. def _vehicle_to_read(
  556. vehicle: PlVehicle,
  557. summary: Optional[str] = None,
  558. default_driver_name: Optional[str] = None,
  559. *,
  560. cert_list: Optional[list[CertRead]] = None,
  561. ) -> VehicleRead:
  562. data = VehicleRead.model_validate(vehicle)
  563. if summary is not None:
  564. data.personnel_summary = summary
  565. if default_driver_name is not None:
  566. data.default_driver_name = default_driver_name
  567. if cert_list is not None:
  568. data.cert_list = cert_list
  569. return data
  570. def _vehicle_to_detail(
  571. db: Session, tenant_no: str, vehicle: PlVehicle
  572. ) -> VehicleDetailRead:
  573. summaries = load_vehicle_personnel_summaries(db, tenant_no, [vehicle.id])
  574. summary, default_name = summaries.get(vehicle.id, ("", None))
  575. personnel_list = load_vehicle_personnel_rows(db, tenant_no, vehicle.id)
  576. cert_list = certs_to_read(
  577. list_owner_certs(db, tenant_no, "vehicle", vehicle.id)
  578. )
  579. base = _vehicle_to_read(vehicle, summary, default_name)
  580. return VehicleDetailRead(
  581. **base.model_dump(by_alias=False, exclude={"cert_list"}),
  582. personnel_list=personnel_list,
  583. cert_list=cert_list,
  584. )
  585. @router.get("/vehicle", response_model=VehicleListResponse)
  586. def list_vehicles(
  587. request: Request,
  588. q: Optional[str] = Query(default=None, description="模糊搜索车牌号"),
  589. vehicle_type: Optional[str] = Query(default=None, alias="vehicleType", description="车辆类型"),
  590. carrier_id: Optional[str] = Query(default=None, alias="carrierId", description="承运商ID"),
  591. vehicle_status: Optional[str] = Query(default=None, alias="vehicleStatus", description="车辆状态"),
  592. exclude_ineligible_carrier: Optional[bool] = Query(
  593. default=False,
  594. alias="excludeIneligibleCarrier",
  595. description="排除资质不全或协议过期的承运商车辆",
  596. ),
  597. mine_vehicles: Optional[bool] = Query(
  598. default=None,
  599. alias="mineVehicles",
  600. description="仅返回当前用户关联驾押人员所挂靠车辆",
  601. ),
  602. page: int = Query(default=1, ge=1),
  603. page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
  604. db: Session = Depends(get_db),
  605. user: DbUserContext = Depends(get_db_current_user),
  606. ) -> VehicleListResponse:
  607. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  608. params = _merge_query(
  609. request,
  610. q=q,
  611. vehicleType=vehicle_type,
  612. carrierId=carrier_id,
  613. vehicleStatus=vehicle_status,
  614. mineVehicles=mine_vehicles,
  615. )
  616. query = db.query(PlVehicle).filter(
  617. PlVehicle.tenant_no == tenant_no,
  618. PlVehicle.del_flag == 0,
  619. )
  620. vt = _param_get(params, "vehicleType", "vehicle_type")
  621. if vt:
  622. query = query.filter(PlVehicle.vehicle_type == vt)
  623. cid = _param_get(params, "carrierId", "carrier_id")
  624. if cid:
  625. query = query.filter(PlVehicle.carrier_id == cid)
  626. qv = _param_get(params, "q")
  627. if qv:
  628. query = query.filter(PlVehicle.license_plate.contains(str(qv)))
  629. from app.services.pl.vehicle_list_filters import build_vehicle_column_filters
  630. column_filters = build_vehicle_column_filters(
  631. **_vehicle_column_filter_kwargs(params)
  632. )
  633. if column_filters:
  634. query = query.filter(*column_filters)
  635. mine_flag = _param_get(params, "mineVehicles", "mine_vehicles")
  636. if mine_flag is True or (
  637. mine_flag is not None
  638. and str(mine_flag).strip().lower() in {"1", "true", "yes", "y"}
  639. ):
  640. from app.services.pl_vehicle_mine_vehicles import (
  641. license_plates_for_user_personnel,
  642. should_restrict_mine_vehicles,
  643. )
  644. if should_restrict_mine_vehicles(
  645. db, tenant_no, user.id, mine_vehicles=True
  646. ):
  647. plates = license_plates_for_user_personnel(db, tenant_no, user.id)
  648. if not plates:
  649. return VehicleListResponse(total=0, list=[])
  650. query = query.filter(PlVehicle.license_plate.in_(list(plates)))
  651. # 车辆为主数据,列表不做 create_by 数据范围过滤
  652. query = query.order_by(
  653. case((PlVehicle.order_num.is_(None), 1), else_=0).asc(),
  654. PlVehicle.create_time.desc(),
  655. )
  656. if exclude_ineligible_carrier:
  657. vehicles = query.all()
  658. carrier_ids = {
  659. v.carrier_id for v in vehicles if v.vehicle_type == "carrier" and v.carrier_id
  660. }
  661. carrier_map: dict[str, PlCarrier] = {}
  662. if carrier_ids:
  663. carriers = (
  664. db.query(PlCarrier)
  665. .filter(
  666. PlCarrier.id.in_(carrier_ids),
  667. PlCarrier.tenant_no == tenant_no,
  668. PlCarrier.del_flag == 0,
  669. )
  670. .all()
  671. )
  672. carrier_map = {c.id: c for c in carriers}
  673. vehicles = [
  674. v
  675. for v in vehicles
  676. if v.vehicle_type != "carrier"
  677. or not v.carrier_id
  678. or (
  679. v.carrier_id in carrier_map
  680. and carrier_eligible_for_transport(carrier_map[v.carrier_id])
  681. )
  682. ]
  683. summaries = load_vehicle_personnel_summaries(
  684. db, tenant_no, [v.id for v in vehicles]
  685. )
  686. certs_by_owner = list_owner_certs_batch(
  687. db, tenant_no, "vehicle", [v.id for v in vehicles]
  688. )
  689. items = [
  690. _vehicle_to_read(
  691. v,
  692. *summaries.get(v.id, ("", None)),
  693. cert_list=certs_to_read(certs_by_owner.get(v.id, [])),
  694. )
  695. for v in vehicles
  696. ]
  697. return VehicleListResponse(total=len(items), list=items)
  698. total = query.count()
  699. offset = (page - 1) * page_size
  700. vehicles = query.offset(offset).limit(page_size).all()
  701. summaries = load_vehicle_personnel_summaries(
  702. db, tenant_no, [v.id for v in vehicles]
  703. )
  704. certs_by_owner = list_owner_certs_batch(
  705. db, tenant_no, "vehicle", [v.id for v in vehicles]
  706. )
  707. return VehicleListResponse(
  708. total=total,
  709. list=[
  710. _vehicle_to_read(
  711. v,
  712. *summaries.get(v.id, ("", None)),
  713. cert_list=certs_to_read(certs_by_owner.get(v.id, [])),
  714. )
  715. for v in vehicles
  716. ],
  717. )
  718. VEHICLE_EXPORT_COLUMNS = [
  719. ExportColumn(title="车牌号", field="licensePlate"),
  720. ExportColumn(title="关联人员", field="personnelSummary"),
  721. ExportColumn(title="车辆类型", field="vehicleTypeLabel"),
  722. ExportColumn(title="车辆状态", field="vehicleStatusLabel"),
  723. ExportColumn(title="载重(吨)", field="loadCapacity"),
  724. ExportColumn(title="承运商", field="carrierName"),
  725. ExportColumn(title="备注", field="remark"),
  726. ]
  727. _VEHICLE_TYPE_LABEL = {"self": "自有车", "carrier": "承运商车"}
  728. _VEHICLE_STATUS_LABEL = {"1": "启用", "0": "停用", "idle": "空闲", "busy": "在途", "maintain": "维保"}
  729. class VehicleExportBody(BaseModel):
  730. q: Optional[str] = None
  731. vehicle_type: Optional[str] = Field(default=None, alias="vehicleType")
  732. carrier_id: Optional[str] = Field(default=None, alias="carrierId")
  733. vehicle_status: Optional[str] = Field(default=None, alias="vehicleStatus")
  734. ids: Optional[List[str]] = None
  735. columns: Optional[List[str]] = None
  736. model_config = {"populate_by_name": True, "extra": "allow"}
  737. @router.post("/vehicle/export")
  738. def export_vehicles(
  739. body: VehicleExportBody,
  740. db: Session = Depends(get_db),
  741. user: DbUserContext = Depends(get_db_current_user),
  742. ):
  743. """导出车辆列表(当前筛选全量,或 ids 已选)。"""
  744. try:
  745. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  746. params = _body_to_params(body)
  747. query = db.query(PlVehicle).filter(
  748. PlVehicle.tenant_no == tenant_no,
  749. PlVehicle.del_flag == 0,
  750. )
  751. vt = _param_get(params, "vehicleType", "vehicle_type")
  752. if vt:
  753. query = query.filter(PlVehicle.vehicle_type == vt)
  754. cid = _param_get(params, "carrierId", "carrier_id")
  755. if cid:
  756. query = query.filter(PlVehicle.carrier_id == cid)
  757. qv = _param_get(params, "q")
  758. if qv:
  759. query = query.filter(PlVehicle.license_plate.contains(str(qv)))
  760. from app.services.pl.vehicle_list_filters import build_vehicle_column_filters
  761. column_filters = build_vehicle_column_filters(
  762. **_vehicle_column_filter_kwargs(params)
  763. )
  764. if column_filters:
  765. query = query.filter(*column_filters)
  766. if body.ids:
  767. query = query.filter(PlVehicle.id.in_(body.ids))
  768. query = query.order_by(
  769. case((PlVehicle.order_num.is_(None), 1), else_=0).asc(),
  770. PlVehicle.create_time.desc(),
  771. )
  772. vehicles = query.limit(EXPORT_MAX_ROWS + 1).all()
  773. summaries = load_vehicle_personnel_summaries(db, tenant_no, [v.id for v in vehicles])
  774. items = []
  775. for v in vehicles:
  776. data = _vehicle_to_read(v, *summaries.get(v.id, ("", None))).model_dump(by_alias=True)
  777. vt = str(data.get("vehicleType") or "")
  778. data["vehicleTypeLabel"] = _VEHICLE_TYPE_LABEL.get(vt, vt)
  779. vs = str(data.get("vehicleStatus") or "")
  780. data["vehicleStatusLabel"] = _VEHICLE_STATUS_LABEL.get(vs, vs)
  781. items.append(data)
  782. requested = None
  783. if body.columns:
  784. requested = []
  785. for key in body.columns:
  786. if key in ("photoUrls", "certAttachments", "certList"):
  787. continue
  788. if key == "vehicleType":
  789. requested.append("vehicleTypeLabel")
  790. elif key == "vehicleStatus":
  791. requested.append("vehicleStatusLabel")
  792. else:
  793. requested.append(key)
  794. cols = filter_export_columns(VEHICLE_EXPORT_COLUMNS, requested)
  795. date_str = datetime.now().strftime("%Y%m%d")
  796. return export_rows_to_xlsx_response(
  797. db,
  798. tenant_no=tenant_no,
  799. sheet_title="车辆列表",
  800. columns=cols,
  801. rows=items,
  802. filename_stem=f"车辆列表_{date_str}",
  803. )
  804. except ExportValidationError as e:
  805. raise HTTPException(status_code=400, detail=str(e)) from e
  806. @router.post("/vehicle", response_model=VehicleDetailRead, status_code=status.HTTP_201_CREATED)
  807. def create_vehicle(
  808. payload: VehicleCreate,
  809. db: Session = Depends(get_db),
  810. user: DbUserContext = Depends(get_db_current_user),
  811. ) -> VehicleDetailRead:
  812. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  813. existing = (
  814. db.query(PlVehicle)
  815. .filter(
  816. PlVehicle.license_plate == payload.license_plate,
  817. PlVehicle.del_flag == 0,
  818. )
  819. .first()
  820. )
  821. if existing:
  822. raise HTTPException(status_code=400, detail="车牌号已存在")
  823. validate_vehicle_save_certs(
  824. db,
  825. tenant_no,
  826. vehicle_type=payload.vehicle_type,
  827. trailer_no=payload.trailer_no,
  828. cert_rows=payload.cert_list,
  829. personnel_rows=payload.personnel_list,
  830. )
  831. vehicle = PlVehicle(
  832. id=generate_uuid(),
  833. tenant_no=tenant_no,
  834. vehicle_type=payload.vehicle_type,
  835. carrier_id=payload.carrier_id,
  836. carrier_name=payload.carrier_name,
  837. license_plate=payload.license_plate,
  838. trailer_no=payload.trailer_no,
  839. load_capacity=payload.load_capacity,
  840. vehicle_status=payload.vehicle_status,
  841. photo_urls=payload.photo_urls,
  842. remark=payload.remark,
  843. create_by=user.id,
  844. create_dept_id=user.dept_id,
  845. )
  846. db.add(vehicle)
  847. db.flush()
  848. try:
  849. save_vehicle_personnel(
  850. db,
  851. vehicle.id,
  852. payload.personnel_list,
  853. tenant_no,
  854. user_id=user.id,
  855. dept_id=user.dept_id,
  856. vehicle_type=payload.vehicle_type,
  857. )
  858. save_owner_certs(
  859. db,
  860. "vehicle",
  861. vehicle.id,
  862. payload.cert_list,
  863. tenant_no,
  864. user_id=user.id,
  865. dept_id=user.dept_id,
  866. )
  867. db.commit()
  868. except HTTPException:
  869. db.rollback()
  870. raise
  871. except Exception:
  872. db.rollback()
  873. raise
  874. db.refresh(vehicle)
  875. return _vehicle_to_detail(db, tenant_no, vehicle)
  876. @router.get("/vehicle/{id}", response_model=VehicleDetailRead)
  877. def get_vehicle(
  878. id: str,
  879. db: Session = Depends(get_db),
  880. user: DbUserContext = Depends(get_db_current_user),
  881. ) -> VehicleDetailRead:
  882. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  883. vehicle = _get_vehicle(db, tenant_no, id, user)
  884. return _vehicle_to_detail(db, tenant_no, vehicle)
  885. @router.put("/vehicle/{id}", response_model=VehicleDetailRead)
  886. def update_vehicle(
  887. id: str,
  888. payload: VehicleUpdate,
  889. db: Session = Depends(get_db),
  890. user: DbUserContext = Depends(get_db_current_user),
  891. ) -> VehicleDetailRead:
  892. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  893. vehicle = _get_vehicle(db, tenant_no, id, user)
  894. personnel_list = payload.personnel_list
  895. cert_list = payload.cert_list
  896. data = payload.model_dump(
  897. exclude_unset=True, exclude={"personnel_list", "cert_list"}
  898. )
  899. effective_vehicle_type = data.get("vehicle_type", vehicle.vehicle_type)
  900. effective_trailer_no = data.get("trailer_no", vehicle.trailer_no)
  901. effective_cert_rows = (
  902. cert_list
  903. if cert_list is not None
  904. else list_owner_certs(db, tenant_no, "vehicle", vehicle.id)
  905. )
  906. effective_personnel_rows = personnel_list
  907. if effective_personnel_rows is None:
  908. effective_personnel_rows = load_vehicle_personnel_rows(db, tenant_no, vehicle.id)
  909. validate_vehicle_save_certs(
  910. db,
  911. tenant_no,
  912. vehicle_type=effective_vehicle_type,
  913. trailer_no=effective_trailer_no,
  914. cert_rows=effective_cert_rows,
  915. personnel_rows=effective_personnel_rows,
  916. )
  917. for key, value in data.items():
  918. setattr(vehicle, key, value)
  919. vehicle.update_by = user.id
  920. try:
  921. if personnel_list is not None:
  922. save_vehicle_personnel(
  923. db,
  924. vehicle.id,
  925. personnel_list,
  926. tenant_no,
  927. user_id=user.id,
  928. dept_id=user.dept_id,
  929. vehicle_type=effective_vehicle_type,
  930. )
  931. if cert_list is not None:
  932. save_owner_certs(
  933. db,
  934. "vehicle",
  935. vehicle.id,
  936. cert_list,
  937. tenant_no,
  938. user_id=user.id,
  939. dept_id=user.dept_id,
  940. )
  941. db.commit()
  942. except HTTPException:
  943. db.rollback()
  944. raise
  945. except Exception:
  946. db.rollback()
  947. raise
  948. db.refresh(vehicle)
  949. return _vehicle_to_detail(db, tenant_no, vehicle)
  950. @router.delete("/vehicle/{id}", status_code=status.HTTP_204_NO_CONTENT)
  951. def delete_vehicle(
  952. id: str,
  953. db: Session = Depends(get_db),
  954. user: DbUserContext = Depends(get_db_current_user),
  955. ) -> None:
  956. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  957. vehicle = _get_vehicle(db, tenant_no, id, user)
  958. assert_row_deletable_by_creator(vehicle, user_info_from_context(user))
  959. vehicle.del_flag = 1
  960. vehicle.update_by = user.id
  961. db.commit()
  962. def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str, user: DbUserContext) -> PlVehicle:
  963. vehicle = (
  964. db.query(PlVehicle)
  965. .filter(
  966. PlVehicle.id == vehicle_id,
  967. PlVehicle.tenant_no == tenant_no,
  968. PlVehicle.del_flag == 0,
  969. )
  970. .first()
  971. )
  972. if not vehicle:
  973. raise HTTPException(status_code=404, detail="车辆不存在")
  974. return vehicle
  975. # =============================================================================
  976. # 人员 API
  977. # =============================================================================
  978. def _personnel_to_read(
  979. db: Session,
  980. tenant_no: str,
  981. personnel: PlPersonnel,
  982. *,
  983. cert_list: Optional[list[CertRead]] = None,
  984. ) -> PersonnelRead:
  985. nearest = nearest_expire_date(db, tenant_no, "personnel", personnel.id)
  986. data = PersonnelRead.model_validate(personnel)
  987. data.nearest_expire_date = nearest
  988. if cert_list is not None:
  989. data.cert_list = cert_list
  990. return data
  991. def _assert_personnel_sys_user_bind(
  992. db: Session,
  993. tenant_no: str,
  994. *,
  995. personnel_id: str,
  996. sys_user_id: Optional[str],
  997. ) -> None:
  998. uid = (sys_user_id or "").strip()
  999. if not uid:
  1000. return
  1001. existing = (
  1002. db.query(PlPersonnel)
  1003. .filter(
  1004. PlPersonnel.tenant_no == tenant_no,
  1005. PlPersonnel.sys_user_id == uid,
  1006. PlPersonnel.del_flag == 0,
  1007. )
  1008. .all()
  1009. )
  1010. try:
  1011. assert_sys_user_bind_available(
  1012. existing=existing,
  1013. personnel_id=personnel_id,
  1014. sys_user_id=uid,
  1015. )
  1016. except ValueError as exc:
  1017. raise HTTPException(status_code=400, detail=str(exc)) from exc
  1018. def _personnel_to_detail(
  1019. db: Session, tenant_no: str, personnel: PlPersonnel
  1020. ) -> PersonnelDetailRead:
  1021. base = _personnel_to_read(db, tenant_no, personnel)
  1022. cert_list = certs_to_read(
  1023. list_owner_certs(db, tenant_no, "personnel", personnel.id)
  1024. )
  1025. return PersonnelDetailRead(
  1026. **base.model_dump(by_alias=False, exclude={"cert_list"}),
  1027. cert_list=cert_list,
  1028. )
  1029. @router.get("/personnel", response_model=PersonnelListResponse)
  1030. def list_personnel(
  1031. request: Request,
  1032. q: Optional[str] = Query(default=None, description="模糊搜索姓名/手机号"),
  1033. status: Optional[str] = Query(default=None, description="状态"),
  1034. personnel_affiliation: Optional[str] = Query(
  1035. default=None, alias="personnelAffiliation", description="人员归属 self/carrier"
  1036. ),
  1037. personnel_type: Optional[str] = Query(
  1038. default=None, alias="personnelType", description="人员类型 driver/escort"
  1039. ),
  1040. page: int = Query(default=1, ge=1),
  1041. page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
  1042. db: Session = Depends(get_db),
  1043. user: DbUserContext = Depends(get_db_current_user),
  1044. ) -> PersonnelListResponse:
  1045. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1046. params = _merge_query(
  1047. request,
  1048. q=q,
  1049. status=status,
  1050. personnelAffiliation=personnel_affiliation,
  1051. personnelType=personnel_type,
  1052. )
  1053. query = db.query(PlPersonnel).filter(
  1054. PlPersonnel.tenant_no == tenant_no,
  1055. PlPersonnel.del_flag == 0,
  1056. )
  1057. affiliation = _param_get(params, "personnelAffiliation", "personnel_affiliation")
  1058. if affiliation:
  1059. query = query.filter(PlPersonnel.personnel_affiliation == affiliation)
  1060. qv = _param_get(params, "q")
  1061. if qv:
  1062. query = query.filter(
  1063. or_(
  1064. func.lower(PlPersonnel.personnel_name).contains(str(qv).lower()),
  1065. PlPersonnel.phone_no.contains(str(qv)),
  1066. )
  1067. )
  1068. from app.services.pl.personnel_list_filters import build_personnel_column_filters
  1069. column_filters = build_personnel_column_filters(
  1070. **_personnel_column_filter_kwargs(params)
  1071. )
  1072. if column_filters:
  1073. query = query.filter(*column_filters)
  1074. # 人员为主数据,列表不做 create_by 数据范围过滤
  1075. query = query.order_by(
  1076. case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),
  1077. PlPersonnel.create_time.desc(),
  1078. )
  1079. total = query.count()
  1080. offset = (page - 1) * page_size
  1081. rows = query.offset(offset).limit(page_size).all()
  1082. personnel_ids = [p.id for p in rows]
  1083. certs_by_owner = list_owner_certs_batch(db, tenant_no, "personnel", personnel_ids)
  1084. return PersonnelListResponse(
  1085. total=total,
  1086. list=[
  1087. _personnel_to_read(
  1088. db,
  1089. tenant_no,
  1090. p,
  1091. cert_list=certs_to_read(certs_by_owner.get(p.id, [])),
  1092. )
  1093. for p in rows
  1094. ],
  1095. )
  1096. PERSONNEL_EXPORT_COLUMNS = [
  1097. ExportColumn(title="姓名", field="personnelName"),
  1098. ExportColumn(title="人员归属", field="personnelAffiliationLabel"),
  1099. ExportColumn(title="人员类型", field="personnelTypeLabel"),
  1100. ExportColumn(title="手机号", field="phoneNo"),
  1101. ExportColumn(title="年龄", field="age"),
  1102. ExportColumn(title="身份证号", field="idCardNo"),
  1103. ExportColumn(title="状态", field="statusLabel"),
  1104. ExportColumn(title="最近到期", field="nearestExpireDate"),
  1105. ExportColumn(title="备注", field="remark"),
  1106. ]
  1107. _AFFILIATION_LABEL = {"self": "自有", "carrier": "承运商"}
  1108. _PERSONNEL_TYPE_LABEL = {"driver": "司机", "escort": "押运员"}
  1109. class PersonnelExportBody(BaseModel):
  1110. q: Optional[str] = None
  1111. status: Optional[str] = None
  1112. personnel_affiliation: Optional[str] = Field(default=None, alias="personnelAffiliation")
  1113. personnel_type: Optional[str] = Field(default=None, alias="personnelType")
  1114. ids: Optional[List[str]] = None
  1115. columns: Optional[List[str]] = None
  1116. model_config = {"populate_by_name": True, "extra": "allow"}
  1117. @router.post("/personnel/export")
  1118. def export_personnel(
  1119. body: PersonnelExportBody,
  1120. db: Session = Depends(get_db),
  1121. user: DbUserContext = Depends(get_db_current_user),
  1122. ):
  1123. """导出人员列表(当前筛选全量,或 ids 已选)。"""
  1124. try:
  1125. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1126. params = _body_to_params(body)
  1127. query = db.query(PlPersonnel).filter(
  1128. PlPersonnel.tenant_no == tenant_no,
  1129. PlPersonnel.del_flag == 0,
  1130. )
  1131. affiliation = _param_get(params, "personnelAffiliation", "personnel_affiliation")
  1132. if affiliation:
  1133. query = query.filter(PlPersonnel.personnel_affiliation == affiliation)
  1134. qv = _param_get(params, "q")
  1135. if qv:
  1136. query = query.filter(
  1137. or_(
  1138. func.lower(PlPersonnel.personnel_name).contains(str(qv).lower()),
  1139. PlPersonnel.phone_no.contains(str(qv)),
  1140. )
  1141. )
  1142. from app.services.pl.personnel_list_filters import build_personnel_column_filters
  1143. column_filters = build_personnel_column_filters(
  1144. **_personnel_column_filter_kwargs(params)
  1145. )
  1146. if column_filters:
  1147. query = query.filter(*column_filters)
  1148. if body.ids:
  1149. query = query.filter(PlPersonnel.id.in_(body.ids))
  1150. query = query.order_by(
  1151. case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),
  1152. PlPersonnel.create_time.desc(),
  1153. )
  1154. rows = query.limit(EXPORT_MAX_ROWS + 1).all()
  1155. items = []
  1156. for p in rows:
  1157. data = _personnel_to_read(db, tenant_no, p).model_dump(by_alias=True)
  1158. af = str(data.get("personnelAffiliation") or "")
  1159. data["personnelAffiliationLabel"] = _AFFILIATION_LABEL.get(af, af)
  1160. pt = str(data.get("personnelType") or "")
  1161. data["personnelTypeLabel"] = _PERSONNEL_TYPE_LABEL.get(pt, pt)
  1162. st = str(data.get("status") or "")
  1163. data["statusLabel"] = "启用" if st == "1" else ("停用" if st == "0" else st)
  1164. items.append(data)
  1165. requested = None
  1166. if body.columns:
  1167. requested = []
  1168. for key in body.columns:
  1169. if key in ("photoUrls", "certAttachments", "certList"):
  1170. continue
  1171. if key == "personnelAffiliation":
  1172. requested.append("personnelAffiliationLabel")
  1173. elif key == "personnelType":
  1174. requested.append("personnelTypeLabel")
  1175. elif key == "status":
  1176. requested.append("statusLabel")
  1177. else:
  1178. requested.append(key)
  1179. cols = filter_export_columns(PERSONNEL_EXPORT_COLUMNS, requested)
  1180. date_str = datetime.now().strftime("%Y%m%d")
  1181. return export_rows_to_xlsx_response(
  1182. db,
  1183. tenant_no=tenant_no,
  1184. sheet_title="人员列表",
  1185. columns=cols,
  1186. rows=items,
  1187. filename_stem=f"人员列表_{date_str}",
  1188. )
  1189. except ExportValidationError as e:
  1190. raise HTTPException(status_code=400, detail=str(e)) from e
  1191. @router.post("/personnel", response_model=PersonnelDetailRead, status_code=status.HTTP_201_CREATED)
  1192. def create_personnel(
  1193. payload: PersonnelCreate,
  1194. db: Session = Depends(get_db),
  1195. user: DbUserContext = Depends(get_db_current_user),
  1196. ) -> PersonnelDetailRead:
  1197. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1198. validate_personnel_required_certs(
  1199. payload.cert_list,
  1200. payload.personnel_type,
  1201. personnel_affiliation=payload.personnel_affiliation,
  1202. subject_label=f"「{payload.personnel_name}」",
  1203. )
  1204. personnel_id = generate_uuid()
  1205. _assert_personnel_sys_user_bind(
  1206. db,
  1207. tenant_no,
  1208. personnel_id=personnel_id,
  1209. sys_user_id=payload.sys_user_id,
  1210. )
  1211. personnel = PlPersonnel(
  1212. id=personnel_id,
  1213. tenant_no=tenant_no,
  1214. personnel_name=payload.personnel_name,
  1215. phone_no=payload.phone_no,
  1216. age=payload.age,
  1217. id_card_no=payload.id_card_no,
  1218. personnel_type=payload.personnel_type,
  1219. personnel_affiliation=payload.personnel_affiliation,
  1220. status=payload.status,
  1221. photo_urls=payload.photo_urls,
  1222. sys_user_id=(payload.sys_user_id or "").strip() or None,
  1223. remark=payload.remark,
  1224. create_by=user.id,
  1225. create_dept_id=user.dept_id,
  1226. )
  1227. db.add(personnel)
  1228. db.flush()
  1229. try:
  1230. save_owner_certs(
  1231. db,
  1232. "personnel",
  1233. personnel.id,
  1234. payload.cert_list,
  1235. tenant_no,
  1236. user_id=user.id,
  1237. dept_id=user.dept_id,
  1238. )
  1239. db.commit()
  1240. except Exception:
  1241. db.rollback()
  1242. raise
  1243. db.refresh(personnel)
  1244. return _personnel_to_detail(db, tenant_no, personnel)
  1245. @router.get("/personnel/{id}", response_model=PersonnelDetailRead)
  1246. def get_personnel(
  1247. id: str,
  1248. db: Session = Depends(get_db),
  1249. user: DbUserContext = Depends(get_db_current_user),
  1250. ) -> PersonnelDetailRead:
  1251. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1252. personnel = _get_personnel(db, tenant_no, id, user)
  1253. return _personnel_to_detail(db, tenant_no, personnel)
  1254. @router.put("/personnel/{id}", response_model=PersonnelDetailRead)
  1255. def update_personnel(
  1256. id: str,
  1257. payload: PersonnelUpdate,
  1258. db: Session = Depends(get_db),
  1259. user: DbUserContext = Depends(get_db_current_user),
  1260. ) -> PersonnelDetailRead:
  1261. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1262. personnel = _get_personnel(db, tenant_no, id, user)
  1263. cert_list = payload.cert_list
  1264. data = payload.model_dump(exclude_unset=True, exclude={"cert_list"})
  1265. effective_personnel_type = data.get("personnel_type", personnel.personnel_type)
  1266. effective_personnel_affiliation = data.get(
  1267. "personnel_affiliation", personnel.personnel_affiliation
  1268. )
  1269. effective_cert_rows = (
  1270. cert_list
  1271. if cert_list is not None
  1272. else list_owner_certs(db, tenant_no, "personnel", personnel.id)
  1273. )
  1274. subject_name = data.get("personnel_name", personnel.personnel_name)
  1275. validate_personnel_required_certs(
  1276. effective_cert_rows,
  1277. effective_personnel_type,
  1278. personnel_affiliation=effective_personnel_affiliation,
  1279. subject_label=f"「{subject_name}」",
  1280. )
  1281. if "sys_user_id" in data:
  1282. raw_uid = data.get("sys_user_id")
  1283. data["sys_user_id"] = (raw_uid or "").strip() or None
  1284. _assert_personnel_sys_user_bind(
  1285. db,
  1286. tenant_no,
  1287. personnel_id=personnel.id,
  1288. sys_user_id=data["sys_user_id"],
  1289. )
  1290. for key, value in data.items():
  1291. setattr(personnel, key, value)
  1292. personnel.update_by = user.id
  1293. try:
  1294. if cert_list is not None:
  1295. save_owner_certs(
  1296. db,
  1297. "personnel",
  1298. personnel.id,
  1299. cert_list,
  1300. tenant_no,
  1301. user_id=user.id,
  1302. dept_id=user.dept_id,
  1303. )
  1304. db.commit()
  1305. except Exception:
  1306. db.rollback()
  1307. raise
  1308. db.refresh(personnel)
  1309. return _personnel_to_detail(db, tenant_no, personnel)
  1310. @router.delete("/personnel/{id}", status_code=status.HTTP_204_NO_CONTENT)
  1311. def delete_personnel(
  1312. id: str,
  1313. db: Session = Depends(get_db),
  1314. user: DbUserContext = Depends(get_db_current_user),
  1315. ) -> None:
  1316. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1317. personnel = _get_personnel(db, tenant_no, id, user)
  1318. assert_row_deletable_by_creator(personnel, user_info_from_context(user))
  1319. personnel.del_flag = 1
  1320. personnel.update_by = user.id
  1321. db.commit()
  1322. def _get_personnel(db: Session, tenant_no: str, personnel_id: str, user: DbUserContext) -> PlPersonnel:
  1323. personnel = (
  1324. db.query(PlPersonnel)
  1325. .filter(
  1326. PlPersonnel.id == personnel_id,
  1327. PlPersonnel.tenant_no == tenant_no,
  1328. PlPersonnel.del_flag == 0,
  1329. )
  1330. .first()
  1331. )
  1332. if not personnel:
  1333. raise HTTPException(status_code=404, detail="人员不存在")
  1334. return personnel
  1335. # =============================================================================
  1336. # 司机 API(兼容别名,内部使用 pl_personnel)
  1337. # =============================================================================
  1338. def _personnel_to_driver_read(personnel: PlPersonnel) -> DriverRead:
  1339. return DriverRead(
  1340. id=personnel.id,
  1341. driver_name=personnel.personnel_name,
  1342. phone_no=personnel.phone_no,
  1343. id_card_no=personnel.id_card_no,
  1344. tenant_no=personnel.tenant_no,
  1345. order_num=personnel.order_num,
  1346. remark=personnel.remark,
  1347. create_by=personnel.create_by,
  1348. create_dept_id=personnel.create_dept_id,
  1349. create_time=personnel.create_time,
  1350. update_by=personnel.update_by,
  1351. update_time=personnel.update_time,
  1352. del_flag=personnel.del_flag,
  1353. )
  1354. @router.get("/driver", response_model=DriverListResponse)
  1355. def list_drivers(
  1356. q: Optional[str] = Query(default=None, description="模糊搜索姓名/手机号"),
  1357. page: int = Query(default=1, ge=1),
  1358. page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
  1359. db: Session = Depends(get_db),
  1360. user: DbUserContext = Depends(get_db_current_user),
  1361. ) -> DriverListResponse:
  1362. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1363. query = db.query(PlPersonnel).filter(
  1364. PlPersonnel.tenant_no == tenant_no,
  1365. PlPersonnel.del_flag == 0,
  1366. )
  1367. if q:
  1368. query = query.filter(
  1369. or_(
  1370. func.lower(PlPersonnel.personnel_name).contains(q.lower()),
  1371. PlPersonnel.phone_no.contains(q),
  1372. )
  1373. )
  1374. # 司机列表为主数据选项,不做 create_by 数据范围过滤
  1375. query = query.order_by(
  1376. case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),
  1377. PlPersonnel.create_time.desc(),
  1378. )
  1379. total = query.count()
  1380. offset = (page - 1) * page_size
  1381. rows = query.offset(offset).limit(page_size).all()
  1382. return DriverListResponse(
  1383. total=total,
  1384. list=[_personnel_to_driver_read(p) for p in rows],
  1385. )
  1386. DRIVER_EXPORT_COLUMNS = [
  1387. ExportColumn(title="司机姓名", field="driverName"),
  1388. ExportColumn(title="手机号", field="phoneNo"),
  1389. ExportColumn(title="身份证号", field="idCardNo"),
  1390. ExportColumn(title="备注", field="remark"),
  1391. ]
  1392. class DriverExportBody(BaseModel):
  1393. q: Optional[str] = None
  1394. ids: Optional[List[str]] = None
  1395. columns: Optional[List[str]] = None
  1396. @router.post("/driver/export")
  1397. def export_drivers(
  1398. body: DriverExportBody,
  1399. db: Session = Depends(get_db),
  1400. user: DbUserContext = Depends(get_db_current_user),
  1401. ):
  1402. """导出司机列表(当前筛选全量,或 ids 已选)。"""
  1403. try:
  1404. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1405. query = db.query(PlPersonnel).filter(
  1406. PlPersonnel.tenant_no == tenant_no,
  1407. PlPersonnel.del_flag == 0,
  1408. )
  1409. if body.q:
  1410. query = query.filter(
  1411. or_(
  1412. func.lower(PlPersonnel.personnel_name).contains(body.q.lower()),
  1413. PlPersonnel.phone_no.contains(body.q),
  1414. )
  1415. )
  1416. if body.ids:
  1417. query = query.filter(PlPersonnel.id.in_(body.ids))
  1418. query = query.order_by(
  1419. case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),
  1420. PlPersonnel.create_time.desc(),
  1421. )
  1422. rows = query.limit(EXPORT_MAX_ROWS + 1).all()
  1423. items = [_personnel_to_driver_read(p).model_dump(by_alias=True) for p in rows]
  1424. cols = filter_export_columns(DRIVER_EXPORT_COLUMNS, body.columns)
  1425. date_str = datetime.now().strftime("%Y%m%d")
  1426. return export_rows_to_xlsx_response(
  1427. db,
  1428. tenant_no=tenant_no,
  1429. sheet_title="司机列表",
  1430. columns=cols,
  1431. rows=items,
  1432. filename_stem=f"司机列表_{date_str}",
  1433. )
  1434. except ExportValidationError as e:
  1435. raise HTTPException(status_code=400, detail=str(e)) from e
  1436. @router.post("/driver", response_model=DriverRead, status_code=status.HTTP_201_CREATED)
  1437. def create_driver(
  1438. payload: DriverCreate,
  1439. db: Session = Depends(get_db),
  1440. user: DbUserContext = Depends(get_db_current_user),
  1441. ) -> DriverRead:
  1442. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1443. personnel = PlPersonnel(
  1444. id=generate_uuid(),
  1445. tenant_no=tenant_no,
  1446. personnel_name=payload.driver_name,
  1447. phone_no=payload.phone_no,
  1448. id_card_no=payload.id_card_no,
  1449. status="1",
  1450. remark=payload.remark,
  1451. create_by=user.id,
  1452. create_dept_id=user.dept_id,
  1453. )
  1454. db.add(personnel)
  1455. db.commit()
  1456. db.refresh(personnel)
  1457. return _personnel_to_driver_read(personnel)
  1458. @router.get("/driver/{id}", response_model=DriverRead)
  1459. def get_driver(
  1460. id: str,
  1461. db: Session = Depends(get_db),
  1462. user: DbUserContext = Depends(get_db_current_user),
  1463. ) -> DriverRead:
  1464. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1465. personnel = _get_personnel(db, tenant_no, id, user)
  1466. return _personnel_to_driver_read(personnel)
  1467. @router.put("/driver/{id}", response_model=DriverRead)
  1468. def update_driver(
  1469. id: str,
  1470. payload: DriverUpdate,
  1471. db: Session = Depends(get_db),
  1472. user: DbUserContext = Depends(get_db_current_user),
  1473. ) -> DriverRead:
  1474. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1475. personnel = _get_personnel(db, tenant_no, id, user)
  1476. if payload.driver_name is not None:
  1477. personnel.personnel_name = payload.driver_name
  1478. if payload.phone_no is not None:
  1479. personnel.phone_no = payload.phone_no
  1480. if payload.id_card_no is not None:
  1481. personnel.id_card_no = payload.id_card_no
  1482. if payload.remark is not None:
  1483. personnel.remark = payload.remark
  1484. personnel.update_by = user.id
  1485. db.commit()
  1486. db.refresh(personnel)
  1487. return _personnel_to_driver_read(personnel)
  1488. @router.delete("/driver/{id}", status_code=status.HTTP_204_NO_CONTENT)
  1489. def delete_driver(
  1490. id: str,
  1491. db: Session = Depends(get_db),
  1492. user: DbUserContext = Depends(get_db_current_user),
  1493. ) -> None:
  1494. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1495. personnel = _get_personnel(db, tenant_no, id, user)
  1496. assert_row_deletable_by_creator(personnel, user_info_from_context(user))
  1497. personnel.del_flag = 1
  1498. personnel.update_by = user.id
  1499. db.commit()
  1500. # =============================================================================
  1501. # 车辆-司机绑定 API(兼容,写入 pl_vehicle_personnel)
  1502. # =============================================================================
  1503. @router.post("/vehicle-driver", status_code=status.HTTP_201_CREATED)
  1504. def bind_vehicle_driver(
  1505. payload: VehicleDriverCreate,
  1506. db: Session = Depends(get_db),
  1507. user: DbUserContext = Depends(get_db_current_user),
  1508. ) -> dict:
  1509. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1510. _get_vehicle(db, tenant_no, payload.vehicle_id, user)
  1511. _get_personnel(db, tenant_no, payload.driver_id, user)
  1512. if payload.is_default == 1:
  1513. db.query(PlVehiclePersonnel).filter(
  1514. PlVehiclePersonnel.vehicle_id == payload.vehicle_id,
  1515. PlVehiclePersonnel.role == "driver",
  1516. PlVehiclePersonnel.del_flag == 0,
  1517. ).update({"is_default": 0})
  1518. vd = PlVehiclePersonnel(
  1519. id=generate_uuid(),
  1520. tenant_no=tenant_no,
  1521. vehicle_id=payload.vehicle_id,
  1522. personnel_id=payload.driver_id,
  1523. role="driver",
  1524. is_default=payload.is_default,
  1525. create_by=user.id,
  1526. create_dept_id=user.dept_id,
  1527. )
  1528. db.add(vd)
  1529. db.commit()
  1530. return {"id": vd.id}
  1531. @router.delete("/vehicle-driver/{id}", status_code=status.HTTP_204_NO_CONTENT)
  1532. def unbind_vehicle_driver(
  1533. id: str,
  1534. db: Session = Depends(get_db),
  1535. user: DbUserContext = Depends(get_db_current_user),
  1536. ) -> None:
  1537. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1538. vd = (
  1539. db.query(PlVehiclePersonnel)
  1540. .filter(
  1541. PlVehiclePersonnel.id == id,
  1542. PlVehiclePersonnel.tenant_no == tenant_no,
  1543. PlVehiclePersonnel.del_flag == 0,
  1544. )
  1545. .first()
  1546. )
  1547. if not vd:
  1548. vd = (
  1549. db.query(PlVehicleDriver)
  1550. .filter(
  1551. PlVehicleDriver.id == id,
  1552. PlVehicleDriver.tenant_no == tenant_no,
  1553. PlVehicleDriver.del_flag == 0,
  1554. )
  1555. .first()
  1556. )
  1557. if vd:
  1558. assert_row_deletable_by_creator(vd, user_info_from_context(user))
  1559. vd.del_flag = 1
  1560. db.commit()
  1561. @router.post("/paste-recognize/commit", response_model=PasteCommitResult)
  1562. def paste_recognize_commit(
  1563. payload: PasteCommitBody,
  1564. db: Session = Depends(get_db),
  1565. user: DbUserContext = Depends(get_db_current_user),
  1566. ) -> PasteCommitResult:
  1567. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  1568. try:
  1569. result = commit_paste_recognize(
  1570. db,
  1571. tenant_no,
  1572. payload,
  1573. user_id=user.id,
  1574. dept_id=user.dept_id,
  1575. )
  1576. db.commit()
  1577. return result
  1578. except HTTPException:
  1579. db.rollback()
  1580. raise
  1581. except Exception:
  1582. db.rollback()
  1583. raise