= 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'); // ---------------- 目录初始化 ---------------- 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' => [], ]; } function ny_config_file(): string { return NY_DATA . '/config.json'; } function ny_load_config(): array { $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 { return ny_write_json(ny_config_file(), $cfg); } /** 安全的字符串化(避免数组/对象转字符串报错) */ 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', ]; if (isset($map[$class]) && is_file($map[$class])) { require $map[$class]; } });