Timer.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2025 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\timer\command;
  13. use think\console\Command;
  14. use think\console\Input;
  15. use think\console\input\Argument;
  16. use think\console\input\Option;
  17. use think\console\Output;
  18. use think\facade\Event;
  19. use Workerman\Worker;
  20. /**
  21. * 定时器 (Workerman)
  22. * 用于执行系统里的定时任务, 例如自动确认收货
  23. * 使用方法: 打开命令行 - 执行命令: php think timer start
  24. * Class Timer
  25. * @package app\common\command
  26. */
  27. class Timer extends Command
  28. {
  29. // 定时器句柄/ID
  30. protected $timer;
  31. // 时间间隔 (单位: 秒, 默认1秒)
  32. protected int $interval = 1;
  33. protected function configure()
  34. {
  35. // 指令配置
  36. $this->setName('timer')
  37. ->addArgument('status', Argument::REQUIRED, 'start/stop/reload/status/connections')
  38. ->addOption('d', null, Option::VALUE_NONE, 'daemon(守护进程)方式启动')
  39. ->addOption('i', null, Option::VALUE_OPTIONAL, '多长时间执行一次')
  40. ->setDescription('start/stop/restart 定时任务');
  41. }
  42. protected function init(Input $input, Output $output)
  43. {
  44. global $argv;
  45. if ($input->hasOption('i'))
  46. $this->interval = (int)$input->getOption('i');
  47. $argv[1] = $input->getArgument('status') ?: 'start';
  48. if ($input->hasOption('d')) {
  49. $argv[2] = '-d';
  50. } else {
  51. unset($argv[2]);
  52. }
  53. }
  54. /**
  55. * 创建定时器
  56. * @param Input $input
  57. * @param Output $output
  58. * @return int|void|null
  59. */
  60. protected function execute(Input $input, Output $output)
  61. {
  62. // PHP 8.1+ 会把「浮点隐式转 int 丢失精度」作为 E_DEPRECATED 抛出,
  63. // ThinkPHP 的错误处理会把它升级为 ErrorException 导致 worker 进程直接退出。
  64. // 该告警不影响实际计算(仅截断小数),定时任务链路里暂时屏蔽,避免进程反复挂掉。
  65. error_reporting(E_ALL & ~E_DEPRECATED);
  66. // worker 进程内的未捕获异常不会走 ThinkPHP 的异常渲染,堆栈会丢失,
  67. // 这里统一落盘到 runtime/timer_error.log,便于定位崩溃位置。
  68. $this->registerErrorLogger();
  69. $this->init($input, $output);
  70. // 创建定时器任务
  71. $worker = new Worker();
  72. $worker->onWorkerStart = [$this, 'start'];
  73. $worker->runAll();
  74. }
  75. /**
  76. * 注册未捕获异常/致命错误日志(worker 场景下堆栈默认会被吞掉)
  77. */
  78. private function registerErrorLogger(): void
  79. {
  80. $logFile = runtime_path() . 'timer_error.log';
  81. set_error_handler($this->makeErrorHandler($logFile));
  82. set_exception_handler(function (\Throwable $e) use ($logFile) {
  83. $this->writeErrorLog($logFile, sprintf(
  84. "%s %s: %s in %s:%d\n%s\n",
  85. date('Y-m-d H:i:s'),
  86. get_class($e),
  87. $e->getMessage(),
  88. $e->getFile(),
  89. $e->getLine(),
  90. $e->getTraceAsString()
  91. ));
  92. });
  93. register_shutdown_function(function () use ($logFile) {
  94. $error = error_get_last();
  95. if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
  96. $this->writeErrorLog($logFile, sprintf(
  97. "%s [fatal] %s in %s:%d\n",
  98. date('Y-m-d H:i:s'),
  99. $error['message'],
  100. $error['file'],
  101. $error['line']
  102. ));
  103. }
  104. });
  105. }
  106. /**
  107. * 自定义错误处理器:告警/弃用类错误直接落盘并吞掉(返回 true),
  108. * 阻止其被 ThinkPHP 升级为 ErrorException 打崩 worker 进程。
  109. * 相同位置重复报错做去重(首次 + 每 500 次记录一条),避免日志刷屏。
  110. */
  111. private function makeErrorHandler(string $logFile): \Closure
  112. {
  113. return function (int $no, string $str, string $file = '', int $line = 0) use ($logFile): bool {
  114. static $seen = [];
  115. $key = md5($no . $str . $file . $line);
  116. $seen[$key] = ($seen[$key] ?? 0) + 1;
  117. if ($seen[$key] === 1 || $seen[$key] % 500 === 0) {
  118. $this->writeErrorLog($logFile, sprintf(
  119. "%s [%d] %s in %s:%d (累计 %d 次)\n",
  120. date('Y-m-d H:i:s'),
  121. $no,
  122. $str,
  123. $file,
  124. $line,
  125. $seen[$key]
  126. ));
  127. }
  128. return true;
  129. };
  130. }
  131. /**
  132. * 写入错误日志文件
  133. * @param string $logFile
  134. * @param string $content
  135. */
  136. private function writeErrorLog(string $logFile, string $content): void
  137. {
  138. @file_put_contents($logFile, $content . PHP_EOL, FILE_APPEND);
  139. echo $content . PHP_EOL;
  140. }
  141. /**
  142. * 定时器执行的内容
  143. * @return false|int
  144. */
  145. public function start()
  146. {
  147. // workerman runAll 会重设错误处理器,这里在 worker 进程内重新挂回,
  148. // 确保告警类错误只落盘、不再升级为 ErrorException。
  149. set_error_handler($this->makeErrorHandler(runtime_path() . 'timer_error.log'));
  150. // 每隔n秒执行一次
  151. return $this->timer = \Workerman\Lib\Timer::add($this->interval, function () {
  152. try {
  153. // 这里执行系统预设的定时任务事件
  154. echo 'timer...' . PHP_EOL;
  155. Event::trigger('StoreTask');
  156. } catch (\Throwable $e) {
  157. $this->writeErrorLog(runtime_path() . 'timer_error.log', sprintf(
  158. "%s %s: %s in %s:%d\n%s\n",
  159. date('Y-m-d H:i:s'),
  160. get_class($e),
  161. $e->getMessage(),
  162. $e->getFile(),
  163. $e->getLine(),
  164. $e->getTraceAsString()
  165. ));
  166. $this->stop();
  167. }
  168. });
  169. }
  170. /**
  171. * 停止/删除定时器
  172. * @return bool
  173. */
  174. public function stop(): bool
  175. {
  176. return \Workerman\Lib\Timer::del($this->timer);
  177. }
  178. }