basic_info.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. """基础信息管理 API(客户、样本类型、采集地区/医院、协议号池、样本编码池)"""
  2. from typing import Any, 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.crud import cell_sample_type as cell_type_svc
  12. from app.crud import collection_region as region_svc
  13. from app.crud import collection_protocol as collection_protocol_svc
  14. from app.crud import collection_protocol_no as protocol_no_svc
  15. from app.crud import customer as customer_svc
  16. from app.crud import sample_code as sample_code_svc
  17. from app.models.models import (
  18. BizCellSampleType,
  19. BizCollectionProtocolNo,
  20. BizCollectionRegion,
  21. BizCustomer,
  22. BizSampleCode,
  23. SysUser,
  24. )
  25. router = APIRouter(prefix="/web/admin", tags=["基础信息管理"])
  26. def _get_tenant_no(db: Session, user: dict) -> str:
  27. user_id = user.get("user_id")
  28. if not user_id:
  29. raise PermissionError("用户身份信息缺失,无法确定租户")
  30. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  31. if not sys_user or not sys_user.tenant_no:
  32. raise PermissionError("用户租户信息缺失")
  33. return sys_user.tenant_no
  34. # ---------------------------------------------------------------------------
  35. # 客户
  36. # ---------------------------------------------------------------------------
  37. class CustomerCreateBody(BaseModel):
  38. model_config = ConfigDict(populate_by_name=True)
  39. customer_name: str = Field(..., alias="customerName")
  40. sex: Optional[str] = Field("2", alias="sex")
  41. id_card_no: Optional[str] = Field(None, alias="idCardNo")
  42. age: Optional[int] = None
  43. phone_no: Optional[str] = Field(None, alias="phoneNo")
  44. customer_tags: Optional[str] = Field(None, alias="customerTags")
  45. order_num: Optional[int] = Field(0, alias="orderNum")
  46. remark: Optional[str] = None
  47. class CustomerUpdateBody(BaseModel):
  48. model_config = ConfigDict(populate_by_name=True)
  49. customer_name: Optional[str] = Field(None, alias="customerName")
  50. sex: Optional[str] = None
  51. id_card_no: Optional[str] = Field(None, alias="idCardNo")
  52. age: Optional[int] = None
  53. phone_no: Optional[str] = Field(None, alias="phoneNo")
  54. customer_tags: Optional[str] = Field(None, alias="customerTags")
  55. order_num: Optional[int] = Field(None, alias="orderNum")
  56. remark: Optional[str] = None
  57. def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
  58. val = (value or "").strip()
  59. if len(val) == 10:
  60. return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
  61. return val
  62. @router.get("/customers")
  63. async def list_customers(
  64. page: int = Query(1, ge=1),
  65. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  66. keyword: Optional[str] = None,
  67. createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
  68. createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
  69. user: dict = Depends(require_menu_code("biz:customer:list")),
  70. db: Session = Depends(get_db),
  71. ):
  72. tenant_no = _get_tenant_no(db, user)
  73. query = db.query(BizCustomer).filter(
  74. BizCustomer.del_flag == 0,
  75. BizCustomer.tenant_no == tenant_no,
  76. )
  77. if keyword:
  78. kw = keyword.strip()
  79. if kw:
  80. like = f"%{kw}%"
  81. query = query.filter(
  82. or_(
  83. BizCustomer.customer_name.like(like),
  84. BizCustomer.phone_no.like(like),
  85. BizCustomer.id_card_no.like(like),
  86. BizCustomer.customer_tags.like(like),
  87. )
  88. )
  89. if createTimeStart:
  90. query = query.filter(
  91. BizCustomer.create_time >= _normalize_date_bound(createTimeStart)
  92. )
  93. if createTimeEnd:
  94. query = query.filter(
  95. BizCustomer.create_time <= _normalize_date_bound(createTimeEnd, end_of_day=True)
  96. )
  97. query = apply_data_scope(query, BizCustomer, user, db, tenant_no)
  98. total = query.count()
  99. items = query.order_by(BizCustomer.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  100. result_list = [customer_svc._row_to_dict(r) for r in items]
  101. tp = customer_svc.total_pages(total, pageSize)
  102. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  103. @router.post("/customers")
  104. async def create_customer(
  105. body: CustomerCreateBody,
  106. user: dict = Depends(require_admin_or_super_admin),
  107. db: Session = Depends(get_db),
  108. ):
  109. try:
  110. tenant_no = _get_tenant_no(db, user)
  111. obj = customer_svc.create_customer(
  112. db,
  113. customer_name=body.customer_name,
  114. sex=body.sex or "2",
  115. id_card_no=body.id_card_no,
  116. age=body.age,
  117. phone_no=body.phone_no,
  118. customer_tags=body.customer_tags,
  119. tenant_no=tenant_no,
  120. order_num=body.order_num,
  121. remark=body.remark,
  122. user_id=user.get("user_id"),
  123. )
  124. return success_response(customer_svc._row_to_dict(obj), message="创建成功")
  125. except ValueError as e:
  126. return error_response(400, str(e))
  127. @router.put("/customers/{customer_id}")
  128. async def update_customer(
  129. customer_id: str,
  130. body: CustomerUpdateBody,
  131. user: dict = Depends(require_admin_or_super_admin),
  132. db: Session = Depends(get_db),
  133. ):
  134. try:
  135. tenant_no = _get_tenant_no(db, user)
  136. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  137. obj = customer_svc.update_customer(
  138. db,
  139. customer_id,
  140. customer_name=payload.get("customer_name"),
  141. sex=payload.get("sex"),
  142. id_card_no=payload.get("id_card_no"),
  143. age=payload.get("age"),
  144. phone_no=payload.get("phone_no"),
  145. customer_tags=payload.get("customer_tags"),
  146. order_num=payload.get("order_num"),
  147. remark=payload.get("remark"),
  148. user_id=user.get("user_id"),
  149. tenant_no=tenant_no,
  150. )
  151. return success_response(customer_svc._row_to_dict(obj), message="更新成功")
  152. except ValueError as e:
  153. return error_response(400, str(e))
  154. @router.delete("/customers/{customer_id}")
  155. async def delete_customer(
  156. customer_id: str,
  157. user: dict = Depends(require_admin_or_super_admin),
  158. db: Session = Depends(get_db),
  159. ):
  160. try:
  161. tenant_no = _get_tenant_no(db, user)
  162. customer_svc.soft_delete_customer(db, customer_id, tenant_no=tenant_no)
  163. return success_response(None, message="删除成功")
  164. except ValueError as e:
  165. return error_response(400, str(e))
  166. # ---------------------------------------------------------------------------
  167. # 细胞样本类型
  168. # ---------------------------------------------------------------------------
  169. class CellSampleTypeCreateBody(BaseModel):
  170. model_config = ConfigDict(populate_by_name=True)
  171. type_code: str = Field(..., alias="typeCode")
  172. type_name: str = Field(..., alias="typeName")
  173. storage_years: Optional[int] = Field(None, alias="storageYears")
  174. storage_price: Optional[float] = Field(None, alias="storagePrice")
  175. order_num: Optional[int] = Field(0, alias="orderNum")
  176. remark: Optional[str] = None
  177. class CellSampleTypeUpdateBody(BaseModel):
  178. model_config = ConfigDict(populate_by_name=True)
  179. type_code: Optional[str] = Field(None, alias="typeCode")
  180. type_name: Optional[str] = Field(None, alias="typeName")
  181. storage_years: Optional[int] = Field(None, alias="storageYears")
  182. storage_price: Optional[float] = Field(None, alias="storagePrice")
  183. order_num: Optional[int] = Field(None, alias="orderNum")
  184. remark: Optional[str] = None
  185. @router.get("/cell-sample-types")
  186. async def list_cell_sample_types(
  187. page: int = Query(1, ge=1),
  188. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  189. keyword: Optional[str] = None,
  190. user: dict = Depends(require_menu_code("biz:cellSampleType:list")),
  191. db: Session = Depends(get_db),
  192. ):
  193. tenant_no = _get_tenant_no(db, user)
  194. query = db.query(BizCellSampleType).filter(
  195. BizCellSampleType.del_flag == 0,
  196. BizCellSampleType.tenant_no == tenant_no,
  197. )
  198. if keyword:
  199. kw = keyword.strip()
  200. if kw:
  201. like = f"%{kw}%"
  202. query = query.filter(
  203. or_(
  204. BizCellSampleType.type_code.like(like),
  205. BizCellSampleType.type_name.like(like),
  206. )
  207. )
  208. query = apply_data_scope(query, BizCellSampleType, user, db, tenant_no)
  209. total = query.count()
  210. items = query.order_by(BizCellSampleType.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  211. result_list = [cell_type_svc._row_to_dict(r) for r in items]
  212. tp = cell_type_svc.total_pages(total, pageSize)
  213. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  214. @router.post("/cell-sample-types")
  215. async def create_cell_sample_type(
  216. body: CellSampleTypeCreateBody,
  217. user: dict = Depends(require_admin_or_super_admin),
  218. db: Session = Depends(get_db),
  219. ):
  220. try:
  221. tenant_no = _get_tenant_no(db, user)
  222. obj = cell_type_svc.create_cell_sample_type(
  223. db,
  224. type_code=body.type_code,
  225. type_name=body.type_name,
  226. storage_years=body.storage_years,
  227. storage_price=body.storage_price,
  228. tenant_no=tenant_no,
  229. order_num=body.order_num,
  230. remark=body.remark,
  231. user_id=user.get("user_id"),
  232. )
  233. return success_response(cell_type_svc._row_to_dict(obj), message="创建成功")
  234. except ValueError as e:
  235. return error_response(400, str(e))
  236. @router.put("/cell-sample-types/{row_id}")
  237. async def update_cell_sample_type(
  238. row_id: str,
  239. body: CellSampleTypeUpdateBody,
  240. user: dict = Depends(require_admin_or_super_admin),
  241. db: Session = Depends(get_db),
  242. ):
  243. try:
  244. tenant_no = _get_tenant_no(db, user)
  245. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  246. obj = cell_type_svc.update_cell_sample_type(
  247. db,
  248. row_id,
  249. type_code=payload.get("type_code"),
  250. type_name=payload.get("type_name"),
  251. storage_years=payload.get("storage_years"),
  252. storage_price=payload.get("storage_price"),
  253. order_num=payload.get("order_num"),
  254. remark=payload.get("remark"),
  255. user_id=user.get("user_id"),
  256. tenant_no=tenant_no,
  257. )
  258. return success_response(cell_type_svc._row_to_dict(obj), message="更新成功")
  259. except ValueError as e:
  260. return error_response(400, str(e))
  261. @router.delete("/cell-sample-types/{row_id}")
  262. async def delete_cell_sample_type(
  263. row_id: str,
  264. user: dict = Depends(require_admin_or_super_admin),
  265. db: Session = Depends(get_db),
  266. ):
  267. try:
  268. tenant_no = _get_tenant_no(db, user)
  269. cell_type_svc.soft_delete_cell_sample_type(db, row_id, tenant_no=tenant_no)
  270. return success_response(None, message="删除成功")
  271. except ValueError as e:
  272. return error_response(400, str(e))
  273. # ---------------------------------------------------------------------------
  274. # 采集地区
  275. # ---------------------------------------------------------------------------
  276. class CollectionRegionCreateBody(BaseModel):
  277. model_config = ConfigDict(populate_by_name=True)
  278. region_name: str = Field(..., alias="regionName")
  279. order_num: Optional[int] = Field(0, alias="orderNum")
  280. remark: Optional[str] = None
  281. class CollectionRegionUpdateBody(BaseModel):
  282. model_config = ConfigDict(populate_by_name=True)
  283. region_name: Optional[str] = Field(None, alias="regionName")
  284. order_num: Optional[int] = Field(None, alias="orderNum")
  285. remark: Optional[str] = None
  286. @router.get("/collection-regions")
  287. async def list_collection_regions(
  288. page: int = Query(1, ge=1),
  289. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  290. keyword: Optional[str] = None,
  291. user: dict = Depends(require_menu_code("biz:collectionRegion:list")),
  292. db: Session = Depends(get_db),
  293. ):
  294. tenant_no = _get_tenant_no(db, user)
  295. query = db.query(BizCollectionRegion).filter(
  296. BizCollectionRegion.del_flag == 0,
  297. BizCollectionRegion.tenant_no == tenant_no,
  298. )
  299. if keyword:
  300. query = query.filter(BizCollectionRegion.region_name.like(f"%{keyword}%"))
  301. query = apply_data_scope(query, BizCollectionRegion, user, db, tenant_no)
  302. total = query.count()
  303. items = query.order_by(BizCollectionRegion.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  304. region_ids = [r.id for r in items]
  305. hospital_map = region_svc._hospital_names_by_region_ids(db, region_ids)
  306. result_list = [
  307. region_svc._region_to_dict(r, hospital_names=hospital_map.get(r.id, [])) for r in items
  308. ]
  309. tp = region_svc.total_pages(total, pageSize)
  310. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  311. @router.post("/collection-regions")
  312. async def create_collection_region(
  313. body: CollectionRegionCreateBody,
  314. user: dict = Depends(require_admin_or_super_admin),
  315. db: Session = Depends(get_db),
  316. ):
  317. try:
  318. tenant_no = _get_tenant_no(db, user)
  319. obj = region_svc.create_collection_region(
  320. db,
  321. region_name=body.region_name,
  322. tenant_no=tenant_no,
  323. order_num=body.order_num,
  324. remark=body.remark,
  325. user_id=user.get("user_id"),
  326. )
  327. return success_response(region_svc._region_to_dict(obj), message="创建成功")
  328. except ValueError as e:
  329. return error_response(400, str(e))
  330. @router.put("/collection-regions/{region_id}")
  331. async def update_collection_region(
  332. region_id: str,
  333. body: CollectionRegionUpdateBody,
  334. user: dict = Depends(require_admin_or_super_admin),
  335. db: Session = Depends(get_db),
  336. ):
  337. try:
  338. tenant_no = _get_tenant_no(db, user)
  339. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  340. obj = region_svc.update_collection_region(
  341. db,
  342. region_id,
  343. region_name=payload.get("region_name"),
  344. order_num=payload.get("order_num"),
  345. remark=payload.get("remark"),
  346. user_id=user.get("user_id"),
  347. tenant_no=tenant_no,
  348. )
  349. return success_response(region_svc._region_to_dict(obj), message="更新成功")
  350. except ValueError as e:
  351. return error_response(400, str(e))
  352. @router.delete("/collection-regions/{region_id}")
  353. async def delete_collection_region(
  354. region_id: str,
  355. user: dict = Depends(require_admin_or_super_admin),
  356. db: Session = Depends(get_db),
  357. ):
  358. try:
  359. tenant_no = _get_tenant_no(db, user)
  360. region_svc.soft_delete_collection_region(db, region_id, tenant_no=tenant_no)
  361. return success_response(None, message="删除成功")
  362. except ValueError as e:
  363. return error_response(400, str(e))
  364. # ---------------------------------------------------------------------------
  365. # 采集地区 — 下属医院
  366. # ---------------------------------------------------------------------------
  367. class HospitalCreateBody(BaseModel):
  368. model_config = ConfigDict(populate_by_name=True)
  369. hospital_name: str = Field(..., alias="hospitalName")
  370. order_num: Optional[int] = Field(0, alias="orderNum")
  371. remark: Optional[str] = None
  372. class HospitalUpdateBody(BaseModel):
  373. model_config = ConfigDict(populate_by_name=True)
  374. hospital_name: Optional[str] = Field(None, alias="hospitalName")
  375. order_num: Optional[int] = Field(None, alias="orderNum")
  376. remark: Optional[str] = None
  377. @router.get("/collection-regions/{region_id}/hospitals")
  378. async def list_hospitals(
  379. region_id: str,
  380. page: int = Query(1, ge=1),
  381. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  382. keyword: Optional[str] = None,
  383. user: dict = Depends(require_menu_code("biz:collectionRegion:list")),
  384. db: Session = Depends(get_db),
  385. ):
  386. try:
  387. tenant_no = _get_tenant_no(db, user)
  388. items, total = region_svc.list_hospitals(
  389. db, region_id=region_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
  390. )
  391. tp = region_svc.total_pages(total, pageSize)
  392. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  393. except ValueError as e:
  394. return error_response(400, str(e))
  395. @router.post("/collection-regions/{region_id}/hospitals")
  396. async def create_hospital(
  397. region_id: str,
  398. body: HospitalCreateBody,
  399. user: dict = Depends(require_admin_or_super_admin),
  400. db: Session = Depends(get_db),
  401. ):
  402. try:
  403. tenant_no = _get_tenant_no(db, user)
  404. obj = region_svc.create_hospital(
  405. db,
  406. region_id=region_id,
  407. hospital_name=body.hospital_name,
  408. tenant_no=tenant_no,
  409. order_num=body.order_num,
  410. remark=body.remark,
  411. user_id=user.get("user_id"),
  412. )
  413. return success_response(region_svc._hospital_to_dict(obj), message="创建成功")
  414. except ValueError as e:
  415. return error_response(400, str(e))
  416. @router.put("/collection-regions/{region_id}/hospitals/{hospital_id}")
  417. async def update_hospital(
  418. region_id: str,
  419. hospital_id: str,
  420. body: HospitalUpdateBody,
  421. user: dict = Depends(require_admin_or_super_admin),
  422. db: Session = Depends(get_db),
  423. ):
  424. try:
  425. tenant_no = _get_tenant_no(db, user)
  426. h = region_svc.get_hospital(db, hospital_id, tenant_no=tenant_no)
  427. if not h or h.region_id != region_id:
  428. return error_response(400, "医院不存在")
  429. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  430. obj = region_svc.update_hospital(
  431. db,
  432. hospital_id,
  433. hospital_name=payload.get("hospital_name"),
  434. order_num=payload.get("order_num"),
  435. remark=payload.get("remark"),
  436. user_id=user.get("user_id"),
  437. tenant_no=tenant_no,
  438. )
  439. return success_response(region_svc._hospital_to_dict(obj), message="更新成功")
  440. except ValueError as e:
  441. return error_response(400, str(e))
  442. @router.delete("/collection-regions/{region_id}/hospitals/{hospital_id}")
  443. async def delete_hospital(
  444. region_id: str,
  445. hospital_id: str,
  446. user: dict = Depends(require_admin_or_super_admin),
  447. db: Session = Depends(get_db),
  448. ):
  449. try:
  450. tenant_no = _get_tenant_no(db, user)
  451. h = region_svc.get_hospital(db, hospital_id, tenant_no=tenant_no)
  452. if not h or h.region_id != region_id:
  453. return error_response(400, "医院不存在")
  454. region_svc.soft_delete_hospital(db, hospital_id, tenant_no=tenant_no)
  455. return success_response(None, message="删除成功")
  456. except ValueError as e:
  457. return error_response(400, str(e))
  458. # ---------------------------------------------------------------------------
  459. # 采集协议号池
  460. # ---------------------------------------------------------------------------
  461. class ProtocolNoCreateBody(BaseModel):
  462. model_config = ConfigDict(populate_by_name=True)
  463. protocol_no: str = Field(..., alias="protocolNo")
  464. issued: int = 0
  465. order_num: Optional[int] = Field(0, alias="orderNum")
  466. remark: Optional[str] = None
  467. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  468. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  469. issued_time: Optional[str] = Field(None, alias="issuedTime")
  470. class ProtocolNoUpdateBody(BaseModel):
  471. model_config = ConfigDict(populate_by_name=True)
  472. protocol_no: Optional[str] = Field(None, alias="protocolNo")
  473. issued: Optional[int] = None
  474. order_num: Optional[int] = Field(None, alias="orderNum")
  475. remark: Optional[str] = None
  476. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  477. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  478. issued_time: Optional[str] = Field(None, alias="issuedTime")
  479. @router.get("/collection-protocol-nos")
  480. async def list_protocol_nos(
  481. page: int = Query(1, ge=1),
  482. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  483. keyword: Optional[str] = None,
  484. issued_user_id: Optional[str] = Query(None, alias="issuedUserId"),
  485. user: dict = Depends(require_menu_code("biz:collectionProtocolNo:list")),
  486. db: Session = Depends(get_db),
  487. ):
  488. tenant_no = _get_tenant_no(db, user)
  489. query = db.query(BizCollectionProtocolNo).filter(
  490. BizCollectionProtocolNo.del_flag == 0,
  491. BizCollectionProtocolNo.tenant_no == tenant_no,
  492. )
  493. kw = (keyword or "").strip()
  494. if kw:
  495. like = f"%{kw}%"
  496. query = query.filter(
  497. or_(
  498. BizCollectionProtocolNo.protocol_no.like(like),
  499. BizCollectionProtocolNo.remark.like(like),
  500. )
  501. )
  502. uid = (issued_user_id or "").strip()
  503. if uid:
  504. query = query.filter(BizCollectionProtocolNo.issued_user_id == uid)
  505. query = apply_data_scope(query, BizCollectionProtocolNo, user, db, tenant_no)
  506. total = query.count()
  507. items = query.order_by(BizCollectionProtocolNo.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  508. result_list = [protocol_no_svc._row_to_dict(r) for r in items]
  509. protocol_no_svc.enrich_issued_user_names(db, result_list)
  510. used_nos = collection_protocol_svc.get_protocol_nos_used_in_protocols(
  511. db,
  512. tenant_no=tenant_no,
  513. protocol_nos=[row["protocolNo"] for row in result_list],
  514. )
  515. for row in result_list:
  516. row["usedInProtocol"] = row.get("protocolNo") in used_nos
  517. tp = protocol_no_svc.total_pages(total, pageSize)
  518. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  519. class ProtocolNoBatchGenerateBody(BaseModel):
  520. model_config = ConfigDict(populate_by_name=True)
  521. prefix: str = Field("ZKZX", alias="prefix")
  522. middle: str = Field("34", alias="middle")
  523. seq_start: int = Field(..., alias="seqStart")
  524. seq_end: int = Field(..., alias="seqEnd")
  525. digit_count: int = Field(6, alias="digitCount")
  526. class ProtocolNoBatchIssueBody(BaseModel):
  527. model_config = ConfigDict(populate_by_name=True)
  528. ids: List[str] = Field(..., alias="ids")
  529. issued_user_id: str = Field(..., alias="issuedUserId")
  530. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  531. @router.post("/collection-protocol-nos/batch-generate")
  532. async def batch_generate_protocol_nos(
  533. body: ProtocolNoBatchGenerateBody,
  534. user: dict = Depends(require_admin_or_super_admin),
  535. db: Session = Depends(get_db),
  536. ):
  537. try:
  538. tenant_no = _get_tenant_no(db, user)
  539. result = protocol_no_svc.batch_generate_protocol_nos(
  540. db,
  541. prefix=body.prefix,
  542. middle=body.middle,
  543. seq_start=body.seq_start,
  544. seq_end=body.seq_end,
  545. digit_count=body.digit_count,
  546. tenant_no=tenant_no,
  547. user_id=user.get("user_id"),
  548. )
  549. msg = f"成功生成 {result['created']} 条"
  550. if result['skipped'] > 0:
  551. msg += f",跳过已存在 {result['skipped']} 条"
  552. return success_response(result, message=msg)
  553. except ValueError as e:
  554. return error_response(400, str(e))
  555. @router.post("/collection-protocol-nos/batch-issue")
  556. async def batch_issue_protocol_nos(
  557. body: ProtocolNoBatchIssueBody,
  558. user: dict = Depends(require_admin_or_super_admin),
  559. db: Session = Depends(get_db),
  560. ):
  561. try:
  562. tenant_no = _get_tenant_no(db, user)
  563. result = protocol_no_svc.batch_issue_protocol_nos(
  564. db,
  565. ids=body.ids,
  566. issued_user_id=body.issued_user_id,
  567. issued_user_name=body.issued_user_name,
  568. tenant_no=tenant_no,
  569. user_id=user.get("user_id"),
  570. )
  571. msg = f"成功下发 {result['issuedCount']} 条"
  572. if result['alreadyIssued']:
  573. msg += f",{len(result['alreadyIssued'])} 条已下发被跳过"
  574. return success_response(result, message=msg)
  575. except ValueError as e:
  576. return error_response(400, str(e))
  577. class ProtocolNoBatchRevokeBody(BaseModel):
  578. model_config = ConfigDict(populate_by_name=True)
  579. ids: List[str] = Field(..., alias="ids")
  580. @router.post("/collection-protocol-nos/batch-revoke")
  581. async def batch_revoke_protocol_nos(
  582. body: ProtocolNoBatchRevokeBody,
  583. user: dict = Depends(require_admin_or_super_admin),
  584. db: Session = Depends(get_db),
  585. ):
  586. try:
  587. tenant_no = _get_tenant_no(db, user)
  588. result = protocol_no_svc.batch_revoke_protocol_nos(
  589. db,
  590. ids=body.ids,
  591. tenant_no=tenant_no,
  592. user_id=user.get("user_id"),
  593. )
  594. msg = f"成功收回 {result['revokedCount']} 条"
  595. if result["notIssued"]:
  596. msg += f",{len(result['notIssued'])} 条未下发已跳过"
  597. return success_response(result, message=msg)
  598. except ValueError as e:
  599. return error_response(400, str(e))
  600. @router.post("/collection-protocol-nos")
  601. async def create_protocol_no(
  602. body: ProtocolNoCreateBody,
  603. user: dict = Depends(require_admin_or_super_admin),
  604. db: Session = Depends(get_db),
  605. ):
  606. try:
  607. tenant_no = _get_tenant_no(db, user)
  608. obj = protocol_no_svc.create_protocol_no(
  609. db,
  610. protocol_no=body.protocol_no,
  611. issued=body.issued,
  612. tenant_no=tenant_no,
  613. order_num=body.order_num,
  614. remark=body.remark,
  615. user_id=user.get("user_id"),
  616. issued_user_id=body.issued_user_id,
  617. issued_user_name=body.issued_user_name,
  618. issued_time=body.issued_time,
  619. )
  620. row = protocol_no_svc._row_to_dict(obj)
  621. protocol_no_svc.enrich_issued_user_names(db, [row])
  622. return success_response(row, message="创建成功")
  623. except ValueError as e:
  624. return error_response(400, str(e))
  625. @router.put("/collection-protocol-nos/{row_id}")
  626. async def update_protocol_no(
  627. row_id: str,
  628. body: ProtocolNoUpdateBody,
  629. user: dict = Depends(require_admin_or_super_admin),
  630. db: Session = Depends(get_db),
  631. ):
  632. try:
  633. tenant_no = _get_tenant_no(db, user)
  634. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  635. obj = protocol_no_svc.update_protocol_no(
  636. db,
  637. row_id,
  638. protocol_no=payload.get("protocol_no"),
  639. issued=payload.get("issued"),
  640. order_num=payload.get("order_num"),
  641. remark=payload.get("remark"),
  642. user_id=user.get("user_id"),
  643. tenant_no=tenant_no,
  644. issued_user_id=payload.get("issued_user_id"),
  645. issued_user_name=payload.get("issued_user_name"),
  646. issued_time=payload.get("issued_time"),
  647. )
  648. return success_response(protocol_no_svc._row_to_dict(obj), message="更新成功")
  649. except ValueError as e:
  650. return error_response(400, str(e))
  651. @router.delete("/collection-protocol-nos/{row_id}")
  652. async def delete_protocol_no(
  653. row_id: str,
  654. user: dict = Depends(require_admin_or_super_admin),
  655. db: Session = Depends(get_db),
  656. ):
  657. try:
  658. tenant_no = _get_tenant_no(db, user)
  659. protocol_no_svc.soft_delete_protocol_no(db, row_id, tenant_no=tenant_no)
  660. return success_response(None, message="删除成功")
  661. except ValueError as e:
  662. return error_response(400, str(e))
  663. # ---------------------------------------------------------------------------
  664. # 样本编码池
  665. # ---------------------------------------------------------------------------
  666. class SampleCodeCreateBody(BaseModel):
  667. model_config = ConfigDict(populate_by_name=True)
  668. sample_code: str = Field(..., alias="sampleCode")
  669. sample_code_type: Optional[str] = Field(None, alias="sampleCodeType")
  670. issued: int = 0
  671. order_num: Optional[int] = Field(0, alias="orderNum")
  672. remark: Optional[str] = None
  673. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  674. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  675. issued_time: Optional[str] = Field(None, alias="issuedTime")
  676. class SampleCodeUpdateBody(BaseModel):
  677. model_config = ConfigDict(populate_by_name=True)
  678. sample_code: Optional[str] = Field(None, alias="sampleCode")
  679. issued: Optional[int] = None
  680. order_num: Optional[int] = Field(None, alias="orderNum")
  681. remark: Optional[str] = None
  682. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  683. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  684. issued_time: Optional[str] = Field(None, alias="issuedTime")
  685. class SampleCodeBatchGenerateBody(BaseModel):
  686. model_config = ConfigDict(populate_by_name=True)
  687. sample_code_type: str = Field(..., alias="sampleCodeType")
  688. middle: str = Field("34", alias="middle")
  689. seq_start: int = Field(..., alias="seqStart")
  690. seq_end: int = Field(..., alias="seqEnd")
  691. digit_count: int = Field(6, alias="digitCount")
  692. class SampleCodeBatchIssueBody(BaseModel):
  693. model_config = ConfigDict(populate_by_name=True)
  694. ids: List[str] = Field(..., alias="ids")
  695. issued_user_id: str = Field(..., alias="issuedUserId")
  696. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  697. @router.post("/sample-codes/batch-generate")
  698. async def batch_generate_sample_codes(
  699. body: SampleCodeBatchGenerateBody,
  700. user: dict = Depends(require_admin_or_super_admin),
  701. db: Session = Depends(get_db),
  702. ):
  703. try:
  704. tenant_no = _get_tenant_no(db, user)
  705. result = sample_code_svc.batch_generate_sample_codes(
  706. db,
  707. sample_code_type=body.sample_code_type,
  708. middle=body.middle,
  709. seq_start=body.seq_start,
  710. seq_end=body.seq_end,
  711. digit_count=body.digit_count,
  712. tenant_no=tenant_no,
  713. user_id=user.get("user_id"),
  714. )
  715. msg = f"成功生成 {result['created']} 条"
  716. if result["skipped"] > 0:
  717. msg += f",跳过已存在 {result['skipped']} 条"
  718. return success_response(result, message=msg)
  719. except ValueError as e:
  720. return error_response(400, str(e))
  721. @router.post("/sample-codes/batch-issue")
  722. async def batch_issue_sample_codes(
  723. body: SampleCodeBatchIssueBody,
  724. user: dict = Depends(require_admin_or_super_admin),
  725. db: Session = Depends(get_db),
  726. ):
  727. try:
  728. tenant_no = _get_tenant_no(db, user)
  729. result = sample_code_svc.batch_issue_sample_codes(
  730. db,
  731. ids=body.ids,
  732. issued_user_id=body.issued_user_id,
  733. issued_user_name=body.issued_user_name,
  734. tenant_no=tenant_no,
  735. user_id=user.get("user_id"),
  736. )
  737. msg = f"成功下发 {result['issuedCount']} 条"
  738. if result["alreadyIssued"]:
  739. msg += f",{len(result['alreadyIssued'])} 条已下发被跳过"
  740. return success_response(result, message=msg)
  741. except ValueError as e:
  742. return error_response(400, str(e))
  743. class SampleCodeBatchRevokeBody(BaseModel):
  744. model_config = ConfigDict(populate_by_name=True)
  745. ids: List[str] = Field(..., alias="ids")
  746. @router.post("/sample-codes/batch-revoke")
  747. async def batch_revoke_sample_codes(
  748. body: SampleCodeBatchRevokeBody,
  749. user: dict = Depends(require_admin_or_super_admin),
  750. db: Session = Depends(get_db),
  751. ):
  752. try:
  753. tenant_no = _get_tenant_no(db, user)
  754. result = sample_code_svc.batch_revoke_sample_codes(
  755. db,
  756. ids=body.ids,
  757. tenant_no=tenant_no,
  758. user_id=user.get("user_id"),
  759. )
  760. msg = f"成功收回 {result['revokedCount']} 条"
  761. if result["notIssued"]:
  762. msg += f",{len(result['notIssued'])} 条未下发已跳过"
  763. return success_response(result, message=msg)
  764. except ValueError as e:
  765. return error_response(400, str(e))
  766. @router.get("/sample-codes")
  767. async def list_sample_codes(
  768. page: int = Query(1, ge=1),
  769. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  770. keyword: Optional[str] = None,
  771. sample_code_type: Optional[str] = Query(None, alias="sampleCodeType"),
  772. issued_user_id: Optional[str] = Query(None, alias="issuedUserId"),
  773. user: dict = Depends(require_menu_code("biz:sampleCode:list")),
  774. db: Session = Depends(get_db),
  775. ):
  776. tenant_no = _get_tenant_no(db, user)
  777. query = db.query(BizSampleCode).filter(
  778. BizSampleCode.del_flag == 0,
  779. BizSampleCode.tenant_no == tenant_no,
  780. )
  781. kw = (keyword or "").strip()
  782. if kw:
  783. like = f"%{kw}%"
  784. query = query.filter(
  785. or_(
  786. BizSampleCode.sample_code.like(like),
  787. BizSampleCode.remark.like(like),
  788. )
  789. )
  790. code_type = (sample_code_type or "").strip()
  791. if code_type:
  792. query = query.filter(BizSampleCode.sample_code_type == code_type)
  793. uid = (issued_user_id or "").strip()
  794. if uid:
  795. query = query.filter(BizSampleCode.issued_user_id == uid)
  796. query = apply_data_scope(query, BizSampleCode, user, db, tenant_no)
  797. total = query.count()
  798. items = query.order_by(BizSampleCode.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  799. result_list = [sample_code_svc._row_to_dict(r) for r in items]
  800. used_codes = collection_protocol_svc.get_sample_codes_used_in_protocol_samples(
  801. db,
  802. tenant_no=tenant_no,
  803. sample_codes=[row["sampleCode"] for row in result_list],
  804. )
  805. for row in result_list:
  806. row["usedInProtocol"] = row.get("sampleCode") in used_codes
  807. tp = sample_code_svc.total_pages(total, pageSize)
  808. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  809. @router.post("/sample-codes")
  810. async def create_sample_code(
  811. body: SampleCodeCreateBody,
  812. user: dict = Depends(require_admin_or_super_admin),
  813. db: Session = Depends(get_db),
  814. ):
  815. try:
  816. tenant_no = _get_tenant_no(db, user)
  817. obj = sample_code_svc.create_sample_code(
  818. db,
  819. sample_code=body.sample_code,
  820. sample_code_type=body.sample_code_type,
  821. issued=body.issued,
  822. tenant_no=tenant_no,
  823. order_num=body.order_num,
  824. remark=body.remark,
  825. user_id=user.get("user_id"),
  826. issued_user_id=body.issued_user_id,
  827. issued_user_name=body.issued_user_name,
  828. issued_time=body.issued_time,
  829. )
  830. return success_response(sample_code_svc._row_to_dict(obj), message="创建成功")
  831. except ValueError as e:
  832. return error_response(400, str(e))
  833. @router.put("/sample-codes/{row_id}")
  834. async def update_sample_code(
  835. row_id: str,
  836. body: SampleCodeUpdateBody,
  837. user: dict = Depends(require_admin_or_super_admin),
  838. db: Session = Depends(get_db),
  839. ):
  840. try:
  841. tenant_no = _get_tenant_no(db, user)
  842. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  843. obj = sample_code_svc.update_sample_code(
  844. db,
  845. row_id,
  846. sample_code=payload.get("sample_code"),
  847. issued=payload.get("issued"),
  848. order_num=payload.get("order_num"),
  849. remark=payload.get("remark"),
  850. user_id=user.get("user_id"),
  851. tenant_no=tenant_no,
  852. issued_user_id=payload.get("issued_user_id"),
  853. issued_user_name=payload.get("issued_user_name"),
  854. issued_time=payload.get("issued_time"),
  855. )
  856. return success_response(sample_code_svc._row_to_dict(obj), message="更新成功")
  857. except ValueError as e:
  858. return error_response(400, str(e))
  859. @router.delete("/sample-codes/{row_id}")
  860. async def delete_sample_code(
  861. row_id: str,
  862. user: dict = Depends(require_admin_or_super_admin),
  863. db: Session = Depends(get_db),
  864. ):
  865. try:
  866. tenant_no = _get_tenant_no(db, user)
  867. sample_code_svc.soft_delete_sample_code(db, row_id, tenant_no=tenant_no)
  868. return success_response(None, message="删除成功")
  869. except ValueError as e:
  870. return error_response(400, str(e))