basic_info.py 35 KB

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