basic_info.py 42 KB

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