Skip to content

6.11 定时任务 ​

概述

定时任务系统由三部分组成:JobLogic(调度逻辑)+ BaseTask(任务处理器)+ JobRunCommand(命令行入口)。支持三种触发方式:Linux Crontab、Windows 计划任务、Daemon 守护进程。

系统架构 ​

text
┌─────────────────────────────────────────────────────────────────┐
│                    触发层(三选一)                               │
│                                                                 │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │ Linux Crontab│  │Windows 计划  │  │ Daemon 守护  │          │
│  │              │  │任务          │  │ 进程         │          │
│  │ * * * * *    │  │ schtasks     │  │ php think    │          │
│  │ php think    │  │              │  │ job:daemon   │          │
│  │ job:run      │  │              │  │              │          │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘          │
│         │                 │                 │                   │
│         └─────────────────┼─────────────────┘                   │
│                           │                                     │
│                           ▼                                     │
│              ┌─────────────────────────┐                        │
│              │   JobRunCommand          │                        │
│              │   php think job:run      │                        │
│              │   或 php think job:daemon│                        │
│              └────────────┬────────────┘                        │
└───────────────────────────┼─────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                    调度层(JobLogic)                             │
│                                                                 │
│  runPending()              shouldRun()                          │
│  ├─ 查询 status=1 的任务    ├─ 解析 cron 表达式                  │
│  ├─ 逐个判断是否到执行时间   ├─ 分 时 日 月 周 逐段匹配          │
│  └─ 到期则执行              └─ 全部匹配返回 true                 │
│                                                                 │
│  executeJob($job)                                               │
│  ├─ 检查执行策略(立即/排队/放弃)                               │
│  ├─ 调用 doExecute() 实际执行                                   │
│  ├─ 记录 JobLog 执行日志                                        │
│  └─ 返回执行结果                                                │
└───────────────────────────┼─────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                    执行层                                        │
│                                                                 │
│  doExecute($job)                                                │
│  ├─ 判断 URL 类型                                               │
│  │                                                              │
│  ├─ http:// / https:// → executeHttp()                          │
│  │   └─ curl 发起 HTTP GET/POST 请求                            │
│  │                                                              │
│  └─ 其它 → executeInternal()                                    │
│      └─ 映射到 app\task\{Name}Task::run()                      │
│                                                                 │
│  示例:                                                         │
│  url = "SendSms"     → app\task\SendSmsTask::run()             │
│  url = "Order/Close" → app\task\Order\CloseTask::run()         │
│  url = "https://..." → HTTP GET/POST                           │
└─────────────────────────────────────────────────────────────────┘

状态说明 ​

状态值说明可执行可暂停可启动
0未发布✗✗✓
1运行中✓✓✗
2暂停✗✗✓
3已删除✗✗✗

执行策略 ​

策略值说明行为
1立即执行丢弃之前的执行,立即运行
2执行一次等待上一次完成后执行
3放弃执行如果上一次还在运行则跳过

枚举文本自动翻译

JobLogic 配置了 serializeMaps,API 响应中会自动包含翻译后的文本字段:

原始字段文本字段字典编码说明
statusstatusTextjob_status任务状态(未发布/运行中/暂停/删除)
execute_policyexecutePolicyTextjob_execute_policy执行策略(立即执行/执行一次/放弃执行)

字典数据在代码生成器自动创建,也可在「系统管理 → 字典管理」中手动维护。

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 每分钟触发一次 PHP 命令。

配置步骤 ​

1. 编辑 crontab ​

bash
crontab -e

2. 添加定时任务 ​

bash
# 每分钟执行一次(推荐)
* * * * * cd /www/api && php think job:run >> /dev/null 2>&1

# 每分钟执行一次,日志写入文件(调试用)
* * * * * cd /www/api && php think job:run >> /var/log/job.log 2>&1

# 每分钟执行一次,带环境变量
* * * * * cd /www/api && /usr/bin/php think job:run >> /dev/null 2>&1

路径注意

  • cd /www/api 必须切换到项目根目录,否则 ThinkPHP 无法找到配置文件
  • php 建议使用绝对路径(如 /usr/bin/php),避免 cron 环境变量不同导致找不到 PHP
  • >> /dev/null 2>&1 表示丢弃输出,调试时可改为写入日志文件

3. 保存并验证 ​

bash
# 保存后 cron 自动生效

# 查看已配置的 cron
crontab -l

# 查看 cron 日志(确认是否正常触发)
tail -f /var/log/syslog | grep CRON

# 手动执行一次验证
cd /www/api && php think job:run

4. 权限设置 ​

bash
# 确保 PHP 有权限执行
chmod +x /usr/bin/php

# 确保项目目录权限
chmod -R 755 /www/api/runtime/

Crontab 常用命令 ​

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

系统 cron 目录 ​

text
/etc/crontab            # 系统级 cron 配置
/etc/cron.d/            # 额外的 cron 配置文件目录
/etc/cron.daily/        # 每天执行的脚本
/etc/cron.hourly/       # 每小时执行的脚本
/etc/cron.weekly/       # 每周执行的脚本
/etc/cron.monthly/      # 每月执行的脚本

方式二:Windows 计划任务 ​

适用场景

Windows Server 或本地开发环境,通过任务计划程序定时触发。

方式 A:图形界面配置 ​

1. 打开任务计划程序 ​

text
方法1:Win + R → 输入 taskschd.msc → 回车
方法2:控制面板 → 管理工具 → 任务计划程序
方法3:开始菜单搜索"任务计划程序"

2. 创建基本任务 ​

  1. 右侧点击"创建基本任务"
  2. 名称:RXThinkCMF 定时任务
  3. 描述:每分钟执行一次定时任务扫描
  4. 触发器:选择"每天"
  5. 开始时间:设置当前时间
  6. 重复任务间隔:1 分钟
  7. 持续时间:无限期
  8. 操作:选择"启动程序"
  9. 程序或脚本:
text
C:\xampp\php\php.exe
  1. 添加参数:
text
think job:run
  1. 起始于:
text
D:\xampp\htdocs\v3\thinkphp6
  1. 勾选"不管用户是否登录都要运行"
  2. 点击完成

3. 验证 ​

powershell
# 手动运行测试
cd D:\xampp\htdocs\v3\thinkphp6
C:\xampp\php\php.exe think job:run

# 在任务计划程序中右键任务 → 运行
# 查看"上次运行结果"是否为 0x0

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

powershell
# 创建每分钟执行的任务
schtasks /create /tn "RXThinkCMF_Job" /tr "C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /st 00:00 /f

# 参数说明:
# /tn  任务名称
# /tr  要执行的命令
# /sc  频率:minute(分钟)、hourly(小时)、daily(天)
# /mo  间隔:1 表示每 1 分钟
# /st  开始时间
# /f   强制覆盖同名任务

# 创建任务(指定工作目录)
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp\htdocs\v3\thinkphp6 && C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /f

# 查询任务
schtasks /query /tn "RXThinkCMF_Job"

# 手动运行
schtasks /run /tn "RXThinkCMF_Job"

# 删除任务
schtasks /delete /tn "RXThinkCMF_Job" /f

# 查看所有任务
schtasks /query /fo table

Windows 注意事项 ​

常见问题

  1. 路径空格:如果路径含空格,需要用引号包裹
  2. PHP 路径:使用 php.exe 的完整路径
  3. 工作目录:必须设置为项目根目录
  4. 权限:确保任务以有权限的用户身份运行
  5. 日志:可在参数中添加 >> log.txt 2>&1 重定向输出
powershell
# 带日志输出的完整命令
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp\htdocs\v3\thinkphp6 && C:\xampp\php\php.exe think job:run >> runtime\job.log 2>&1" /sc minute /mo 1 /f

方式三:Daemon 守护进程 ​

适用场景

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

原理 ​

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

实现方式 ​

方式 A:使用 Supervisor 管理(推荐生产环境) ​

创建命令文件:

php
// app/command/JobDaemonCommand.php
<?php
declare(strict_types=1);

namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use app\logic\JobLogic;

/**
 * 定时任务守护进程命令
 *
 * 常驻内存运行,每分钟扫描并执行到期任务。
 * 建议通过 Supervisor 管理,确保进程异常退出后自动重启。
 *
 * 用法:php think job:daemon
 */
class JobDaemonCommand extends Command
{
    protected function configure(): void
    {
        $this->setName('job:daemon')
            ->setDescription('定时任务守护进程(常驻内存)');
    }

    protected function execute(Input $input, Output $output): void
    {
        $output->writeln('[' . date('Y-m-d H:i:s') . '] Daemon 启动...');

        $logic = new JobLogic();

        while (true) {
            try {
                $count = $logic->runPending();
                if ($count > 0) {
                    $output->writeln('[' . date('Y-m-d H:i:s') . '] 执行了 ' . $count . ' 个任务');
                }
            } catch (\Exception $e) {
                $output->writeln('[' . date('Y-m-d H:i:s') . '] 异常: ' . $e->getMessage());
            }

            // 休眠 60 秒
            sleep(60);
        }
    }
}

Supervisor 配置:

ini
; /etc/supervisor/conf.d/rxthinkcmf-job.conf

[program:rxthinkcmf-job]
command=php /www/api/think job:daemon
directory=/www/api
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

# 重启
sudo supervisorctl restart rxthinkcmf-job

# 查看日志
tail -f /var/log/rxthinkcmf-job.log

方式 B:Windows CMD 运行 ​

在项目根目录打开 CMD,执行以下命令:

cmd
:: 启动 Daemon(前台运行,关闭窗口则停止)
php think job:daemon

:: 停止 Daemon(另开一个 CMD 窗口执行)
php think job:daemon --stop

启动成功输出:

text
[2026-09-23 09:06:07] Daemon 已启动 (PID: 22720)
[2026-09-23 09:06:07] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出

运行过程中的日志输出:

text
[2026-09-23 11:25:00] [执行] 定时任务演示 (cron: */5 * * * *)
[2026-09-23 11:25:00] [执行] 定时任务案例 (cron: */5 * * * *)
[2026-09-23 11:25:00] 本轮执行完成,共执行 2 个任务
[2026-09-23 11:30:00] [执行] 定时任务演示 (cron: */5 * * * *)
[2026-09-23 11:30:00] [跳过] 定时任务案例 - 当前分钟已执行过
[2026-09-23 11:30:00] 本轮执行完成,共执行 1 个任务

日志类型说明:

标记含义
[执行]任务已触发执行,括号内显示 cron 表达式
[跳过]同一分钟内该任务已执行过,自动跳过(防重复)
[失败]任务执行异常,后跟错误信息
本轮执行完成当前分钟的所有任务扫描完毕,显示执行数量

提示

  • 没有到期任务时,Daemon 不输出任何日行(静默等待),属于正常行为
  • [跳过] 是防重复机制,同一任务在同一分钟内只会执行一次
  • 出现 [失败] 时检查对应的任务处理器代码,单个任务失败不影响其他任务

前端联动: Daemon 启动后,后台管理系统的「定时任务」模块会自动检测到守护进程状态,页面顶部显示:

text
┌──────────────────────────────────────────────────────┐
│  [新增]  [删除]  [🟢 守护进程运行中 (PID: 22720)]    │
└──────────────────────────────────────────────────────┘
  • 绿色「守护进程运行中」按钮 — 表示 Daemon 正常工作,鼠标悬停可查看 PID
  • 灰色「守护进程未运行」— 表示 Daemon 未启动或已停止

状态检测原理

前端通过 GET /api/job/daemonStatus 接口轮询状态,后端读取 runtime/job_daemon.lock 锁文件中的 PID 并检查进程是否存活。因此即使 Daemon 非正常退出(如强制杀进程),只要锁文件残留,前端仍可能显示「运行中」,此时需手动删除 runtime/job_daemon.lock 或执行 php think job:daemon --stop 清理。

Windows 注意事项

  1. 无信号扩展:Windows 不支持 pcntl_signal,Ctrl+C 会强制终止进程而非优雅退出
  2. 停止方式:推荐使用 php think job:daemon --stop,或关闭 CMD 窗口
  3. 防重复启动:命令内部通过文件锁(runtime/job_daemon.lock)防止重复启动,启动前会自动检测并提示 Daemon 已在运行中 (PID: xxx)
  4. 后台运行:如需后台运行,可使用 start /B php think job:daemon,停止时用 php think job:daemon --stop
  5. 窗口关闭即停止:前台运行时关闭 CMD 窗口会终止进程,生产环境建议配合 Windows 服务或计划任务守护

方式 C:nohup 后台运行(简单场景) ​

bash
# 后台启动
nohup php /www/api/think job:daemon >> /var/log/job.log 2>&1 &

# 查看进程
ps aux | grep job:daemon

# 停止
kill $(pgrep -f "job:daemon")

方式 D:systemd 服务(推荐) ​

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/api
ExecStart=/usr/bin/php /www/api/think 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

# 重启
sudo systemctl restart rxthinkcmf-job

三种方式对比 ​

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

推荐选择

  • Linux 生产环境:Crontab(简单可靠)
  • Windows 生产环境:任务计划程序 或 Daemon 守护进程
  • 需要秒级精度:Daemon + Supervisor
  • Docker 环境:Daemon(容器内运行)
  • 本地开发调试:Daemon 前台运行(直接观察日志输出)

新建定时任务步骤 ​

1. 创建任务处理器 ​

php
<?php
// app/task/CleanLogTask.php
declare(strict_types=1);

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 = \app\model\LoginLog::where('create_time', '<', $cutoffDate)->delete();

        // 清理操作日志
        $operCount = \app\model\OperationLog::where('create_time', '<', $cutoffDate)->delete();

        // 清理任务日志
        $jobCount = \app\model\JobLog::where('start_time', '<', $cutoffDate)->delete();

        return "已清理 {$days} 天前的日志:登录日志 {$loginCount} 条,操作日志 {$operCount} 条,任务日志 {$jobCount} 条";
    }
}

2. 在数据库 job 表中配置 ​

sql
INSERT INTO think_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 /www/api && php think job:run >> /dev/null 2>&1

# 方式二:Windows 计划任务
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp\htdocs\v3\thinkphp6 && C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /f

# 方式三:Daemon 守护进程
php think job:daemon
# 停止:php think job:daemon --stop

API 接口 ​

方法路径权限码说明
GET/api/job/pagesys:job:page分页列表
GET/api/job/listsys:job:list全量列表
GET/api/job/detail/:idsys:job:detail详情
GET/api/job/runOnce/:idsys:job:execute手动执行一次
GET/api/job/pause/:idsys:job:pause暂停任务
GET/api/job/resume/:idsys: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/:idsys:job:delete删除任务
DELETE/api/job/batchDeletesys:job:batchDelete批量删除

任务日志 ​

方法路径权限码说明
GET/api/job/log/pagesys:job:log:page日志分页
GET/api/job/log/detail/:idsys:job:log:detail日志详情
DELETE/api/job/log/delete/:idsys:job:log:delete删除日志
DELETE/api/job/log/batchDeletesys:job:log:batchDelete批量删除

核心代码解析 ​

Cron 表达式匹配(JobLogic::matchCronField) ​

php
// app/logic/JobLogic.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;
}

防重复执行(JobLogic::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;
}

常见问题 ​

Q: 任务不执行? ​

A: 检查以下几点:

  1. 任务状态是否为 1(运行中)
  2. Cron 表达式格式是否正确(5 段)
  3. Crontab 是否已配置且 cron 服务已启动
  4. PHP 路径是否正确(建议使用绝对路径)
  5. 手动执行 php think job:run 是否正常

Q: 任务重复执行? ​

A: Daemon 模式下检查 hasRunThisMinute 是否生效。Crontab 模式下确保只有一个 cron 在触发。

Q: 任务执行超时? ​

A: 检查 PHP 的 max_execution_time 配置,长时间任务建议使用 Daemon 模式或设置 set_time_limit(0)。

Q: 如何查看任务执行日志? ​

A: 两种方式:

  1. 控制台实时日志:Daemon 前台运行时,[执行]、[跳过]、[失败] 等日志直接输出到终端
  2. 数据库持久化日志:通过 API /api/job/log/page 查询,或直接查 think_job_log 表

Q: Daemon 控制台没有新日志输出,是卡住了吗? ​

A: 不是。Daemon 每分钟检查一次,没有到期任务时静默等待,不输出任何日志。这是正常行为,说明当前没有任务需要执行。

Q: Windows 下 Daemon 如何启动和停止? ​

A: 在项目根目录打开 CMD:

cmd
:: 启动
php think job:daemon

:: 停止(另开一个 CMD 窗口)
php think job:daemon --stop

启动成功输出:

text
[2026-09-23 09:06:07] Daemon 已启动 (PID: 22720)
[2026-09-23 09:06:07] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出

启动后,后台管理系统「定时任务」模块顶部会显示绿色「守护进程运行中」按钮(带 PID 提示),表示 Daemon 正常工作。

如提示 Daemon 已在运行中 (PID: xxx),先执行 php think job:daemon --stop 停止旧进程,或手动删除 runtime/job_daemon.lock 文件。

小蚂蚁云团队 · 提供技术支持