feat(job): 新增定时任务插件模块

- 添加定时任务管理核心插件 coder-common-thin-job
- 实现双重调度机制:Spring @Scheduled + Hutool CronUtil
- 支持动态增删改查定时任务
- 支持任务启停控制和立即执行功能
- 完整的权限验证和API文档
- 支持多参数类型和异常处理机制
This commit is contained in:
Leo 2025-09-27 14:19:46 +08:00
parent 9b8703e192
commit 87c7e593df
11 changed files with 1063 additions and 26 deletions

View File

@ -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 "其他";
}
}
}

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.thin</groupId>
<artifactId>coder-common-thin-plugins</artifactId>
<version>${revision}</version>
</parent>
<artifactId>coder-common-thin-job</artifactId>
<description>定时任务插件模块</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- Apache Commons Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- SpringDoc OpenAPI 3.0 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 项目内部依赖 -->
<dependency>
<groupId>org.leocoder.thin</groupId>
<artifactId>coder-common-thin-common</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.leocoder.thin</groupId>
<artifactId>coder-common-thin-model</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.leocoder.thin</groupId>
<artifactId>coder-common-thin-mybatisplus</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@ -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 {
}

View File

@ -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("定时任务插件已启用");
}
}

View File

@ -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<SysJob> listPage(SysJobVo vo) {
// 分页构造器
Page<SysJob> page = new Page<>(vo.getPageNo(), vo.getPageSize());
// 条件构造器
LambdaQueryWrapper<SysJob> 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<SysJob> list(SysJobVo vo) {
LambdaQueryWrapper<SysJob> 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<Long> 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();
}
}
}

View File

@ -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<SysJob> {
/**
* @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);
}

View File

@ -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<SysJobMapper, SysJob> 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<SysJob> 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<SysJob> 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<SysJob> 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<SysJob> 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<SysJob> 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<SysJob> 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<SysJob> 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<SysJob> 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<SysJob> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysJob::getPolicyStatus, "3");
updateWrapper.eq(SysJob::getJobId, job.getJobId());
boolean updateBoolean = this.update(updateWrapper);
if (!updateBoolean) {
throw new RuntimeException("操作失败,请重试");
}
}
}
}

View File

@ -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<ApplicationStartedEvent>, Ordered {
@Resource
private SysJobService sysJobService;
@SuppressWarnings("ALL")
@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
try {
// 查询状态正常的定时任务 AND 执行策略是立即执行类型的数据筛选后进行开始定时任务
LambdaQueryWrapper<SysJob> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysJob::getJobStatus, "0"); // 正常状态
wrapper.eq(SysJob::getPolicyStatus, "1"); // 立即执行
List<SysJob> 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;
}
}

View File

@ -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);
// 这里可以执行具体的业务逻辑
// 示例根据参数处理不同类型的数据
}
}

View File

@ -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);
}

View File

@ -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);