Search by

laraveladmin / think-schedule

laraveladmin

Laravel-style task scheduler for ThinkPHP: define tasks in PHP and keep only one system cron entry.

Package info

gitee.com/zsping1989/think-schedule.git

pkg:composer/laraveladmin/think-schedule

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

1.0.0 2026-10-18 11:35 UTC

This package is not auto-updated.

Last update: 2026-10-19 09:55:56 UTC


README

Packagist

ThinkPHP 的 Laravel 风格任务调度器:系统 crontab 只留一条,所有任务定义写在 PHP 代码里。

* * * * * cd /path/to/project && php think schedule:run >> /dev/null 2>&1
// app/ScheduleDefinition.php
protected function schedule(Schedule $schedule): void
{
    $schedule->command('stats:daily')->dailyAt('01:00')->withoutOverlapping(120);
    $schedule->command('wx:sync --type=cron')->everyThirtyMinutes()->runInBackground();
    $schedule->exec('node /path/to/script.js')->hourly();
    $schedule->call(fn () => cache()->clear())->weeklyOn(1, '03:00');
}

以后增减任务、调整时间只改 PHP,不需要再登录服务器改 crontab。

一、为什么又做一个包

调研过的现成方案:

方案结论
yzh52521/schedule最接近,API 直接照搬 Laravel。但最后一次发版是 2023-08,依赖 Carbon 2 + Symfony Process;CacheEventMutex::create() 用普通 set() 加锁(非原子),withoutOverlapping 也没有先检查 exists();没有测试。
topthink/think-crontabThinkPHP 5 时代产物,任务主要写在配置/数据库里,不是"代码里定义"的 Laravel 风格。
peppeocchi/php-cron-scheduler通用 PHP 调度器,没有 ThinkPHP 命令/容器/服务发现集成。

本包的目标是:不把 Carbon / Symfony Process 拉进业务工程,同时把互斥锁做成真正原子、可跨机(Redis)的实现,并补齐测试。

二、安装

composer require laraveladmin/think-schedule

包通过 extra.think.services 自动注册 schedule:* 命令;composer install/dump-autoload 时会执行 php think service:discover,无需手工改 config/console.php。

首次使用生成定义类和配置:

php think schedule:init

会创建:

  • app/ScheduleDefinition.php:任务定义(日常只改这里)
  • config/schedule.php:锁驱动、时区、PHP 路径等运行参数

如果不想放在 app/ 根目录,可以把定义类移到 app/crontab/ScheduleDefinition.php 之类的独立目录,再把 config/schedule.php 的 definition 指向新类即可。

如果配置已经通过 vendor:publish 发布过,schedule:init 会跳过,不覆盖。

源码与安装方式

已发布到 Packagist,直接安装即可,不需要在业务工程里额外配置 VCS:

composer require laraveladmin/think-schedule

知睿侯 CRM 的 server/composer.json 也只保留普通 require,生产 composer install 会从 Packagist 解析并安装。

三、快速开始

1. 定义任务

<?php
declare(strict_types=1);

namespace app;

use LaravelAdmin\ThinkSchedule\Schedule;
use LaravelAdmin\ThinkSchedule\ScheduleDefinition as BaseScheduleDefinition;

class ScheduleDefinition extends BaseScheduleDefinition
{
    protected function schedule(Schedule $schedule): void
    {
        // ThinkPHP 命令(可带参数)
        $schedule->command('stats:daily')
            ->dailyAt('01:00')
            ->withoutOverlapping(120)
            ->appendOutputTo(runtime_path('schedule/stats-daily.log'));

        // 命令类 + 参数数组
        $schedule->command(\app\command\WxSync::class, ['--type' => 'cron'])
            ->everyThirtyMinutes()
            ->runInBackground();

        // 系统命令
        $schedule->exec('node /home/forge/script.js')->hourly();

        // 闭包(支持容器依赖注入)
        $schedule->call(function (\app\common\service\Foo $foo) {
            $foo->run();
        })->everyFiveMinutes();
    }
}

2. 查看

php think schedule:list
php think schedule:list --format=json

3. 手工触发某个任务

php think schedule:test stats:daily
php think schedule:test stats:daily --force   # 先清互斥锁再执行

4. 安装 crontab(只此一条)

* * * * * cd /path/to/project && php think schedule:run >> /dev/null 2>&1

调试时建议把输出留到日志:

* * * * * cd /path/to/project && php think schedule:run >> runtime/schedule/runner.log 2>&1

5. 没有 cron 的本地开发

php think schedule:work            # 常驻,每秒检查
php think schedule:work --once     # 只跑一轮

schedule:work 也是使用 everySecond() 等秒级频率的唯一方式。

四、命令

命令作用
schedule:run执行当前时刻到期的任务(crontab 每分钟调用)
schedule:run --dry-run --at="2026-10-19 01:00"调试:看指定时间会跑哪些任务,不执行
schedule:list列出任务、cron、下次运行时间、标记
schedule:list --format=jsonJSON 输出,便于监控/脚本
schedule:test <片段>立即执行匹配的任务,忽略 cron
schedule:work常驻调度器,支持秒级频率
schedule:clear-cache清理 withoutOverlapping / onOneServer 锁
schedule:clear-cache --all清空当前前缀下全部调度锁
schedule:init生成定义类与配置文件
schedule:exec内部命令:后台任务执行器(不要手工调用)

schedule:run 的退出码:有任务失败返回 1,否则 0;配合 cron 告警:

* * * * * cd /path/to/project && php think schedule:run >> runtime/schedule/runner.log 2>&1 || echo "schedule failed $(date)" >> runtime/schedule/alert.log

五、频率

方法说明
->cron('* * * * *')自定义 5 段 cron
->everySecond() … ->everyThirtySeconds()秒级(需 schedule:work)
->everyMinute() / ->everyTwoMinutes() … ->everyThirtyMinutes()分钟级
->hourly() / ->hourlyAt(17)每小时 / 每小时第 17 分钟
->everyTwoHours() / everyThreeHours() / everyFourHours() / everySixHours()小时间隔
->daily() / ->dailyAt('13:00')每天
->twiceDaily(1, 13) / ->twiceDailyAt(1, 13, 15)每天两次
->weekly() / ->weeklyOn(1, '8:00')每周
->monthly() / ->monthlyOn(4, '15:00')每月
->twiceMonthly(1, 16, '13:00')每月两次
->lastDayOfMonth('15:00')每月最后一天
->quarterly() / ->quarterlyOn(4, '14:00')每季度
->yearly() / ->yearlyOn(6, 1, '17:00')每年

约束:

$schedule->command('report:generate')
    ->weekdays()
    ->hourly()
    ->between('8:00', '17:00')
    ->timezone('Asia/Shanghai')
    ->when(fn () => config('app.report_enabled'))
    ->skip(fn () => is_holiday());

weekdays() / weekends() / mondays() … sundays() / days([1, 3])、 between() / unlessBetween() / when() / skip() 均可用。 Schedule::SUNDAY … Schedule::SATURDAY 常量与 Laravel 一致。

六、互斥、单机/多机

$schedule->command('wx:sync --type=cron')
    ->everyThirtyMinutes()
    ->withoutOverlapping(1440)   // 锁最长 1440 分钟
    ->runInBackground();
$schedule->command('report:generate')
    ->dailyAt('17:00')
    ->onOneServer();             // 多机部署时同一分钟只允许一台执行

锁驱动

config/schedule.php:

'lock' => [
    'driver' => env('SCHEDULE_LOCK_DRIVER', 'file'), // file | redis
    'store'  => env('SCHEDULE_LOCK_STORE', 'redis'),
    'prefix' => env('SCHEDULE_LOCK_PREFIX', 'schedule:'),
    'path'   => '', // file 驱动目录,留空 = runtime/schedule/locks
],
  • file:单机锁,fopen(..., 'x') 原子占位 + pid/TTL 自愈;不依赖 flock,Docker Desktop / 网络盘也能用。
  • redis:SET NX EX 原子加锁 + Lua 比较 token 释放;跨机共享,onOneServer() 必须使用它。

多机部署:

SCHEDULE_LOCK_DRIVER=redis
SCHEDULE_LOCK_STORE=redis

调度器自身锁

默认 run.overlap_lock = false:schedule:run 每分钟独立执行,上一分钟的长任务不会阻塞下一分钟的其它任务。 同一个任务是否并发由 withoutOverlapping() 控制(本包所有耗时任务都建议加上)。

如果业务上确实要求"整台机器同时只能有一个 schedule:run",可打开:

'run' => ['overlap_lock' => true, 'lock_ttl' => 3600],

打开后长任务会让后续任务延迟,按需选择。

后台任务

runInBackground() 会把真正的命令交给内部子进程 schedule:exec; 子进程持有 withoutOverlapping 锁并执行任务,所以父进程退出后锁仍然有效。 后台任务无法在启动时得知最终退出码,因此 onSuccess / onFailure 对后台任务不生效; 子进程失败会追加到 runtime/schedule/background.log,任务自身输出仍在 appendOutputTo() 指定的文件。

七、输出与回调

$schedule->command('report:generate')
    ->daily()
    ->sendOutputTo(runtime_path('report.log'))       // 覆盖
    ->appendOutputTo(runtime_path('report.log'))     // 追加(后者生效)
    ->before(fn () => Log::info('report start'))
    ->after(fn () => Log::info('report end'))
    ->onSuccess(fn () => Log::info('report ok'))
    ->onFailure(function (\Throwable $e) {
        Log::error('report failed: ' . $e->getMessage());
    });
  • sendOutputTo() / appendOutputTo() 会自动创建目录;
  • timeout(300) 可限制前台任务执行时间,超时退出码为 124;
  • onFailure 可以接收 Throwable 参数(容器会按类型注入)。

八、时区

默认取 config/app.php 的 default_timezone。单任务可覆盖:

$schedule->command('report:generate')
    ->timezone('America/New_York')
    ->dailyAt('02:00');

注意:使用夏令时时区的任务在切换日可能重复或跳过,能不用尽量不用。

九、实现说明 / 限制

  • 依赖只有 dragonmantank/cron-expression 和 topthink/framework,不引入 Carbon / Symfony Process。
  • schedule:run 按分钟评估;everySecond() 等秒级频率需要 schedule:work(或自己写一个常驻进程)。
  • file 锁只保证单机互斥;多机请用 redis + onOneServer()。
  • 后台任务的 onSuccess / onFailure 不会在任务真正结束时触发(父进程已退出)。
  • 锁目录 / 日志文件默认在 runtime/ 下,记得纳入日志轮转与清理策略。
  • schedule:clear-cache --all 会清空当前前缀下的锁;只应在确认没有任务运行时使用。

十、测试

# 在接入本包的业务工程里(本仓库)
vendor/bin/phpunit -c ../packages/think-schedule/phpunit.xml

# 或包独立开发
composer install
vendor/bin/phpunit

测试覆盖:cron 频率、时区、between/when/skip、闭包/命令/脚本执行、 输出重定向、withoutOverlapping、后台子进程、文件锁自愈、Redis 互斥锁(Redis 可用时)。

License

MIT