| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- """运输计划 - 证书 CRUD 与到期扫描"""
- from __future__ import annotations
- from datetime import date
- from typing import Optional
- from fastapi import HTTPException
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.models.pl_models import PlCert, PlDocumentReminder, PlPersonnel, PlVehicle
- from app.schemas.pl.base_data import CertRead, CertRow
- def list_owner_certs(
- db: Session, tenant_no: str, owner_type: str, owner_id: str
- ) -> list[PlCert]:
- return (
- db.query(PlCert)
- .filter(
- PlCert.tenant_no == tenant_no,
- PlCert.owner_type == owner_type,
- PlCert.owner_id == owner_id,
- PlCert.del_flag == 0,
- )
- .order_by(PlCert.order_num.asc(), PlCert.create_time.asc())
- .all()
- )
- def list_owner_certs_batch(
- db: Session, tenant_no: str, owner_type: str, owner_ids: list[str]
- ) -> dict[str, list[PlCert]]:
- if not owner_ids:
- return {}
- rows = (
- db.query(PlCert)
- .filter(
- PlCert.tenant_no == tenant_no,
- PlCert.owner_type == owner_type,
- PlCert.owner_id.in_(owner_ids),
- PlCert.del_flag == 0,
- )
- .order_by(PlCert.order_num.asc(), PlCert.create_time.asc())
- .all()
- )
- grouped: dict[str, list[PlCert]] = {}
- for cert in rows:
- grouped.setdefault(cert.owner_id, []).append(cert)
- return grouped
- def certs_to_read(certs: list[PlCert]) -> list[CertRead]:
- return [
- CertRead(
- id=c.id,
- cert_id=c.id,
- vehicle_part=c.vehicle_part,
- cert_name=c.cert_name,
- cert_no=c.cert_no,
- expire_date=c.expire_date,
- remind_days=c.remind_days,
- attachment_urls=c.attachment_urls,
- owner_type=c.owner_type,
- owner_id=c.owner_id,
- status=c.status,
- tenant_no=c.tenant_no,
- create_time=c.create_time,
- update_time=c.update_time,
- )
- for c in certs
- ]
- def _normalize_cert_row(row: CertRow | dict) -> CertRow:
- if isinstance(row, CertRow):
- return row
- return CertRow.model_validate(row)
- def save_owner_certs(
- db: Session,
- owner_type: str,
- owner_id: str,
- cert_rows: list[CertRow | dict],
- tenant_no: str,
- *,
- user_id: str,
- dept_id: str,
- ) -> None:
- """按 ID 差量保存;旧请求仅复用唯一业务键,遗漏旧证软删。"""
- existing = db.query(PlCert).filter(
- PlCert.tenant_no == tenant_no,
- PlCert.owner_type == owner_type,
- PlCert.owner_id == owner_id,
- PlCert.del_flag == 0,
- ).populate_existing().with_for_update().all()
- existing_map = {c.id: c for c in existing}
- rows = [_normalize_cert_row(row) for row in cert_rows]
- explicit_ids: set[str] = set()
- for row in rows:
- cert_id = (row.cert_id or "").strip()
- if cert_id:
- if cert_id in explicit_ids or cert_id not in existing_map:
- raise HTTPException(400, "证书ID重复或不属于当前主体")
- explicit_ids.add(cert_id)
- def business_key(row):
- return tuple((getattr(row, key) or "").strip() for key in (
- "vehicle_part", "cert_name", "cert_no"
- ))
- by_key = {}
- for cert in existing:
- if cert.id not in explicit_ids:
- by_key.setdefault(business_key(cert), []).append(cert)
- prepared = []
- legacy_keys = set()
- for row in rows:
- cert_id = (row.cert_id or "").strip()
- if cert_id:
- cert = existing_map[cert_id]
- else:
- key = business_key(row)
- matches = by_key.get(key, [])
- if key in legacy_keys or len(matches) > 1:
- raise HTTPException(400, "证书业务键重复或不唯一,请刷新后使用证书ID保存")
- legacy_keys.add(key)
- cert = matches[0] if matches else None
- prepared.append((row, cert))
- keep_ids: set[str] = set()
- for idx, (row, cert) in enumerate(prepared):
- if cert is not None:
- for field in (
- "vehicle_part", "cert_name", "cert_no", "expire_date",
- "remind_days", "attachment_urls",
- ):
- # A submitted certificate is a full form row; UI-cleared optional
- # fields are omitted by JSON and use the schema's empty defaults.
- setattr(cert, field, getattr(row, field))
- cert.order_num = idx
- cert.update_by = user_id
- keep_ids.add(cert.id)
- else:
- cert = PlCert(
- id=generate_uuid(),
- tenant_no=tenant_no,
- owner_type=owner_type,
- owner_id=owner_id,
- vehicle_part=row.vehicle_part,
- cert_name=row.cert_name,
- cert_no=row.cert_no,
- expire_date=row.expire_date,
- remind_days=row.remind_days,
- attachment_urls=row.attachment_urls,
- status="valid",
- order_num=idx,
- create_by=user_id,
- create_dept_id=dept_id,
- )
- db.add(cert)
- db.flush()
- keep_ids.add(cert.id)
- for cert in existing:
- if cert.id not in keep_ids:
- cert.del_flag = 1
- cert.update_by = user_id
- scan_cert_expiry(db, tenant_no)
- def nearest_expire_date(
- db: Session, tenant_no: str, owner_type: str, owner_id: str
- ) -> Optional[date]:
- row = (
- db.query(PlCert.expire_date)
- .filter(
- PlCert.tenant_no == tenant_no,
- PlCert.owner_type == owner_type,
- PlCert.owner_id == owner_id,
- PlCert.del_flag == 0,
- PlCert.expire_date.isnot(None),
- )
- .order_by(PlCert.expire_date.asc())
- .first()
- )
- return row[0] if row else None
- def _resolve_owner_label(
- db: Session, tenant_no: str, owner_type: str, owner_id: str
- ) -> str:
- if owner_type == "vehicle":
- vehicle = (
- db.query(PlVehicle)
- .filter(
- PlVehicle.id == owner_id,
- PlVehicle.tenant_no == tenant_no,
- PlVehicle.del_flag == 0,
- )
- .first()
- )
- return vehicle.license_plate if vehicle else owner_id
- personnel = (
- db.query(PlPersonnel)
- .filter(
- PlPersonnel.id == owner_id,
- PlPersonnel.tenant_no == tenant_no,
- PlPersonnel.del_flag == 0,
- )
- .first()
- )
- return personnel.personnel_name if personnel else owner_id
- def _pending_reminder_for_cert(
- db: Session, cert_id: str
- ) -> Optional[PlDocumentReminder]:
- return (
- db.query(PlDocumentReminder)
- .filter(
- PlDocumentReminder.cert_id == cert_id,
- PlDocumentReminder.status == "pending",
- PlDocumentReminder.del_flag == 0,
- )
- .first()
- )
- def _resolve_pending_reminder(
- reminder: PlDocumentReminder,
- *,
- remark: str,
- expire_date: Optional[date] = None,
- days_remaining: Optional[int] = None,
- document_type: Optional[str] = None,
- document_no: Optional[str] = None,
- ) -> None:
- """关闭待处理提醒:证件已处理/退出窗口后不再继续提醒。"""
- reminder.status = "resolved"
- reminder.remark = remark
- if expire_date is not None:
- reminder.expire_date = expire_date
- if days_remaining is not None:
- reminder.days_remaining = days_remaining
- if document_type is not None:
- reminder.document_type = document_type
- if document_no is not None:
- reminder.document_no = document_no
- def scan_cert_expiry(db: Session, tenant_no: Optional[str] = None, *, today: Optional[date] = None) -> int:
- """扫描 pl_cert 生成到期提醒;退出提醒窗口或证件失效时关闭旧 pending。"""
- today = today or date.today()
- query = db.query(PlCert).filter(
- PlCert.del_flag == 0,
- PlCert.expire_date.isnot(None),
- PlCert.status == "valid",
- )
- if tenant_no:
- query = query.filter(PlCert.tenant_no == tenant_no)
- active_cert_ids: set[str] = set()
- created_count = 0
- for cert in query.all():
- active_cert_ids.add(cert.id)
- days_remaining = (cert.expire_date - today).days
- existing = _pending_reminder_for_cert(db, cert.id)
- # 尚未进入提醒窗口:关闭因旧有效期留下的 pending,等真正临近再提醒
- if days_remaining > cert.remind_days:
- if existing:
- _resolve_pending_reminder(
- existing,
- remark="系统自动关闭:证件有效期已更新,已退出提醒窗口",
- expire_date=cert.expire_date,
- days_remaining=days_remaining,
- document_type=cert.cert_name,
- document_no=cert.cert_no,
- )
- continue
- owner_label = _resolve_owner_label(
- db, cert.tenant_no, cert.owner_type, cert.owner_id
- )
- driver_id = cert.owner_id if cert.owner_type == "personnel" else None
- driver_name = owner_label if cert.owner_type == "personnel" else None
- if existing:
- existing.owner_type = cert.owner_type
- existing.owner_id = cert.owner_id
- existing.owner_label = owner_label
- existing.driver_id = driver_id
- existing.driver_name = driver_name
- existing.document_type = cert.cert_name
- existing.document_no = cert.cert_no
- existing.expire_date = cert.expire_date
- existing.days_remaining = days_remaining
- continue
- reminder = PlDocumentReminder(
- id=generate_uuid(),
- tenant_no=cert.tenant_no,
- cert_id=cert.id,
- owner_type=cert.owner_type,
- owner_id=cert.owner_id,
- owner_label=owner_label,
- driver_id=driver_id,
- driver_name=driver_name,
- document_type=cert.cert_name,
- document_no=cert.cert_no,
- expire_date=cert.expire_date,
- days_remaining=days_remaining,
- status="pending",
- create_by=cert.create_by or "system",
- create_dept_id=cert.create_dept_id or "",
- )
- db.add(reminder)
- created_count += 1
- # 软删/作废/无有效期的证件:关闭仍挂着的 pending
- stale_q = db.query(PlDocumentReminder).filter(
- PlDocumentReminder.status == "pending",
- PlDocumentReminder.del_flag == 0,
- PlDocumentReminder.cert_id.isnot(None),
- )
- if tenant_no:
- stale_q = stale_q.filter(PlDocumentReminder.tenant_no == tenant_no)
- for reminder in stale_q.all():
- if reminder.cert_id in active_cert_ids:
- continue
- _resolve_pending_reminder(
- reminder,
- remark="系统自动关闭:关联证件已删除或失效",
- )
- return created_count
|