diff --git a/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/pojo/system/SysJob.java b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/pojo/system/SysJob.java new file mode 100644 index 0000000..5864dec --- /dev/null +++ b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/pojo/system/SysJob.java @@ -0,0 +1,149 @@ +package org.leocoder.thin.domain.pojo.system; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author Leocoder + * @description [定时任务-模型] + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("sys_job") +public class SysJob implements Serializable { + + /** + * 任务ID + */ + @TableId(value = "job_id", type = IdType.ASSIGN_ID) + private Long jobId; + + /** + * 任务名称 + */ + @NotBlank(message = "任务名称不能为空") + @TableField("job_name") + private String jobName; + + /** + * 任务类型[1-管理平台 2-小程序 3-App] + */ + @NotBlank(message = "任务类型不能为空") + @TableField("job_type") + private String jobType; + + /** + * 类路径 + */ + @NotBlank(message = "类路径不能为空") + @TableField("class_path") + private String classPath; + + /** + * 方法名称 + */ + @NotBlank(message = "方法名称不能为空") + @TableField("method_name") + private String methodName; + + /** + * cron执行表达式 + */ + @NotBlank(message = "cron执行表达式不能为空") + @TableField("cron_expression") + private String cronExpression; + + /** + * cron计划策略[1-立即执行 2-执行一次 3-放弃执行] + */ + @NotBlank(message = "cron计划策略不能为空") + @TableField("policy_status") + private String policyStatus; + + /** + * 任务状态 [0正常 1暂停] + */ + @NotBlank(message = "任务状态不能为空") + @TableField("job_status") + private String jobStatus; + + /** + * 任务参数 + */ + @TableField("job_params") + private String jobParams; + + /** + * 任务备注 + */ + @TableField("remark") + private String remark; + + /** + * 创建者 + */ + @TableField("create_by") + private String createBy; + + /** + * 创建时间 + */ + @TableField(value = "create_time", fill = FieldFill.INSERT) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 更新者 + */ + @TableField("update_by") + private String updateBy; + + /** + * 更新时间 + */ + @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + + /** + * @description [获取任务状态文本] + * @author Leocoder + */ + public String getJobStatusText() { + return "0".equals(jobStatus) ? "正常" : "暂停"; + } + + /** + * @description [获取策略状态文本] + * @author Leocoder + */ + public String getPolicyStatusText() { + switch (policyStatus) { + case "1": return "立即执行"; + case "2": return "执行一次"; + case "3": return "放弃执行"; + default: return "未知"; + } + } + + /** + * @description [获取任务类型文本] + * @author Leocoder + */ + public String getJobTypeText() { + switch (jobType) { + case "1": return "管理平台"; + case "2": return "小程序"; + case "3": return "App"; + default: return "其他"; + } + } +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/pom.xml b/coder-common-thin-plugins/coder-common-thin-job/pom.xml new file mode 100644 index 0000000..b6e6c5a --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + org.leocoder.thin + coder-common-thin-plugins + ${revision} + + + coder-common-thin-job + 定时任务插件模块 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + + cn.dev33 + sa-token-spring-boot3-starter + + + + + cn.hutool + hutool-all + + + + + org.apache.commons + commons-lang3 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + org.projectlombok + lombok + true + + + + + org.leocoder.thin + coder-common-thin-common + ${revision} + + + + org.leocoder.thin + coder-common-thin-model + ${revision} + + + + org.leocoder.thin + coder-common-thin-mybatisplus + ${revision} + + + + \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/anno/EnableCoderJob.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/anno/EnableCoderJob.java new file mode 100644 index 0000000..9dbac7a --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/anno/EnableCoderJob.java @@ -0,0 +1,19 @@ +package org.leocoder.thin.job.anno; + +import org.leocoder.thin.job.config.JobConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * @author Leocoder + * @description [启用定时任务插件注解] + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({JobConfiguration.class}) +public @interface EnableCoderJob { + +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/config/JobConfiguration.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/config/JobConfiguration.java new file mode 100644 index 0000000..365f709 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/config/JobConfiguration.java @@ -0,0 +1,20 @@ +package org.leocoder.thin.job.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * @author Leocoder + * @description [定时任务插件配置类] + */ +@Slf4j +@Configuration +@ComponentScan(basePackages = "org.leocoder.thin.job") +public class JobConfiguration { + + public JobConfiguration() { + log.info("定时任务插件已启用"); + } + +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/controller/SysJobController.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/controller/SysJobController.java new file mode 100644 index 0000000..e3c2a44 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/controller/SysJobController.java @@ -0,0 +1,201 @@ +package org.leocoder.thin.job.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.cron.CronUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.leocoder.thin.domain.model.vo.system.SysJobVo; +import org.leocoder.thin.domain.pojo.system.SysJob; +import org.leocoder.thin.job.service.SysJobService; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author Leocoder + * @description [定时任务管理控制器] + */ +@Tag(name = "定时任务管理", description = "系统定时任务的增删改查和执行控制") +@Slf4j +@RequestMapping("/coder") +@RequiredArgsConstructor +@RestController +public class SysJobController { + + private final SysJobService sysJobService; + + /** + * @description [多条件分页查询] + * @author Leocoder + */ + @Operation(summary = "分页查询定时任务", description = "根据查询条件分页获取系统定时任务列表") + @SaCheckPermission("monitor:job:list") + @GetMapping("/sysJob/listPage") + public IPage listPage(SysJobVo vo) { + // 分页构造器 + Page page = new Page<>(vo.getPageNo(), vo.getPageSize()); + // 条件构造器 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(StringUtils.isNotBlank(vo.getJobName()), SysJob::getJobName, vo.getJobName()); + wrapper.eq(StringUtils.isNotBlank(vo.getJobType()), SysJob::getJobType, vo.getJobType()); + wrapper.eq(StringUtils.isNotBlank(vo.getJobStatus()), SysJob::getJobStatus, vo.getJobStatus()); + wrapper.orderByDesc(SysJob::getCreateTime); + // 进行分页查询 + page = sysJobService.page(page, wrapper); + return page; + } + + /** + * @description [查询所有] + * @author Leocoder + */ + @Operation(summary = "查询所有定时任务", description = "获取系统中所有定时任务信息") + @SaCheckPermission("monitor:job:list") + @GetMapping("/sysJob/list") + public List list(SysJobVo vo) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(StringUtils.isNotBlank(vo.getJobName()), SysJob::getJobName, vo.getJobName()); + wrapper.eq(StringUtils.isNotBlank(vo.getJobType()), SysJob::getJobType, vo.getJobType()); + wrapper.eq(StringUtils.isNotBlank(vo.getJobStatus()), SysJob::getJobStatus, vo.getJobStatus()); + wrapper.orderByDesc(SysJob::getCreateTime); + return sysJobService.list(wrapper); + } + + /** + * @description [根据ID查询数据] + * @author Leocoder + */ + @Operation(summary = "根据ID查询任务", description = "根据任务ID获取定时任务详细信息") + @SaCheckPermission("monitor:job:list") + @GetMapping("/sysJob/getById/{id}") + public SysJob getById(@PathVariable("id") Long id) { + return sysJobService.getById(id); + } + + /** + * @description [删除任务] + * @author Leocoder + */ + @Operation(summary = "删除定时任务", description = "根据任务ID删除指定的定时任务") + @SaCheckPermission("monitor:job:delete") + @PostMapping("/sysJob/deleteById/{id}") + public String deleteById(@PathVariable("id") Long id) { + try { + // 1、停止定时任务 + CronUtil.remove(id + ""); + // 2、进行删除 + boolean remove = sysJobService.removeById(id); + if (!remove) { + return "删除任务失败,请重试"; + } + return "删除成功"; + } catch (Exception e) { + log.error("删除定时任务失败", e); + return "删除任务失败:" + e.getMessage(); + } + } + + /** + * @description [批量删除] + * @author Leocoder + */ + @Operation(summary = "批量删除定时任务", description = "根据任务ID列表批量删除定时任务") + @SaCheckPermission("monitor:job:delete") + @Transactional(rollbackFor = Exception.class) + @PostMapping("/sysJob/batchDelete") + public String batchDelete(@RequestBody List jobIds) { + try { + // 1、停止定时任务 + for (Long jobId : jobIds) { + CronUtil.remove(jobId + ""); + } + // 2、批量删除 + boolean batch = sysJobService.removeBatchByIds(jobIds); + if (!batch) { + return "删除任务失败,请重试"; + } + return "批量删除成功"; + } catch (Exception e) { + log.error("批量删除定时任务失败", e); + return "删除任务失败:" + e.getMessage(); + } + } + + /** + * @description [任务调度状态修改] + * @author Leocoder + */ + @Operation(summary = "修改任务状态", description = "修改定时任务的运行状态和执行策略") + @SaCheckPermission("monitor:job:update") + @PostMapping("/sysJob/updateStatus/{id}/{jobStatus}/{policyStatus}") + public String updateStatus(@PathVariable("id") Long id, + @PathVariable("jobStatus") String jobStatus, + @PathVariable("policyStatus") String policyStatus) { + try { + sysJobService.updateStatus(id, jobStatus, policyStatus); + return "操作成功"; + } catch (Exception e) { + log.error("修改任务状态失败", e); + return "操作失败:" + e.getMessage(); + } + } + + /** + * @description [立即运行任务-执行一次] + * @author Leocoder + */ + @Operation(summary = "立即执行任务", description = "手动立即执行指定的定时任务") + @SaCheckPermission("monitor:job:run") + @GetMapping("/sysJob/runNow/{id}") + public String runNow(@PathVariable Long id) { + try { + sysJobService.runNow(id); + return "任务执行成功"; + } catch (Exception e) { + log.error("立即执行任务失败", e); + return "任务执行失败:" + e.getMessage(); + } + } + + /** + * @description [添加定时任务] + * @author Leocoder + */ + @Operation(summary = "新增定时任务", description = "创建新的定时任务配置") + @SaCheckPermission("monitor:job:add") + @PostMapping("/sysJob/add") + public String addJob(@RequestBody SysJob job) { + try { + sysJobService.addJob(job); + return "添加成功"; + } catch (Exception e) { + log.error("添加定时任务失败", e); + return "添加失败:" + e.getMessage(); + } + } + + /** + * @description [修改定时任务] + * @author Leocoder + */ + @Operation(summary = "修改定时任务", description = "更新现有定时任务的配置信息") + @SaCheckPermission("monitor:job:update") + @PostMapping("/sysJob/update") + public String updateJob(@RequestBody SysJob job) { + try { + sysJobService.updateJob(job); + return "修改成功"; + } catch (Exception e) { + log.error("修改定时任务失败", e); + return "修改失败:" + e.getMessage(); + } + } + +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/service/SysJobService.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/service/SysJobService.java new file mode 100644 index 0000000..dffecdb --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/service/SysJobService.java @@ -0,0 +1,48 @@ +package org.leocoder.thin.job.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.leocoder.thin.domain.pojo.system.SysJob; + +/** + * @author Leocoder + * @description [定时任务服务接口] + */ +public interface SysJobService extends IService { + + /** + * @description [停止任务] + * @author Leocoder + */ + void pauseJob(Long id); + + /** + * @description [启动定时任务] + * @author Leocoder + */ + void resumeJob(Long id); + + /** + * @description [任务调度状态修改] + * @author Leocoder + */ + void updateStatus(Long id, String jobStatus, String policyStatus); + + /** + * @description [立即运行任务-执行一次] + * @author Leocoder + */ + void runNow(Long id); + + /** + * @description [添加定时任务] + * @author Leocoder + */ + void addJob(SysJob job); + + /** + * @description [修改定时任务] + * @author Leocoder + */ + void updateJob(SysJob job); + +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/service/impl/SysJobServiceImpl.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/service/impl/SysJobServiceImpl.java new file mode 100644 index 0000000..1b490e0 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/service/impl/SysJobServiceImpl.java @@ -0,0 +1,392 @@ +package org.leocoder.thin.job.service.impl; + +import cn.hutool.cron.CronUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.leocoder.thin.common.satoken.CoderLoginUtil; +import org.leocoder.thin.domain.pojo.system.SysJob; +import org.leocoder.thin.job.service.SysJobService; +import org.leocoder.thin.job.task.CommonTimerTaskRunner; +import org.leocoder.thin.mybatisplus.mapper.system.SysJobMapper; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.lang.reflect.Method; + +/** + * @author Leocoder + * @description [定时任务服务实现类] + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysJobServiceImpl extends ServiceImpl implements SysJobService { + + private final SysJobMapper sysJobMapper; + + /** + * @description [停止任务] + * @author Leocoder + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void pauseJob(Long id) { + SysJob byId = this.getById(id); + if (byId == null) { + throw new RuntimeException("任务不存在"); + } + if ("1".equals(byId.getJobStatus())) { + throw new RuntimeException("该任务已处于停止状态"); + } + + CronUtil.remove(id + ""); + + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(SysJob::getJobStatus, "1"); + updateWrapper.eq(SysJob::getJobId, id); + boolean update = this.update(updateWrapper); + + if (!update) { + throw new RuntimeException("暂停任务失败,请重试"); + } + } + + /** + * @description [启动定时任务] + * @author Leocoder + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void resumeJob(Long id) { + if (null == id) { + throw new RuntimeException("该任务未查询到"); + } + + SysJob job = this.getById(id); + if (job == null) { + throw new RuntimeException("任务不存在"); + } + + // 先停止定时任务 + CronUtil.remove(id + ""); + + // 再注册定时任务 + CronUtil.schedule(job.getJobId() + "", job.getCronExpression(), () -> { + executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams()); + }); + + // 更新任务状态为正常 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(SysJob::getJobStatus, "0"); + updateWrapper.eq(SysJob::getJobId, id); + this.update(updateWrapper); + } + + /** + * @description [执行方法,多个参数传递,必须使用逗号分割,一个一个对应] + * @author Leocoder + */ + private void executeMethod(String classPath, String methodName, String params) { + if (StringUtils.isBlank(classPath)) { + throw new RuntimeException("类绝对路径不能为空"); + } + if (StringUtils.isBlank(methodName)) { + throw new RuntimeException("方法名称不能为空"); + } + + try { + // 使用SpringUtil获取类的实例 + CommonTimerTaskRunner runner = (CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(classPath)); + + Method[] methods = runner.getClass().getMethods(); + Method targetMethod = null; + + for (Method method : methods) { + if (method.getName().equals(methodName)) { + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length > 0 && StringUtils.isBlank(params)) { + throw new IllegalArgumentException("缺少参数"); + } + if (parameterTypes.length == 0 && StringUtils.isNotBlank(params)) { + throw new IllegalArgumentException("不应传递参数"); + } + if (parameterTypes.length > 0 && StringUtils.isNotBlank(params)) { + String[] paramArray = params.split(","); + if (paramArray.length != parameterTypes.length) { + throw new IllegalArgumentException("参数数量不匹配"); + } + for (int i = 0; i < paramArray.length; i++) { + Object convertedValue = convertToType(parameterTypes[i], paramArray[i]); + if (convertedValue == null) { + throw new IllegalArgumentException("参数类型不匹配"); + } + } + targetMethod = method; + } else { + targetMethod = method; + } + break; + } + } + + if (targetMethod != null) { + // 调用方法 + if (StringUtils.isNotBlank(params)) { + targetMethod.invoke(runner, extractTypedParams(targetMethod.getParameterTypes(), params)); + } else { + targetMethod.invoke(runner); + } + } else { + throw new IllegalArgumentException("指定方法不存在"); + } + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + log.error("执行定时任务出现异常", e); + throw new RuntimeException("执行定时任务出现异常:" + e.getMessage()); + } + } + + /** + * @description [多参数使用逗号分隔开] + * @author Leocoder + */ + private Object[] extractTypedParams(Class[] parameterTypes, String params) { + String[] paramArray = params.split(","); + Object[] typedParams = new Object[paramArray.length]; + for (int i = 0; i < paramArray.length; i++) { + typedParams[i] = convertToType(parameterTypes[i], paramArray[i].trim()); + } + return typedParams; + } + + /** + * @description [类型转换] + * @author Leocoder + */ + private Object convertToType(Class targetType, String value) { + if (targetType.equals(String.class)) { + return value; + } else if (targetType.equals(Integer.class) || targetType.equals(int.class)) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException ex) { + return null; + } + } else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) { + return Boolean.parseBoolean(value); + } else if (targetType.equals(Long.class) || targetType.equals(long.class)) { + try { + return Long.parseLong(value); + } catch (NumberFormatException ex) { + return null; + } + } + // 根据需要添加更多类型转换 + return null; + } + + /** + * @description [任务调度状态修改] + * @author Leocoder + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void updateStatus(Long id, String jobStatus, String policyStatus) { + if (StringUtils.isBlank(jobStatus) || id == null) { + throw new RuntimeException("请传递相关信息"); + } + + if ("0".equals(jobStatus)) { + if ("1".equals(policyStatus)) { + resumeJob(id); // 启动定时任务 + } + } else if ("1".equals(jobStatus)) { + pauseJob(id); // 停止定时任务 + } + + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.set(SysJob::getJobStatus, jobStatus); + wrapper.set(SysJob::getPolicyStatus, policyStatus); + wrapper.eq(SysJob::getJobId, id); + boolean update = this.update(wrapper); + + if (!update) { + throw new RuntimeException("操作失败,请重试"); + } + } + + /** + * @description [立即运行任务-执行一次,不影响定时调度] + * @author Leocoder + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void runNow(Long id) { + SysJob job = this.getById(id); + if (ObjectUtils.isEmpty(job) || job.getJobId() == null) { + throw new RuntimeException("未查到当前任务"); + } + + // 直接执行一次任务,不影响现有的定时调度 + executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams()); + + // 注意:这里不移除定时任务,让定时调度继续按计划运行 + log.info("立即执行任务完成,任务ID:{},定时调度继续保持运行", id); + } + + /** + * @description [添加定时任务] + * @author Leocoder + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void addJob(SysJob job) { + // 1、参数校验 + checkParams(job); + + // 2、是否添加重复的定时任务 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysJob::getClassPath, job.getClassPath()); + wrapper.eq(SysJob::getMethodName, job.getMethodName()); + wrapper.eq(SysJob::getCronExpression, job.getCronExpression()); + long count = this.count(wrapper); + if (count > 0) { + throw new RuntimeException("存在重复执行的定时任务,名称为:" + job.getJobName()); + } + + // 3、添加定时任务 + boolean save = this.save(job); + if (!save) { + throw new RuntimeException("添加失败,请重试"); + } + + // 4、根据任务状态,进行执行定时策略 + if ("0".equals(job.getJobStatus())) { + if ("1".equals(job.getPolicyStatus())) { + // 开启定时任务 + resumeJob(job.getJobId()); + } else if ("2".equals(job.getPolicyStatus())) { + // 先停止定时任务 + CronUtil.remove(job.getJobId() + ""); + // 执行一次 + executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams()); + } else { + // 停止任务 + CronUtil.remove(job.getJobId() + ""); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(SysJob::getPolicyStatus, "3"); + updateWrapper.eq(SysJob::getJobId, job.getJobId()); + boolean update = this.update(updateWrapper); + if (!update) { + throw new RuntimeException("操作失败,请重试"); + } + } + } else { + // 停止任务,计划策略并改为放弃执行 + CronUtil.remove(job.getJobId() + ""); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(SysJob::getPolicyStatus, "3"); + updateWrapper.eq(SysJob::getJobId, job.getJobId()); + boolean update = this.update(updateWrapper); + if (!update) { + throw new RuntimeException("操作失败,请重试"); + } + } + } + + /** + * @description [参数校验] + * @author Leocoder + */ + private void checkParams(SysJob job) { + // 校验表达式 + if (!CronExpression.isValidExpression(job.getCronExpression())) { + throw new RuntimeException("cron表达式:" + job.getCronExpression() + "格式不正确"); + } + + // 校验定时任务类 + try { + Class actionClass = Class.forName(job.getClassPath()); + if (!CommonTimerTaskRunner.class.isAssignableFrom(actionClass)) { + throw new RuntimeException("定时任务对应的类:" + job.getClassPath() + "不符合要求"); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException("定时任务找不到对应的类,名称为:" + job.getClassPath()); + } + } + + /** + * @description [修改定时任务] + * @author Leocoder + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void updateJob(SysJob job) { + // 1、参数校验 + checkParams(job); + if (job.getJobId() == null) { + throw new RuntimeException("请选择需要修改的任务"); + } + + // 2、是否修改为数据库已经存在的定时任务(保持与添加任务相同的检查逻辑) + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysJob::getClassPath, job.getClassPath()); + wrapper.eq(SysJob::getMethodName, job.getMethodName()); + wrapper.eq(SysJob::getCronExpression, job.getCronExpression()); + wrapper.ne(SysJob::getJobId, job.getJobId()); // 排除当前任务 + long count = this.count(wrapper); + if (count > 0) { + throw new RuntimeException("存在重复执行的定时任务,名称为:" + job.getJobName()); + } + + // 3、修改任务 + // 这里应该从登录用户上下文获取 + job.setUpdateBy(CoderLoginUtil.getLoginName()); + boolean update = this.updateById(job); + if (!update) { + throw new RuntimeException("修改失败,请重试"); + } + + // 4、根据任务状态,进行执行定时策略 + if ("0".equals(job.getJobStatus())) { + if ("1".equals(job.getPolicyStatus())) { + // 先停止定时任务,再开启定时任务 + resumeJob(job.getJobId()); + } else if ("2".equals(job.getPolicyStatus())) { + // 先停止定时任务 + CronUtil.remove(job.getJobId() + ""); + // 再开始执行一次定时任务 + executeMethod(job.getClassPath(), job.getMethodName(), job.getJobParams()); + } else { + // 停止任务,计划策略并改为放弃执行 + CronUtil.remove(job.getJobId() + ""); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(SysJob::getPolicyStatus, "3"); + updateWrapper.eq(SysJob::getJobId, job.getJobId()); + boolean updateBoolean = this.update(updateWrapper); + if (!updateBoolean) { + throw new RuntimeException("操作失败,请重试"); + } + } + } else { + // 停止任务,计划策略并改为放弃执行 + CronUtil.remove(job.getJobId() + ""); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(SysJob::getPolicyStatus, "3"); + updateWrapper.eq(SysJob::getJobId, job.getJobId()); + boolean updateBoolean = this.update(updateWrapper); + if (!updateBoolean) { + throw new RuntimeException("操作失败,请重试"); + } + } + } + +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CoderJobListener.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CoderJobListener.java new file mode 100644 index 0000000..af2f521 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CoderJobListener.java @@ -0,0 +1,67 @@ +package org.leocoder.thin.job.task; + +import cn.hutool.cron.CronUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.leocoder.thin.domain.pojo.system.SysJob; +import org.leocoder.thin.job.service.SysJobService; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; + +import java.util.List; + +/** + * @author Leocoder + * @description [定时任务监听器,系统启动时将定时任务启动] + */ +@Slf4j +@Configuration +public class CoderJobListener implements ApplicationListener, Ordered { + + @Resource + private SysJobService sysJobService; + + @SuppressWarnings("ALL") + @Override + public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) { + try { + // 查询状态正常的定时任务 AND 执行策略是立即执行类型的数据,筛选后进行开始定时任务 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysJob::getJobStatus, "0"); // 正常状态 + wrapper.eq(SysJob::getPolicyStatus, "1"); // 立即执行 + + List jobList = sysJobService.list(wrapper); + if(CollectionUtils.isNotEmpty(jobList)){ + for (SysJob sysJob : jobList) { + try { + // 启动定时任务 + sysJobService.resumeJob(sysJob.getJobId()); + log.info("自动启动定时任务:{},表达式:{}", sysJob.getJobName(), sysJob.getCronExpression()); + } catch (Exception e) { + log.error("自动启动定时任务失败:{},错误:{}", sysJob.getJobName(), e.getMessage()); + } + } + } + + // 设置秒级别的启用 + CronUtil.setMatchSecond(true); + log.info("启动定时器执行器,支持秒级精度"); + CronUtil.restart(); + + log.info("定时任务系统初始化完成,共启动{}个任务", jobList != null ? jobList.size() : 0); + + } catch (Exception e) { + log.error("定时任务系统初始化失败", e); + } + } + + @Override + public int getOrder() { + return LOWEST_PRECEDENCE; + } + +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CoderJobTimerTaskRunner.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CoderJobTimerTaskRunner.java new file mode 100644 index 0000000..3c8a46a --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CoderJobTimerTaskRunner.java @@ -0,0 +1,55 @@ +package org.leocoder.thin.job.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.leocoder.thin.job.service.SysJobService; +import org.springframework.stereotype.Component; + +/** + * @author Leocoder + * @description [定时任务执行器示例实现] + * 注意:仅支持字符串、数字、布尔值进行传递参数。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CoderJobTimerTaskRunner implements CommonTimerTaskRunner { + + private final SysJobService sysJobService; + + /** + * @description [单个参数,param可以传递参数,通过数据库的job_params] + * @author Leocoder + */ + @Override + public void paramAction(String param) { + log.info("执行单参数定时任务,参数:{}", param); + // 这里可以执行具体的业务逻辑 + // 示例:发送邮件、清理缓存、同步数据等 + } + + /** + * @description [无参数执行] + * @author Leocoder + */ + @Override + public void noParamAction() { + log.info("执行无参数定时任务"); + // 示例:查询所有定时任务并输出 + sysJobService.list().forEach(job -> + log.info("任务:{},状态:{}", job.getJobName(), job.getJobStatusText()) + ); + } + + /** + * @description [多参数执行] + * @author Leocoder + */ + @Override + public void manyParamsAction(String param1, Integer param2) { + log.info("执行多参数定时任务,参数1:{},参数2:{}", param1, param2); + // 这里可以执行具体的业务逻辑 + // 示例:根据参数处理不同类型的数据 + } + +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CommonTimerTaskRunner.java b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CommonTimerTaskRunner.java new file mode 100644 index 0000000..29f5bd0 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-job/src/main/java/org/leocoder/thin/job/task/CommonTimerTaskRunner.java @@ -0,0 +1,27 @@ +package org.leocoder.thin.job.task; + +/** + * @author Leocoder + * @description [定时任务执行器通用接口 - 所有定时任务都实现这个接口,方便我们遍历所有可用的定时任务类] + */ +public interface CommonTimerTaskRunner { + + /** + * @description [单参数执行] + * @author Leocoder + */ + void paramAction(String param); + + /** + * @description [无参数执行] + * @author Leocoder + */ + void noParamAction(); + + /** + * @description [多参数执行,多个参数传递,必须使用逗号分割,一个一个对应] + * @author Leocoder + */ + void manyParamsAction(String param1, Integer param2); + +} \ No newline at end of file diff --git a/sql/20250926-sys_menu_dict_buttons.sql b/sql/20250926-sys_menu_dict_buttons.sql deleted file mode 100644 index aab2930..0000000 --- a/sql/20250926-sys_menu_dict_buttons.sql +++ /dev/null @@ -1,26 +0,0 @@ --- 字典管理按钮权限补充数据 --- 时间:2025-09-26 --- 功能:为字典管理添加操作按钮和子菜单 - --- 为字典管理主菜单(ID:24)添加操作按钮 --- 搜索按钮 -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`) -VALUES (25, '搜索', NULL, 24, '3', '', '', '', '', 'system:dict:search', '0', '', '0', '', '0', '1', '1', '1', 1, 'admin', NOW(), 'admin', NOW()); - --- 新增按钮 -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`) -VALUES (26, '新增', NULL, 24, '3', '', '', '', '', 'system:dict:add', '0', '', '0', '', '0', '1', '1', '1', 2, 'admin', NOW(), 'admin', NOW()); - --- 修改按钮 -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`) -VALUES (27, '修改', NULL, 24, '3', '', '', '', '', 'system:dict:update', '0', '', '0', '', '0', '1', '1', '1', 3, 'admin', NOW(), 'admin', NOW()); - --- 删除按钮 -INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `en_name`, `parent_id`, `menu_type`, `path`, `name`, `component`, `icon`, `auth`, `menu_status`, `active_menu`, `is_hide`, `is_link`, `is_keep_alive`, `is_full`, `is_affix`, `is_spread`, `sorted`, `create_by`, `create_time`, `update_by`, `update_time`) -VALUES (28, '删除', NULL, 24, '3', '', '', '', '', 'system:dict:delete', '0', '', '0', '', '0', '1', '1', '1', 4, 'admin', NOW(), 'admin', NOW()); - --- 为超级管理员角色添加这些新权限(假设角色ID为1) -INSERT INTO `sys_role_menu` VALUES (1, 25); -INSERT INTO `sys_role_menu` VALUES (1, 26); -INSERT INTO `sys_role_menu` VALUES (1, 27); -INSERT INTO `sys_role_menu` VALUES (1, 28); \ No newline at end of file