basic_info.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155
  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. staff_map = region_svc._staff_names_by_region_ids(db, region_ids)
  315. result_list = [
  316. region_svc._region_to_dict(
  317. r,
  318. hospital_names=hospital_map.get(r.id, []),
  319. staff_names=staff_map.get(r.id, ([], []))[0],
  320. leader_names=staff_map.get(r.id, ([], []))[1],
  321. )
  322. for r in items
  323. ]
  324. tp = region_svc.total_pages(total, pageSize)
  325. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  326. @router.post("/collection-regions")
  327. async def create_collection_region(
  328. body: CollectionRegionCreateBody,
  329. user: dict = Depends(require_admin_or_super_admin),
  330. db: Session = Depends(get_db),
  331. ):
  332. try:
  333. tenant_no = _get_tenant_no(db, user)
  334. obj = region_svc.create_collection_region(
  335. db,
  336. region_name=body.region_name,
  337. tenant_no=tenant_no,
  338. order_num=body.order_num,
  339. remark=body.remark,
  340. user_id=user.get("user_id"),
  341. )
  342. return success_response(region_svc._region_to_dict(obj), message="创建成功")
  343. except ValueError as e:
  344. return error_response(400, str(e))
  345. @router.put("/collection-regions/{region_id}")
  346. async def update_collection_region(
  347. region_id: str,
  348. body: CollectionRegionUpdateBody,
  349. user: dict = Depends(require_admin_or_super_admin),
  350. db: Session = Depends(get_db),
  351. ):
  352. try:
  353. tenant_no = _get_tenant_no(db, user)
  354. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  355. obj = region_svc.update_collection_region(
  356. db,
  357. region_id,
  358. region_name=payload.get("region_name"),
  359. order_num=payload.get("order_num"),
  360. remark=payload.get("remark"),
  361. user_id=user.get("user_id"),
  362. tenant_no=tenant_no,
  363. )
  364. return success_response(region_svc._region_to_dict(obj), message="更新成功")
  365. except ValueError as e:
  366. return error_response(400, str(e))
  367. @router.delete("/collection-regions/{region_id}")
  368. async def delete_collection_region(
  369. region_id: str,
  370. user: dict = Depends(require_admin_or_super_admin),
  371. db: Session = Depends(get_db),
  372. ):
  373. try:
  374. tenant_no = _get_tenant_no(db, user)
  375. region_svc.soft_delete_collection_region(db, region_id, tenant_no=tenant_no)
  376. return success_response(None, message="删除成功")
  377. except ValueError as e:
  378. return error_response(400, str(e))
  379. # ---------------------------------------------------------------------------
  380. # 采集地区 — 下属医院
  381. # ---------------------------------------------------------------------------
  382. class HospitalCreateBody(BaseModel):
  383. model_config = ConfigDict(populate_by_name=True)
  384. hospital_name: str = Field(..., alias="hospitalName")
  385. order_num: Optional[int] = Field(0, alias="orderNum")
  386. remark: Optional[str] = None
  387. class HospitalUpdateBody(BaseModel):
  388. model_config = ConfigDict(populate_by_name=True)
  389. hospital_name: Optional[str] = Field(None, alias="hospitalName")
  390. order_num: Optional[int] = Field(None, alias="orderNum")
  391. remark: Optional[str] = None
  392. @router.get("/collection-hospitals/search")
  393. async def search_collection_hospitals(
  394. keyword: str = Query(..., min_length=1),
  395. limit: int = Query(20, ge=1, le=50),
  396. user: dict = Depends(require_menu_code("biz:collectionRegion:list")),
  397. db: Session = Depends(get_db),
  398. ):
  399. tenant_no = _get_tenant_no(db, user)
  400. items = region_svc.search_hospitals_by_keyword(db, keyword=keyword, tenant_no=tenant_no, limit=limit)
  401. return success_response({"list": items})
  402. @router.get("/collection-regions/{region_id}/hospitals")
  403. async def list_hospitals(
  404. region_id: str,
  405. page: int = Query(1, ge=1),
  406. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  407. keyword: Optional[str] = None,
  408. user: dict = Depends(require_menu_code("biz:collectionRegion:list")),
  409. db: Session = Depends(get_db),
  410. ):
  411. try:
  412. tenant_no = _get_tenant_no(db, user)
  413. items, total = region_svc.list_hospitals(
  414. db, region_id=region_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no
  415. )
  416. tp = region_svc.total_pages(total, pageSize)
  417. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  418. except ValueError as e:
  419. return error_response(400, str(e))
  420. @router.post("/collection-regions/{region_id}/hospitals")
  421. async def create_hospital(
  422. region_id: str,
  423. body: HospitalCreateBody,
  424. user: dict = Depends(require_admin_or_super_admin),
  425. db: Session = Depends(get_db),
  426. ):
  427. try:
  428. tenant_no = _get_tenant_no(db, user)
  429. obj = region_svc.create_hospital(
  430. db,
  431. region_id=region_id,
  432. hospital_name=body.hospital_name,
  433. tenant_no=tenant_no,
  434. order_num=body.order_num,
  435. remark=body.remark,
  436. user_id=user.get("user_id"),
  437. )
  438. return success_response(region_svc._hospital_to_dict(obj), message="创建成功")
  439. except ValueError as e:
  440. return error_response(400, str(e))
  441. @router.put("/collection-regions/{region_id}/hospitals/{hospital_id}")
  442. async def update_hospital(
  443. region_id: str,
  444. hospital_id: str,
  445. body: HospitalUpdateBody,
  446. user: dict = Depends(require_admin_or_super_admin),
  447. db: Session = Depends(get_db),
  448. ):
  449. try:
  450. tenant_no = _get_tenant_no(db, user)
  451. h = region_svc.get_hospital(db, hospital_id, tenant_no=tenant_no)
  452. if not h or h.region_id != region_id:
  453. return error_response(400, "医院不存在")
  454. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  455. obj = region_svc.update_hospital(
  456. db,
  457. hospital_id,
  458. hospital_name=payload.get("hospital_name"),
  459. order_num=payload.get("order_num"),
  460. remark=payload.get("remark"),
  461. user_id=user.get("user_id"),
  462. tenant_no=tenant_no,
  463. )
  464. return success_response(region_svc._hospital_to_dict(obj), message="更新成功")
  465. except ValueError as e:
  466. return error_response(400, str(e))
  467. @router.delete("/collection-regions/{region_id}/hospitals/{hospital_id}")
  468. async def delete_hospital(
  469. region_id: str,
  470. hospital_id: str,
  471. user: dict = Depends(require_admin_or_super_admin),
  472. db: Session = Depends(get_db),
  473. ):
  474. try:
  475. tenant_no = _get_tenant_no(db, user)
  476. h = region_svc.get_hospital(db, hospital_id, tenant_no=tenant_no)
  477. if not h or h.region_id != region_id:
  478. return error_response(400, "医院不存在")
  479. region_svc.soft_delete_hospital(db, hospital_id, tenant_no=tenant_no)
  480. return success_response(None, message="删除成功")
  481. except ValueError as e:
  482. return error_response(400, str(e))
  483. # ---------------------------------------------------------------------------
  484. # 采集地区 — 工作人员
  485. # ---------------------------------------------------------------------------
  486. class RegionStaffCreateBody(BaseModel):
  487. model_config = ConfigDict(populate_by_name=True)
  488. user_id: str = Field(..., alias="userId")
  489. is_leader: Optional[bool] = Field(False, alias="isLeader")
  490. order_num: Optional[int] = Field(0, alias="orderNum")
  491. remark: Optional[str] = None
  492. class RegionStaffUpdateBody(BaseModel):
  493. model_config = ConfigDict(populate_by_name=True)
  494. is_leader: Optional[bool] = Field(None, alias="isLeader")
  495. order_num: Optional[int] = Field(None, alias="orderNum")
  496. remark: Optional[str] = None
  497. @router.get("/collection-regions/{region_id}/staff")
  498. async def list_region_staff(
  499. region_id: str,
  500. page: int = Query(1, ge=1),
  501. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  502. user: dict = Depends(require_menu_code("biz:collectionRegion:list")),
  503. db: Session = Depends(get_db),
  504. ):
  505. try:
  506. tenant_no = _get_tenant_no(db, user)
  507. items, total = region_svc.list_staff(
  508. db, region_id=region_id, page=page, page_size=pageSize, tenant_no=tenant_no
  509. )
  510. tp = region_svc.total_pages(total, pageSize)
  511. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  512. except ValueError as e:
  513. return error_response(400, str(e))
  514. @router.post("/collection-regions/{region_id}/staff")
  515. async def create_region_staff(
  516. region_id: str,
  517. body: RegionStaffCreateBody,
  518. user: dict = Depends(require_admin_or_super_admin),
  519. db: Session = Depends(get_db),
  520. ):
  521. try:
  522. tenant_no = _get_tenant_no(db, user)
  523. obj = region_svc.create_staff(
  524. db,
  525. region_id=region_id,
  526. user_id=body.user_id,
  527. is_leader=bool(body.is_leader),
  528. tenant_no=tenant_no,
  529. order_num=body.order_num,
  530. remark=body.remark,
  531. user_id_actor=user.get("user_id"),
  532. )
  533. # enrich response with user display fields
  534. items, _ = region_svc.list_staff(db, region_id=region_id, page=1, page_size=100, tenant_no=tenant_no)
  535. matched = next((x for x in items if x["id"] == obj.id), region_svc._staff_to_dict(obj))
  536. return success_response(matched, message="创建成功")
  537. except ValueError as e:
  538. return error_response(400, str(e))
  539. @router.put("/collection-regions/{region_id}/staff/{staff_id}")
  540. async def update_region_staff(
  541. region_id: str,
  542. staff_id: str,
  543. body: RegionStaffUpdateBody,
  544. user: dict = Depends(require_admin_or_super_admin),
  545. db: Session = Depends(get_db),
  546. ):
  547. try:
  548. tenant_no = _get_tenant_no(db, user)
  549. s = region_svc.get_staff(db, staff_id, tenant_no=tenant_no)
  550. if not s or s.region_id != region_id:
  551. return error_response(400, "绑定记录不存在")
  552. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  553. obj = region_svc.update_staff(
  554. db,
  555. staff_id,
  556. is_leader=payload.get("is_leader"),
  557. order_num=payload.get("order_num"),
  558. remark=payload.get("remark"),
  559. user_id=user.get("user_id"),
  560. tenant_no=tenant_no,
  561. )
  562. items, _ = region_svc.list_staff(db, region_id=region_id, page=1, page_size=100, tenant_no=tenant_no)
  563. matched = next((x for x in items if x["id"] == obj.id), region_svc._staff_to_dict(obj))
  564. return success_response(matched, message="更新成功")
  565. except ValueError as e:
  566. return error_response(400, str(e))
  567. @router.delete("/collection-regions/{region_id}/staff/{staff_id}")
  568. async def delete_region_staff(
  569. region_id: str,
  570. staff_id: str,
  571. user: dict = Depends(require_admin_or_super_admin),
  572. db: Session = Depends(get_db),
  573. ):
  574. try:
  575. tenant_no = _get_tenant_no(db, user)
  576. s = region_svc.get_staff(db, staff_id, tenant_no=tenant_no)
  577. if not s or s.region_id != region_id:
  578. return error_response(400, "绑定记录不存在")
  579. region_svc.soft_delete_staff(db, staff_id, tenant_no=tenant_no)
  580. return success_response(None, message="删除成功")
  581. except ValueError as e:
  582. return error_response(400, str(e))
  583. # ---------------------------------------------------------------------------
  584. # 采集协议号池
  585. # ---------------------------------------------------------------------------
  586. class ProtocolNoCreateBody(BaseModel):
  587. model_config = ConfigDict(populate_by_name=True)
  588. protocol_no: str = Field(..., alias="protocolNo")
  589. issued: int = 0
  590. order_num: Optional[int] = Field(0, alias="orderNum")
  591. remark: Optional[str] = None
  592. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  593. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  594. issued_time: Optional[str] = Field(None, alias="issuedTime")
  595. class ProtocolNoUpdateBody(BaseModel):
  596. model_config = ConfigDict(populate_by_name=True)
  597. protocol_no: Optional[str] = Field(None, alias="protocolNo")
  598. issued: Optional[int] = None
  599. order_num: Optional[int] = Field(None, alias="orderNum")
  600. remark: Optional[str] = None
  601. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  602. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  603. issued_time: Optional[str] = Field(None, alias="issuedTime")
  604. @router.get("/collection-protocol-nos")
  605. async def list_protocol_nos(
  606. page: int = Query(1, ge=1),
  607. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  608. keyword: Optional[str] = None,
  609. issued_user_id: Optional[str] = Query(None, alias="issuedUserId"),
  610. user: dict = Depends(require_menu_code("biz:collectionProtocolNo:list")),
  611. db: Session = Depends(get_db),
  612. ):
  613. tenant_no = _get_tenant_no(db, user)
  614. query = db.query(BizCollectionProtocolNo).filter(
  615. BizCollectionProtocolNo.del_flag == 0,
  616. BizCollectionProtocolNo.tenant_no == tenant_no,
  617. )
  618. kw = (keyword or "").strip()
  619. if kw:
  620. like = f"%{kw}%"
  621. query = query.filter(
  622. or_(
  623. BizCollectionProtocolNo.protocol_no.like(like),
  624. BizCollectionProtocolNo.remark.like(like),
  625. )
  626. )
  627. uid = (issued_user_id or "").strip()
  628. if uid:
  629. query = query.filter(BizCollectionProtocolNo.issued_user_id == uid)
  630. query = apply_data_scope(query, BizCollectionProtocolNo, user, db, tenant_no)
  631. total = query.count()
  632. items = query.order_by(BizCollectionProtocolNo.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  633. result_list = [protocol_no_svc._row_to_dict(r) for r in items]
  634. protocol_no_svc.enrich_issued_user_names(db, result_list)
  635. used_nos = collection_protocol_svc.get_protocol_nos_used_in_protocols(
  636. db,
  637. tenant_no=tenant_no,
  638. protocol_nos=[row["protocolNo"] for row in result_list],
  639. )
  640. for row in result_list:
  641. row["usedInProtocol"] = row.get("protocolNo") in used_nos
  642. tp = protocol_no_svc.total_pages(total, pageSize)
  643. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  644. class ProtocolNoBatchGenerateBody(BaseModel):
  645. model_config = ConfigDict(populate_by_name=True)
  646. prefix: str = Field("ZKZX", alias="prefix")
  647. middle: str = Field("34", alias="middle")
  648. seq_start: int = Field(..., alias="seqStart")
  649. seq_end: int = Field(..., alias="seqEnd")
  650. digit_count: int = Field(6, alias="digitCount")
  651. class ProtocolNoBatchIssueBody(BaseModel):
  652. model_config = ConfigDict(populate_by_name=True)
  653. ids: List[str] = Field(..., alias="ids")
  654. issued_user_id: str = Field(..., alias="issuedUserId")
  655. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  656. @router.post("/collection-protocol-nos/batch-generate")
  657. async def batch_generate_protocol_nos(
  658. body: ProtocolNoBatchGenerateBody,
  659. user: dict = Depends(require_admin_or_super_admin),
  660. db: Session = Depends(get_db),
  661. ):
  662. try:
  663. tenant_no = _get_tenant_no(db, user)
  664. result = protocol_no_svc.batch_generate_protocol_nos(
  665. db,
  666. prefix=body.prefix,
  667. middle=body.middle,
  668. seq_start=body.seq_start,
  669. seq_end=body.seq_end,
  670. digit_count=body.digit_count,
  671. tenant_no=tenant_no,
  672. user_id=user.get("user_id"),
  673. )
  674. msg = f"成功生成 {result['created']} 条"
  675. if result['skipped'] > 0:
  676. msg += f",跳过已存在 {result['skipped']} 条"
  677. return success_response(result, message=msg)
  678. except ValueError as e:
  679. return error_response(400, str(e))
  680. @router.post("/collection-protocol-nos/batch-issue")
  681. async def batch_issue_protocol_nos(
  682. body: ProtocolNoBatchIssueBody,
  683. user: dict = Depends(require_admin_or_super_admin),
  684. db: Session = Depends(get_db),
  685. ):
  686. try:
  687. tenant_no = _get_tenant_no(db, user)
  688. result = protocol_no_svc.batch_issue_protocol_nos(
  689. db,
  690. ids=body.ids,
  691. issued_user_id=body.issued_user_id,
  692. issued_user_name=body.issued_user_name,
  693. tenant_no=tenant_no,
  694. user_id=user.get("user_id"),
  695. )
  696. msg = f"成功下发 {result['issuedCount']} 条"
  697. if result['alreadyIssued']:
  698. msg += f",{len(result['alreadyIssued'])} 条已下发被跳过"
  699. return success_response(result, message=msg)
  700. except ValueError as e:
  701. return error_response(400, str(e))
  702. class ProtocolNoBatchRevokeBody(BaseModel):
  703. model_config = ConfigDict(populate_by_name=True)
  704. ids: List[str] = Field(..., alias="ids")
  705. @router.post("/collection-protocol-nos/batch-revoke")
  706. async def batch_revoke_protocol_nos(
  707. body: ProtocolNoBatchRevokeBody,
  708. user: dict = Depends(require_admin_or_super_admin),
  709. db: Session = Depends(get_db),
  710. ):
  711. try:
  712. tenant_no = _get_tenant_no(db, user)
  713. result = protocol_no_svc.batch_revoke_protocol_nos(
  714. db,
  715. ids=body.ids,
  716. tenant_no=tenant_no,
  717. user_id=user.get("user_id"),
  718. )
  719. msg = f"成功收回 {result['revokedCount']} 条"
  720. if result["notIssued"]:
  721. msg += f",{len(result['notIssued'])} 条未下发已跳过"
  722. return success_response(result, message=msg)
  723. except ValueError as e:
  724. return error_response(400, str(e))
  725. @router.post("/collection-protocol-nos")
  726. async def create_protocol_no(
  727. body: ProtocolNoCreateBody,
  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. obj = protocol_no_svc.create_protocol_no(
  734. db,
  735. protocol_no=body.protocol_no,
  736. issued=body.issued,
  737. tenant_no=tenant_no,
  738. order_num=body.order_num,
  739. remark=body.remark,
  740. user_id=user.get("user_id"),
  741. issued_user_id=body.issued_user_id,
  742. issued_user_name=body.issued_user_name,
  743. issued_time=body.issued_time,
  744. )
  745. row = protocol_no_svc._row_to_dict(obj)
  746. protocol_no_svc.enrich_issued_user_names(db, [row])
  747. return success_response(row, message="创建成功")
  748. except ValueError as e:
  749. return error_response(400, str(e))
  750. @router.put("/collection-protocol-nos/{row_id}")
  751. async def update_protocol_no(
  752. row_id: str,
  753. body: ProtocolNoUpdateBody,
  754. user: dict = Depends(require_admin_or_super_admin),
  755. db: Session = Depends(get_db),
  756. ):
  757. try:
  758. tenant_no = _get_tenant_no(db, user)
  759. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  760. obj = protocol_no_svc.update_protocol_no(
  761. db,
  762. row_id,
  763. protocol_no=payload.get("protocol_no"),
  764. issued=payload.get("issued"),
  765. order_num=payload.get("order_num"),
  766. remark=payload.get("remark"),
  767. user_id=user.get("user_id"),
  768. tenant_no=tenant_no,
  769. issued_user_id=payload.get("issued_user_id"),
  770. issued_user_name=payload.get("issued_user_name"),
  771. issued_time=payload.get("issued_time"),
  772. )
  773. return success_response(protocol_no_svc._row_to_dict(obj), message="更新成功")
  774. except ValueError as e:
  775. return error_response(400, str(e))
  776. @router.delete("/collection-protocol-nos/{row_id}")
  777. async def delete_protocol_no(
  778. row_id: str,
  779. user: dict = Depends(require_admin_or_super_admin),
  780. db: Session = Depends(get_db),
  781. ):
  782. try:
  783. tenant_no = _get_tenant_no(db, user)
  784. protocol_no_svc.soft_delete_protocol_no(db, row_id, tenant_no=tenant_no)
  785. return success_response(None, message="删除成功")
  786. except ValueError as e:
  787. return error_response(400, str(e))
  788. # ---------------------------------------------------------------------------
  789. # 样本编码池
  790. # ---------------------------------------------------------------------------
  791. class SampleCodeCreateBody(BaseModel):
  792. model_config = ConfigDict(populate_by_name=True)
  793. sample_code: str = Field(..., alias="sampleCode")
  794. sample_code_type: Optional[str] = Field(None, alias="sampleCodeType")
  795. issued: int = 0
  796. order_num: Optional[int] = Field(0, alias="orderNum")
  797. remark: Optional[str] = None
  798. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  799. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  800. issued_time: Optional[str] = Field(None, alias="issuedTime")
  801. class SampleCodeUpdateBody(BaseModel):
  802. model_config = ConfigDict(populate_by_name=True)
  803. sample_code: Optional[str] = Field(None, alias="sampleCode")
  804. issued: Optional[int] = None
  805. order_num: Optional[int] = Field(None, alias="orderNum")
  806. remark: Optional[str] = None
  807. issued_user_id: Optional[str] = Field(None, alias="issuedUserId")
  808. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  809. issued_time: Optional[str] = Field(None, alias="issuedTime")
  810. class SampleCodeBatchGenerateBody(BaseModel):
  811. model_config = ConfigDict(populate_by_name=True)
  812. sample_code_type: str = Field(..., alias="sampleCodeType")
  813. middle: str = Field("34", alias="middle")
  814. seq_start: int = Field(..., alias="seqStart")
  815. seq_end: int = Field(..., alias="seqEnd")
  816. digit_count: int = Field(6, alias="digitCount")
  817. class SampleCodeBatchIssueBody(BaseModel):
  818. model_config = ConfigDict(populate_by_name=True)
  819. ids: List[str] = Field(..., alias="ids")
  820. issued_user_id: str = Field(..., alias="issuedUserId")
  821. issued_user_name: Optional[str] = Field(None, alias="issuedUserName")
  822. @router.post("/sample-codes/batch-generate")
  823. async def batch_generate_sample_codes(
  824. body: SampleCodeBatchGenerateBody,
  825. user: dict = Depends(require_admin_or_super_admin),
  826. db: Session = Depends(get_db),
  827. ):
  828. try:
  829. tenant_no = _get_tenant_no(db, user)
  830. result = sample_code_svc.batch_generate_sample_codes(
  831. db,
  832. sample_code_type=body.sample_code_type,
  833. middle=body.middle,
  834. seq_start=body.seq_start,
  835. seq_end=body.seq_end,
  836. digit_count=body.digit_count,
  837. tenant_no=tenant_no,
  838. user_id=user.get("user_id"),
  839. )
  840. msg = f"成功生成 {result['created']} 条"
  841. if result["skipped"] > 0:
  842. msg += f",跳过已存在 {result['skipped']} 条"
  843. return success_response(result, message=msg)
  844. except ValueError as e:
  845. return error_response(400, str(e))
  846. @router.post("/sample-codes/batch-issue")
  847. async def batch_issue_sample_codes(
  848. body: SampleCodeBatchIssueBody,
  849. user: dict = Depends(require_admin_or_super_admin),
  850. db: Session = Depends(get_db),
  851. ):
  852. try:
  853. tenant_no = _get_tenant_no(db, user)
  854. result = sample_code_svc.batch_issue_sample_codes(
  855. db,
  856. ids=body.ids,
  857. issued_user_id=body.issued_user_id,
  858. issued_user_name=body.issued_user_name,
  859. tenant_no=tenant_no,
  860. user_id=user.get("user_id"),
  861. )
  862. msg = f"成功下发 {result['issuedCount']} 条"
  863. if result["alreadyIssued"]:
  864. msg += f",{len(result['alreadyIssued'])} 条已下发被跳过"
  865. return success_response(result, message=msg)
  866. except ValueError as e:
  867. return error_response(400, str(e))
  868. class SampleCodeBatchRevokeBody(BaseModel):
  869. model_config = ConfigDict(populate_by_name=True)
  870. ids: List[str] = Field(..., alias="ids")
  871. @router.post("/sample-codes/batch-revoke")
  872. async def batch_revoke_sample_codes(
  873. body: SampleCodeBatchRevokeBody,
  874. user: dict = Depends(require_admin_or_super_admin),
  875. db: Session = Depends(get_db),
  876. ):
  877. try:
  878. tenant_no = _get_tenant_no(db, user)
  879. result = sample_code_svc.batch_revoke_sample_codes(
  880. db,
  881. ids=body.ids,
  882. tenant_no=tenant_no,
  883. user_id=user.get("user_id"),
  884. )
  885. msg = f"成功收回 {result['revokedCount']} 条"
  886. if result["notIssued"]:
  887. msg += f",{len(result['notIssued'])} 条未下发已跳过"
  888. return success_response(result, message=msg)
  889. except ValueError as e:
  890. return error_response(400, str(e))
  891. @router.get("/sample-codes")
  892. async def list_sample_codes(
  893. page: int = Query(1, ge=1),
  894. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  895. keyword: Optional[str] = None,
  896. sample_code_type: Optional[str] = Query(None, alias="sampleCodeType"),
  897. code_filter: Optional[str] = Query(None, alias="codeFilter"),
  898. issued_user_id: Optional[str] = Query(None, alias="issuedUserId"),
  899. user: dict = Depends(require_menu_code("biz:sampleCode:list")),
  900. db: Session = Depends(get_db),
  901. ):
  902. tenant_no = _get_tenant_no(db, user)
  903. query = db.query(BizSampleCode).filter(
  904. BizSampleCode.del_flag == 0,
  905. BizSampleCode.tenant_no == tenant_no,
  906. )
  907. kw = (keyword or "").strip()
  908. if kw:
  909. like = f"%{kw}%"
  910. query = query.filter(
  911. or_(
  912. BizSampleCode.sample_code.like(like),
  913. BizSampleCode.remark.like(like),
  914. )
  915. )
  916. code_type = (sample_code_type or "").strip()
  917. if code_type:
  918. query = query.filter(BizSampleCode.sample_code_type == code_type)
  919. cf = (code_filter or "").strip()
  920. if cf:
  921. query = query.filter(BizSampleCode.sample_code.like(f"%{cf}%"))
  922. uid = (issued_user_id or "").strip()
  923. if uid:
  924. query = query.filter(BizSampleCode.issued_user_id == uid)
  925. query = apply_data_scope(query, BizSampleCode, user, db, tenant_no)
  926. total = query.count()
  927. items = (
  928. query.order_by(BizSampleCode.create_time.desc(), BizSampleCode.sample_code.desc())
  929. .offset((page - 1) * pageSize)
  930. .limit(pageSize)
  931. .all()
  932. )
  933. result_list = [sample_code_svc._row_to_dict(r) for r in items]
  934. used_codes = collection_protocol_svc.get_sample_codes_used_in_protocol_samples(
  935. db,
  936. tenant_no=tenant_no,
  937. sample_codes=[row["sampleCode"] for row in result_list],
  938. )
  939. for row in result_list:
  940. row["usedInProtocol"] = row.get("sampleCode") in used_codes
  941. tp = sample_code_svc.total_pages(total, pageSize)
  942. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  943. @router.post("/sample-codes")
  944. async def create_sample_code(
  945. body: SampleCodeCreateBody,
  946. user: dict = Depends(require_admin_or_super_admin),
  947. db: Session = Depends(get_db),
  948. ):
  949. try:
  950. tenant_no = _get_tenant_no(db, user)
  951. obj = sample_code_svc.create_sample_code(
  952. db,
  953. sample_code=body.sample_code,
  954. sample_code_type=body.sample_code_type,
  955. issued=body.issued,
  956. tenant_no=tenant_no,
  957. order_num=body.order_num,
  958. remark=body.remark,
  959. user_id=user.get("user_id"),
  960. issued_user_id=body.issued_user_id,
  961. issued_user_name=body.issued_user_name,
  962. issued_time=body.issued_time,
  963. )
  964. return success_response(sample_code_svc._row_to_dict(obj), message="创建成功")
  965. except ValueError as e:
  966. return error_response(400, str(e))
  967. @router.put("/sample-codes/{row_id}")
  968. async def update_sample_code(
  969. row_id: str,
  970. body: SampleCodeUpdateBody,
  971. user: dict = Depends(require_admin_or_super_admin),
  972. db: Session = Depends(get_db),
  973. ):
  974. try:
  975. tenant_no = _get_tenant_no(db, user)
  976. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  977. obj = sample_code_svc.update_sample_code(
  978. db,
  979. row_id,
  980. sample_code=payload.get("sample_code"),
  981. issued=payload.get("issued"),
  982. order_num=payload.get("order_num"),
  983. remark=payload.get("remark"),
  984. user_id=user.get("user_id"),
  985. tenant_no=tenant_no,
  986. issued_user_id=payload.get("issued_user_id"),
  987. issued_user_name=payload.get("issued_user_name"),
  988. issued_time=payload.get("issued_time"),
  989. )
  990. return success_response(sample_code_svc._row_to_dict(obj), message="更新成功")
  991. except ValueError as e:
  992. return error_response(400, str(e))
  993. @router.delete("/sample-codes/{row_id}")
  994. async def delete_sample_code(
  995. row_id: str,
  996. user: dict = Depends(require_admin_or_super_admin),
  997. db: Session = Depends(get_db),
  998. ):
  999. try:
  1000. tenant_no = _get_tenant_no(db, user)
  1001. sample_code_svc.soft_delete_sample_code(db, row_id, tenant_no=tenant_no)
  1002. return success_response(None, message="删除成功")
  1003. except ValueError as e:
  1004. return error_response(400, str(e))