ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

第 6 章:定时任务管理

第 6 章:定时任务管理 本章学习目标理解 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{RUNNINGrunning,// 运行中PAUSEDpaused,// 已暂停FINISHEDfinished,// 已结束ERRORerror,// 异常}exportenumTaskType{TICKET_CREATEticket_create,// 自动创建任务单DATA_SYNCdata_sync,// 数据同步REPORT_GENERATEreport_generate,// 生成报表NOTIFICATIONnotification,// 通知提醒DATA_CLEANUPdata_cleanup,// 数据清理}exportinterfaceScheduledTask{id:string;name:string;// 任务名称description:string;// 任务描述type:TaskType;// 任务类型cronExpression:string;// Cron 表达式cronDescription:string;// Cron 描述人类可读status:ScheduledTaskStatus;params:Recordstring,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-cron6.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: Recordstring, 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 directionvertical sizemiddle style{{ width: 100% }} Card sizesmall 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 typesecondary 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: RecordScheduledTaskStatus, { 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: RecordTaskType, 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] useStateScheduledTask | 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: ColumnsTypeScheduledTask [ { 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 directionvertical size{4} Tag color{statusConfig[status]?.color}{statusConfig[status]?.label}/Tag {record.status ! ScheduledTaskStatus.FINISHED ( Switch sizesmall 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 typetext 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 typetext icon{MoreOutlined /} / /Dropdown /Space ), }, ]; return ( div style{{ padding: 24 }} Card title定时任务 extra{ Button typeprimary icon{PlusOutlined /} onClick{() navigate(/scheduled-task/create)} 新建定时任务 /Button } Table rowKeyid 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] useStateTaskExecutionRecord[]([]); 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: ColumnsTypeTaskExecutionRecord [ { 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: Recordstring, string { success: success, failed: error, running: processing, }; const labels: Recordstring, 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 rowKeyid 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 typetext icon{ArrowLeftOutlined /} onClick{() navigate(-1)} 返回 /Button /div Card title新建定时任务 Form form{form} layoutvertical style{{ maxWidth: 680 }} Form.Item label任务名称 namename rules{[{ required: true, message: 请输入任务名称 }]} Input placeholder例如每日数据报表 maxLength{50} showCount / /Form.Item Form.Item label任务类型 nametype 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任务描述 namedescription Input.TextArea rows{3} placeholder简要描述任务的用途... maxLength{500} showCount / /Form.Item Form.Item Space Button typeprimary 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 对话组件的设计。
返回列表