瀏覽代碼

用户访问后token续签处理

lingmin_package@163.com 2 周之前
父節點
當前提交
fce2874667

+ 1 - 1
src/app/config/config.ini

@@ -37,7 +37,7 @@ REFRESH_TOKEN_EXPIRE_DAYS=30
 JWT_SECRET_KEY=dev-jwt-secret-key-change-in-production-12345678901234567890
 
 # 后台管理Token配置
-ADMIN_TOKEN_EXPIRE_MINUTES=60
+ADMIN_TOKEN_EXPIRE_MINUTES=3
 ADMIN_REFRESH_TOKEN_EXPIRE_HOURS=24
 
 # OAuth2配置

+ 3 - 2
src/app/middleware/token_refresh_middleware.py

@@ -56,6 +56,7 @@ class TokenRefreshMiddleware(BaseHTTPMiddleware):
             
             if not payload:
                 # Token无效,返回401错误
+                logger.warning(f"无效的访问令牌,路径: {request.url.path}")
                 return JSONResponse(
                     status_code=401,
                     content={
@@ -72,10 +73,10 @@ class TokenRefreshMiddleware(BaseHTTPMiddleware):
             if new_token:
                 response.headers["X-New-Token"] = new_token
                 response.headers["X-Token-Refreshed"] = "true"
-                logger.info(f"Token已刷新,用户: {payload.get('username', 'unknown')}")
+                logger.info(f"Token已刷新,用户: {payload.get('username', 'unknown')}, 路径: {request.url.path}")
             
             return response
             
         except Exception as e:
-            logger.error(f"Token刷新中间件错误: {e}")
+            logger.error(f"Token刷新中间件错误: {e}, 路径: {request.url.path}")
             return await call_next(request)

+ 49 - 0
src/app/server/app.py

@@ -146,8 +146,57 @@ app.add_middleware(
     allow_credentials=True,
     allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
     allow_headers=["*"],
+    expose_headers=["X-New-Token", "X-Token-Refreshed"],  # 关键:暴露自定义响应头
 )
 
+# 添加Token刷新中间件
+from app.middleware.token_refresh_middleware import TokenRefreshMiddleware
+
+app.add_middleware(
+    TokenRefreshMiddleware,
+    exclude_paths=[
+        "/auth/login",
+        "/auth/captcha",
+        "/auth/refresh",
+        "/docs",
+        "/openapi.json",
+        "/redoc",
+        "/favicon.ico",
+        "/health",
+        "/"
+    ]
+)
+
+logger.info("✅ Token刷新中间件已注册")
+
+# 添加响应处理中间件 - 用于在响应头中添加新token
+@app.middleware("http")
+async def add_new_token_to_response(request: Request, call_next):
+    """
+    响应处理中间件
+    如果request.state中有new_token,将其添加到响应头
+    """
+    logger.info(f">>> [响应中间件] 处理请求: {request.url.path}")
+    
+    response = await call_next(request)
+    
+    logger.info(f">>> [响应中间件] 请求处理完成,检查是否有新token")
+    logger.info(f">>> [响应中间件] hasattr(request.state, 'new_token'): {hasattr(request.state, 'new_token')}")
+    
+    # 检查是否有新token需要返回
+    if hasattr(request.state, "new_token"):
+        logger.info(f">>> [响应中间件] 发现新token,添加到响应头")
+        response.headers["X-New-Token"] = request.state.new_token
+        response.headers["X-Token-Refreshed"] = "true"
+        logger.info(f">>> [响应中间件] 已在响应头中添加新token")
+        logger.info(f">>> [响应中间件] X-New-Token前20字符: {request.state.new_token[:20]}...")
+    else:
+        logger.info(f">>> [响应中间件] 没有新token需要添加")
+    
+    return response
+
+logger.info("✅ Token响应处理中间件已注册")
+
 # --- 调试中间件 ---
 """
 @app.middleware("http")

+ 34 - 5
src/app/services/jwt_token.py

@@ -110,6 +110,9 @@ def verify_and_refresh_token(token: str) -> tuple[Optional[dict], Optional[str]]
     Returns:
         tuple: (payload, new_token) - 如果需要刷新则返回新token,否则new_token为None
     """
+    import logging
+    logger = logging.getLogger(__name__)
+    
     try:
         payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
         
@@ -121,28 +124,54 @@ def verify_and_refresh_token(token: str) -> tuple[Optional[dict], Optional[str]]
             current_time = datetime.now(timezone.utc)
             exp_time = datetime.fromtimestamp(exp, tz=timezone.utc)
             iat_time = datetime.fromtimestamp(iat, tz=timezone.utc)
+            # 如果需要显示本地时间
+            local_iat_time = iat_time.astimezone()  # 转换为本地时区
+            local_exp_time = exp_time.astimezone()  # 转换为本地时区
             
-            # 获取token总有效期
+            # 获取token总有效期(分钟)
             admin_expire_minutes = config_handler.get_int("admin_app", "ADMIN_TOKEN_EXPIRE_MINUTES", None)
             total_expire_minutes = admin_expire_minutes if admin_expire_minutes is not None else ACCESS_TOKEN_EXPIRE_MINUTES
             
-            # 计算token已使用时间
+            # 计算token已使用时间和剩余时间
             token_age = current_time - iat_time
+            token_age_seconds = token_age.total_seconds()
+            token_age_minutes = token_age_seconds / 60
+            
+            time_remaining = exp_time - current_time
+            time_remaining_seconds = time_remaining.total_seconds()
+            time_remaining_minutes = time_remaining_seconds / 60
+
+            
+            
+            # 获取用户名用于日志
+            username = payload.get('username', 'unknown')
+            
+            # 打印详细的token信息日志
+            logger.info(f"Token验证信息 - 用户: {username}")
+            logger.info(f"  Token配置时长: {total_expire_minutes} 分钟")
+            logger.info(f"  Token创建时间: {local_iat_time.strftime('%Y-%m-%d %H:%M:%S')}")
+            logger.info(f"  Token过期时间: {local_exp_time.strftime('%Y-%m-%d %H:%M:%S')}")
+            logger.info(f"  Token已使用时长: {token_age_minutes:.2f} 分钟 ({token_age_seconds:.0f} 秒)")
+            logger.info(f"  Token剩余时长: {time_remaining_minutes:.2f} 分钟 ({time_remaining_seconds:.0f} 秒)")
             
             # 如果token使用时间超过总有效期的一半,则刷新token
-            if token_age.total_seconds() > (total_expire_minutes * 60 / 2):
+            refresh_threshold_seconds = total_expire_minutes * 60 / 2
+            if token_age_seconds > refresh_threshold_seconds:
                 # 创建新的token数据,保持原有信息
                 new_token_data = {k: v for k, v in payload.items() if k not in ['exp', 'iat']}
                 new_token = create_access_token(new_token_data)
+                logger.info(f"  Token刷新: 已使用时长超过阈值 ({refresh_threshold_seconds/60:.2f} 分钟),生成新token")
                 return payload, new_token
+            else:
+                logger.info(f"  Token刷新: 未达到刷新阈值 ({refresh_threshold_seconds/60:.2f} 分钟),继续使用当前token")
         
         return payload, None
         
     except JWT_ERROR as e:
-        print(f"Token验证失败: {e}")
+        logger.error(f"Token验证失败: {e}")
         return None, None
     except Exception as e:
-        print(f"Token验证异常: {e}")
+        logger.error(f"Token验证异常: {e}")
         return None, None
 
 

+ 7 - 5
src/app/services/snippet_service.py

@@ -43,12 +43,12 @@ class SnippetService:
         kb_name_map = {}
         try:
             # 修正引用路径
-            from app.base.async_mysql_connection import get_db_connection
+            from app.base.async_mysql_connection import get_db
             from app.sample.models.knowledge_base import KnowledgeBase
             from sqlalchemy import select
             
-            # 使用 get_db_connection 上下文管理器
-            async with get_db_connection() as db:
+            # 使用异步数据库会话
+            async for db in get_db():
                 res = await db.execute(select(KnowledgeBase.collection_name_parent, KnowledgeBase.collection_name_children, KnowledgeBase.name))
                 for row in res.all():
                     # 映射 collection_name_parent -> name
@@ -57,6 +57,7 @@ class SnippetService:
                     # 映射 collection_name_children -> name
                     if row[1]:
                         kb_name_map[row[1]] = row[2]
+                break  # 只需要一次迭代
                         
         except Exception as e:
             logger.exception("Failed to load KB map")
@@ -267,15 +268,16 @@ class SnippetService:
         if doc_ids:
             try:
                 # 修正引用路径
-                from app.base.async_mysql_connection import get_db_connection
+                from app.base.async_mysql_connection import get_db
                 from app.sample.models.base_info import DocumentMain
                 from sqlalchemy import select
 
-                async with get_db_connection() as db:
+                async for db in get_db():
                     stmt = select(DocumentMain.id, DocumentMain.title).where(DocumentMain.id.in_(list(doc_ids)))
                     result = await db.execute(stmt)
                     rows = result.all()
                     doc_name_map = {str(row[0]): row[1] for row in rows}
+                    break  # 只需要一次迭代
                 
                 if doc_name_map:
                     for item in items:

+ 100 - 0
src/app/utils/auth_dependency.py

@@ -0,0 +1,100 @@
+"""
+认证依赖函数
+统一处理token验证和刷新
+"""
+import logging
+from fastapi import Depends, HTTPException, Request
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from app.services.jwt_token import verify_and_refresh_token
+
+logger = logging.getLogger(__name__)
+
+security = HTTPBearer()
+
+
+async def get_current_user_with_refresh(
+    request: Request,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+) -> dict:
+    """
+    获取当前用户并处理token刷新
+    
+    这个依赖函数会:
+    1. 验证token是否有效
+    2. 检查是否需要刷新token
+    3. 如果需要刷新,生成新token并存储到request.state
+    4. 返回用户信息(payload)
+    
+    Args:
+        request: FastAPI请求对象
+        credentials: HTTP Bearer认证凭据
+        
+    Returns:
+        dict: Token payload(包含用户信息)
+        
+    Raises:
+        HTTPException: Token无效时抛出401错误
+    """
+    logger.info(f">>> [依赖函数] get_current_user_with_refresh 被调用")
+    logger.info(f">>> [依赖函数] 请求路径: {request.url.path}")
+    
+    token = credentials.credentials
+    logger.info(f">>> [依赖函数] Token前20字符: {token[:20]}...")
+    
+    # 验证token并检查是否需要刷新
+    logger.info(f">>> [依赖函数] 调用 verify_and_refresh_token")
+    payload, new_token = verify_and_refresh_token(token)
+    logger.info(f">>> [依赖函数] verify_and_refresh_token 返回: payload={'存在' if payload else '无'}, new_token={'存在' if new_token else '无'}")
+    
+    if not payload:
+        logger.warning(f"无效的访问令牌,路径: {request.url.path}")
+        raise HTTPException(
+            status_code=401,
+            detail="无效的访问令牌"
+        )
+    
+    logger.info(f">>> [依赖函数] Token验证成功,用户: {payload.get('username', 'unknown')}")
+    
+    # 如果生成了新token,存储到request.state中
+    # 稍后在响应中间件中会将其添加到响应头
+    if new_token:
+        request.state.new_token = new_token
+        request.state.token_refreshed = True
+        logger.info(f">>> [依赖函数] Token已刷新,新token已存储到request.state")
+        logger.info(f">>> [依赖函数] 新Token前20字符: {new_token[:20]}...")
+        logger.info(f"Token已刷新,用户: {payload.get('username', 'unknown')}, 路径: {request.url.path}")
+    else:
+        logger.info(f">>> [依赖函数] Token未刷新(未达到阈值或其他原因)")
+    
+    return payload
+
+
+async def get_current_user(
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+) -> dict:
+    """
+    获取当前用户(不刷新token)
+    
+    用于不需要token刷新的场景
+    
+    Args:
+        credentials: HTTP Bearer认证凭据
+        
+    Returns:
+        dict: Token payload(包含用户信息)
+        
+    Raises:
+        HTTPException: Token无效时抛出401错误
+    """
+    from app.services.jwt_token import verify_token
+    
+    token = credentials.credentials
+    payload = verify_token(token)
+    
+    if not payload:
+        raise HTTPException(
+            status_code=401,
+            detail="无效的访问令牌"
+        )
+    
+    return payload

+ 15 - 50
src/views/image_view.py

@@ -9,6 +9,7 @@ from app.services.image_service import ImageService
 from app.services.jwt_token import verify_token
 from app.schemas.base import ApiResponse
 from pydantic import BaseModel
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 # 获取logger
 logger = logging.getLogger(__name__)
@@ -46,13 +47,9 @@ class BatchAddRequest(BaseModel):
 # --- 分类管理 API ---
 
 @router.get("/categories")
-async def get_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_categories(current_user: dict = Depends(get_current_user_with_refresh)):
     """获取所有图片分类树"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
         service = ImageService()
         data = await service.get_categories()
         return ApiResponse(code=0, message="成功", data=data, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
@@ -61,14 +58,10 @@ async def get_categories(credentials: HTTPAuthorizationCredentials = Depends(sec
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/categories")
-async def add_category(data: CategoryAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def add_category(data: CategoryAdd, current_user: dict = Depends(get_current_user_with_refresh)):
     """新增分类"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        user_id = payload.get("sub", "system")
+        user_id = current_user.get("sub", "system")
         service = ImageService()
         success, message, category_id = await service.add_category(data.model_dump(), user_id)
         
@@ -81,14 +74,10 @@ async def add_category(data: CategoryAdd, credentials: HTTPAuthorizationCredenti
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.put("/categories/{category_id}")
-async def edit_category(category_id: str, data: CategoryUpdate, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def edit_category(category_id: str, data: CategoryUpdate, current_user: dict = Depends(get_current_user_with_refresh)):
     """编辑分类"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        user_id = payload.get("sub", "system")
+        user_id = current_user.get("sub", "system")
         service = ImageService()
         success, message = await service.edit_category(category_id, data.model_dump(), user_id)
         
@@ -101,13 +90,9 @@ async def edit_category(category_id: str, data: CategoryUpdate, credentials: HTT
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.delete("/categories/{category_id}")
-async def delete_category(category_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def delete_category(category_id: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """删除分类"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
         service = ImageService()
         success, message = await service.delete_category(category_id)
         
@@ -126,14 +111,10 @@ async def get_image_list(
     category_id: Optional[str] = None, 
     page: int = 1, 
     page_size: int = 10, 
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取图片列表"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
         service = ImageService()
         data = await service.get_image_list(category_id, page, page_size)
         return ApiResponse(code=0, message="成功", data=data, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
@@ -142,14 +123,10 @@ async def get_image_list(
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("")
-async def add_image(data: ImageAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def add_image(data: ImageAdd, current_user: dict = Depends(get_current_user_with_refresh)):
     """保存图片信息"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        user_id = payload.get("sub", "system")
+        user_id = current_user.get("sub", "system")
         service = ImageService()
         success, message = await service.add_image(data.model_dump(), user_id)
         
@@ -162,13 +139,9 @@ async def add_image(data: ImageAdd, credentials: HTTPAuthorizationCredentials =
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.delete("/{image_id}")
-async def delete_image(image_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def delete_image(image_id: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """删除图片"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
         service = ImageService()
         success, message = await service.delete_image(image_id)
         
@@ -181,13 +154,9 @@ async def delete_image(image_id: str, credentials: HTTPAuthorizationCredentials
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/upload-url")
-async def get_upload_url(req: UploadUrlRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_upload_url(req: UploadUrlRequest, current_user: dict = Depends(get_current_user_with_refresh)):
     """获取 MinIO 预签名上传 URL"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
         service = ImageService()
         success, message, data = await service.get_upload_url(req.filename, req.content_type, prefix=req.prefix)
         
@@ -200,15 +169,11 @@ async def get_upload_url(req: UploadUrlRequest, credentials: HTTPAuthorizationCr
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/batch-add-to-task")
-async def batch_add_to_task(req: BatchAddRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def batch_add_to_task(req: BatchAddRequest, current_user: dict = Depends(get_current_user_with_refresh)):
     """批量加入任务中心 (设置 whether_to_task = 1)"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
-        
-        user_id = payload.get("sub", "admin")
-        username = payload.get("username", user_id)
+        user_id = current_user.get("sub", "admin")
+        username = current_user.get("username", user_id)
         
         service = ImageService()
         success, message = await service.batch_add_to_task(req.ids, username, req.project_name, tags=req.tags)

+ 10 - 42
src/views/knowledge_base_view.py

@@ -1,7 +1,7 @@
 """
 知识库视图路由
 """
-from fastapi import APIRouter, Depends, Query, Path
+from fastapi import APIRouter, Depends, Query, Path, Request
 from sqlalchemy.ext.asyncio import AsyncSession
 from typing import Optional
 
@@ -15,6 +15,7 @@ from app.sample.schemas.knowledge_base import (
 from app.schemas.base import PaginatedResponseSchema, ResponseSchema
 from app.services.jwt_token import verify_token
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 router = APIRouter(prefix="/sample/knowledge-base", tags=["样本中心-知识库"])
 security = HTTPBearer()
@@ -26,14 +27,9 @@ async def get_knowledge_bases(
     keyword: str = Query(None, description="搜索关键词"),
     status: str = Query(None, description="状态筛选"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取知识库列表"""
-    # 鉴权
-    payload = verify_token(credentials.credentials)
-    if not payload:
-        return PaginatedResponseSchema(code=401, message="无效的访问令牌", data=[], meta=None)
-
     items, meta = await knowledge_base_service.get_list(
         db, page=page, page_size=page_size, keyword=keyword, status=status
     )
@@ -48,13 +44,9 @@ async def get_knowledge_bases(
 @router.get("/list", response_model=ResponseSchema)
 async def get_knowledge_base_simple_list(
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取知识库简单列表 (用于下拉选择)"""
-    payload = verify_token(credentials.credentials)
-    if not payload:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     # 只获取状态正常的知识库
     items, _ = await knowledge_base_service.get_list(db, page=1, page_size=1000, status="normal")
     return ResponseSchema(
@@ -67,13 +59,9 @@ async def get_knowledge_base_simple_list(
 async def create_knowledge_base(
     payload: KnowledgeBaseCreate,
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """创建新知识库"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     try:
         new_kb = await knowledge_base_service.create(db, payload)
         return ResponseSchema(code=0, message="创建成功", data=KnowledgeBaseResponse.model_validate(new_kb))
@@ -87,13 +75,9 @@ async def update_knowledge_base(
     payload: KnowledgeBaseUpdate,
     id: str = Path(..., description="知识库ID"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """更新知识库信息"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     try:
         kb = await knowledge_base_service.update(db, id, payload)
         return ResponseSchema(code=0, message="更新成功", data=KnowledgeBaseResponse.model_validate(kb))
@@ -107,13 +91,9 @@ async def update_knowledge_base_status(
     id: str = Path(..., description="知识库ID"),
     status: str = Query(..., description="状态: normal/test/disabled"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """更新知识库状态"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     await knowledge_base_service.update_status(db, id, status)
     return ResponseSchema(code=0, message=f"状态已更新为 {status}")
 
@@ -121,13 +101,9 @@ async def update_knowledge_base_status(
 async def delete_knowledge_base(
     id: str = Path(..., description="知识库ID"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """删除知识库"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     await knowledge_base_service.delete(db, id)
     return ResponseSchema(code=0, message="删除成功")
 
@@ -135,13 +111,9 @@ async def delete_knowledge_base(
 async def get_knowledge_base_metadata(
     id: str = Path(..., description="知识库ID"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取知识库的元数据字段定义和自定义Schema"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     try:
         data = await knowledge_base_service.get_metadata_and_schema(db, id)
         return ResponseSchema(code=0, message="获取成功", data=data)
@@ -154,13 +126,9 @@ async def get_knowledge_base_metadata(
 async def sync_knowledge_base(
     id: str = Path(..., description="知识库ID"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """同步创建Milvus集合"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     try:
         await knowledge_base_service.sync_to_milvus(db, id)
         return ResponseSchema(code=0, message="同步成功")

+ 5 - 12
src/views/oauth_view.py

@@ -20,6 +20,7 @@ from datetime import datetime, timezone
 from app.services.jwt_token import verify_token, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES
 from app.services.oauth_service import OAuthService
 from app.base.async_mysql_connection import get_db_connection
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 # 获取logger
 logger = logging.getLogger(__name__)
@@ -536,20 +537,12 @@ async def oauth_token(request: Request):
         }
 
 @router.get("/userinfo")
-async def oauth_userinfo(credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def oauth_userinfo(current_user: dict = Depends(get_current_user_with_refresh)):
     """OAuth2用户信息端点"""
     try:
-        # 验证令牌
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return {
-                "error": "invalid_token",
-                "error_description": "Invalid or expired access token"
-            }
-        
-        user_id = payload.get("sub")
-        client_id = payload.get("client_id")
-        scope = payload.get("scope", "").split()
+        user_id = current_user.get("sub")
+        client_id = current_user.get("client_id")
+        scope = current_user.get("scope", "").split()
         
         logger.info(f"用户信息请求: user_id={user_id}, client_id={client_id}, scope={scope}")
         

+ 37 - 113
src/views/sample_view.py

@@ -16,6 +16,7 @@ from app.services.sample_service import SampleService
 from app.services.jwt_token import verify_token
 from app.schemas.base import ApiResponse
 from app.base import get_mineru_manager
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 from app.services.task_service import task_service
 
@@ -29,13 +30,9 @@ security_optional = HTTPBearer(auto_error=False)
 
 
 @router.get("/tasks")
-async def get_tasks(type: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_tasks(type: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """获取任务项目列表 (聚合显示)"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌").model_dump()
-        
         projects = await task_service.get_task_list(type)
         return ApiResponse(code=0, message="成功", data=projects).model_dump()
     except Exception as e:
@@ -44,13 +41,9 @@ async def get_tasks(type: str, credentials: HTTPAuthorizationCredentials = Depen
 
 
 @router.get("/tasks/details")
-async def get_task_details(project_id: str, type: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_task_details(project_id: str, type: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """获取项目下的文件详情"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌").model_dump()
-        
         files = await task_service.get_project_details(project_id, type)
         return ApiResponse(code=0, message="成功", data=files).model_dump()
     except Exception as e:
@@ -61,15 +54,11 @@ async def get_task_details(project_id: str, type: str, credentials: HTTPAuthoriz
 # --- 外部联动接口 API ---
 
 @router.post("/external/projects/init", status_code=201)
-async def init_external_project(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def init_external_project(request: Request, current_user: dict = Depends(get_current_user_with_refresh)):
     """项目初始化接口:由标注平台调用,同步数据"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌").model_dump()
-        
         # 简单验证是否为管理员(根据业务需求调整)
-        if not payload.get("is_superuser") and payload.get("role") != "admin":
+        if not current_user.get("is_superuser") and current_user.get("role") != "admin":
             return ApiResponse(code=403, message="权限不足").model_dump()
 
         data = await request.json()
@@ -87,14 +76,10 @@ async def init_external_project(request: Request, credentials: HTTPAuthorization
 @router.get("/external/projects/progress")
 async def get_external_project_progress(
     project_id: Optional[str] = None, 
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """查询项目进度"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌").model_dump()
-            
         if not project_id:
             return ApiResponse(code=400, message="缺少项目ID").model_dump()
 
@@ -112,15 +97,10 @@ async def get_external_project_progress(
 async def export_external_project(
     req: ExportRequest, 
     project_id: Optional[str] = None,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """导出项目已完成的标注数据"""
     try:
-        logger.debug(f"收到导出请求: project_id={project_id}, req={req.model_dump()}, has_credentials={bool(credentials.credentials)}")
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            logger.warning(f"导出请求 Token 验证失败: {credentials.credentials[:10]}...")
-            return ApiResponse(code=401, message="无效的访问令牌").model_dump()
         
         # 优先从路径获取 project_id,否则从 body 获取
         actual_project_id = project_id or req.project_id
@@ -146,12 +126,9 @@ async def export_external_project(
         return ApiResponse(code=500, message=str(e)).model_dump()
 
 @router.get("/external/download-proxy")
-async def download_proxy(url: str, filename: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def download_proxy(url: str, filename: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """后端中转下载外部平台文件,解决跨域和 Token 携带问题"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            raise HTTPException(status_code=401, detail="无效的访问令牌")
             
         # 确保 URL 是完整的
         if not url.startswith('http'):
@@ -166,7 +143,7 @@ async def download_proxy(url: str, filename: str, credentials: HTTPAuthorization
         admin_token = config_handler.get("external_api", "admin_token", "")
         if not admin_token:
             # 如果配置中没有,则尝试使用当前用户的 Token(兜底)
-            admin_token = credentials.credentials
+            admin_token = current_user.credentials
         
         client = httpx.AsyncClient(timeout=60.0, follow_redirects=True)
         headers = {
@@ -204,13 +181,9 @@ async def download_proxy(url: str, filename: str, credentials: HTTPAuthorization
         return ApiResponse(code=500, message=str(e)).model_dump()
 
 @router.post("/documents/upload-url")
-async def get_upload_url(req: UploadUrlRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_upload_url(req: UploadUrlRequest, credentials: dict = Depends(get_current_user_with_refresh)):
     """获取 MinIO 预签名上传 URL"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌").model_dump()
-        
         sample_service = SampleService()
         success, message, data = await sample_service.get_upload_url(req.filename, req.content_type, prefix=req.prefix)
         
@@ -226,22 +199,19 @@ async def get_upload_url(req: UploadUrlRequest, credentials: HTTPAuthorizationCr
 async def proxy_view(url: str, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
     """抓取外部文档内容并返回,支持 HTML 和 PDF 等二进制文件。支持从 Header 或 Query 参数获取 Token。"""
     try:
+        logger.info(f"token={token},credentials={credentials}")
         # 确保 URL 已解码
         url = urllib.parse.unquote(url)
-        
-        # 优先从 Header 获取,如果没有则从参数获取
+         # 优先从 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:
@@ -348,9 +318,6 @@ async def download_document(url: str, filename: Optional[str] = None, token: Opt
         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=60.0, follow_redirects=True) as client:
@@ -379,14 +346,11 @@ async def download_document(url: str, filename: Optional[str] = None, token: Opt
         return ApiResponse(code=500, message=f"下载失败: {str(e)} (URL: {url})", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/documents/batch-enter")
-async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def batch_enter_knowledge_base(req: BatchEnterRequest, current_user: dict = Depends(get_current_user_with_refresh)):
     """批量将文档加入知识库"""
     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()
         
-        username = payload.get("sub")
+        username = current_user.get("sub")
         if not username:
             return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -408,12 +372,9 @@ async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAu
         return ApiResponse(code=500, message=f"批量操作失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/documents/batch-delete")
-async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def batch_delete_documents(req: BatchDeleteRequest, current_user: dict = Depends(get_current_user_with_refresh)):
     """批量删除文档"""
     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()
         
         sample_service = SampleService()
         affected_rows, message = await sample_service.batch_delete_documents(req.ids)
@@ -435,18 +396,15 @@ class BatchAddTaskRequest(BaseModel):
     tags: Optional[List[str]] = None
 
 @router.post("/documents/batch-add-to-task")
-async def batch_add_to_task(req: BatchAddTaskRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def batch_add_to_task(req: BatchAddTaskRequest, current_user: dict = Depends(get_current_user_with_refresh)):
     """批量加入任务中心 (设置 whether_to_task = 1)"""
     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()
         
-        user_id = payload.get("sub")
+        user_id = current_user.get("sub")
         if not user_id:
             return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
-        username = payload.get("username", user_id)
+        username = current_user.get("username", user_id)
         
         sample_service = SampleService()
         success, message = await sample_service.batch_add_to_task(req.doc_ids, username, req.project_name, task_tags=req.tags)
@@ -461,12 +419,9 @@ async def batch_add_to_task(req: BatchAddTaskRequest, credentials: HTTPAuthoriza
         return ApiResponse(code=500, message=f"批量加入任务失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/documents/convert")
-async def convert_document(req: ConvertRequest, background_tasks: BackgroundTasks, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def convert_document(req: ConvertRequest, background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user_with_refresh)):
     """启动文档转换 (使用 MinerUManager 在后台执行)"""
     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()
         
         doc_id = str(req.id)
         sample_service = SampleService()
@@ -508,14 +463,11 @@ async def convert_document(req: ConvertRequest, background_tasks: BackgroundTask
         return ApiResponse(code=500, message=f"启动转换失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/documents/add")
-async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def add_document(doc: DocumentAdd, current_user: dict = Depends(get_current_user_with_refresh)):
     """添加新文档 (同步主表和子表)"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
-        user_id = payload.get("sub")
+        user_id = current_user.get("sub")
         if not user_id:
             return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -535,12 +487,9 @@ async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentia
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.get("/documents/detail/{doc_id}")
-async def get_document_detail(doc_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_document_detail(doc_id: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """获取文档详情 (关联查询子表)"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
         sample_service = SampleService()
         doc = await sample_service.get_document_detail(doc_id)
@@ -566,13 +515,10 @@ async def get_document_list(
     level_4_classification: Optional[str] = None,
     page: int = 1, 
     size: int = 50,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取文档列表 (从主表查询)"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
         sample_service = SampleService()
         items, total, all_total, total_entered = await sample_service.get_document_list(
@@ -606,12 +552,9 @@ async def get_document_list(
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/documents/edit")
-async def edit_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def edit_document(doc: DocumentAdd, current_user: dict = Depends(get_current_user_with_refresh)):
     """编辑文档 (同步主表和子表)"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
             
         if not doc.id:
             return ApiResponse(code=400, message="缺少ID参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
@@ -620,7 +563,7 @@ async def edit_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredenti
         sample_service = SampleService()
         
         # 获取更新人ID
-        updater_id = payload.get("sub", "admin")
+        updater_id = current_user.get("sub", "admin")
         
         # 将 DocumentAdd 对象转换为字典,包含所有字段
         doc_data = doc.model_dump()
@@ -636,15 +579,14 @@ async def edit_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredenti
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/documents/enter")
-async def enter_document(data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def enter_document(data: dict, current_user: dict = Depends(get_current_user_with_refresh)):
     """文档入库"""
     try:
         doc_id = data.get("id")
         if not doc_id:
             return ApiResponse(code=400, message="缺少ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
             
-        payload = verify_token(credentials.credentials)
-        username = payload.get("sub", "admin") if payload else "admin"
+        username = current_user.get("sub", "admin") if payload else "admin"
         
         # 调用 service 层
         sample_service = SampleService()
@@ -679,13 +621,10 @@ async def get_basic_info_list(
     level_2_classification: Optional[str] = None,
     level_3_classification: Optional[str] = None,
     level_4_classification: Optional[str] = None,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取基本信息列表 (支持多条件检索)"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
         sample_service = SampleService()
         
@@ -738,14 +677,11 @@ async def get_basic_info_list(
         return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/basic-info/add")
-async def add_basic_info(type: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def add_basic_info(type: str, data: dict, current_user: dict = Depends(get_current_user_with_refresh)):
     """新增基本信息"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
-        user_id = payload.get("sub")
+        user_id = current_user.get("sub")
         if not user_id:
             return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -761,14 +697,11 @@ async def add_basic_info(type: str, data: dict, credentials: HTTPAuthorizationCr
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/basic-info/edit")
-async def edit_basic_info(type: str, id: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def edit_basic_info(type: str, id: str, data: dict, current_user: dict = Depends(get_current_user_with_refresh)):
     """编辑基本信息"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
-        user_id = payload.get("sub")
+        user_id = current_user.get("sub")
         if not user_id:
             return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
             
@@ -784,12 +717,9 @@ async def edit_basic_info(type: str, id: str, data: dict, credentials: HTTPAutho
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.post("/basic-info/delete")
-async def delete_basic_info(type: str, id: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def delete_basic_info(type: str, id: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """删除基本信息"""
     try:
-        payload = verify_token(credentials.credentials)
-        if not payload:
-            return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
         sample_service = SampleService()
         success, message = await sample_service.delete_basic_info(type, id)
@@ -803,12 +733,9 @@ async def delete_basic_info(type: str, id: str, credentials: HTTPAuthorizationCr
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.get("/documents/categories/primary")
-async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_primary_categories(current_user: dict = Depends(get_current_user_with_refresh)):
     """获取所有一级分类(仅保留指定的分类)"""
     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 = ["办公制度", "行业标准", "法律法规", "施工方案", "施工图片"]
@@ -818,12 +745,9 @@ async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Dep
         return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
 
 @router.get("/documents/categories/secondary")
-async def get_secondary_categories(primaryId: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_secondary_categories(primaryId: str, current_user: dict = Depends(get_current_user_with_refresh)):
     """根据一级分类获取二级分类(仅保留指定的分类)"""
     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 = []
@@ -842,7 +766,7 @@ async def search_documents(
     table_type: Optional[str] = "standard",
     page: int = 1, 
     size: int = 50,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """关键词搜索文档,统一调用 get_document_list 以支持组合过滤"""
     return await get_document_list(

+ 8 - 32
src/views/search_engine_view.py

@@ -1,7 +1,7 @@
 """
 检索引擎视图路由
 """
-from fastapi import APIRouter, Depends, Query, Path, Body
+from fastapi import APIRouter, Depends, Query, Path, Body, Request
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.base.async_mysql_connection import get_db
@@ -16,6 +16,7 @@ from app.sample.schemas.search_engine import (
 from app.schemas.base import PaginatedResponseSchema, ResponseSchema
 from app.services.jwt_token import verify_token
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 router = APIRouter(prefix="/sample/search-engine", tags=["样本中心-检索引擎"])
 security = HTTPBearer()
@@ -25,13 +26,9 @@ security = HTTPBearer()
 async def search_knowledge_base(
     payload: KBSearchRequest,
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """知识库语义搜索"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     try:
         result = await search_engine_service.search_kb(db, payload)
         return ResponseSchema(code=0, message="搜索成功", data=result)
@@ -49,14 +46,9 @@ async def get_search_engines(
     keyword: str = Query(None, description="搜索关键词"),
     status: str = Query(None, description="状态筛选"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取检索引擎列表"""
-    # 鉴权
-    payload = verify_token(credentials.credentials)
-    if not payload:
-        return PaginatedResponseSchema(code=401, message="无效的访问令牌", data=[], meta=None)
-
     items, meta = await search_engine_service.get_list(
         db, page=page, page_size=page_size, keyword=keyword, status=status
     )
@@ -72,13 +64,9 @@ async def get_search_engines(
 async def create_search_engine(
     payload: SearchEngineCreate,
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """创建新检索引擎"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     new_engine = await search_engine_service.create(db, payload)
     return ResponseSchema(code=0, message="创建成功", data=SearchEngineResponse.model_validate(new_engine))
 
@@ -87,13 +75,9 @@ async def update_search_engine(
     payload: SearchEngineUpdate,
     id: str = Path(..., description="检索引擎ID"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """更新检索引擎信息"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     engine = await search_engine_service.update(db, id, payload)
     return ResponseSchema(code=0, message="更新成功", data=SearchEngineResponse.model_validate(engine))
 
@@ -102,13 +86,9 @@ async def update_search_engine_status(
     id: str = Path(..., description="检索引擎ID"),
     status: str = Query(..., description="状态: normal/disabled"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """更新检索引擎状态"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     await search_engine_service.update_status(db, id, status)
     return ResponseSchema(code=0, message=f"状态已更新为 {status}")
 
@@ -116,12 +96,8 @@ async def update_search_engine_status(
 async def delete_search_engine(
     id: str = Path(..., description="检索引擎ID"),
     db: AsyncSession = Depends(get_db),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """删除检索引擎"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     await search_engine_service.delete(db, id)
     return ResponseSchema(code=0, message="删除成功")

+ 9 - 32
src/views/snippet_view.py

@@ -1,7 +1,7 @@
 """
 知识片段视图路由
 """
-from fastapi import APIRouter, Depends, Query, Path, Body
+from fastapi import APIRouter, Depends, Query, Path, Body, Request
 from fastapi.responses import StreamingResponse
 from typing import Optional, Dict, Any
 from datetime import datetime
@@ -15,6 +15,7 @@ from app.schemas.base import ResponseSchema, PaginatedResponseSchema
 from app.services.jwt_token import verify_token
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from pydantic import BaseModel
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 router = APIRouter(prefix="/document/snippet", tags=["样本中心-知识片段"])
 security = HTTPBearer()
@@ -40,7 +41,7 @@ async def get_snippets(
     kb: Optional[str] = Query(None, description="知识库集合名称"),
     keyword: Optional[str] = Query(None),
     status: Optional[str] = Query(None),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """获取知识片段列表 (跨集合查询)"""
     items, meta = await snippet_service.get_list(page, page_size, kb, keyword, status)
@@ -55,13 +56,9 @@ async def get_snippets(
 @router.get("/document/{doc_id}", response_model=ResponseSchema)
 async def get_document_chunks(
     doc_id: str = Path(..., description="文档ID"),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """提取单文档的所有知识片段"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-        
     chunks = await snippet_service.get_chunks_by_document(doc_id)
     
     return ResponseSchema(
@@ -74,14 +71,10 @@ async def get_document_chunks(
 async def get_snippet_detail(
     kb: str = Query(..., description="知识库名称"),
     id: str = Query(..., description="片段ID (document_id 或 pk)"),
-    credentials: HTTPAuthorizationCredentials = Depends(security),
+    current_user: dict = Depends(get_current_user_with_refresh),
     db: AsyncSession = Depends(get_db)
 ):
     """获取知识片段详情"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-        
     data = await snippet_service.get_by_id(db, kb, id)
     if not data:
         return ResponseSchema(code=404, message="未找到该片段")
@@ -93,13 +86,9 @@ async def export_snippets(
     kb: Optional[str] = Query(None, description="知识库集合名称"),
     keyword: Optional[str] = Query(None),
     status: Optional[str] = Query(None),
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ):
     """导出知识片段"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     filename = f"snippets_export_{datetime.now().strftime('%Y%m%d%H%M%S')}.csv"
     encoded_filename = urllib.parse.quote(filename)
     
@@ -114,14 +103,10 @@ async def export_snippets(
 @router.post("", response_model=ResponseSchema)
 async def create_snippet(
     payload: SnippetCreate,
-    credentials: HTTPAuthorizationCredentials = Depends(security),
+    current_user: dict = Depends(get_current_user_with_refresh),
     db: AsyncSession = Depends(get_db)
 ):
     """创建知识片段"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     data = await snippet_service.create(db, payload)
     return ResponseSchema(code=0, message="创建成功", data=data)
 
@@ -129,14 +114,10 @@ async def create_snippet(
 async def update_snippet(
     id: str,
     payload: SnippetUpdate,
-    credentials: HTTPAuthorizationCredentials = Depends(security),
+    current_user: dict = Depends(get_current_user_with_refresh),
     db: AsyncSession = Depends(get_db)
 ):
     """更新知识片段"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-
     msg = await snippet_service.update(db, id, payload)
     return ResponseSchema(code=0, message=msg)
 
@@ -144,14 +125,10 @@ async def update_snippet(
 async def delete_snippet(
     id: str, 
     kb: str = Query(..., description="知识库名称"), 
-    credentials: HTTPAuthorizationCredentials = Depends(security),
+    current_user: dict = Depends(get_current_user_with_refresh),
     db: AsyncSession = Depends(get_db)
 ):
     """删除知识片段"""
-    payload_token = verify_token(credentials.credentials)
-    if not payload_token:
-        return ResponseSchema(code=401, message="无效的访问令牌")
-        
     snippet_service.delete(id, kb)
 
     # 更新知识库文档数量并返回最新计数,便于前端立即刷新展示

+ 82 - 66
src/views/system_view.py

@@ -29,6 +29,7 @@ from app.schemas.base import ApiResponse
 from app.utils.security import hash_password, verify_password
 from app.services.system_service import SystemService
 from app.services.system_service_ext import SystemServiceExt
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 # 获取logger
 logger = logging.getLogger(__name__)
@@ -39,7 +40,8 @@ security = HTTPBearer()
 
 @router.get("/dashboard")
 async def get_dashboard(
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    request: Request,
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """获取仪表盘数据(支持滑动过期)"""
     try:
@@ -142,7 +144,7 @@ async def get_logs(
     page: int = 1,
     page_size: int = 20,
     log_type: str = "",
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """获取系统日志"""
     return {
@@ -160,13 +162,15 @@ async def get_logs(
 
 
 @router.get("/users/profile")
-async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_user_profile( request: Request,
+                            credentials: dict = Depends(get_current_user_with_refresh)
+    ):
     """获取用户资料"""
     try:
 
         # 验证令牌
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -215,12 +219,12 @@ async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(s
 async def update_user_profile(
     request: Request,
     profile_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """更新用户资料"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -261,12 +265,12 @@ async def update_user_profile(
 async def change_user_password(
     request: Request,
     password_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """修改用户密码"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -359,10 +363,13 @@ def verify_password_simple(plain_password: str, hashed_password: str) -> bool:
 
 # RBAC权限管理API
 @router.get("/user/menus")
-async def api_get_user_menus(credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def api_get_user_menus(
+    request: Request,
+    credentials: dict = Depends(get_current_user_with_refresh)
+    ):
     """获取用户菜单"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=401,
@@ -396,14 +403,17 @@ async def api_get_user_menus(credentials: HTTPAuthorizationCredentials = Depends
 
 @router.get("/admin/menus")
 async def api_get_all_menus(
+    request: Request,
+    credentials: dict = Depends(get_current_user_with_refresh),
     page: int = 1,
     page_size: int = 1000,  # 增大默认页面大小,确保返回所有菜单
     keyword: Optional[str] = None,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
 ):
     """获取所有菜单(管理员)"""
     try:
-        payload = verify_token(credentials.credentials)
+        #payload = verify_token(credentials.credentials)
+        payload = credentials
+        logger.info(f"credentials={credentials}")
         if not payload:
             return ApiResponse(
                 code=401,
@@ -441,14 +451,17 @@ async def api_get_all_menus(
 
 @router.get("/admin/roles")
 async def api_get_all_roles(
+    request: Request,
+    credentials: dict = Depends(get_current_user_with_refresh),
     page: int = 1,
     page_size: int = 20,
     keyword: Optional[str] = None,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
 ):
     """获取所有角色"""
     try:
-        payload = verify_token(credentials.credentials)
+        #payload = verify_token(credentials.credentials)
+        payload = credentials
+        logger.info(f"credentials={credentials}")
         if not payload:
             return ApiResponse(
                 code=401,
@@ -484,10 +497,10 @@ async def api_get_all_roles(
 
 
 @router.get("/user/permissions")
-async def api_get_user_permissions(credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def api_get_user_permissions(credentials: dict = Depends(get_current_user_with_refresh)):
     """获取用户权限"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=401,
@@ -523,18 +536,19 @@ async def api_get_user_permissions(credentials: HTTPAuthorizationCredentials = D
 # 用户管理API
 @router.get("/admin/users")
 async def get_users(
+    request: Request,
+    credentials: dict = Depends(get_current_user_with_refresh),
     page: int = 1,
     page_size: int = 20,
-    keyword: Optional[str] = None,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    keyword: Optional[str] = None
 ):
     """获取用户列表"""
     try:
-        payload = verify_token(credentials.credentials)
+        
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
-        
         # 调用 service 层
         system_service_ext = SystemServiceExt()
         users, total = await system_service_ext.get_users(page, page_size, keyword)
@@ -557,12 +571,14 @@ async def get_users(
 
 @router.post("/admin/users")
 async def create_user(
+    request: Request,
     user_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """创建用户"""
     try:
-        payload = verify_token(credentials.credentials)
+        #payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -592,11 +608,11 @@ async def create_user(
 @router.get("/admin/users/{user_id}")
 async def get_user_detail(
     user_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """获取用户详情"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -622,11 +638,11 @@ async def get_user_detail(
 async def update_user(
     user_id: str,
     user_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """更新用户"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -657,11 +673,11 @@ async def update_user(
 @router.delete("/admin/users/{user_id}")
 async def delete_user(
     user_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """删除用户"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -695,11 +711,11 @@ async def delete_user(
 @router.post("/admin/roles")
 async def create_role(
     role_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """创建角色"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -723,11 +739,11 @@ async def create_role(
 async def update_role(
     role_id: str,
     role_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """更新角色"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -753,11 +769,11 @@ async def update_role(
 @router.delete("/admin/roles/{role_id}")
 async def delete_role(
     role_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """删除角色"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -783,11 +799,11 @@ async def delete_role(
 @router.get("/admin/roles/{role_id}/menus")
 async def get_role_menus(
     role_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """获取角色的菜单权限"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=401,
@@ -827,11 +843,11 @@ async def get_role_menus(
 async def update_role_menus(
     role_id: str,
     request: Request,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """更新角色的菜单权限"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=401,
@@ -884,11 +900,11 @@ async def update_role_menus(
 @router.post("/admin/menus")
 async def create_menu(
     request: Request,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """创建菜单"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=401,
@@ -939,11 +955,11 @@ async def create_menu(
 async def update_menu(
     menu_id: str,
     request: Request,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """更新菜单"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=401,
@@ -983,11 +999,11 @@ async def update_menu(
 @router.delete("/admin/menus/{menu_id}")
 async def delete_menu(
     menu_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """删除菜单"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=401,
@@ -1022,11 +1038,11 @@ async def delete_menu(
 @router.post("/admin/menus")
 async def create_menu(
     menu_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """创建菜单"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -1052,11 +1068,11 @@ async def create_menu(
 async def update_menu(
     menu_id: str,
     menu_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """更新菜单"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -1082,11 +1098,11 @@ async def update_menu(
 @router.delete("/admin/menus/{menu_id}")
 async def delete_menu(
     menu_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """删除菜单"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -1109,10 +1125,10 @@ async def delete_menu(
 
 # 获取所有角色(用于下拉选择)
 @router.get("/roles/all")
-async def get_all_roles_simple(credentials: HTTPAuthorizationCredentials = Depends(security)):
+async def get_all_roles_simple(credentials: dict = Depends(get_current_user_with_refresh)):
     """获取所有角色(简化版,用于下拉选择)"""
     try:
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
         
@@ -1136,12 +1152,12 @@ async def get_apps(
     page_size: int = 20,
     keyword: str = "",
     status: str = "",
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """获取应用列表"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -1181,12 +1197,12 @@ async def get_apps(
 @router.get("/apps/{app_id}")
 async def get_app_detail(
     app_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """获取应用详情(包含密钥)"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -1226,12 +1242,12 @@ async def get_app_detail(
 async def create_app(
     request: Request,
     app_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """创建应用"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -1279,12 +1295,12 @@ async def create_app(
 async def toggle_app_status(
     app_id: str,
     status_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """切换应用状态"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -1331,12 +1347,12 @@ async def toggle_app_status(
 async def update_app(
     app_id: str,
     app_data: dict,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """更新应用信息"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -1384,12 +1400,12 @@ async def update_app(
 @router.delete("/apps/{app_id}")
 async def delete_app(
     app_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """删除应用"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,
@@ -1437,12 +1453,12 @@ async def delete_app(
 @router.post("/apps/{app_id}/reset-secret")
 async def reset_app_secret(
     app_id: str,
-    credentials: HTTPAuthorizationCredentials = Depends(security)
+    credentials: dict = Depends(get_current_user_with_refresh)
 ):
     """重置应用密钥"""
     try:
         # 验证令牌
-        payload = verify_token(credentials.credentials)
+        payload = credentials
         if not payload:
             return ApiResponse(
                 code=200002,

+ 4 - 6
src/views/tag_view.py

@@ -2,7 +2,7 @@
 标签分类视图路由
 处理标签分类的 API 接口
 """
-from fastapi import APIRouter, Query, Path, Depends
+from fastapi import APIRouter, Query, Path, Depends, Request
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from sqlalchemy.ext.asyncio import AsyncSession
 
@@ -18,19 +18,17 @@ from app.sample.schemas.tag_category import (
     TagCategoryTreeResponse
 )
 from app.schemas.base import ResponseSchema, PaginatedResponseSchema
+from app.utils.auth_dependency import get_current_user_with_refresh
 
 router = APIRouter(prefix="/sample/tag", tags=["样本中心-标签分类"])
 security = HTTPBearer()
 
 
 async def get_current_user_id(
-    credentials: HTTPAuthorizationCredentials = Depends(security),
-    session: AsyncSession = Depends(get_db)
+    current_user: dict = Depends(get_current_user_with_refresh)
 ) -> str:
     """获取当前登录用户UUID"""
-    auth_service = AuthService(session)
-    user , token = await auth_service.get_current_user(credentials.credentials)
-    return user.id
+    return current_user.get("sub")
 
 
 @router.post("/create", response_model=ResponseSchema)

+ 400 - 0
项目/Token机制调整完成报告.md

@@ -0,0 +1,400 @@
+#### 本次调整说明
+  - 1、LQAdminFront 前端调用接口返回401 无效的访问令牌,直接调整到登录页面
+  - 2、LQAdminPlatform 后台管理调整
+    - 2.1、验证token是否按预期处理,用户每次请求后台接口时,token的时长将重置,只有用户在token设置时间范围内都没有请求 才作为token超时(需要重新登录)
+    - 2.2、用户每次访问后台接口时,日志打印token配置时长、当前token的创建时间、使用时长、剩余时长、刷新token时长信息,用于验证token是否按预期处理
+
+
+
+
+#### token 再最大时间范围内自动重置处理问题
+  - 测试情况如下: 
+    - 首次登录后 18:00:00
+      - token配置时长:5 分钟
+      - Token创建时间: 18:00:00
+      - Token过期时间: 18:05:00
+    - 时间到 18:04:00 访问一次后台请求,可以正常访问
+    - 时间到 18:05:10 访问二次后台请求时 token已经失效,需要重新登录 
+    - 正常情况下本次情况可以正常访问,因为18:04的时候访问过一次,这时token时间已经重置,并且token需要等到 18:10:10分才会过期
+    - 并且为什么每次后台请求的时候 没有打印token验证信息呢?按理说每次后台API请求的时候都会验证token,都会打印token验证信息的
+
+
+用户列表接口和角色列表接口 都按要求增加get_current_user_with_refresh ,token还是同样的问题,未得到解决
+
+
+@router.get("/admin/users")
+async def get_users(
+    request: Request,
+    current_user: dict = Depends(get_current_user_with_refresh),
+    page: int = 1,
+    page_size: int = 20,
+    keyword: Optional[str] = None
+):
+
+
+
+@router.get("/admin/roles")
+async def api_get_all_roles(
+    request: Request,
+    current_user: dict = Depends(get_current_user_with_refresh),
+    page: int = 1,
+    page_size: int = 20,
+    keyword: Optional[str] = None,
+    #credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+
+
+---
+
+## 🔍 当前状态(2026-02-08)
+
+### ✅ 已完成的工作
+
+1. **前端401处理** - 完成
+   - 同时处理HTTP 401和业务错误码401
+   - 多重跳转保障机制
+   - 自动清除认证信息
+
+2. **后端Token验证和刷新逻辑** - 完成
+   - `verify_and_refresh_token()` 函数实现滑动过期
+   - 详细的token日志输出
+   - 刷新阈值设置为50%
+
+3. **认证依赖函数** - 完成
+   - `get_current_user_with_refresh` 已创建
+   - 包含详细的调试日志(`>>> [依赖函数]` 开头)
+   - 会将新token存储到 `request.state.new_token`
+
+4. **响应处理中间件** - 完成
+   - 检查 `request.state.new_token`
+   - 添加 `X-New-Token` 到响应头
+   - 包含详细的调试日志(`>>> [响应中间件]` 开头)
+
+5. **中间件注册** - 完成
+   - `TokenRefreshMiddleware` 已注册
+   - 响应处理中间件已注册
+   - 启动时会打印注册成功日志
+
+6. **视图函数更新** - 部分完成
+   - ✅ 用户列表接口已使用新依赖
+   - ✅ 角色列表接口已使用新依赖
+   - ⚠️  其他接口仍使用旧的 `verify_token`
+
+### ❌ 问题现象
+
+用户反馈:即使用户列表和角色列表接口已使用新依赖,token仍然没有刷新。
+
+### 🎯 下一步操作
+
+**关键:必须重启后台服务器!**
+
+代码修改后,如果没有重启服务器,修改不会生效。
+
+#### 步骤1: 运行诊断脚本
+
+```bash
+cd LQAdminPlatform
+python 诊断Token刷新问题.py
+```
+
+这个脚本会检查:
+- 所有关键文件是否存在
+- 配置是否正确
+- 依赖函数是否正确实现
+- 中间件是否正确注册
+- 视图函数是否使用新依赖
+
+#### 步骤2: 重启后台服务器
+
+```bash
+cd LQAdminPlatform
+# 按 Ctrl+C 停止当前服务器
+python src/main.py
+```
+
+**检查启动日志**,必须看到:
+```
+✅ Token刷新中间件已注册
+✅ Token响应处理中间件已注册
+```
+
+#### 步骤3: 测试Token刷新
+
+详细步骤请查看:`快速测试Token刷新.md`
+
+简要步骤:
+1. 登录系统(记录时间,如 18:00:00)
+2. 等待3分钟(超过2.5分钟阈值)
+3. 访问用户列表或角色列表
+4. 查看后台日志
+
+**预期日志**:
+```
+>>> [响应中间件] 处理请求: /api/v1/system/admin/users
+>>> [依赖函数] get_current_user_with_refresh 被调用
+Token验证信息 - 用户: admin
+  Token配置时长: 5 分钟
+  Token已使用时长: 3.00 分钟
+  Token刷新: 已使用时长超过阈值,生成新token
+>>> [依赖函数] Token已刷新,新token已存储到request.state
+>>> [响应中间件] 发现新token,添加到响应头
+```
+
+#### 步骤4: 检查响应头
+
+在浏览器开发者工具 -> Network -> 选择请求 -> Headers -> Response Headers
+
+应该看到:
+```
+X-New-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+X-Token-Refreshed: true
+```
+
+#### 步骤5: 检查浏览器控制台
+
+应该看到:
+```
+Token已自动刷新
+```
+
+### 📚 相关文档
+
+1. **快速测试Token刷新.md** - 5分钟快速测试指南
+2. **调试Token刷新问题.md** - 详细的调试步骤和检查点
+3. **诊断Token刷新问题.py** - 自动诊断脚本
+4. **如何使用新的认证依赖.md** - 如何在其他接口中使用新依赖
+
+### 🐛 可能的问题
+
+1. **服务器没有重启** - 最常见的问题
+2. **日志级别不对** - 确保 `LOG_LEVEL=INFO`
+3. **等待时间不够** - 必须超过2.5分钟
+4. **访问了错误的接口** - 必须访问使用新依赖的接口
+5. **前端没有重启** - 前端代码修改后也需要重启
+
+### 💡 提示
+
+- 所有带 `>>>` 前缀的日志都是为了调试添加的,方便快速定位问题
+- 如果看不到这些日志,说明对应的代码没有执行
+- 日志会按顺序输出,可以看出执行流程
+
+
+
+
+
+# Token机制调整实现说明
+
+## 调整内容
+
+### 1. 前端调整(LQAdminFront)✅
+
+**文件**: `LQAdminFront/src/api/request.ts`
+
+**改动说明**:
+- 当接口返回 401 无效的访问令牌时,前端不再尝试刷新token
+- 直接清除本地认证信息并跳转到登录页面
+- 提示用户"无效的访问令牌,请重新登录"
+
+**测试方法**:
+1. 正常登录系统
+2. 手动修改浏览器 localStorage 中的 token 为无效值
+3. 刷新页面或访问任意需要认证的接口
+4. 验证是否自动跳转到登录页面并显示提示信息
+
+### 2. 后台调整(LQAdminPlatform)✅
+
+#### 2.1 Token滑动过期机制验证
+
+**文件**: `LQAdminPlatform/src/app/services/jwt_token.py`
+
+**改动说明**:
+- Token采用滑动过期机制:用户每次请求后台接口时,如果token已使用时长超过总时长的一半,则自动刷新token
+- 只有用户在token设置时间范围内都没有请求,才会导致token超时(需要重新登录)
+
+**配置参数**:
+- `ADMIN_TOKEN_EXPIRE_MINUTES`: Token总有效期(分钟),当前配置为 60 分钟
+- 刷新阈值:Token使用时长超过总时长的 50% 时自动刷新
+
+**测试方法**:
+1. 登录系统获取token
+2. 在30分钟内持续访问接口,观察token是否自动刷新
+3. 停止访问超过60分钟,再次访问应返回401需要重新登录
+4. 查看后台日志验证token信息是否正确打印
+
+#### 2.2 Token详细日志记录
+
+**文件**: 
+- `LQAdminPlatform/src/app/services/jwt_token.py`
+- `LQAdminPlatform/src/app/middleware/token_refresh_middleware.py`
+
+**改动说明**:
+用户每次访问后台接口时,日志会打印以下信息:
+
+```
+Token验证信息 - 用户: admin
+  Token配置时长: 60 分钟
+  Token创建时间: 2026-02-08 10:00:00 UTC
+  Token过期时间: 2026-02-08 11:00:00 UTC
+  Token已使用时长: 15.50 分钟 (930 秒)
+  Token剩余时长: 44.50 分钟 (2670 秒)
+  Token刷新: 未达到刷新阈值 (30.00 分钟),继续使用当前token
+```
+
+当token被刷新时:
+```
+Token刷新: 已使用时长超过阈值 (30.00 分钟),生成新token
+Token已刷新,用户: admin, 路径: /api/system/profile
+```
+
+**测试方法**:
+1. 启动后台服务
+2. 登录系统
+3. 访问任意需要认证的接口
+4. 查看后台日志文件(logs/lq-admin-app.log 或控制台输出)
+5. 验证是否打印了完整的token信息
+
+## 验证要点
+
+### Token滑动过期机制验证
+
+1. **正常使用场景**(应该不会超时):
+   - 用户持续操作,每隔10-20分钟访问一次接口
+   - Token应该自动刷新,用户无需重新登录
+   - 日志应显示token刷新记录
+
+2. **长时间不操作场景**(应该超时):
+   - 用户登录后60分钟内不进行任何操作
+   - 60分钟后访问接口应返回401
+   - 前端自动跳转到登录页
+
+3. **边界测试**:
+   - 在第29分钟访问:不应刷新
+   - 在第31分钟访问:应该刷新
+   - 在第59分钟访问:应该刷新且继续有效
+   - 在第61分钟访问:应该返回401
+
+### 日志验证
+
+检查日志中是否包含:
+- ✅ Token配置时长
+- ✅ Token创建时间
+- ✅ Token过期时间
+- ✅ Token已使用时长
+- ✅ Token剩余时长
+- ✅ Token刷新决策信息(是否刷新及原因)
+
+## 配置说明
+
+Token时长配置位于 `LQAdminPlatform/src/app/config/config.ini`:
+
+```ini
+[admin_app]
+# 后台管理Token配置
+ADMIN_TOKEN_EXPIRE_MINUTES=60  # Token总有效期(分钟)
+ADMIN_REFRESH_TOKEN_EXPIRE_HOURS=24  # Refresh Token有效期(小时)
+```
+
+可根据实际需求调整这些参数。
+
+## 注意事项
+
+1. Token刷新是自动的,前端会在响应头中接收新token(X-New-Token)
+2. 前端需要处理响应头中的新token并更新本地存储(如果有相关逻辑)
+3. 所有时间计算使用UTC时区,避免时区问题
+4. 日志级别需要设置为INFO或更低才能看到详细的token信息
+
+## 问题根源和修复 ✅
+
+### 问题发现
+从错误堆栈 `request.ts:65:27` 可以看出,错误发生在**成功响应拦截器**中,而不是401错误处理器中。
+
+**原因**: 后台返回的是 HTTP 200 + 业务错误码401,而不是 HTTP 401
+
+```
+HTTP/1.1 200 OK
+{
+  "code": 401,
+  "message": "无效的访问令牌"
+}
+```
+
+### 修复方案
+在成功响应拦截器中添加了对业务错误码401的检查:
+- 检测到 `code === 401` 时立即跳转
+- 清除所有存储
+- 显示错误提示
+- 阻止后续执行
+
+现在代码同时处理两种401情况:
+1. HTTP状态码401(中间件返回)
+2. HTTP 200 + 业务错误码401(业务逻辑返回)
+
+## 修改文件清单
+
+1. ✅ `LQAdminFront/src/api/request.ts` - 前端401处理逻辑 + Token自动更新(已增强调试日志和多重跳转保障)
+2. ✅ `LQAdminPlatform/src/app/services/jwt_token.py` - Token验证和刷新逻辑,添加详细日志
+3. ✅ `LQAdminPlatform/src/app/middleware/token_refresh_middleware.py` - 中间件日志增强
+
+## 前端401跳转问题修复
+
+### 问题
+前端调用接口返回401时没有自动跳转到登录页面。
+
+### 解决方案
+在 `LQAdminFront/src/api/request.ts` 中实施了以下增强:
+
+1. **详细的调试日志**: 每个步骤都输出日志,方便排查问题
+2. **多重跳转保障**: 
+   - 方法1: `router.push('/login')`
+   - 方法2: `router.replace('/login')` (如果push失败)
+   - 方法3: `window.location.href = '/login'` (最后的保障)
+3. **强制清除认证信息**: 直接操作localStorage确保token被清除
+
+### 测试步骤
+
+**重要!修改后必须重启前端开发服务器:**
+
+```bash
+# 停止当前服务器(Ctrl+C)
+cd LQAdminFront
+npm run dev
+```
+
+**清除浏览器缓存:**
+- 按F12打开开发者工具
+- 右键点击刷新按钮
+- 选择"清空缓存并硬性重新加载"
+
+**测试401跳转:**
+1. 正常登录系统
+2. 打开开发者工具(F12)-> Application -> Local Storage
+3. 修改 `sso_access_token` 的值为 `invalid_token_test`
+4. 刷新页面或点击任意菜单
+5. 观察控制台日志和页面跳转
+
+**预期结果:**
+- ✅ 控制台显示 `=== 401错误处理开始 ===` 等日志
+- ✅ 页面自动跳转到登录页
+- ✅ 显示提示:"无效的访问令牌,请重新登录"
+- ✅ localStorage中的token被清除
+
+### 排查指南
+
+详细的排查步骤请查看:`LQAdminFront/401跳转问题排查指南.md`
+
+## 前端Token自动更新机制
+
+前端响应拦截器会自动检查响应头中的 `X-New-Token`,如果存在则自动更新本地存储的token:
+
+```typescript
+// 检查响应头中是否有新的token(滑动过期机制)
+const newToken = response.headers['x-new-token']
+if (newToken) {
+  // 更新本地存储的token,保留原有的refresh token
+  const refreshToken = getRefreshToken()
+  saveToken(newToken, refreshToken || undefined)
+  console.log('Token已自动刷新')
+}
+```
+
+这样用户在正常使用过程中,token会自动刷新,无需手动处理。

+ 209 - 0
项目/test.md

@@ -0,0 +1,209 @@
+# Token机制调整测试说明
+
+## 本次调整说明
+
+### 1. 前端调整
+- LQAdminFront 前端调用接口返回401 无效的访问令牌,直接跳转到登录页面
+
+### 2. 后台调整
+- 2.1 验证token是否按预期处理,用户每次请求后台接口时,token的时长将重置,只有用户在token设置时间范围内都没有请求才作为token超时(需要重新登录)
+- 2.2 用户每次访问后台接口时,日志打印token配置时长、当前token的创建时间、使用时长、剩余时长、刷新token时长信息,用于验证token是否按预期处理
+
+## 问题根源和修复 ✅
+
+### 问题1: 前端401跳转
+**原因**: 后台返回的是 HTTP 200 + 业务错误码401,而不是 HTTP 401
+
+**解决方案**: 在成功响应拦截器中添加对业务错误码401的检查
+
+### 问题2: Token没有自动刷新 ⚠️ 关键问题!
+**原因**: **Token刷新中间件没有被注册!**
+
+查看 `LQAdminPlatform/src/app/server/app.py`,发现只注册了CORS中间件,但没有注册 `TokenRefreshMiddleware`。
+
+**影响**:
+- ❌ Token验证逻辑从未执行
+- ❌ Token刷新逻辑从未执行  
+- ❌ 日志从未打印
+- ❌ 新token从未生成
+
+**解决方案**: 在 `app.py` 中添加中间件注册:
+```python
+from app.middleware.token_refresh_middleware import TokenRefreshMiddleware
+
+app.add_middleware(
+    TokenRefreshMiddleware,
+    exclude_paths=[
+        "/auth/login",
+        "/auth/captcha",
+        "/auth/refresh",
+        "/docs",
+        "/openapi.json",
+        "/redoc",
+        "/favicon.ico",
+        "/health",
+        "/"
+    ]
+)
+```
+
+### 问题3: 数据库连接错误
+**原因**: 错误地使用了 `async with get_db_connection()`
+
+**解决方案**: 改用 `async for db in get_db()`
+
+## 修改文件清单
+
+### 前端(LQAdminFront)
+1. ✅ `src/api/request.ts` - 401处理 + Token自动更新
+
+### 后端(LQAdminPlatform)
+1. ✅ `src/app/services/jwt_token.py` - Token验证刷新 + 详细日志
+2. ✅ `src/app/middleware/token_refresh_middleware.py` - 中间件日志增强
+3. ✅ `src/app/server/app.py` - **注册Token刷新中间件**(关键修复)
+4. ✅ `src/app/services/snippet_service.py` - 数据库连接修复
+
+## 测试步骤
+
+### 1. 重启后台服务器 ⚠️ 必须!
+
+```bash
+cd LQAdminPlatform
+# Ctrl+C 停止
+python src/main.py
+```
+
+**启动时应该看到**:
+```
+✅ Token刷新中间件已注册
+```
+
+如果没有这条日志,说明中间件没有注册成功。
+
+### 2. 重启前端服务器
+
+```bash
+cd LQAdminFront
+# Ctrl+C 停止
+npm run dev
+```
+
+### 3. 清除浏览器缓存
+
+- F12 → 右键刷新按钮 → "清空缓存并硬性重新加载"
+- 或使用无痕模式(Ctrl+Shift+N)
+
+### 4. 测试Token滑动刷新(5分钟配置)
+
+#### 配置测试环境
+编辑 `LQAdminPlatform/src/app/config/config.ini`:
+```ini
+[admin_app]
+ADMIN_TOKEN_EXPIRE_MINUTES=5  # 测试用:5分钟
+```
+
+#### 测试时间表
+
+| 时间 | 操作 | 预期结果 |
+|------|------|----------|
+| 18:00:00 | 登录 | 获取token,有效期至18:05:00 |
+| 18:02:00 | 访问接口 | 正常访问,不刷新(40%) |
+| 18:02:30 | 访问接口 | 正常访问,不刷新(50%临界) |
+| 18:03:00 | 访问接口 | 正常访问,**刷新token**(60%),新有效期至18:08:00 |
+| 18:04:00 | 访问接口 | 正常访问,不刷新(使用新token,20%) |
+| 18:07:00 | 访问接口 | 正常访问,**刷新token**(使用新token,80%) |
+| 18:13:00 | 访问接口 | **401错误**,跳转登录(超过5分钟未访问) |
+
+#### 查看后台日志
+
+每次访问接口时,应该看到:
+```
+Token验证信息 - 用户: admin
+  Token配置时长: 5 分钟
+  Token创建时间: 2026-02-08 18:00:00 UTC
+  Token过期时间: 2026-02-08 18:05:00 UTC
+  Token已使用时长: 3.00 分钟 (180 秒)
+  Token剩余时长: 2.00 分钟 (120 秒)
+  Token刷新: 已使用时长超过阈值 (2.50 分钟),生成新token
+```
+
+当token刷新时:
+```
+Token已刷新,用户: admin, 路径: /api/v1/xxx
+```
+
+#### 查看浏览器控制台
+
+当token刷新时,应该看到:
+```
+Token已自动刷新
+```
+
+### 5. 测试401跳转
+
+在浏览器控制台执行:
+```javascript
+// 设置无效token
+localStorage.setItem('sso_access_token', 'invalid_token_test')
+
+// 刷新页面
+location.reload()
+```
+
+**预期结果**:
+- ✅ 控制台显示 `>>> 检测到业务错误码401`
+- ✅ 页面自动跳转到 `/login`
+- ✅ 显示错误提示
+- ✅ localStorage已清空
+
+## 验证成功标准
+
+### 后台
+- [x] 启动时看到 "Token刷新中间件已注册"
+- [x] 每次请求都打印token验证日志
+- [x] 超过50%时长后,token自动刷新
+- [x] 响应头包含 `X-New-Token`
+- [x] 完全过期后返回401
+
+### 前端
+- [x] 401响应自动跳转登录页
+- [x] 显示错误提示
+- [x] localStorage已清空
+- [x] 控制台显示 "Token已自动刷新"
+- [x] 可以重新登录
+
+## 常见问题
+
+### Q1: 为什么18:04:00访问后,18:05:10还是过期?
+
+**A**: 因为中间件没有注册,token从未刷新。修复后重启服务器即可。
+
+### Q2: 为什么没有打印日志?
+
+**A**: 因为中间件没有注册,验证逻辑从未执行。修复后重启服务器即可。
+
+### Q3: 如何确认中间件已注册?
+
+**A**: 启动服务器时,查看日志是否有:
+```
+✅ Token刷新中间件已注册
+```
+
+### Q4: 如何确认token已刷新?
+
+**A**: 
+1. 查看后台日志:`Token已刷新,用户: xxx`
+2. 查看浏览器控制台:`Token已自动刷新`
+3. 查看响应头:`X-New-Token` 存在
+
+## 相关文档
+
+- **Token中间件问题修复.md** - 详细的问题分析和修复说明
+- **Token机制调整完成报告.md** - 完整的技术文档
+- **数据库连接问题修复.md** - 数据库问题说明
+
+---
+
+**最后更新**: 2026-02-08
+**状态**: ✅ 已修复
+**关键修复**: 注册Token刷新中间件

+ 4 - 0
项目/项目追加开发说明.md

@@ -92,6 +92,10 @@
     - 
   - 所有列表界面的创建人、修改人使用表(t_sys_user username字段)进行显示
 
+- view 逻辑规范定义
+  - 获取当前用户请使用 函数参数 current_user: dict = Depends(get_current_user_with_refresh)
+  - 通过 current_user.get("sub") 用户ID
+
 - 系统异常处理规范
   - 所有的try catch 捕获后(except Exception as e:),通过 logger.exception("xxxx")打印异常堆栈信息便于问题定位分析,如: logger.exception("更新用户资料错误") 
   -