# 6.11 定时任务 :::tip 概述 定时任务系统由三部分组成:`JobService`(调度逻辑)+ `BaseTask`(任务处理器)+ Artisan 命令(`job:run` / `job:daemon` 命令行入口)。支持三种触发方式:Linux Crontab、Windows 计划任务、Daemon 守护进程。 ::: ## 系统架构 ```text ┌─────────────────────────────────────────────────────────────────┐ │ 触发层(三选一) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 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 段格式:`分 时 日 月 周` ```text ┌────────── 分钟(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 命令。 ::: ### 配置步骤 ```bash # 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 常用命令 ```bash crontab -e # 编辑当前用户的 cron crontab -l # 列出当前用户的 cron crontab -r # 删除当前用户的所有 cron(慎用) ``` ## 方式二:Windows 计划任务 :::tip 适用场景 Windows Server 或本地开发环境(Laragon),通过任务计划程序定时触发。 ::: ### 方式 A:图形界面配置 ```text 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) ```powershell # 创建每分钟执行的任务(含工作目录与日志重定向) 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 常见问题 1. **路径空格**:路径含空格时必须用引号包裹 2. **PHP 路径**:使用 `php.exe` 的完整路径(Laragon 多版本共存时尤其注意 8.3 的路径) 3. **工作目录**:必须设置为项目根目录 4. **输出重定向**:`>>` 在 `cmd /c` 内生效,注意相对路径基于工作目录 ::: ## 方式三:Daemon 守护进程 :::tip 适用场景 需要更精确的执行时机,或不想依赖系统 cron 的场景。进程常驻内存,自行调度。 ::: ### 原理 ```text Daemon 进程启动 │ ▼ ┌─────────────────────┐ │ while (true) │ 无限循环 │ │ │ │ ├─ runPending() │ 扫描并执行到期任务 │ ├─ sleep(60) │ 休眠 60 秒 │ └─ 继续循环 │ └─────────────────────┘ ``` - 锁文件:`storage/app/job_daemon.lock`(内容为当前进程 PID,防止重复启动) - 防重复:同一任务在同一分钟内只执行一次(`hasRunThisMinute` 查 JobLog) ### 运行方式 **常用命令:** ```bash 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 :: 项目根目录打开 CMD —— 启动 php artisan job:daemon :: 停止(另开一个 CMD 窗口) php artisan job:daemon --stop ``` **启动成功输出:** ```text [2026-10-07 22:10:06] Daemon 已启动 (PID: 32116) [2026-10-07 22:10:06] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出 ``` **运行日志:** ```text [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 生产推荐) ```ini ; /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 ``` ```bash 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 生产推荐) ```ini ; /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 ``` ```bash sudo systemctl enable rxthinkcmf-job sudo systemctl start rxthinkcmf-job sudo systemctl status rxthinkcmf-job sudo journalctl -u rxthinkcmf-job -f ``` #### 方式 D:nohup 后台运行(简单场景) ```bash nohup php artisan job:daemon > storage/logs/job_daemon.log 2>&1 & ps aux | grep job:daemon kill $(pgrep -f "job:daemon") ``` #### Docker 环境 ```bash 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 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 表中配置 ```sql 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. 配置触发方式(三选一) ```bash # 方式一: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` 实际代码) ```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 定时任务](/zh/modules/job.html) 状态说明渲染,`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) ```php // 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) ```php /** * 检查任务在当前分钟是否已执行过 * * 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`) | ## 注意事项 1. **互斥运行**:Cron 模式和 Daemon 模式不要同时使用,否则会导致任务重复执行 2. **防重复执行**:Daemon 模式内置防重复机制,同一任务在同一分钟内不会重复执行 3. **错误隔离**:单个任务执行失败不影响其他任务和主循环 4. **优雅退出**:Daemon 支持 `SIGTERM`/`SIGINT` 信号,`Ctrl+C` 或 `kill` 均可优雅停止 5. **锁文件**:Daemon 的锁文件位于 `storage/app/job_daemon.lock`,异常退出后残留锁会自动检测并清理 6. **Windows 兼容**:Daemon 在 Windows 下可正常运行,但信号处理(pcntl)不可用,需通过 `--stop` 参数或 `taskkill` 停止 7. **日志截断**:执行结果消息截取前 500 字符,异常信息截取前 2000 字符写入 job_log 8. **create_user**:命令行执行的任务日志中 `create_user` 由 HasAuditColumns trait 自动处理(无登录用户时为空) ## 常见问题 ### Q: 任务不执行? A: 检查以下几点: 1. 任务状态是否为 `1`(运行中) 2. Cron 表达式格式是否正确(5 段) 3. Crontab 是否已配置且 cron 服务已启动 4. PHP 路径是否正确(建议使用绝对路径) 5. 手动执行 `php artisan job:run` 是否正常 ### Q: 任务重复执行? A: Daemon 模式下检查 `hasRunThisMinute` 是否生效;Crontab 模式下确保只有一个 cron 在触发(`crontab -l` 检查重复项)。 ### Q: 任务执行超时? A: 检查 PHP 的 `max_execution_time` 配置,长时间任务建议拆分或在处理器内调大 `set_time_limit(0)`。 ### Q: 如何查看任务执行日志? A: 两种方式: 1. **控制台实时日志**:Daemon 前台运行时,`[执行]`、`[跳过]`、`[失败]` 直接输出到终端 2. **数据库持久化日志**:通过 `GET /api/job/log/page` 查询,或直接查 `laravel_job_log` 表 ### Q: Daemon 控制台没有新日志输出,是卡住了吗? A: 不是。Daemon 每分钟检查一次,没有到期任务时静默等待,不输出任何日志。 ### Q: Windows 下 Daemon 如何启动和停止? A: 项目根目录打开 CMD: ```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`)。