storage_entry_sms_notification.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. """样本入库短信通知 CRUD"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import and_, or_
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import generate_uuid
  9. from app.models.models import (
  10. BizCellSampleType,
  11. BizCollectionProtocol,
  12. BizProtocolPartyCustomer,
  13. BizProtocolSample,
  14. BizStorageEntrySmsNotification,
  15. BizStorageReleaseReview,
  16. SysUser,
  17. )
  18. _UNICODE_CI = "utf8mb4_unicode_ci"
  19. def _unicode_ci(col):
  20. return col.collate(_UNICODE_CI)
  21. def total_pages(total: int, page_size: int) -> int:
  22. if total <= 0:
  23. return 0
  24. return int(math.ceil(total / float(page_size)))
  25. def _fmt_dt(t: Optional[datetime]) -> str:
  26. return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
  27. def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
  28. val = (value or "").strip()
  29. if len(val) == 10:
  30. return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
  31. return val
  32. def _storage_status_label(review: Optional[BizStorageReleaseReview]) -> str:
  33. if not review:
  34. return "待入库确认"
  35. if review.storage_status == "yrk":
  36. return "已入库"
  37. if review.review_conclusion == "btg":
  38. return "拒绝入库"
  39. return "待入库确认"
  40. def _row_to_dict(
  41. *,
  42. notification_id: str = "",
  43. storage_review_id: str = "",
  44. protocol_id: str = "",
  45. sms_notified: int = 0,
  46. sms_content: str = "",
  47. protocol_no: str = "",
  48. mother_name: str = "",
  49. newborn_name: str = "",
  50. sample_code: str = "",
  51. lab_storage_code: str = "",
  52. sample_type_name: str = "",
  53. storage_time: str = "",
  54. storage_status_label: str = "",
  55. collection_protocol_id: str = "",
  56. protocol_status: str = "",
  57. ) -> Dict[str, Any]:
  58. return {
  59. "id": notification_id,
  60. "storageReviewId": storage_review_id,
  61. "protocolId": protocol_id,
  62. "protocolNo": protocol_no,
  63. "protocolStatus": protocol_status,
  64. "motherName": mother_name,
  65. "newbornName": newborn_name,
  66. "sampleCode": sample_code,
  67. "labStorageCode": lab_storage_code,
  68. "sampleTypeName": sample_type_name,
  69. "storageTime": storage_time,
  70. "storageStatusLabel": storage_status_label,
  71. "smsNotified": sms_notified,
  72. "smsContent": sms_content,
  73. "collectionProtocolId": collection_protocol_id,
  74. }
  75. def _base_query(db: Session, tenant_no: str):
  76. return (
  77. db.query(
  78. BizProtocolSample,
  79. BizStorageReleaseReview,
  80. BizStorageEntrySmsNotification,
  81. BizCollectionProtocol,
  82. BizProtocolPartyCustomer,
  83. )
  84. .outerjoin(
  85. BizStorageReleaseReview,
  86. and_(
  87. _unicode_ci(BizStorageReleaseReview.sample_code)
  88. == _unicode_ci(BizProtocolSample.sample_code),
  89. BizStorageReleaseReview.tenant_no == tenant_no,
  90. BizStorageReleaseReview.reception_source == "cell_storage",
  91. BizStorageReleaseReview.del_flag == 0,
  92. ),
  93. )
  94. .outerjoin(
  95. BizStorageEntrySmsNotification,
  96. and_(
  97. BizStorageEntrySmsNotification.storage_review_id == BizStorageReleaseReview.id,
  98. BizStorageEntrySmsNotification.tenant_no == tenant_no,
  99. BizStorageEntrySmsNotification.del_flag == "0",
  100. ),
  101. )
  102. .outerjoin(
  103. BizCollectionProtocol,
  104. and_(
  105. BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
  106. BizCollectionProtocol.del_flag == 0,
  107. ),
  108. )
  109. .outerjoin(
  110. BizProtocolPartyCustomer,
  111. and_(
  112. BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
  113. BizProtocolPartyCustomer.del_flag == 0,
  114. ),
  115. )
  116. .filter(
  117. BizProtocolSample.tenant_no == tenant_no,
  118. BizProtocolSample.del_flag == 0,
  119. )
  120. )
  121. def _ensure_notification(
  122. db: Session,
  123. review: Optional[BizStorageReleaseReview],
  124. tenant_no: str,
  125. user_id: Optional[str] = None,
  126. ) -> Optional[BizStorageEntrySmsNotification]:
  127. if not review:
  128. return None
  129. existing = (
  130. db.query(BizStorageEntrySmsNotification)
  131. .filter(
  132. BizStorageEntrySmsNotification.storage_review_id == review.id,
  133. BizStorageEntrySmsNotification.tenant_no == tenant_no,
  134. BizStorageEntrySmsNotification.del_flag == "0",
  135. )
  136. .first()
  137. )
  138. if existing:
  139. return existing
  140. obj = BizStorageEntrySmsNotification(
  141. id=generate_uuid(),
  142. storage_review_id=review.id,
  143. protocol_id=review.protocol_id or "",
  144. sms_notified=0,
  145. tenant_no=tenant_no,
  146. create_by=user_id,
  147. update_by=user_id,
  148. )
  149. db.add(obj)
  150. db.flush()
  151. db.commit()
  152. db.refresh(obj)
  153. return obj
  154. def resolve_notification(
  155. db: Session,
  156. *,
  157. notification_id: str,
  158. tenant_no: str,
  159. storage_review_id: Optional[str] = None,
  160. user_id: Optional[str] = None,
  161. ) -> Optional[BizStorageEntrySmsNotification]:
  162. """Find notification by id; fallback to storage_review_id and ensure row exists."""
  163. notif = (
  164. db.query(BizStorageEntrySmsNotification)
  165. .filter(
  166. BizStorageEntrySmsNotification.id == notification_id,
  167. BizStorageEntrySmsNotification.tenant_no == tenant_no,
  168. BizStorageEntrySmsNotification.del_flag == "0",
  169. )
  170. .first()
  171. )
  172. if notif:
  173. return notif
  174. if not storage_review_id:
  175. return None
  176. review = (
  177. db.query(BizStorageReleaseReview)
  178. .filter(
  179. BizStorageReleaseReview.id == storage_review_id,
  180. BizStorageReleaseReview.tenant_no == tenant_no,
  181. BizStorageReleaseReview.del_flag == 0,
  182. )
  183. .first()
  184. )
  185. if not review:
  186. return None
  187. return _ensure_notification(db, review, tenant_no=tenant_no, user_id=user_id)
  188. def _fetch_sample_type_name(db: Session, sample_type_id: Optional[str], tenant_no: str) -> str:
  189. if not sample_type_id:
  190. return ""
  191. t = (
  192. db.query(BizCellSampleType)
  193. .filter(
  194. BizCellSampleType.id == sample_type_id,
  195. BizCellSampleType.del_flag == 0,
  196. )
  197. .first()
  198. )
  199. return (t.type_name or "") if t else ""
  200. def list_notifications(
  201. db: Session,
  202. *,
  203. page: int = 1,
  204. page_size: int = 10,
  205. tenant_no: str,
  206. keyword: Optional[str] = None,
  207. storage_status: Optional[str] = None,
  208. sms_notified: Optional[int] = None,
  209. storage_time_start: Optional[str] = None,
  210. storage_time_end: Optional[str] = None,
  211. user_info=None,
  212. ) -> Tuple[List[Dict[str, Any]], int]:
  213. if not tenant_no:
  214. return [], 0
  215. q = _base_query(db, tenant_no)
  216. if keyword and keyword.strip():
  217. kw = f"%{keyword.strip()}%"
  218. kw_conds = [
  219. BizProtocolPartyCustomer.mother_name.like(kw),
  220. BizProtocolSample.sample_code.like(kw),
  221. ]
  222. from app.crud import sample_reception as reception_svc
  223. rec_ids = reception_svc.reception_ids_matching_lab_storage_keyword(
  224. db, tenant_no, keyword, reception_source="cell_storage"
  225. )
  226. if rec_ids:
  227. kw_conds.append(BizStorageReleaseReview.reception_id.in_(rec_ids))
  228. lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(
  229. db, tenant_no, keyword, reception_source="cell_storage"
  230. )
  231. if lab_codes:
  232. kw_conds.append(BizProtocolSample.sample_code.in_(lab_codes))
  233. q = q.filter(or_(*kw_conds))
  234. if storage_status:
  235. if storage_status == "yrk":
  236. q = q.filter(BizStorageReleaseReview.storage_status == "yrk")
  237. elif storage_status == "btg":
  238. q = q.filter(BizStorageReleaseReview.review_conclusion == "btg")
  239. elif storage_status == "drk":
  240. q = q.filter(
  241. or_(
  242. BizStorageReleaseReview.storage_status == "drk",
  243. BizStorageReleaseReview.id.is_(None),
  244. )
  245. )
  246. if sms_notified is not None:
  247. if sms_notified == 0:
  248. q = q.filter(
  249. or_(
  250. BizStorageEntrySmsNotification.sms_notified == 0,
  251. BizStorageEntrySmsNotification.id.is_(None),
  252. )
  253. )
  254. else:
  255. q = q.filter(BizStorageEntrySmsNotification.sms_notified == sms_notified)
  256. if storage_time_start:
  257. q = q.filter(
  258. BizStorageReleaseReview.storage_time >= _normalize_date_bound(storage_time_start)
  259. )
  260. if storage_time_end:
  261. q = q.filter(
  262. BizStorageReleaseReview.storage_time
  263. <= _normalize_date_bound(storage_time_end, end_of_day=True)
  264. )
  265. if user_info:
  266. from app.core.data_scope import apply_data_scope
  267. q = apply_data_scope(q, BizProtocolSample, user_info, db, tenant_no)
  268. total = q.count()
  269. page = max(1, page)
  270. page_size = min(max(1, page_size), 100)
  271. # 末尾加 BizProtocolSample.id 作为确定性排序,避免相同时间的记录顺序不确定
  272. rows = (
  273. q.order_by(
  274. BizStorageReleaseReview.storage_time.is_(None),
  275. BizStorageReleaseReview.storage_time.desc(),
  276. BizProtocolSample.create_time.desc(),
  277. BizProtocolSample.id.desc(),
  278. )
  279. .offset((page - 1) * page_size)
  280. .limit(page_size)
  281. .all()
  282. )
  283. sample_type_ids = list({
  284. psample.sample_type_id
  285. for psample, _, _, _, _ in rows
  286. if psample and psample.sample_type_id
  287. })
  288. type_map: Dict[str, str] = {}
  289. if sample_type_ids:
  290. for t in (
  291. db.query(BizCellSampleType)
  292. .filter(BizCellSampleType.id.in_(sample_type_ids), BizCellSampleType.del_flag == 0)
  293. .all()
  294. ):
  295. type_map[t.id] = t.type_name or ""
  296. items: List[Dict[str, Any]] = []
  297. for psample, review, notif, proto, party in rows:
  298. if notif is None and review is not None:
  299. notif = _ensure_notification(db, review, tenant_no=tenant_no)
  300. sample_type_name = ""
  301. if psample and psample.sample_type_id:
  302. sample_type_name = type_map.get(psample.sample_type_id, "")
  303. display_time = ""
  304. if review:
  305. if review.storage_status == "yrk" and review.storage_time:
  306. display_time = _fmt_dt(review.storage_time)
  307. elif review.review_time:
  308. display_time = _fmt_dt(review.review_time)
  309. items.append(
  310. _row_to_dict(
  311. notification_id=(notif.id if notif else ""),
  312. storage_review_id=(review.id if review else ""),
  313. protocol_id=(notif.protocol_id or "") if notif else "",
  314. sms_notified=(notif.sms_notified or 0) if notif else 0,
  315. sms_content=(notif.sms_content or "") if notif else "",
  316. protocol_no=(proto.protocol_no or "") if proto else "",
  317. mother_name=(party.mother_name or "") if party else "",
  318. newborn_name=(party.newborn_name or "") if party else "",
  319. sample_code=psample.sample_code or "",
  320. sample_type_name=sample_type_name,
  321. storage_time=display_time,
  322. storage_status_label=_storage_status_label(review),
  323. collection_protocol_id=(proto.id or "") if proto else "",
  324. protocol_status=(proto.protocol_status or "") if proto else "",
  325. )
  326. )
  327. if items:
  328. from app.crud import sample_reception as reception_svc
  329. codes = [i["sampleCode"] for i in items if i.get("sampleCode")]
  330. code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
  331. db, tenant_no, codes, reception_source="cell_storage"
  332. )
  333. for item in items:
  334. sc = (item.get("sampleCode") or "").strip()
  335. item["labStorageCode"] = code_map.get(sc, "") if sc else ""
  336. return items, int(total)
  337. def toggle_notification_status(
  338. db: Session,
  339. *,
  340. notification_id: str,
  341. tenant_no: str,
  342. sms_notified: int,
  343. user_id: Optional[str] = None,
  344. storage_review_id: Optional[str] = None,
  345. ) -> Optional[BizStorageEntrySmsNotification]:
  346. notif = resolve_notification(
  347. db,
  348. notification_id=notification_id,
  349. tenant_no=tenant_no,
  350. storage_review_id=storage_review_id,
  351. user_id=user_id,
  352. )
  353. if not notif:
  354. return None
  355. notif.sms_notified = sms_notified
  356. if sms_notified == 1:
  357. notif.notified_by = user_id
  358. notif.notified_time = datetime.now()
  359. _user = db.query(SysUser.nick_name).filter(SysUser.id == user_id).first()
  360. notif.notified_by_name = _user.nick_name if _user else None
  361. else:
  362. notif.notified_by = None
  363. notif.notified_by_name = None
  364. notif.notified_time = None
  365. notif.update_by = user_id
  366. notif.update_time = datetime.now()
  367. db.commit()
  368. db.refresh(notif)
  369. return notif
  370. def generate_sms_content(
  371. db: Session,
  372. *,
  373. notification_id: str,
  374. tenant_no: str,
  375. storage_review_id: Optional[str] = None,
  376. user_id: Optional[str] = None,
  377. ) -> Optional[str]:
  378. notif = resolve_notification(
  379. db,
  380. notification_id=notification_id,
  381. tenant_no=tenant_no,
  382. storage_review_id=storage_review_id,
  383. user_id=user_id,
  384. )
  385. if not notif:
  386. return None
  387. review = (
  388. db.query(BizStorageReleaseReview)
  389. .filter(
  390. BizStorageReleaseReview.id == notif.storage_review_id,
  391. BizStorageReleaseReview.tenant_no == tenant_no,
  392. BizStorageReleaseReview.del_flag == 0,
  393. )
  394. .first()
  395. )
  396. if not review:
  397. return None
  398. mother_name = ""
  399. proto = (
  400. db.query(BizCollectionProtocol)
  401. .filter(
  402. BizCollectionProtocol.id == review.protocol_id,
  403. BizCollectionProtocol.del_flag == 0,
  404. )
  405. .first()
  406. )
  407. if proto:
  408. party = (
  409. db.query(BizProtocolPartyCustomer)
  410. .filter(
  411. BizProtocolPartyCustomer.collection_protocol_id == proto.id,
  412. BizProtocolPartyCustomer.del_flag == 0,
  413. )
  414. .first()
  415. )
  416. if party:
  417. mother_name = party.mother_name or ""
  418. sample_code = review.sample_code or ""
  419. # 短信模板:区分已入库、拒绝入库、待入库确认
  420. if review.storage_status == "yrk":
  421. storage_date = _fmt_dt(review.storage_time)[:10] if review.storage_time else ""
  422. content = (
  423. f"尊敬的{mother_name}女士,您的样本(编码:{sample_code})"
  424. f"已于{storage_date}完成入库处理。如有疑问请联系客服。"
  425. )
  426. elif review.review_conclusion == "btg":
  427. content = (
  428. f"尊敬的{mother_name}女士,您的样本(编码:{sample_code})"
  429. f"入库审核未通过。如有疑问请联系客服。"
  430. )
  431. else:
  432. return "暂无短信内容,样本尚未完成入库确认"
  433. notif.sms_content = content
  434. notif.update_time = datetime.now()
  435. db.commit()
  436. db.refresh(notif)
  437. return content