buildLocalizedHtml($records); $fileName = 'lottery_' . bin2hex(random_bytes(16)) . '.png'; $outputPath = storage_path('app/public/lottery/' . $fileName); $htmlPath = null; $process = null; $completed = false; $timeout = max(1, (float) config('services.lottery_image.timeout', 20)); $startedAt = microtime(true); try { if (!is_dir(dirname($outputPath)) && !mkdir(dirname($outputPath), 0755, true) && !is_dir(dirname($outputPath))) { throw new \RuntimeException('无法创建开奖图片目录'); } $htmlPath = tempnam(storage_path('app'), 'lottery_'); if ($htmlPath === false) { throw new \RuntimeException('无法创建开奖图片临时HTML'); } $temporaryHtmlPath = $htmlPath . '.html'; if (!rename($htmlPath, $temporaryHtmlPath)) { throw new \RuntimeException('无法创建开奖图片临时HTML文件'); } $htmlPath = $temporaryHtmlPath; if (file_put_contents($htmlPath, $html) === false) { throw new \RuntimeException('无法保存开奖图片临时HTML'); } $process = new Process([ (string) config('services.lottery_image.binary', 'wkhtmltoimage'), '--quality', '100', $htmlPath, $outputPath, ]); $process->setTimeout($timeout); $process->run(); if (!$process->isSuccessful() || !is_file($outputPath) || filesize($outputPath) === 0) { throw new \RuntimeException('渲染进程未生成有效图片'); } $url = Util::replacePartInUrl(Storage::url('lottery/' . $fileName)); Log::channel('issue')->info('开奖记录图片生成成功', $this->renderLogContext($process, $startedAt, $timeout)); $completed = true; return $url; } catch (\Throwable $exception) { $timedOut = $exception instanceof ProcessTimedOutException; Log::channel('issue')->error('开奖记录图片生成失败', $this->renderLogContext($process, $startedAt, $timeout) + [ 'reason' => $timedOut ? 'timeout' : 'render_failed', 'error_type' => get_class($exception), ]); $message = $timedOut ? '图片生成超时:渲染超过 ' . $timeout . ' 秒' : '图片生成失败,请检查 wkhtmltoimage、存储目录权限和开奖记录图片日志'; throw new \RuntimeException($message, 0, $exception); } finally { if ($htmlPath && is_file($htmlPath)) { unlink($htmlPath); } if (!$completed && is_file($outputPath)) { unlink($outputPath); } } } protected function buildLocalizedHtml(array $records): string { $lang = App::getLocale(); $group_language = Config::where('field', 'group_language')->first()->val; try { App::setLocale($group_language); return $this->buildHtml($records); } finally { App::setLocale($lang); } } private function renderLogContext(?Process $process, float $startedAt, float $timeout): array { $output = ''; if ($process && $process->isStarted()) { $output = trim($process->getErrorOutput() . "\n" . $process->getOutput()); } return [ 'duration_seconds' => round(microtime(true) - $startedAt, 3), 'timeout_seconds' => $timeout, 'exit_code' => $process ? $process->getExitCode() : null, 'renderer_output' => substr($output, 0, 2000), ]; } /** * 生成HTML内容 */ protected function buildHtml($records): string { $time = now()->format('Y-m-d H:i:s'); $rows = ''; foreach ($records as $row) { $rows .= ''; $rows .= '' . htmlspecialchars($row['issue_no']) . " " . lang("期") . ''; $rows .= ''; foreach ($row['winning_numbers'] as $i => $num) { $color = match ($i) { 0 => '#FF8A8A', 1 => '#8AB8FF', 2 => '#FFDF80', default => '#000', }; $rows .= "{$num}"; if ($i < count($row['winning_numbers']) - 1) $rows .= ' + '; } $rows .= ' = ' . array_sum($row['winning_numbers']) . ''; $rows .= '' . $row['combo'] . ''; $rows .= '' . $row['extreme'] . ''; $rows .= '' . $row['tail'] . ''; $rows .= ''; } $Canada28LotteryResults = lang("加拿大28开奖记录"); $recentLotteryResults = lang("最近开奖记录"); $th1 = lang("回合"); $th2 = lang("结果"); $th3 = lang("组合"); $th4 = lang("极值"); $th5 = lang("尾数"); $createdTime = lang("生成时间"); $dataForReferenceOnly = lang("数据仅供参考"); return << {$Canada28LotteryResults}

{$Canada28LotteryResults}

{$recentLotteryResults}
{$rows}
{$th1} {$th2} {$th3} {$th4} {$th5}
HTML; } }