= 8.0 */ declare(strict_types=1); const NY_APP_NAME = 'NY问卷考试系统数据平台'; const NY_VERSION = '1.0.0'; define('NY_ROOT', str_replace('\\', '/', __DIR__)); define('NY_DATA', NY_ROOT . '/data'); // ---------------- 环境预检 ---------------- if (PHP_VERSION_ID < 80000) { http_response_code(500); header('Content-Type: text/html; charset=utf-8'); echo '

环境不满足

本平台需要 PHP 8.0 及以上,当前版本:' . PHP_VERSION . '

'; exit; } $missing = []; foreach (['curl' => 'curl', 'json' => 'json', 'mbstring' => 'mbstring'] as $fn => $ext) { if (!extension_loaded($ext)) { $missing[] = $ext; } } if ($missing !== []) { http_response_code(500); header('Content-Type: text/html; charset=utf-8'); echo '

缺少 PHP 扩展

请在 php.ini 中启用:' . implode('、', $missing) . '

' . '

Windows:去掉 extension=' . $missing[0] . ' 前面的分号并重启服务。' . '(zip 扩展可选,未启用时会用内置解析器读取 xlsx)

'; exit; } date_default_timezone_set('Asia/Shanghai'); mb_internal_encoding('UTF-8'); // ---------------- 会话 ---------------- function ny_session_start(): void { if (session_status() === PHP_SESSION_ACTIVE) { return; } $secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; session_name('NYSESS'); session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'httponly' => true, 'samesite' => 'Lax', 'secure' => $secure, ]); @session_start(); } /** 当前是否已登录(本系统会话) */ function ny_is_auth(): bool { ny_session_start(); return !empty($_SESSION['ny_auth']); } /** 标记登录 */ function ny_login(): void { ny_session_start(); $_SESSION['ny_auth'] = true; } /** 登出 */ function ny_logout(): void { ny_session_start(); $_SESSION = []; if (ini_get('session.use_cookies')) { $p = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']); } session_destroy(); } /** 判断是否已安装(有过连接配置且能流转):文件 config 或 DB settings 存在 base_url 且账号非空 */ function ny_is_installed(): bool { $cfg = ny_load_config(); if (trim((string)($cfg['base_url'] ?? '')) === '') { return false; } // 有无可用的后端(文件 config 存在 ,或 DB 连得上) if (class_exists('Db') && Db::isConnected()) { return true; } return is_file(ny_config_file()); } // ---------------- 目录初始化 ---------------- foreach (['', '/cache', '/uploads'] as $sub) { $dir = NY_DATA . $sub; if (!is_dir($dir)) { @mkdir($dir, 0777, true); } } // 保护数据目录(Apache) if (!file_exists(NY_DATA . '/.htaccess')) { @file_put_contents(NY_DATA . '/.htaccess', "Deny from all\n"); } // ---------------- 默认配置 ---------------- function ny_default_config(): array { return [ 'base_url' => 'https://exam.nxxy335.top', 'api_prefix' => '/api', 'username' => 'admin', 'password' => '123456', 'verify_ssl' => true, 'timeout' => 20, 'refresh_interval' => 15, 'page_size' => 200, 'include_temp_save' => false, 'dedupe' => true, 'name_keywords' => '姓名,名字,学生姓名,考生姓名,您的姓名,姓名:,姓名:', 'full_score' => 100, 'full_scores' => [], 'layout' => 'default', ]; } function ny_config_file(): string { return NY_DATA . '/config.json'; } function ny_load_config(): array { // 数据库已初始化且能连上 → 从 DB 读,否则回退文件 $dbCfg = ny_db_config(); if ($dbCfg !== null) { $cfg = ny_default_config(); foreach ($cfg as $k => $v) { if (array_key_exists($k, $dbCfg)) { $cfg[$k] = is_bool($v) ? (bool)$dbCfg[$k] : $dbCfg[$k]; } } $cfg['base_url'] = rtrim(trim((string)$cfg['base_url']), '/'); $cfg['api_prefix'] = '/' . trim((string)$cfg['api_prefix'], '/'); return $cfg; } $cfg = ny_default_config(); $saved = ny_read_json(ny_config_file(), null); if (is_array($saved)) { foreach ($cfg as $k => $v) { if (array_key_exists($k, $saved)) { $cfg[$k] = is_bool($v) ? (bool)$saved[$k] : $saved[$k]; } } } $cfg['base_url'] = rtrim(trim((string)$cfg['base_url']), '/'); $cfg['api_prefix'] = '/' . trim((string)$cfg['api_prefix'], '/'); return $cfg; } function ny_save_config(array $cfg): bool { // 数据库已连接则写入 DB settings(键 config = JSON),否则回退文件 if (class_exists('Db')) { $db = Db::pdo(); if ($db !== null) { try { Db::settingsSet('config', json_encode($cfg, JSON_UNESCAPED_UNICODE)); return true; } catch (Throwable $e) { // 落库失败则回退文件 } } } return ny_write_json(ny_config_file(), $cfg); } /** 数据库初始化后从 DB 读取配置(无则返回 null) */ function ny_db_config(): ?array { if (!class_exists('Db')) { return null; } $db = Db::pdo(); if ($db === null) { return null; } try { $raw = (string)(Db::settingsAll()['config'] ?? ''); } catch (Throwable $e) { return null; } $data = json_decode($raw, true); return is_array($data) ? $data : null; } /** 安全的字符串化(避免数组/对象转字符串报错) */ function ny_str(mixed $v): string { if ($v === null || is_array($v) || is_object($v)) { return ''; } return trim((string)$v); } // ---------------- JSON 文件读写 ---------------- function ny_read_json(string $file, mixed $default = null): mixed { if (!is_file($file)) { return $default; } $raw = @file_get_contents($file); if ($raw === false || trim($raw) === '') { return $default; } $data = json_decode($raw, true); return $data === null ? $default : $data; } function ny_write_json(string $file, mixed $data): bool { $tmp = $file . '.tmp'; $ok = @file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX); if ($ok === false) { return false; } return @rename($tmp, $file) || @copy($tmp, $file); } // ---------------- 输出 ---------------- function ny_json(mixed $data, int $http = 200): void { http_response_code($http); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE); } function ny_ok(mixed $data = null): void { ny_json(['ok' => true, 'data' => $data]); exit; } function ny_fail(string $msg, mixed $extra = null): void { ny_json(['ok' => false, 'msg' => $msg, 'extra' => $extra]); exit; } // ---------------- 工具函数 ---------------- /** 规范化姓名:去空白、去全角空格、统一大小写 */ function ny_norm_name(string $s): string { $s = str_replace(["\xC2\xA0", ' '], '', $s); $s = preg_replace('/\s+/u', '', $s) ?? ''; return mb_strtolower(trim($s)); } /** 把任意结构的答卷值压平成字符串 */ function ny_flatten_value(mixed $v): string { if ($v === null) { return ''; } if (is_bool($v)) { return $v ? '1' : ''; } if (is_int($v) || is_float($v)) { return trim((string)$v); } if (is_string($v)) { return trim($v); } if (is_array($v)) { foreach (['value', 'text', 'label', 'name', 'content', 'title'] as $k) { if (isset($v[$k]) && (is_string($v[$k]) || is_numeric($v[$k]))) { return trim((string)$v[$k]); } } $parts = []; foreach ($v as $item) { $s = ny_flatten_value($item); if ($s !== '') { $parts[] = $s; } } return implode(',', $parts); } return trim((string)$v); } /** 时间字段 -> 时间戳(秒) */ function ny_to_timestamp(mixed $v): int { if ($v === null || $v === '') { return 0; } if (is_numeric($v)) { $n = (int)$v; if ($n > 1000000000000) { // 毫秒 return intdiv($n, 1000); } return $n; } $t = strtotime((string)$v); return $t === false ? 0 : $t; } /** 秒 -> 12分34秒 */ function ny_format_duration(?int $sec): string { if ($sec === null || $sec < 0) { return '-'; } if ($sec < 60) { return $sec . '秒'; } $m = intdiv($sec, 60); $s = $sec % 60; if ($m < 60) { return $m . '分' . str_pad((string)$s, 2, '0', STR_PAD_LEFT) . '秒'; } $h = intdiv($m, 60); return $h . '小时' . str_pad((string)($m % 60), 2, '0', STR_PAD_LEFT) . '分' . str_pad((string)$s, 2, '0', STR_PAD_LEFT) . '秒'; } // ---------------- 类自动加载 ---------------- spl_autoload_register(function (string $class): void { $map = [ 'XlsxReader' => NY_ROOT . '/lib/XlsxReader.php', 'SurveyKingClient' => NY_ROOT . '/lib/SurveyKingClient.php', 'Roster' => NY_ROOT . '/lib/Roster.php', 'DataService' => NY_ROOT . '/lib/DataService.php', 'Db' => NY_ROOT . '/lib/Db.php', ]; if (isset($map[$class]) && is_file($map[$class])) { require $map[$class]; } });