"""样本入库短信通知 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, BizStorageEntrySmsNotification, BizStorageReleaseReview, SysUser, ) _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 _storage_status_label(review: Optional[BizStorageReleaseReview]) -> str: if not review: return "待入库确认" if review.storage_status == "yrk": return "已入库" if review.review_conclusion == "btg": return "拒绝入库" return "待入库确认" def _row_to_dict( *, notification_id: str = "", storage_review_id: str = "", protocol_id: str = "", sms_notified: int = 0, sms_content: str = "", protocol_no: str = "", mother_name: str = "", newborn_name: str = "", sample_code: str = "", lab_storage_code: str = "", sample_type_name: str = "", storage_time: str = "", storage_status_label: str = "", collection_protocol_id: str = "", protocol_status: str = "", ) -> Dict[str, Any]: return { "id": notification_id, "storageReviewId": storage_review_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, "storageTime": storage_time, "storageStatusLabel": storage_status_label, "smsNotified": sms_notified, "smsContent": sms_content, "collectionProtocolId": collection_protocol_id, } def _base_query(db: Session, tenant_no: str): return ( db.query( BizProtocolSample, BizStorageReleaseReview, BizStorageEntrySmsNotification, BizCollectionProtocol, BizProtocolPartyCustomer, ) .outerjoin( BizStorageReleaseReview, and_( _unicode_ci(BizStorageReleaseReview.sample_code) == _unicode_ci(BizProtocolSample.sample_code), BizStorageReleaseReview.tenant_no == tenant_no, BizStorageReleaseReview.reception_source == "cell_storage", BizStorageReleaseReview.del_flag == 0, ), ) .outerjoin( BizStorageEntrySmsNotification, and_( BizStorageEntrySmsNotification.storage_review_id == BizStorageReleaseReview.id, BizStorageEntrySmsNotification.tenant_no == tenant_no, BizStorageEntrySmsNotification.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, review: Optional[BizStorageReleaseReview], tenant_no: str, user_id: Optional[str] = None, ) -> Optional[BizStorageEntrySmsNotification]: if not review: return None existing = ( db.query(BizStorageEntrySmsNotification) .filter( BizStorageEntrySmsNotification.storage_review_id == review.id, BizStorageEntrySmsNotification.tenant_no == tenant_no, BizStorageEntrySmsNotification.del_flag == "0", ) .first() ) if existing: return existing obj = BizStorageEntrySmsNotification( id=generate_uuid(), storage_review_id=review.id, protocol_id=review.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, storage_review_id: Optional[str] = None, user_id: Optional[str] = None, ) -> Optional[BizStorageEntrySmsNotification]: """Find notification by id; fallback to storage_review_id and ensure row exists.""" notif = ( db.query(BizStorageEntrySmsNotification) .filter( BizStorageEntrySmsNotification.id == notification_id, BizStorageEntrySmsNotification.tenant_no == tenant_no, BizStorageEntrySmsNotification.del_flag == "0", ) .first() ) if notif: return notif if not storage_review_id: return None review = ( db.query(BizStorageReleaseReview) .filter( BizStorageReleaseReview.id == storage_review_id, BizStorageReleaseReview.tenant_no == tenant_no, BizStorageReleaseReview.del_flag == 0, ) .first() ) if not review: return None return _ensure_notification(db, review, tenant_no=tenant_no, user_id=user_id) 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, storage_status: Optional[str] = None, sms_notified: Optional[int] = None, storage_time_start: Optional[str] = None, storage_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), ] from app.crud import sample_reception as reception_svc rec_ids = reception_svc.reception_ids_matching_lab_storage_keyword( db, tenant_no, keyword, reception_source="cell_storage" ) if rec_ids: kw_conds.append(BizStorageReleaseReview.reception_id.in_(rec_ids)) 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 storage_status: if storage_status == "yrk": q = q.filter(BizStorageReleaseReview.storage_status == "yrk") elif storage_status == "btg": q = q.filter(BizStorageReleaseReview.review_conclusion == "btg") elif storage_status == "drk": q = q.filter( or_( BizStorageReleaseReview.storage_status == "drk", BizStorageReleaseReview.id.is_(None), ) ) if sms_notified is not None: if sms_notified == 0: q = q.filter( or_( BizStorageEntrySmsNotification.sms_notified == 0, BizStorageEntrySmsNotification.id.is_(None), ) ) else: q = q.filter(BizStorageEntrySmsNotification.sms_notified == sms_notified) if storage_time_start: q = q.filter( BizStorageReleaseReview.storage_time >= _normalize_date_bound(storage_time_start) ) if storage_time_end: q = q.filter( BizStorageReleaseReview.storage_time <= _normalize_date_bound(storage_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) # 末尾加 BizProtocolSample.id 作为确定性排序,避免相同时间的记录顺序不确定 rows = ( q.order_by( BizStorageReleaseReview.storage_time.is_(None), BizStorageReleaseReview.storage_time.desc(), BizProtocolSample.create_time.desc(), BizProtocolSample.id.desc(), ) .offset((page - 1) * page_size) .limit(page_size) .all() ) sample_type_ids = list({ psample.sample_type_id for psample, _, _, _, _ in rows if psample and psample.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, review, notif, proto, party in rows: if notif is None and review is not None: notif = _ensure_notification(db, review, tenant_no=tenant_no) sample_type_name = "" if psample and psample.sample_type_id: sample_type_name = type_map.get(psample.sample_type_id, "") display_time = "" if review: if review.storage_status == "yrk" and review.storage_time: display_time = _fmt_dt(review.storage_time) elif review.review_time: display_time = _fmt_dt(review.review_time) items.append( _row_to_dict( notification_id=(notif.id if notif else ""), storage_review_id=(review.id if review else ""), protocol_id=(notif.protocol_id or "") if notif else "", sms_notified=(notif.sms_notified or 0) if notif else 0, sms_content=(notif.sms_content or "") if notif else "", 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 "", sample_type_name=sample_type_name, storage_time=display_time, storage_status_label=_storage_status_label(review), collection_protocol_id=(proto.id or "") if proto else "", protocol_status=(proto.protocol_status or "") if proto else "", ) ) if items: from app.crud import sample_reception as reception_svc codes = [i["sampleCode"] for i in items if i.get("sampleCode")] code_map = reception_svc.get_lab_storage_code_map_by_sample_codes( db, tenant_no, codes, reception_source="cell_storage" ) for item in items: sc = (item.get("sampleCode") or "").strip() item["labStorageCode"] = code_map.get(sc, "") if sc 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, storage_review_id: Optional[str] = None, ) -> Optional[BizStorageEntrySmsNotification]: notif = resolve_notification( db, notification_id=notification_id, tenant_no=tenant_no, storage_review_id=storage_review_id, user_id=user_id, ) if not notif: return None notif.sms_notified = sms_notified if sms_notified == 1: notif.notified_by = user_id notif.notified_time = datetime.now() _user = db.query(SysUser.nick_name).filter(SysUser.id == user_id).first() notif.notified_by_name = _user.nick_name if _user else None else: notif.notified_by = None notif.notified_by_name = None notif.notified_time = 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, storage_review_id: Optional[str] = None, user_id: Optional[str] = None, ) -> Optional[str]: notif = resolve_notification( db, notification_id=notification_id, tenant_no=tenant_no, storage_review_id=storage_review_id, user_id=user_id, ) if not notif: return None review = ( db.query(BizStorageReleaseReview) .filter( BizStorageReleaseReview.id == notif.storage_review_id, BizStorageReleaseReview.tenant_no == tenant_no, BizStorageReleaseReview.del_flag == 0, ) .first() ) if not review: return None mother_name = "" proto = ( db.query(BizCollectionProtocol) .filter( BizCollectionProtocol.id == review.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 = review.sample_code or "" # 短信模板:区分已入库、拒绝入库、待入库确认 if review.storage_status == "yrk": storage_date = _fmt_dt(review.storage_time)[:10] if review.storage_time else "" content = ( f"尊敬的{mother_name}女士,您的样本(编码:{sample_code})" f"已于{storage_date}完成入库处理。如有疑问请联系客服。" ) elif review.review_conclusion == "btg": content = ( f"尊敬的{mother_name}女士,您的样本(编码:{sample_code})" f"入库审核未通过。如有疑问请联系客服。" ) else: return "暂无短信内容,样本尚未完成入库确认" notif.sms_content = content notif.update_time = datetime.now() db.commit() db.refresh(notif) return content