pl_cert_service.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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 scan_cert_expiry(db: Session, tenant_no: Optional[str] = None) -> int:
  161. """扫描 pl_cert 生成到期提醒,返回新建条数。"""
  162. today = date.today()
  163. query = db.query(PlCert).filter(
  164. PlCert.del_flag == 0,
  165. PlCert.expire_date.isnot(None),
  166. PlCert.status == "valid",
  167. )
  168. if tenant_no:
  169. query = query.filter(PlCert.tenant_no == tenant_no)
  170. created_count = 0
  171. for cert in query.all():
  172. days_remaining = (cert.expire_date - today).days
  173. # 仅跳过尚未进入提醒窗口的证书;已过期(days_remaining < 0)仍需提醒
  174. if days_remaining > cert.remind_days:
  175. continue
  176. owner_label = _resolve_owner_label(
  177. db, cert.tenant_no, cert.owner_type, cert.owner_id
  178. )
  179. driver_id = cert.owner_id if cert.owner_type == "personnel" else None
  180. driver_name = owner_label if cert.owner_type == "personnel" else None
  181. existing = (
  182. db.query(PlDocumentReminder)
  183. .filter(
  184. PlDocumentReminder.cert_id == cert.id,
  185. PlDocumentReminder.status == "pending",
  186. PlDocumentReminder.del_flag == 0,
  187. )
  188. .first()
  189. )
  190. if existing:
  191. existing.owner_type = cert.owner_type
  192. existing.owner_id = cert.owner_id
  193. existing.owner_label = owner_label
  194. existing.driver_id = driver_id
  195. existing.driver_name = driver_name
  196. existing.document_type = cert.cert_name
  197. existing.document_no = cert.cert_no
  198. existing.expire_date = cert.expire_date
  199. existing.days_remaining = days_remaining
  200. continue
  201. reminder = PlDocumentReminder(
  202. id=generate_uuid(),
  203. tenant_no=cert.tenant_no,
  204. cert_id=cert.id,
  205. owner_type=cert.owner_type,
  206. owner_id=cert.owner_id,
  207. owner_label=owner_label,
  208. driver_id=driver_id,
  209. driver_name=driver_name,
  210. document_type=cert.cert_name,
  211. document_no=cert.cert_no,
  212. expire_date=cert.expire_date,
  213. days_remaining=days_remaining,
  214. status="pending",
  215. create_by=cert.create_by or "system",
  216. create_dept_id=cert.create_dept_id or "",
  217. )
  218. db.add(reminder)
  219. created_count += 1
  220. return created_count