sample_reception.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. """质量检测 — 样本接收 API"""
  2. from typing import List, Optional
  3. from fastapi import APIRouter, Depends, Query
  4. from pydantic import BaseModel, ConfigDict, Field
  5. from sqlalchemy import or_
  6. from sqlalchemy.orm import Session
  7. from app.core.data_scope import apply_data_scope
  8. from app.core.database import get_db
  9. from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
  10. from app.core.response import error_response, success_response
  11. from app.core.utils import parse_datetime
  12. from app.crud import sample_reception as sr_svc
  13. from app.crud import sample_reception_check_item as check_item_svc
  14. from app.crud.collection_protocol import total_pages
  15. from app.models.models import BizSampleReception, BizSampleReceptionCheckItem, SysUser
  16. router = APIRouter(prefix="/web/admin", tags=["质量检测"])
  17. def _get_tenant_no(db: Session, user: dict) -> str:
  18. user_id = user.get("user_id")
  19. if not user_id:
  20. raise PermissionError("用户身份信息缺失,无法确定租户")
  21. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  22. if not sys_user or not sys_user.tenant_no:
  23. raise PermissionError("用户租户信息缺失")
  24. return sys_user.tenant_no
  25. def _get_current_user_id(user: dict) -> str:
  26. user_id = user.get("user_id")
  27. if not user_id:
  28. raise PermissionError("用户身份信息缺失")
  29. return str(user_id)
  30. def _page_payload(items: list, total: int, page: int, page_size: int) -> dict:
  31. return {
  32. "list": items,
  33. "total": total,
  34. "page": page,
  35. "pageSize": page_size,
  36. "totalPages": total_pages(total, page_size),
  37. }
  38. # ---------- 检查项配置 ----------
  39. class CheckItemBody(BaseModel):
  40. model_config = ConfigDict(populate_by_name=True)
  41. item_code: str = Field(..., alias="itemCode")
  42. item_name: str = Field(..., alias="itemName")
  43. status: str = Field("1", alias="status")
  44. order_num: Optional[int] = Field(None, alias="orderNum")
  45. remark: Optional[str] = None
  46. class CheckItemUpdateBody(BaseModel):
  47. model_config = ConfigDict(populate_by_name=True)
  48. item_code: Optional[str] = Field(None, alias="itemCode")
  49. item_name: Optional[str] = Field(None, alias="itemName")
  50. status: Optional[str] = None
  51. order_num: Optional[int] = Field(None, alias="orderNum")
  52. remark: Optional[str] = None
  53. @router.get("/sample-reception-check-items")
  54. async def list_check_items(
  55. page: int = Query(1, ge=1),
  56. pageSize: int = Query(10, ge=1, le=100),
  57. keyword: Optional[str] = None,
  58. status: Optional[str] = None,
  59. db: Session = Depends(get_db),
  60. user: dict = Depends(require_menu_code("biz:sampleReception:list")),
  61. ):
  62. try:
  63. tenant_no = _get_tenant_no(db, user)
  64. query = db.query(BizSampleReceptionCheckItem).filter(
  65. BizSampleReceptionCheckItem.del_flag == 0,
  66. BizSampleReceptionCheckItem.tenant_no == tenant_no,
  67. )
  68. kw = (keyword or "").strip()
  69. if kw:
  70. like = f"%{kw}%"
  71. query = query.filter(
  72. or_(
  73. BizSampleReceptionCheckItem.item_code.like(like),
  74. BizSampleReceptionCheckItem.item_name.like(like),
  75. )
  76. )
  77. if status:
  78. query = query.filter(BizSampleReceptionCheckItem.status == status)
  79. query = apply_data_scope(query, BizSampleReceptionCheckItem, user, db, tenant_no)
  80. total = query.count()
  81. items = query.order_by(BizSampleReceptionCheckItem.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  82. result_list = [check_item_svc._row_to_dict(r) for r in items]
  83. return success_response(_page_payload(result_list, total, page, pageSize))
  84. except PermissionError as e:
  85. return error_response(403, str(e))
  86. except Exception as e:
  87. return error_response(400, str(e))
  88. @router.get("/sample-reception-check-items/active")
  89. async def list_active_check_items(
  90. db: Session = Depends(get_db),
  91. user: dict = Depends(require_menu_code("biz:sampleReception:list")),
  92. ):
  93. try:
  94. tenant_no = _get_tenant_no(db, user)
  95. items = check_item_svc.list_active_check_items(db, tenant_no)
  96. return success_response(items)
  97. except PermissionError as e:
  98. return error_response(403, str(e))
  99. except Exception as e:
  100. return error_response(400, str(e))
  101. @router.post("/sample-reception-check-items")
  102. async def create_check_item(
  103. body: CheckItemBody,
  104. db: Session = Depends(get_db),
  105. user: dict = Depends(require_admin_or_super_admin),
  106. ):
  107. try:
  108. tenant_no = _get_tenant_no(db, user)
  109. user_id = _get_current_user_id(user)
  110. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  111. dept_id = (sys_user.dept_id or "") if sys_user else ""
  112. obj = check_item_svc.create_check_item(
  113. db,
  114. item_code=body.item_code,
  115. item_name=body.item_name,
  116. status=body.status,
  117. tenant_no=tenant_no,
  118. order_num=body.order_num,
  119. remark=body.remark,
  120. user_id=user_id,
  121. create_dept_id=dept_id,
  122. )
  123. return success_response(check_item_svc._row_to_dict(obj))
  124. except PermissionError as e:
  125. return error_response(403, str(e))
  126. except ValueError as e:
  127. return error_response(400, str(e))
  128. except Exception as e:
  129. return error_response(400, str(e))
  130. @router.put("/sample-reception-check-items/{row_id}")
  131. async def update_check_item(
  132. row_id: str,
  133. body: CheckItemUpdateBody,
  134. db: Session = Depends(get_db),
  135. user: dict = Depends(require_admin_or_super_admin),
  136. ):
  137. try:
  138. tenant_no = _get_tenant_no(db, user)
  139. user_id = _get_current_user_id(user)
  140. obj = check_item_svc.update_check_item(
  141. db,
  142. row_id,
  143. item_code=body.item_code,
  144. item_name=body.item_name,
  145. status=body.status,
  146. order_num=body.order_num,
  147. remark=body.remark,
  148. user_id=user_id,
  149. tenant_no=tenant_no,
  150. )
  151. return success_response(check_item_svc._row_to_dict(obj))
  152. except PermissionError as e:
  153. return error_response(403, str(e))
  154. except ValueError as e:
  155. return error_response(400, str(e))
  156. except Exception as e:
  157. return error_response(400, str(e))
  158. @router.delete("/sample-reception-check-items/{row_id}")
  159. async def delete_check_item(
  160. row_id: str,
  161. db: Session = Depends(get_db),
  162. user: dict = Depends(require_admin_or_super_admin),
  163. ):
  164. try:
  165. tenant_no = _get_tenant_no(db, user)
  166. check_item_svc.soft_delete_check_item(db, row_id, tenant_no=tenant_no)
  167. return success_response(None)
  168. except PermissionError as e:
  169. return error_response(403, str(e))
  170. except ValueError as e:
  171. return error_response(400, str(e))
  172. except Exception as e:
  173. return error_response(400, str(e))
  174. # ---------- 样本接收 ----------
  175. @router.get("/sample-receptions/pending")
  176. async def list_pending_samples(
  177. page: int = Query(1, ge=1),
  178. pageSize: int = Query(10, ge=1, le=100),
  179. keyword: Optional[str] = None,
  180. sourceType: Optional[str] = None,
  181. db: Session = Depends(get_db),
  182. user: dict = Depends(require_menu_code("biz:sampleReception:list")),
  183. ):
  184. try:
  185. tenant_no = _get_tenant_no(db, user)
  186. items, total = sr_svc.list_pending_protocol_samples(
  187. db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
  188. source_type=sourceType, user_info=user,
  189. )
  190. return success_response(_page_payload(items, total, page, pageSize))
  191. except PermissionError as e:
  192. return error_response(403, str(e))
  193. except Exception as e:
  194. return error_response(400, str(e))
  195. @router.get("/sample-receptions/received")
  196. async def list_received_samples(
  197. page: int = Query(1, ge=1),
  198. pageSize: int = Query(10, ge=1, le=100),
  199. keyword: Optional[str] = None,
  200. sourceType: Optional[str] = None,
  201. db: Session = Depends(get_db),
  202. user: dict = Depends(require_menu_code("biz:sampleReception:list")),
  203. ):
  204. try:
  205. tenant_no = _get_tenant_no(db, user)
  206. items, total = sr_svc.list_received_protocol_samples(
  207. db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
  208. source_type=sourceType, user_info=user,
  209. )
  210. return success_response(_page_payload(items, total, page, pageSize))
  211. except PermissionError as e:
  212. return error_response(403, str(e))
  213. except Exception as e:
  214. return error_response(400, str(e))
  215. @router.get("/sample-receptions/{reception_id}")
  216. async def get_sample_reception_detail(
  217. reception_id: str,
  218. db: Session = Depends(get_db),
  219. user: dict = Depends(require_menu_code("biz:sampleReception:list")),
  220. ):
  221. try:
  222. tenant_no = _get_tenant_no(db, user)
  223. item = sr_svc.get_reception_by_id(db, reception_id, tenant_no)
  224. if not item:
  225. return error_response(404, "接收记录不存在")
  226. return success_response(item)
  227. except PermissionError as e:
  228. return error_response(403, str(e))
  229. except Exception as e:
  230. return error_response(400, str(e))
  231. class ReceptionCheckSubmit(BaseModel):
  232. model_config = ConfigDict(populate_by_name=True)
  233. check_item_id: str = Field(..., alias="checkItemId")
  234. checked: bool = False
  235. class SampleReceptionUpdateBody(BaseModel):
  236. model_config = ConfigDict(populate_by_name=True)
  237. reception_time: str = Field(..., alias="receptionTime")
  238. handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
  239. handler_name: Optional[str] = Field(None, alias="handlerName")
  240. receive_temperature: Optional[str] = Field(None, alias="receiveTemperature")
  241. volume_ml: Optional[float] = Field(None, alias="volumeMl")
  242. net_weight_g: Optional[float] = Field(None, alias="netWeightG")
  243. checks: List[ReceptionCheckSubmit] = Field(default_factory=list)
  244. accept_conclusion: str = Field(..., alias="acceptConclusion")
  245. receive_attachments: Optional[str] = Field(None, alias="receiveAttachments")
  246. remark: Optional[str] = None
  247. class SampleReceptionCreateBody(BaseModel):
  248. model_config = ConfigDict(populate_by_name=True)
  249. protocol_sample_id: Optional[str] = Field(None, alias="protocolSampleId")
  250. reception_time: str = Field(..., alias="receptionTime")
  251. handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
  252. handler_name: Optional[str] = Field(None, alias="handlerName")
  253. receive_temperature: Optional[str] = Field(None, alias="receiveTemperature")
  254. volume_ml: Optional[float] = Field(None, alias="volumeMl")
  255. net_weight_g: Optional[float] = Field(None, alias="netWeightG")
  256. checks: List[ReceptionCheckSubmit] = Field(default_factory=list)
  257. accept_conclusion: str = Field(..., alias="acceptConclusion")
  258. receive_attachments: Optional[str] = Field(None, alias="receiveAttachments")
  259. remark: Optional[str] = None
  260. source_type: Optional[str] = Field("cell_storage", alias="sourceType")
  261. donation_sample_id: Optional[str] = Field(None, alias="donationSampleId")
  262. @router.post("/sample-receptions")
  263. async def post_sample_reception(
  264. body: SampleReceptionCreateBody,
  265. db: Session = Depends(get_db),
  266. user: dict = Depends(require_admin_or_super_admin),
  267. ):
  268. try:
  269. tenant_no = _get_tenant_no(db, user)
  270. user_id = _get_current_user_id(user)
  271. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  272. dept_id = (sys_user.dept_id or "") if sys_user else ""
  273. rt = parse_datetime(body.reception_time)
  274. if not rt:
  275. return error_response(400, "样本接收时间格式无效")
  276. checks_payload = [
  277. {"checkItemId": c.check_item_id, "checked": c.checked} for c in body.checks
  278. ]
  279. obj = sr_svc.create_sample_reception(
  280. db,
  281. reception_source=body.source_type or "cell_storage",
  282. protocol_sample_id=body.protocol_sample_id if body.source_type != "research_donation" else None,
  283. donation_sample_id=body.donation_sample_id if body.source_type == "research_donation" else None,
  284. tenant_no=tenant_no,
  285. user_id=user_id,
  286. create_dept_id=dept_id,
  287. reception_time=rt,
  288. handler_user_id=body.handler_user_id or user_id,
  289. handler_name=body.handler_name,
  290. receive_temperature=body.receive_temperature,
  291. volume_ml=body.volume_ml,
  292. net_weight_g=body.net_weight_g,
  293. checks=checks_payload,
  294. accept_conclusion=body.accept_conclusion,
  295. receive_attachments=body.receive_attachments,
  296. remark=body.remark,
  297. )
  298. detail = sr_svc.get_reception_by_id(db, obj.id, tenant_no)
  299. return success_response(detail or {"id": obj.id})
  300. except PermissionError as e:
  301. return error_response(403, str(e))
  302. except ValueError as e:
  303. return error_response(400, str(e))
  304. except Exception as e:
  305. return error_response(400, str(e))
  306. @router.put("/sample-receptions/{reception_id}")
  307. async def put_sample_reception(
  308. reception_id: str,
  309. body: SampleReceptionUpdateBody,
  310. db: Session = Depends(get_db),
  311. user: dict = Depends(require_admin_or_super_admin),
  312. ):
  313. try:
  314. tenant_no = _get_tenant_no(db, user)
  315. user_id = _get_current_user_id(user)
  316. rt = parse_datetime(body.reception_time)
  317. if not rt:
  318. return error_response(400, "样本接收时间格式无效")
  319. checks_payload = [
  320. {"checkItemId": c.check_item_id, "checked": c.checked} for c in body.checks
  321. ]
  322. obj = sr_svc.update_sample_reception(
  323. db,
  324. reception_id=reception_id,
  325. tenant_no=tenant_no,
  326. user_id=user_id,
  327. reception_time=rt,
  328. handler_user_id=body.handler_user_id or user_id,
  329. handler_name=body.handler_name,
  330. receive_temperature=body.receive_temperature,
  331. volume_ml=body.volume_ml,
  332. net_weight_g=body.net_weight_g,
  333. checks=checks_payload,
  334. accept_conclusion=body.accept_conclusion,
  335. receive_attachments=body.receive_attachments,
  336. remark=body.remark,
  337. )
  338. detail = sr_svc.get_reception_by_id(db, obj.id, tenant_no)
  339. return success_response(detail or {"id": obj.id})
  340. except PermissionError as e:
  341. return error_response(403, str(e))
  342. except ValueError as e:
  343. return error_response(400, str(e))
  344. except Exception as e:
  345. return error_response(400, str(e))
  346. @router.put("/sample-receptions/{reception_id}")
  347. async def put_sample_reception(
  348. reception_id: str,
  349. body: SampleReceptionUpdateBody,
  350. db: Session = Depends(get_db),
  351. user: dict = Depends(require_admin_or_super_admin),
  352. ):
  353. try:
  354. tenant_no = _get_tenant_no(db, user)
  355. user_id = _get_current_user_id(user)
  356. rt = parse_datetime(body.reception_time)
  357. if not rt:
  358. return error_response(400, "样本接收时间格式无效")
  359. checks_payload = [
  360. {"checkItemId": c.check_item_id, "checked": c.checked} for c in body.checks
  361. ]
  362. obj = sr_svc.update_sample_reception(
  363. db,
  364. reception_id=reception_id,
  365. tenant_no=tenant_no,
  366. user_id=user_id,
  367. reception_time=rt,
  368. handler_user_id=body.handler_user_id or user_id,
  369. handler_name=body.handler_name,
  370. receive_temperature=body.receive_temperature,
  371. volume_ml=body.volume_ml,
  372. net_weight_g=body.net_weight_g,
  373. checks=checks_payload,
  374. accept_conclusion=body.accept_conclusion,
  375. receive_attachments=body.receive_attachments,
  376. remark=body.remark,
  377. )
  378. detail = sr_svc.get_reception_by_id(db, obj.id, tenant_no)
  379. return success_response(detail or {"id": obj.id})
  380. except PermissionError as e:
  381. return error_response(403, str(e))
  382. except ValueError as e:
  383. return error_response(400, str(e))
  384. except Exception as e:
  385. return error_response(400, str(e))