Files
zkxw/backend/app/crud/storage_entry_sms_notification.py
T
2026-06-11 15:42:58 +08:00

492 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""样本入库短信通知 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