sample_view.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. import sys
  2. import os
  3. import logging
  4. import httpx
  5. import urllib.parse
  6. import asyncio
  7. from datetime import datetime, timezone
  8. from typing import Optional, List, Any, Union
  9. from fastapi import APIRouter, Depends, HTTPException, Request, Response, BackgroundTasks
  10. from fastapi.responses import HTMLResponse
  11. from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
  12. from app.sample.schemas.sample_schemas import BatchEnterRequest, BatchDeleteRequest, ConvertRequest, DocumentAdd, UploadUrlRequest
  13. from app.services.sample_service import SampleService
  14. from app.services.jwt_token import verify_token
  15. from app.schemas.base import ApiResponse
  16. from app.base import get_mineru_manager
  17. from app.services.task_service import task_service
  18. # 获取logger
  19. logger = logging.getLogger(__name__)
  20. router = APIRouter(prefix="/sample", tags=["样本中心"])
  21. security = HTTPBearer()
  22. security_optional = HTTPBearer(auto_error=False)
  23. @router.get("/tasks")
  24. async def get_tasks(type: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  25. """获取任务列表"""
  26. try:
  27. payload = verify_token(credentials.credentials)
  28. if not payload:
  29. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  30. tasks = await task_service.get_task_list(type)
  31. return ApiResponse(code=0, message="成功", data=tasks, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  32. except Exception as e:
  33. logger.exception("获取任务列表失败")
  34. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  35. # --- 文档管理中心 API ---
  36. @router.post("/documents/upload-url")
  37. async def get_upload_url(req: UploadUrlRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  38. """获取 MinIO 预签名上传 URL"""
  39. try:
  40. payload = verify_token(credentials.credentials)
  41. if not payload:
  42. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  43. sample_service = SampleService()
  44. success, message, data = await sample_service.get_upload_url(req.filename, req.content_type, prefix=req.prefix)
  45. if success:
  46. return ApiResponse(code=0, message=message, data=data, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  47. else:
  48. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  49. except Exception as e:
  50. logger.exception("获取上传链接失败")
  51. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  52. @router.get("/documents/proxy-view")
  53. async def proxy_view(url: str, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
  54. """抓取外部文档内容并返回,支持 HTML 和 PDF 等二进制文件。支持从 Header 或 Query 参数获取 Token。"""
  55. try:
  56. # 确保 URL 已解码
  57. url = urllib.parse.unquote(url)
  58. # 优先从 Header 获取,如果没有则从参数获取
  59. actual_token = None
  60. if credentials:
  61. actual_token = credentials.credentials
  62. elif token:
  63. actual_token = token
  64. if not actual_token:
  65. return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  66. payload = verify_token(actual_token)
  67. if not payload or not payload.get("is_superuser"):
  68. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  69. # 增加超时时间,支持大文件下载
  70. async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
  71. headers = {
  72. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
  73. }
  74. response = await client.get(url, headers=headers)
  75. response.raise_for_status()
  76. content_type = response.headers.get("content-type", "").lower()
  77. # 如果是 PDF 或其他二进制文件
  78. binary_extensions = {
  79. ".pdf": "application/pdf",
  80. ".png": "image/png",
  81. ".jpg": "image/jpeg",
  82. ".jpeg": "image/jpeg",
  83. ".gif": "image/gif",
  84. ".doc": "application/msword",
  85. ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  86. ".xls": "application/vnd.ms-excel",
  87. ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  88. ".ppt": "application/vnd.ms-powerpoint",
  89. ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  90. ".zip": "application/zip",
  91. ".rar": "application/x-rar-compressed",
  92. ".7z": "application/x-7z-compressed"
  93. }
  94. is_binary = "application/pdf" in content_type or \
  95. "application/vnd." in content_type or \
  96. "application/msword" in content_type or \
  97. "application/octet-stream" in content_type or \
  98. any(ext in url.lower() for ext in binary_extensions.keys())
  99. if is_binary:
  100. # 尝试根据扩展名修正 media_type
  101. for ext, m_type in binary_extensions.items():
  102. if ext in url.lower():
  103. content_type = m_type
  104. break
  105. return Response(
  106. content=response.content,
  107. media_type=content_type,
  108. headers={"Content-Disposition": "inline"}
  109. )
  110. # 默认处理为 HTML
  111. try:
  112. # 尝试多种编码解码 content
  113. data = response.content
  114. content = None
  115. encodings = ['utf-8', 'gbk', 'utf-8-sig', 'gb18030']
  116. for enc in encodings:
  117. try:
  118. content = data.decode(enc)
  119. break
  120. except UnicodeDecodeError:
  121. continue
  122. if content is None:
  123. content = data.decode('utf-8', errors='ignore')
  124. # 简单的注入一些基础样式,确保内容在 iframe 中显示良好
  125. base_style = """
  126. <style>
  127. body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 20px; line-height: 1.6; color: #333; }
  128. img { max-width: 100%; height: auto; }
  129. </style>
  130. """
  131. if "</head>" in content:
  132. content = content.replace("</head>", f"{base_style}</head>")
  133. else:
  134. content = f"{base_style}{content}"
  135. return HTMLResponse(content=content)
  136. except Exception:
  137. # 如果文本解析失败,返回原始字节
  138. return Response(content=response.content, media_type=content_type)
  139. except Exception as e:
  140. error_msg = f"<html><body><h3>无法加载内容</h3><p>错误原因: {str(e)}</p><p>URL: {url}</p></body></html>"
  141. return HTMLResponse(content=error_msg, status_code=500)
  142. @router.get("/documents/download")
  143. async def download_document(url: str, filename: Optional[str] = None, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
  144. """代理下载云端文件,支持从 MinIO 等外部地址下载"""
  145. try:
  146. if not url:
  147. return ApiResponse(code=400, message="缺少URL参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  148. # 确保 URL 已解码
  149. url = urllib.parse.unquote(url)
  150. # 优先从 Header 获取,如果没有则从参数获取
  151. actual_token = None
  152. if credentials:
  153. actual_token = credentials.credentials
  154. elif token:
  155. actual_token = token
  156. if not actual_token:
  157. return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  158. payload = verify_token(actual_token)
  159. if not payload or not payload.get("is_superuser"):
  160. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  161. # 增加超时时间,支持大文件下载
  162. async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
  163. headers = {
  164. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
  165. }
  166. response = await client.get(url, headers=headers)
  167. response.raise_for_status()
  168. content_type = response.headers.get("content-type", "application/octet-stream")
  169. # 设置下载文件名
  170. headers = {
  171. "Content-Disposition": f"attachment; filename*=UTF-8''{urllib.parse.quote(filename or 'downloaded_file')}" if filename else "attachment",
  172. "Content-Type": content_type
  173. }
  174. return Response(
  175. content=response.content,
  176. media_type=content_type,
  177. headers=headers
  178. )
  179. except Exception as e:
  180. logger.exception(f"文件下载失败, url={url}")
  181. return ApiResponse(code=500, message=f"下载失败: {str(e)} (URL: {url})", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  182. @router.post("/documents/batch-enter")
  183. async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  184. """批量将文档加入知识库"""
  185. try:
  186. payload = verify_token(credentials.credentials)
  187. if not payload or not payload.get("is_superuser"):
  188. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  189. username = payload.get("sub")
  190. if not username:
  191. return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  192. sample_service = SampleService()
  193. success_count, message = await sample_service.batch_enter_knowledge_base(
  194. req.ids,
  195. username,
  196. kb_method=req.kb_method,
  197. chunk_size=req.chunk_size,
  198. separator=req.separator
  199. )
  200. # 如果全部失败,返回非零状态码,触发前端错误提示
  201. code = 0 if success_count > 0 else 1
  202. return ApiResponse(code=code, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  203. except Exception as e:
  204. logger.exception("批量操作失败")
  205. return ApiResponse(code=500, message=f"批量操作失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  206. @router.post("/documents/batch-delete")
  207. async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  208. """批量删除文档"""
  209. try:
  210. payload = verify_token(credentials.credentials)
  211. if not payload or not payload.get("is_superuser"):
  212. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  213. sample_service = SampleService()
  214. affected_rows, message = await sample_service.batch_delete_documents(req.ids)
  215. return ApiResponse(
  216. code=0,
  217. message=message,
  218. timestamp=datetime.now(timezone.utc).isoformat()
  219. ).model_dump()
  220. except Exception as e:
  221. logger.exception("批量删除失败")
  222. return ApiResponse(code=500, message=f"批量删除失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  223. @router.post("/documents/batch-add-to-task")
  224. async def batch_add_to_task(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  225. """批量加入任务中心 (设置 whether_to_task = 1)"""
  226. try:
  227. payload = verify_token(credentials.credentials)
  228. if not payload or not payload.get("is_superuser"):
  229. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  230. user_id = payload.get("sub")
  231. if not user_id:
  232. return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  233. username = payload.get("username", user_id)
  234. sample_service = SampleService()
  235. success, message = await sample_service.batch_add_to_task(req.ids, username)
  236. return ApiResponse(
  237. code=0 if success else 500,
  238. message=message,
  239. timestamp=datetime.now(timezone.utc).isoformat()
  240. ).model_dump()
  241. except Exception as e:
  242. logger.exception("批量加入任务失败")
  243. return ApiResponse(code=500, message=f"批量加入任务失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  244. @router.post("/documents/convert")
  245. async def convert_document(req: ConvertRequest, background_tasks: BackgroundTasks, credentials: HTTPAuthorizationCredentials = Depends(security)):
  246. """启动文档转换 (使用 MinerUManager 在后台执行)"""
  247. try:
  248. payload = verify_token(credentials.credentials)
  249. if not payload or not payload.get("is_superuser"):
  250. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  251. doc_id = str(req.id)
  252. sample_service = SampleService()
  253. # 1. 获取文档详情以取得 title 和 file_url
  254. doc = await sample_service.get_document_detail(doc_id)
  255. if not doc:
  256. return ApiResponse(code=404, message="文档不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  257. title = doc.get("title")
  258. file_url = doc.get("file_url")
  259. table_type = doc.get("source_type") # 获取业务模块前缀
  260. status = doc.get("conversion_status")
  261. # 2. 检查当前状态,避免重复请求
  262. if status == 1:
  263. return ApiResponse(code=0, message="文档正在转换中,请勿重复操作", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  264. if status == 2:
  265. return ApiResponse(code=0, message="文档已转换完成", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  266. if not file_url:
  267. return ApiResponse(code=400, message="文档缺少文件链接,无法转换", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  268. # 3. 立即将状态更新为“转换中”,避免前端轮询延迟
  269. await sample_service.update_conversion_status(doc_id, status=1)
  270. # 4. 启动后台任务
  271. manager = get_mineru_manager()
  272. background_tasks.add_task(manager.process_document, doc_id, title, file_url, table_type)
  273. return ApiResponse(
  274. code=0,
  275. message="转换任务已在后台启动",
  276. timestamp=datetime.now(timezone.utc).isoformat()
  277. ).model_dump()
  278. except Exception as e:
  279. logger.exception("启动转换失败")
  280. return ApiResponse(code=500, message=f"启动转换失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  281. @router.post("/documents/add")
  282. async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
  283. """添加新文档 (同步主表和子表)"""
  284. try:
  285. payload = verify_token(credentials.credentials)
  286. if not payload:
  287. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  288. user_id = payload.get("sub")
  289. if not user_id:
  290. return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  291. sample_service = SampleService()
  292. # 将 DocumentAdd 对象转换为字典,包含所有字段
  293. doc_data = doc.model_dump()
  294. success, message, doc_id = await sample_service.add_document(doc_data, user_id)
  295. if success:
  296. return ApiResponse(code=0, message=message, data={"id": doc_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  297. else:
  298. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  299. except Exception as e:
  300. logger.exception("添加文档失败")
  301. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  302. @router.get("/documents/detail/{doc_id}")
  303. async def get_document_detail(doc_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  304. """获取文档详情 (关联查询子表)"""
  305. try:
  306. payload = verify_token(credentials.credentials)
  307. if not payload:
  308. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  309. sample_service = SampleService()
  310. doc = await sample_service.get_document_detail(doc_id)
  311. if not doc:
  312. return ApiResponse(code=404, message="文档不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  313. return ApiResponse(code=0, message="获取详情成功", data=doc, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  314. except Exception as e:
  315. logger.exception("获取文档详情失败")
  316. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  317. @router.get("/documents/list")
  318. async def get_document_list(
  319. whether_to_enter: Optional[int] = None,
  320. conversion_status: Optional[int] = None,
  321. keyword: Optional[str] = None,
  322. table_type: Optional[str] = None,
  323. plan_category: Optional[str] = None,
  324. level_2_classification: Optional[str] = None,
  325. level_3_classification: Optional[str] = None,
  326. level_4_classification: Optional[str] = None,
  327. page: int = 1,
  328. size: int = 50,
  329. credentials: HTTPAuthorizationCredentials = Depends(security)
  330. ):
  331. """获取文档列表 (从主表查询)"""
  332. try:
  333. payload = verify_token(credentials.credentials)
  334. if not payload:
  335. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  336. sample_service = SampleService()
  337. items, total, all_total, total_entered = await sample_service.get_document_list(
  338. whether_to_enter=whether_to_enter,
  339. conversion_status=conversion_status,
  340. keyword=keyword,
  341. table_type=table_type,
  342. plan_category=plan_category,
  343. level_2_classification=level_2_classification,
  344. level_3_classification=level_3_classification,
  345. level_4_classification=level_4_classification,
  346. page=page,
  347. size=size
  348. )
  349. return ApiResponse(
  350. code=0,
  351. message="查询成功",
  352. data={
  353. "items": items,
  354. "total": total,
  355. "page": page,
  356. "size": size,
  357. "all_total": all_total,
  358. "total_entered": total_entered
  359. },
  360. timestamp=datetime.now(timezone.utc).isoformat()
  361. ).model_dump()
  362. except Exception as e:
  363. logger.exception("获取文档列表失败")
  364. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  365. @router.post("/documents/edit")
  366. async def edit_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
  367. """编辑文档 (同步主表和子表)"""
  368. try:
  369. payload = verify_token(credentials.credentials)
  370. if not payload:
  371. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  372. if not doc.id:
  373. return ApiResponse(code=400, message="缺少ID参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  374. # 调用 service 层
  375. sample_service = SampleService()
  376. # 获取更新人ID
  377. updater_id = payload.get("sub", "admin")
  378. # 将 DocumentAdd 对象转换为字典,包含所有字段
  379. doc_data = doc.model_dump()
  380. success, message = await sample_service.edit_document(doc_data, updater_id)
  381. if success:
  382. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  383. else:
  384. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  385. except Exception as e:
  386. logger.exception("编辑文档失败")
  387. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  388. @router.post("/documents/enter")
  389. async def enter_document(data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
  390. """文档入库"""
  391. try:
  392. doc_id = data.get("id")
  393. if not doc_id:
  394. return ApiResponse(code=400, message="缺少ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  395. payload = verify_token(credentials.credentials)
  396. username = payload.get("sub", "admin") if payload else "admin"
  397. # 调用 service 层
  398. sample_service = SampleService()
  399. success, message = await sample_service.enter_document(doc_id, username)
  400. if success:
  401. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  402. else:
  403. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  404. except Exception as e:
  405. logger.exception("入库失败")
  406. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  407. @router.get("/basic-info/list")
  408. async def get_basic_info_list(
  409. type: str,
  410. page: int = 1,
  411. size: int = 50,
  412. keyword: Optional[str] = None,
  413. title: Optional[str] = None,
  414. standard_no: Optional[str] = None,
  415. document_type: Optional[str] = None,
  416. professional_field: Optional[str] = None,
  417. validity: Optional[str] = None,
  418. issuing_authority: Optional[str] = None,
  419. release_date_start: Optional[str] = None,
  420. release_date_end: Optional[str] = None,
  421. plan_category: Optional[str] = None,
  422. level_1_classification: Optional[str] = None,
  423. level_2_classification: Optional[str] = None,
  424. level_3_classification: Optional[str] = None,
  425. level_4_classification: Optional[str] = None,
  426. credentials: HTTPAuthorizationCredentials = Depends(security)
  427. ):
  428. """获取基本信息列表 (支持多条件检索)"""
  429. try:
  430. payload = verify_token(credentials.credentials)
  431. if not payload:
  432. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  433. sample_service = SampleService()
  434. # 构建过滤条件
  435. filters = {}
  436. if title:
  437. filters['title'] = title
  438. if standard_no:
  439. filters['standard_no'] = standard_no
  440. if document_type:
  441. filters['document_type'] = document_type
  442. if professional_field:
  443. filters['professional_field'] = professional_field
  444. if validity:
  445. filters['validity'] = validity
  446. if issuing_authority:
  447. filters['issuing_authority'] = issuing_authority
  448. if release_date_start:
  449. filters['release_date_start'] = release_date_start
  450. if release_date_end:
  451. filters['release_date_end'] = release_date_end
  452. if plan_category:
  453. filters['plan_category'] = plan_category
  454. if level_1_classification:
  455. filters['level_1_classification'] = level_1_classification
  456. if level_2_classification:
  457. filters['level_2_classification'] = level_2_classification
  458. if level_3_classification:
  459. filters['level_3_classification'] = level_3_classification
  460. if level_4_classification:
  461. filters['level_4_classification'] = level_4_classification
  462. items, total = await sample_service.get_basic_info_list(
  463. type=type,
  464. page=page,
  465. size=size,
  466. keyword=keyword,
  467. **filters
  468. )
  469. return ApiResponse(
  470. code=0,
  471. message="查询成功",
  472. data={"items": items, "total": total, "page": page, "size": size},
  473. timestamp=datetime.now(timezone.utc).isoformat()
  474. ).model_dump()
  475. except Exception as e:
  476. logger.exception("查询基本信息失败")
  477. return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  478. @router.post("/basic-info/add")
  479. async def add_basic_info(type: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
  480. """新增基本信息"""
  481. try:
  482. payload = verify_token(credentials.credentials)
  483. if not payload:
  484. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  485. user_id = payload.get("sub")
  486. if not user_id:
  487. return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  488. sample_service = SampleService()
  489. success, message, doc_id = await sample_service.add_basic_info(type, data, user_id)
  490. if success:
  491. return ApiResponse(code=0, message=message, data={"id": doc_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  492. else:
  493. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  494. except Exception as e:
  495. logger.exception("新增基本信息失败")
  496. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  497. @router.post("/basic-info/edit")
  498. async def edit_basic_info(type: str, id: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
  499. """编辑基本信息"""
  500. try:
  501. payload = verify_token(credentials.credentials)
  502. if not payload:
  503. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  504. user_id = payload.get("sub")
  505. if not user_id:
  506. return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  507. sample_service = SampleService()
  508. success, message = await sample_service.edit_basic_info(type, id, data, user_id)
  509. if success:
  510. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  511. else:
  512. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  513. except Exception as e:
  514. logger.exception("编辑基本信息失败")
  515. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  516. @router.post("/basic-info/delete")
  517. async def delete_basic_info(type: str, id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  518. """删除基本信息"""
  519. try:
  520. payload = verify_token(credentials.credentials)
  521. if not payload:
  522. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  523. sample_service = SampleService()
  524. success, message = await sample_service.delete_basic_info(type, id)
  525. if success:
  526. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  527. else:
  528. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  529. except Exception as e:
  530. logger.exception("删除基本信息失败")
  531. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  532. @router.get("/documents/categories/primary")
  533. async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
  534. """获取所有一级分类(仅保留指定的分类)"""
  535. try:
  536. payload = verify_token(credentials.credentials)
  537. if not payload or not payload.get("is_superuser"):
  538. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  539. # 仅保留用户要求的分类
  540. default_categories = ["办公制度", "行业标准", "法律法规", "施工方案", "施工图片"]
  541. categories = [{"id": name, "name": name} for name in default_categories]
  542. return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  543. except Exception as e:
  544. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  545. @router.get("/documents/categories/secondary")
  546. async def get_secondary_categories(primaryId: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  547. """根据一级分类获取二级分类(仅保留指定的分类)"""
  548. try:
  549. payload = verify_token(credentials.credentials)
  550. if not payload or not payload.get("is_superuser"):
  551. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  552. # 针对“办公制度”的预设二级分类,其他分类暂时没有二级分类
  553. categories = []
  554. if primaryId == "办公制度":
  555. secondary_names = ["采购", "报销", "审批"]
  556. categories = [{"id": name, "name": name} for name in secondary_names]
  557. return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  558. except Exception as e:
  559. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  560. @router.get("/documents/search")
  561. async def search_documents(
  562. keyword: str,
  563. whether_to_enter: Optional[int] = None,
  564. table_type: Optional[str] = "standard",
  565. page: int = 1,
  566. size: int = 50,
  567. credentials: HTTPAuthorizationCredentials = Depends(security)
  568. ):
  569. """关键词搜索文档,统一调用 get_document_list 以支持组合过滤"""
  570. return await get_document_list(
  571. whether_to_enter=whether_to_enter,
  572. keyword=keyword,
  573. table_type=table_type,
  574. page=page,
  575. size=size,
  576. credentials=credentials
  577. )