// +---------------------------------------------------------------------- declare (strict_types=1); namespace app\timer\command; use think\console\Command; use think\console\Input; use think\console\input\Argument; use think\console\input\Option; use think\console\Output; use think\facade\Event; use Workerman\Worker; /** * 定时器 (Workerman) * 用于执行系统里的定时任务, 例如自动确认收货 * 使用方法: 打开命令行 - 执行命令: php think timer start * Class Timer * @package app\common\command */ class Timer extends Command { // 定时器句柄/ID protected $timer; // 时间间隔 (单位: 秒, 默认1秒) protected int $interval = 1; protected function configure() { // 指令配置 $this->setName('timer') ->addArgument('status', Argument::REQUIRED, 'start/stop/reload/status/connections') ->addOption('d', null, Option::VALUE_NONE, 'daemon(守护进程)方式启动') ->addOption('i', null, Option::VALUE_OPTIONAL, '多长时间执行一次') ->setDescription('start/stop/restart 定时任务'); } protected function init(Input $input, Output $output) { global $argv; if ($input->hasOption('i')) $this->interval = (int)$input->getOption('i'); $argv[1] = $input->getArgument('status') ?: 'start'; if ($input->hasOption('d')) { $argv[2] = '-d'; } else { unset($argv[2]); } } /** * 创建定时器 * @param Input $input * @param Output $output * @return int|void|null */ protected function execute(Input $input, Output $output) { // PHP 8.1+ 会把「浮点隐式转 int 丢失精度」作为 E_DEPRECATED 抛出, // ThinkPHP 的错误处理会把它升级为 ErrorException 导致 worker 进程直接退出。 // 该告警不影响实际计算(仅截断小数),定时任务链路里暂时屏蔽,避免进程反复挂掉。 error_reporting(E_ALL & ~E_DEPRECATED); // worker 进程内的未捕获异常不会走 ThinkPHP 的异常渲染,堆栈会丢失, // 这里统一落盘到 runtime/timer_error.log,便于定位崩溃位置。 $this->registerErrorLogger(); $this->init($input, $output); // 创建定时器任务 $worker = new Worker(); $worker->onWorkerStart = [$this, 'start']; $worker->runAll(); } /** * 注册未捕获异常/致命错误日志(worker 场景下堆栈默认会被吞掉) */ private function registerErrorLogger(): void { $logFile = runtime_path() . 'timer_error.log'; set_error_handler($this->makeErrorHandler($logFile)); set_exception_handler(function (\Throwable $e) use ($logFile) { $this->writeErrorLog($logFile, sprintf( "%s %s: %s in %s:%d\n%s\n", date('Y-m-d H:i:s'), get_class($e), $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString() )); }); register_shutdown_function(function () use ($logFile) { $error = error_get_last(); if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) { $this->writeErrorLog($logFile, sprintf( "%s [fatal] %s in %s:%d\n", date('Y-m-d H:i:s'), $error['message'], $error['file'], $error['line'] )); } }); } /** * 自定义错误处理器:告警/弃用类错误直接落盘并吞掉(返回 true), * 阻止其被 ThinkPHP 升级为 ErrorException 打崩 worker 进程。 * 相同位置重复报错做去重(首次 + 每 500 次记录一条),避免日志刷屏。 */ private function makeErrorHandler(string $logFile): \Closure { return function (int $no, string $str, string $file = '', int $line = 0) use ($logFile): bool { static $seen = []; $key = md5($no . $str . $file . $line); $seen[$key] = ($seen[$key] ?? 0) + 1; if ($seen[$key] === 1 || $seen[$key] % 500 === 0) { $this->writeErrorLog($logFile, sprintf( "%s [%d] %s in %s:%d (累计 %d 次)\n", date('Y-m-d H:i:s'), $no, $str, $file, $line, $seen[$key] )); } return true; }; } /** * 写入错误日志文件 * @param string $logFile * @param string $content */ private function writeErrorLog(string $logFile, string $content): void { @file_put_contents($logFile, $content . PHP_EOL, FILE_APPEND); echo $content . PHP_EOL; } /** * 定时器执行的内容 * @return false|int */ public function start() { // workerman runAll 会重设错误处理器,这里在 worker 进程内重新挂回, // 确保告警类错误只落盘、不再升级为 ErrorException。 set_error_handler($this->makeErrorHandler(runtime_path() . 'timer_error.log')); // 每隔n秒执行一次 return $this->timer = \Workerman\Lib\Timer::add($this->interval, function () { try { // 这里执行系统预设的定时任务事件 echo 'timer...' . PHP_EOL; Event::trigger('StoreTask'); } catch (\Throwable $e) { $this->writeErrorLog(runtime_path() . 'timer_error.log', sprintf( "%s %s: %s in %s:%d\n%s\n", date('Y-m-d H:i:s'), get_class($e), $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString() )); $this->stop(); } }); } /** * 停止/删除定时器 * @return bool */ public function stop(): bool { return \Workerman\Lib\Timer::del($this->timer); } }