# ThinkPHP 任务调度 **Repository Path**: zsping1989/think-schedule ## Basic Information - **Project Name**: ThinkPHP 任务调度 - **Description**: ThinkPHP 任务调度 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-18 - **Last Updated**: 2026-10-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # laraveladmin/think-schedule [![Packagist](https://img.shields.io/badge/packagist-laraveladmin%2Fthink--schedule-blue)](https://packagist.org/packages/laraveladmin/think-schedule) > ThinkPHP 的 Laravel 风格任务调度器:**系统 crontab 只留一条**,所有任务定义写在 PHP 代码里。 ```cron * * * * * cd /path/to/project && php think schedule:run >> /dev/null 2>&1 ``` ```php // 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`](https://packagist.org/packages/yzh52521/schedule) | 最接近,API 直接照搬 Laravel。但最后一次发版是 2023-08,依赖 Carbon 2 + Symfony Process;`CacheEventMutex::create()` 用普通 `set()` 加锁(非原子),`withoutOverlapping` 也没有先检查 `exists()`;没有测试。 | | `topthink/think-crontab` | ThinkPHP 5 时代产物,任务主要写在配置/数据库里,不是"代码里定义"的 Laravel 风格。 | | [`peppeocchi/php-cron-scheduler`](https://packagist.org/packages/peppeocchi/php-cron-scheduler) | 通用 PHP 调度器,没有 ThinkPHP 命令/容器/服务发现集成。 | 本包的目标是:**不把 Carbon / Symfony Process 拉进业务工程**,同时把互斥锁做成真正原子、可跨机(Redis)的实现,并补齐测试。 --- ## 二、安装 ```bash composer require laraveladmin/think-schedule ``` 包通过 `extra.think.services` 自动注册 `schedule:*` 命令;`composer install/dump-autoload` 时会执行 `php think service:discover`,无需手工改 `config/console.php`。 首次使用生成定义类和配置: ```bash 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: - 源码: - 当前版本:`1.0.0` 已发布到 Packagist,直接安装即可,不需要在业务工程里额外配置 VCS: ```bash composer require laraveladmin/think-schedule ``` 知睿侯 CRM 的 `server/composer.json` 也只保留普通 `require`,生产 `composer install` 会从 Packagist 解析并安装。 ## 三、快速开始 ### 1. 定义任务 ```php 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. 查看 ```bash php think schedule:list php think schedule:list --format=json ``` ### 3. 手工触发某个任务 ```bash php think schedule:test stats:daily php think schedule:test stats:daily --force # 先清互斥锁再执行 ``` ### 4. 安装 crontab(只此一条) ```cron * * * * * cd /path/to/project && php think schedule:run >> /dev/null 2>&1 ``` 调试时建议把输出留到日志: ```cron * * * * * cd /path/to/project && php think schedule:run >> runtime/schedule/runner.log 2>&1 ``` ### 5. 没有 cron 的本地开发 ```bash 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=json` | JSON 输出,便于监控/脚本 | | `schedule:test <片段>` | 立即执行匹配的任务,忽略 cron | | `schedule:work` | 常驻调度器,支持秒级频率 | | `schedule:clear-cache` | 清理 `withoutOverlapping` / `onOneServer` 锁 | | `schedule:clear-cache --all` | 清空当前前缀下全部调度锁 | | `schedule:init` | 生成定义类与配置文件 | | `schedule:exec` | 内部命令:后台任务执行器(不要手工调用) | `schedule:run` 的退出码:有任务失败返回 `1`,否则 `0`;配合 cron 告警: ```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')` | 每年 | 约束: ```php $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 一致。 --- ## 六、互斥、单机/多机 ```php $schedule->command('wx:sync --type=cron') ->everyThirtyMinutes() ->withoutOverlapping(1440) // 锁最长 1440 分钟 ->runInBackground(); ``` ```php $schedule->command('report:generate') ->dailyAt('17:00') ->onOneServer(); // 多机部署时同一分钟只允许一台执行 ``` ### 锁驱动 `config/schedule.php`: ```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()` 必须使用它。 多机部署: ```dotenv SCHEDULE_LOCK_DRIVER=redis SCHEDULE_LOCK_STORE=redis ``` ### 调度器自身锁 默认 `run.overlap_lock = false`:`schedule:run` 每分钟独立执行,上一分钟的长任务不会阻塞下一分钟的其它任务。 同一个任务是否并发由 `withoutOverlapping()` 控制(本包所有耗时任务都建议加上)。 如果业务上确实要求"整台机器同时只能有一个 schedule:run",可打开: ```php 'run' => ['overlap_lock' => true, 'lock_ttl' => 3600], ``` 打开后长任务会让后续任务延迟,按需选择。 ### 后台任务 `runInBackground()` 会把真正的命令交给内部子进程 `schedule:exec`; 子进程持有 `withoutOverlapping` 锁并执行任务,所以父进程退出后锁仍然有效。 后台任务无法在启动时得知最终退出码,因此 `onSuccess` / `onFailure` 对后台任务不生效; 子进程失败会追加到 `runtime/schedule/background.log`,任务自身输出仍在 `appendOutputTo()` 指定的文件。 --- ## 七、输出与回调 ```php $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`。单任务可覆盖: ```php $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` 会清空当前前缀下的锁;只应在确认没有任务运行时使用。 --- ## 十、测试 ```bash # 在接入本包的业务工程里(本仓库) vendor/bin/phpunit -c ../packages/think-schedule/phpunit.xml # 或包独立开发 composer install vendor/bin/phpunit ``` 测试覆盖:cron 频率、时区、`between/when/skip`、闭包/命令/脚本执行、 输出重定向、`withoutOverlapping`、后台子进程、文件锁自愈、Redis 互斥锁(Redis 可用时)。 --- ## License MIT