sample_reception_lab_code.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. """实验室入库编码校验与(遗留)RK 自动占号分配。"""
  2. import re
  3. from datetime import datetime
  4. from typing import List, Optional, Tuple
  5. from sqlalchemy import text
  6. from sqlalchemy.orm import Session
  7. from app.core.utils import generate_uuid
  8. from app.models.models import BizSampleReception
  9. PREFIX = "RK"
  10. SEQ_DIGITS = 3
  11. MAX_SEQ = 10**SEQ_DIGITS - 1
  12. LAB_STORAGE_CODE_RE = re.compile(rf"^{PREFIX}(\d{{8}})(\d{{{SEQ_DIGITS}}})$")
  13. def parse_reception_date(reception_date: str) -> str:
  14. s = (reception_date or "").strip()
  15. if len(s) != 10 or s[4] != "-" or s[7] != "-":
  16. raise ValueError("接收日期格式无效,应为 YYYY-MM-DD")
  17. y, m, d = s[:4], s[5:7], s[8:10]
  18. if not (y.isdigit() and m.isdigit() and d.isdigit()):
  19. raise ValueError("接收日期格式无效,应为 YYYY-MM-DD")
  20. return f"{y}{m}{d}"
  21. def format_lab_storage_code(biz_date: str, seq: int) -> str:
  22. return f"{PREFIX}{biz_date}{seq:0{SEQ_DIGITS}d}"
  23. def _max_existing_seq(db: Session, tenant_no: str, biz_date: str) -> int:
  24. prefix_pattern = f"{PREFIX}{biz_date}%"
  25. rows = (
  26. db.query(BizSampleReception.lab_storage_code)
  27. .filter(
  28. BizSampleReception.tenant_no == tenant_no,
  29. BizSampleReception.del_flag == 0,
  30. BizSampleReception.lab_storage_code.isnot(None),
  31. BizSampleReception.lab_storage_code.like(prefix_pattern),
  32. )
  33. .all()
  34. )
  35. max_seq = 0
  36. for row in rows:
  37. code = (row.lab_storage_code or "").strip()
  38. m = LAB_STORAGE_CODE_RE.match(code)
  39. if m and m.group(1) == biz_date:
  40. try:
  41. max_seq = max(max_seq, int(m.group(2)))
  42. except ValueError:
  43. pass
  44. return max_seq
  45. def _sync_sequence_from_existing(
  46. db: Session,
  47. tenant_no: str,
  48. biz_date: str,
  49. user_id: Optional[str] = None,
  50. ) -> None:
  51. max_existing = _max_existing_seq(db, tenant_no, biz_date)
  52. if max_existing < 1:
  53. return
  54. now = datetime.now()
  55. uid = user_id or ""
  56. db.execute(
  57. text(
  58. """
  59. INSERT INTO biz_biz_no_sequence (
  60. id, tenant_no, prefix, biz_date, last_seq,
  61. order_num, remark, create_by, create_dept_id, create_time,
  62. update_by, update_time, del_flag
  63. ) VALUES (
  64. :id, :tenant_no, :prefix, :biz_date, :max_existing,
  65. 0, NULL, :user_id, '', :now,
  66. :user_id, :now, 0
  67. )
  68. ON DUPLICATE KEY UPDATE
  69. last_seq = GREATEST(last_seq, :max_existing),
  70. update_by = :user_id,
  71. update_time = :now
  72. """
  73. ),
  74. {
  75. "id": generate_uuid(),
  76. "tenant_no": tenant_no,
  77. "prefix": PREFIX,
  78. "biz_date": biz_date,
  79. "max_existing": max_existing,
  80. "user_id": uid,
  81. "now": now,
  82. },
  83. )
  84. def _allocate_one_seq(
  85. db: Session,
  86. tenant_no: str,
  87. biz_date: str,
  88. user_id: Optional[str] = None,
  89. ) -> int:
  90. now = datetime.now()
  91. uid = user_id or ""
  92. db.execute(
  93. text(
  94. """
  95. INSERT INTO biz_biz_no_sequence (
  96. id, tenant_no, prefix, biz_date, last_seq,
  97. order_num, remark, create_by, create_dept_id, create_time,
  98. update_by, update_time, del_flag
  99. ) VALUES (
  100. :id, :tenant_no, :prefix, :biz_date, LAST_INSERT_ID(1),
  101. 0, NULL, :user_id, '', :now,
  102. :user_id, :now, 0
  103. )
  104. ON DUPLICATE KEY UPDATE
  105. last_seq = LAST_INSERT_ID(last_seq + 1),
  106. update_by = :user_id,
  107. update_time = :now
  108. """
  109. ),
  110. {
  111. "id": generate_uuid(),
  112. "tenant_no": tenant_no,
  113. "prefix": PREFIX,
  114. "biz_date": biz_date,
  115. "user_id": uid,
  116. "now": now,
  117. },
  118. )
  119. seq = db.execute(text("SELECT LAST_INSERT_ID()")).scalar()
  120. if seq is None or int(seq) < 1:
  121. raise ValueError("实验室入库编码序号分配失败")
  122. seq_int = int(seq)
  123. if seq_int > MAX_SEQ:
  124. raise ValueError(f"当日实验室入库编码序号已用尽(最大 {MAX_SEQ})")
  125. return seq_int
  126. def allocate_lab_storage_codes(
  127. db: Session,
  128. *,
  129. tenant_no: str,
  130. reception_date: str,
  131. count: int = 1,
  132. user_id: Optional[str] = None,
  133. commit: bool = True,
  134. ) -> List[str]:
  135. if count < 1:
  136. return []
  137. count = min(count, 50)
  138. biz_date = parse_reception_date(reception_date)
  139. _sync_sequence_from_existing(db, tenant_no, biz_date, user_id)
  140. codes: List[str] = []
  141. for _ in range(count):
  142. seq = _allocate_one_seq(db, tenant_no, biz_date, user_id)
  143. codes.append(format_lab_storage_code(biz_date, seq))
  144. if commit:
  145. db.commit()
  146. return codes
  147. def validate_lab_storage_code(code: str) -> str:
  148. """校验入库编码:非空、长度不超过 32;不限制具体格式。"""
  149. c = (code or "").strip()
  150. if not c:
  151. raise ValueError("实验室入库编码不能为空")
  152. if len(c) > 32:
  153. raise ValueError("实验室入库编码长度不能超过 32 个字符")
  154. return c
  155. def validate_and_parse_lab_storage_code(
  156. code: str,
  157. ) -> Tuple[str, Optional[str], Optional[int]]:
  158. """兼容旧调用:校验编码;若为 RK+日期+序号 则额外解析供占号逻辑使用。"""
  159. c = validate_lab_storage_code(code)
  160. m = LAB_STORAGE_CODE_RE.match(c)
  161. if not m:
  162. return c, None, None
  163. biz_date, seq_str = m.group(1), m.group(2)
  164. return c, biz_date, int(seq_str)
  165. def assert_lab_storage_code_available(
  166. db: Session,
  167. *,
  168. tenant_no: str,
  169. code: str,
  170. exclude_reception_id: Optional[str] = None,
  171. ) -> None:
  172. q = db.query(BizSampleReception.id).filter(
  173. BizSampleReception.tenant_no == tenant_no,
  174. BizSampleReception.del_flag == 0,
  175. BizSampleReception.lab_storage_code == code,
  176. )
  177. if exclude_reception_id:
  178. q = q.filter(BizSampleReception.id != exclude_reception_id)
  179. if q.first():
  180. raise ValueError("实验室入库编码已存在")
  181. def sync_lab_storage_seq_after_use(
  182. db: Session,
  183. *,
  184. tenant_no: str,
  185. biz_date: str,
  186. seq: int,
  187. user_id: Optional[str] = None,
  188. ) -> None:
  189. if seq < 1:
  190. return
  191. now = datetime.now()
  192. uid = user_id or ""
  193. db.execute(
  194. text(
  195. """
  196. INSERT INTO biz_biz_no_sequence (
  197. id, tenant_no, prefix, biz_date, last_seq,
  198. order_num, remark, create_by, create_dept_id, create_time,
  199. update_by, update_time, del_flag
  200. ) VALUES (
  201. :id, :tenant_no, :prefix, :biz_date, :seq,
  202. 0, NULL, :user_id, '', :now,
  203. :user_id, :now, 0
  204. )
  205. ON DUPLICATE KEY UPDATE
  206. last_seq = GREATEST(last_seq, :seq),
  207. update_by = :user_id,
  208. update_time = :now
  209. """
  210. ),
  211. {
  212. "id": generate_uuid(),
  213. "tenant_no": tenant_no,
  214. "prefix": PREFIX,
  215. "biz_date": biz_date,
  216. "seq": seq,
  217. "user_id": uid,
  218. "now": now,
  219. },
  220. )