basic_info.py 37 KB

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