Files
zcglxt/app/middleware/operation_log.py
Claude e71181f0a3 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>
2026-01-25 00:26:21 +08:00

195 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
操作日志中间件
"""
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