Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_EleVue/zh/modules/job.md
T
2026-10-08 17:50:29 +08:00

648 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<?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 表中配置
```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`)。