| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- <?php
- namespace App\Services;
- use App\Constants\Util;
- use App\Models\Config;
- use Illuminate\Support\Facades\App;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Storage;
- use Symfony\Component\Process\Exception\ProcessTimedOutException;
- use Symfony\Component\Process\Process;
- class LotteryImageService
- {
- /**
- * 生成开奖记录图片
- * @param array $records 开奖记录数组
- * @return string 图片的访问URL
- * @throws \Exception
- */
- public function generate(array $records): string
- {
- $html = $this->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 .= '<tr>';
- $rows .= '<td>' . htmlspecialchars($row['issue_no']) . " " . lang("期") . '</td>';
- $rows .= '<td>';
- foreach ($row['winning_numbers'] as $i => $num) {
- $color = match ($i) {
- 0 => '#FF8A8A',
- 1 => '#8AB8FF',
- 2 => '#FFDF80',
- default => '#000',
- };
- $rows .= "<span style='color: {$color};'>{$num}</span>";
- if ($i < count($row['winning_numbers']) - 1) $rows .= ' + ';
- }
- $rows .= ' = ' . array_sum($row['winning_numbers']) . '</td>';
- $rows .= '<td>' . $row['combo'] . '</td>';
- $rows .= '<td>' . $row['extreme'] . '</td>';
- $rows .= '<td>' . $row['tail'] . '</td>';
- $rows .= '</tr>';
- }
- $Canada28LotteryResults = lang("加拿大28开奖记录");
- $recentLotteryResults = lang("最近开奖记录");
- $th1 = lang("回合");
- $th2 = lang("结果");
- $th3 = lang("组合");
- $th4 = lang("极值");
- $th5 = lang("尾数");
- $createdTime = lang("生成时间");
- $dataForReferenceOnly = lang("数据仅供参考");
- return <<<HTML
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8">
- <title>{$Canada28LotteryResults}</title>
- <style>
- body {
- font-family: "Microsoft YaHei", Arial, sans-serif;
- /* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
- /* background: #667eea 0%; */
- margin: 0;
- padding: 5px;
- min-height: 100vh;
- }
- .container {
- max-width: 800px;
- margin: 0 auto;
- background: white;
- border-radius: 15px;
- box-shadow: 0 10px 30px rgba(0,0,0,0.3);
- overflow: hidden;
- }
- .header {
- /* background: linear-gradient(45deg, #e74c3c, #c0392b); */
- background: #e74c3c;
- color: white;
- padding: 20px;
- text-align: center;
- }
- .header h1 {
- margin: 0;
- font-size: 28px;
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
- }
- .results-table {
- width: 100%;
- border-collapse: collapse;
- }
- .results-table th {
- background: #34495e;
- color: white;
- padding: 15px 10px;
- text-align: center;
- }
- .results-table td {
- padding: 12px 10px;
- text-align: center;
- border-bottom: 1px solid #e0e0e0;
- font-weight: 900;
- }
- .footer {
- background: #ecf0f1;
- padding: 15px;
- text-align: center;
- color: #7f8c8d;
- font-size: 12px;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>{$Canada28LotteryResults}</h1>
- <div class="subtitle">{$recentLotteryResults}</div>
- </div>
- <table class="results-table">
- <thead>
- <tr>
- <th>{$th1}</th>
- <th>{$th2}</th>
- <th>{$th3}</th>
- <th>{$th4}</th>
- <th>{$th5}</th>
- </tr>
- </thead>
- <tbody>
- {$rows}
- </tbody>
- </table>
- <div class="footer">
- {$createdTime}: {$time} | {$dataForReferenceOnly}
- </div>
- </div>
- </body>
- </html>
- HTML;
- }
- }
|