basic_info.py 36 KB

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