sample_sms_notification.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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. BizSampleReception,
  15. BizSampleSmsNotification,
  16. )
  17. # biz_sample_sms_notification may use general_ci; align with unicode_ci tables on JOIN
  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 _accept_conclusion_label(v: str) -> str:
  33. return {"js": "接收", "bj": "拒收"}.get(v, "待接收确认")
  34. def _row_to_dict(
  35. *,
  36. notification_id: str = "",
  37. sample_reception_id: str = "",
  38. protocol_id: str = "",
  39. sms_notified: int = 0,
  40. sms_content: str = "",
  41. protocol_no: str = "",
  42. mother_name: str = "",
  43. newborn_name: str = "",
  44. sample_code: str = "",
  45. sample_type_name: str = "",
  46. reception_time: str = "",
  47. accept_conclusion: str = "",
  48. lab_storage_code: str = "",
  49. collection_protocol_id: str = "",
  50. protocol_status: str = "",
  51. ) -> Dict[str, Any]:
  52. return {
  53. "id": notification_id,
  54. "sampleReceptionId": sample_reception_id,
  55. "protocolId": protocol_id,
  56. "protocolNo": protocol_no,
  57. "protocolStatus": protocol_status,
  58. "motherName": mother_name,
  59. "newbornName": newborn_name,
  60. "sampleCode": sample_code,
  61. "labStorageCode": lab_storage_code,
  62. "sampleTypeName": sample_type_name,
  63. "receptionTime": reception_time,
  64. "acceptConclusion": accept_conclusion,
  65. "acceptConclusionLabel": _accept_conclusion_label(accept_conclusion),
  66. "smsNotified": sms_notified,
  67. "smsContent": sms_content,
  68. "collectionProtocolId": collection_protocol_id,
  69. }
  70. def _base_query(db: Session, tenant_no: str):
  71. return (
  72. db.query(
  73. BizProtocolSample,
  74. BizSampleReception,
  75. BizSampleSmsNotification,
  76. BizCollectionProtocol,
  77. BizProtocolPartyCustomer,
  78. )
  79. .outerjoin(
  80. BizSampleReception,
  81. and_(
  82. _unicode_ci(BizSampleReception.sample_code)
  83. == _unicode_ci(BizProtocolSample.sample_code),
  84. BizSampleReception.tenant_no == tenant_no,
  85. BizSampleReception.reception_source == "cell_storage",
  86. BizSampleReception.del_flag == 0,
  87. ),
  88. )
  89. .outerjoin(
  90. BizSampleSmsNotification,
  91. and_(
  92. BizSampleSmsNotification.sample_reception_id == BizSampleReception.id,
  93. BizSampleSmsNotification.tenant_no == tenant_no,
  94. BizSampleSmsNotification.del_flag == "0",
  95. ),
  96. )
  97. .outerjoin(
  98. BizCollectionProtocol,
  99. and_(
  100. BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
  101. BizCollectionProtocol.del_flag == 0,
  102. ),
  103. )
  104. .outerjoin(
  105. BizProtocolPartyCustomer,
  106. and_(
  107. BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
  108. BizProtocolPartyCustomer.del_flag == 0,
  109. ),
  110. )
  111. .filter(
  112. BizProtocolSample.tenant_no == tenant_no,
  113. BizProtocolSample.del_flag == 0,
  114. )
  115. )
  116. def _ensure_notification(
  117. db: Session,
  118. reception: Optional[BizSampleReception],
  119. tenant_no: str,
  120. user_id: Optional[str] = None,
  121. *,
  122. psample: Optional[BizProtocolSample] = None,
  123. ) -> BizSampleSmsNotification:
  124. lookup_id = reception.id if reception else (psample.id if psample else None)
  125. if not lookup_id:
  126. raise ValueError("reception or psample required")
  127. existing = (
  128. db.query(BizSampleSmsNotification)
  129. .filter(
  130. BizSampleSmsNotification.sample_reception_id == lookup_id,
  131. BizSampleSmsNotification.tenant_no == tenant_no,
  132. BizSampleSmsNotification.del_flag == "0",
  133. )
  134. .first()
  135. )
  136. if existing:
  137. return existing
  138. obj = BizSampleSmsNotification(
  139. id=generate_uuid(),
  140. sample_reception_id=lookup_id,
  141. protocol_id=(reception.collection_protocol_id if reception else psample.collection_protocol_id) or "",
  142. sms_notified=0,
  143. tenant_no=tenant_no,
  144. create_by=user_id,
  145. update_by=user_id,
  146. )
  147. db.add(obj)
  148. db.flush()
  149. db.commit()
  150. db.refresh(obj)
  151. return obj
  152. def resolve_notification(
  153. db: Session,
  154. *,
  155. notification_id: str,
  156. tenant_no: str,
  157. sample_reception_id: Optional[str] = None,
  158. user_id: Optional[str] = None,
  159. ) -> Optional[BizSampleSmsNotification]:
  160. notif = (
  161. db.query(BizSampleSmsNotification)
  162. .filter(
  163. BizSampleSmsNotification.id == notification_id,
  164. BizSampleSmsNotification.tenant_no == tenant_no,
  165. BizSampleSmsNotification.del_flag == "0",
  166. )
  167. .first()
  168. )
  169. if notif:
  170. return notif
  171. if not sample_reception_id:
  172. return None
  173. rec = (
  174. db.query(BizSampleReception)
  175. .filter(
  176. BizSampleReception.id == sample_reception_id,
  177. BizSampleReception.tenant_no == tenant_no,
  178. BizSampleReception.del_flag == 0,
  179. )
  180. .first()
  181. )
  182. if rec:
  183. return _ensure_notification(db, rec, tenant_no=tenant_no, user_id=user_id)
  184. psample = (
  185. db.query(BizProtocolSample)
  186. .filter(
  187. BizProtocolSample.id == sample_reception_id,
  188. BizProtocolSample.tenant_no == tenant_no,
  189. BizProtocolSample.del_flag == 0,
  190. )
  191. .first()
  192. )
  193. if psample:
  194. return _ensure_notification(db, None, tenant_no=tenant_no, user_id=user_id, psample=psample)
  195. return None
  196. def _fetch_sample_type_name(db: Session, sample_type_id: Optional[str], tenant_no: str) -> str:
  197. if not sample_type_id:
  198. return ""
  199. t = (
  200. db.query(BizCellSampleType)
  201. .filter(
  202. BizCellSampleType.id == sample_type_id,
  203. BizCellSampleType.del_flag == 0,
  204. )
  205. .first()
  206. )
  207. return (t.type_name or "") if t else ""
  208. def list_notifications(
  209. db: Session,
  210. *,
  211. page: int = 1,
  212. page_size: int = 10,
  213. tenant_no: str,
  214. keyword: Optional[str] = None,
  215. accept_conclusion: Optional[str] = None,
  216. sms_notified: Optional[int] = None,
  217. reception_time_start: Optional[str] = None,
  218. reception_time_end: Optional[str] = None,
  219. user_info=None,
  220. ) -> Tuple[List[Dict[str, Any]], int]:
  221. if not tenant_no:
  222. return [], 0
  223. q = _base_query(db, tenant_no)
  224. if keyword and keyword.strip():
  225. kw = f"%{keyword.strip()}%"
  226. kw_conds = [
  227. BizProtocolPartyCustomer.mother_name.like(kw),
  228. BizProtocolSample.sample_code.like(kw),
  229. BizSampleReception.lab_storage_code.like(kw),
  230. ]
  231. from app.crud import sample_reception as reception_svc
  232. lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(
  233. db, tenant_no, keyword, reception_source="cell_storage"
  234. )
  235. if lab_codes:
  236. kw_conds.append(BizProtocolSample.sample_code.in_(lab_codes))
  237. q = q.filter(or_(*kw_conds))
  238. if accept_conclusion:
  239. if accept_conclusion == "dqr":
  240. q = q.filter(BizSampleReception.id.is_(None))
  241. else:
  242. q = q.filter(BizSampleReception.accept_conclusion == accept_conclusion)
  243. if sms_notified is not None:
  244. if sms_notified == 0:
  245. q = q.filter(
  246. or_(
  247. BizSampleSmsNotification.sms_notified == 0,
  248. BizSampleSmsNotification.id.is_(None),
  249. )
  250. )
  251. else:
  252. q = q.filter(BizSampleSmsNotification.sms_notified == sms_notified)
  253. if reception_time_start:
  254. q = q.filter(
  255. BizSampleReception.reception_time >= _normalize_date_bound(reception_time_start)
  256. )
  257. if reception_time_end:
  258. q = q.filter(
  259. BizSampleReception.reception_time
  260. <= _normalize_date_bound(reception_time_end, end_of_day=True)
  261. )
  262. if user_info:
  263. from app.core.data_scope import apply_data_scope
  264. q = apply_data_scope(q, BizProtocolSample, user_info, db, tenant_no)
  265. total = q.count()
  266. page = max(1, page)
  267. page_size = min(max(1, page_size), 100)
  268. # MySQL 5.7 不支持 NULLS LAST;用 IS NULL 升序把空接收时间排到最后
  269. # 末尾加 BizProtocolSample.id 作为确定性排序,避免相同时间的记录顺序不确定
  270. rows = (
  271. q.order_by(
  272. BizSampleReception.reception_time.is_(None),
  273. BizSampleReception.reception_time.desc(),
  274. BizProtocolSample.create_time.desc(),
  275. BizProtocolSample.id.desc(),
  276. )
  277. .offset((page - 1) * page_size)
  278. .limit(page_size)
  279. .all()
  280. )
  281. sample_type_ids = list({
  282. ps.sample_type_id
  283. for ps, _, _, _, _ in rows
  284. if ps and ps.sample_type_id
  285. })
  286. type_map: Dict[str, str] = {}
  287. if sample_type_ids:
  288. for t in (
  289. db.query(BizCellSampleType)
  290. .filter(BizCellSampleType.id.in_(sample_type_ids), BizCellSampleType.del_flag == 0)
  291. .all()
  292. ):
  293. type_map[t.id] = t.type_name or ""
  294. items: List[Dict[str, Any]] = []
  295. for psample, rec, notif, proto, party in rows:
  296. if notif is None:
  297. notif = _ensure_notification(db, rec, tenant_no=tenant_no, psample=psample)
  298. sample_type_name = ""
  299. if psample and psample.sample_type_id:
  300. sample_type_name = type_map.get(psample.sample_type_id, "")
  301. items.append(
  302. _row_to_dict(
  303. notification_id=notif.id,
  304. sample_reception_id=rec.id if rec else psample.id,
  305. protocol_id=notif.protocol_id or "",
  306. sms_notified=notif.sms_notified or 0,
  307. sms_content=notif.sms_content or "",
  308. protocol_no=(proto.protocol_no or "") if proto else "",
  309. mother_name=(party.mother_name or "") if party else "",
  310. newborn_name=(party.newborn_name or "") if party else "",
  311. sample_code=psample.sample_code or (rec.sample_code if rec else "") or "",
  312. sample_type_name=sample_type_name,
  313. reception_time=_fmt_dt(rec.reception_time) if rec else "",
  314. accept_conclusion=rec.accept_conclusion if rec else "",
  315. lab_storage_code=(rec.lab_storage_code or "") if rec else "",
  316. collection_protocol_id=(proto.id or "") if proto else "",
  317. protocol_status=(proto.protocol_status or "") if proto else "",
  318. )
  319. )
  320. return items, int(total)
  321. def toggle_notification_status(
  322. db: Session,
  323. *,
  324. notification_id: str,
  325. tenant_no: str,
  326. sms_notified: int,
  327. user_id: Optional[str] = None,
  328. sample_reception_id: Optional[str] = None,
  329. ) -> Optional[BizSampleSmsNotification]:
  330. notif = resolve_notification(
  331. db,
  332. notification_id=notification_id,
  333. tenant_no=tenant_no,
  334. sample_reception_id=sample_reception_id,
  335. user_id=user_id,
  336. )
  337. if not notif:
  338. return None
  339. notif.sms_notified = sms_notified
  340. notif.notified_by = user_id if sms_notified == 1 else None
  341. notif.notified_time = datetime.now() if sms_notified == 1 else None
  342. notif.update_by = user_id
  343. notif.update_time = datetime.now()
  344. db.commit()
  345. db.refresh(notif)
  346. return notif
  347. def generate_sms_content(
  348. db: Session,
  349. *,
  350. notification_id: str,
  351. tenant_no: str,
  352. sample_reception_id: Optional[str] = None,
  353. user_id: Optional[str] = None,
  354. ) -> Optional[str]:
  355. notif = resolve_notification(
  356. db,
  357. notification_id=notification_id,
  358. tenant_no=tenant_no,
  359. sample_reception_id=sample_reception_id,
  360. user_id=user_id,
  361. )
  362. if not notif:
  363. return None
  364. rec = (
  365. db.query(BizSampleReception)
  366. .filter(
  367. BizSampleReception.id == notif.sample_reception_id,
  368. BizSampleReception.tenant_no == tenant_no,
  369. BizSampleReception.del_flag == 0,
  370. )
  371. .first()
  372. )
  373. if not rec:
  374. notif.sms_content = "暂无短信内容,样本尚未完成接收确认"
  375. notif.update_time = datetime.now()
  376. db.commit()
  377. db.refresh(notif)
  378. return notif.sms_content
  379. mother_name = ""
  380. proto = (
  381. db.query(BizCollectionProtocol)
  382. .filter(
  383. BizCollectionProtocol.id == rec.collection_protocol_id,
  384. BizCollectionProtocol.del_flag == 0,
  385. )
  386. .first()
  387. )
  388. if proto:
  389. party = (
  390. db.query(BizProtocolPartyCustomer)
  391. .filter(
  392. BizProtocolPartyCustomer.collection_protocol_id == proto.id,
  393. BizProtocolPartyCustomer.del_flag == 0,
  394. )
  395. .first()
  396. )
  397. if party:
  398. mother_name = party.mother_name or ""
  399. sample_code = rec.sample_code or ""
  400. reception_date = _fmt_dt(rec.reception_time)[:10] if rec.reception_time else ""
  401. conclusion_label = _accept_conclusion_label(rec.accept_conclusion or "")
  402. content = (
  403. f"尊敬的{mother_name}女士,您的样本(编码:{sample_code})"
  404. f"已于{reception_date}完成接收处理,结论:{conclusion_label}。"
  405. f"如有疑问请联系客服。"
  406. )
  407. notif.sms_content = content
  408. notif.update_time = datetime.now()
  409. db.commit()
  410. db.refresh(notif)
  411. return content