470 lines
14 KiB
Python
470 lines
14 KiB
Python
"""样本入库短信通知 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 = "",
|
||
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,
|
||
"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()}%"
|
||
q = q.filter(
|
||
or_(
|
||
BizCollectionProtocol.protocol_no.like(kw),
|
||
BizProtocolPartyCustomer.mother_name.like(kw),
|
||
BizProtocolSample.sample_code.like(kw),
|
||
)
|
||
)
|
||
|
||
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 "",
|
||
)
|
||
)
|
||
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
|