Просмотр исходного кода

优化定时任务执行一次与授权

SpringSunYY 4 месяцев назад
Родитель
Сommit
57fd446955
2 измененных файлов с 137 добавлено и 103 удалено
  1. 74 59
      ruoyi-ui/src/views/system/role/index.vue
  2. 63 44
      ruoyi_apscheduler/service/job.py

+ 74 - 59
ruoyi-ui/src/views/system/role/index.vue

@@ -60,7 +60,8 @@
           size="mini"
           @click="handleAdd"
           v-hasPermi="['system:role:add']"
-        >新增</el-button>
+        >新增
+        </el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
@@ -71,7 +72,8 @@
           :disabled="single"
           @click="handleUpdate"
           v-hasPermi="['system:role:edit']"
-        >修改</el-button>
+        >修改
+        </el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
@@ -82,7 +84,8 @@
           :disabled="multiple"
           @click="handleDelete"
           v-hasPermi="['system:role:remove']"
-        >删除</el-button>
+        >删除
+        </el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button
@@ -92,17 +95,18 @@
           size="mini"
           @click="handleExport"
           v-hasPermi="['system:role:export']"
-        >导出</el-button>
+        >导出
+        </el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
     <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="角色编号" prop="roleId" width="120" />
-      <el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
-      <el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150" />
-      <el-table-column label="显示顺序" prop="roleSort" width="100" />
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="角色编号" prop="roleId" width="120"/>
+      <el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150"/>
+      <el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="150"/>
+      <el-table-column label="显示顺序" prop="roleSort" width="100"/>
       <el-table-column label="状态" align="center" width="100">
         <template slot-scope="scope">
           <el-switch
@@ -126,23 +130,28 @@
             icon="el-icon-edit"
             @click="handleUpdate(scope.row)"
             v-hasPermi="['system:role:edit']"
-          >修改</el-button>
+          >修改
+          </el-button>
           <el-button
             size="mini"
             type="text"
             icon="el-icon-delete"
             @click="handleDelete(scope.row)"
             v-hasPermi="['system:role:remove']"
-          >删除</el-button>
-          <el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['system:role:edit']">
+          >删除
+          </el-button>
+          <el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)"
+                       v-hasPermi="['system:role:edit']">
             <span class="el-dropdown-link">
               <i class="el-icon-d-arrow-right el-icon--right"></i>更多
             </span>
             <el-dropdown-menu slot="dropdown">
               <el-dropdown-item command="handleDataScope" icon="el-icon-circle-check"
-                v-hasPermi="['system:role:edit']">数据权限</el-dropdown-item>
+                                v-hasPermi="['system:role:edit']">数据权限
+              </el-dropdown-item>
               <el-dropdown-item command="handleAuthUser" icon="el-icon-user"
-                v-hasPermi="['system:role:edit']">分配用户</el-dropdown-item>
+                                v-hasPermi="['system:role:edit']">分配用户
+              </el-dropdown-item>
             </el-dropdown-menu>
           </el-dropdown>
         </template>
@@ -161,7 +170,7 @@
     <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="100px">
         <el-form-item label="角色名称" prop="roleName">
-          <el-input v-model="form.roleName" placeholder="请输入角色名称" />
+          <el-input v-model="form.roleName" placeholder="请输入角色名称"/>
         </el-form-item>
         <el-form-item prop="roleKey">
           <span slot="label">
@@ -170,10 +179,10 @@
             </el-tooltip>
             权限字符
           </span>
-          <el-input v-model="form.roleKey" placeholder="请输入权限字符" />
+          <el-input v-model="form.roleKey" placeholder="请输入权限字符"/>
         </el-form-item>
         <el-form-item label="角色顺序" prop="roleSort">
-          <el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
+          <el-input-number v-model="form.roleSort" controls-position="right" :min="0"/>
         </el-form-item>
         <el-form-item label="状态">
           <el-radio-group v-model="form.status">
@@ -181,13 +190,16 @@
               v-for="dict in dict.type.sys_normal_disable"
               :key="dict.value"
               :label="dict.value"
-            >{{dict.label}}</el-radio>
+            >{{ dict.label }}
+            </el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="菜单权限">
           <el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
-          <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
-          <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
+          <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选
+          </el-checkbox>
+          <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动
+          </el-checkbox>
           <el-tree
             class="tree-border"
             :data="menuOptions"
@@ -213,10 +225,10 @@
     <el-dialog :title="title" :visible.sync="openDataScope" width="500px" append-to-body>
       <el-form :model="form" label-width="80px">
         <el-form-item label="角色名称">
-          <el-input v-model="form.roleName" :disabled="true" />
+          <el-input v-model="form.roleName" :disabled="true"/>
         </el-form-item>
         <el-form-item label="权限字符">
-          <el-input v-model="form.roleKey" :disabled="true" />
+          <el-input v-model="form.roleKey" :disabled="true"/>
         </el-form-item>
         <el-form-item label="权限范围">
           <el-select v-model="form.dataScope" @change="dataScopeSelectChange">
@@ -230,8 +242,10 @@
         </el-form-item>
         <el-form-item label="数据权限" v-show="form.dataScope == 2">
           <el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">展开/折叠</el-checkbox>
-          <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
-          <el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
+          <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选
+          </el-checkbox>
+          <el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动
+          </el-checkbox>
           <el-tree
             class="tree-border"
             :data="deptOptions"
@@ -254,9 +268,9 @@
 </template>
 
 <script>
-import { listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus } from "@/api/system/role";
-import { treeselect as menuTreeselect, roleMenuTreeselect } from "@/api/system/menu";
-import { treeselect as deptTreeselect, roleDeptTreeselect } from "@/api/system/dept";
+import {listRole, getRole, delRole, addRole, updateRole, dataScope, changeRoleStatus} from "@/api/system/role";
+import {treeselect as menuTreeselect, roleMenuTreeselect} from "@/api/system/menu";
+import {treeselect as deptTreeselect, roleDeptTreeselect} from "@/api/system/dept";
 
 export default {
   name: "Role",
@@ -333,13 +347,13 @@ export default {
       // 表单校验
       rules: {
         roleName: [
-          { required: true, message: "角色名称不能为空", trigger: "blur" }
+          {required: true, message: "角色名称不能为空", trigger: "blur"}
         ],
         roleKey: [
-          { required: true, message: "权限字符不能为空", trigger: "blur" }
+          {required: true, message: "权限字符不能为空", trigger: "blur"}
         ],
         roleSort: [
-          { required: true, message: "角色顺序不能为空", trigger: "blur" }
+          {required: true, message: "角色顺序不能为空", trigger: "blur"}
         ]
       }
     };
@@ -405,11 +419,11 @@ export default {
     // 角色状态修改
     handleStatusChange(row) {
       let text = row.status === "0" ? "启用" : "停用";
-      this.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?').then(function() {
+      this.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?').then(function () {
         return changeRoleStatus(row.roleId, row.status);
       }).then(() => {
         this.$modal.msgSuccess(text + "成功");
-      }).catch(function() {
+      }).catch(function () {
         row.status = row.status === "0" ? "1" : "0";
       });
     },
@@ -429,21 +443,21 @@ export default {
         this.$refs.menu.setCheckedKeys([]);
       }
       this.menuExpand = false,
-      this.menuNodeAll = false,
-      this.deptExpand = true,
-      this.deptNodeAll = false,
-      this.form = {
-        roleId: undefined,
-        roleName: undefined,
-        roleKey: undefined,
-        roleSort: 0,
-        status: "0",
-        menuIds: [],
-        deptIds: [],
-        menuCheckStrictly: true,
-        deptCheckStrictly: true,
-        remark: undefined
-      };
+        this.menuNodeAll = false,
+        this.deptExpand = true,
+        this.deptNodeAll = false,
+        this.form = {
+          roleId: undefined,
+          roleName: undefined,
+          roleKey: undefined,
+          roleSort: 0,
+          status: "0",
+          menuIds: [],
+          deptIds: [],
+          menuCheckStrictly: false,
+          deptCheckStrictly: false,
+          remark: undefined
+        };
       this.resetForm("form");
     },
     /** 搜索按钮操作 */
@@ -460,7 +474,7 @@ export default {
     // 多选框选中数据
     handleSelectionChange(selection) {
       this.ids = selection.map(item => item.roleId)
-      this.single = selection.length!=1
+      this.single = selection.length != 1
       this.multiple = !selection.length
     },
     // 更多操作触发
@@ -493,17 +507,17 @@ export default {
     // 树权限(全选/全不选)
     handleCheckedTreeNodeAll(value, type) {
       if (type == 'menu') {
-        this.$refs.menu.setCheckedNodes(value ? this.menuOptions: []);
+        this.$refs.menu.setCheckedNodes(value ? this.menuOptions : []);
       } else if (type == 'dept') {
-        this.$refs.dept.setCheckedNodes(value ? this.deptOptions: []);
+        this.$refs.dept.setCheckedNodes(value ? this.deptOptions : []);
       }
     },
     // 树权限(父子联动)
     handleCheckedTreeConnect(value, type) {
       if (type == 'menu') {
-        this.form.menuCheckStrictly = value ? true: false;
+        this.form.menuCheckStrictly = value ? true : false;
       } else if (type == 'dept') {
-        this.form.deptCheckStrictly = value ? true: false;
+        this.form.deptCheckStrictly = value ? true : false;
       }
     },
     /** 新增按钮操作 */
@@ -531,7 +545,7 @@ export default {
     },
     /** 选择角色权限范围触发 */
     dataScopeSelectChange(value) {
-      if(value !== '2') {
+      if (value !== '2') {
         this.$refs.dept.setCheckedKeys([]);
       }
     },
@@ -551,12 +565,12 @@ export default {
       });
     },
     /** 分配用户操作 */
-    handleAuthUser: function(row) {
+    handleAuthUser: function (row) {
       const roleId = row.roleId;
       this.$router.push("/system/role-auth/user/" + roleId);
     },
     /** 提交按钮 */
-    submitForm: function() {
+    submitForm: function () {
       this.$refs["form"].validate(valid => {
         if (valid) {
           if (this.form.roleId != undefined) {
@@ -578,7 +592,7 @@ export default {
       });
     },
     /** 提交按钮(数据权限) */
-    submitDataScope: function() {
+    submitDataScope: function () {
       if (this.form.roleId != undefined) {
         this.form.deptIds = this.getDeptAllCheckedKeys();
         dataScope(this.form).then(response => {
@@ -591,12 +605,13 @@ export default {
     /** 删除按钮操作 */
     handleDelete(row) {
       const roleIds = row.roleId || this.ids;
-      this.$modal.confirm('是否确认删除角色编号为"' + roleIds + '"的数据项?').then(function() {
+      this.$modal.confirm('是否确认删除角色编号为"' + roleIds + '"的数据项?').then(function () {
         return delRole(roleIds);
       }).then(() => {
         this.getList();
         this.$modal.msgSuccess("删除成功");
-      }).catch(() => {});
+      }).catch(() => {
+      });
     },
     /** 导出按钮操作 */
     handleExport() {
@@ -606,4 +621,4 @@ export default {
     }
   }
 };
-</script>
+</script>

+ 63 - 44
ruoyi_apscheduler/service/job.py

@@ -7,6 +7,7 @@ from typing import List, Optional
 from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR, \
     EVENT_JOB_MISSED, EVENT_JOB_SUBMITTED, EVENT_JOB_REMOVED, JobEvent
 from flask import Flask
+import importlib
 
 from ruoyi_common.base.signal import app_completed
 from ruoyi_common.exception import ServiceException
@@ -14,48 +15,48 @@ from ruoyi_common.sqlalchemy.transaction import Transactional
 from ruoyi_apscheduler.constant import ScheduleStatus
 from ruoyi_apscheduler.domain.entity import SysJob, SysJobLog
 from ruoyi_apscheduler.mapper.job import SysJobMapper
-from ruoyi_apscheduler.util import ScheduleUtil
+from ruoyi_apscheduler.util import ScheduleUtil, check_method_importable
 from ruoyi_apscheduler.service.job_log import SysJobLogService
 from ruoyi_admin.ext import db
-from .. import reg,scheduler
+from .. import reg, scheduler
 
 
 class SysJobService:
-    
+
     @classmethod
     def init(cls):
         """
         初始化定时任务
         """
         scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | \
-        EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_JOB_SUBMITTED | \
-        EVENT_JOB_REMOVED)
-        
+                               EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_JOB_SUBMITTED | \
+                               EVENT_JOB_REMOVED)
+
         scheduler.remove_all_jobs()
         try:
             for job in SysJobMapper.select_job_all():
-                ScheduleUtil.create_schedule_job(scheduler,job)
+                ScheduleUtil.create_schedule_job(scheduler, job)
         except ImportError as e:
             raise ServiceException(f"导入定时任务失败,请检查表SysJob的数据:{e}")
-        
+
         scheduler.start()
         atexit.register(lambda: scheduler.shutdown())
 
     @classmethod
-    def select_job_list(cls, job:SysJob) -> List[SysJob]:
+    def select_job_list(cls, job: SysJob) -> List[SysJob]:
         """
         查询定时任务列表
 
         Args:
-            job (SysJob): 包含查询条件的任务 
+            job (SysJob): 包含查询条件的任务
 
         Returns:
-            List[SysJob]: 任务信息列表 
+            List[SysJob]: 任务信息列表
         """
         return SysJobMapper.select_job_list(job)
-    
+
     @classmethod
-    def select_job_by_id(cls, job_id:int) -> Optional[SysJob]:
+    def select_job_by_id(cls, job_id: int) -> Optional[SysJob]:
         """
         查询定时任务
 
@@ -66,10 +67,10 @@ class SysJobService:
             Optional[SysJob]: 任务信息
         """
         return SysJobMapper.select_job_by_id(job_id)
-    
+
     @classmethod
     @Transactional(db.session)
-    def insert_job(cls, job:SysJob) -> bool:
+    def insert_job(cls, job: SysJob) -> bool:
         """
         新增定时任务
 
@@ -81,33 +82,33 @@ class SysJobService:
         """
         flag = SysJobMapper.insert_job(job)
         if flag:
-            ScheduleUtil.create_schedule_job(scheduler,job)
+            ScheduleUtil.create_schedule_job(scheduler, job)
         return flag > 0
-        
+
     @classmethod
     @Transactional(db.session)
-    def update_job(cls, job:SysJob) -> bool:
+    def update_job(cls, job: SysJob) -> bool:
         """
         更新定时任务
 
         Args:
             job (SysJob): 任务信息
-            
+
         Returns:
             bool: 操作结果
         """
-        db_job:SysJob = cls.select_job_by_id(job.job_id)
+        db_job: SysJob = cls.select_job_by_id(job.job_id)
         flag = SysJobMapper.update_job(job)
         if flag > 0:
             sched_job = scheduler.get_job(db_job.job_key)
             if sched_job:
                 scheduler.remove_job(db_job.job_key)
-            ScheduleUtil.create_schedule_job(scheduler,job)
+            ScheduleUtil.create_schedule_job(scheduler, job)
         return flag > 0
-    
+
     @classmethod
     @Transactional(db.session)
-    def delete_job_by_id(cls, job:SysJob) -> int:
+    def delete_job_by_id(cls, job: SysJob) -> int:
         """
         删除定时任务
 
@@ -121,10 +122,10 @@ class SysJobService:
         if num > 0:
             scheduler.remove_job(job.job_key)
         return num
-    
+
     @classmethod
     @Transactional(db.session)
-    def delete_job_by_ids(cls, job_ids:List[int]):
+    def delete_job_by_ids(cls, job_ids: List[int]):
         """
         批量删除定时任务
 
@@ -135,10 +136,10 @@ class SysJobService:
             job = cls.select_job_by_id(job_id)
             if job:
                 cls.delete_job_by_id(job)
-    
+
     @classmethod
     @Transactional(db.session)
-    def pause_job(cls, job:SysJob) -> int:
+    def pause_job(cls, job: SysJob) -> int:
         """
         暂停定时任务
 
@@ -153,10 +154,10 @@ class SysJobService:
         if num > 0:
             scheduler.pause_job(job.job_key)
         return num
-    
+
     @classmethod
     @Transactional(db.session)
-    def resume_job(cls, job:SysJob) -> int:
+    def resume_job(cls, job: SysJob) -> int:
         """
         恢复定时任务
 
@@ -176,10 +177,10 @@ class SysJobService:
             else:
                 scheduler.resume_job(job.job_key)
         return num
-    
+
     @classmethod
     @Transactional(db.session)
-    def change_job_status(cls, job:SysJob) -> int:
+    def change_job_status(cls, job: SysJob) -> int:
         """
         更改定时任务状态
 
@@ -193,46 +194,64 @@ class SysJobService:
         if job.status == ScheduleStatus.NORMAL.value:
             num = cls.resume_job(job)
         elif job.status == ScheduleStatus.PAUSED.value:
-            num =cls.pause_job(job)
+            num = cls.pause_job(job)
         return num
 
     @classmethod
-    def run(cls, job:SysJob):
+    def run(cls, job: SysJob):
         """
         立即执行定时任务
 
         Args:
             job (SysJob): 任务信息
         """
-        ScheduleUtil.reschedule_job(scheduler,job)
+        # 先从数据库取全量信息,避免缺失 job_group 等导致 job_key 不匹配
+        db_job = cls.select_job_by_id(job.job_id)
+        if not db_job:
+            raise ServiceException("任务不存在")
+        print(f"[job_run] 请求立即执行,job_key={db_job.job_key}, invoke_target={db_job.invoke_target}")
 
+        # 直接调用目标函数,确保“执行一次”必定执行
+        module_name, method_name, args, kwargs = ScheduleUtil.parse_target(db_job.invoke_target)
+        if not check_method_importable(module_name, method_name):
+            raise ServiceException(f"方法不存在: {db_job.invoke_target}")
+        func = getattr(importlib.import_module(module_name), method_name)
+        try:
+            func(*args, **kwargs)
+        except Exception as e:
+            raise ServiceException(f"立即执行任务失败: {e}")
 
+        # 维持调度器中的任务(下一次仍按 cron 跑)
+        sched_job = scheduler.get_job(db_job.job_key)
+        if sched_job is None:
+            ScheduleUtil.create_schedule_job(scheduler, db_job)
 
-def job_listener(event:JobEvent):
+
+def job_listener(event: JobEvent):
     """
     任务监听器
 
     Args:
         event (JobEvent): 任务事件
     """
-    job , _ = scheduler._lookup_job(event.job_id,event.jobstore)
+    job, _ = scheduler._lookup_job(event.job_id, event.jobstore)
     job_state = job.__getstate__()
     invoke_target = ScheduleUtil.unparse_target_by_funcname(
-        job_state["func"], 
-        job_state["args"], 
+        job_state["func"],
+        job_state["args"],
         job_state["kwargs"]
     )
     name = job_state["name"]
-    _,group = job_state["id"].split("_")
-    
+    _, group = job_state["id"].split("_")
+
     joblog = SysJobLog(
         job_name=name,
         job_group=group,
         invoke_target=invoke_target,
         create_time=datetime.now()
     )
-    
-    if event.code ==  EVENT_JOB_EXECUTED:
+
+    if event.code == EVENT_JOB_EXECUTED:
         pass
     elif event.code == EVENT_JOB_ERROR:
         if event.exception:
@@ -251,10 +270,10 @@ def job_listener(event:JobEvent):
 
 
 @app_completed.connect_via(reg.app)
-def init(sender:Flask):
+def init(sender: Flask):
     '''
     初始化操作
-    
+
     Args:
         sender (Flask): 消息发送者
     '''