research_donation.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. """科研捐赠管理 — 捐赠单 CRUD / 审核 / 状态变更 / 转换 API"""
  2. from typing import Any, Dict, 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.database import get_db
  7. from app.core.menu_guard import require_admin_or_super_admin
  8. from app.core.response import error_response, success_response
  9. from app.crud import research_donation as rd_svc
  10. from app.models.models import SysUser
  11. from app.models.research_donation import BizResearchDonation
  12. router = APIRouter(prefix="/web/admin", tags=["科研捐赠管理"])
  13. def _get_tenant_no(db: Session, user: dict) -> str:
  14. user_id = user.get("user_id")
  15. if not user_id:
  16. raise PermissionError("用户身份信息缺失,无法确定租户")
  17. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  18. if not sys_user or not sys_user.tenant_no:
  19. raise PermissionError("用户租户信息缺失")
  20. return sys_user.tenant_no
  21. def _get_current_user_id(user: dict) -> str:
  22. user_id = user.get("user_id")
  23. if not user_id:
  24. raise PermissionError("用户身份信息缺失")
  25. return str(user_id)
  26. # ---------------------------------------------------------------------------
  27. # Pydantic bodies
  28. # ---------------------------------------------------------------------------
  29. class DonationCreateBody(BaseModel):
  30. model_config = ConfigDict(populate_by_name=True)
  31. donation_no: Optional[str] = Field(None, alias="donationNo")
  32. donation_status: Optional[str] = Field("dsx", alias="donationStatus")
  33. donation_date: Optional[str] = Field(None, alias="donationDate")
  34. source_type: Optional[str] = Field("direct", alias="sourceType")
  35. source_protocol_id: Optional[str] = Field(None, alias="sourceProtocolId")
  36. consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
  37. consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
  38. attachment_urls: Optional[str] = Field(None, alias="attachmentUrls")
  39. other_attachments: Optional[list] = Field(None, alias="otherAttachments")
  40. infectious_disease_report_urls: Optional[str] = Field(
  41. None, alias="infectiousDiseaseReportUrls"
  42. )
  43. infectious_disease_report_attachments: Optional[list] = Field(
  44. None, alias="infectiousDiseaseReportAttachments"
  45. )
  46. remark: Optional[str] = None
  47. party: Optional[dict] = None
  48. party_customer: Optional[dict] = Field(None, alias="partyCustomer")
  49. collection: Optional[dict] = None
  50. collection_info: Optional[dict] = Field(None, alias="collectionInfo")
  51. samples: Optional[list] = None
  52. collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
  53. class DonationUpdateBody(BaseModel):
  54. model_config = ConfigDict(populate_by_name=True)
  55. donation_no: Optional[str] = Field(None, alias="donationNo")
  56. donation_status: Optional[str] = Field(None, alias="donationStatus")
  57. donation_date: Optional[str] = Field(None, alias="donationDate")
  58. source_type: Optional[str] = Field(None, alias="sourceType")
  59. source_protocol_id: Optional[str] = Field(None, alias="sourceProtocolId")
  60. consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
  61. consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
  62. attachment_urls: Optional[str] = Field(None, alias="attachmentUrls")
  63. other_attachments: Optional[list] = Field(None, alias="otherAttachments")
  64. infectious_disease_report_urls: Optional[str] = Field(
  65. None, alias="infectiousDiseaseReportUrls"
  66. )
  67. infectious_disease_report_attachments: Optional[list] = Field(
  68. None, alias="infectiousDiseaseReportAttachments"
  69. )
  70. remark: Optional[str] = None
  71. party: Optional[dict] = None
  72. party_customer: Optional[dict] = Field(None, alias="partyCustomer")
  73. collection: Optional[dict] = None
  74. collection_info: Optional[dict] = Field(None, alias="collectionInfo")
  75. samples: Optional[list] = None
  76. collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
  77. class BatchDeleteBody(BaseModel):
  78. model_config = ConfigDict(populate_by_name=True)
  79. ids: List[str] = Field(..., alias="ids", min_length=1)
  80. class AuditSubmitBody(BaseModel):
  81. model_config = ConfigDict(populate_by_name=True)
  82. audit_status: str = Field(..., alias="auditStatus")
  83. audit_remark: Optional[str] = Field(None, alias="auditRemark")
  84. class StatusChangeBody(BaseModel):
  85. model_config = ConfigDict(populate_by_name=True)
  86. new_status: str = Field(..., alias="newStatus")
  87. desc: Optional[str] = Field(None, alias="desc")
  88. time_val: Optional[str] = Field(None, alias="timeVal")
  89. void_type: Optional[str] = Field(None, alias="voidType")
  90. class ConvertFromStorageBody(BaseModel):
  91. model_config = ConfigDict(populate_by_name=True)
  92. protocol_id: str = Field(..., alias="protocolId")
  93. # ---------------------------------------------------------------------------
  94. # 1. 列表
  95. # ---------------------------------------------------------------------------
  96. @router.get("/research-donations")
  97. async def list_research_donations(
  98. page: int = Query(1, ge=1),
  99. pageSize: int = Query(20, ge=1, le=200, alias="pageSize"),
  100. keyword: Optional[str] = Query(None, alias="keyword"),
  101. donationStatus: Optional[str] = Query(None, alias="donationStatus"),
  102. createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
  103. createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
  104. sampleCollectTimeStart: Optional[str] = Query(None, alias="sampleCollectTimeStart"),
  105. sampleCollectTimeEnd: Optional[str] = Query(None, alias="sampleCollectTimeEnd"),
  106. user: dict = Depends(require_admin_or_super_admin),
  107. db: Session = Depends(get_db),
  108. ):
  109. tenant_no = _get_tenant_no(db, user)
  110. result = rd_svc.list_donations(
  111. db,
  112. tenant_no=tenant_no,
  113. page=page,
  114. page_size=pageSize,
  115. keyword=keyword,
  116. donation_status=donationStatus,
  117. create_time_start=createTimeStart,
  118. create_time_end=createTimeEnd,
  119. sample_collect_time_start=sampleCollectTimeStart,
  120. sample_collect_time_end=sampleCollectTimeEnd,
  121. user_info=user,
  122. )
  123. return success_response(result)
  124. # ---------------------------------------------------------------------------
  125. # 2. 审核列表(须在 {donation_id} 详情路由之前注册,避免 audits 被当作 id)
  126. # ---------------------------------------------------------------------------
  127. @router.get("/research-donations/audits")
  128. async def list_research_donation_audits(
  129. page: int = Query(1, ge=1),
  130. pageSize: int = Query(20, ge=1, le=200, alias="pageSize"),
  131. keyword: Optional[str] = Query(None, alias="keyword"),
  132. auditStatus: Optional[str] = Query(None, alias="auditStatus"),
  133. researchDonationId: Optional[str] = Query(None, alias="researchDonationId"),
  134. user: dict = Depends(require_admin_or_super_admin),
  135. db: Session = Depends(get_db),
  136. ):
  137. tenant_no = _get_tenant_no(db, user)
  138. result = rd_svc.list_audits(
  139. db,
  140. tenant_no=tenant_no,
  141. page=page,
  142. page_size=pageSize,
  143. keyword=keyword,
  144. audit_status=auditStatus,
  145. research_donation_id=researchDonationId,
  146. user_info=user,
  147. )
  148. return success_response(result)
  149. # ---------------------------------------------------------------------------
  150. # 3. 下一捐赠样本编码(须在 {donation_id} 详情路由之前注册)
  151. # ---------------------------------------------------------------------------
  152. @router.get("/research-donations/next-sample-code")
  153. async def next_research_donation_sample_code(
  154. pendingCodes: Optional[str] = Query(None, alias="pendingCodes"),
  155. user: dict = Depends(require_admin_or_super_admin),
  156. db: Session = Depends(get_db),
  157. ):
  158. tenant_no = _get_tenant_no(db, user)
  159. extra = [c.strip() for c in (pendingCodes or "").split(",") if c.strip()]
  160. code = rd_svc.next_donation_sample_code(db, tenant_no=tenant_no, extra_codes=extra)
  161. return success_response({"sampleCode": code})
  162. # ---------------------------------------------------------------------------
  163. # 4. 详情
  164. # ---------------------------------------------------------------------------
  165. @router.get("/research-donations/{donation_id}")
  166. async def get_research_donation_detail(
  167. donation_id: str,
  168. user: dict = Depends(require_admin_or_super_admin),
  169. db: Session = Depends(get_db),
  170. ):
  171. tenant_no = _get_tenant_no(db, user)
  172. result = rd_svc.get_donation_detail(db, donation_id, tenant_no)
  173. if not result:
  174. return error_response(404, "捐赠单不存在或无权查看")
  175. return success_response(result)
  176. # ---------------------------------------------------------------------------
  177. # 4. 创建
  178. # ---------------------------------------------------------------------------
  179. @router.post("/research-donations")
  180. async def create_research_donation(
  181. body: DonationCreateBody,
  182. user: dict = Depends(require_admin_or_super_admin),
  183. db: Session = Depends(get_db),
  184. ):
  185. tenant_no = _get_tenant_no(db, user)
  186. user_id = _get_current_user_id(user)
  187. try:
  188. data = body.model_dump(exclude_none=False, by_alias=True)
  189. collector = (data.get("collectorUserId") or data.get("collector_user_id") or "").strip()
  190. if not collector:
  191. data["collectorUserId"] = user_id
  192. obj = rd_svc.create_donation(
  193. db,
  194. data=data,
  195. tenant_no=tenant_no,
  196. user_id=user_id,
  197. )
  198. return success_response(
  199. rd_svc._donation_to_dict(obj, db, tenant_no),
  200. message="创建成功",
  201. )
  202. except ValueError as e:
  203. return error_response(400, str(e))
  204. # ---------------------------------------------------------------------------
  205. # 5. 更新
  206. # ---------------------------------------------------------------------------
  207. @router.put("/research-donations/{donation_id}")
  208. async def update_research_donation(
  209. donation_id: str,
  210. body: DonationUpdateBody,
  211. user: dict = Depends(require_admin_or_super_admin),
  212. db: Session = Depends(get_db),
  213. ):
  214. tenant_no = _get_tenant_no(db, user)
  215. user_id = _get_current_user_id(user)
  216. try:
  217. data = body.model_dump(exclude_unset=True, by_alias=True)
  218. obj = rd_svc.update_donation(
  219. db,
  220. donation_id,
  221. data=data,
  222. tenant_no=tenant_no,
  223. user_id=user_id,
  224. )
  225. return success_response(
  226. rd_svc._donation_to_dict(obj, db, tenant_no),
  227. message="更新成功",
  228. )
  229. except ValueError as e:
  230. return error_response(400, str(e))
  231. # ---------------------------------------------------------------------------
  232. # 6. 删除
  233. # ---------------------------------------------------------------------------
  234. @router.delete("/research-donations/{donation_id}")
  235. async def delete_research_donation(
  236. donation_id: str,
  237. user: dict = Depends(require_admin_or_super_admin),
  238. db: Session = Depends(get_db),
  239. ):
  240. tenant_no = _get_tenant_no(db, user)
  241. user_id = _get_current_user_id(user)
  242. try:
  243. rd_svc.delete_donation(
  244. db,
  245. donation_id,
  246. tenant_no=tenant_no,
  247. user_id=user_id,
  248. )
  249. return success_response(None, message="删除成功")
  250. except ValueError as e:
  251. return error_response(400, str(e))
  252. # ---------------------------------------------------------------------------
  253. # 7. 批量删除
  254. # ---------------------------------------------------------------------------
  255. @router.post("/research-donations/batch-delete")
  256. async def batch_delete_research_donations(
  257. body: BatchDeleteBody,
  258. user: dict = Depends(require_admin_or_super_admin),
  259. db: Session = Depends(get_db),
  260. ):
  261. tenant_no = _get_tenant_no(db, user)
  262. user_id = _get_current_user_id(user)
  263. try:
  264. count = rd_svc.batch_delete_donations(
  265. db,
  266. ids=body.ids,
  267. tenant_no=tenant_no,
  268. user_id=user_id,
  269. )
  270. return success_response({"count": count}, message=f"已成功删除 {count} 条")
  271. except ValueError as e:
  272. return error_response(400, str(e))
  273. # ---------------------------------------------------------------------------
  274. # 8. 提交审核
  275. # ---------------------------------------------------------------------------
  276. @router.post("/research-donations/{donation_id}/audit")
  277. async def submit_donation_audit(
  278. donation_id: str,
  279. body: AuditSubmitBody,
  280. user: dict = Depends(require_admin_or_super_admin),
  281. db: Session = Depends(get_db),
  282. ):
  283. tenant_no = _get_tenant_no(db, user)
  284. try:
  285. rd_svc.submit_audit(
  286. db,
  287. donation_id,
  288. audit_status=body.audit_status,
  289. audit_remark=body.audit_remark,
  290. user_id=user.get("user_id"),
  291. tenant_no=tenant_no,
  292. )
  293. d = (
  294. db.query(BizResearchDonation)
  295. .filter(BizResearchDonation.id == donation_id)
  296. .first()
  297. )
  298. dn = (d.donation_no if d else "") or ""
  299. return success_response({"donationNo": dn}, message="审核已提交")
  300. except ValueError as e:
  301. return error_response(400, str(e))
  302. # ---------------------------------------------------------------------------
  303. # 9. 状态变更
  304. # ---------------------------------------------------------------------------
  305. @router.post("/research-donations/{donation_id}/status-change")
  306. async def change_donation_status(
  307. donation_id: str,
  308. body: StatusChangeBody,
  309. user: dict = Depends(require_admin_or_super_admin),
  310. db: Session = Depends(get_db),
  311. ):
  312. tenant_no = _get_tenant_no(db, user)
  313. user_id = _get_current_user_id(user)
  314. try:
  315. obj = rd_svc.change_status(
  316. db,
  317. donation_id,
  318. new_status=body.new_status,
  319. desc=body.desc,
  320. time_val=body.time_val,
  321. void_type=body.void_type,
  322. user_id=user_id,
  323. tenant_no=tenant_no,
  324. )
  325. return success_response(
  326. rd_svc._donation_to_dict(obj, db, tenant_no),
  327. message="状态变更成功",
  328. )
  329. except ValueError as e:
  330. return error_response(400, str(e))
  331. # ---------------------------------------------------------------------------
  332. # 10. 细胞存储 → 科研捐赠
  333. # ---------------------------------------------------------------------------
  334. @router.post("/research-donations/convert-from-storage")
  335. async def convert_from_storage(
  336. body: ConvertFromStorageBody,
  337. user: dict = Depends(require_admin_or_super_admin),
  338. db: Session = Depends(get_db),
  339. ):
  340. tenant_no = _get_tenant_no(db, user)
  341. user_id = _get_current_user_id(user)
  342. try:
  343. obj = rd_svc.convert_from_storage(
  344. db,
  345. body.protocol_id,
  346. tenant_no=tenant_no,
  347. user_id=user_id,
  348. )
  349. return success_response(
  350. rd_svc._donation_to_dict(obj, db, tenant_no),
  351. message="转换成功",
  352. )
  353. except ValueError as e:
  354. return error_response(400, str(e))
  355. # ---------------------------------------------------------------------------
  356. # 11. 科研捐赠 → 细胞存储
  357. # ---------------------------------------------------------------------------
  358. @router.post("/research-donations/{donation_id}/convert-to-storage")
  359. async def convert_to_storage(
  360. donation_id: str,
  361. user: dict = Depends(require_admin_or_super_admin),
  362. db: Session = Depends(get_db),
  363. ):
  364. tenant_no = _get_tenant_no(db, user)
  365. user_id = _get_current_user_id(user)
  366. try:
  367. obj = rd_svc.convert_to_storage(
  368. db,
  369. donation_id,
  370. tenant_no=tenant_no,
  371. user_id=user_id,
  372. )
  373. return success_response(
  374. rd_svc._donation_to_dict(obj, db, tenant_no),
  375. message="转换成功",
  376. )
  377. except ValueError as e:
  378. return error_response(400, str(e))