sample_view.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  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_tasks(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)
  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("username", "admin")
  190. sample_service = SampleService()
  191. affected_rows, message = await sample_service.batch_enter_knowledge_base(req.ids, username)
  192. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  193. except Exception as e:
  194. logger.exception("批量操作失败")
  195. return ApiResponse(code=500, message=f"批量操作失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  196. @router.post("/documents/batch-delete")
  197. async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  198. """批量删除文档"""
  199. try:
  200. payload = verify_token(credentials.credentials)
  201. if not payload or not payload.get("is_superuser"):
  202. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  203. sample_service = SampleService()
  204. affected_rows, message = await sample_service.batch_delete_documents(req.ids)
  205. return ApiResponse(
  206. code=0,
  207. message=message,
  208. timestamp=datetime.now(timezone.utc).isoformat()
  209. ).model_dump()
  210. except Exception as e:
  211. logger.exception("批量删除失败")
  212. return ApiResponse(code=500, message=f"批量删除失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  213. async def simulate_conversion(doc_id: str):
  214. """模拟文档转换过程 (仅保留状态切换)"""
  215. sample_service = SampleService()
  216. try:
  217. # 1. 模拟开始
  218. await sample_service.update_conversion_status(doc_id, status=1)
  219. await asyncio.sleep(2)
  220. # 2. 模拟完成
  221. converted_file_name = f"http://192.168.91.15:19000/aidata/sampledata/converted/simulated/{doc_id}.pdf"
  222. json_url = f"http://192.168.91.15:19000/aidata/sampledata/converted/simulated/{doc_id}.json"
  223. await sample_service.update_conversion_status(doc_id, status=2,
  224. md_url=converted_file_name,
  225. json_url=json_url)
  226. except Exception as e:
  227. logger.exception("模拟转换出错")
  228. await sample_service.update_conversion_status(doc_id, status=3,
  229. error_message=str(e))
  230. @router.post("/documents/convert")
  231. async def convert_document(req: ConvertRequest, background_tasks: BackgroundTasks, credentials: HTTPAuthorizationCredentials = Depends(security)):
  232. """启动文档转换 (使用 MinerUManager 在后台执行)"""
  233. try:
  234. payload = verify_token(credentials.credentials)
  235. if not payload or not payload.get("is_superuser"):
  236. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  237. doc_id = str(req.id)
  238. sample_service = SampleService()
  239. # 1. 获取文档详情以取得 title 和 file_url
  240. doc = await sample_service.get_document_detail(doc_id)
  241. if not doc:
  242. return ApiResponse(code=404, message="文档不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  243. title = doc.get("title")
  244. file_url = doc.get("file_url")
  245. # 如果主表没有 file_url,尝试从子表获取的逻辑已在 MinerUManager 或 service 中处理?
  246. # 其实 MinerUManager.process_document 需要 file_url。
  247. # 这里的 doc 是 detail,已经包含了子表关联。
  248. if not file_url:
  249. return ApiResponse(code=400, message="文档缺少文件链接,无法转换", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  250. # 2. 立即将状态更新为“转换中”,避免前端轮询延迟
  251. await sample_service.update_conversion_status(doc_id, status=1)
  252. # 3. 启动后台任务
  253. manager = get_mineru_manager()
  254. background_tasks.add_task(manager.process_document, doc_id, title, file_url)
  255. return ApiResponse(
  256. code=0,
  257. message="转换任务已在后台启动",
  258. timestamp=datetime.now(timezone.utc).isoformat()
  259. ).model_dump()
  260. except Exception as e:
  261. logger.exception("启动转换失败")
  262. return ApiResponse(code=500, message=f"启动转换失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  263. @router.post("/documents/add")
  264. async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
  265. """添加新文档 (同步主表和子表)"""
  266. try:
  267. payload = verify_token(credentials.credentials)
  268. if not payload:
  269. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  270. user_id = payload.get("username", "admin")
  271. sample_service = SampleService()
  272. # 将 DocumentAdd 对象转换为字典
  273. doc_data = {
  274. 'title': doc.title,
  275. 'note': doc.note,
  276. 'table_type': doc.table_type,
  277. 'primary_category_id': doc.primary_category_id,
  278. 'secondary_category_id': doc.secondary_category_id,
  279. 'year': doc.year,
  280. 'file_url': doc.file_url,
  281. 'json_url': doc.json_url,
  282. 'file_extension': doc.file_extension,
  283. 'standard_no': doc.standard_no,
  284. 'issuing_authority': doc.issuing_authority,
  285. 'release_date': doc.release_date,
  286. 'document_type': doc.document_type,
  287. 'professional_field': doc.professional_field,
  288. 'validity': doc.validity,
  289. 'project_name': doc.project_name,
  290. 'project_section': doc.project_section,
  291. 'compilation_basis': doc.compilation_basis,
  292. 'plan_summary': doc.plan_summary,
  293. 'plan_category': doc.plan_category,
  294. 'level_1_classification': doc.level_1_classification,
  295. 'level_2_classification': doc.level_2_classification,
  296. 'level_3_classification': doc.level_3_classification,
  297. 'level_4_classification': doc.level_4_classification
  298. }
  299. success, message, doc_id = await sample_service.add_document(doc_data, user_id)
  300. if success:
  301. return ApiResponse(code=0, message=message, data={"id": doc_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  302. else:
  303. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  304. except Exception as e:
  305. logger.exception("添加文档失败")
  306. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  307. @router.get("/documents/detail/{doc_id}")
  308. async def get_document_detail(doc_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  309. """获取文档详情 (关联查询子表)"""
  310. logger.info(f"正在获取文档详情: {doc_id}")
  311. try:
  312. payload = verify_token(credentials.credentials)
  313. if not payload:
  314. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  315. sample_service = SampleService()
  316. doc = await sample_service.get_document_detail(doc_id)
  317. if not doc:
  318. logger.warning(f"文档不存在: {doc_id}")
  319. return ApiResponse(code=404, message="文档不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  320. logger.info(f"找到文档数据: {doc.get('title')}")
  321. return ApiResponse(code=0, message="获取详情成功", data=doc, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  322. except Exception as e:
  323. logger.exception("获取文档详情失败")
  324. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  325. @router.get("/documents/list")
  326. async def get_document_list(
  327. whether_to_enter: Optional[int] = None,
  328. keyword: Optional[str] = None,
  329. table_type: Optional[str] = None,
  330. plan_category: Optional[str] = None,
  331. level_2_classification: Optional[str] = None,
  332. level_3_classification: Optional[str] = None,
  333. level_4_classification: Optional[str] = None,
  334. page: int = 1,
  335. size: int = 50,
  336. credentials: HTTPAuthorizationCredentials = Depends(security)
  337. ):
  338. """获取文档列表 (从主表查询)"""
  339. try:
  340. payload = verify_token(credentials.credentials)
  341. if not payload:
  342. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  343. sample_service = SampleService()
  344. items, total, all_total, total_entered = await sample_service.get_document_list(
  345. whether_to_enter=whether_to_enter,
  346. keyword=keyword,
  347. table_type=table_type,
  348. plan_category=plan_category,
  349. level_2_classification=level_2_classification,
  350. level_3_classification=level_3_classification,
  351. level_4_classification=level_4_classification,
  352. page=page,
  353. size=size
  354. )
  355. return ApiResponse(
  356. code=0,
  357. message="查询成功",
  358. data={
  359. "items": items,
  360. "total": total,
  361. "page": page,
  362. "size": size,
  363. "all_total": all_total,
  364. "total_entered": total_entered
  365. },
  366. timestamp=datetime.now(timezone.utc).isoformat()
  367. ).model_dump()
  368. except Exception as e:
  369. logger.exception("获取文档列表失败")
  370. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  371. @router.post("/documents/edit")
  372. async def edit_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
  373. """编辑文档 (同步主表和子表)"""
  374. try:
  375. payload = verify_token(credentials.credentials)
  376. if not payload:
  377. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  378. if not doc.id:
  379. return ApiResponse(code=400, message="缺少ID参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  380. # 调用 service 层
  381. sample_service = SampleService()
  382. # 获取更新人ID
  383. updater_id = payload.get("username", "admin")
  384. # 将 DocumentAdd 对象转换为字典
  385. doc_data = {
  386. 'id': doc.id,
  387. 'title': doc.title,
  388. 'note': doc.note,
  389. 'table_type': doc.table_type,
  390. 'primary_category_id': doc.primary_category_id,
  391. 'secondary_category_id': doc.secondary_category_id,
  392. 'year': doc.year,
  393. 'file_url': doc.file_url,
  394. 'json_url': doc.json_url,
  395. 'file_extension': doc.file_extension,
  396. 'standard_no': doc.standard_no if hasattr(doc, 'standard_no') else None,
  397. 'issuing_authority': doc.issuing_authority if hasattr(doc, 'issuing_authority') else None,
  398. 'release_date': doc.release_date if hasattr(doc, 'release_date') else None,
  399. 'document_type': doc.document_type if hasattr(doc, 'document_type') else None,
  400. 'professional_field': doc.professional_field if hasattr(doc, 'professional_field') else None,
  401. 'validity': doc.validity if hasattr(doc, 'validity') else None,
  402. 'project_name': doc.project_name if hasattr(doc, 'project_name') else None,
  403. 'project_section': doc.project_section if hasattr(doc, 'project_section') else None,
  404. 'compilation_basis': doc.compilation_basis if hasattr(doc, 'compilation_basis') else None,
  405. 'plan_summary': doc.plan_summary if hasattr(doc, 'plan_summary') else None,
  406. 'plan_category': doc.plan_category if hasattr(doc, 'plan_category') else None,
  407. 'level_1_classification': doc.level_1_classification if hasattr(doc, 'level_1_classification') else None,
  408. 'level_2_classification': doc.level_2_classification if hasattr(doc, 'level_2_classification') else None,
  409. 'level_3_classification': doc.level_3_classification if hasattr(doc, 'level_3_classification') else None,
  410. 'level_4_classification': doc.level_4_classification if hasattr(doc, 'level_4_classification') else None
  411. }
  412. success, message = await sample_service.edit_document(doc_data, updater_id)
  413. if success:
  414. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  415. else:
  416. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  417. except Exception as e:
  418. logger.exception("编辑文档失败")
  419. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  420. @router.post("/documents/enter")
  421. async def enter_document(data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
  422. """文档入库"""
  423. try:
  424. doc_id = data.get("id")
  425. if not doc_id:
  426. return ApiResponse(code=400, message="缺少ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  427. payload = verify_token(credentials.credentials)
  428. username = payload.get("username", "admin") if payload else "admin"
  429. # 调用 service 层
  430. sample_service = SampleService()
  431. success, message = await sample_service.enter_document(doc_id, username)
  432. if success:
  433. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  434. else:
  435. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  436. except Exception as e:
  437. logger.exception("入库失败")
  438. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  439. @router.get("/basic-info/list")
  440. async def get_basic_info_list(
  441. type: str,
  442. page: int = 1,
  443. size: int = 50,
  444. keyword: Optional[str] = None,
  445. title: Optional[str] = None,
  446. standard_no: Optional[str] = None,
  447. document_type: Optional[str] = None,
  448. professional_field: Optional[str] = None,
  449. validity: Optional[str] = None,
  450. issuing_authority: Optional[str] = None,
  451. release_date_start: Optional[str] = None,
  452. release_date_end: Optional[str] = None,
  453. plan_category: Optional[str] = None,
  454. level_1_classification: Optional[str] = None,
  455. level_2_classification: Optional[str] = None,
  456. level_3_classification: Optional[str] = None,
  457. level_4_classification: Optional[str] = None,
  458. credentials: HTTPAuthorizationCredentials = Depends(security)
  459. ):
  460. """获取基本信息列表 (支持多条件检索)"""
  461. try:
  462. payload = verify_token(credentials.credentials)
  463. if not payload:
  464. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  465. sample_service = SampleService()
  466. # 构建过滤条件
  467. filters = {}
  468. if title:
  469. filters['title'] = title
  470. if standard_no:
  471. filters['standard_no'] = standard_no
  472. if document_type:
  473. filters['document_type'] = document_type
  474. if professional_field:
  475. filters['professional_field'] = professional_field
  476. if validity:
  477. filters['validity'] = validity
  478. if issuing_authority:
  479. filters['issuing_authority'] = issuing_authority
  480. if release_date_start:
  481. filters['release_date_start'] = release_date_start
  482. if release_date_end:
  483. filters['release_date_end'] = release_date_end
  484. if plan_category:
  485. filters['plan_category'] = plan_category
  486. if level_1_classification:
  487. filters['level_1_classification'] = level_1_classification
  488. if level_2_classification:
  489. filters['level_2_classification'] = level_2_classification
  490. if level_3_classification:
  491. filters['level_3_classification'] = level_3_classification
  492. if level_4_classification:
  493. filters['level_4_classification'] = level_4_classification
  494. items, total = await sample_service.get_basic_info_list(
  495. type=type,
  496. page=page,
  497. size=size,
  498. keyword=keyword,
  499. **filters
  500. )
  501. return ApiResponse(
  502. code=0,
  503. message="查询成功",
  504. data={"items": items, "total": total, "page": page, "size": size},
  505. timestamp=datetime.now(timezone.utc).isoformat()
  506. ).model_dump()
  507. except Exception as e:
  508. logger.exception("查询基本信息失败")
  509. return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  510. @router.post("/basic-info/add")
  511. async def add_basic_info(type: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
  512. """新增基本信息"""
  513. try:
  514. payload = verify_token(credentials.credentials)
  515. if not payload:
  516. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  517. user_id = payload.get("username", "admin")
  518. sample_service = SampleService()
  519. success, message, doc_id = await sample_service.add_basic_info(type, data, user_id)
  520. if success:
  521. return ApiResponse(code=0, message=message, data={"id": doc_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  522. else:
  523. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  524. except Exception as e:
  525. logger.exception("新增基本信息失败")
  526. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  527. @router.post("/basic-info/edit")
  528. async def edit_basic_info(type: str, id: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
  529. """编辑基本信息"""
  530. try:
  531. payload = verify_token(credentials.credentials)
  532. if not payload:
  533. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  534. sample_service = SampleService()
  535. success, message = await sample_service.edit_basic_info(type, id, data, payload.get("username", "admin"))
  536. if success:
  537. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  538. else:
  539. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  540. except Exception as e:
  541. logger.exception("编辑基本信息失败")
  542. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  543. @router.post("/basic-info/delete")
  544. async def delete_basic_info(type: str, id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  545. """删除基本信息"""
  546. try:
  547. payload = verify_token(credentials.credentials)
  548. if not payload:
  549. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  550. sample_service = SampleService()
  551. success, message = await sample_service.delete_basic_info(type, id)
  552. if success:
  553. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  554. else:
  555. return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  556. except Exception as e:
  557. logger.exception("删除基本信息失败")
  558. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  559. @router.get("/documents/categories/primary")
  560. async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
  561. """获取所有一级分类(仅保留指定的分类)"""
  562. try:
  563. payload = verify_token(credentials.credentials)
  564. if not payload or not payload.get("is_superuser"):
  565. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  566. # 仅保留用户要求的分类
  567. default_categories = ["办公制度", "行业标准", "法律法规", "施工方案", "施工图片"]
  568. categories = [{"id": name, "name": name} for name in default_categories]
  569. return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  570. except Exception as e:
  571. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  572. @router.get("/documents/categories/secondary")
  573. async def get_secondary_categories(primaryId: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  574. """根据一级分类获取二级分类(仅保留指定的分类)"""
  575. try:
  576. payload = verify_token(credentials.credentials)
  577. if not payload or not payload.get("is_superuser"):
  578. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  579. # 针对“办公制度”的预设二级分类,其他分类暂时没有二级分类
  580. categories = []
  581. if primaryId == "办公制度":
  582. secondary_names = ["采购", "报销", "审批"]
  583. categories = [{"id": name, "name": name} for name in secondary_names]
  584. return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  585. except Exception as e:
  586. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  587. @router.get("/documents/search")
  588. async def search_documents(
  589. keyword: str,
  590. whether_to_enter: Optional[int] = None,
  591. table_type: Optional[str] = "basis",
  592. page: int = 1,
  593. size: int = 50,
  594. credentials: HTTPAuthorizationCredentials = Depends(security)
  595. ):
  596. """关键词搜索文档,统一调用 get_document_list 以支持组合过滤"""
  597. return await get_document_list(
  598. whether_to_enter=whether_to_enter,
  599. keyword=keyword,
  600. table_type=table_type,
  601. page=page,
  602. size=size,
  603. credentials=credentials
  604. )