springboot 基于quartz 实现的单机可管理的调度任务(最新推荐)
先看效果图:
由于是个单体项目走的【单体登录】,属于别的系统继承的方式,所以将他设置为可访问的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.Res编程ourceHandlerRegistry; 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 faandroidlse; } } /** * 删除任务 */ @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> &编程客栈lt;!-- 分页控件 --> <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) { jobTablpythoneBody.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 && startPaTwPNlFfutBge > 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>
还等什么【罩起来】。。。。。。
到此这篇关于springboot 基于quartz 实现的单机可管理的调度任务的文章就介绍到这了,更多相关springboot quartz调度任务内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论