fix(auth): 在请求上下文中添加会话ID设置

feat(gencode): 改进导入验证错误信息格式

fix(scheduler): 修复任务重复添加问题并合并错过的任务

refactor(router): 优化路由转换逻辑避免Layout嵌套

feat(middleware): 增强会话ID提取功能并改进日志记录
This commit is contained in:
zhangtao
2025-12-05 00:19:44 +08:00
parent 7cbcbf73f8
commit b2e0a3d9c4
6 changed files with 99 additions and 14 deletions
@@ -44,7 +44,7 @@ executors = {
} }
# 配置默认参数 # 配置默认参数
job_defaults = { job_defaults = {
'coalesce': False, # 是否合并执行 'coalesce': True, # 合并执行错过的任务
'max_instances': 1, # 最大实例数 'max_instances': 1, # 最大实例数
} }
# 配置调度器 # 配置调度器
@@ -166,7 +166,11 @@ class SchedulerUtil:
job_list = await JobCRUD(auth).get_obj_list_crud() job_list = await JobCRUD(auth).get_obj_list_crud()
for item in job_list: for item in job_list:
cls.remove_job(job_id=item.id) # 删除旧任务 cls.remove_job(job_id=item.id) # 删除旧任务
cls.add_job(item) # 检查任务是否已经存在
existing_job = cls.get_job(job_id=item.id)
if not existing_job:
# 任务不存在才添加
cls.add_job(item)
# 根据数据库中保存的状态来设置任务状态 # 根据数据库中保存的状态来设置任务状态
if hasattr(item, 'status') and item.status == "1": if hasattr(item, 'status') and item.status == "1":
# 如果任务状态为暂停,则立即暂停刚添加的任务 # 如果任务状态为暂停,则立即暂停刚添加的任务
@@ -236,9 +236,15 @@ class DemoService:
# 验证必填字段 # 验证必填字段
required_fields = ['name', 'status'] required_fields = ['name', 'status']
errors = []
for field in required_fields: for field in required_fields:
missing_rows = df[df[field].isnull()].index.tolist() missing_rows = df[df[field].isnull()].index.tolist()
raise CustomException(msg=f"{[k for k,v in header_dict.items() if v == field][0]}不能为空,第{[i+1 for i in missing_rows]}") if missing_rows:
field_name = [k for k,v in header_dict.items() if v == field][0]
rows_str = "".join([str(i+1) for i in missing_rows])
errors.append(f"{field_name}不能为空,第{rows_str}")
if errors:
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
error_msgs = [] error_msgs = []
success_count = 0 success_count = 0
@@ -147,9 +147,14 @@ class {{ class_name }}Service:
# 验证必填字段 # 验证必填字段
{% for column in columns %} {% for column in columns %}
{% if column.required == '1' %} {% if column.required == '1' %}
errors = []
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist() missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
if missing_rows: if missing_rows:
raise CustomException(msg="{{ column.column_comment }}不能为空,第{0}行".format([i+1 for i in missing_rows])) field_name = [k for k,v in header_dict.items() if v == field][0]
rows_str = "、".join([str(i+1) for i in missing_rows])
errors.append(f"{field_name}不能为空,第{rows_str}行")
if errors:
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@@ -131,7 +131,7 @@ class LoginService:
login_location = await IpLocalUtil.get_ip_location(request_ip) login_location = await IpLocalUtil.get_ip_location(request_ip)
request.scope["login_location"] = login_location request.scope["login_location"] = login_location
# 确保在请求上下文中设置用户名 # 确保在请求上下文中设置用户名和会话ID
request.scope["user_username"] = user.username request.scope["user_username"] = user.username
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+51 -2
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json
import time import time
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.types import ASGIApp from starlette.types import ASGIApp
@@ -12,6 +13,7 @@ from app.common.response import ErrorResponse
from app.config.setting import settings from app.config.setting import settings
from app.core.logger import log from app.core.logger import log
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.security import decode_access_token
from app.api.v1.module_system.params.service import ParamsService from app.api.v1.module_system.params.service import ParamsService
@@ -35,14 +37,60 @@ class RequestLogMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp) -> None: def __init__(self, app: ASGIApp) -> None:
super().__init__(app) super().__init__(app)
@staticmethod
def _extract_session_id_from_request(request: Request) -> str | None:
"""
从请求中提取session_id(支持从Token或已设置的scope中获取)
参数:
- request (Request): 请求对象
返回:
- str | None: 会话ID,如果无法提取则返回None
"""
# 1. 先检查 scope 中是否已经有 session_id(登录接口会设置)
session_id = request.scope.get('session_id')
if session_id:
return session_id
# 2. 尝试从 Authorization Header 中提取
try:
authorization = request.headers.get("Authorization")
if not authorization:
return None
# 处理Bearer token
token = authorization.replace('Bearer ', '').strip()
# 解码token
payload = decode_access_token(token)
if not payload or not hasattr(payload, 'sub'):
return None
# 从payload中提取session_id
user_info = json.loads(payload.sub)
session_id = user_info.get("session_id")
# 同时设置到request.scope中,避免后续重复解析
if session_id:
request.scope["session_id"] = session_id
return session_id
except Exception:
# 解析失败静默处理,返回None(可能是未认证请求)
return None
async def dispatch( async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint self, request: Request, call_next: RequestResponseEndpoint
) -> Response: ) -> Response:
start_time = time.time() start_time = time.time()
session_id = request.scope.get('session_id')
# 尝试提取session_id
session_id = self._extract_session_id_from_request(request)
# 组装请求日志字段 # 组装请求日志字段
log_fields = [ log_fields = [
f"会话ID: {session_id}",
f"请求来源: {request.client.host if request.client else '未知'}", f"请求来源: {request.client.host if request.client else '未知'}",
f"请求方法: {request.method}", f"请求方法: {request.method}",
f"请求路径: {request.url.path}", f"请求路径: {request.url.path}",
@@ -111,6 +159,7 @@ class RequestLogMiddleware(BaseHTTPMiddleware):
if should_block: if should_block:
# 增强安全审计:记录详细的拦截日志 # 增强安全审计:记录详细的拦截日志
log.warning([ log.warning([
f"会话ID: {session_id or '未认证'}",
f"请求被拦截: {block_reason}", f"请求被拦截: {block_reason}",
f"请求来源: {request_ip}", f"请求来源: {request_ip}",
f"请求方法: {request.method}", f"请求方法: {request.method}",
+28 -7
View File
@@ -149,20 +149,41 @@ export const usePermissionStore = defineStore("permission", () => {
* 转换后端路由数据为Vue Router配置 * 转换后端路由数据为Vue Router配置
* 处理组件路径映射和Layout层级嵌套 * 处理组件路径映射和Layout层级嵌套
*/ */
const transformRoutes = (routes: RouteVO[]): RouteRecordRaw[] => { const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => {
return routes.map((route) => { return routes.map((route) => {
// 创建路由对象,保留所有路由属性 // 创建路由对象,保留所有路由属性
const normalizedRoute = { ...route } as RouteRecordRaw; const normalizedRoute = { ...route } as RouteRecordRaw;
// 处理组件路径映射 // 处理组件路径映射
normalizedRoute.component = !normalizedRoute.component // normalizedRoute.component = !normalizedRoute.component
? Layout // ? Layout
: modules[`../../views/${normalizedRoute.component}.vue`] || // : modules[`../../views/${normalizedRoute.component}.vue`] ||
modules["../../views/error/404.vue"]; // modules["../../views/error/404.vue"];
// 递归处理子路由 // 关键优化:
// 1. 顶级路由(一级目录)使用Layout组件,确保菜单和navbar能正常显示
// 2. 二级及以上的父路由不使用Layout组件,只作为路由容器,避免Layout嵌套
// 3. 叶子路由使用实际组件
// 4. 递归处理子路由,实现无限层级菜单
if (normalizedRoute.children && normalizedRoute.children.length > 0) { if (normalizedRoute.children && normalizedRoute.children.length > 0) {
normalizedRoute.children = transformRoutes(route.children); // normalizedRoute.children = transformRoutes(route.children);
// 非叶子路由
if (isTopLevel) {
// 顶级路由(一级目录),使用Layout组件
normalizedRoute.component = Layout;
} else {
// 二级及以上的父路由,不使用Layout组件,只作为路由容器
normalizedRoute.component = undefined;
}
// 递归处理子路由,标记为非顶级路由
normalizedRoute.children = transformRoutes(route.children, false);
} else {
// 叶子路由,使用实际组件
normalizedRoute.component = normalizedRoute.component
? modules[`../../views/${normalizedRoute.component}.vue`] ||
modules["../../views/error/404.vue"]
: modules["../../views/error/404.vue"];
} }
return normalizedRoute; return normalizedRoute;