sample_reception.py 16 KB

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