false, 'error' => 'ต้องเปิด PHP extension curl (ติดตั้งแพ็กเกจ php-curl แล้วรีสตาร์ท PHP-FPM)'], 500); } $port = preg_replace('/[^0-9]/', '', (string)(getenv('GAME_NODE_PORT') ?: '3001')) ?: '3001'; $host = getenv('GAME_NODE_INTERNAL_HOST') ?: '127.0.0.1'; $target = 'http://' . $host . ':' . $port . '/Game/api/quiz-settings'; function quiz_settings_disk_path(): string { return dirname(__DIR__, 2) . '/Game/data/quiz-settings.json'; } /** * รวมฟิลด์ที่ส่งมาใน PUT เข้ากับไฟล์บนดิสก์ (ไม่ทับฟิลด์ที่ไม่ได้ส่งมา) */ function merge_quiz_settings_disk_from_put(string $rawBody): bool { $patch = json_decode($rawBody, true); if (!is_array($patch)) { return false; } $path = quiz_settings_disk_path(); $base = []; $hadFileButInvalidJson = false; if (is_file($path)) { $prev = @file_get_contents($path); if ($prev !== false && $prev !== '') { $d = json_decode($prev, true); if (is_array($d)) { $base = $d; } else { $hadFileButInvalidJson = true; } } } if ($hadFileButInvalidJson) { return false; } $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'quizRoundQuestionCount', 'questions', 'carryQuestions', 'battleQuizMcq']; foreach ($mergeKeys as $k) { if (array_key_exists($k, $patch)) { $base[$k] = $patch[$k]; } } $dir = dirname($path); if (!is_dir($dir)) { if (!@mkdir($dir, 0755, true)) { return false; } } $json = json_encode($base, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); if ($json === false) { return false; } return @file_put_contents($path, $json) !== false; } /** * แปะค่าจาก quiz-settings.json ใต้ docroot ทับ JSON จาก Node — ให้แอดมินรีเฟรชแล้วเห็นค่าที่ merge หลัง PUT (กันคนละไฟล์กับ Node) * Overlay keys from on-disk quiz-settings.json over Node JSON — Admin refresh matches post-PUT merge (fixes Node vs disk path mismatch). */ function overlay_quiz_settings_from_disk(string $nodeBody): string { $j = json_decode($nodeBody, true); if (!is_array($j)) { return $nodeBody; } $path = quiz_settings_disk_path(); if (!is_file($path)) { return $nodeBody; } $raw = @file_get_contents($path); if ($raw === false || $raw === '') { return $nodeBody; } $disk = json_decode($raw, true); if (!is_array($disk)) { return $nodeBody; } if (array_key_exists('battleQuizMcq', $disk)) { $j['battleQuizMcq'] = $disk['battleQuizMcq']; } if (array_key_exists('carryMapPanelTheme', $disk) && is_array($disk['carryMapPanelTheme'])) { $j['carryMapPanelTheme'] = $disk['carryMapPanelTheme']; } if (array_key_exists('quizMapPanelTheme', $disk) && is_array($disk['quizMapPanelTheme'])) { $j['quizMapPanelTheme'] = $disk['quizMapPanelTheme']; } if (array_key_exists('carryEmbedCountdownTheme', $disk) && is_array($disk['carryEmbedCountdownTheme'])) { $j['carryEmbedCountdownTheme'] = $disk['carryEmbedCountdownTheme']; } /* ไม่ทับ carryChoicePlaqueThemes / carryChoicePlaqueTheme จากดิสก์ — ใช้ค่าจาก Node (อ่านไฟล์เดียวกันกับ disk merge) เพื่อกันทับด้วย JSON เก่าที่ไม่มี plaqueImageUrl ทำให้ช่อง URL ใน Admin ว่างหลังรีเฟรช */ foreach (['carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'quizRoundQuestionCount'] as $ck) { if (array_key_exists($ck, $disk)) { $j[$ck] = $disk[$ck]; } } if (array_key_exists('carryQuestions', $disk) && is_array($disk['carryQuestions'])) { $j['carryQuestions'] = $disk['carryQuestions']; } $enc = json_encode($j, JSON_UNESCAPED_UNICODE); return $enc !== false ? $enc : $nodeBody; } function proxy_quiz_curl(string $method, ?string $body = null): void { global $target; $ch = curl_init($target); if ($ch === false) { json_response(['ok' => false, 'error' => 'curl init failed'], 500); } $headers = ['Accept: application/json']; if ($body !== null && $method !== 'GET') { $headers[] = 'Content-Type: application/json'; } $opts = [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, ]; if ($body !== null && $method !== 'GET') { $opts[CURLOPT_POSTFIELDS] = $body; } curl_setopt_array($ch, $opts); $out = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($method === 'PUT' && $body !== null && $body !== '') { merge_quiz_settings_disk_from_put($body); } if ($out === false || $out === '') { if ($method === 'GET') { $disk = quiz_settings_disk_path(); if (is_file($disk)) { $rawFile = @file_get_contents($disk); if ($rawFile !== false && $rawFile !== '') { http_response_code(200); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); echo $rawFile; exit; } } } if ($method === 'PUT' && $body !== null && $body !== '' && is_file(quiz_settings_disk_path())) { http_response_code(200); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); $payload = ['ok' => true, 'diskFallback' => true]; $rawDisk = @file_get_contents(quiz_settings_disk_path()); if ($rawDisk !== false && $rawDisk !== '') { $dj = json_decode($rawDisk, true); if (is_array($dj)) { if (isset($dj['carryMapPanelTheme']) && is_array($dj['carryMapPanelTheme'])) { $payload['carryMapPanelTheme'] = $dj['carryMapPanelTheme']; } if (isset($dj['carryEmbedCountdownTheme']) && is_array($dj['carryEmbedCountdownTheme'])) { $payload['carryEmbedCountdownTheme'] = $dj['carryEmbedCountdownTheme']; } if (isset($dj['carryChoicePlaqueThemes']) && is_array($dj['carryChoicePlaqueThemes'])) { $payload['carryChoicePlaqueThemes'] = $dj['carryChoicePlaqueThemes']; } if (isset($dj['carryChoicePlaqueTheme']) && is_array($dj['carryChoicePlaqueTheme'])) { $payload['carryChoicePlaqueTheme'] = $dj['carryChoicePlaqueTheme']; } if (array_key_exists('carryReadMs', $dj)) { $payload['carryReadMs'] = $dj['carryReadMs']; } if (array_key_exists('carryAnswerMs', $dj)) { $payload['carryAnswerMs'] = $dj['carryAnswerMs']; } if (array_key_exists('carrySessionLength', $dj)) { $payload['carrySessionLength'] = $dj['carrySessionLength']; } if (array_key_exists('carryWalkSpeedMultForMapId', $dj)) { $payload['carryWalkSpeedMultForMapId'] = $dj['carryWalkSpeedMultForMapId']; } if (array_key_exists('carryWalkSpeedMult', $dj)) { $payload['carryWalkSpeedMult'] = $dj['carryWalkSpeedMult']; } } } echo json_encode($payload, JSON_UNESCAPED_UNICODE); exit; } json_response([ 'ok' => false, 'error' => 'ไม่ต่อถึงเซิร์ฟเวอร์เกม (Node) — ตรวจว่า pm2/systemd รัน Game/server.js อยู่ที่พอร์ต ' . (getenv('GAME_NODE_PORT') ?: '3001') . ' · ' . $err, ], 502); } http_response_code($code > 0 ? $code : 500); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store'); if ($method === 'GET' && $code >= 200 && $code < 300) { echo overlay_quiz_settings_from_disk($out); } else { echo $out; } exit; } $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; if ($method === 'GET') { proxy_quiz_curl('GET', null); } if ($method === 'PUT') { $raw = file_get_contents('php://input'); if ($raw === false) { $raw = ''; } proxy_quiz_curl('PUT', $raw); } json_response(['ok' => false, 'error' => 'Method not allowed'], 405);