feat(job): 新增定时任务插件模块
- 添加定时任务管理核心插件 coder-common-thin-job - 实现双重调度机制:Spring @Scheduled + Hutool CronUtil - 支持动态增删改查定时任务 - 支持任务启停控制和立即执行功能 - 完整的权限验证和API文档 - 支持多参数类型和异常处理机制
This commit is contained in:
parent
9b8703e192
commit
87c7e593df
@ -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 "其他";
|
||||
}
|
||||
}
|
||||
}
|
||||
85
coder-common-thin-plugins/coder-common-thin-job/pom.xml
Normal file
85
coder-common-thin-plugins/coder-common-thin-job/pom.xml
Normal 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>
|
||||
@ -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 {
|
||||
|
||||
}
|
||||
@ -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("定时任务插件已启用");
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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("操作失败,请重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
// 这里可以执行具体的业务逻辑
|
||||
// 示例:根据参数处理不同类型的数据
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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);
|
||||
Loading…
Reference in New Issue
Block a user