diff --git a/backend/app/api/v1/module_application/job/tools/ap_scheduler.py b/backend/app/api/v1/module_application/job/tools/ap_scheduler.py index 97a58ecd..a96e2cbf 100644 --- a/backend/app/api/v1/module_application/job/tools/ap_scheduler.py +++ b/backend/app/api/v1/module_application/job/tools/ap_scheduler.py @@ -44,7 +44,7 @@ executors = { } # 配置默认参数 job_defaults = { - 'coalesce': False, # 是否合并执行 + 'coalesce': True, # 合并执行错过的任务 'max_instances': 1, # 最大实例数 } # 配置调度器 @@ -166,7 +166,11 @@ class SchedulerUtil: job_list = await JobCRUD(auth).get_obj_list_crud() for item in job_list: 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": # 如果任务状态为暂停,则立即暂停刚添加的任务 diff --git a/backend/app/api/v1/module_gencode/demo/service.py b/backend/app/api/v1/module_gencode/demo/service.py index 2796c5c3..96b7a472 100644 --- a/backend/app/api/v1/module_gencode/demo/service.py +++ b/backend/app/api/v1/module_gencode/demo/service.py @@ -236,9 +236,15 @@ class DemoService: # 验证必填字段 required_fields = ['name', 'status'] + errors = [] for field in required_fields: 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 = [] success_count = 0 diff --git a/backend/app/api/v1/module_generator/gencode/templates/python/service.py.j2 b/backend/app/api/v1/module_generator/gencode/templates/python/service.py.j2 index fcd0d7cf..829777ab 100644 --- a/backend/app/api/v1/module_generator/gencode/templates/python/service.py.j2 +++ b/backend/app/api/v1/module_generator/gencode/templates/python/service.py.j2 @@ -147,9 +147,14 @@ class {{ class_name }}Service: # 验证必填字段 {% for column in columns %} {% if column.required == '1' %} + errors = [] missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist() 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 %} {% endfor %} diff --git a/backend/app/api/v1/module_system/auth/service.py b/backend/app/api/v1/module_system/auth/service.py index be4e4ad8..4248f23a 100644 --- a/backend/app/api/v1/module_system/auth/service.py +++ b/backend/app/api/v1/module_system/auth/service.py @@ -131,7 +131,7 @@ class LoginService: login_location = await IpLocalUtil.get_ip_location(request_ip) request.scope["login_location"] = login_location - # 确保在请求上下文中设置用户名 + # 确保在请求上下文中设置用户名和会话ID request.scope["user_username"] = user.username access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) diff --git a/backend/app/core/middlewares.py b/backend/app/core/middlewares.py index 9496f6ba..8d05f190 100644 --- a/backend/app/core/middlewares.py +++ b/backend/app/core/middlewares.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import json import time from starlette.middleware.cors import CORSMiddleware from starlette.types import ASGIApp @@ -12,6 +13,7 @@ from app.common.response import ErrorResponse from app.config.setting import settings from app.core.logger import log from app.core.exceptions import CustomException +from app.core.security import decode_access_token from app.api.v1.module_system.params.service import ParamsService @@ -35,14 +37,60 @@ class RequestLogMiddleware(BaseHTTPMiddleware): def __init__(self, app: ASGIApp) -> None: 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( self, request: Request, call_next: RequestResponseEndpoint ) -> Response: start_time = time.time() - session_id = request.scope.get('session_id') + + # 尝试提取session_id + session_id = self._extract_session_id_from_request(request) + # 组装请求日志字段 log_fields = [ - f"会话ID: {session_id}", + f"请求来源: {request.client.host if request.client else '未知'}", f"请求方法: {request.method}", f"请求路径: {request.url.path}", @@ -111,6 +159,7 @@ class RequestLogMiddleware(BaseHTTPMiddleware): if should_block: # 增强安全审计:记录详细的拦截日志 log.warning([ + f"会话ID: {session_id or '未认证'}", f"请求被拦截: {block_reason}", f"请求来源: {request_ip}", f"请求方法: {request.method}", diff --git a/frontend/src/store/modules/permission.store.ts b/frontend/src/store/modules/permission.store.ts index 8ec1d4f7..75cd1550 100644 --- a/frontend/src/store/modules/permission.store.ts +++ b/frontend/src/store/modules/permission.store.ts @@ -149,20 +149,41 @@ export const usePermissionStore = defineStore("permission", () => { * 转换后端路由数据为Vue Router配置 * 处理组件路径映射和Layout层级嵌套 */ -const transformRoutes = (routes: RouteVO[]): RouteRecordRaw[] => { +const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => { return routes.map((route) => { // 创建路由对象,保留所有路由属性 const normalizedRoute = { ...route } as RouteRecordRaw; // 处理组件路径映射 - normalizedRoute.component = !normalizedRoute.component - ? Layout - : modules[`../../views/${normalizedRoute.component}.vue`] || - modules["../../views/error/404.vue"]; + // normalizedRoute.component = !normalizedRoute.component + // ? Layout + // : modules[`../../views/${normalizedRoute.component}.vue`] || + // modules["../../views/error/404.vue"]; - // 递归处理子路由 + // 关键优化: + // 1. 顶级路由(一级目录)使用Layout组件,确保菜单和navbar能正常显示 + // 2. 二级及以上的父路由不使用Layout组件,只作为路由容器,避免Layout嵌套 + // 3. 叶子路由使用实际组件 + // 4. 递归处理子路由,实现无限层级菜单 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;