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 .= '
| {$th1} | {$th2} | {$th3} | {$th4} | {$th5} |
|---|