tasks


我将帮您创建一个完整的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)
);

运行说明

  1. 后端启动

bash

cd backend
mvn clean install
mvn spring-boot:run
  1. 前端启动

bash

cd frontend
npm install
npm start
  1. 访问应用

功能特性

  1. 任务管理:创建、查看、暂停、恢复、删除定时任务
  2. 执行记录:查看所有任务执行历史,包括成功/失败状态、执行耗时、结果等
  3. 详情查看:查看每次执行的详细信息和错误日志
  4. 搜索过滤:按任务名称或状态搜索执行记录
  5. Cron表达式:支持灵活的时间调度配置

这个系统提供了完整的任务调度和管理功能,所有任务的执行结果都会保存到数据库,并通过API提供给前端查询和展示。


文章作者: 倪春恩
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 倪春恩 !