使用Node.js实现一个定时任务调度中心

⚠️ 本文最后更新于2023年12月12日,已经过了348天没有更新,若内容或图片失效,请留言反馈

数据库设计

-- 任务记录表CREATETABLE `schedule_job` (
  `job_id` int(11) NOTNULL AUTO_INCREMENT,
  `cron` varchar(50) NOTNULLDEFAULT'' COMMENT 'cron表达式',
  `jobName` varchar(100) NOTNULLDEFAULT'' COMMENT '任务名',
  `jobHandler` varchar(100) NOTNULLDEFAULT'' COMMENT '任务处理方法',
  `params` varchar(255) NOTNULL COMMENT '参数',
  `description` varchar(255) NOTNULLDEFAULT'' COMMENT '描述',
  `status` int(1) NOTNULLDEFAULT'-1' COMMENT '状态 0启用 -1停止',
  `create_by` varchar(100) NOTNULL COMMENT '创建人',
  `update_by` varchar(100) NOTNULL COMMENT '更新人',
  `create_time` timestampNOTNULLDEFAULTCURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`job_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定时任务表';
-- 任务执行日志表CREATETABLE `schedule_job_log`  (
  `id` int(11) NOTNULL AUTO_INCREMENT,
  `job_id` int(11) NOTNULL COMMENT '任务ID',
  `job_handler` varchar(100) CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NOTNULLDEFAULT'' COMMENT '任务处理方法',
  `job_param` varchar(255) CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NOTNULLDEFAULT'' COMMENT '任务参数',
  `handle_time` timestampNOTNULLDEFAULTCURRENT_TIMESTAMP COMMENT '任务执行时间',
  `job_log` text CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NOTNULL COMMENT '任务日志',
  `job_status` int(1) NOTNULLDEFAULT0 COMMENT '任务执行状态:0-成功 -1-失败',
  `error_log` varchar(255) CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NOTNULLDEFAULT'' COMMENT '任务异常日志',
  `create_time` timestampNOTNULLDEFAULTCURRENT_TIMESTAMP COMMENT '创建时间',
  `trigger_type` int(1) NOTNULLDEFAULT0 COMMENT '触发类型:0-任务触发 1-手动触发',
  `execution_status` int(1) NOTNULLDEFAULT0 COMMENT '任务状态:0-执行中 1-执行完成',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT =1CHARACTERSET= utf8mb4 COLLATE= utf8mb4_general_ci COMMENT ='定时任务执行日志' ROW_FORMAT =Dynamic;

任务的增删改查

// app/routers/task.js
'use strict';
module.exports = app => {
  const { router, controller, config, middleware } = app;
  const checkTokenHandler = middleware.checkTokenHandler();
  // 定时任务列表
  router.get(`${config.contextPath}/task/schedule/list`, checkTokenHandler, controller.task.scheduleList);
  // 修改/新增定时任务
  router.post(`${config.contextPath}/task/schedule/edit`, checkTokenHandler, controller.task.editSchedule);
  // 删除定时任务
  router.post(`${config.contextPath}/task/schedule/delete`, checkTokenHandler, controller.task.deleteSchedule);
  // 更新定时任务状态
  router.post(`${config.contextPath}/task/schedule/status/update`, checkTokenHandler, controller.task.updateStatusSchedule);
};
 
// app/controller/task.js
'use strict';
const Controller = require('egg').Controller;
const { setResult } = require('../utils');
class TaskController extends Controller {
  /**
   * 定时任务管理
   */
  async scheduleList() {
    const { ctx } = this;
    const result = await ctx.service.taskService.scheduleList(ctx.request.query);
    ctx.body = setResult({ data: result });
  }
  /**
   * 修改/新增定时任务
   */
  async editSchedule() {
    const { ctx } = this;
    const { username } = ctx.request.headers;
    await ctx.service.taskService.editSchedule(username, ctx.request.body);
    ctx.body = setResult();
  }
  /**
   * 删除定时任务
   */
  async deleteSchedule() {
    const { ctx } = this;
    await ctx.service.taskService.deleteSchedule(ctx.request.body);
    ctx.body = setResult();
  }
  /**
   * 更新定时任务状态
   */
  async updateStatusSchedule() {
    const { ctx } = this;
    await ctx.service.taskService.updateStatusSchedule(ctx.request.body);
    ctx.body = setResult();
  }
  /**
   * 执行任务
   */
  async runSchedule() {
    const { ctx } = this;
    await ctx.service.taskService.runSchedule(ctx.request.body);
    ctx.body = setResult();
  }
  /**
   * 获取任务执行日志
   */
  async scheduleLogList() {
    const { ctx } = this;
    const result = await ctx.service.taskService.scheduleLogList(ctx.request.query);
    ctx.body = setResult({ data: result });
  }
  /**
   * 获取任务执行日志详细信息
   */
  async scheduleLogDateil() {
    const { ctx } = this;
    const result = await ctx.service.taskService.scheduleLogDateil(ctx.request.query);
    console.log(result)
    ctx.body = setResult({ data: result });
  }
}
module.exports = TaskController;
 
// app/service/taskService.js
'use strict';
const { Service } = require('egg');
const { SCHEDULE_STATUS } = require('../constants');
class TaskService extends Service {
  // 定时任务管理
  async scheduleList({ page = 1, size = 20 }) {
    const limit = parseInt(size),
      offset = parseInt(page - 1) * parseInt(size);
 
    const [ list, total ] = await Promise.all([
      this.app.mysql.select('schedule_job', {
        orders: [[ 'create_time', 'desc' ]],
        limit,
        offset,
      }),
      this.app.mysql.count('schedule_job'),
    ]);
    return { list, total };
  }
  // 修改/新增定时任务
  async editSchedule(userName, { job_id, cron, jobName, jobHandler, params = '', description = '' }) {
    if (!job_id) {
      // 新增
      await this.app.mysql.insert('schedule_job', {
        cron,
        jobName,
        jobHandler,
        description,
        params,
        create_by: userName,
        update_by: userName,
        create_time: new Date(),
        update_time: new Date(),
      });
      return;
    }
    // 修改
    await this.app.mysql.update('schedule_job', {
      cron,
      jobName,
      jobHandler,
      description,
      params,
      update_by: userName,
      update_time: new Date(),
    }, { where: { job_id } });
  }
  // 删除定时任务
  async deleteSchedule({ job_id }) {
    const result = await this.app.mysql.delete('schedule_job', { job_id });
    if (result.affectedRows === 1) {
      const schedule = await this.app.mysql.get('schedule_job', { job_id });
      if (schedule.status === SCHEDULE_STATUS.RUN) {
        // 停止任务
        await this.ctx.helper.cancelSchedule(schedule.jobName);
      }
    }
  }
  // 更新定时任务状态
  async updateStatusSchedule({ job_id, status }) {
    await this.app.mysql.update('schedule_job', { status }, { where: { job_id } });
  }
  // 执行任务
  async runSchedule({ job_id }) {
    const schedule = await this.app.mysql.get('schedule_job', { job_id });
    if (schedule === null) throw new GlobalError(RESULT_FAIL, '任务不存在');
 
    const jobHandlerLog = new JobHandlerLog(this.app);
 
    try {
      // 执行日志初始化
      await jobHandlerLog.init(schedule, SCHEDULE_TRIGGER_TYPE.MANUAL)
      // 执行任务
      this.service.scheduleService[schedule.jobHandler](schedule.params, jobHandlerLog); 
    } catch (error) {
      await this.logger.info('执行任务`%s`失败,时间:%s, 错误信息:%j', schedule.jobName, new Date().toLocaleString(), error);
      // 记录失败日志
      await jobHandlerLog.error('执行任务`{0}`失败,时间:{1}, 错误信息:{2}', schedule.jobName, new Date().toLocaleString(), error);
    } finally {
      // 更新日志记录状态
      await jobHandlerLog.end();
    }
  }
  // 获取任务执行日志
  async scheduleLogList({ job_id, page = 1, size = 20 }) {
    const limit = parseInt(size),
      offset = parseInt(page - 1) * parseInt(size);
 
    const [ list, total ] = await Promise.all([
      this.app.mysql.query(`SELECT job.jobName jobName, log.id id, log.job_handler jobHandler, log.job_param jobParam, log.handle_time handleTime,
      log.job_status jobStatus, log.trigger_type triggerType, log.execution_status executionStatus, log.error_log errorLog FROM schedule_job job,
      schedule_job_log log WHERE job.job_id = log.job_id AND log.job_id = ? ORDER BY log.create_time DESC LIMIT ?,?`, [ job_id, offset, limit]),
      this.app.mysql.count('schedule_job_log', { job_id })
    ]);
 
    return { list, total };
  }
  // 获取任务执行日志详细信息
  async scheduleLogDateil({ id }) {
    const result = await this.app.mysql.get('schedule_job_log', { id })
 
    return { detail: result.job_log, executionStatus: result.execution_status };
  }
}
module.exports = TaskService;

实现定时任务的启动、取消与所有任务
node-schedule是用于Node.js的灵活的cron类和非cron类作业调度程序。它允许使用可选的重复规则来安排(任意函数)在特定日期执行。它在任何给定时间仅使用一个计时器(而不是每秒/分钟重新评估即将到来的作业),提供了启动与停止等方法来管理任务。

// app/utils/JobHandlerLog.js
// 任务执行日志记录类
'use strict';
 
const { SCHEDULE_EXECUTION_STATUS, SCHEDULE_TRIGGER_TYPE } = require('../constants');
const { formatStr } = require('./index')
 
class JobHandlerLog {
  constructor(app) {
    this.app = app;
    this.ctx = app.ctx;
  }
 
  // 初始化日志
  async init(schedule, triggerType = SCHEDULE_TRIGGER_TYPE.TASK) {
    const result = await this.app.mysql.insert('schedule_job_log', {
      job_id: schedule.job_id, job_handler: schedule.jobHandler, job_param: schedule.params, trigger_type: triggerType, job_log: '' });
    this.id = result.insertId;
  }
 
  // 追加日志
  async log(logStr, ...args) {
    const content = formatStr(logStr, ...args);
    await this.app.mysql.query('UPDATE schedule_job_log SET job_log = CONCAT(job_log, ?) WHERE id = ?', [ `${content}<br/>`, this.id ]);
  }
 
  // 记录执行异常日志
  async error(logStr, ...args) {
    const errorMsg = formatStr(logStr, ...args);
    await this.app.mysql.query('UPDATE schedule_job_log SET job_status = -1 AND error_log = ? WHERE id = ?', [ errorMsg, this.id ]);
  }
 
  // 定时任务执行结束
  async end() {
    await this.app.mysql.update('schedule_job_log', { execution_status: SCHEDULE_EXECUTION_STATUS.END }, { where: { id: this.id } });
  }
}
 
module.exports = JobHandlerLog;
 
// app/extend/helper.js
module.exports = {
  /**
   * 创建定时任务
   * @param {*} id 任务ID
   * @param {*} cron Cron
   * @param {*} jobName 任务名
   * @param {*} jobHandler 任务方法
   * 在日常使用中,可能会存在同一处理程序有不同的处理逻辑,所以需要传入任务的ID
   * 如:在消息推送中,会存在不同时间对相同用户推送不同内容,而内容存放在任务信息中,业务代码需要查询到对应的任务信息读取推送信息,处理下一步逻辑
   */
  async generateSchedule(id, cron, jobName, jobHandler) {
    this.ctx.logger.info('[创建定时任务],任务ID: %s,cron: %s,任务名: %s,任务方法: %s', id, cron, jobName, jobHandler);
    this.app.scheduleStacks[jobName] = schedule.scheduleJob(cron, async () => {
      // 读取锁,保证一个任务同时只能有一个进程执行
      const locked = await this.app.redlock.lock('sendAllUserBroadcast:' + id, 'sendAllUserBroadcast', 180);
      if (!locked) return false;
 
      const jobHandlerLog = new JobHandlerLog(this.app);
 
      try {
        // 获取任务信息
        const schedule = await this.app.mysql.get('schedule_job', { job_id: id });
 
        // 执行日志初始化
        await jobHandlerLog.init(schedule)
 
        // 调用任务方法
        await this.service.scheduleService[jobHandler](schedule.params, jobHandlerLog);
      } catch (error) {
        await this.logger.info('执行任务`%s`失败,时间:%s, 错误信息:%j', jobName, new Date().toLocaleString(), error);
        // 记录失败日志
        await jobHandlerLog.error('执行任务`{0}`失败,时间:{1}, 错误信息:{2}', jobName, new Date().toLocaleString(), error);
      } finally {
        // 释放锁
        await this.app.redlock.unlock('sendAllUserBroadcast:' + id);
        // 更新日志记录状态
        await jobHandlerLog.end();
      }
    });
  },
  /**
   * 取消/停止定时任务
   * @param {*} jobName 任务名
   */
  async cancelSchedule(jobName) {
    this.ctx.logger.info('[取消定时任务],任务名:%s', jobName);
    this.app.scheduleStacks[jobName] && this.app.scheduleStacks[jobName].cancel();
  },
};

任务的具体处理程序
scheduleService存放所有任务处理程序,目前只实现少量任务的管理,如果任务叫庞大的时候可根据不同的任务类型调用不同service的方法.

当前只实现了一次性执行,未考虑到任务的失败、异常等现象,后面有时间了再完善

// app/service/scheduleService.js
'use strict';
 
const { Service } = require('egg');
 
class ScheduleService extends Service {
  /**
   * 测试处理程序
   * @param {*} params 任务参数
   */
  async testHandler(params) {
    // 此处替换成具体业务代码
    await this.logger.info('我是测试任务,任务参数: %s', params);
  }
  /**
   * 测试调用接口任务
   * @param {*} params 任务参数
   */
  async testCurlHandler(params) {
    // 获取参数
    const paramsObj = JSON.parse(params)
    const result = await this.ctx.curl(paramsObj.url, {
      method: paramsObj.method,
      data: paramsObj.data
    });
    // await this.logger.info('测试调用接口任务,状态码:%d,返回值:%j', result.status);
  }
}
 
module.exports = ScheduleService;

服务重启自动加载定时任务

// app.js
'use strict';
const { SCHEDULE_STATUS } = require('./app/constants');
class AppBootHook {
  constructor(app) {
    this.app = app;
    this.ctx = app.createAnonymousContext();
  }
  async willReady() {
    await this.app.logger.info('【初始化定时任务】开始...');
    // 查询当前启动状态的定时任务
    const schedules = await this.app.mysql.select('schedule_job', { where: { status: SCHEDULE_STATUS.RUN } });
      // 循环注册定时任务
    schedules.forEach(async schedule => {
      await this.ctx.helper.generateSchedule(schedule.job_id, schedule.cron, schedule.jobName, schedule.jobHandler);
    });
    await this.app.logger.info('【初始化定时任务】初始化定时任务: %d,结束...', schedules.length);
  }
  async beforeClose() {
    await this.app.logger.info('【销毁定时任务】开始...');
    const scheduleStacks = await this.ctx.helper.getScheduleStacks();
    Reflect.ownKeys(scheduleStacks).forEach(async key => {
      await this.ctx.helper.cancelSchedule(key);
    });
    await this.app.logger.info('【销毁定时任务】销毁定时任务数: %d,结束...', Reflect.ownKeys(scheduleStacks).length);
  }
}
module.exports = AppBootHook;

完善任务的管理

// app/routers/task.js
...
// 执行任务
router.post(`${config.contextPath}/task/schedule/run`, checkTokenHandler, controller.task.runSchedule);
...
 
// app/controller/task.js
/**
 * 执行任务
 */
async runSchedule() {
  const { ctx } = this;
  await ctx.service.taskService.runSchedule(ctx.request.body);
  ctx.body = setResult();
}
 
// app/service/taskService.js
// 修改/新增定时任务
async editSchedule(userName, { job_id, cron, jobName, jobHandler, params = '', description = '' }) {
  if (result.affectedRows === 1) {
    const schedule = await this.app.mysql.get('schedule_job', { job_id });
    // 此处在版本允许的情况下可使用可选链操作符`?`
    if (schedule && schedule.status === SCHEDULE_STATUS.RUN) {
      // 启动状态下重置任务
      await this.ctx.helper.cancelSchedule(jobName);
      await this.ctx.helper.generateSchedule(job_id, cron, jobName, jobHandler);
    }
  }
}
// 更新定时任务状态
async updateStatusSchedule({ job_id, status }) {
  const result = await this.app.mysql.update('schedule_job', { status }, { where: { job_id } });
  // 判断是否更新成功
  if (result.affectedRows === 1) {
    const schedule = await this.app.mysql.get('schedule_job', { job_id });
    if (status === SCHEDULE_STATUS.RUN) {
      // 启动任务
      await this.ctx.helper.generateSchedule(job_id, schedule.cron, schedule.jobName, schedule.jobHandler);
    } else {
      // 停止任务
      await this.ctx.helper.cancelSchedule(schedule.jobName);
    }
  }
}
// 执行任务
async runSchedule({ job_id }) {
  const schedule = await this.app.mysql.get('schedule_job', { job_id });
  if (schedule === null) throw new VideoError(RESULT_FAIL, '任务不存在');
  // 执行任务
  this.service.scheduleService[schedule.jobHandler](schedule.params);
}

管理系统页面实现
UI的实现相对简单,就不做解释了

// src/api/task.js
import request from '@/utils/request'
 
/**
 * 定时任务列表
 * @param {*} params
 */
export function scheduleList(params) {
  return request({
    url: '/task/schedule/list',
    method: 'GET',
    params
  })
}
 
/**
 * 修改/新增定时任务
 * @param {*} data
 */
export function editSchedule(data) {
  return request({
    url: '/task/schedule/edit',
    method: 'post',
    data
  })
}
 
/**
 * 删除定时任务
 * @param {*} data
 */
export function deleteSchedule(data) {
  return request({
    url: '/task/schedule/delete',
    method: 'post',
    data
  })
}
 
/**
 * 更新定时任务状态
 * @param {*} data
 */
export function updateStatusSchedule(data) {
  return request({
    url: '/task/schedule/status/update',
    method: 'post',
    data
  })
}
 
/**
 * 执行任务
 * @param {*} data
 */
export function runSchedule(data) {
  return request({
    url: '/task/schedule/run',
    method: 'post',
    data
  })
}
 
/**
 * 定时任务日志列表
 * @param {*} params
 */
export function scheduleLogList(params) {
  return request({
    url: '/task/schedule/log/list',
    method: 'GET',
    params
  })
}
 
/**
 * 获取任务执行日志详细信息
 * @param {*} params
 */
export function scheduleLogDetail(params) {
  return request({
    url: '/task/schedule/log/detail',
    method: 'GET',
    params
  })
}
// src/views/task/schedule.vue
<template>
  <div class="app-container">
    <div class="filter-container">
      <el-button v-waves class="filter-item" type="primary" icon="el-icon-plus" @click="handleEdit(null)">新增</el-button>
    </div>
    <i style="color: red;"><b>当前项目中存在两个jobHandler方法,testHandler(定时打印日志)与testCurlHandler(定时调用接口,参数为必填,必须为json字符串格式)</b></i>
    <el-table v-loading="listLoading" :data="list" border fit highlight-current-row style="width: 100%">
      <el-table-column align="center" prop="job_id" label="任务ID" />
      <el-table-column align="center" prop="jobName" label="任务名" />
      <el-table-column align="center" prop="cron" label="Cron" />
      <el-table-column align="center" prop="jobHandler" label="jobHandler" />
      <el-table-column align="center" prop="params" label="参数" />
      <el-table-column align="center" prop="description" label="任务描述" />
      <el-table-column align="center" prop="status" label="状态">
        <template slot-scope="{row}">
          <el-tag v-if="row.status==0" type="success">run</el-tag>
          <el-tag v-else type="info">stop</el-tag>
        </template>
      </el-table-column>
      <el-table-column align="center" label="操作">
        <template slot-scope="{row}">
          <el-button v-if="row.status==-1" type="text" @click="updateStatus(row.job_id, 0)">启动</el-button>
          <el-button v-else type="text" @click="updateStatus(row.job_id, -1)">停止</el-button>
          <el-button type="text" @click="run(row.job_id)">执行</el-button>
          <el-button type="text" @click="showLog(row.job_id)">日志</el-button>
          <el-button type="text" @click="handleEdit(row)">编辑</el-button>
          <el-button type="text" @click="del(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.size" @pagination="getList" />
    <el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'编辑任务':'新增任务'" width="400px">
      <el-form ref="editForm" :rules="rules" :model="fromData" label-width="100px" label-position="right">
        <el-form-item label="Cron" prop="cron">
          <el-input v-model="fromData.cron" placeholder="请输入Cron" />
        </el-form-item>
        <el-form-item label="任务名" prop="jobName">
          <el-input v-model="fromData.jobName" placeholder="请输入任务名" />
        </el-form-item>
        <el-form-item label="jobHandler" prop="jobHandler">
          <el-input v-model="fromData.jobHandler" placeholder="请输入jobHandler" />
        </el-form-item>
        <el-form-item label="参数" prop="params">
          <el-input v-model="fromData.params" type="textarea" placeholder="请输入参数" />
        </el-form-item>
        <el-form-item label="任务描述" prop="description">
          <el-input v-model="fromData.description" type="textarea" placeholder="请输入任务描述" />
        </el-form-item>
      </el-form>
      <div style="text-align:right;">
        <el-button type="danger" @click="dialogVisible=false">取消</el-button>
        <el-button type="primary" @click="confirm">提交</el-button>
      </div>
    </el-dialog>
    <el-dialog :visible.sync="logDialogVisible" fullscreen title="执行记录">
      <el-table :data="logList" border fit highlight-current-row style="width: 100%">
        <el-table-column align="center" prop="id" label="ID" />
        <el-table-column align="center" prop="jobName" label="任务名" />
        <el-table-column align="center" prop="jobHandler" label="处理方法" />
        <el-table-column align="center" prop="jobParam" label="参数" />
        <el-table-column align="center" prop="handleTime" label="执行时间" />
        <el-table-column align="center" prop="jobStatus" label="执行状态">
          <template slot-scope="{row}">
            <el-tag v-if="row.jobStatus==0" type="success">成功</el-tag>
            <el-tag v-else type="info">失败</el-tag>
          </template>
        </el-table-column>
        <el-table-column align="center" prop="triggerType" label="触发类型">
          <template slot-scope="{row}">
            {{ row.triggerType | triggerTypeFilter }}
          </template>
        </el-table-column>
        <el-table-column align="center" prop="executionStatus" label="任务状态">
          <template slot-scope="{row}">
            {{ row.executionStatus | executionStatusFilter }}
          </template>
        </el-table-column>
        <el-table-column align="center" label="操作">
          <template slot-scope="{row}">
            <el-button type="text" @click="showDetail(row.id)">详情日志</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="logTotal>0" :total="logTotal" :page.sync="logListQuery.page" :limit.sync="logListQuery.size" @pagination="getLogList" />
    </el-dialog>
 
    <el-dialog :visible.sync="logDetailDialogVisible" title="执行日志">
      <div v-html="logDetail" />
      <!-- isShowExecutionAnimation -->
    </el-dialog>
  </div>
</template>
 
<script>
import Pagination from '@/components/Pagination'
import waves from '@/directive/waves'
import { scheduleList, editSchedule, deleteSchedule, updateStatusSchedule, runSchedule, scheduleLogList, scheduleLogDetail } from '@/api/task'
export default {
  components: { Pagination },
  directives: { waves },
  filters: {
    triggerTypeFilter(val) {
      return val === 0 ? '任务触发' : '手动触发'
    },
    executionStatusFilter(val) {
      return val === 0 ? '执行中' : '执行完成'
    }
  },
  data() {
    return {
      listLoading: false,
      list: [],
      total: 0,
      listQuery: {
        page: 1,
        size: 20
      },
 
      dialogVisible: false,
      dialogType: 'new',
      fromData: {},
      rules: {
        cron: { required: true, message: '请输入Cron', trigger: 'blur' },
        jobName: { required: true, message: '请输入任务名', trigger: 'blur' },
        jobHandler: { required: true, message: '请输入jobHandler', trigger: 'blur' }
      },
 
      // 日志浮窗
      logDialogVisible: false,
      logListQuery: {
        page: 1,
        size: 20,
        job_id: ''
      },
      logList: [],
      logTotal: 0,
 
      // 日志详情
      logDetailDialogVisible: false,
      logDetail: '',
      timer: null,
      // 是否展示执行中动画
      isShowExecutionAnimation: false
    }
  },
  mounted() {
    this.getList()
  },
  beforeDestroy() {
    this.timer && clearInterval(this.timer)
  },
  methods: {
    async getList() {
      this.listLoading = true
      const { code, data } = await scheduleList(this.listQuery)
      this.listLoading = false
      if (code === 0) {
        this.list = data.list
        this.total = data.total
      }
    },
    handleEdit(row) {
      this.fromData = {}
      if (row) {
        this.fromData = JSON.parse(JSON.stringify(row))
        this.dialogType = 'edit'
      } else {
        this.dialogType = 'new'
      }
      this.dialogVisible = true
    },
    async confirm() {
      this.$refs.editForm.validate(async valid => {
        if (!valid) return false
        const { code } = await editSchedule(this.fromData)
        if (code === 0) {
          this.$message({
            message: this.dialogType === 'edit' ? '编辑成功' : '新增成功',
            type: 'success'
          })
          this.dialogVisible = false
          this.getList()
        }
      })
    },
    del(row) {
      this.$confirm('确定要删除该任务吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(async() => {
        const { code } = await deleteSchedule({ job_id: row.job_id })
        if (code === 0) {
          this.$message({
            message: '删除成功',
            type: 'success'
          })
          this.getList()
        }
      })
    },
    async updateStatus(job_id, status) {
      const { code } = await updateStatusSchedule({ job_id, status })
      if (code === 0) {
        this.$message({
          message: status === 0 ? '任务启动成功' : '任务停止成功',
          type: 'success'
        })
        this.getList()
      }
    },
    async run(job_id) {
      const { code } = await runSchedule({ job_id })
      if (code === 0) {
        this.$message({
          message: '执行成功',
          type: 'success'
        })
      }
    },
    showLog(job_id) {
      this.logListQuery.job_id = job_id
      this.getLogList()
      this.logDialogVisible = true
    },
    async getLogList() {
      const { code, data } = await scheduleLogList(this.logListQuery)
      if (code === 0) {
        this.logList = data.list
        this.logTotal = data.total
      }
    },
    showDetail(id) {
      this.isShowExecutionAnimation = true
      this.logDetail = ''
      this.getLogDetail(id)
      this.timer = setInterval(() => {
        this.getLogDetail(id)
      }, 1000)
      this.logDetailDialogVisible = true
    },
    async getLogDetail(id) {
      const { code, data } = await scheduleLogDetail({ id })
      if (code === 0) {
        this.logDetail = data.detail
        if (data.executionStatus === 1) {
          this.isShowExecutionAnimation = false
          clearInterval(this.timer)
        }
      }
    }
  }
}
</script>
By alex On