|
|
@@ -15,11 +15,11 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
|
|
from dotenv import load_dotenv
|
|
|
load_dotenv()
|
|
|
|
|
|
-from fastapi import FastAPI, HTTPException, Depends, Request
|
|
|
+from fastapi import FastAPI, HTTPException, Depends, Request, Response
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
|
from pydantic import BaseModel
|
|
|
-from typing import Optional, Any
|
|
|
+from typing import Optional, Any, Union
|
|
|
import hashlib
|
|
|
import secrets
|
|
|
# 修复JWT导入 - 确保使用正确的JWT库
|
|
|
@@ -50,7 +50,7 @@ except (ImportError, AttributeError, TypeError) as e:
|
|
|
print(f"❌ PyJWT安装失败: {install_error}")
|
|
|
raise ImportError("无法导入JWT库,请手动安装: pip install PyJWT")
|
|
|
|
|
|
-from datetime import datetime, timedelta, timezone
|
|
|
+from datetime import datetime, timedelta, timezone, date
|
|
|
import pymysql
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
@@ -87,6 +87,33 @@ class ApiResponse(BaseModel):
|
|
|
data: Optional[Any] = None
|
|
|
timestamp: str
|
|
|
|
|
|
+# 文档管理数据模型
|
|
|
+# --- 文档中心配置 ---
|
|
|
+TABLE_MAP = {
|
|
|
+ "basis": "t_basis_of_preparation", # 编制依据
|
|
|
+ "work": "t_work_of_preparation", # 施工方案
|
|
|
+ "job": "t_job_of_preparation" # 办公制度
|
|
|
+}
|
|
|
+
|
|
|
+def get_table_name(table_type: Optional[str]) -> str:
|
|
|
+ """根据类型获取对应的数据库表名,默认为编制依据"""
|
|
|
+ return TABLE_MAP.get(table_type, "t_basis_of_preparation")
|
|
|
+
|
|
|
+class DocumentAdd(BaseModel):
|
|
|
+ title: str
|
|
|
+ content: str
|
|
|
+ primary_category_id: Optional[Any] = None
|
|
|
+ secondary_category_id: Optional[Any] = None
|
|
|
+ year: Optional[int] = None
|
|
|
+ table_type: Optional[str] = "basis" # 增加表类型参数
|
|
|
+
|
|
|
+class DocumentListRequest(BaseModel):
|
|
|
+ primaryCategoryId: Optional[int] = None
|
|
|
+ secondaryCategoryId: Optional[int] = None
|
|
|
+ page: int = 1
|
|
|
+ size: int = 50
|
|
|
+ sort_by: str = "created_at" # created_at or updated_at
|
|
|
+
|
|
|
# 配置
|
|
|
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-jwt-secret-key-12345")
|
|
|
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
|
|
|
@@ -176,6 +203,7 @@ app.add_middleware(
|
|
|
)
|
|
|
|
|
|
security = HTTPBearer()
|
|
|
+security_optional = HTTPBearer(auto_error=False)
|
|
|
|
|
|
@app.get("/")
|
|
|
async def root():
|
|
|
@@ -210,6 +238,8 @@ async def login(request: Request, login_data: LoginRequest):
|
|
|
"""用户登录"""
|
|
|
print(f"🔐 收到登录请求: username={login_data.username}")
|
|
|
|
|
|
+ conn = None
|
|
|
+ cursor = None
|
|
|
try:
|
|
|
# 获取数据库连接
|
|
|
print("📊 尝试连接数据库...")
|
|
|
@@ -235,9 +265,6 @@ async def login(request: Request, login_data: LoginRequest):
|
|
|
user_data = cursor.fetchone()
|
|
|
print(f"👤 用户查询结果: {user_data is not None}")
|
|
|
|
|
|
- cursor.close()
|
|
|
- conn.close()
|
|
|
-
|
|
|
if not user_data:
|
|
|
print("❌ 用户不存在")
|
|
|
return ApiResponse(
|
|
|
@@ -307,6 +334,11 @@ async def login(request: Request, login_data: LoginRequest):
|
|
|
message="服务器内部错误",
|
|
|
timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
).model_dump()
|
|
|
+ finally:
|
|
|
+ if cursor:
|
|
|
+ cursor.close()
|
|
|
+ if conn:
|
|
|
+ conn.close()
|
|
|
|
|
|
@app.get("/api/v1/users/profile")
|
|
|
async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
@@ -2277,6 +2309,16 @@ async def api_get_user_menus(credentials: HTTPAuthorizationCredentials = Depends
|
|
|
|
|
|
menus = []
|
|
|
for row in cursor.fetchall():
|
|
|
+ menu_id = str(row[0])
|
|
|
+ menu_name = str(row[2])
|
|
|
+ menu_title = str(row[3])
|
|
|
+ menu_path = str(row[4])
|
|
|
+
|
|
|
+ # 只过滤掉明确不想要的“文档处理中心”
|
|
|
+ # 保留数据库中原本就有的“文档管理中心” (/admin/documents)
|
|
|
+ if "文档处理中心" in menu_title:
|
|
|
+ continue
|
|
|
+
|
|
|
menu = {
|
|
|
"id": row[0],
|
|
|
"parent_id": row[1],
|
|
|
@@ -2293,8 +2335,9 @@ async def api_get_user_menus(credentials: HTTPAuthorizationCredentials = Depends
|
|
|
}
|
|
|
menus.append(menu)
|
|
|
|
|
|
- # 构建菜单树
|
|
|
- menu_tree = build_menu_tree(menus)
|
|
|
+ # 构建菜单树前,过滤掉 button 类型的项,侧边栏只显示 menu 类型
|
|
|
+ sidebar_menus = [m for m in menus if m.get("menu_type") == "menu"]
|
|
|
+ menu_tree = build_menu_tree(sidebar_menus)
|
|
|
|
|
|
cursor.close()
|
|
|
conn.close()
|
|
|
@@ -3506,6 +3549,398 @@ async def get_all_roles_simple(credentials: HTTPAuthorizationCredentials = Depen
|
|
|
print(f"获取角色列表错误: {e}")
|
|
|
return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
|
|
|
+import httpx
|
|
|
+from fastapi.responses import HTMLResponse
|
|
|
+
|
|
|
+class BatchEnterRequest(BaseModel):
|
|
|
+ ids: list[int]
|
|
|
+ table_type: Optional[str] = "basis"
|
|
|
+
|
|
|
+class BatchDeleteRequest(BaseModel):
|
|
|
+ ids: list[Union[int, str]]
|
|
|
+ table_type: Optional[str] = "basis"
|
|
|
+
|
|
|
+class ConvertRequest(BaseModel):
|
|
|
+ id: Union[int, str]
|
|
|
+ table_type: Optional[str] = "basis"
|
|
|
+
|
|
|
+# --- 文档管理中心 API ---
|
|
|
+
|
|
|
+@app.get("/api/v1/documents/proxy-view")
|
|
|
+async def proxy_view(url: str, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
|
|
|
+ """抓取外部文档内容并返回,支持 HTML 和 PDF 等二进制文件。支持从 Header 或 Query 参数获取 Token。"""
|
|
|
+ try:
|
|
|
+ # 优先从 Header 获取,如果没有则从参数获取
|
|
|
+ actual_token = None
|
|
|
+ if credentials:
|
|
|
+ actual_token = credentials.credentials
|
|
|
+ elif token:
|
|
|
+ actual_token = token
|
|
|
+
|
|
|
+ if not actual_token:
|
|
|
+ return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ payload = verify_token(actual_token)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 增加超时时间,支持大文件下载
|
|
|
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
|
|
+ headers = {
|
|
|
+ "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"
|
|
|
+ }
|
|
|
+ response = await client.get(url, headers=headers)
|
|
|
+ response.raise_for_status()
|
|
|
+
|
|
|
+ content_type = response.headers.get("content-type", "").lower()
|
|
|
+
|
|
|
+ # 如果是 PDF 或其他二进制文件
|
|
|
+ if "application/pdf" in content_type or any(ext in url.lower() for ext in [".pdf", ".png", ".jpg", ".jpeg", ".gif"]):
|
|
|
+ return Response(
|
|
|
+ content=response.content,
|
|
|
+ media_type=content_type,
|
|
|
+ headers={"Content-Disposition": "inline"}
|
|
|
+ )
|
|
|
+
|
|
|
+ # 默认处理为 HTML
|
|
|
+ try:
|
|
|
+ content = response.text
|
|
|
+
|
|
|
+ # 简单的注入一些基础样式,确保内容在 iframe 中显示良好
|
|
|
+ base_style = """
|
|
|
+ <style>
|
|
|
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 20px; line-height: 1.6; color: #333; }
|
|
|
+ img { max-width: 100%; height: auto; }
|
|
|
+ </style>
|
|
|
+ """
|
|
|
+ if "</head>" in content:
|
|
|
+ content = content.replace("</head>", f"{base_style}</head>")
|
|
|
+ else:
|
|
|
+ content = f"{base_style}{content}"
|
|
|
+
|
|
|
+ return HTMLResponse(content=content)
|
|
|
+ except Exception:
|
|
|
+ # 如果文本解析失败,返回原始字节
|
|
|
+ return Response(content=response.content, media_type=content_type)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"<html><body><h3>无法加载内容</h3><p>错误原因: {str(e)}</p><p>URL: {url}</p></body></html>"
|
|
|
+ return HTMLResponse(content=error_msg, status_code=500)
|
|
|
+
|
|
|
+@app.post("/api/v1/documents/batch-enter")
|
|
|
+async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """批量将文档加入知识库"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+ table_name = get_table_name(req.table_type)
|
|
|
+ # 批量更新 whether_to_enter 为 1
|
|
|
+ # 只更新尚未入库的数据 (whether_to_enter = 0)
|
|
|
+ placeholders = ', '.join(['%s'] * len(req.ids))
|
|
|
+ sql = f"UPDATE {table_name} SET whether_to_enter = 1, updated_at = NOW() WHERE id IN ({placeholders}) AND whether_to_enter = 0"
|
|
|
+ cursor.execute(sql, req.ids)
|
|
|
+ conn.commit()
|
|
|
+
|
|
|
+ affected_rows = cursor.rowcount
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ message = f"成功将 {affected_rows} 条数据加入知识库"
|
|
|
+ if affected_rows < len(req.ids):
|
|
|
+ message += f"(跳过了 {len(req.ids) - affected_rows} 条已入库数据)"
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+ except Exception as e:
|
|
|
+ return ApiResponse(code=500, message=f"批量操作失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.post("/api/v1/documents/batch-delete")
|
|
|
+async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """批量删除文档"""
|
|
|
+ conn = None
|
|
|
+ cursor = None
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+ table_name = get_table_name(req.table_type)
|
|
|
+
|
|
|
+ if not req.ids:
|
|
|
+ return ApiResponse(code=400, message="未指定要删除的文档 ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ placeholders = ', '.join(['%s'] * len(req.ids))
|
|
|
+ sql = f"DELETE FROM {table_name} WHERE id IN ({placeholders})"
|
|
|
+ cursor.execute(sql, req.ids)
|
|
|
+ conn.commit()
|
|
|
+
|
|
|
+ affected_rows = cursor.rowcount
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message=f"成功删除 {affected_rows} 条文档数据",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+ except Exception as e:
|
|
|
+ print(f"批量删除失败: {e}")
|
|
|
+ return ApiResponse(code=500, message=f"批量删除失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+ finally:
|
|
|
+ if cursor:
|
|
|
+ cursor.close()
|
|
|
+ if conn:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+@app.post("/api/v1/documents/convert")
|
|
|
+async def convert_document(req: ConvertRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """异步启动文档转换"""
|
|
|
+ import subprocess
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 启动后台进程执行转换
|
|
|
+ # 脚本位于 d:\UGit\LQAdminPlatform\scripts\miner_u.py
|
|
|
+ script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "scripts", "miner_u.py"))
|
|
|
+ # 使用当前 python 解释器
|
|
|
+ python_exe = sys.executable
|
|
|
+
|
|
|
+ # 异步启动,不等待结束
|
|
|
+ subprocess.Popen([python_exe, script_path, str(req.table_type), str(req.id)],
|
|
|
+ stdout=subprocess.DEVNULL,
|
|
|
+ stderr=subprocess.DEVNULL,
|
|
|
+ creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="转换任务已启动",
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+ except Exception as e:
|
|
|
+ print(f"启动转换失败: {e}")
|
|
|
+ return ApiResponse(code=500, message=f"启动转换失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.post("/api/v1/documents/add")
|
|
|
+async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """添加新文档"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+ table_name = get_table_name(doc.table_type)
|
|
|
+ # 修正列名:reference_basis -> reference_basis_list
|
|
|
+ sql = f"""
|
|
|
+ INSERT INTO {table_name}
|
|
|
+ (chinese_name, reference_basis_list, document_type, professional_field, release_date, created_at, updated_at)
|
|
|
+ VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
|
|
|
+ """
|
|
|
+ # 构造日期:如果是年份,转为 YYYY-01-01
|
|
|
+ release_date = f"{doc.year}-01-01" if doc.year else None
|
|
|
+
|
|
|
+ cursor.execute(sql, (doc.title, doc.content, str(doc.primary_category_id) if doc.primary_category_id else None,
|
|
|
+ str(doc.secondary_category_id) if doc.secondary_category_id else None, release_date))
|
|
|
+ conn.commit()
|
|
|
+ cursor.close()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="文档添加成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+ except Exception as e:
|
|
|
+ print(f"添加文档错误: {e}")
|
|
|
+ return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.get("/api/v1/documents/list")
|
|
|
+async def get_document_list(
|
|
|
+ primaryCategoryId: Optional[str] = None,
|
|
|
+ secondaryCategoryId: Optional[str] = None,
|
|
|
+ year: Optional[int] = None,
|
|
|
+ whether_to_enter: Optional[int] = None,
|
|
|
+ keyword: Optional[str] = None,
|
|
|
+ table_type: Optional[str] = "basis",
|
|
|
+ page: int = 1,
|
|
|
+ size: int = 50,
|
|
|
+ sort_by: str = "created_at",
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """获取文档列表(支持过滤与搜索)"""
|
|
|
+ conn = None
|
|
|
+ cursor = None
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ if not conn:
|
|
|
+ return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ cursor = conn.cursor()
|
|
|
+ table_name = get_table_name(table_type)
|
|
|
+
|
|
|
+ where_clauses = []
|
|
|
+ params = []
|
|
|
+
|
|
|
+ if primaryCategoryId:
|
|
|
+ where_clauses.append("document_type = %s")
|
|
|
+ params.append(primaryCategoryId)
|
|
|
+ if secondaryCategoryId:
|
|
|
+ where_clauses.append("professional_field = %s")
|
|
|
+ params.append(secondaryCategoryId)
|
|
|
+ if year:
|
|
|
+ where_clauses.append("YEAR(release_date) = %s")
|
|
|
+ params.append(year)
|
|
|
+ if whether_to_enter is not None:
|
|
|
+ where_clauses.append("CAST(whether_to_enter AS UNSIGNED) = %s")
|
|
|
+ params.append(whether_to_enter)
|
|
|
+ if keyword:
|
|
|
+ where_clauses.append("(chinese_name LIKE %s OR reference_basis_list LIKE %s OR standard_no LIKE %s)")
|
|
|
+ like_keyword = f"%{keyword}%"
|
|
|
+ params.extend([like_keyword, like_keyword, like_keyword])
|
|
|
+
|
|
|
+ where_stmt = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
|
|
|
+
|
|
|
+ # 排序逻辑:按创建时间倒序
|
|
|
+ sort_field = "created_at" if sort_by == "created_at" else "updated_at"
|
|
|
+ order_by = f"ORDER BY {sort_field} DESC"
|
|
|
+
|
|
|
+ # 分页
|
|
|
+ offset = (page - 1) * size
|
|
|
+
|
|
|
+ # 返回更多字段
|
|
|
+ sql = f"""
|
|
|
+ SELECT id, chinese_name as title, reference_basis_list as content,
|
|
|
+ document_type, professional_field,
|
|
|
+ YEAR(release_date) as year, release_date, standard_no, status,
|
|
|
+ CAST(whether_to_enter AS UNSIGNED) as whether_to_enter, file_url,
|
|
|
+ conversion_status, conversion_progress, conversion_error,
|
|
|
+ created_at, updated_at
|
|
|
+ FROM {table_name} {where_stmt}
|
|
|
+ {order_by} LIMIT %s OFFSET %s
|
|
|
+ """
|
|
|
+ params.extend([size, offset])
|
|
|
+
|
|
|
+ cursor.execute(sql, params)
|
|
|
+ columns = [col[0] for col in cursor.description]
|
|
|
+ items = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
|
+
|
|
|
+ # 格式化时间
|
|
|
+ for item in items:
|
|
|
+ for key, value in item.items():
|
|
|
+ if isinstance(value, (datetime, date)):
|
|
|
+ item[key] = value.isoformat()
|
|
|
+
|
|
|
+ # 获取总数
|
|
|
+ count_sql = f"SELECT COUNT(*) FROM {table_name} {where_stmt}"
|
|
|
+ cursor.execute(count_sql, params[:-2])
|
|
|
+ total = cursor.fetchone()[0]
|
|
|
+
|
|
|
+ # 优化统计查询:合并全局总数和已入库总数的查询,减少数据库交互
|
|
|
+ stats_sql = f"SELECT COUNT(*), SUM(CASE WHEN CAST(whether_to_enter AS UNSIGNED) = 1 THEN 1 ELSE 0 END) FROM {table_name}"
|
|
|
+ cursor.execute(stats_sql)
|
|
|
+ stats_result = cursor.fetchone()
|
|
|
+
|
|
|
+ all_total = 0
|
|
|
+ total_entered = 0
|
|
|
+ if stats_result:
|
|
|
+ all_total = stats_result[0] or 0
|
|
|
+ total_entered = int(stats_result[1] or 0)
|
|
|
+
|
|
|
+ return ApiResponse(
|
|
|
+ code=0,
|
|
|
+ message="获取成功",
|
|
|
+ data={
|
|
|
+ "items": items,
|
|
|
+ "total": total,
|
|
|
+ "all_total": all_total,
|
|
|
+ "total_entered": total_entered,
|
|
|
+ "page": page,
|
|
|
+ "size": size
|
|
|
+ },
|
|
|
+ timestamp=datetime.now(timezone.utc).isoformat()
|
|
|
+ ).model_dump()
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取文档列表错误: {e}")
|
|
|
+ return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+ finally:
|
|
|
+ if cursor:
|
|
|
+ cursor.close()
|
|
|
+ if conn:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+@app.get("/api/v1/documents/categories/primary")
|
|
|
+async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """获取所有一级分类(仅保留指定的分类)"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 仅保留用户要求的分类
|
|
|
+ default_categories = ["办公制度", "行业标准", "法律法规", "施工方案", "施工图片"]
|
|
|
+ categories = [{"id": name, "name": name} for name in default_categories]
|
|
|
+ return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+ except Exception as e:
|
|
|
+ return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.get("/api/v1/documents/categories/secondary")
|
|
|
+async def get_secondary_categories(primaryId: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
|
+ """根据一级分类获取二级分类(仅保留指定的分类)"""
|
|
|
+ try:
|
|
|
+ payload = verify_token(credentials.credentials)
|
|
|
+ if not payload or not payload.get("is_superuser"):
|
|
|
+ return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+ # 针对“办公制度”的预设二级分类,其他分类暂时没有二级分类
|
|
|
+ categories = []
|
|
|
+ if primaryId == "办公制度":
|
|
|
+ secondary_names = ["采购", "报销", "审批"]
|
|
|
+ categories = [{"id": name, "name": name} for name in secondary_names]
|
|
|
+
|
|
|
+ return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+ except Exception as e:
|
|
|
+ return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
|
|
|
+
|
|
|
+@app.get("/api/v1/documents/search")
|
|
|
+async def search_documents(
|
|
|
+ keyword: str,
|
|
|
+ primaryCategoryId: Optional[str] = None,
|
|
|
+ secondaryCategoryId: Optional[str] = None,
|
|
|
+ year: Optional[int] = None,
|
|
|
+ whether_to_enter: Optional[int] = None,
|
|
|
+ table_type: Optional[str] = "basis",
|
|
|
+ page: int = 1,
|
|
|
+ size: int = 50,
|
|
|
+ credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
|
+):
|
|
|
+ """关键词搜索文档,统一调用 get_document_list 以支持组合过滤"""
|
|
|
+ return await get_document_list(
|
|
|
+ primaryCategoryId=primaryCategoryId,
|
|
|
+ secondaryCategoryId=secondaryCategoryId,
|
|
|
+ year=year,
|
|
|
+ whether_to_enter=whether_to_enter,
|
|
|
+ keyword=keyword,
|
|
|
+ table_type=table_type,
|
|
|
+ page=page,
|
|
|
+ size=size,
|
|
|
+ credentials=credentials
|
|
|
+ )
|
|
|
+
|
|
|
if __name__ == "__main__":
|
|
|
import uvicorn
|
|
|
|