--- name: "ulthon-timer" description: "内置秒级定时器(php think timer)的使用与扩展规范;用于新增/调整定时任务(site/call、并发分片、TimerController 防刷、timer.mode normal/parallel)。" --- # timer(内置秒级定时器) ## 核心机制(你要记住的 3 件事) 1) 任务配置统一从 `app_file_path('common/command/timer/config.php')` 读取 2) 每个配置会按 `concurrency` 自动展开成多份任务实例,并自动注入 `concurrency_id` / `concurrency_count` 3) “是否该执行”主要由定时器侧的 Cache 节流控制(可选叠加控制器侧的防刷保护) 相关实现入口: - 系统入口(优先从 app 层理解): - 定时器命令入口:`app/common/command/Timer.php`(实现继承自 `extend/base/common/command/TimerBase.php`) - 任务实例服务入口:`app/common/service/TimerService.php`(实现继承自 `extend/base/common/service/TimerServiceBase.php`) - site 任务控制器基类入口:`app/common/controller/TimerController.php`(实现继承自 `extend/base/common/controller/TimerControllerBase.php`) - 实现文件(需要深入机制时再看 Base): - [Timer.php](../../../app/common/command/Timer.php) / [TimerBase.php](../../../extend/base/common/command/TimerBase.php) - [TimerService.php](../../../app/common/service/TimerService.php) / [TimerServiceBase.php](../../../extend/base/common/service/TimerServiceBase.php) - [TimerController.php](../../../app/common/controller/TimerController.php) / [TimerControllerBase.php](../../../extend/base/common/controller/TimerControllerBase.php) - 运行模式配置:[timer.php](../../../config/timer.php) ## 新增定时任务(默认规则) 无特殊情况下,新增定时任务应当通过本机制实现:在 `timer/config.php` 注册任务 + 以 `site` 或 `call` 的方式实现目标逻辑。 ### 1) 选择任务类型 - `site`:通过 HTTP 访问一个控制器地址(默认优先使用)。即使任务需要“长时间运行”,也尽量设计成 `site` 模式(分片/分批/可重入),以复用框架的页面/接口同体、鉴权、日志、事务、限流等能力。 - `call`:直接执行一个 PHP callable(不推荐;除非万不得已)。仅在确实不适合走 HTTP 上下文、且不希望暴露为控制器入口时使用。 长时间运行任务的推荐写法(仍用 `site`): - 设计为“可重入”的短任务:每次 `do()` 只处理一小批数据,处理进度写入缓存/表,下次继续 - 结合 `concurrency` 做分片:按 `$this->concurrencyId` 划分数据范围,多个实例并行推进 - 结合 `frequency` 控制节奏:用调度频率限制整体吞吐,避免单次占用过久 ### 2) 编写任务目标(target) #### A. site 类型(HTTP 任务) 实现方式建议: - 框架使用者(做业务):仅在 `app/tools/controller/timer/` 新增控制器 `*.php` 即可;不需要、也不应该在 `extend/base/` 增加 `*Base.php` - 框架作者(维护内核):在 `extend/base/tools/controller/timer/` 新增 `*Base.php` 作为默认实现,同时在 `app/tools/controller/timer/` 增加同名入口类 `*.php` 继承 Base(系统唯一调用入口) - 控制器建议继承 `app\common\controller\TimerController`(以获得并发参数校验与可选的 `$frequency` 防刷);执行入口一般提供 `do()` 示例(已有实现可参考):[ClearLog.php](../../../app/tools/controller/timer/ClearLog.php)、[ClearLogBase.php](../../../extend/base/tools/controller/timer/ClearLogBase.php) 并发与防刷建议: - 如需并发分片处理,在控制器中设置: - `protected $concurrency = N;` - 使用 `$this->concurrencyId` 做分片编号(0 ~ N-1) - 如希望防止外部重复请求(不仅是定时器自身节流),在控制器中设置: - `protected $frequency = 秒数;` - 这会启用 `TimerControllerBase::protectVisit()` 基于 URL 的防刷限制 #### B. call 类型(函数任务) 目标形态: - `target` 为 `call_user_func` 可执行的 callable,例如:`[SomeService::class, 'method']`、闭包等 - 由于 Base/App 双层机制的入口要求,类引用应指向 `app/` 下的入口类(由入口类继承 Base 实现) - 长时间/重任务不建议用 `call`:它缺少 HTTP 上下文与控制器层通用能力,排错与复用成本更高 ### 3) 注册到任务配置(timer/config.php) 默认配置文件位置(支持分层覆盖): - 优先覆盖:`app/common/command/timer/config.php`(一旦存在,`app_file_path(...)` 将优先读取此文件) - 框架默认:`extend/base/common/command/timer/config.php`(当 app 未提供时回落到该文件) 字段说明(兜底默认值由 Base 层的 `initConfigItem()` 提供): - `name`:任务唯一名称(用于 Cache key),不可重复 - `type`:`site` 或 `call` - `target`: - `site`:以 `/` 开头的相对路径(建议指向 `tools/timer.*` 控制器的 `do` 方法) - `call`:callable - `frequency`:执行频率(秒),小于 0 会被修正为 0 - `concurrency`:并发数量(默认 1)。`site` 类型会自动把并发参数写入 query - `run_type`:保留字段(当前未参与调度逻辑) 示例(site): ```php return [ [ 'name' => 'clear_log', 'type' => 'site', 'target' => '/tools/timer.ClearLog/do', 'frequency' => 600, 'concurrency' => 1, ], ]; ``` 示例(call): ```php return [ [ 'name' => 'system_host_register', 'type' => 'call', 'target' => [\app\common\service\HostService::class, 'heartbeat'], 'frequency' => 30, ], ]; ``` ## 运行与验证 ### 运行命令 - 常规运行:`php think timer` - 只跑一轮(便于验证):`php think timer --temp` - 无任务时不输出“no request”:`php think timer --quit` ### 本地调试(指定请求 Host) site 任务会按站点域名发起请求,默认从 `sysconfig('site','site_domain')` 读取。 - 本地调试:`php think timer --local --local-host=http://localhost --local-port=8000` ### 运行模式 配置在 [timer.php](../../../config/timer.php): - `normal`:单进程循环 + Guzzle async(默认) - `parallel`:Workerman 多进程模式(并发更高,相关连接参数在 `timer.php` 中) ## 常见坑位(快速自检) - `name` 重复:会导致 Cache key 冲突,表现为任务“莫名其妙不跑/跑得不对” - `concurrency` 与控制器侧 `$concurrency` 不一致:会触发 `concurrency id/count error` - 只依赖控制器侧 `$frequency`:它只是防刷,不是调度;调度频率以定时器侧 Cache 节流为准