先看效果图:
由于是个单体项目走的【单体登录】,属于别的系统继承的方式,所以将他设置为可访问的html,便于管理,使用步骤是项目启动后,需要先保存注册内部的quartz实现,方可以实现调度任务运行
原本的 quartz 需要11 张表,但是我都用不到,我就用我自己的表,
-
QRTZ_JOB_DETAILS
存储任务的基本信息(如任务名称、所属组、实现类等)。 -
QRTZ_TRIGGERS
存储触发器的基本信息(如触发器名称、所属组、关联的任务等)。 -
QRTZ_SIMPLE_TRIGGERS
存储简单触发器(SimpleTrigger)的信息(如重复次数、间隔时间等)。 -
QRTZ_CRON_TRIGGERS
存储 cron 表达式触发器(CronTrigger)的信息(如 cron 表达式、时区等)。 -
QRTZ_SIMPROP_TRIGGERS
存储复杂触发器(SimplePropertiesTrigger)的信息(支持更多配置参数)。 -
QRTZ_BLOB_TRIGGERS
存储自定义触发器的序列化数据(用于 Quartz 不直接支持的触发器类型)。 -
QRTZ_CALENDARS
存储日历信息(用于排除特定日期,如节假日不执行任务)。 -
QRTZ_PAUSED_TRIGGER_GRPS
存储被暂停的触发器组信息。 -
QRTZ_FIRED_TRIGGERS
存储正在执行或已触发的触发器记录(用于跟踪任务执行状态)。 -
QRTZ_SCHEDULER_STATE
存储调度器的状态信息(如当前节点标识、最后启动时间等)。 -
QRTZ_LOCKS
存储分布式调度时的锁信息(用于避免多节点并发执行同一任务)。
这是我自己的业务所需要的表,独此一张
CREATE TABLE `schedule_job` (
`id` varchar(255) NOT NULL COMMENT '主键',
`task_id` varchar(255) NOT NULL COMMENT ' 任务id',
`job_class` varchar(255) DEFAULT NULL COMMENT '任务调度类名称',
`job_name` varchar(255) DEFAULT NULL COMMENT '工程名称',
`cron_expression` varchar(255) DEFAULT NULL COMMENT 'Cron表达式',
`status` varchar(1) DEFAULT NULL COMMENT '是否开启(0:未开,1:开启)',
`create_time` varchar(255) DEFAULT NULL COMMENT '创建时间',
`update_time` varchar(255) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
下面上代码:
配置类:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 请求跨域配置
*/
@Configuration
public class WebsConfig implements WebMvcConfigurer {
/**
* 允许访问静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 允许静态文件直接访问excel上传页面
registry.addResourceHandler("/html/**")
.addResourceLocations("classpath:/html/");
}
/**
* 允许跨域访问
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Content-Type", "Authorization")
.allowedHeaders("*")
.maxAge(3600);
}
}
controller 接口层面:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcky.entity.ScheduleJob;
import com.njcky.respones.CommonResult;
import com.njcky.service.ScheduleJobService;
import com.njcky.util.ToolCollectionUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.quartz.SchedulerException;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
import java.util.List;
/**
* (schedule_job)表控制层
*/
@RestController
@RequestMapping("/schedule_job")
@Api(tags = "定时任务类管理")
public class ScheduleJobController {
/**
* 服务对象
*/
@Autowired
private ScheduleJobService scheduleJobService;
/**
* 查询所有定时任务
*
* @return 单条数据
*/
@GetMapping("/list")
@ApiOperation("查询所有定时任务")
public CommonResult selectOne() {
return CommonResult.success(scheduleJobService.list());
}
/**
* 新增定时任务
*
* @param scheduleJob 定时任务对象
* @return 新增结果
*/
@PostMapping("/add")
@ApiOperation("新增定时任务")
public CommonResult add(@RequestBody ScheduleJob scheduleJob) {
scheduleJob.setCreateTime(ToolCollectionUtil.dataStr(new Date()));
scheduleJob.setTaskId(ToolCollectionUtil.UUID());
scheduleJob.setId(ToolCollectionUtil.UUID());
return CommonResult.success(scheduleJobService.createJob(scheduleJob));
}
/**
* 更新定时任务
*
* @param scheduleJob 更新后的任务信息
* @return 更新结果
*/
@PutMapping("/update")
@ApiOperation("更新定时任务")
public CommonResult update(@RequestBody ScheduleJob scheduleJob) {
return CommonResult.success(scheduleJobService.updateJob(scheduleJob));
}
/**
* 删除定时任务
*
* @param taskId 任务ID
* @return 删除结果
*/
@GetMapping("/delete")
@ApiOperation("删除定时任务")
public CommonResult delete(@RequestParam String taskId) {
return CommonResult.success(scheduleJobService.deleteJob(taskId));
}
/**
* 查询所有定时任务(分页)
*
* @param page 当前页
* @param size 每页大小
* @return 分页数据
*/
@GetMapping("/page")
@ApiOperation("获取定时任务列表分页")
public CommonResult list(@RequestParam int page,
@RequestParam int size,
@RequestParam(required = false) String search) {
Page<ScheduleJob> jobPage = new Page<>();
jobPage.setCurrent(page);
jobPage.setSize(size);
QueryWrapper<ScheduleJob> queryWrapper = new QueryWrapper<>();
if (search != null) {
queryWrapper.like("job_name", search);
} else {
queryWrapper = new QueryWrapper<>();
}
Page<ScheduleJob> scheduleJobs = scheduleJobService.page(jobPage, queryWrapper);
return CommonResult.success(scheduleJobs);
}
/**
* 启动定时任务
*
* @param taskId 任务ID
* @return 启动定时任务
*/
@GetMapping("/startTask")
@ApiOperation("启动定时任务")
public CommonResult startTask(@RequestParam String taskId) {
return CommonResult.success(scheduleJobService.startJob(taskId));
}
/**
* 停止定时任务
*
* @param taskId 任务ID
* @return 停止定时任务
*/
@GetMapping("/stopTask")
@ApiOperation("停止定时任务")
public CommonResult stopTask(@RequestParam String taskId) {
return CommonResult.success(scheduleJobService.stopJob(taskId));
}
}
service 层面:
import com.baomidou.mybatisplus.extension.service.IService;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Service;
import com.njcky.entity.ScheduleJob;
public interface ScheduleJobService extends IService<ScheduleJob> {
/**
* 创建定时任务
*
* @param job
*/
boolean createJob(ScheduleJob job);
/**
* 更新定时任务
*
* @param job
*/
boolean updateJob(ScheduleJob job);
/**
* 启动定时任务
*
* @param taskId
*/
boolean startJob(String taskId);
/**
* 停止定时任务
*
* @param taskId
*/
boolean stopJob(String taskId);
/**
* 删除定时任务
*
* @param taskId
*/
boolean deleteJob(String taskId);
}
业务实现:impl
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcky.entity.ScheduleJob;
import com.njcky.mapper.ScheduleJobMapper;
import com.njcky.service.ScheduleJobService;
import com.njcky.util.ToolCollectionUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
/**
* (ScheduleJob)表服务实现类
*/
@Slf4j
@Service
public class ScheduleJobServiceImpl extends ServiceImpl<ScheduleJobMapper, ScheduleJob> implements ScheduleJobService {
private static final String JOB_GROUP_NAME = "DEFAULT_JOB_GROUP";
private static final String TRIGGER_GROUP_NAME = "DEFAULT_TRIGGER_GROUP";
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Autowired
private ScheduleJobMapper jobMapper;
/**
* 创建定时任务
*/
@Transactional
public boolean createJob(ScheduleJob job) {
try {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
// 检查任务是否已存在
JobKey jobKey = new JobKey(job.getTaskId(), JOB_GROUP_NAME);
if (scheduler.checkExists(jobKey)) {
log.error("任务创建失败,任务已存在,taskId: {}", job.getTaskId());
return false;
}
// 设置默认值
job.setCreateTime(ToolCollectionUtil.dataStr(new Date()));
job.setStatus("0");
// 保存到数据库
int insertResult = jobMapper.insert(job);
if (insertResult <= 0) {
log.error("数据库插入失败,taskId: {}", job.getTaskId());
return false;
}
// 创建JobDetail
Class<? extends Job> jobClass = getJobClass(job.getJobClass());
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(job.getTaskId(), JOB_GROUP_NAME)
.usingJobData("taskId", job.getTaskId())
.build();
// 创建Trigger
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(job.getTaskId(), TRIGGER_GROUP_NAME)
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
.build();
// 注册任务和触发器
scheduler.scheduleJob(jobDetail, trigger);
log.info("任务创建成功,taskId: {}", job.getTaskId());
return true;
} catch (SchedulerException e) {
log.error("调度器异常,任务创建失败,taskId: {}", job.getTaskId(), e);
// 回滚数据库操作(如果已执行)
return false;
} catch (Exception e) {
log.error("系统异常,任务创建失败,taskId: {}", job.getTaskId(), e);
return false;
}
}
/**
* 更新定时任务
* 更新后自动停止定时任务
*/
@Transactional
public boolean updateJob(ScheduleJob job) {
try {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
QueryWrapper<ScheduleJob> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_id", job.getTaskId());
ScheduleJob jobResp = jobMapper.selectOne(queryWrapper);
if (jobResp == null) {
log.error("任务不存在,taskId: {}", job.getTaskId());
return false;
}
job.setId(jobResp.getId());
job.setStatus("0");
job.setCreateTime(jobResp.getCreateTime());
job.setUpdateTime(ToolCollectionUtil.dataStr(new Date()));
log.info("JOB:{}", job);
int rowsAffected = jobMapper.updateById(job);
if (rowsAffected <= 0) {
log.error("数据库更新失败,taskId: {}", job.getTaskId());
return false;
}
// 更新触发器
TriggerKey triggerKey = new TriggerKey(job.getTaskId(), TRIGGER_GROUP_NAME);
CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (oldTrigger == null) {
log.error("触发器不存在,无法更新,taskId: {}", job.getTaskId());
return false;
}
// 构建新的触发器
CronTrigger newTrigger = oldTrigger.getTriggerBuilder()
.withIdentity(triggerKey)
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
.build();
// 重新调度任务
Date scheduleTime = scheduler.rescheduleJob(triggerKey, newTrigger);
if (scheduleTime == null) {
log.error("任务重新调度失败,taskId: {}", job.getTaskId());
return false;
}
log.info("任务更新成功,taskId: {}", job.getTaskId());
return true;
} catch (SchedulerException e) {
log.error("调度器异常,任务更新失败,taskId: {}", job.getTaskId(), e);
return false;
} catch (Exception e) {
log.error("系统异常,任务更新失败,taskId: {}", job.getTaskId(), e);
return false;
}
}
/**
* 启动任务
*/
@Transactional
public boolean startJob(String taskId) {
try {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = new JobKey(taskId, JOB_GROUP_NAME);
// 先检查数据库中是否存在任务
QueryWrapper<ScheduleJob> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_id", taskId);
ScheduleJob job = jobMapper.selectOne(queryWrapper);
if (job == null) {
log.error("任务不存在,taskId: {}", taskId);
return false;
}
// 如果调度器中不存在任务,但数据库存在,则重新注册
if (!scheduler.checkExists(jobKey)) {
log.info("任务在调度器中不存在,重新注册,taskId: {}", taskId);
JobDetail jobDetail = JobBuilder.newJob(getJobClass(job.getJobClass()))
.withIdentity(taskId, JOB_GROUP_NAME)
.usingJobData("taskId", taskId)
.build();
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(taskId, TRIGGER_GROUP_NAME)
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
// 更新状态并启动任务
if (!"1".equals(job.getStatus())) {
job.setStatus("1");
job.setUpdateTime(ToolCollectionUtil.dataStr(new Date()));
jobMapper.updateById(job);
}
scheduler.resumeJob(jobKey);
log.info("任务启动成功,taskId: {}", taskId);
return true;
} catch (Exception e) {
log.error("任务启动失败,taskId: {}", taskId, e);
return false;
}
}
/**
* 暂停任务
*/
@Transactional
public boolean stopJob(String taskId) {
try {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = new JobKey(taskId, JOB_GROUP_NAME);
// 检查任务是否存在
if (!scheduler.checkExists(jobKey)) {
log.error("任务停止失败,任务不存在,taskId: {}", taskId);
return false;
}
// 查询任务信息
QueryWrapper<ScheduleJob> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_id", taskId);
ScheduleJob job = jobMapper.selectOne(queryWrapper);
// 更新任务状态(如果需要)
if (job != null && !"0".equals(job.getStatus())) {
job.setStatus("0");
job.setUpdateTime(new Date().toString());
jobMapper.updateById(job);
}
// 暂停任务
scheduler.pauseJob(jobKey);
log.info("任务停止成功,taskId: {}", taskId);
return true;
} catch (SchedulerException e) {
log.error("任务停止异常,taskId: {}", taskId, e);
return false;
} catch (Exception e) {
log.error("系统异常,taskId: {}", taskId, e);
return false;
}
}
/**
* 删除任务
*/
@Transactional
public boolean deleteJob(String taskId) {
try {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = new JobKey(taskId, JOB_GROUP_NAME);
// 尝试停止任务 (忽略任务不存在的错误)
try {
scheduler.pauseJob(jobKey);
} catch (SchedulerException e) {
log.debug("尝试暂停任务失败,可能任务不存在: {}", taskId);
}
// 尝试删除触发器 (忽略触发器不存在的错误)
try {
TriggerKey triggerKey = new TriggerKey(taskId, TRIGGER_GROUP_NAME);
scheduler.unscheduleJob(triggerKey);
} catch (SchedulerException e) {
log.debug("尝试删除触发器失败,可能触发器不存在: {}", taskId);
}
// 尝试删除任务 (忽略任务不存在的错误)
try {
scheduler.deleteJob(jobKey);
} catch (SchedulerException e) {
log.error("删除任务时发生异常,taskId: {}", taskId, e);
// 这里选择不中断流程,继续尝试删除数据库记录
}
// 删除数据库记录
QueryWrapper<ScheduleJob> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_id", taskId);
int rowsAffected = jobMapper.delete(queryWrapper);
if (rowsAffected > 0) {
log.info("数据库记录删除成功,taskId: {}", taskId);
return true;
} else {
log.warn("数据库记录删除失败或未找到记录,taskId: {}", taskId);
return false;
}
} catch (Exception e) {
log.error("系统异常,taskId: {}", taskId, e);
return false;
}
}
/**
* 根据类名获取Job实现类
*/
private Class<? extends Job> getJobClass(String className) throws SchedulerException {
try {
return (Class<? extends Job>) Class.forName(className);
} catch (ClassNotFoundException e) {
throw new SchedulerException("找不到任务类: " + className, e);
}
}
}
好像忘了什么!!!!! 哦,原来是 前端代码【前端代码有点烧火棍哈。。。 。。。】
对于咱这种菜鸟来说,作为后台自己的页面我感觉,前端差不多就行了
这是官网地址:
Tailwind CSS - 只需书写 HTML 代码,无需书写 CSS,即可快速构建美观的网站。 | TailwindCSS中文文档 | TailwindCSS中文网
https://cdn.tailwindcss.com/3.4.16
这是:tailwindcss可下载的js直接访问,复制下来保存就行,
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>定时任务管理系统</title>
<script src="../html/js/tailwindcss.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#10b981',
danger: '#ef4444',
warning: '#f59e0b',
dark: '#1e293b',
light: '#f8fafc'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.transition-bg {
transition: background-color 0.2s ease;
}
}
</style>
</head>
<body class="bg-gray-50 font-sans text-gray-800 min-h-screen flex flex-col">
<!-- 顶部导航 -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<div class="flex items-center space-x-2">
<span class="text-primary text-2xl">⏱️</span>
<h1 class="text-xl font-bold text-dark">定时任务管理系统</h1>
</div>
<div class="flex items-center space-x-4">
<button id="refreshBtn" class="flex items-center text-gray-600 hover:text-primary transition-colors">
<span>↺ 刷新</span>
</button>
<button id="addJobBtn"
class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-md flex items-center transition-bg">
<span>+ 新增任务</span>
</button>
</div>
</div>
</header>
<!-- 主内容区 -->
<main class="flex-grow container mx-auto px-4 py-6">
<!-- 搜索和筛选 -->
<div class="bg-white rounded-lg p-4 mb-6 card-shadow">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="relative w-full md:w-1/3">
<input type="text" id="searchInput" placeholder="搜索任务名称..."
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">🔍</span>
</div>
<div class="flex items-center space-x-3">
<div class="flex items-center">
<span class="text-gray-600 mr-2">每页显示:</span>
<select id="pageSizeSelect"
class="border border-gray-300 rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="5">5条</option>
<option value="10">10条</option>
<option value="20">20条</option>
</select>
</div>
<button id="searchBtn"
class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-md flex items-center transition-bg">
<span>搜索</span>
</button>
</div>
</div>
</div>
<!-- 任务列表 -->
<div class="bg-white rounded-lg overflow-hidden card-shadow mb-6">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-gray-50 border-b border-gray-200">
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">标识
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">任务编码
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">启动类名称
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
任务名称
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cron表达式
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
状态
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
创建时间
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
更新时间
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody id="jobTableBody" class="divide-y divide-gray-200">
<!-- 任务数据将通过JavaScript动态填充 -->
<tr class="text-center">
<td colspan="8" class="px-4 py-8 text-gray-500">
<span>加载中...</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="px-4 py-3 flex items-center justify-between border-t border-gray-200">
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
显示 <span id="pageStart">0</span> 到 <span id="pageEnd">0</span> 条,共 <span
id="totalCount">0</span> 条
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button id="prevPageBtn"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span>← 上一页</span>
</button>
<div id="pageNumbers" class="flex">
<!-- 页码将通过JavaScript动态填充 -->
</div>
<button id="nextPageBtn"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span>下一页 →</span>
</button>
</nav>
</div>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="bg-white border-t border-gray-200 py-4">
<div class="container mx-auto px-4 text-center text-gray-500 text-sm">
<p>© 2025 定时任务管理系统 - 版权所有</p>
</div>
</footer>
<!-- 新增/编辑任务模态框 -->
<div id="jobModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg w-full max-w-md mx-4 overflow-hidden shadow-xl">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 id="modalTitle" class="text-lg font-medium text-gray-900">新增任务</h3>
<button id="closeModalBtn" class="text-gray-400 hover:text-gray-500 focus:outline-none">
<span>×</span>
</button>
</div>
<div class="px-6 py-4">
<form id="jobForm" class="space-y-4">
<input type="hidden" id="jobTaskId"> <!-- 改为taskId -->
<div>
<label for="jobName" class="block text-sm font-medium text-gray-700 mb-1">任务名称</label>
<input type="text" id="jobName" name="jobName" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<p class="mt-1 text-xs text-gray-500">例如: 游客数据统计(任务执行任务名称)</p>
</div>
<div>
<label for="jobClass" class="block text-sm font-medium text-gray-700 mb-1">启动类名称</label>
<input type="text" id="jobClass" name="jobClass" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<p class="mt-1 text-xs text-gray-500">例如: com.njcky.task.TestJob(任务执行的全类名)</p>
</div>
<div>
<label for="cronExpression" class="block text-sm font-medium text-gray-700 mb-1">Cron表达式</label>
<input type="text" id="cronExpression" name="cronExpression" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<p class="mt-1 text-xs text-gray-500">例如: 0 0/10 * * * ? 表示每10分钟执行一次</p>
</div>
</form>
</div>
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-3">
<button id="cancelModalBtn"
class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
取消
</button>
<button id="saveJobBtn"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
保存
</button>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg w-full max-w-md mx-4 overflow-hidden shadow-xl">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">确认删除</h3>
</div>
<div class="px-6 py-4">
<p class="text-sm text-gray-700">你确定要删除这个定时任务吗?此操作不可撤销。</p>
<input type="hidden" id="deleteJobTaskId"> <!-- 改为taskId -->
</div>
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-3">
<button id="cancelDeleteBtn"
class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
取消
</button>
<button id="confirmDeleteBtn"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-danger hover:bg-danger/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-danger">
确认删除
</button>
</div>
</div>
</div>
<!-- 消息提示 -->
<div id="toast"
class="fixed bottom-4 right-4 bg-dark text-white px-4 py-3 rounded-md shadow-lg transform translate-y-10 opacity-0 transition-all duration-300 flex items-center z-50">
<span id="toastIcon" class="mr-2">✓</span>
<span id="toastMessage"></span>
</div>
<script>
// API基础URL
const API_BASE_URL = 'http://192.168.2.102:8889/schedule_job';
// 当前页码和每页大小
let currentPage = 1;
let pageSize = 5;
let searchTerm = '';
let totalPages = 1;
// DOM元素(更新了部分ID)
const jobTableBody = document.getElementById('jobTableBody');
const pageNumbers = document.getElementById('pageNumbers');
const prevPageBtn = document.getElementById('prevPageBtn');
const nextPageBtn = document.getElementById('nextPageBtn');
const pageStart = document.getElementById('pageStart');
const pageEnd = document.getElementById('pageEnd');
const totalCount = document.getElementById('totalCount');
const refreshBtn = document.getElementById('refreshBtn');
const addJobBtn = document.getElementById('addJobBtn');
const jobModal = document.getElementById('jobModal');
const closeModalBtn = document.getElementById('closeModalBtn');
const cancelModalBtn = document.getElementById('cancelModalBtn');
const saveJobBtn = document.getElementById('saveJobBtn');
const modalTitle = document.getElementById('modalTitle');
const jobTaskIdInput = document.getElementById('jobTaskId'); // 改为taskId
const jobNameInput = document.getElementById('jobName');
const jobClassInput = document.getElementById('jobClass');
const cronExpressionInput = document.getElementById('cronExpression');
const deleteModal = document.getElementById('deleteModal');
const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
const deleteJobTaskIdInput = document.getElementById('deleteJobTaskId'); // 改为taskId
const toast = document.getElementById('toast');
const toastIcon = document.getElementById('toastIcon');
const toastMessage = document.getElementById('toastMessage');
const searchBtn = document.getElementById('searchBtn');
const searchInput = document.getElementById('searchInput');
const pageSizeSelect = document.getElementById('pageSizeSelect');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadJobs();
setupEventListeners();
});
// 设置事件监听器
function setupEventListeners() {
// 分页控制
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
loadJobs();
}
});
nextPageBtn.addEventListener('click', () => {
if (currentPage < totalPages) {
currentPage++;
loadJobs();
}
});
// 刷新
refreshBtn.addEventListener('click', loadJobs);
// 新增任务
addJobBtn.addEventListener('click', () => {
modalTitle.textContent = '新增任务';
jobTaskIdInput.value = ''; // 清空taskId
jobNameInput.value = '';
jobClassInput.value = '';
cronExpressionInput.value = '';
jobModal.classList.remove('hidden');
});
// 关闭模态框
closeModalBtn.addEventListener('click', () => {
jobModal.classList.add('hidden');
});
cancelModalBtn.addEventListener('click', () => {
jobModal.classList.add('hidden');
});
// 保存任务
saveJobBtn.addEventListener('click', saveJob);
// 删除任务
cancelDeleteBtn.addEventListener('click', () => {
deleteModal.classList.add('hidden');
});
confirmDeleteBtn.addEventListener('click', deleteJob);
// 搜索
searchBtn.addEventListener('click', () => {
searchTerm = searchInput.value.trim();
currentPage = 1;
loadJobs();
});
// 回车键搜索
searchInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
searchTerm = searchInput.value.trim();
currentPage = 1;
loadJobs();
}
});
// 每页显示数量变化
pageSizeSelect.addEventListener('change', () => {
pageSize = parseInt(pageSizeSelect.value);
currentPage = 1;
loadJobs();
});
}
// 加载任务列表
function loadJobs() {
jobTableBody.innerHTML = `
<tr class="text-center">
<td colspan="8" class="px-4 py-8 text-gray-500">
<span>加载中...</span>
</td>
</tr>
`;
// 构建请求URL(支持搜索)
let url = `${API_BASE_URL}/page?page=${currentPage}&size=${pageSize}`;
if (searchTerm) {
url += `&search=${encodeURIComponent(searchTerm)}`;
}
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('加载任务列表失败');
}
return response.json();
})
.then(data => {
if (data.code !== 200) {
throw new Error(data.message || '加载任务列表失败');
}
const jobs = data.data.records;
const total = data.data.total;
totalPages = data.data.pages;
renderJobs(jobs);
updatePagination(total);
})
.catch(error => {
showToast('error', error.message || '加载任务列表失败');
jobTableBody.innerHTML = `
<tr class="text-center">
<td colspan="8" class="px-4 py-8 text-gray-500">
<span>加载失败,请重试</span>
</td>
</tr>
`;
});
}
// 渲染任务列表(新增状态列和启动/停止按钮)
function renderJobs(jobs) {
if (jobs.length === 0) {
jobTableBody.innerHTML = `
<tr class="text-center">
<td colspan="8" class="px-4 py-8 text-gray-500">
<span>没有找到匹配的任务</span>
</td>
</tr>
`;
return;
}
jobTableBody.innerHTML = '';
jobs.forEach(job => {
const row = document.createElement('tr');
// 状态显示:1=运行中,0=已停止
const statusText = job.status === '1' ? '运行中' : '已停止';
const statusClass = job.status === '1' ? 'text-secondary' : 'text-gray-500';
// 操作按钮:根据状态显示启动/停止
const actionButtons = job.status === '1'
? `<button class="stop-btn text-warning hover:text-warning/80 transition-colors mr-2" data-taskid="${job.taskId}">
<span>停止</span>
</button>`
: `<button class="start-btn text-secondary hover:text-secondary/80 transition-colors mr-2" data-taskid="${job.taskId}">
<span>启动</span>
</button>`;
row.innerHTML = `
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${job.id}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${job.taskId}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">${job.jobName}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${job.jobClass}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${job.cronExpression}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm ${statusClass}">${statusText}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${formatDateTime(job.createTime)}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${formatDateTime(job.updateTime)}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
<div class="flex space-x-2">
${actionButtons}
<button class="edit-btn text-primary hover:text-primary/80 transition-colors mr-2" data-taskid="${job.taskId}">
<span>编辑</span>
</button>
<button class="delete-btn text-danger hover:text-danger/80 transition-colors" data-taskid="${job.taskId}">
<span>删除</span>
</button>
</div>
</td>
`;
row.className = 'hover:bg-gray-50 transition-bg';
jobTableBody.appendChild(row);
// 启动按钮事件
if (row.querySelector('.start-btn')) {
row.querySelector('.start-btn').addEventListener('click', (e) => {
const taskId = e.currentTarget.dataset.taskid;
startJob(taskId);
});
}
// 停止按钮事件
if (row.querySelector('.stop-btn')) {
row.querySelector('.stop-btn').addEventListener('click', (e) => {
const taskId = e.currentTarget.dataset.taskid;
stopJob(taskId);
});
}
// 编辑按钮事件
row.querySelector('.edit-btn').addEventListener('click', (e) => {
const taskId = e.currentTarget.dataset.taskid;
editJob(taskId);
});
// 删除按钮事件
row.querySelector('.delete-btn').addEventListener('click', (e) => {
const taskId = e.currentTarget.dataset.taskid;
deleteJobTaskIdInput.value = taskId;
deleteModal.classList.remove('hidden');
});
});
}
// 更新分页控件
function updatePagination(total) {
// 更新页码信息
const start = (currentPage - 1) * pageSize + 1;
const end = Math.min(currentPage * pageSize, total);
pageStart.textContent = start;
pageEnd.textContent = end;
totalCount.textContent = total;
// 更新页码按钮
pageNumbers.innerHTML = '';
// 计算显示的页码范围
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(startPage + 4, totalPages);
if (endPage - startPage < 4 && startPage > 1) {
startPage = Math.max(1, endPage - 4);
}
// 添加第一页按钮
if (startPage > 1) {
addPageButton(1);
if (startPage > 2) {
addEllipsis();
}
}
// 添加页码按钮
for (let i = startPage; i <= endPage; i++) {
addPageButton(i);
}
// 添加最后一页按钮
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
addEllipsis();
}
addPageButton(totalPages);
}
// 禁用/启用上一页、下一页按钮
prevPageBtn.disabled = currentPage === 1;
prevPageBtn.classList.toggle('opacity-50', currentPage === 1);
nextPageBtn.disabled = currentPage === totalPages;
nextPageBtn.classList.toggle('opacity-50', currentPage === totalPages);
}
// 添加页码按钮
function addPageButton(pageNum) {
const button = document.createElement('button');
button.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 ${currentPage === pageNum ? 'bg-primary text-white border-primary' : ''}`;
button.textContent = pageNum;
button.addEventListener('click', () => {
if (currentPage !== pageNum) {
currentPage = pageNum;
loadJobs();
}
});
pageNumbers.appendChild(button);
}
// 添加省略号
function addEllipsis() {
const ellipsis = document.createElement('span');
ellipsis.className = 'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700';
ellipsis.textContent = '...';
ellipsis.style.pointerEvents = 'none';
pageNumbers.appendChild(ellipsis);
}
// 编辑任务(基于taskId)
function editJob(taskId) {
// 通过taskId查找表格行
const row = document.querySelector(`tr:has(.edit-btn[data-taskid="${taskId}"])`);
if (!row) {
showToast('error', '找不到该任务');
return;
}
// 从表格行提取数据(根据列索引)
const jobClass = row.querySelector('td:nth-child(4)').textContent;
const jobName = row.querySelector('td:nth-child(3)').textContent;
const cronExpression = row.querySelector('td:nth-child(5)').textContent;
// 填充表单
modalTitle.textContent = '编辑任务';
jobTaskIdInput.value = taskId;
jobNameInput.value = jobName;
jobClassInput.value = jobClass;
cronExpressionInput.value = cronExpression;
jobModal.classList.remove('hidden');
}
// 保存任务(使用taskId)
function saveJob() {
const taskId = jobTaskIdInput.value;
const jobName = jobNameInput.value.trim();
const jobClass = jobClassInput.value.trim();
const cronExpression = cronExpressionInput.value.trim();
// 简单验证
if (!jobName) {
showToast('error', '请输入任务名称');
return;
}
if (!jobClass) {
showToast('error', '请输入启动类名称');
return;
}
if (!cronExpression) {
showToast('error', '请输入Cron表达式');
return;
}
// 验证Cron表达式格式 - 支持步进值和特殊字符,修正步进值范围
const cronPattern = /^(\*|([0-5]?[0-9]|([0-5]?[0-9]\/[1-9][0-9]*)))(\s+|\b)(\*|([01]?[0-9]|2[0-3]|([01]?[0-9]\/[1-9][0-9]*)))(\s+|\b)(\*|([1-9]|[12][0-9]|3[01]|([1-9]\/[1-9][0-9]*)))(\s+|\b)(\*|([1-9]|1[012]|([1-9]\/[1-9][0-9]*)))(\s+|\b)(\*|([0-6]|([0-6]\/[1-9][0-9]*)))(\s+|\b)(\?|\*)$/;
if (!cronPattern.test(cronExpression)) {
showToast('error', 'Cron表达式格式不正确');
return;
}
// 准备请求数据
const jobData = {
taskId: taskId,
jobClass: jobClass,
jobName: jobName,
cronExpression: cronExpression
};
// 确定请求方法和URL
const method = taskId ? 'PUT' : 'POST';
const url = taskId ? `${API_BASE_URL}/update` : `${API_BASE_URL}/add`;
// 显示加载状态
saveJobBtn.disabled = true;
saveJobBtn.textContent = '保存中...';
// 发送请求
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jobData)
})
.then(response => {
// 先检查HTTP状态码
if (!response.ok) {
// 尝试解析错误信息
return response.json().then(errData => {
throw new Error(errData.message || `HTTP错误 ${response.status}`);
}).catch(() => {
// 如果无法解析JSON,使用默认错误信息
throw new Error(`HTTP错误 ${response.status}`);
});
}
return response.json();
})
.then(data => {
// 检查业务状态码
if (data.code !== 200) {
throw new Error(data.message || '保存任务失败');
}
showToast('success', taskId ? '任务更新成功' : '任务创建成功');
jobModal.classList.add('hidden');
loadJobs(); // 刷新列表
})
.catch(error => {
// 显示更详细的错误信息
showToast('error', `操作失败: ${error.message}`);
console.error('保存任务时出错:', error);
})
.finally(() => {
saveJobBtn.disabled = false;
saveJobBtn.textContent = '保存';
});
}
// 删除任务(使用taskId)
function deleteJob() {
const taskId = deleteJobTaskIdInput.value;
fetch(`${API_BASE_URL}/delete?taskId=${taskId}`, { // 调整为taskId参数
method: 'GET'
})
.then(response => {
if (!response.ok) {
throw new Error('删除任务失败');
}
return response.json();
})
.then(data => {
if (data.code !== 200) {
throw new Error(data.message || '删除任务失败');
}
showToast('success', '任务删除成功');
deleteModal.classList.add('hidden');
loadJobs();
})
.catch(error => {
showToast('error', error.message || '删除任务失败');
});
}
// 启动任务(对接后端startTask接口)
function startJob(taskId) {
fetch(`${API_BASE_URL}/startTask?taskId=${taskId}`, {
method: 'GET'
})
.then(response => {
if (!response.ok) {
throw new Error('启动任务失败');
}
return response.json();
})
.then(data => {
if (data.code !== 200 || !data.data) { // 后端返回true表示成功
throw new Error(data.message || '启动任务失败');
}
showToast('success', '任务启动成功');
loadJobs(); // 刷新列表
})
.catch(error => {
showToast('error', error.message || '启动任务失败');
});
}
// 停止任务(对接后端stopTask接口)
function stopJob(taskId) {
fetch(`${API_BASE_URL}/stopTask?taskId=${taskId}`, {
method: 'GET'
})
.then(response => {
if (!response.ok) {
throw new Error('停止任务失败');
}
return response.json();
})
.then(data => {
if (data.code !== 200 || !data.data) { // 后端返回true表示成功
throw new Error(data.message || '停止任务失败');
}
showToast('success', '任务停止成功');
loadJobs(); // 刷新列表
})
.catch(error => {
showToast('error', error.message || '停止任务失败');
});
}
// 显示提示消息
function showToast(type, message) {
toastMessage.textContent = message;
if (type === 'success') {
toastIcon.textContent = '✓';
toast.classList.remove('bg-danger');
toast.classList.add('bg-secondary');
} else {
toastIcon.textContent = '✗';
toast.classList.remove('bg-secondary');
toast.classList.add('bg-danger');
}
toast.classList.remove('translate-y-10', 'opacity-0');
toast.classList.add('translate-y-0', 'opacity-100');
setTimeout(() => {
toast.classList.remove('translate-y-0', 'opacity-100');
toast.classList.add('translate-y-10', 'opacity-0');
}, 3000);
}
// 格式化日期时间
function formatDateTime(dateTimeStr) {
if (!dateTimeStr) return '';
// 处理时间戳或字符串格式
const date = new Date(dateTimeStr);
if (isNaN(date.getTime())) {
return dateTimeStr; // 无法解析时返回原始字符串
}
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
</script>
</body>
</html>
还等什么【罩起来】。。。。。。