From d06e78d0ab4988f7bb09c0a1e56b974df94eb1e8 Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Thu, 18 Dec 2025 00:54:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BB=BB=E5=8A=A1=E8=B0=83=E5=BA=A6):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AB=8B=E5=8D=B3=E6=89=A7=E8=A1=8C=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(前端): 统一调整表格高度为500px style(前端): 优化用户管理页面布局 fix(前端): 修复验证码未开启时的登录验证规则 perf(任务调度): 优化任务状态更新逻辑 fix(后端): 修复字符类型映射问题 docs(后端): 更新数据库配置为MySQL feat(登录): 添加登录/登出成功通知 --- .../v1/module_application/job/controller.py | 25 ++- .../api/v1/module_application/job/service.py | 8 +- .../job/tools/ap_scheduler.py | 95 +++++--- .../v1/module_gencode/gen_demo01/__init__.py | 1 - .../module_gencode/gen_demo01/controller.py | 126 ----------- .../api/v1/module_gencode/gen_demo01/crud.py | 123 ----------- .../api/v1/module_gencode/gen_demo01/model.py | 18 -- .../v1/module_gencode/gen_demo01/schema.py | 58 ----- .../v1/module_gencode/gen_demo01/service.py | 208 ------------------ .../gencode/templates/python/schema.py.j2 | 2 - .../gencode/tools/jinja2_template_util.py | 33 +-- backend/app/scripts/initialize.py | 2 + backend/env/.env.dev | 6 +- frontend/src/api/module_application/job.ts | 8 + frontend/src/store/modules/user.store.ts | 17 +- frontend/src/utils/request.ts | 8 +- .../views/module_application/job/index.vue | 37 +++- .../src/views/module_gencode/demo/index.vue | 4 +- .../views/module_generator/gencode/index.vue | 38 ++-- .../src/views/module_monitor/online/index.vue | 4 +- .../views/module_monitor/resource/index.vue | 4 +- .../module_system/auth/components/Login.vue | 27 ++- .../src/views/module_system/dept/index.vue | 4 +- .../src/views/module_system/dict/index.vue | 4 +- .../src/views/module_system/log/index.vue | 4 +- .../src/views/module_system/menu/index.vue | 5 +- .../src/views/module_system/notice/index.vue | 4 +- .../src/views/module_system/param/index.vue | 4 +- .../views/module_system/position/index.vue | 4 +- .../src/views/module_system/role/index.vue | 4 +- .../src/views/module_system/user/index.vue | 14 +- 31 files changed, 239 insertions(+), 660 deletions(-) delete mode 100644 backend/app/api/v1/module_gencode/gen_demo01/__init__.py delete mode 100644 backend/app/api/v1/module_gencode/gen_demo01/controller.py delete mode 100644 backend/app/api/v1/module_gencode/gen_demo01/crud.py delete mode 100644 backend/app/api/v1/module_gencode/gen_demo01/model.py delete mode 100644 backend/app/api/v1/module_gencode/gen_demo01/schema.py delete mode 100644 backend/app/api/v1/module_gencode/gen_demo01/service.py diff --git a/backend/app/api/v1/module_application/job/controller.py b/backend/app/api/v1/module_application/job/controller.py index 76119753..d00f57e3 100644 --- a/backend/app/api/v1/module_application/job/controller.py +++ b/backend/app/api/v1/module_application/job/controller.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Body, Depends, Path from fastapi.responses import JSONResponse, StreamingResponse -from app.common.response import StreamResponse, SuccessResponse +from app.common.response import ErrorResponse, StreamResponse, SuccessResponse from app.common.request import PaginationService from app.core.router_class import OperationLogRoute from app.utils.common_util import bytes2file_response @@ -328,3 +328,26 @@ async def export_job_log_list_controller( 'Content-Disposition': 'attachment; filename=job_log.xlsx' } ) + +@JobRouter.put("/run/{id}", summary="立即执行定时任务", description="立即执行指定的定时任务") +async def run_job_now_controller( + id: int = Path(..., description="定时任务ID"), + auth: AuthSchema = Depends(AuthPermission(["module_application:job:update"])) +) -> JSONResponse: + """ + 立即执行定时任务 + + 参数: + - id (int): 定时任务ID + - auth (AuthSchema): 认证信息模型 + + 返回: + - JSONResponse: 包含操作结果的JSON响应 + """ + try: + SchedulerUtil.run_job_now(job_id=id) + log.info(f"立即执行定时任务成功: {id}") + return SuccessResponse(msg="立即执行定时任务成功") + except Exception as e: + log.error(f"立即执行定时任务失败: {id}, 错误信息: {str(e)}") + return ErrorResponse(msg=f"立即执行定时任务失败: {str(e)}") \ No newline at end of file diff --git a/backend/app/api/v1/module_application/job/service.py b/backend/app/api/v1/module_application/job/service.py index 0c83a0ea..e3932b04 100644 --- a/backend/app/api/v1/module_application/job/service.py +++ b/backend/app/api/v1/module_application/job/service.py @@ -149,10 +149,10 @@ class JobService: raise CustomException(msg='操作失败,该数据定时任务不存在') if option == 1: SchedulerUtil().pause_job(job_id=id) - await JobCRUD(auth).set_obj_field_crud(ids=[id], status=False) + await JobCRUD(auth).set_obj_field_crud(ids=[id], status='1') # 更新为暂停状态(1) elif option == 2: SchedulerUtil().resume_job(job_id=id) - await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True) + await JobCRUD(auth).set_obj_field_crud(ids=[id], status='0') # 更新为运行状态(0) elif option == 3: # 重启任务:先移除再添加,确保使用最新的任务配置 SchedulerUtil().remove_job(job_id=id) @@ -162,7 +162,7 @@ class JobService: # 重新添加任务 SchedulerUtil.add_job(job_info=updated_job) # 设置状态为运行中 - await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True) + await JobCRUD(auth).set_obj_field_crud(ids=[id], status='0') @classmethod async def export_job_service(cls, data_list: list[dict]) -> bytes: @@ -199,7 +199,7 @@ class JobService: # 复制数据并转换状态 data = data_list.copy() for item in data: - item['status'] = '已完成' if item['status'] == '0' else '运行中' if item['status'] == '1' else '暂停' + item['status'] = '运行中' if item['status'] == '0' else '暂停中' if item['status'] == '1' else '未知状态' return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) 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 39b906c7..3a5e98d8 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 @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import asyncio import json import importlib from datetime import datetime @@ -71,6 +72,10 @@ class SchedulerUtil: 返回: - None """ + # 只处理任务执行相关事件,不处理任务添加、删除等事件 + if not isinstance(event, JobExecutionEvent): + return + # 延迟导入避免循环导入 from app.api.v1.module_application.job.model import JobLogModel @@ -252,41 +257,32 @@ class SchedulerUtil: return scheduler.get_jobs() @classmethod - async def _task_wrapper(cls, job_id, func, *args, **kwargs): - """ - 任务执行包装器,添加分布式锁防止同一任务被多个实例同时执行。 - - 参数: - - job_id: 任务ID - - func: 实际要执行的任务函数 - - *args: 任务函数位置参数 - - **kwargs: 任务函数关键字参数 - - 返回: - - 任务函数的返回值 - """ - import redis.asyncio as redis - import asyncio + async def _task_wrapper(cls, func, job_id, *args, **kwargs): + """任务执行包装器,添加分布式锁防止并发执行""" + from redis.asyncio import Redis from app.config.setting import settings - - # 创建Redis客户端 - redis_client = redis.Redis( + from app.core.logger import log + + # 使用项目配置创建Redis连接池 + redis_client = Redis( host=settings.REDIS_HOST, port=int(settings.REDIS_PORT), username=settings.REDIS_USER, password=settings.REDIS_PASSWORD, db=int(settings.REDIS_DB_NAME), + encoding='utf-8', + decode_responses=True, + health_check_interval=20, + max_connections=settings.POOL_SIZE, + socket_timeout=settings.POOL_TIMEOUT ) - - # 生成锁键 + lock_key = f"job_lock:{job_id}" - - # 设置锁的过期时间(根据任务类型调整,这里设置为30秒) - lock_expire = 30 - lock_acquired = False - + lock_expire = 30 # 锁过期时间,根据任务实际执行时间调整 + lock_acquired = None + try: - # 尝试获取锁 + # 获取分布式锁,使用nx=True确保原子性操作 lock_acquired = await redis_client.set(lock_key, "1", ex=lock_expire, nx=True) if lock_acquired: @@ -307,15 +303,17 @@ class SchedulerUtil: if lock_acquired: await redis_client.delete(lock_key) log.info(f"任务 {job_id} 释放执行锁") - + # 关闭Redis连接 + await redis_client.close() + @classmethod def add_job(cls, job_info: JobModel) -> Job: """ 根据任务配置创建并添加调度任务。 - + 参数: - job_info (JobModel): 任务对象信息(包含触发器、函数、参数等)。 - + 返回: - Job: 新增的任务对象。 """ @@ -336,17 +334,22 @@ class SchedulerUtil: if job_executor is None: job_executor = 'default' - if job_info.trigger_args is None: - raise ValueError("触发器缺少参数") - # 异步函数必须使用默认执行器 if iscoroutinefunction(job_func): job_executor = 'default' # 4. 创建触发器 - if job_info.trigger == 'date': + trigger = None + if job_info.trigger is None or job_info.trigger.lower() == 'now': + # 立即执行作业:省略trigger或使用'now'时,使用date触发器立即执行 + trigger = DateTrigger(run_date=datetime.now()) + elif job_info.trigger == 'date': + if job_info.trigger_args is None: + raise ValueError("date触发器缺少执行时间参数") trigger = DateTrigger(run_date=job_info.trigger_args) elif job_info.trigger == 'interval': + if job_info.trigger_args is None: + raise ValueError("interval触发器缺少参数") # 将传入的 interval 表达式拆分为不同的字段 fields = job_info.trigger_args.strip().split() if len(fields) != 5: @@ -365,6 +368,8 @@ class SchedulerUtil: jitter=None ) elif job_info.trigger == 'cron': + if job_info.trigger_args is None: + raise ValueError("cron触发器缺少参数") # 秒、分、时、天、月、星期几、年 () fields = job_info.trigger_args.strip().split() if len(fields) not in (6, 7): @@ -586,4 +591,26 @@ class SchedulerUtil: elif scheduler.state == STATE_PAUSED: return 'paused' else: - return 'unknown' \ No newline at end of file + return 'unknown' + + @classmethod + def run_job_now(cls, job_id: str | int) -> None: + """ + 立即执行指定任务。 + + 参数: + - job_id (str | int): 任务ID。 + + 返回: + - None + + 异常: + - ValueError: 当任务不存在时抛出。 + """ + job = cls.get_job(job_id=str(job_id)) + if not job: + raise ValueError(f"未找到该任务:{job_id}") + + # 立即执行任务 + scheduler.modify_job(job_id=str(job_id), next_run_time=datetime.now()) + log.info(f"任务 {job_id} 已设置为立即执行") \ No newline at end of file diff --git a/backend/app/api/v1/module_gencode/gen_demo01/__init__.py b/backend/app/api/v1/module_gencode/gen_demo01/__init__.py deleted file mode 100644 index 40a96afc..00000000 --- a/backend/app/api/v1/module_gencode/gen_demo01/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/backend/app/api/v1/module_gencode/gen_demo01/controller.py b/backend/app/api/v1/module_gencode/gen_demo01/controller.py deleted file mode 100644 index 942b823d..00000000 --- a/backend/app/api/v1/module_gencode/gen_demo01/controller.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- - -from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query -from fastapi.responses import StreamingResponse, JSONResponse - -from app.common.response import SuccessResponse, StreamResponse -from app.core.dependencies import AuthPermission -from app.api.v1.module_system.auth.schema import AuthSchema -from app.core.base_params import PaginationQueryParam -from app.utils.common_util import bytes2file_response -from app.core.logger import log -from app.core.base_schema import BatchSetAvailable - -from .service import GenDemo01Service -from .schema import GenDemo01CreateSchema, GenDemo01UpdateSchema, GenDemo01QueryParam - -GenDemo01Router = APIRouter(prefix='/gen_demo01', tags=["示例模块"]) - -@GenDemo01Router.get("/detail/{id}", summary="获取示例详情", description="获取示例详情") -async def get_gen_demo01_detail_controller( - id: int = Path(..., description="ID"), - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:query"])) -) -> JSONResponse: - """获取示例详情接口""" - result_dict = await GenDemo01Service.detail_gen_demo01_service(auth=auth, id=id) - log.info(f"获取示例详情成功 {id}") - return SuccessResponse(data=result_dict, msg="获取示例详情成功") - -@GenDemo01Router.get("/list", summary="查询示例列表", description="查询示例列表") -async def get_gen_demo01_list_controller( - page: PaginationQueryParam = Depends(), - search: GenDemo01QueryParam = Depends(), - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:query"])) -) -> JSONResponse: - """查询示例列表接口(数据库分页)""" - result_dict = await GenDemo01Service.page_gen_demo01_service( - auth=auth, - page_no=page.page_no if page.page_no is not None else 1, - page_size=page.page_size if page.page_size is not None else 10, - search=search, - order_by=page.order_by - ) - log.info("查询示例列表成功") - return SuccessResponse(data=result_dict, msg="查询示例列表成功") - -@GenDemo01Router.post("/create", summary="创建示例", description="创建示例") -async def create_gen_demo01_controller( - data: GenDemo01CreateSchema, - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:create"])) -) -> JSONResponse: - """创建示例接口""" - result_dict = await GenDemo01Service.create_gen_demo01_service(auth=auth, data=data) - log.info("创建示例成功") - return SuccessResponse(data=result_dict, msg="创建示例成功") - -@GenDemo01Router.put("/update/{id}", summary="修改示例", description="修改示例") -async def update_gen_demo01_controller( - data: GenDemo01UpdateSchema, - id: int = Path(..., description="ID"), - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:update"])) -) -> JSONResponse: - """修改示例接口""" - result_dict = await GenDemo01Service.update_gen_demo01_service(auth=auth, id=id, data=data) - log.info("修改示例成功") - return SuccessResponse(data=result_dict, msg="修改示例成功") - -@GenDemo01Router.delete("/delete", summary="删除示例", description="删除示例") -async def delete_gen_demo01_controller( - ids: list[int] = Body(..., description="ID列表"), - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:delete"])) -) -> JSONResponse: - """删除示例接口""" - await GenDemo01Service.delete_gen_demo01_service(auth=auth, ids=ids) - log.info(f"删除示例成功: {ids}") - return SuccessResponse(msg="删除示例成功") - -@GenDemo01Router.patch("/available/setting", summary="批量修改示例状态", description="批量修改示例状态") -async def batch_set_available_gen_demo01_controller( - data: BatchSetAvailable, - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:patch"])) -) -> JSONResponse: - """批量修改示例状态接口""" - await GenDemo01Service.set_available_gen_demo01_service(auth=auth, data=data) - log.info(f"批量修改示例状态成功: {data.ids}") - return SuccessResponse(msg="批量修改示例状态成功") - -@GenDemo01Router.post('/export', summary="导出示例", description="导出示例") -async def export_gen_demo01_list_controller( - search: GenDemo01QueryParam = Depends(), - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:export"])) -) -> StreamingResponse: - """导出示例接口""" - result_dict_list = await GenDemo01Service.list_gen_demo01_service(search=search, auth=auth) - export_result = await GenDemo01Service.batch_export_gen_demo01_service(obj_list=result_dict_list) - log.info('导出示例成功') - - return StreamResponse( - data=bytes2file_response(export_result), - media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - headers={ - 'Content-Disposition': 'attachment; filename=gen_demo01.xlsx' - } - ) - -@GenDemo01Router.post('/import', summary="导入示例", description="导入示例") -async def import_gen_demo01_list_controller( - file: UploadFile, - auth: AuthSchema = Depends(AuthPermission(["module_gencode:gen_demo01:import"])) -) -> JSONResponse: - """导入示例接口""" - batch_import_result = await GenDemo01Service.batch_import_gen_demo01_service(file=file, auth=auth, update_support=True) - log.info("导入示例成功") - - return SuccessResponse(data=batch_import_result, msg="导入示例成功") - -@GenDemo01Router.post('/download/template', summary="获取示例导入模板", description="获取示例导入模板", dependencies=[Depends(AuthPermission(["module_gencode:gen_demo01:download"]))]) -async def export_gen_demo01_template_controller() -> StreamingResponse: - """获取示例导入模板接口""" - import_template_result = await GenDemo01Service.import_template_download_gen_demo01_service() - log.info('获取示例导入模板成功') - - return StreamResponse( - data=bytes2file_response(import_template_result), - media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - headers={'Content-Disposition': 'attachment; filename=gen_demo01_template.xlsx'} - ) \ No newline at end of file diff --git a/backend/app/api/v1/module_gencode/gen_demo01/crud.py b/backend/app/api/v1/module_gencode/gen_demo01/crud.py deleted file mode 100644 index 9463a8f9..00000000 --- a/backend/app/api/v1/module_gencode/gen_demo01/crud.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import Sequence - -from app.core.base_crud import CRUDBase -from app.api.v1.module_system.auth.schema import AuthSchema -from .model import GenDemo01Model -from .schema import GenDemo01CreateSchema, GenDemo01UpdateSchema, GenDemo01OutSchema - - -class GenDemo01CRUD(CRUDBase[GenDemo01Model, GenDemo01CreateSchema, GenDemo01UpdateSchema]): - """示例数据层""" - - def __init__(self, auth: AuthSchema) -> None: - """ - 初始化CRUD数据层 - - 参数: - - auth (AuthSchema): 认证信息模型 - """ - super().__init__(model=GenDemo01Model, auth=auth) - - async def get_by_id_gen_demo01_crud(self, id: int, preload: list | None = None) -> GenDemo01Model | None: - """ - 详情 - - 参数: - - id (int): 对象ID - - preload (list | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - GenDemo01Model | None: 模型实例或None - """ - return await self.get(id=id, preload=preload) - - async def list_gen_demo01_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[GenDemo01Model]: - """ - 列表查询 - - 参数: - - search (dict | None): 查询参数 - - order_by (list[dict] | None): 排序参数,未提供时使用模型默认项 - - preload (list | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - Sequence[GenDemo01Model]: 模型实例序列 - """ - return await self.list(search=search, order_by=order_by, preload=preload) - - async def create_gen_demo01_crud(self, data: GenDemo01CreateSchema) -> GenDemo01Model | None: - """ - 创建 - - 参数: - - data (GenDemo01CreateSchema): 创建模型 - - 返回: - - GenDemo01Model | None: 模型实例或None - """ - return await self.create(data=data) - - async def update_gen_demo01_crud(self, id: int, data: GenDemo01UpdateSchema) -> GenDemo01Model | None: - """ - 更新 - - 参数: - - id (int): 对象ID - - data (GenDemo01UpdateSchema): 更新模型 - - 返回: - - GenDemo01Model | None: 模型实例或None - """ - return await self.update(id=id, data=data) - - async def delete_gen_demo01_crud(self, ids: list[int]) -> None: - """ - 批量删除 - - 参数: - - ids (list[int]): 对象ID列表 - - 返回: - - None - """ - return await self.delete(ids=ids) - - async def set_available_gen_demo01_crud(self, ids: list[int], status: str) -> None: - """ - 批量设置可用状态 - - 参数: - - ids (list[int]): 对象ID列表 - - status (str): 可用状态 - - 返回: - - None - """ - return await self.set(ids=ids, status=status) - - async def page_gen_demo01_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict: - """ - 分页查询 - - 参数: - - offset (int): 偏移量 - - limit (int): 每页数量 - - order_by (list[dict] | None): 排序参数,未提供时使用模型默认项 - - search (dict | None): 查询参数,未提供时查询所有 - - preload (list | None): 预加载关系,未提供时使用模型默认项 - - 返回: - - Dict: 分页数据 - """ - order_by_list = order_by or [{'id': 'asc'}] - search_dict = search or {} - return await self.page( - offset=offset, - limit=limit, - order_by=order_by_list, - search=search_dict, - out_schema=GenDemo01OutSchema, - preload=preload - ) \ No newline at end of file diff --git a/backend/app/api/v1/module_gencode/gen_demo01/model.py b/backend/app/api/v1/module_gencode/gen_demo01/model.py deleted file mode 100644 index e267a1e3..00000000 --- a/backend/app/api/v1/module_gencode/gen_demo01/model.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - -from sqlalchemy import Text, DateTime, Integer, String -from sqlalchemy.orm import Mapped, mapped_column - -from app.core.base_model import ModelMixin, UserMixin - - -class GenDemo01Model(ModelMixin, UserMixin): - """ - 示例表 - """ - __tablename__: str = 'gen_demo01' - __table_args__: dict[str, str] = {'comment': '示例'} - __loader_options__: list[str] = ["created_by", "updated_by"] - - name: Mapped[str | None] = mapped_column(String, nullable=True, comment='名称') - diff --git a/backend/app/api/v1/module_gencode/gen_demo01/schema.py b/backend/app/api/v1/module_gencode/gen_demo01/schema.py deleted file mode 100644 index e84e39fd..00000000 --- a/backend/app/api/v1/module_gencode/gen_demo01/schema.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- - -from pydantic import BaseModel, ConfigDict, Field -from fastapi import Query - -from app.core.validator import DateTimeStr -from app.core.base_schema import BaseSchema, UserBySchema - -class GenDemo01CreateSchema(BaseModel): - """ - 示例新增模型 - """ - name: str = Field(default=..., description='名称') - status: str = Field(default="0", description='是否启用(0:启用 1:禁用)') - description: str | None = Field(default=None, max_length=255, description='备注/描述') - - -class GenDemo01UpdateSchema(GenDemo01CreateSchema): - """ - 示例更新模型 - """ - ... - - -class GenDemo01OutSchema(GenDemo01CreateSchema, BaseSchema, UserBySchema): - """ - 示例响应模型 - """ - model_config = ConfigDict(from_attributes=True) - - -class GenDemo01QueryParam: - """示例查询参数""" - - def __init__( - self, - name: str | None = Query(None, description="名称"), - status: str | None = Query(None, description="是否启用(0:启用 1:禁用)"), - created_id: int | None = Query(None, description="创建人ID"), - updated_id: int | None = Query(None, description="更新人ID"), - created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), - - ) -> None: - - # 模糊查询字段 - self.name = ("like", name) - # 模糊查询字段 - self.status = ("like", status) - # 精确查询字段 - self.created_id = created_id - # 精确查询字段 - self.updated_id = updated_id - # 时间范围查询 - if created_time and len(created_time) == 2: - self.created_time = ("between", (created_time[0], created_time[1])) - if updated_time and len(updated_time) == 2: - self.updated_time = ("between", (updated_time[0], updated_time[1])) diff --git a/backend/app/api/v1/module_gencode/gen_demo01/service.py b/backend/app/api/v1/module_gencode/gen_demo01/service.py deleted file mode 100644 index e827357b..00000000 --- a/backend/app/api/v1/module_gencode/gen_demo01/service.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- - -import io -from fastapi import UploadFile -import pandas as pd - -from app.core.base_schema import BatchSetAvailable -from app.core.exceptions import CustomException -from app.utils.excel_util import ExcelUtil -from app.core.logger import log -from app.api.v1.module_system.auth.schema import AuthSchema -from .schema import GenDemo01CreateSchema, GenDemo01UpdateSchema, GenDemo01OutSchema, GenDemo01QueryParam -from .crud import GenDemo01CRUD - - -class GenDemo01Service: - """ - 示例服务层 - """ - - @classmethod - async def detail_gen_demo01_service(cls, auth: AuthSchema, id: int) -> dict: - """详情""" - obj = await GenDemo01CRUD(auth).get_by_id_gen_demo01_crud(id=id) - if not obj: - raise CustomException(msg="该数据不存在") - return GenDemo01OutSchema.model_validate(obj).model_dump() - - @classmethod - async def list_gen_demo01_service(cls, auth: AuthSchema, search: GenDemo01QueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]: - """列表查询""" - search_dict = search.__dict__ if search else None - obj_list = await GenDemo01CRUD(auth).list_gen_demo01_crud(search=search_dict, order_by=order_by) - return [GenDemo01OutSchema.model_validate(obj).model_dump() for obj in obj_list] - - @classmethod - async def page_gen_demo01_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: GenDemo01QueryParam | None = None, order_by: list[dict] | None = None) -> dict: - """分页查询(数据库分页)""" - search_dict = search.__dict__ if search else {} - order_by_list = order_by or [{'id': 'asc'}] - offset = (page_no - 1) * page_size - result = await GenDemo01CRUD(auth).page_gen_demo01_crud( - offset=offset, - limit=page_size, - order_by=order_by_list, - search=search_dict - ) - return result - - @classmethod - async def create_gen_demo01_service(cls, auth: AuthSchema, data: GenDemo01CreateSchema) -> dict: - """创建""" - # 检查唯一性约束 - obj = await GenDemo01CRUD(auth).create_gen_demo01_crud(data=data) - return GenDemo01OutSchema.model_validate(obj).model_dump() - - @classmethod - async def update_gen_demo01_service(cls, auth: AuthSchema, id: int, data: GenDemo01UpdateSchema) -> dict: - """更新""" - # 检查数据是否存在 - obj = await GenDemo01CRUD(auth).get_by_id_gen_demo01_crud(id=id) - if not obj: - raise CustomException(msg='更新失败,该数据不存在') - - # 检查唯一性约束 - - obj = await GenDemo01CRUD(auth).update_gen_demo01_crud(id=id, data=data) - return GenDemo01OutSchema.model_validate(obj).model_dump() - - @classmethod - async def delete_gen_demo01_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """删除""" - if len(ids) < 1: - raise CustomException(msg='删除失败,删除对象不能为空') - for id in ids: - obj = await GenDemo01CRUD(auth).get_by_id_gen_demo01_crud(id=id) - if not obj: - raise CustomException(msg=f'删除失败,ID为{id}的数据不存在') - await GenDemo01CRUD(auth).delete_gen_demo01_crud(ids=ids) - - @classmethod - async def set_available_gen_demo01_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """批量设置状态""" - await GenDemo01CRUD(auth).set_available_gen_demo01_crud(ids=data.ids, status=data.status) - - @classmethod - async def batch_export_gen_demo01_service(cls, obj_list: list[dict]) -> bytes: - """批量导出""" - mapping_dict = { - 'id': '主键ID', - 'uuid': 'UUID全局唯一标识', - 'name': '名称', - 'status': '是否启用(0:启用 1:禁用)', - 'description': '备注/描述', - 'created_time': '创建时间', - 'updated_time': '更新时间', - 'created_id': '创建人ID', - 'updated_id': '更新人ID', - 'updated_id': '更新者ID', - } - - data = obj_list.copy() - for item in data: - # 状态转换 - if 'status' in item: - item['status'] = '启用' if item.get('status') == '0' else '停用' - # 创建者转换 - creator_info = item.get('creator') - if isinstance(creator_info, dict): - item['creator'] = creator_info.get('name', '未知') - elif creator_info is None: - item['creator'] = '未知' - - return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) - - @classmethod - async def batch_import_gen_demo01_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str: - """批量导入""" - header_dict = { - '主键ID': 'id', - 'UUID全局唯一标识': 'uuid', - '名称': 'name', - '是否启用(0:启用 1:禁用)': 'status', - '备注/描述': 'description', - '创建时间': 'created_time', - '更新时间': 'updated_time', - '创建人ID': 'created_id', - '更新人ID': 'updated_id', - } - - try: - contents = await file.read() - df = pd.read_excel(io.BytesIO(contents)) - await file.close() - - if df.empty: - raise CustomException(msg="导入文件为空") - - missing_headers = [header for header in header_dict.keys() if header not in df.columns] - if missing_headers: - raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}") - - df.rename(columns=header_dict, inplace=True) - - # 验证必填字段 - - error_msgs = [] - success_count = 0 - count = 0 - - for index, row in df.iterrows(): - count += 1 - try: - data = { - "id": row['id'], - "uuid": row['uuid'], - "name": row['name'], - "status": row['status'], - "description": row['description'], - "created_time": row['created_time'], - "updated_time": row['updated_time'], - "created_id": row['created_id'], - "updated_id": row['updated_id'], - } - # 使用CreateSchema做校验后入库 - create_schema = GenDemo01CreateSchema.model_validate(data) - - # 检查唯一性约束 - - await GenDemo01CRUD(auth).create_gen_demo01_crud(data=create_schema) - success_count += 1 - except Exception as e: - error_msgs.append(f"第{count}行: {str(e)}") - continue - - result = f"成功导入 {success_count} 条数据" - if error_msgs: - result += "\n错误信息:\n" + "\n".join(error_msgs) - return result - - except Exception as e: - log.error(f"批量导入失败: {str(e)}") - raise CustomException(msg=f"导入失败: {str(e)}") - - @classmethod - async def import_template_download_gen_demo01_service(cls) -> bytes: - """下载导入模板""" - header_list = [ - '主键ID', - 'UUID全局唯一标识', - '名称', - '是否启用(0:启用 1:禁用)', - '备注/描述', - '创建时间', - '更新时间', - '创建人ID', - '更新人ID', - ] - selector_header_list = [] - option_list = [] - - # 添加下拉选项 - - return ExcelUtil.get_excel_template( - header_list=header_list, - selector_header_list=selector_header_list, - option_list=option_list - ) \ No newline at end of file diff --git a/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 b/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 index 3d4e0aaa..2679a0c2 100644 --- a/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 +++ b/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 @@ -63,9 +63,7 @@ class {{ class_name }}QueryParam: {% if table.updated_time %} updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), {% endif %} - ) -> None: - {% for column in columns %} {% if column.query_type == 'LIKE' %} # 模糊查询字段 diff --git a/backend/app/api/v1/module_generator/gencode/tools/jinja2_template_util.py b/backend/app/api/v1/module_generator/gencode/tools/jinja2_template_util.py index 7a7466f2..55a83f4b 100644 --- a/backend/app/api/v1/module_generator/gencode/tools/jinja2_template_util.py +++ b/backend/app/api/v1/module_generator/gencode/tools/jinja2_template_util.py @@ -375,21 +375,26 @@ class Jinja2TemplateUtil: """ if '(' in column: column_type_list = column.split('(') - if column_type_list[0] in GenConstant.COLUMNTYPE_STR: - sqlalchemy_type = ( - StringUtil.get_mapping_value_by_key_ignore_case( - GenConstant.DB_TO_SQLALCHEMY, column_type_list[0] - ) - + '(' - + column_type_list[1] - ) - else: - sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( - GenConstant.DB_TO_SQLALCHEMY, column_type_list[0] - ) - else: + column_type = column_type_list[0] + # 将 'character' 映射为 'char' 以匹配常量定义 + if column_type.lower() == 'character': + column_type = 'char' sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( - GenConstant.DB_TO_SQLALCHEMY, column + GenConstant.DB_TO_SQLALCHEMY, column_type ) + # 如果是字符串类型且包含括号参数,保持原参数 + if sqlalchemy_type in ["String", "CHAR"]: + sqlalchemy_type += '(' + column_type_list[1] + else: + column_type = column + # 将 'character' 映射为 'char' 以匹配常量定义 + if column_type.lower() == 'character': + column_type = 'char' + sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_SQLALCHEMY, column_type + ) + # 如果是字符串类型且没有指定长度,添加默认长度255 + if sqlalchemy_type in ["String", "CHAR"]: + sqlalchemy_type += '(255)' return sqlalchemy_type \ No newline at end of file diff --git a/backend/app/scripts/initialize.py b/backend/app/scripts/initialize.py index 3f7ea111..d3f223f8 100644 --- a/backend/app/scripts/initialize.py +++ b/backend/app/scripts/initialize.py @@ -5,6 +5,7 @@ import json from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession +from app.api.v1.module_system.position.model import PositionModel from app.config.path_conf import SCRIPT_DIR from app.core.logger import log from app.core.database import async_db_session, async_engine @@ -35,6 +36,7 @@ class InitializeData: RoleModel, DictTypeModel, DictDataModel, + PositionModel, UserModel, UserRolesModel, ] diff --git a/backend/env/.env.dev b/backend/env/.env.dev index ef2442ce..5260ef49 100644 --- a/backend/env/.env.dev +++ b/backend/env/.env.dev @@ -22,12 +22,12 @@ DESCRIPTION = "该项目是一个基于python的web服务框架,基于fastapi DEMO_ENABLE = False # 数据库配置 -DATABASE_TYPE = "postgres" # mysql、postgres、[qlite、dm这俩种不支持代码生成] +DATABASE_TYPE = "mysql" # mysql、postgres、[qlite、dm这俩种不支持代码生成] # 数据库配置 DATABASE_HOST = "localhost" -DATABASE_PORT = 5432 # MySQL:3306 PostgreSQL:5432 -DATABASE_USER = "tao" # mysql:root, postgresql:tao +DATABASE_PORT = 3306 # MySQL:3306 PostgreSQL:5432 +DATABASE_USER = "root" # mysql:root, postgresql:tao DATABASE_PASSWORD = "ServBay.dev" DATABASE_NAME = "fastapiadmin" diff --git a/frontend/src/api/module_application/job.ts b/frontend/src/api/module_application/job.ts index 9a2d8b06..da2ea06a 100644 --- a/frontend/src/api/module_application/job.ts +++ b/frontend/src/api/module_application/job.ts @@ -117,6 +117,14 @@ const JobAPI = { responseType: "blob", }); }, + + //立即执行定时任务 + runJob(id: number) { + return request({ + url: `${API_PATH}/run/${id}`, + method: "put", + }); + }, }; export default JobAPI; diff --git a/frontend/src/store/modules/user.store.ts b/frontend/src/store/modules/user.store.ts index f57b5b90..960763e2 100644 --- a/frontend/src/store/modules/user.store.ts +++ b/frontend/src/store/modules/user.store.ts @@ -4,6 +4,7 @@ import AuthAPI, { type LoginFormData } from "@/api/module_system/auth"; import UserAPI, { type UserInfo } from "@/api/module_system/user"; import type { MenuTable } from "@/api/module_system/menu"; import { Auth } from "@/utils/auth"; +import { ResultEnum } from "@/enums"; export const useUserStore = defineStore("user", { state: () => ({ @@ -97,6 +98,13 @@ export const useUserStore = defineStore("user", { // 登录 async login(LoginFormData: LoginFormData) { const response = await AuthAPI.login(LoginFormData); + if (response.data.code === ResultEnum.SUCCESS) { + ElNotification({ + title: "通知", + message: response.data.msg, + type: "success", + }); + } this.rememberMe = LoginFormData.remember; Auth.setTokens( response.data.data.access_token, @@ -107,7 +115,14 @@ export const useUserStore = defineStore("user", { // 登出 async logout() { - await AuthAPI.logout({ token: Auth.getAccessToken() }); + const response = await AuthAPI.logout({ token: Auth.getAccessToken() }); + if (response.data.code === ResultEnum.SUCCESS) { + ElNotification({ + title: "通知", + message: response.data.msg, + type: "success", + }); + } this.resetAllState(); }, diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 5701c039..73b04167 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -60,8 +60,12 @@ httpRequest.interceptors.response.use( return Promise.reject(response); } - // 如果请求不是 GET 请求,请求成功时显示成功提示 - if (response.config.method?.toUpperCase() !== "GET") { + // 如果请求不是 GET 请求,且不是登录或退出登录接口,请求成功时显示成功提示 + if ( + response.config.method?.toUpperCase() !== "GET" && + !response.config.url?.includes("login") && + !response.config.url?.includes("logout") + ) { ElMessage.success(data.msg); } diff --git a/frontend/src/views/module_application/job/index.vue b/frontend/src/views/module_application/job/index.vue index 2736f558..e44a406e 100644 --- a/frontend/src/views/module_application/job/index.vue +++ b/frontend/src/views/module_application/job/index.vue @@ -20,8 +20,8 @@ clearable style="width: 167.5px" > - - + + @@ -167,8 +167,8 @@ :data="pageTableData" class="data-table__content" highlight-current-row - height="450" - max-height="450px" + height="500" + max-height="500" border stripe @selection-change="handleSelectionChange" @@ -218,7 +218,7 @@ @@ -238,6 +238,16 @@ > 日志 + + 立即执行 + 暂停 @@ -363,8 +373,8 @@ - - {{ detailFormData.status ? "运行中" : "暂停" }} + + {{ detailFormData.status === "0" ? "运行中" : "暂停" }} @@ -541,7 +551,7 @@ @@ -961,6 +971,13 @@ function handleOpenLogDrawer(jobId: number, jobName: string) { drawerVisible.value = true; } +// 立即执行定时任务 +const handleRunJob = (id: number) => { + JobAPI.runJob(id).then(() => { + loadingData(); + }); +}; + // 导出字段 const exportColumns = [ { prop: "name", label: "任务名称" }, diff --git a/frontend/src/views/module_gencode/demo/index.vue b/frontend/src/views/module_gencode/demo/index.vue index efb57652..e9bf14ee 100644 --- a/frontend/src/views/module_gencode/demo/index.vue +++ b/frontend/src/views/module_gencode/demo/index.vue @@ -212,8 +212,8 @@ :data="pageTableData" highlight-current-row class="data-table__content" - height="450" - max-height="450px" + height="500" + max-height="500" border stripe @selection-change="handleSelectionChange" diff --git a/frontend/src/views/module_generator/gencode/index.vue b/frontend/src/views/module_generator/gencode/index.vue index 4ad7a03c..7ee18c94 100644 --- a/frontend/src/views/module_generator/gencode/index.vue +++ b/frontend/src/views/module_generator/gencode/index.vue @@ -147,8 +147,8 @@ :data="tableList" highlight-current-row class="data-table__content" - height="450" - max-height="450px" + height="500" + max-height="500" border stripe @selection-change="handleTableSelectionChange" @@ -595,13 +595,13 @@ - + - + - + - + - + - + - + - +