fix: 修复多个关键问题
- 修复前端路由守卫:未登录时不显示提示,直接跳转登录页 - 修复API拦截器:401错误不显示提示,直接跳转 - 增强验证码显示:图片尺寸从120x40增加到200x80 - 增大验证码字体:从28号增加到48号 - 优化验证码字符:排除易混淆的0和1 - 减少干扰线:从5条减少到3条,添加背景色优化 - 增强登录API日志:添加详细的调试日志 - 增强验证码生成和验证日志 - 优化异常处理和错误追踪 影响文件: - src/router/index.ts - src/api/request.ts - app/services/auth_service.py - app/api/v1/auth.py - app/schemas/user.py 测试状态: - 前端构建通过 - 后端语法检查通过 - 验证码显示效果优化完成 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
194
app/middleware/operation_log.py
Normal file
194
app/middleware/operation_log.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
操作日志中间件
|
||||
"""
|
||||
import time
|
||||
import json
|
||||
from typing import Callable
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import async_session_maker
|
||||
from app.schemas.operation_log import OperationLogCreate, OperationModuleEnum, OperationTypeEnum, OperationResultEnum
|
||||
from app.services.operation_log_service import operation_log_service
|
||||
|
||||
|
||||
class OperationLogMiddleware(BaseHTTPMiddleware):
|
||||
"""操作日志中间件"""
|
||||
|
||||
# 不需要记录的路径
|
||||
EXCLUDE_PATHS = [
|
||||
"/health",
|
||||
"/docs",
|
||||
"/openapi.json",
|
||||
"/api/v1/auth/login",
|
||||
"/api/v1/auth/captcha",
|
||||
]
|
||||
|
||||
# 路径到模块的映射
|
||||
PATH_MODULE_MAP = {
|
||||
"/auth": OperationModuleEnum.AUTH,
|
||||
"/device-types": OperationModuleEnum.DEVICE_TYPE,
|
||||
"/organizations": OperationModuleEnum.ORGANIZATION,
|
||||
"/assets": OperationModuleEnum.ASSET,
|
||||
"/brands": OperationModuleEnum.BRAND_SUPPLIER,
|
||||
"/suppliers": OperationModuleEnum.BRAND_SUPPLIER,
|
||||
"/allocation-orders": OperationModuleEnum.ALLOCATION,
|
||||
"/maintenance-records": OperationModuleEnum.MAINTENANCE,
|
||||
"/system-config": OperationModuleEnum.SYSTEM_CONFIG,
|
||||
"/users": OperationModuleEnum.USER,
|
||||
"/statistics": OperationModuleEnum.STATISTICS,
|
||||
"/operation-logs": OperationModuleEnum.SYSTEM_CONFIG,
|
||||
"/notifications": OperationModuleEnum.SYSTEM_CONFIG,
|
||||
}
|
||||
|
||||
# 方法到操作类型的映射
|
||||
METHOD_OPERATION_MAP = {
|
||||
"GET": OperationTypeEnum.QUERY,
|
||||
"POST": OperationTypeEnum.CREATE,
|
||||
"PUT": OperationTypeEnum.UPDATE,
|
||||
"PATCH": OperationTypeEnum.UPDATE,
|
||||
"DELETE": OperationTypeEnum.DELETE,
|
||||
}
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""处理请求"""
|
||||
# 检查是否需要记录
|
||||
if self._should_log(request):
|
||||
# 记录开始时间
|
||||
start_time = time.time()
|
||||
|
||||
# 获取用户信息
|
||||
user = getattr(request.state, "user", None)
|
||||
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
# 计算执行时长
|
||||
duration = int((time.time() - start_time) * 1000)
|
||||
|
||||
# 异步记录日志
|
||||
if user:
|
||||
await self._log_operation(request, response, user, duration)
|
||||
|
||||
return response
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
def _should_log(self, request: Request) -> bool:
|
||||
"""判断是否需要记录日志"""
|
||||
path = request.url.path
|
||||
|
||||
# 检查排除路径
|
||||
for exclude_path in self.EXCLUDE_PATHS:
|
||||
if path.startswith(exclude_path):
|
||||
return False
|
||||
|
||||
# 只记录API请求
|
||||
return path.startswith("/api/")
|
||||
|
||||
async def _log_operation(
|
||||
self,
|
||||
request: Request,
|
||||
response: Response,
|
||||
user,
|
||||
duration: int
|
||||
):
|
||||
"""记录操作日志"""
|
||||
try:
|
||||
# 获取模块
|
||||
module = self._get_module(request.url.path)
|
||||
|
||||
# 获取操作类型
|
||||
operation_type = self.METHOD_OPERATION_MAP.get(request.method, OperationTypeEnum.QUERY)
|
||||
|
||||
# 特殊处理:如果是登录/登出
|
||||
if "/auth/login" in request.url.path:
|
||||
operation_type = OperationTypeEnum.LOGIN
|
||||
elif "/auth/logout" in request.url.path:
|
||||
operation_type = OperationTypeEnum.LOGOUT
|
||||
|
||||
# 获取请求参数
|
||||
params = await self._get_request_params(request)
|
||||
|
||||
# 构建日志数据
|
||||
log_data = OperationLogCreate(
|
||||
operator_id=user.id,
|
||||
operator_name=user.real_name or user.username,
|
||||
operator_ip=request.client.host if request.client else None,
|
||||
module=module,
|
||||
operation_type=operation_type,
|
||||
method=request.method,
|
||||
url=request.url.path,
|
||||
params=params,
|
||||
result=OperationResultEnum.SUCCESS if response.status_code < 400 else OperationResultEnum.FAILED,
|
||||
error_msg=None if response.status_code < 400 else f"HTTP {response.status_code}",
|
||||
duration=duration,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
# 异步保存日志
|
||||
async with async_session_maker() as db:
|
||||
await operation_log_service.create_log(db, log_data)
|
||||
|
||||
except Exception as e:
|
||||
# 记录日志失败不应影响业务
|
||||
print(f"Failed to log operation: {e}")
|
||||
|
||||
def _get_module(self, path: str) -> OperationModuleEnum:
|
||||
"""根据路径获取模块"""
|
||||
for path_prefix, module in self.PATH_MODULE_MAP.items():
|
||||
if path_prefix in path:
|
||||
return module
|
||||
return OperationModuleEnum.SYSTEM_CONFIG
|
||||
|
||||
async def _get_request_params(self, request: Request) -> str:
|
||||
"""获取请求参数"""
|
||||
try:
|
||||
# GET请求
|
||||
if request.method == "GET":
|
||||
params = dict(request.query_params)
|
||||
return json.dumps(params, ensure_ascii=False)
|
||||
|
||||
# POST/PUT/DELETE请求
|
||||
if request.method in ["POST", "PUT", "DELETE", "PATCH"]:
|
||||
try:
|
||||
body = await request.body()
|
||||
if body:
|
||||
# 尝试解析JSON
|
||||
try:
|
||||
body_json = json.loads(body.decode())
|
||||
# 过滤敏感字段
|
||||
filtered_body = self._filter_sensitive_data(body_json)
|
||||
return json.dumps(filtered_body, ensure_ascii=False)
|
||||
except json.JSONDecodeError:
|
||||
# 不是JSON,返回原始数据
|
||||
return body.decode()[:500] # 限制长度
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _filter_sensitive_data(self, data: dict) -> dict:
|
||||
"""过滤敏感数据"""
|
||||
sensitive_fields = ["password", "old_password", "new_password", "token", "secret"]
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
filtered = {}
|
||||
for key, value in data.items():
|
||||
if key in sensitive_fields:
|
||||
filtered[key] = "******"
|
||||
elif isinstance(value, dict):
|
||||
filtered[key] = self._filter_sensitive_data(value)
|
||||
elif isinstance(value, list):
|
||||
filtered[key] = [
|
||||
self._filter_sensitive_data(item) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
else:
|
||||
filtered[key] = value
|
||||
|
||||
return filtered
|
||||
Reference in New Issue
Block a user