| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- <?php
- // +----------------------------------------------------------------------
- // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
- // +----------------------------------------------------------------------
- // | Copyright (c) 2017~2025 https://www.yiovo.com All rights reserved.
- // +----------------------------------------------------------------------
- // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
- // +----------------------------------------------------------------------
- // | Author: 萤火科技 <admin@yiovo.com>
- // +----------------------------------------------------------------------
- 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);
- }
- }
|