☰
第 6 章:定时任务管理
2026/9/28 4:39:08 网站建设 项目流程

本章学习目标

  • 理解 Cron 表达式的基本原理与语法
  • 集成 react-js-cron 可视化编辑器
  • 掌握定时任务列表与状态管理
  • 实现任务执行记录追踪
  • 学会任务的启用/暂停/删除操作

6.1 Cron 表达式与定时任务原理

Cron 是 Unix 系统中用于定期执行任务的工具。在后台系统中,我们经常需要创建定时任务,比如:

  • 每天早上 9 点生成昨日数据报表
  • 每周一上午 10 点同步部门数据
  • 每月 1 号凌晨 2 点执行数据归档

6.1.1 Cron 表达式语法

标准的 Cron 表达式有 6 或 7 个字段(用空格分隔):

┌───────────── 秒(0-59)│ ┌───────────── 分(0-59)│ │ ┌───────────── 时(0-23)│ │ │ ┌───────────── 日(1-31)│ │ │ │ ┌───────────── 月(1-12)│ │ │ │ │ ┌───────────── 周(0-7)(0或7都是周日)│ │ │ │ │ │ * * * * * *

6.1.2 常用特殊符号

符号含义示例
*任意值分钟字段为*表示每分钟
,列举小时字段9,12,18表示 9/12/18 点
-范围小时字段9-18表示 9 点到 18 点
/间隔分钟字段*/5表示每 5 分钟
?不指定(日/周互斥)日字段用?表示不关心具体日期

6.1.3 常见示例

表达式含义
0 0 9 * * ?每天上午 9 点
0 0 9 ? * MON-FRI工作日上午 9 点
0 0/30 * * * ?每 30 分钟
0 0 2 1 * ?每月 1 号凌晨 2 点
0 0 10 ? * 2每周一上午 10 点
0 0/5 9-18 * * ?9 点到 18 点每 5 分钟

6.2 数据模型

// src/types/scheduledTask.tsexportenumScheduledTaskStatus{RUNNING='running',// 运行中PAUSED='paused',// 已暂停FINISHED='finished',// 已结束ERROR='error',// 异常}exportenumTaskType{TICKET_CREATE='ticket_create',// 自动创建任务单DATA_SYNC='data_sync',// 数据同步REPORT_GENERATE='report_generate',// 生成报表NOTIFICATION='notification',// 通知提醒DATA_CLEANUP='data_cleanup',// 数据清理}exportinterfaceScheduledTask{id:string;name:string;// 任务名称description:string;// 任务描述type:TaskType;// 任务类型cronExpression:string;// Cron 表达式cronDescription:string;// Cron 描述(人类可读)status:ScheduledTaskStatus;params:Record<string,any>;// 任务参数creator:string;creatorId:string;lastRunAt?:string;// 上次执行时间nextRunAt?:string;// 下次执行时间runCount:number;// 执行次数successCount:number;// 成功次数failCount:number;// 失败次数createdAt:string;updatedAt:string;}exportinterfaceTaskExecutionRecord{id:string;taskId:string;taskName:string;status:'success'|'failed'|'running';startTime:string;endTime?:string;duration?:number;// 执行时长(秒)result?:string;// 执行结果摘要errorMessage?:string;// 错误信息triggerType:'cron'|'manual';// 触发方式}

6.3 Cron 可视化编辑器集成

手动输入 Cron 表达式对用户不友好,我们用react-js-cron库来实现可视化的 Cron 编辑器。

6.3.1 安装

npminstallreact-js-cron

6.3.2 封装 CronPicker 组件

// src/components/CronPicker/index.tsx import { useState, useEffect } from 'react'; import { Cron } from 'react-js-cron'; import 'react-js-cron/dist/styles.css'; import { Card, Input, Space, Typography } from 'antd'; const { Text } = Typography; interface Props { value?: string; onChange?: (cron: string, description: string) => void; } // 简易的 Cron 表达式描述生成 function generateCronDescription(cron: string): string { const parts = cron.trim().split(/\s+/); if (parts.length < 5) return '无效的 Cron 表达式'; const [minute, hour, dayOfMonth, month, dayOfWeek] = parts; const partsList: string[] = []; // 频率描述 if (minute === '*' && hour === '*') { return '每分钟执行'; } if (minute === '0' && hour === '*') { return '每小时执行'; } if (minute === '0' && hour === '0') { return '每天凌晨执行'; } // 时间描述 if (hour !== '*' && minute !== '*') { if (hour.includes(',')) { partsList.push(`每天 ${hour.split(',').map((h) => `${h}:${minute.padStart(2, '0')}`).join('、')} 执行`); } else if (hour.includes('/')) { partsList.push(`每 ${hour.split('/')[1]} 小时执行`); } else { partsList.push(`每天 ${hour}:${minute.padStart(2, '0')} 执行`); } } else if (hour !== '*') { partsList.push(`每小时第 ${minute} 分执行`); } // 日期限制 if (dayOfWeek !== '*' && dayOfWeek !== '?') { const weekMap: Record<string, string> = { '1': '周一', '2': '周二', '3': '周三', '4': '周四', '5': '周五', '6': '周六', '7': '周日', '0': '周日', }; if (dayOfWeek.includes(',')) { const days = dayOfWeek.split(',').map((d) => weekMap[d] || d).join('、'); partsList.unshift(`${days}`); } else if (dayOfWeek.includes('-')) { const [s, e] = dayOfWeek.split('-'); partsList.unshift(`${weekMap[s] || s}至${weekMap[e] || e}`); } else { partsList.unshift(`每周${weekMap[dayOfWeek] || dayOfWeek}`); } } if (dayOfMonth !== '*' && dayOfMonth !== '?') { if (dayOfMonth.includes(',')) { partsList.unshift(`每月 ${dayOfMonth.split(',').join('、')} 号`); } else { partsList.unshift(`每月 ${dayOfMonth} 号`); } } return partsList.join(',') || cron; } export default function CronPicker({ value = '0 0 * * *', onChange }: Props) { const [cronValue, setCronValue] = useState(value); const [description, setDescription] = useState(''); useEffect(() => { const desc = generateCronDescription(cronValue); setDescription(desc); onChange?.(cronValue, desc); }, [cronValue, onChange]); return ( <Space direction="vertical" size="middle" style={{ width: '100%' }}> <Card size="small" bordered styles={{ body: { padding: 12 } }}> <Cron value={cronValue} setValue={setCronValue} humanizeLabels={true} humanizeValue={{ year: () => '', month: () => '', day: () => '', weekday: ['日', '一', '二', '三', '四', '五', '六'], hour: () => '', minute: () => '', second: () => '', }} allowedPeriods={['minute', 'hour', 'day', 'weekday']} /> </Card> <Space.Compact style={{ width: '100%' }}> <Input value={cronValue} readOnly style={{ fontFamily: 'monospace' }} /> <Text type="secondary" style={{ padding: '4px 12px' }}> {description} </Text> </Space.Compact> </Space> ); }

注意:react-js-cron默认生成的是 5 段式 Cron(分/时/日/月/周)。如果你的后端需要 6 段式(带秒),需要手动加上秒的前缀。


6.4 定时任务列表与状态管理

6.4.1 列表页面

// src/pages/scheduled-task/list/index.tsx import { useState } from 'react'; import { Card, Table, Tag, Space, Button, Switch, Modal, message, Dropdown } from 'antd'; import { PlusOutlined, PlayCircleOutlined, PauseCircleOutlined, HistoryOutlined, DeleteOutlined, MoreOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; import type { ColumnsType } from 'antd/es/table'; import { scheduledTaskApi } from '@services/scheduledTask'; import { ScheduledTask, ScheduledTaskStatus, TaskType } from '@types/scheduledTask'; import { useTable } from '@hooks/useTable'; import ExecutionRecordDrawer from './components/ExecutionRecordDrawer'; const statusConfig: Record<ScheduledTaskStatus, { label: string; color: string }> = { [ScheduledTaskStatus.RUNNING]: { label: '运行中', color: 'success' }, [ScheduledTaskStatus.PAUSED]: { label: '已暂停', color: 'default' }, [ScheduledTaskStatus.FINISHED]: { label: '已结束', color: 'processing' }, [ScheduledTaskStatus.ERROR]: { label: '异常', color: 'error' }, }; const typeLabels: Record<TaskType, string> = { [TaskType.TICKET_CREATE]: '自动创建任务单', [TaskType.DATA_SYNC]: '数据同步', [TaskType.REPORT_GENERATE]: '生成报表', [TaskType.NOTIFICATION]: '通知提醒', [TaskType.DATA_CLEANUP]: '数据清理', }; export default function ScheduledTaskList() { const navigate = useNavigate(); const [selectedTask, setSelectedTask] = useState<ScheduledTask | null>(null); const [recordDrawerOpen, setRecordDrawerOpen] = useState(false); const { dataSource, total, loading, refresh, handlePageChange, page, pageSize } = useTable< ScheduledTask, { keyword?: string; status?: ScheduledTaskStatus; type?: TaskType } >({ fetchFn: (params) => scheduledTaskApi.getList(params), defaultPageSize: 20, }); const handleToggleStatus = async (task: ScheduledTask, checked: boolean) => { try { if (checked) { await scheduledTaskApi.resume(task.id); message.success('已启用'); } else { await scheduledTaskApi.pause(task.id); message.success('已暂停'); } refresh(); } catch (e) { // 错误拦截 } }; const handleDelete = (task: ScheduledTask) => { Modal.confirm({ title: '确认删除', content: `确定要删除定时任务「${task.name}」吗?`, okButtonProps: { danger: true }, onOk: async () => { await scheduledTaskApi.delete(task.id); message.success('删除成功'); refresh(); }, }); }; const handleManualRun = async (task: ScheduledTask) => { Modal.confirm({ title: '手动执行', content: `确定要立即执行任务「${task.name}」吗?`, onOk: async () => { await scheduledTaskApi.run(task.id); message.success('已触发执行'); setTimeout(refresh, 1000); }, }); }; const columns: ColumnsType<ScheduledTask> = [ { title: '任务名称', dataIndex: 'name', render: (text, record) => ( <a onClick={() => navigate(`/scheduled-task/${record.id}`)}>{text}</a> ), }, { title: '任务类型', dataIndex: 'type', width: 120, render: (type: TaskType) => typeLabels[type] || type, }, { title: 'Cron 表达式', dataIndex: 'cronExpression', width: 140, render: (text) => <code style={{ fontSize: 12 }}>{text}</code>, }, { title: '执行说明', dataIndex: 'cronDescription', width: 180, ellipsis: true, }, { title: '状态', dataIndex: 'status', width: 100, render: (status: ScheduledTaskStatus, record) => ( <Space direction="vertical" size={4}> <Tag color={statusConfig[status]?.color}>{statusConfig[status]?.label}</Tag> {record.status !== ScheduledTaskStatus.FINISHED && ( <Switch size="small" checked={record.status === ScheduledTaskStatus.RUNNING} onChange={(c) => handleToggleStatus(record, c)} checkedChildren="启用" unCheckedChildren="暂停" /> )} </Space> ), }, { title: '执行统计', width: 140, render: (_, record) => ( <div style={{ fontSize: 12 }}> <div>总计:{record.runCount} 次</div> <div> 成功 <span style={{ color: '#52c41a' }}>{record.successCount}</span> {' / '} 失败 <span style={{ color: '#ff4d4f' }}>{record.failCount}</span> </div> </div> ), }, { title: '上次执行', dataIndex: 'lastRunAt', width: 160, render: (t) => (t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'), }, { title: '下次执行', dataIndex: 'nextRunAt', width: 160, render: (t) => (t ? dayjs(t).format('YYYY-MM-DD HH:mm:ss') : '-'), }, { title: '操作', key: 'action', width: 120, fixed: 'right', render: (_, record) => ( <Space> <Button type="text" icon={<HistoryOutlined />} onClick={() => { setSelectedTask(record); setRecordDrawerOpen(true); }} > 执行记录 </Button> <Dropdown menu={{ items: [ { key: 'run', label: '立即执行', icon: <PlayCircleOutlined />, onClick: () => handleManualRun(record), }, record.status === ScheduledTaskStatus.RUNNING ? { key: 'pause', label: '暂停', icon: <PauseCircleOutlined />, onClick: () => handleToggleStatus(record, false), } : { key: 'resume', label: '启用', icon: <PlayCircleOutlined />, onClick: () => handleToggleStatus(record, true), }, { type: 'divider' as const }, { key: 'delete', label: '删除', icon: <DeleteOutlined />, danger: true, onClick: () => handleDelete(record), }, ], }} > <Button type="text" icon={<MoreOutlined />} /> </Dropdown> </Space> ), }, ]; return ( <div style={{ padding: 24 }}> <Card title="定时任务" extra={ <Button type="primary" icon={<PlusOutlined />} onClick={() => navigate('/scheduled-task/create')}> 新建定时任务 </Button> } > <Table rowKey="id" columns={columns} dataSource={dataSource} loading={loading} scroll={{ x: 1300 }} pagination={{ current: page, pageSize, total, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: handlePageChange, }} /> </Card> <ExecutionRecordDrawer open={recordDrawerOpen} task={selectedTask} onClose={() => setRecordDrawerOpen(false)} /> </div> ); }

6.5 任务执行记录追踪

每个任务都有独立的执行历史记录,方便排查问题。

6.5.1 执行记录抽屉组件

// src/pages/scheduled-task/list/components/ExecutionRecordDrawer.tsx import { useEffect, useState } from 'react'; import { Drawer, Table, Tag, Space, Typography } from 'antd'; import dayjs from 'dayjs'; import type { ColumnsType } from 'antd/es/table'; import { ScheduledTask, TaskExecutionRecord } from '@types/scheduledTask'; import { scheduledTaskApi } from '@services/scheduledTask'; const { Text } = Typography; interface Props { open: boolean; task: ScheduledTask | null; onClose: () => void; } export default function ExecutionRecordDrawer({ open, task, onClose }: Props) { const [records, setRecords] = useState<TaskExecutionRecord[]>([]); const [loading, setLoading] = useState(false); useEffect(() => { if (open && task) { setLoading(true); scheduledTaskApi .getExecutionRecords(task.id) .then((res) => setRecords(res.list || [])) .finally(() => setLoading(false)); } }, [open, task]); const columns: ColumnsType<TaskExecutionRecord> = [ { title: '执行时间', dataIndex: 'startTime', width: 160, render: (t) => dayjs(t).format('YYYY-MM-DD HH:mm:ss'), }, { title: '触发方式', dataIndex: 'triggerType', width: 100, render: (t) => (t === 'cron' ? '定时触发' : '手动触发'), }, { title: '状态', dataIndex: 'status', width: 80, render: (status) => { const colors: Record<string, string> = { success: 'success', failed: 'error', running: 'processing', }; const labels: Record<string, string> = { success: '成功', failed: '失败', running: '执行中', }; return <Tag color={colors[status]}>{labels[status]}</Tag>; }, }, { title: '执行时长', dataIndex: 'duration', width: 100, render: (d) => (d !== undefined ? `${d} 秒` : '-'), }, { title: '结果', dataIndex: status.status === 'failed' ? 'errorMessage' : 'result', ellipsis: true, render: (text, record) => ( <Text type={record.status === 'failed' ? 'danger' : undefined} ellipsis> {record.status === 'failed' ? record.errorMessage : record.result} </Text> ), }, ]; return ( <Drawer title={task ? `${task.name} - 执行记录` : '执行记录'} width={720} open={open} onClose={onClose} > <Table rowKey="id" columns={columns} dataSource={records} loading={loading} pagination={{ pageSize: 20, showTotal: (t) => `共 ${t} 条` }} /> </Drawer> ); }

6.6 新建定时任务表单

// src/pages/scheduled-task/create/index.tsx import { useState } from 'react'; import { Card, Form, Input, Select, Radio, Button, Space, message } from 'antd'; import { ArrowLeftOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import CronPicker from '@components/CronPicker'; import { TaskType } from '@types/scheduledTask'; import { scheduledTaskApi } from '@services/scheduledTask'; export default function ScheduledTaskCreate() { const navigate = useNavigate(); const [form] = Form.useForm(); const [submitting, setSubmitting] = useState(false); const [cron, setCron] = useState('0 0 9 * * ?'); const [cronDesc, setCronDesc] = useState(''); const handleSubmit = async () => { try { const values = await form.validateFields(); setSubmitting(true); await scheduledTaskApi.create({ ...values, cronExpression: cron, cronDescription: cronDesc, }); message.success('创建成功'); navigate('/scheduled-task'); } catch (error: any) { if (error?.errorFields) return; } finally { setSubmitting(false); } }; return ( <div style={{ padding: 24 }}> <div style={{ marginBottom: 16 }}> <Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate(-1)}> 返回 </Button> </div> <Card title="新建定时任务"> <Form form={form} layout="vertical" style={{ maxWidth: 680 }}> <Form.Item label="任务名称" name="name" rules={[{ required: true, message: '请输入任务名称' }]}> <Input placeholder="例如:每日数据报表" maxLength={50} showCount /> </Form.Item> <Form.Item label="任务类型" name="type" rules={[{ required: true }]}> <Select placeholder="请选择任务类型" options={[ { label: '自动创建任务单', value: TaskType.TICKET_CREATE }, { label: '数据同步', value: TaskType.DATA_SYNC }, { label: '生成报表', value: TaskType.REPORT_GENERATE }, { label: '通知提醒', value: TaskType.NOTIFICATION }, { label: '数据清理', value: TaskType.DATA_CLEANUP }, ]} /> </Form.Item> <Form.Item label="执行周期" required> <CronPicker value={cron} onChange={(value, desc) => { setCron(value); setCronDesc(desc); }} /> </Form.Item> <Form.Item label="任务描述" name="description"> <Input.TextArea rows={3} placeholder="简要描述任务的用途..." maxLength={500} showCount /> </Form.Item> <Form.Item> <Space> <Button type="primary" loading={submitting} onClick={handleSubmit}> 创建并启用 </Button> <Button onClick={() => navigate(-1)}>取消</Button> </Space> </Form.Item> </Form> </Card> </div> ); }

本章小结

知识点关键内容
Cron 表达式6 段式语法:秒/分/时/日/月/周,常用符号与示例
可视化编辑器react-js-cron封装,支持人读描述生成
任务列表状态管理(运行/暂停/结束/异常)、执行统计、上次/下次执行时间
执行记录独立抽屉展示,记录每次执行的状态、时长、结果、错误信息
操作交互启用/暂停 Switch、立即执行、删除确认、批量操作
创建表单任务类型选择 + Cron 可视化配置 + 参数配置

下一章预告:我们来集成 AI 助手,学习 SSE 流式响应、智能摘要生成和 AI 对话组件的设计。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询