| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- """样本接收短信通知 CRUD"""
- from __future__ import annotations
- import math
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Tuple
- from sqlalchemy import and_, or_
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.models.models import (
- BizCellSampleType,
- BizCollectionProtocol,
- BizProtocolPartyCustomer,
- BizProtocolSample,
- BizSampleReception,
- BizSampleSmsNotification,
- )
- # biz_sample_sms_notification may use general_ci; align with unicode_ci tables on JOIN
- _UNICODE_CI = "utf8mb4_unicode_ci"
- def _unicode_ci(col):
- return col.collate(_UNICODE_CI)
- def total_pages(total: int, page_size: int) -> int:
- if total <= 0:
- return 0
- return int(math.ceil(total / float(page_size)))
- def _fmt_dt(t: Optional[datetime]) -> str:
- return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
- def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
- val = (value or "").strip()
- if len(val) == 10:
- return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
- return val
- def _accept_conclusion_label(v: str) -> str:
- return {"js": "接收", "bj": "拒收"}.get(v, "待接收确认")
- def _row_to_dict(
- *,
- notification_id: str = "",
- sample_reception_id: str = "",
- protocol_id: str = "",
- sms_notified: int = 0,
- sms_content: str = "",
- protocol_no: str = "",
- mother_name: str = "",
- newborn_name: str = "",
- sample_code: str = "",
- sample_type_name: str = "",
- reception_time: str = "",
- accept_conclusion: str = "",
- lab_storage_code: str = "",
- collection_protocol_id: str = "",
- protocol_status: str = "",
- ) -> Dict[str, Any]:
- return {
- "id": notification_id,
- "sampleReceptionId": sample_reception_id,
- "protocolId": protocol_id,
- "protocolNo": protocol_no,
- "protocolStatus": protocol_status,
- "motherName": mother_name,
- "newbornName": newborn_name,
- "sampleCode": sample_code,
- "labStorageCode": lab_storage_code,
- "sampleTypeName": sample_type_name,
- "receptionTime": reception_time,
- "acceptConclusion": accept_conclusion,
- "acceptConclusionLabel": _accept_conclusion_label(accept_conclusion),
- "smsNotified": sms_notified,
- "smsContent": sms_content,
- "collectionProtocolId": collection_protocol_id,
- }
- def _base_query(db: Session, tenant_no: str):
- return (
- db.query(
- BizProtocolSample,
- BizSampleReception,
- BizSampleSmsNotification,
- BizCollectionProtocol,
- BizProtocolPartyCustomer,
- )
- .outerjoin(
- BizSampleReception,
- and_(
- _unicode_ci(BizSampleReception.sample_code)
- == _unicode_ci(BizProtocolSample.sample_code),
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.reception_source == "cell_storage",
- BizSampleReception.del_flag == 0,
- ),
- )
- .outerjoin(
- BizSampleSmsNotification,
- and_(
- BizSampleSmsNotification.sample_reception_id == BizSampleReception.id,
- BizSampleSmsNotification.tenant_no == tenant_no,
- BizSampleSmsNotification.del_flag == "0",
- ),
- )
- .outerjoin(
- BizCollectionProtocol,
- and_(
- BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
- BizCollectionProtocol.del_flag == 0,
- ),
- )
- .outerjoin(
- BizProtocolPartyCustomer,
- and_(
- BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
- BizProtocolPartyCustomer.del_flag == 0,
- ),
- )
- .filter(
- BizProtocolSample.tenant_no == tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- )
- def _ensure_notification(
- db: Session,
- reception: Optional[BizSampleReception],
- tenant_no: str,
- user_id: Optional[str] = None,
- *,
- psample: Optional[BizProtocolSample] = None,
- ) -> BizSampleSmsNotification:
- lookup_id = reception.id if reception else (psample.id if psample else None)
- if not lookup_id:
- raise ValueError("reception or psample required")
- existing = (
- db.query(BizSampleSmsNotification)
- .filter(
- BizSampleSmsNotification.sample_reception_id == lookup_id,
- BizSampleSmsNotification.tenant_no == tenant_no,
- BizSampleSmsNotification.del_flag == "0",
- )
- .first()
- )
- if existing:
- return existing
- obj = BizSampleSmsNotification(
- id=generate_uuid(),
- sample_reception_id=lookup_id,
- protocol_id=(reception.collection_protocol_id if reception else psample.collection_protocol_id) or "",
- sms_notified=0,
- tenant_no=tenant_no,
- create_by=user_id,
- update_by=user_id,
- )
- db.add(obj)
- db.flush()
- db.commit()
- db.refresh(obj)
- return obj
- def resolve_notification(
- db: Session,
- *,
- notification_id: str,
- tenant_no: str,
- sample_reception_id: Optional[str] = None,
- user_id: Optional[str] = None,
- ) -> Optional[BizSampleSmsNotification]:
- notif = (
- db.query(BizSampleSmsNotification)
- .filter(
- BizSampleSmsNotification.id == notification_id,
- BizSampleSmsNotification.tenant_no == tenant_no,
- BizSampleSmsNotification.del_flag == "0",
- )
- .first()
- )
- if notif:
- return notif
- if not sample_reception_id:
- return None
- rec = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.id == sample_reception_id,
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .first()
- )
- if rec:
- return _ensure_notification(db, rec, tenant_no=tenant_no, user_id=user_id)
- psample = (
- db.query(BizProtocolSample)
- .filter(
- BizProtocolSample.id == sample_reception_id,
- BizProtocolSample.tenant_no == tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- .first()
- )
- if psample:
- return _ensure_notification(db, None, tenant_no=tenant_no, user_id=user_id, psample=psample)
- return None
- def _fetch_sample_type_name(db: Session, sample_type_id: Optional[str], tenant_no: str) -> str:
- if not sample_type_id:
- return ""
- t = (
- db.query(BizCellSampleType)
- .filter(
- BizCellSampleType.id == sample_type_id,
- BizCellSampleType.del_flag == 0,
- )
- .first()
- )
- return (t.type_name or "") if t else ""
- def list_notifications(
- db: Session,
- *,
- page: int = 1,
- page_size: int = 10,
- tenant_no: str,
- keyword: Optional[str] = None,
- accept_conclusion: Optional[str] = None,
- sms_notified: Optional[int] = None,
- reception_time_start: Optional[str] = None,
- reception_time_end: Optional[str] = None,
- user_info=None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- if not tenant_no:
- return [], 0
- q = _base_query(db, tenant_no)
- if keyword and keyword.strip():
- kw = f"%{keyword.strip()}%"
- kw_conds = [
- BizProtocolPartyCustomer.mother_name.like(kw),
- BizProtocolSample.sample_code.like(kw),
- BizSampleReception.lab_storage_code.like(kw),
- ]
- from app.crud import sample_reception as reception_svc
- lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(
- db, tenant_no, keyword, reception_source="cell_storage"
- )
- if lab_codes:
- kw_conds.append(BizProtocolSample.sample_code.in_(lab_codes))
- q = q.filter(or_(*kw_conds))
- if accept_conclusion:
- if accept_conclusion == "dqr":
- q = q.filter(BizSampleReception.id.is_(None))
- else:
- q = q.filter(BizSampleReception.accept_conclusion == accept_conclusion)
- if sms_notified is not None:
- if sms_notified == 0:
- q = q.filter(
- or_(
- BizSampleSmsNotification.sms_notified == 0,
- BizSampleSmsNotification.id.is_(None),
- )
- )
- else:
- q = q.filter(BizSampleSmsNotification.sms_notified == sms_notified)
- if reception_time_start:
- q = q.filter(
- BizSampleReception.reception_time >= _normalize_date_bound(reception_time_start)
- )
- if reception_time_end:
- q = q.filter(
- BizSampleReception.reception_time
- <= _normalize_date_bound(reception_time_end, end_of_day=True)
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizProtocolSample, user_info, db, tenant_no)
- total = q.count()
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- # MySQL 5.7 不支持 NULLS LAST;用 IS NULL 升序把空接收时间排到最后
- # 末尾加 BizProtocolSample.id 作为确定性排序,避免相同时间的记录顺序不确定
- rows = (
- q.order_by(
- BizSampleReception.reception_time.is_(None),
- BizSampleReception.reception_time.desc(),
- BizProtocolSample.create_time.desc(),
- BizProtocolSample.id.desc(),
- )
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- sample_type_ids = list({
- ps.sample_type_id
- for ps, _, _, _, _ in rows
- if ps and ps.sample_type_id
- })
- type_map: Dict[str, str] = {}
- if sample_type_ids:
- for t in (
- db.query(BizCellSampleType)
- .filter(BizCellSampleType.id.in_(sample_type_ids), BizCellSampleType.del_flag == 0)
- .all()
- ):
- type_map[t.id] = t.type_name or ""
- items: List[Dict[str, Any]] = []
- for psample, rec, notif, proto, party in rows:
- if notif is None:
- notif = _ensure_notification(db, rec, tenant_no=tenant_no, psample=psample)
- sample_type_name = ""
- if psample and psample.sample_type_id:
- sample_type_name = type_map.get(psample.sample_type_id, "")
- items.append(
- _row_to_dict(
- notification_id=notif.id,
- sample_reception_id=rec.id if rec else psample.id,
- protocol_id=notif.protocol_id or "",
- sms_notified=notif.sms_notified or 0,
- sms_content=notif.sms_content or "",
- protocol_no=(proto.protocol_no or "") if proto else "",
- mother_name=(party.mother_name or "") if party else "",
- newborn_name=(party.newborn_name or "") if party else "",
- sample_code=psample.sample_code or (rec.sample_code if rec else "") or "",
- sample_type_name=sample_type_name,
- reception_time=_fmt_dt(rec.reception_time) if rec else "",
- accept_conclusion=rec.accept_conclusion if rec else "",
- lab_storage_code=(rec.lab_storage_code or "") if rec else "",
- collection_protocol_id=(proto.id or "") if proto else "",
- protocol_status=(proto.protocol_status or "") if proto else "",
- )
- )
- return items, int(total)
- def toggle_notification_status(
- db: Session,
- *,
- notification_id: str,
- tenant_no: str,
- sms_notified: int,
- user_id: Optional[str] = None,
- sample_reception_id: Optional[str] = None,
- ) -> Optional[BizSampleSmsNotification]:
- notif = resolve_notification(
- db,
- notification_id=notification_id,
- tenant_no=tenant_no,
- sample_reception_id=sample_reception_id,
- user_id=user_id,
- )
- if not notif:
- return None
- notif.sms_notified = sms_notified
- notif.notified_by = user_id if sms_notified == 1 else None
- notif.notified_time = datetime.now() if sms_notified == 1 else None
- notif.update_by = user_id
- notif.update_time = datetime.now()
- db.commit()
- db.refresh(notif)
- return notif
- def generate_sms_content(
- db: Session,
- *,
- notification_id: str,
- tenant_no: str,
- sample_reception_id: Optional[str] = None,
- user_id: Optional[str] = None,
- ) -> Optional[str]:
- notif = resolve_notification(
- db,
- notification_id=notification_id,
- tenant_no=tenant_no,
- sample_reception_id=sample_reception_id,
- user_id=user_id,
- )
- if not notif:
- return None
- rec = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.id == notif.sample_reception_id,
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .first()
- )
- if not rec:
- notif.sms_content = "暂无短信内容,样本尚未完成接收确认"
- notif.update_time = datetime.now()
- db.commit()
- db.refresh(notif)
- return notif.sms_content
- mother_name = ""
- proto = (
- db.query(BizCollectionProtocol)
- .filter(
- BizCollectionProtocol.id == rec.collection_protocol_id,
- BizCollectionProtocol.del_flag == 0,
- )
- .first()
- )
- if proto:
- party = (
- db.query(BizProtocolPartyCustomer)
- .filter(
- BizProtocolPartyCustomer.collection_protocol_id == proto.id,
- BizProtocolPartyCustomer.del_flag == 0,
- )
- .first()
- )
- if party:
- mother_name = party.mother_name or ""
- sample_code = rec.sample_code or ""
- reception_date = _fmt_dt(rec.reception_time)[:10] if rec.reception_time else ""
- conclusion_label = _accept_conclusion_label(rec.accept_conclusion or "")
- content = (
- f"尊敬的{mother_name}女士,您的样本(编码:{sample_code})"
- f"已于{reception_date}完成接收处理,结论:{conclusion_label}。"
- f"如有疑问请联系客服。"
- )
- notif.sms_content = content
- notif.update_time = datetime.now()
- db.commit()
- db.refresh(notif)
- return content
|