180, 'nba_match' => 180, 'cba_match' => 180, 'epl_match' => 180, 'bundesliga_match' => 180, 'laliga_match' => 180, 'seriea_match' => 180, 'ligue1_match' => 180, 'ucl_match' => 180, 'uel_match' => 180, 'tennis_match' => 180, 'esports_match' => 180, 'sports_match' => 180, 'live_detail' => 180, 'match_finish' => 180, 'lottery_draw' => 180, 'lottery_draw_force' => 180, 'article_content' => 600, 'article_content_fetch' => 900, 'league_news' => 900, 'fifa_worldcup_news' => 600, 'dqd_worldcup' => 600, ]; protected function configure() { $this->setName('crontab') ->setDescription('定时任务'); } protected function execute(Input $input, Output $output) { $lists = CrontabModel::where('status', CrontabEnum::START)->select()->toArray(); if (empty($lists)) { return false; } $time = time(); foreach ($lists as $item) { if (empty($item['last_time'])) { $lastTime = (new CronExpression($item['expression'])) ->getNextRunDate() ->getTimestamp(); CrontabModel::where('id', $item['id'])->update([ 'last_time' => $lastTime, ]); continue; } $nextTime = (new CronExpression($item['expression'])) ->getNextRunDate($item['last_time']) ->getTimestamp(); if ($nextTime > $time) { // 未到时间,不执行 continue; } // 开始执行 self::start($item); } } public static function start($item) { if (in_array($item['command'], self::CRAWLER_MIGRATED_COMMANDS, true)) { self::skipMigratedCrawler($item); return; } $startTime = microtime(true); $error = ''; $outputStr = ''; try { ob_start(); $params = explode(' ', $item['params']); if (is_array($params) && !empty($item['params'])) { Console::call($item['command'], $params); } else { Console::call($item['command']); } $outputStr = ob_get_clean() ?: ''; CrontabModel::where('id', $item['id'])->update(['error' => '']); } catch (\Exception $e) { $outputStr = ob_get_clean() ?: ''; $error = $e->getMessage(); CrontabModel::where('id', $item['id'])->update([ 'error' => $error, 'status' => CrontabEnum::ERROR ]); } finally { $endTime = microtime(true); $useTime = round(($endTime - $startTime), 2); $maxTime = max($useTime, $item['max_time']); $logId = 0; CrontabModel::where('id', $item['id'])->update([ 'last_time' => time(), 'time' => $useTime, 'max_time' => $maxTime ]); try { $log = CrontabLog::create([ 'crontab_id' => $item['id'], 'name' => $item['name'], 'command' => $item['command'], 'params' => $item['params'], 'status' => $error ? 2 : 1, 'output' => $error ?: mb_substr($outputStr ?: '执行完成', 0, 5000), 'elapsed' => $useTime, 'trigger_type' => 1, ]); $logId = (int) $log->id; } catch (\Exception $logEx) { } try { if ($error) { CrontabAlertService::upsertCommandException($item, $error ?: $outputStr, $logId); } else { CrontabAlertService::resolveByCrontabId((int) $item['id']); } } catch (\Exception $alertEx) { } } } protected static function skipMigratedCrawler(array $item): void { CrontabModel::where('id', $item['id'])->update([ 'last_time' => time(), 'error' => self::CRAWLER_MIGRATED_MESSAGE, ]); } protected static function startCrawler($item) { $crawlerDir = app()->getRootPath() . 'public/dongqiudi-crawler'; $python = is_file($crawlerDir . '/venv/bin/python3') ? $crawlerDir . '/venv/bin/python3' : 'python3'; $action = $item['params'] ?: 'all'; $timeout = self::getCrawlerActionTimeout($action); $now = time(); self::markStaleCrawlerLogs($item, $timeout, $now); $running = CrontabLog::where('crontab_id', $item['id']) ->where('status', 0) ->where('create_time', '>', $now - $timeout) ->order('id', 'desc') ->findOrEmpty(); if (!$running->isEmpty()) { CrontabModel::where('id', $item['id'])->update([ 'last_time' => $now, 'error' => '上一次任务仍在执行,已跳过本次', ]); return; } $log = CrontabLog::create([ 'crontab_id' => $item['id'], 'name' => $item['name'], 'command' => $item['command'], 'params' => $item['params'], 'status' => 0, 'output' => '', 'elapsed' => 0, 'trigger_type' => 1, ]); $logId = $log->id; $crontabId = $item['id']; $cmd = "cd {$crawlerDir} && nohup {$python} main.py {$action} --log-id={$logId} --crontab-id={$crontabId} > /dev/null 2>&1 &"; \shell_exec($cmd); CrontabModel::where('id', $item['id'])->update([ 'last_time' => time(), 'error' => '', ]); } protected static function getCrawlerActionTimeout(string $action): int { return self::CRAWLER_ACTION_TIMEOUTS[$action] ?? self::CRAWLER_DEFAULT_TIMEOUT; } protected static function markStaleCrawlerLogs(array $item, int $timeout, int $now): void { $staleIds = CrontabLog::where('crontab_id', $item['id']) ->where('status', 0) ->where('create_time', '<=', $now - $timeout) ->column('id'); if (empty($staleIds)) { return; } $message = sprintf('执行超过 %d 秒未结束,已由调度器标记超时', $timeout); CrontabLog::whereIn('id', $staleIds)->update([ 'status' => 2, 'output' => $message, 'elapsed' => $timeout, ]); CrontabModel::where('id', $item['id'])->update([ 'error' => $message, ]); try { CrontabAlertService::upsertCommandException($item, $message, (int) end($staleIds)); } catch (\Exception $e) { } } }