我将帮您创建一个完整的Spring Boot + React项目,使用Quartz调度任务。这个项目包含后端API和前端界面。
项目结构
text
task-scheduler/
├── backend/ # Spring Boot后端
└── frontend/ # React前端
后端实现 (Spring Boot)
1. pom.xml
xml
<?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.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
</parent>
<groupId>com.example</groupId>
<artifactId>task-scheduler</artifactId>
<version>1.0.0</version>
<name>task-scheduler</name>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. application.yml
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/task_scheduler?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always
properties:
org:
quartz:
scheduler:
instanceName: TaskScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: false
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
server:
port: 8080
logging:
level:
com.example: DEBUG
3. 实体类
java
// TaskExecution.java
package com.example.taskscheduler.entity;
import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "task_execution")
@Data
public class TaskExecution {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String taskName;
@Column(nullable = false)
private String jobGroup;
@Column(nullable = false)
private LocalDateTime startTime;
private LocalDateTime endTime;
@Column(length = 50)
private String status; // SUCCESS, FAILED, RUNNING, SKIPPED
@Column(columnDefinition = "TEXT")
private String result;
@Column(columnDefinition = "TEXT")
private String errorMessage;
@Column(nullable = false)
private Long executionDuration; // 执行耗时(毫秒)
private LocalDateTime createTime;
@PrePersist
protected void onCreate() {
createTime = LocalDateTime.now();
}
}
java
// ScheduledTask.java
package com.example.taskscheduler.entity;
import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "scheduled_task")
@Data
public class ScheduledTask {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String taskName;
@Column(nullable = false)
private String jobGroup;
@Column(nullable = false)
private String cronExpression;
private String description;
private Boolean enabled = true;
private LocalDateTime lastExecutionTime;
private LocalDateTime nextExecutionTime;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@PrePersist
protected void onCreate() {
createTime = LocalDateTime.now();
updateTime = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updateTime = LocalDateTime.now();
}
}
4. Repository层
java
// TaskExecutionRepository.java
package com.example.taskscheduler.repository;
import com.example.taskscheduler.entity.TaskExecution;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface TaskExecutionRepository extends JpaRepository<TaskExecution, Long> {
Page<TaskExecution> findByTaskNameContainingOrStatusContaining(
String taskName, String status, Pageable pageable);
Page<TaskExecution> findByTaskName(String taskName, Pageable pageable);
List<TaskExecution> findByTaskNameAndStartTimeBetween(
String taskName, LocalDateTime start, LocalDateTime end);
Page<TaskExecution> findByStatus(String status, Pageable pageable);
}
java
// ScheduledTaskRepository.java
package com.example.taskscheduler.repository;
import com.example.taskscheduler.entity.ScheduledTask;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ScheduledTaskRepository extends JpaRepository<ScheduledTask, Long> {
Optional<ScheduledTask> findByTaskName(String taskName);
List<ScheduledTask> findByEnabled(Boolean enabled);
}
5. Quartz Job实现
java
// BaseJob.java
package com.example.taskscheduler.job;
import com.example.taskscheduler.entity.TaskExecution;
import com.example.taskscheduler.service.TaskExecutionService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public abstract class BaseJob implements Job {
@Autowired
private TaskExecutionService taskExecutionService;
@Override
public void execute(JobExecutionContext context) {
String taskName = context.getJobDetail().getKey().getName();
String jobGroup = context.getJobDetail().getKey().getGroup();
TaskExecution execution = new TaskExecution();
execution.setTaskName(taskName);
execution.setJobGroup(jobGroup);
execution.setStartTime(LocalDateTime.now());
execution.setStatus("RUNNING");
long startTime = System.currentTimeMillis();
try {
// 执行具体任务
String result = doExecute(context);
long endTime = System.currentTimeMillis();
execution.setEndTime(LocalDateTime.now());
execution.setStatus("SUCCESS");
execution.setResult(result);
execution.setExecutionDuration(endTime - startTime);
} catch (Exception e) {
long endTime = System.currentTimeMillis();
execution.setEndTime(LocalDateTime.now());
execution.setStatus("FAILED");
execution.setErrorMessage(e.getMessage());
execution.setExecutionDuration(endTime - startTime);
e.printStackTrace();
}
taskExecutionService.saveExecution(execution);
}
protected abstract String doExecute(JobExecutionContext context) throws Exception;
}
java
// SampleJob.java
package com.example.taskscheduler.job;
import org.quartz.JobExecutionContext;
import org.springframework.stereotype.Component;
@Component
public class SampleJob extends BaseJob {
@Override
protected String doExecute(JobExecutionContext context) throws Exception {
// 示例任务:执行一些业务逻辑
String taskParam = context.getMergedJobDataMap().getString("taskParam");
// 模拟任务执行
Thread.sleep(2000);
return "任务执行成功!参数: " + taskParam + ", 时间: " + new java.util.Date();
}
}
6. Service层
java
// TaskExecutionService.java
package com.example.taskscheduler.service;
import com.example.taskscheduler.entity.TaskExecution;
import com.example.taskscheduler.repository.TaskExecutionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@Service
public class TaskExecutionService {
@Autowired
private TaskExecutionRepository taskExecutionRepository;
public TaskExecution saveExecution(TaskExecution execution) {
return taskExecutionRepository.save(execution);
}
public Page<TaskExecution> getExecutions(String keyword, Pageable pageable) {
if (keyword != null && !keyword.isEmpty()) {
return taskExecutionRepository.findByTaskNameContainingOrStatusContaining(
keyword, keyword, pageable);
}
return taskExecutionRepository.findAll(pageable);
}
public Page<TaskExecution> getExecutionsByTask(String taskName, Pageable pageable) {
return taskExecutionRepository.findByTaskName(taskName, pageable);
}
public TaskExecution getExecutionById(Long id) {
return taskExecutionRepository.findById(id).orElse(null);
}
}
java
// TaskSchedulerService.java
package com.example.taskscheduler.service;
import com.example.taskscheduler.entity.ScheduledTask;
import com.example.taskscheduler.repository.ScheduledTaskRepository;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
@Service
public class TaskSchedulerService {
@Autowired
private Scheduler scheduler;
@Autowired
private ScheduledTaskRepository scheduledTaskRepository;
public void scheduleJob(ScheduledTask task, Class<? extends Job> jobClass) throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(task.getTaskName(), task.getJobGroup())
.usingJobData("taskParam", task.getDescription())
.build();
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(task.getTaskName() + "Trigger", task.getJobGroup())
.withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression()))
.build();
scheduler.scheduleJob(jobDetail, trigger);
// 计算下次执行时间
Date nextFireTime = trigger.getNextFireTime();
if (nextFireTime != null) {
task.setNextExecutionTime(LocalDateTime.ofInstant(
nextFireTime.toInstant(), ZoneId.systemDefault()));
}
scheduledTaskRepository.save(task);
}
public void updateJob(ScheduledTask task) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(task.getTaskName() + "Trigger", task.getJobGroup());
CronTrigger newTrigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression()))
.build();
scheduler.rescheduleJob(triggerKey, newTrigger);
Date nextFireTime = newTrigger.getNextFireTime();
if (nextFireTime != null) {
task.setNextExecutionTime(LocalDateTime.ofInstant(
nextFireTime.toInstant(), ZoneId.systemDefault()));
}
scheduledTaskRepository.save(task);
}
public void pauseJob(String taskName, String jobGroup) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(taskName, jobGroup);
scheduler.pauseJob(jobKey);
}
public void resumeJob(String taskName, String jobGroup) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(taskName, jobGroup);
scheduler.resumeJob(jobKey);
}
public void deleteJob(String taskName, String jobGroup) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(taskName, jobGroup);
scheduler.deleteJob(jobKey);
}
}
7. Controller层
java
// TaskController.java
package com.example.taskscheduler.controller;
import com.example.taskscheduler.entity.ScheduledTask;
import com.example.taskscheduler.entity.TaskExecution;
import com.example.taskscheduler.job.SampleJob;
import com.example.taskscheduler.service.TaskExecutionService;
import com.example.taskscheduler.service.TaskSchedulerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/tasks")
@CrossOrigin(origins = "http://localhost:3000")
public class TaskController {
@Autowired
private TaskSchedulerService schedulerService;
@Autowired
private TaskExecutionService executionService;
@PostMapping("/schedule")
public ResponseEntity<?> scheduleTask(@Valid @RequestBody ScheduledTask task) {
try {
task.setEnabled(true);
schedulerService.scheduleJob(task, SampleJob.class);
return ResponseEntity.ok(Map.of("success", true, "message", "任务调度成功"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", e.getMessage()));
}
}
@PutMapping("/{taskName}/{jobGroup}")
public ResponseEntity<?> updateTask(@PathVariable String taskName,
@PathVariable String jobGroup,
@RequestBody ScheduledTask task) {
try {
task.setTaskName(taskName);
task.setJobGroup(jobGroup);
schedulerService.updateJob(task);
return ResponseEntity.ok(Map.of("success", true, "message", "任务更新成功"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", e.getMessage()));
}
}
@PostMapping("/{taskName}/{jobGroup}/pause")
public ResponseEntity<?> pauseTask(@PathVariable String taskName, @PathVariable String jobGroup) {
try {
schedulerService.pauseJob(taskName, jobGroup);
return ResponseEntity.ok(Map.of("success", true, "message", "任务已暂停"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", e.getMessage()));
}
}
@PostMapping("/{taskName}/{jobGroup}/resume")
public ResponseEntity<?> resumeTask(@PathVariable String taskName, @PathVariable String jobGroup) {
try {
schedulerService.resumeJob(taskName, jobGroup);
return ResponseEntity.ok(Map.of("success", true, "message", "任务已恢复"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", e.getMessage()));
}
}
@DeleteMapping("/{taskName}/{jobGroup}")
public ResponseEntity<?> deleteTask(@PathVariable String taskName, @PathVariable String jobGroup) {
try {
schedulerService.deleteJob(taskName, jobGroup);
return ResponseEntity.ok(Map.of("success", true, "message", "任务已删除"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("success", false, "message", e.getMessage()));
}
}
@GetMapping("/executions")
public ResponseEntity<Map<String, Object>> getExecutions(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {
Page<TaskExecution> executions = executionService.getExecutions(keyword, PageRequest.of(page, size));
Map<String, Object> response = new HashMap<>();
response.put("executions", executions.getContent());
response.put("currentPage", executions.getNumber());
response.put("totalItems", executions.getTotalElements());
response.put("totalPages", executions.getTotalPages());
return ResponseEntity.ok(response);
}
@GetMapping("/executions/{id}")
public ResponseEntity<TaskExecution> getExecutionById(@PathVariable Long id) {
TaskExecution execution = executionService.getExecutionById(id);
if (execution != null) {
return ResponseEntity.ok(execution);
}
return ResponseEntity.notFound().build();
}
}
8. 主启动类
java
// TaskSchedulerApplication.java
package com.example.taskscheduler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TaskSchedulerApplication {
public static void main(String[] args) {
SpringApplication.run(TaskSchedulerApplication.class, args);
}
}
前端实现 (React)
1. package.json
json
{
"name": "task-scheduler-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.4.0",
"react-router-dom": "^6.14.0",
"antd": "^5.7.0",
"@ant-design/icons": "^5.1.4",
"dayjs": "^1.11.9"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"devDependencies": {
"react-scripts": "5.0.1"
}
}
2. 主要组件代码
jsx
// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { Layout, Menu } from 'antd';
import TaskManagement from './components/TaskManagement';
import ExecutionHistory from './components/ExecutionHistory';
import CreateTask from './components/CreateTask';
import {
ScheduleOutlined,
HistoryOutlined,
PlusCircleOutlined,
} from '@ant-design/icons';
const { Header, Content, Sider } = Layout;
function App() {
return (
<Router>
<Layout style={{ minHeight: '100vh' }}>
<Sider width={200} theme="dark">
<div style={{ height: 32, margin: 16, color: 'white', fontSize: 18, textAlign: 'center' }}>
任务调度系统
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <ScheduleOutlined />,
label: <Link to="/">任务列表</Link>,
},
{
key: '2',
icon: <PlusCircleOutlined />,
label: <Link to="/create">创建任务</Link>,
},
{
key: '3',
icon: <HistoryOutlined />,
label: <Link to="/history">执行历史</Link>,
},
]}
/>
</Sider>
<Layout>
<Header style={{ background: '#fff', padding: 0 }} />
<Content style={{ margin: '16px' }}>
<div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
<Routes>
<Route path="/" element={<TaskManagement />} />
<Route path="/create" element={<CreateTask />} />
<Route path="/history" element={<ExecutionHistory />} />
</Routes>
</div>
</Content>
</Layout>
</Layout>
</Router>
);
}
export default App;
jsx
// src/components/TaskManagement.js
import React, { useState, useEffect } from 'react';
import { Table, Button, Space, message, Popconfirm, Tag } from 'antd';
import { PlayCircleOutlined, PauseCircleOutlined, DeleteOutlined } from '@ant-design/icons';
import axios from 'axios';
const TaskManagement = () => {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchTasks();
}, []);
const fetchTasks = async () => {
setLoading(true);
try {
// 这里需要实现获取任务列表的API
const response = await axios.get('http://localhost:8080/api/tasks/list');
setTasks(response.data);
} catch (error) {
console.error('获取任务列表失败:', error);
} finally {
setLoading(false);
}
};
const handlePause = async (taskName, jobGroup) => {
try {
await axios.post(`http://localhost:8080/api/tasks/${taskName}/${jobGroup}/pause`);
message.success('任务已暂停');
fetchTasks();
} catch (error) {
message.error('暂停失败: ' + error.message);
}
};
const handleResume = async (taskName, jobGroup) => {
try {
await axios.post(`http://localhost:8080/api/tasks/${taskName}/${jobGroup}/resume`);
message.success('任务已恢复');
fetchTasks();
} catch (error) {
message.error('恢复失败: ' + error.message);
}
};
const handleDelete = async (taskName, jobGroup) => {
try {
await axios.delete(`http://localhost:8080/api/tasks/${taskName}/${jobGroup}`);
message.success('任务已删除');
fetchTasks();
} catch (error) {
message.error('删除失败: ' + error.message);
}
};
const columns = [
{ title: '任务名称', dataIndex: 'taskName', key: 'taskName' },
{ title: '任务组', dataIndex: 'jobGroup', key: 'jobGroup' },
{ title: 'Cron表达式', dataIndex: 'cronExpression', key: 'cronExpression' },
{ title: '描述', dataIndex: 'description', key: 'description' },
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
render: (enabled) => (
<Tag color={enabled ? 'green' : 'red'}>
{enabled ? '运行中' : '已暂停'}
</Tag>
),
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
{record.enabled ? (
<Button
icon={<PauseCircleOutlined />}
onClick={() => handlePause(record.taskName, record.jobGroup)}
>
暂停
</Button>
) : (
<Button
icon={<PlayCircleOutlined />}
onClick={() => handleResume(record.taskName, record.jobGroup)}
>
恢复
</Button>
)}
<Popconfirm
title="确定删除此任务吗?"
onConfirm={() => handleDelete(record.taskName, record.jobGroup)}
>
<Button icon={<DeleteOutlined />} danger>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<h2>任务管理</h2>
<Table columns={columns} dataSource={tasks} loading={loading} rowKey="taskName" />
</div>
);
};
export default TaskManagement;
jsx
// src/components/ExecutionHistory.js
import React, { useState, useEffect } from 'react';
import { Table, Input, Tag, Space, Button, Modal, Descriptions } from 'antd';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import axios from 'axios';
import dayjs from 'dayjs';
const { Search } = Input;
const ExecutionHistory = () => {
const [executions, setExecutions] = useState([]);
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [totalItems, setTotalItems] = useState(0);
const [keyword, setKeyword] = useState('');
const [selectedExecution, setSelectedExecution] = useState(null);
const [modalVisible, setModalVisible] = useState(false);
useEffect(() => {
fetchExecutions();
}, [currentPage, keyword]);
const fetchExecutions = async () => {
setLoading(true);
try {
const response = await axios.get('http://localhost:8080/api/tasks/executions', {
params: { page: currentPage, size: 10, keyword },
});
setExecutions(response.data.executions);
setTotalPages(response.data.totalPages);
setTotalItems(response.data.totalItems);
} catch (error) {
console.error('获取执行记录失败:', error);
} finally {
setLoading(false);
}
};
const handleViewDetail = async (id) => {
try {
const response = await axios.get(`http://localhost:8080/api/tasks/executions/${id}`);
setSelectedExecution(response.data);
setModalVisible(true);
} catch (error) {
console.error('获取详情失败:', error);
}
};
const getStatusTag = (status) => {
const colors = {
SUCCESS: 'green',
FAILED: 'red',
RUNNING: 'blue',
SKIPPED: 'orange',
};
return <Tag color={colors[status]}>{status}</Tag>;
};
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '任务名称', dataIndex: 'taskName', key: 'taskName' },
{ title: '任务组', dataIndex: 'jobGroup', key: 'jobGroup' },
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
render: (time) => dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
render: (time) => time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status) => getStatusTag(status),
},
{
title: '执行耗时(ms)',
dataIndex: 'executionDuration',
key: 'executionDuration',
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Button type="link" onClick={() => handleViewDetail(record.id)}>
查看详情
</Button>
),
},
];
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<h2>任务执行历史</h2>
<Space>
<Search
placeholder="搜索任务名称或状态"
allowClear
style={{ width: 250 }}
onSearch={(value) => {
setKeyword(value);
setCurrentPage(0);
}}
prefix={<SearchOutlined />}
/>
<Button icon={<ReloadOutlined />} onClick={() => fetchExecutions()}>
刷新
</Button>
</Space>
</div>
<Table
columns={columns}
dataSource={executions}
loading={loading}
rowKey="id"
pagination={{
current: currentPage + 1,
total: totalItems,
pageSize: 10,
onChange: (page) => setCurrentPage(page - 1),
showTotal: (total) => `共 ${total} 条记录`,
}}
/>
<Modal
title="执行详情"
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
width={800}
>
{selectedExecution && (
<Descriptions column={1} bordered>
<Descriptions.Item label="任务名称">{selectedExecution.taskName}</Descriptions.Item>
<Descriptions.Item label="任务组">{selectedExecution.jobGroup}</Descriptions.Item>
<Descriptions.Item label="开始时间">
{dayjs(selectedExecution.startTime).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
<Descriptions.Item label="结束时间">
{selectedExecution.endTime ? dayjs(selectedExecution.endTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</Descriptions.Item>
<Descriptions.Item label="状态">{getStatusTag(selectedExecution.status)}</Descriptions.Item>
<Descriptions.Item label="执行耗时">{selectedExecution.executionDuration} ms</Descriptions.Item>
{selectedExecution.result && (
<Descriptions.Item label="执行结果">
<pre style={{ whiteSpace: 'pre-wrap' }}>{selectedExecution.result}</pre>
</Descriptions.Item>
)}
{selectedExecution.errorMessage && (
<Descriptions.Item label="错误信息">
<pre style={{ color: 'red', whiteSpace: 'pre-wrap' }}>{selectedExecution.errorMessage}</pre>
</Descriptions.Item>
)}
</Descriptions>
)}
</Modal>
</div>
);
};
export default ExecutionHistory;
jsx
// src/components/CreateTask.js
import React, { useState } from 'react';
import { Form, Input, Button, message, Card } from 'antd';
import axios from 'axios';
const CreateTask = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const onFinish = async (values) => {
setLoading(true);
try {
await axios.post('http://localhost:8080/api/tasks/schedule', {
...values,
jobGroup: 'DEFAULT',
enabled: true,
});
message.success('任务创建成功');
form.resetFields();
} catch (error) {
message.error('创建失败: ' + error.response?.data?.message || error.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h2>创建定时任务</h2>
<Card style={{ maxWidth: 600 }}>
<Form
form={form}
layout="vertical"
onFinish={onFinish}
>
<Form.Item
name="taskName"
label="任务名称"
rules={[{ required: true, message: '请输入任务名称' }]}
>
<Input placeholder="例如: DataSyncTask" />
</Form.Item>
<Form.Item
name="cronExpression"
label="Cron表达式"
rules={[{ required: true, message: '请输入Cron表达式' }]}
extra="例如: 0 0/5 * * * ? (每5分钟执行一次)"
>
<Input placeholder="0 0/5 * * * ?" />
</Form.Item>
<Form.Item
name="description"
label="任务描述"
>
<Input.TextArea rows={3} placeholder="描述这个任务的功能" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
创建任务
</Button>
</Form.Item>
</Form>
<div style={{ marginTop: 20 }}>
<h4>Cron表达式示例:</h4>
<ul>
<li>0 0/5 * * * ? - 每5分钟执行一次</li>
<li>0 0 2 * * ? - 每天凌晨2点执行</li>
<li>0 0 9-17 * * MON-FRI - 工作日9-17点每小时执行</li>
<li>0 0 12 * * ? - 每天中午12点执行</li>
</ul>
</div>
</Card>
</div>
);
};
export default CreateTask;
数据库初始化SQL
sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS task_scheduler
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE task_scheduler;
-- 任务执行记录表
CREATE TABLE IF NOT EXISTS task_execution (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
task_name VARCHAR(100) NOT NULL,
job_group VARCHAR(50) NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME,
status VARCHAR(50),
result TEXT,
error_message TEXT,
execution_duration BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_task_name (task_name),
INDEX idx_status (status),
INDEX idx_start_time (start_time)
);
-- 定时任务配置表
CREATE TABLE IF NOT EXISTS scheduled_task (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
task_name VARCHAR(100) NOT NULL UNIQUE,
job_group VARCHAR(50) NOT NULL,
cron_expression VARCHAR(100) NOT NULL,
description TEXT,
enabled BOOLEAN DEFAULT TRUE,
last_execution_time DATETIME,
next_execution_time DATETIME,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_enabled (enabled),
INDEX idx_task_name (task_name)
);
运行说明
- 后端启动:
bash
cd backend
mvn clean install
mvn spring-boot:run
- 前端启动:
bash
cd frontend
npm install
npm start
- 访问应用:
- 前端界面: http://localhost:3000
- 后端API: http://localhost:8080
功能特性
- 任务管理:创建、查看、暂停、恢复、删除定时任务
- 执行记录:查看所有任务执行历史,包括成功/失败状态、执行耗时、结果等
- 详情查看:查看每次执行的详细信息和错误日志
- 搜索过滤:按任务名称或状态搜索执行记录
- Cron表达式:支持灵活的时间调度配置
这个系统提供了完整的任务调度和管理功能,所有任务的执行结果都会保存到数据库,并通过API提供给前端查询和展示。