pl_cert_service.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. """运输计划 - 证书 CRUD 与到期扫描"""
  2. from __future__ import annotations
  3. from datetime import date
  4. from typing import Optional
  5. from sqlalchemy.orm import Session
  6. from app.core.utils import generate_uuid
  7. from app.models.pl_models import PlCert, PlDocumentReminder, PlPersonnel, PlVehicle
  8. from app.schemas.pl.base_data import CertRead, CertRow
  9. def list_owner_certs(
  10. db: Session, tenant_no: str, owner_type: str, owner_id: str
  11. ) -> list[PlCert]:
  12. return (
  13. db.query(PlCert)
  14. .filter(
  15. PlCert.tenant_no == tenant_no,
  16. PlCert.owner_type == owner_type,
  17. PlCert.owner_id == owner_id,
  18. PlCert.del_flag == 0,
  19. )
  20. .order_by(PlCert.order_num.asc(), PlCert.create_time.asc())
  21. .all()
  22. )
  23. def list_owner_certs_batch(
  24. db: Session, tenant_no: str, owner_type: str, owner_ids: list[str]
  25. ) -> dict[str, list[PlCert]]:
  26. if not owner_ids:
  27. return {}
  28. rows = (
  29. db.query(PlCert)
  30. .filter(
  31. PlCert.tenant_no == tenant_no,
  32. PlCert.owner_type == owner_type,
  33. PlCert.owner_id.in_(owner_ids),
  34. PlCert.del_flag == 0,
  35. )
  36. .order_by(PlCert.order_num.asc(), PlCert.create_time.asc())
  37. .all()
  38. )
  39. grouped: dict[str, list[PlCert]] = {}
  40. for cert in rows:
  41. grouped.setdefault(cert.owner_id, []).append(cert)
  42. return grouped
  43. def certs_to_read(certs: list[PlCert]) -> list[CertRead]:
  44. return [
  45. CertRead(
  46. id=c.id,
  47. cert_id=c.id,
  48. vehicle_part=c.vehicle_part,
  49. cert_name=c.cert_name,
  50. cert_no=c.cert_no,
  51. expire_date=c.expire_date,
  52. remind_days=c.remind_days,
  53. attachment_urls=c.attachment_urls,
  54. owner_type=c.owner_type,
  55. owner_id=c.owner_id,
  56. status=c.status,
  57. tenant_no=c.tenant_no,
  58. create_time=c.create_time,
  59. update_time=c.update_time,
  60. )
  61. for c in certs
  62. ]
  63. def _normalize_cert_row(row: CertRow | dict) -> CertRow:
  64. if isinstance(row, CertRow):
  65. return row
  66. return CertRow.model_validate(row)
  67. def save_owner_certs(
  68. db: Session,
  69. owner_type: str,
  70. owner_id: str,
  71. cert_rows: list[CertRow | dict],
  72. tenant_no: str,
  73. *,
  74. user_id: str,
  75. dept_id: str,
  76. ) -> None:
  77. """按 cert_id upsert;未出现在列表中的旧证软删。"""
  78. existing = list_owner_certs(db, tenant_no, owner_type, owner_id)
  79. existing_map = {c.id: c for c in existing}
  80. keep_ids: set[str] = set()
  81. for idx, row in enumerate(cert_rows):
  82. row = _normalize_cert_row(row)
  83. cert_id = row.cert_id
  84. if cert_id and cert_id in existing_map:
  85. cert = existing_map[cert_id]
  86. cert.vehicle_part = row.vehicle_part
  87. cert.cert_name = row.cert_name
  88. cert.cert_no = row.cert_no
  89. cert.expire_date = row.expire_date
  90. cert.remind_days = row.remind_days
  91. cert.attachment_urls = row.attachment_urls
  92. cert.order_num = idx
  93. cert.update_by = user_id
  94. keep_ids.add(cert.id)
  95. else:
  96. cert = PlCert(
  97. id=generate_uuid(),
  98. tenant_no=tenant_no,
  99. owner_type=owner_type,
  100. owner_id=owner_id,
  101. vehicle_part=row.vehicle_part,
  102. cert_name=row.cert_name,
  103. cert_no=row.cert_no,
  104. expire_date=row.expire_date,
  105. remind_days=row.remind_days,
  106. attachment_urls=row.attachment_urls,
  107. status="valid",
  108. order_num=idx,
  109. create_by=user_id,
  110. create_dept_id=dept_id,
  111. )
  112. db.add(cert)
  113. db.flush()
  114. keep_ids.add(cert.id)
  115. for cert in existing:
  116. if cert.id not in keep_ids:
  117. cert.del_flag = 1
  118. cert.update_by = user_id
  119. scan_cert_expiry(db, tenant_no)
  120. def nearest_expire_date(
  121. db: Session, tenant_no: str, owner_type: str, owner_id: str
  122. ) -> Optional[date]:
  123. row = (
  124. db.query(PlCert.expire_date)
  125. .filter(
  126. PlCert.tenant_no == tenant_no,
  127. PlCert.owner_type == owner_type,
  128. PlCert.owner_id == owner_id,
  129. PlCert.del_flag == 0,
  130. PlCert.expire_date.isnot(None),
  131. )
  132. .order_by(PlCert.expire_date.asc())
  133. .first()
  134. )
  135. return row[0] if row else None
  136. def _resolve_owner_label(
  137. db: Session, tenant_no: str, owner_type: str, owner_id: str
  138. ) -> str:
  139. if owner_type == "vehicle":
  140. vehicle = (
  141. db.query(PlVehicle)
  142. .filter(
  143. PlVehicle.id == owner_id,
  144. PlVehicle.tenant_no == tenant_no,
  145. PlVehicle.del_flag == 0,
  146. )
  147. .first()
  148. )
  149. return vehicle.license_plate if vehicle else owner_id
  150. personnel = (
  151. db.query(PlPersonnel)
  152. .filter(
  153. PlPersonnel.id == owner_id,
  154. PlPersonnel.tenant_no == tenant_no,
  155. PlPersonnel.del_flag == 0,
  156. )
  157. .first()
  158. )
  159. return personnel.personnel_name if personnel else owner_id
  160. def _pending_reminder_for_cert(
  161. db: Session, cert_id: str
  162. ) -> Optional[PlDocumentReminder]:
  163. return (
  164. db.query(PlDocumentReminder)
  165. .filter(
  166. PlDocumentReminder.cert_id == cert_id,
  167. PlDocumentReminder.status == "pending",
  168. PlDocumentReminder.del_flag == 0,
  169. )
  170. .first()
  171. )
  172. def _resolve_pending_reminder(
  173. reminder: PlDocumentReminder,
  174. *,
  175. remark: str,
  176. expire_date: Optional[date] = None,
  177. days_remaining: Optional[int] = None,
  178. document_type: Optional[str] = None,
  179. document_no: Optional[str] = None,
  180. ) -> None:
  181. """关闭待处理提醒:证件已处理/退出窗口后不再继续提醒。"""
  182. reminder.status = "resolved"
  183. reminder.remark = remark
  184. if expire_date is not None:
  185. reminder.expire_date = expire_date
  186. if days_remaining is not None:
  187. reminder.days_remaining = days_remaining
  188. if document_type is not None:
  189. reminder.document_type = document_type
  190. if document_no is not None:
  191. reminder.document_no = document_no
  192. def scan_cert_expiry(db: Session, tenant_no: Optional[str] = None) -> int:
  193. """扫描 pl_cert 生成到期提醒;退出提醒窗口或证件失效时关闭旧 pending。"""
  194. today = date.today()
  195. query = db.query(PlCert).filter(
  196. PlCert.del_flag == 0,
  197. PlCert.expire_date.isnot(None),
  198. PlCert.status == "valid",
  199. )
  200. if tenant_no:
  201. query = query.filter(PlCert.tenant_no == tenant_no)
  202. active_cert_ids: set[str] = set()
  203. created_count = 0
  204. for cert in query.all():
  205. active_cert_ids.add(cert.id)
  206. days_remaining = (cert.expire_date - today).days
  207. existing = _pending_reminder_for_cert(db, cert.id)
  208. # 尚未进入提醒窗口:关闭因旧有效期留下的 pending,等真正临近再提醒
  209. if days_remaining > cert.remind_days:
  210. if existing:
  211. _resolve_pending_reminder(
  212. existing,
  213. remark="系统自动关闭:证件有效期已更新,已退出提醒窗口",
  214. expire_date=cert.expire_date,
  215. days_remaining=days_remaining,
  216. document_type=cert.cert_name,
  217. document_no=cert.cert_no,
  218. )
  219. continue
  220. owner_label = _resolve_owner_label(
  221. db, cert.tenant_no, cert.owner_type, cert.owner_id
  222. )
  223. driver_id = cert.owner_id if cert.owner_type == "personnel" else None
  224. driver_name = owner_label if cert.owner_type == "personnel" else None
  225. if existing:
  226. existing.owner_type = cert.owner_type
  227. existing.owner_id = cert.owner_id
  228. existing.owner_label = owner_label
  229. existing.driver_id = driver_id
  230. existing.driver_name = driver_name
  231. existing.document_type = cert.cert_name
  232. existing.document_no = cert.cert_no
  233. existing.expire_date = cert.expire_date
  234. existing.days_remaining = days_remaining
  235. continue
  236. reminder = PlDocumentReminder(
  237. id=generate_uuid(),
  238. tenant_no=cert.tenant_no,
  239. cert_id=cert.id,
  240. owner_type=cert.owner_type,
  241. owner_id=cert.owner_id,
  242. owner_label=owner_label,
  243. driver_id=driver_id,
  244. driver_name=driver_name,
  245. document_type=cert.cert_name,
  246. document_no=cert.cert_no,
  247. expire_date=cert.expire_date,
  248. days_remaining=days_remaining,
  249. status="pending",
  250. create_by=cert.create_by or "system",
  251. create_dept_id=cert.create_dept_id or "",
  252. )
  253. db.add(reminder)
  254. created_count += 1
  255. # 软删/作废/无有效期的证件:关闭仍挂着的 pending
  256. stale_q = db.query(PlDocumentReminder).filter(
  257. PlDocumentReminder.status == "pending",
  258. PlDocumentReminder.del_flag == 0,
  259. PlDocumentReminder.cert_id.isnot(None),
  260. )
  261. if tenant_no:
  262. stale_q = stale_q.filter(PlDocumentReminder.tenant_no == tenant_no)
  263. for reminder in stale_q.all():
  264. if reminder.cert_id in active_cert_ids:
  265. continue
  266. _resolve_pending_reminder(
  267. reminder,
  268. remark="系统自动关闭:关联证件已删除或失效",
  269. )
  270. return created_count