false, 'message' => $message], $code); } function readJsonFile(string $path): array { if (!is_file($path)) { jsonError('数据文件不存在: ' . basename($path), 500); } $data = json_decode(file_get_contents($path) ?: '[]', true); return is_array($data) ? $data : []; } function getRequestBody(): array { $raw = file_get_contents('php://input'); $data = json_decode($raw ?: '{}', true); return is_array($data) ? $data : []; } function requireAdmin(): void { if (empty($_SESSION['admin'])) { jsonError('未登录或会话已过期', 401); } } function readSubmissions(): array { if (!is_file(SUBMISSIONS_FILE)) { return []; } $data = json_decode(file_get_contents(SUBMISSIONS_FILE) ?: '[]', true); return is_array($data) ? $data : []; } function getDataWriteError(): ?string { if (!is_dir(DATA_DIR)) { if (!@mkdir(DATA_DIR, 0755, true) && !is_dir(DATA_DIR)) { return 'data 目录无法创建。请在宝塔:文件 → data → 权限设为 755,属主 www'; } } if (!is_writable(DATA_DIR)) { return 'data 目录不可写。请在宝塔:文件 → data → 权限 755,属主 www,勾选「应用到子目录」'; } if (is_file(SUBMISSIONS_FILE) && !is_writable(SUBMISSIONS_FILE)) { return 'submissions.json 不可写。请在宝塔:data/submissions.json → 权限 664,属主 www'; } return null; } function saveSubmissions(array $submissions): bool { $writeError = getDataWriteError(); if ($writeError !== null) { error_log('[data] ' . $writeError); return false; } $json = json_encode($submissions, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); $written = @file_put_contents(SUBMISSIONS_FILE, $json, LOCK_EX); if ($written === false && !is_file(SUBMISSIONS_FILE)) { $written = @file_put_contents(SUBMISSIONS_FILE, $json, LOCK_EX); } return $written !== false; } function getSaveSubmissionsError(): string { $writeError = getDataWriteError(); if ($writeError !== null) { return $writeError; } return '保存失败,请稍后重试'; } function validatePhone(string $phone): bool { return (bool) preg_match('/^1[3-9]\d{9}$/', $phone); } function getAssetVersion(): string { static $version = null; if ($version !== null) { return $version; } try { $config = loadAppConfig(); if (!empty($config['assetVersion'])) { $version = (string) $config['assetVersion']; return $version; } } catch (Throwable $e) { // 页面未就绪时回退到文件时间戳 } $max = 0; foreach (['css', 'js', 'assets', 'assets/avatars'] as $dirName) { $dir = ROOT_DIR . '/' . $dirName; if (!is_dir($dir)) { continue; } $files = glob($dir . '/*'); if ($files === false) { continue; } foreach ($files as $file) { if (is_file($file)) { $max = max($max, (int) filemtime($file)); } } } $version = $max > 0 ? (string) $max : date('YmdHis'); return $version; } function asset(string $path): string { $path = ltrim($path, '/'); $sep = str_contains($path, '?') ? '&' : '?'; return $path . $sep . 'v=' . rawurlencode(getAssetVersion()); } function sendHtmlNoCacheHeaders(): void { header('Cache-Control: no-cache, no-store, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); }