import{_ as i,o as a,c as n,a2 as t}from"./chunks/framework.BbmSSMjL.js";const o=JSON.parse('{"title":"6.11 定时任务","description":"","frontmatter":{},"headers":[],"relativePath":"zh/modules/job.md","filePath":"zh/modules/job.md"}'),h={name:"zh/modules/job.md"};function l(p,s,k,e,d,r){return a(),n("div",null,[...s[0]||(s[0]=[t(`

6.11 定时任务 ​

概述

定时任务系统由三部分组成: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_namevarchar任务名称
job_aliasvarchar任务别名(唯一)
job_groupvarchar任务分组
statustinyint状态:0=未发布 1=运行中 2=暂停
cron_expressionvarcharCron 表达式(5段:分 时 日 月 周)
execute_policytinyint执行策略:1=立即 2=执行一次 3=放弃
is_synctinyint是否同步:0=否 1=是
urlvarchar任务地址(内部类名或外部URL)
paramstext任务参数(JSON)
notetext备注

job_log 表字段 ​

字段类型说明
job_idint关联任务ID
job_namevarchar任务名称
job_groupvarchar任务分组
job_messagetext执行结果消息(截取前500字符)
cron_expressionvarcharCron 表达式
statustinyint状态:0=正常 1=失败
start_timedatetime开始时间
end_timedatetime结束时间
consume_timeint耗时(毫秒)
exception_infotext异常信息(截取前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(推荐) ​

适用场景

Linux / macOS 服务器,最常用的定时任务触发方式。系统级 cron 每分钟触发一次 Artisan 命令。

配置步骤 ​

bash
# 1. 编辑 crontab
crontab -e

# 2. 添加定时任务(每分钟执行一次,推荐)
* * * * * cd /www/RXThinkCMF_Laravel13_NaiveVue && /usr/bin/php artisan job:run >> /dev/null 2>&1

# 调试时改为写入日志文件
* * * * * cd /www/RXThinkCMF_Laravel13_NaiveVue && /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_NaiveVue && php artisan job:run   # 手动执行一次

路径注意

Crontab 常用命令 ​

bash
crontab -e          # 编辑当前用户的 cron
crontab -l          # 列出当前用户的 cron
crontab -r          # 删除当前用户的所有 cron(慎用)

方式二:Windows 计划任务 ​

适用场景

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_NaiveVue
6. 勾选"不管用户是否登录都要运行" → 完成

方式 B:命令行配置(schtasks) ​

powershell
# 创建每分钟执行的任务(含工作目录与日志重定向)
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\\laragon\\www\\RXThinkCMF_Laravel13_NaiveVue && 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

Windows 常见问题

  1. 路径空格:路径含空格时必须用引号包裹
  2. PHP 路径:使用 php.exe 的完整路径(Laragon 多版本共存时尤其注意 8.3 的路径)
  3. 工作目录:必须设置为项目根目录
  4. 输出重定向:>> 在 cmd /c 内生效,注意相对路径基于工作目录

方式三:Daemon 守护进程 ​

适用场景

需要更精确的执行时机,或不想依赖系统 cron 的场景。进程常驻内存,自行调度。

原理 ​

text
Daemon 进程启动
    │
    ▼
┌─────────────────────┐
│  while (true)       │  无限循环
│    │                │
│    ├─ runPending()  │  扫描并执行到期任务
│    ├─ sleep(60)     │  休眠 60 秒
│    └─ 继续循环      │
└─────────────────────┘

运行方式 ​

常用命令:

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)」按钮;未启动时显示灰色。

状态检测原理

前端通过 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_NaiveVue/artisan job:daemon
directory=/www/RXThinkCMF_Laravel13_NaiveVue
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_NaiveVue
ExecStart=/usr/bin/php /www/RXThinkCMF_Laravel13_NaiveVue/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 命令

三种方式对比 ​

特性CrontabWindows 计划任务Daemon 守护进程
适用系统Linux / macOSWindows全平台
最小间隔1 分钟1 分钟分钟级(每轮 sleep 60s)
资源占用低(按需启动)低(按需启动)常驻内存
可靠性高高高(配合 Supervisor/systemd)
日志追踪需重定向任务计划程序自带输出
推荐场景生产环境Windows 生产开发调试 / 不便配置 cron

推荐选择

新建定时任务步骤 ​

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'];
}

未配置字典翻译与文件字段

任务表的 status、execute_policy 等枚举直接以数值存储,前端按 6.11 定时任务 状态说明渲染,JobService 未声明 serializeMaps / fileFields。

API 接口 ​

任务管理 ​

方法路径权限码说明
GET/api/job/pagesys:job:page分页列表
GET/api/job/listsys: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/addsys:job:add新增任务
POST/api/job/statussys:job:update设置状态
PUT/api/job/updatesys:job:update修改任务
DELETE/api/job/delete/{id}sys:job:delete删除任务
DELETE/api/job/batchDeletesys:job:batchDelete批量删除

任务日志 ​

方法路径权限码说明
GET/api/job/log/pagesys:job:page日志分页
GET/api/job/log/detail/{id}sys:job:detail日志详情
DELETE/api/job/log/delete/{id}sys:job:delete删除日志
DELETE/api/job/log/batchDeletesys: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)。

`,107)])])}const g=i(h,[["render",l]]);export{o as __pageData,g as default};