24 KiB
6.11 定时任务
:::tip 概述
定时任务系统由三部分组成:JobService(调度逻辑)+ BaseTask(任务处理器)+ Artisan 命令(job:run / job:daemon 命令行入口)。支持三种触发方式:Linux Crontab、Windows 计划任务、Daemon 守护进程。
:::
系统架构
┌─────────────────────────────────────────────────────────────────┐
│ 触发层(三选一) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Linux Crontab│ │Windows 计划 │ │ Daemon 守护 │ │
│ │ │ │任务 │ │ 进程 │ │
│ │ * * * * * │ │ schtasks │ │ php artisan │ │
│ │ php artisan │ │ │ │ job:daemon │ │
│ │ job:run │ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ JobRunCommand / JobDaemon │ │
│ │ php artisan job:run │ │
│ │ php artisan job:daemon │ │
│ └────────────┬────────────────┘ │
└───────────────────────────┼─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 调度层(JobService) │
│ │
│ runPending() shouldRun() │
│ ├─ 查询 status=1 的任务 ├─ 解析 cron 表达式 │
│ ├─ 逐个判断是否到执行时间 ├─ 分 时 日 月 周 逐段匹配 │
│ └─ 到期则执行 └─ 全部匹配返回 true │
│ │
│ executeJob($job) │
│ ├─ 检查执行策略(立即/排队/放弃) │
│ ├─ 调用 doExecute() 实际执行 │
│ ├─ 记录 JobLog 执行日志 │
│ └─ 返回执行结果 │
└───────────────────────────┼─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 执行层 │
│ │
│ doExecute($job) │
│ ├─ 判断任务类型 │
│ │ │
│ ├─ http:// / https:// → executeHttp() │
│ │ └─ curl 发起 HTTP 请求 │
│ │ │
│ └─ 其它 → executeInternal() │
│ └─ 映射到 App\Task\{Name}Task::run() │
│ │
│ 示例: │
│ url = "SendSms" → App\Task\SendSmsTask::run() │
│ url = "Order/Close" → App\Task\Order\CloseTask::run() │
│ url = "https://..." → HTTP 请求 │
└─────────────────────────────────────────────────────────────────┘
任务数据结构
job 表字段
| 字段 | 类型 | 说明 |
|---|---|---|
job_name |
varchar | 任务名称 |
job_alias |
varchar | 任务别名(唯一) |
job_group |
varchar | 任务分组 |
status |
tinyint | 状态:0=未发布 1=运行中 2=暂停 |
cron_expression |
varchar | Cron 表达式(5段:分 时 日 月 周) |
execute_policy |
tinyint | 执行策略:1=立即 2=执行一次 3=放弃 |
is_sync |
tinyint | 是否同步:0=否 1=是 |
url |
varchar | 任务地址(内部类名或外部URL) |
params |
text | 任务参数(JSON) |
note |
text | 备注 |
job_log 表字段
| 字段 | 类型 | 说明 |
|---|---|---|
job_id |
int | 关联任务ID |
job_name |
varchar | 任务名称 |
job_group |
varchar | 任务分组 |
job_message |
text | 执行结果消息(截取前500字符) |
cron_expression |
varchar | Cron 表达式 |
status |
tinyint | 状态:0=正常 1=失败 |
start_time |
datetime | 开始时间 |
end_time |
datetime | 结束时间 |
consume_time |
int | 耗时(毫秒) |
exception_info |
text | 异常信息(截取前2000字符) |
状态说明
| 状态值 | 说明 | 可执行 | 可暂停 | 可启动 |
|---|---|---|---|---|
| 0 | 未发布 | ✗ | ✗ | ✓ |
| 1 | 运行中 | ✓ | ✓ | ✗ |
| 2 | 暂停 | ✗ | ✗ | ✓ |
| 3 | 已删除 | ✗ | ✗ | ✗ |
执行策略
| 策略值 | 说明 | 行为 |
|---|---|---|
| 1 | 立即执行 | 丢弃之前的执行,立即运行 |
| 2 | 执行一次 | 等待上一次完成后执行 |
| 3 | 放弃执行 | 如果上一次还在运行则跳过 |
Cron 表达式语法
标准 5 段格式:分 时 日 月 周
┌────────── 分钟(0-59)
│ ┌──────── 小时(0-23)
│ │ ┌────── 日(1-31)
│ │ │ ┌──── 月(1-12)
│ │ │ │ ┌── 周(0-6,0=周日)
│ │ │ │ │
* * * * *
| 表达式 | 说明 |
|---|---|
* * * * * |
每分钟 |
0 * * * * |
每小时整点 |
0 0 * * * |
每天 0 点 |
0 2 * * * |
每天凌晨 2 点 |
0 0 * * 1 |
每周一 0 点 |
0 0 1 * * |
每月 1 号 0 点 |
*/5 * * * * |
每 5 分钟 |
0 */2 * * * |
每 2 小时 |
0 9-18 * * 1-5 |
工作日 9-18 点每小时 |
0 2 1,15 * * |
每月 1 号和 15 号凌晨 2 点 |
方式一:Linux Crontab(推荐)
:::tip 适用场景 Linux / macOS 服务器,最常用的定时任务触发方式。系统级 cron 每分钟触发一次 Artisan 命令。 :::
配置步骤
# 1. 编辑 crontab
crontab -e
# 2. 添加定时任务(每分钟执行一次,推荐)
* * * * * cd /www/RXThinkCMF_Laravel13_EleVue && /usr/bin/php artisan job:run >> /dev/null 2>&1
# 调试时改为写入日志文件
* * * * * cd /www/RXThinkCMF_Laravel13_EleVue && /usr/bin/php artisan job:run >> /var/log/job.log 2>&1
# 3. 保存后验证
crontab -l # 查看已配置的 cron
tail -f /var/log/syslog | grep CRON # 查看 cron 日志
cd /www/RXThinkCMF_Laravel13_EleVue && php artisan job:run # 手动执行一次
:::warning 路径注意
cd必须切换到项目根目录,否则 Laravel 无法找到.env与配置php建议使用绝对路径(如/usr/bin/php),避免 cron 环境变量不同导致找不到 PHP>> /dev/null 2>&1表示丢弃输出,调试时改为写入日志文件 :::
Crontab 常用命令
crontab -e # 编辑当前用户的 cron
crontab -l # 列出当前用户的 cron
crontab -r # 删除当前用户的所有 cron(慎用)
方式二:Windows 计划任务
:::tip 适用场景 Windows Server 或本地开发环境(Laragon),通过任务计划程序定时触发。 :::
方式 A:图形界面配置
1. Win + R → taskschd.msc → 回车(打开任务计划程序)
2. 右侧点击"创建基本任务"
3. 名称:定时任务
4. 触发器:每天,重复间隔 1 分钟,持续时间无限期
5. 操作:启动程序
程序或脚本:D:\laragon\bin\php\php-8.3.x-Win32-vs16-x64\php.exe
添加参数:artisan job:run
起始于:D:\laragon\www\RXThinkCMF_Laravel13_EleVue
6. 勾选"不管用户是否登录都要运行" → 完成
方式 B:命令行配置(schtasks)
# 创建每分钟执行的任务(含工作目录与日志重定向)
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\laragon\www\RXThinkCMF_Laravel13_EleVue && D:\laragon\bin\php\php-8.3.x-Win32-vs16-x64\php.exe artisan job:run >> storage\job.log 2>&1" /sc minute /mo 1 /f
# 查询 / 手动运行 / 删除
schtasks /query /tn "RXThinkCMF_Job"
schtasks /run /tn "RXThinkCMF_Job"
schtasks /delete /tn "RXThinkCMF_Job" /f
:::warning Windows 常见问题
- 路径空格:路径含空格时必须用引号包裹
- PHP 路径:使用
php.exe的完整路径(Laragon 多版本共存时尤其注意 8.3 的路径) - 工作目录:必须设置为项目根目录
- 输出重定向:
>>在cmd /c内生效,注意相对路径基于工作目录 :::
方式三:Daemon 守护进程
:::tip 适用场景 需要更精确的执行时机,或不想依赖系统 cron 的场景。进程常驻内存,自行调度。 :::
原理
Daemon 进程启动
│
▼
┌─────────────────────┐
│ while (true) │ 无限循环
│ │ │
│ ├─ runPending() │ 扫描并执行到期任务
│ ├─ sleep(60) │ 休眠 60 秒
│ └─ 继续循环 │
└─────────────────────┘
- 锁文件:
storage/app/job_daemon.lock(内容为当前进程 PID,防止重复启动) - 防重复:同一任务在同一分钟内只执行一次(
hasRunThisMinute查 JobLog)
运行方式
常用命令:
php artisan job:daemon # 前台常驻(开发观察)
php artisan job:daemon --stop # 停止
php artisan job:run # 一次性执行(交给系统 cron 每分钟触发)
nohup php artisan job:daemon > storage/logs/job_daemon.log 2>&1 & # 后台
方式 A:前台运行(本地调试)
:: 项目根目录打开 CMD —— 启动
php artisan job:daemon
:: 停止(另开一个 CMD 窗口)
php artisan job:daemon --stop
启动成功输出:
[2026-10-07 22:10:06] Daemon 已启动 (PID: 32116)
[2026-10-07 22:10:06] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出
运行日志:
[2026-10-07 22:10:06] [执行] 定时任务演示 (cron: */5 * * * *)
[2026-10-07 22:10:06] [执行] 定时任务案例 (cron: */5 * * * *)
[2026-10-07 22:10:06] 本轮执行完成,共执行 2 个任务
| 标记 | 含义 |
|---|---|
[执行] |
任务已触发执行,括号内显示 cron 表达式 |
[跳过] |
同一分钟内该任务已执行过,自动跳过(防重复) |
[失败] |
任务执行异常,后跟错误信息(单个任务失败不影响其他任务) |
本轮执行完成 |
当前分钟扫描完毕,显示执行数量 |
:::tip 没有到期任务时 Daemon 静默等待(不输出日志),属正常行为。 :::
前端联动: Daemon 启动后,「定时任务」模块顶部显示绿色「守护进程运行中 (PID: xxx)」按钮;未启动时显示灰色。
:::tip 状态检测原理
前端通过 GET /api/job/daemonStatus 轮询,后端读取 storage/app/job_daemon.lock 中的 PID 并检查进程是否存活。若进程非正常退出但锁文件残留,前端可能仍显示「运行中」——执行 php artisan job:daemon --stop 或删除锁文件清理。
:::
方式 B:Supervisor 管理(Linux 生产推荐)
; /etc/supervisor/conf.d/rxthinkcmf-job.conf
[program:rxthinkcmf-job]
command=php /www/RXThinkCMF_Laravel13_EleVue/artisan job:daemon
directory=/www/RXThinkCMF_Laravel13_EleVue
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/rxthinkcmf-job.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
sudo supervisorctl reread && sudo supervisorctl update
sudo supervisorctl start rxthinkcmf-job
sudo supervisorctl status rxthinkcmf-job
tail -f /var/log/rxthinkcmf-job.log
方式 C:systemd 服务(Linux 生产推荐)
; /etc/systemd/system/rxthinkcmf-job.service
[Unit]
Description=RXThinkCMF Job Daemon
After=network.target mysql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/www/RXThinkCMF_Laravel13_EleVue
ExecStart=/usr/bin/php /www/RXThinkCMF_Laravel13_EleVue/artisan job:daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable rxthinkcmf-job
sudo systemctl start rxthinkcmf-job
sudo systemctl status rxthinkcmf-job
sudo journalctl -u rxthinkcmf-job -f
方式 D:nohup 后台运行(简单场景)
nohup php artisan job:daemon > storage/logs/job_daemon.log 2>&1 &
ps aux | grep job:daemon
kill $(pgrep -f "job:daemon")
Docker 环境
docker compose exec app php artisan job:daemon
# 或以独立常驻容器运行 app 镜像的 artisan 命令
三种方式对比
| 特性 | Crontab | Windows 计划任务 | Daemon 守护进程 |
|---|---|---|---|
| 适用系统 | Linux / macOS | Windows | 全平台 |
| 最小间隔 | 1 分钟 | 1 分钟 | 分钟级(每轮 sleep 60s) |
| 资源占用 | 低(按需启动) | 低(按需启动) | 常驻内存 |
| 可靠性 | 高 | 高 | 高(配合 Supervisor/systemd) |
| 日志追踪 | 需重定向 | 任务计划程序 | 自带输出 |
| 推荐场景 | 生产环境 | Windows 生产 | 开发调试 / 不便配置 cron |
:::tip 推荐选择
- Linux 生产环境:Crontab(简单可靠)
- Windows 生产环境:任务计划程序 或 Daemon + 计划任务守护
- Docker 环境:Daemon(容器内运行)
- 本地开发调试:Daemon 前台运行(直接观察日志输出) :::
新建定时任务步骤
1. 创建任务处理器
<?php
// app/Task/CleanLogTask.php(本步骤新建的处理器)
namespace App\Task;
/**
* 清理过期日志任务
*
* 数据库 url 字段:CleanLog
* 参数格式:{"days": 30}
*/
class CleanLogTask extends BaseTask
{
/**
* 执行任务
*
* @param array $params 任务参数
* @return string 执行结果消息
* @throws \Exception 执行失败时抛出
*/
public function run(array $params = []): string
{
$days = $params['days'] ?? 30;
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
// 清理登录日志
$loginCount = LoginLog::where('create_time', '<', $cutoffDate)->delete();
// 清理操作日志
$operCount = OperationLog::where('create_time', '<', $cutoffDate)->delete();
// 清理任务日志
$jobCount = JobLog::where('start_time', '<', $cutoffDate)->delete();
return "已清理 {$days} 天前的日志:登录日志 {$loginCount} 条,操作日志 {$operCount} 条,任务日志 {$jobCount} 条";
}
}
内置示例处理器:app/Task/HelloWorldTask.php(验证 daemon 调度)、app/Task/SendSmsTask.php(示例任务)。
2. 在数据库 job 表中配置
INSERT INTO laravel_job (
job_name, job_alias, job_group, job_trigger,
url, params, cron_expression,
execute_policy, status, is_sync,
is_delete, create_user, create_time
) VALUES (
'清理过期日志', 'CleanLog', '系统维护', 'cron',
'CleanLog', '{"days":30}', '0 2 * * *',
2, 1, 0,
0, 'admin', NOW()
);
(也可在「定时任务」管理界面新增。)
3. 配置触发方式(三选一)
# 方式一:Linux Crontab
crontab -e
# 添加:* * * * * cd /项目路径 && /usr/bin/php artisan job:run >> /dev/null 2>&1
# 方式二:Windows 计划任务(见上文 schtasks 示例)
# 方式三:Daemon 守护进程
php artisan job:daemon
# 停止:php artisan job:daemon --stop
Service 配置(摘自 app/Services/JobService.php 实际代码)
class JobService extends BaseService
{
use HasCrudOperations, HasQueryScopes, HasUniqueCheck;
protected string $modelClass = Job::class;
// 任务名 / 任务别名模糊匹配
protected array $pageLikeFields = ['job_name', 'job_alias'];
// 状态 / 任务分组精确匹配
protected array $pageEqFields = ['status', 'job_group'];
// 任务别名全局唯一
protected array $uniqueFields = ['job_alias'];
}
:::info 未配置字典翻译与文件字段
任务表的 status、execute_policy 等枚举直接以数值存储,前端按 6.11 定时任务 状态说明渲染,JobService 未声明 serializeMaps / fileFields。
:::
API 接口
任务管理
| 方法 | 路径 | 权限码 | 说明 |
|---|---|---|---|
| GET | /api/job/page |
sys:job:page | 分页列表 |
| GET | /api/job/list |
sys:job:list | 全量列表 |
| GET | /api/job/detail/{id} |
sys:job:detail | 详情 |
| GET | /api/job/runOnce/{id} |
sys:job:execute | 手动执行一次 |
| GET | /api/job/pause/{id} |
sys:job:pause | 暂停任务 |
| GET | /api/job/resume/{id} |
sys:job:resume | 恢复任务 |
| GET | /api/job/daemonStatus |
— | 守护进程状态 |
| POST | /api/job/add |
sys:job:add | 新增任务 |
| POST | /api/job/status |
sys:job:update | 设置状态 |
| PUT | /api/job/update |
sys:job:update | 修改任务 |
| DELETE | /api/job/delete/{id} |
sys:job:delete | 删除任务 |
| DELETE | /api/job/batchDelete |
sys:job:batchDelete | 批量删除 |
任务日志
| 方法 | 路径 | 权限码 | 说明 |
|---|---|---|---|
| GET | /api/job/log/page |
sys:job:page | 日志分页 |
| GET | /api/job/log/detail/{id} |
sys:job:detail | 日志详情 |
| DELETE | /api/job/log/delete/{id} |
sys:job:delete | 删除日志 |
| DELETE | /api/job/log/batchDelete |
sys:job:batchDelete | 批量删除 |
核心代码解析
Cron 表达式匹配(JobService::matchCronField)
// app/Services/JobService.php(来自项目实际代码)
/**
* 匹配 cron 字段
*
* 支持以下语法:
* - *:匹配所有
* - */n:每隔 n 个单位
* - n-m:范围匹配
* - n,m,...:逗号分隔的多值
* - n:单个值精确匹配
*/
protected function matchCronField(string $cronField, int $current): bool
{
if ($cronField === '*') {
return true;
}
// 处理 */n(每隔 n 分钟/小时等)
if (preg_match('/^\*\/(\d+)$/', $cronField, $m)) {
return $current % (int) $m[1] === 0;
}
// 处理 n-m(范围)
if (preg_match('/^(\d+)-(\d+)$/', $cronField, $m)) {
return $current >= (int) $m[1] && $current <= (int) $m[2];
}
// 处理逗号分隔的多个值
if (strpos($cronField, ',') !== false) {
$values = array_map('intval', explode(',', $cronField));
return in_array($current, $values);
}
// 单个值
return (int) $cronField === $current;
}
防重复执行(JobService::hasRunThisMinute)
/**
* 检查任务在当前分钟是否已执行过
*
* Daemon 模式下,防止同一分钟内重复执行。
*/
public function hasRunThisMinute(int $jobId): bool
{
$minuteStart = date('Y-m-d H:i') . ':00';
$minuteEnd = date('Y-m-d H:i') . ':59';
return JobLog::where('job_id', $jobId)
->where('start_time', '>=', $minuteStart)
->where('start_time', '<=', $minuteEnd)
->count() > 0;
}
JobService 能力清单
| 方法 | 说明 |
|---|---|
runPending() |
扫描并执行全部到期任务,返回执行数 |
shouldRun($job) |
解析 cron 判断任务是否到期 |
executeJob($job) |
执行入口(策略检查 + 执行 + 记录 JobLog) |
runOnce($id) |
手动执行一次(忽略 cron) |
setStatus($id, $status) / pause($id) / resume($id) |
状态流转 |
executeHttp($url, $params) |
HTTP 类任务执行 |
executeInternal($url, $params) |
内部 Task 类执行(映射 App\Task\{Name}Task) |
注意事项
- 互斥运行:Cron 模式和 Daemon 模式不要同时使用,否则会导致任务重复执行
- 防重复执行:Daemon 模式内置防重复机制,同一任务在同一分钟内不会重复执行
- 错误隔离:单个任务执行失败不影响其他任务和主循环
- 优雅退出:Daemon 支持
SIGTERM/SIGINT信号,Ctrl+C或kill均可优雅停止 - 锁文件:Daemon 的锁文件位于
storage/app/job_daemon.lock,异常退出后残留锁会自动检测并清理 - Windows 兼容:Daemon 在 Windows 下可正常运行,但信号处理(pcntl)不可用,需通过
--stop参数或taskkill停止 - 日志截断:执行结果消息截取前 500 字符,异常信息截取前 2000 字符写入 job_log
- create_user:命令行执行的任务日志中
create_user由 HasAuditColumns trait 自动处理(无登录用户时为空)
常见问题
Q: 任务不执行?
A: 检查以下几点:
- 任务状态是否为
1(运行中) - Cron 表达式格式是否正确(5 段)
- Crontab 是否已配置且 cron 服务已启动
- PHP 路径是否正确(建议使用绝对路径)
- 手动执行
php artisan job:run是否正常
Q: 任务重复执行?
A: Daemon 模式下检查 hasRunThisMinute 是否生效;Crontab 模式下确保只有一个 cron 在触发(crontab -l 检查重复项)。
Q: 任务执行超时?
A: 检查 PHP 的 max_execution_time 配置,长时间任务建议拆分或在处理器内调大 set_time_limit(0)。
Q: 如何查看任务执行日志?
A: 两种方式:
- 控制台实时日志:Daemon 前台运行时,
[执行]、[跳过]、[失败]直接输出到终端 - 数据库持久化日志:通过
GET /api/job/log/page查询,或直接查laravel_job_log表
Q: Daemon 控制台没有新日志输出,是卡住了吗?
A: 不是。Daemon 每分钟检查一次,没有到期任务时静默等待,不输出任何日志。
Q: Windows 下 Daemon 如何启动和停止?
A: 项目根目录打开 CMD:
:: 启动
php artisan job:daemon
:: 停止(另开一个 CMD 窗口)
php artisan job:daemon --stop
如提示 Daemon 已在运行中 (PID: xxx),先执行 php artisan job:daemon --stop 停止旧进程,或手动删除 storage/app/job_daemon.lock。
Q: 提示找不到 Task 类?
A: 确认 url 字段的类名与 app/Task/ 下的类文件一致(命名空间 App\Task),且类名采用大驼峰(SendSms → SendSmsTask)。