-
-
-
@@ -175,26 +154,12 @@ $rosterCount = Roster::count();
+
-
-
-
- 例如 http://192.168.1.10:1991,结尾不要带 /
-
-
-
-
- 社区版默认 /api(企业版可填 /admin-api)
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ 主界面三个面板(成绩明细 / 缺考名单 / 未匹配答卷)的排版方式
@@ -205,15 +170,25 @@ $rosterCount = Roster::count();
-
-
-
未选择试卷时,作为所有试卷的默认值
+ +
+
+
+
+ 未选中试卷时的默认值
+英文逗号分隔,用于自动识别"姓名"题目
+
+
+ 未选中试卷时,此处为空;选中某套试卷后保存即在设置中为该卷单独设定满分
+
+
@@ -223,8 +198,7 @@ $rosterCount = Roster::count();
-
-
+ 连接信息由安装向导配置,此处仅调整显示与统计
diff --git a/install.php b/install.php
new file mode 100644
index 0000000..0243b25
--- /dev/null
+++ b/install.php
@@ -0,0 +1,203 @@
+ $backend,
+ 'host' => trim((string)($_POST['db_host'] ?? '127.0.0.1')),
+ 'port' => (int)($_POST['db_port'] ?? ($backend === 'pgsql' ? 5432 : 3306)),
+ 'dbname' => trim((string)($_POST['db_name'] ?? '')),
+ 'user' => trim((string)($_POST['db_user'] ?? '')),
+ 'password' => (string)($_POST['db_pass'] ?? ''),
+ ];
+ if ($dbp['dbname'] === '') {
+ $err = '请填写数据库名。';
+ } else {
+ try {
+ Db::saveParams($dbp);
+ // 重新建立连接并建表
+ $pdo = Db::connect($dbp);
+ Db::migrate();
+ } catch (Throwable $e) {
+ $err = '数据库连接/初始化失败:' . $e->getMessage();
+ }
+ }
+ }
+
+ if ($err === '') {
+ // 2) 写入系统配置(DB 后端写 settings['config'],文件后端写 config.json)
+ $cfg = ny_default_config();
+ $cfg['base_url'] = $base;
+ $cfg['api_prefix'] = $prefix;
+ $cfg['username'] = $user;
+ $cfg['password'] = $pwd;
+ $cfg['verify_ssl'] = $verify;
+ $cfg['full_score'] = $full;
+ $saved = ny_save_config($cfg);
+ if ($saved) {
+ // 清理 token 缓存(连接信息变化)
+ @unlink(NY_DATA . '/cache/token.json');
+ $ok = true;
+ } else {
+ if ($backend === 'file') {
+ $err = '无法写入文件配置,请检查 data/ 目录权限。';
+ } else {
+ $err = '配置写入失败(已建表但写入配置异常)。';
+ }
+ }
+ }
+ }
+}
+
+$cfg = ny_load_config();
+?>
+
+
+
+
+
+首次安装 · = htmlspecialchars(NY_APP_NAME) ?>
+
+
+
+
+ */
public static function all(): array
{
+ if (self::useDb()) {
+ return Db::rosterAll();
+ }
$d = ny_read_json(self::file(), null);
if (!is_array($d) || !isset($d['students']) || !is_array($d['students'])) {
return [];
@@ -39,8 +48,8 @@ class Roster
{
$d = ny_read_json(self::file(), null);
return [
- 'count' => count(self::all()),
- 'updated_at' => is_array($d) ? (string)($d['updated_at'] ?? '') : '',
+ 'count' => self::count(),
+ 'updated_at' => self::useDb() ? date('Y-m-d H:i:s') : (is_array($d) ? (string)($d['updated_at'] ?? '') : ''),
];
}
@@ -60,6 +69,10 @@ class Roster
'grp' => trim((string)($s['grp'] ?? '')),
];
}
+ if (self::useDb()) {
+ Db::rosterReplace($clean);
+ return true;
+ }
return ny_write_json(self::file(), [
'students' => $clean,
'updated_at' => date('Y-m-d H:i:s'),
@@ -68,11 +81,18 @@ class Roster
public static function clear(): bool
{
+ if (self::useDb()) {
+ Db::rosterReplace([]);
+ return true;
+ }
return ny_write_json(self::file(), ['students' => [], 'updated_at' => date('Y-m-d H:i:s')]);
}
public static function count(): int
{
+ if (self::useDb()) {
+ return Db::rosterCount();
+ }
return count(self::all());
}
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..b4e204e
--- /dev/null
+++ b/login.php
@@ -0,0 +1,94 @@
+getToken(true);
+ // 登录成功:把账号写回配置(如需),并建立会话
+ $cfg['username'] = $user;
+ ny_save_config($cfg);
+ ny_login();
+ $ok = true;
+ } catch (Throwable $e) {
+ $err = '登录失败:' . $e->getMessage();
+ }
+ }
+}
+?>
+
+
+
+
+
+登录 · = htmlspecialchars(NY_APP_NAME) ?>
+
+
+
+
+
+
+
+
diff --git a/lib/Db.php b/lib/Db.php
new file mode 100644
index 0000000..3296a8b
--- /dev/null
+++ b/lib/Db.php
@@ -0,0 +1,278 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ];
+
+ if ($driver === 'sqlite') {
+ $dsn = 'sqlite:' . ($p['path'] ?? ($db ?: (NY_DATA . '/app.sqlite')));
+ } elseif ($driver === 'pgsql') {
+ $dsn = 'pgsql:host=' . $host;
+ if ($port > 0) {
+ $dsn .= ';port=' . $port;
+ }
+ $dsn .= ';dbname=' . $db;
+ $dsn .= ';user=' . $user;
+ $dsn .= ';password=' . $pass;
+ } else { // mysql / mariadb
+ $dsn = 'mysql:host=' . $host;
+ if ($port > 0) {
+ $dsn .= ';port=' . $port;
+ }
+ $dsn .= ';dbname=' . $db . ';charset=utf8mb4';
+ $options[PDO::ATTR_PERSISTENT] = false;
+ }
+
+ $pdo = new PDO($dsn, $user, $pass, $options);
+ if ($driver === 'mysql') {
+ $pdo->exec("SET NAMES utf8mb4");
+ }
+ return $pdo;
+ }
+
+ /** 表名前缀 */
+ public static function prefix(): string
+ {
+ return 'ny_';
+ }
+
+ /**
+ * 建表(幂等)。返回已执行语句数量。
+ * 用通用 SQL,三方言均可执行(表名/列名小写、规范命名)。
+ */
+ public static function migrate(): int
+ {
+ $pdo = self::pdo();
+ if ($pdo === null) {
+ throw new RuntimeException('数据库未连接,无法初始化表结构');
+ }
+ $driver = self::$driver;
+ $n = 0;
+
+ // settings(key,value) —— 系统设置键值表
+ if ($driver === 'sqlite') {
+ $sql = 'CREATE TABLE IF NOT EXISTS ny_settings (' .
+ 'k VARCHAR(128) PRIMARY KEY, ' .
+ 'v TEXT)';
+ } elseif ($driver === 'pgsql') {
+ $sql = 'CREATE TABLE IF NOT EXISTS ny_settings (' .
+ 'k VARCHAR(128) PRIMARY KEY, ' .
+ 'v TEXT)';
+ } else {
+ $sql = 'CREATE TABLE IF NOT EXISTS ny_settings (' .
+ '`k` VARCHAR(128) NOT NULL PRIMARY KEY, ' .
+ '`v` TEXT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
+ }
+ $pdo->exec($sql); $n++;
+
+ // roster —— 名单表(学号/姓名/班级/组别)
+ if ($driver === 'sqlite') {
+ $sql = 'CREATE TABLE IF NOT EXISTS ny_roster (' .
+ 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' .
+ 'sid VARCHAR(64), name VARCHAR(128), cls VARCHAR(64), grp VARCHAR(32))';
+ } elseif ($driver === 'pgsql') {
+ $sql = 'CREATE TABLE IF NOT EXISTS ny_roster (' .
+ 'id SERIAL PRIMARY KEY, ' .
+ 'sid VARCHAR(64), name VARCHAR(128), cls VARCHAR(64), grp VARCHAR(32))';
+ } else {
+ $sql = 'CREATE TABLE IF NOT EXISTS ny_roster (' .
+ 'id INT AUTO_INCREMENT PRIMARY KEY, ' .
+ 'sid VARCHAR(64), name VARCHAR(128), cls VARCHAR(64), grp VARCHAR(32)) ' .
+ 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
+ }
+ $pdo->exec($sql); $n++;
+
+ return $n;
+ }
+
+ /**
+ * 把所有 settings 读成数组
+ */
+ public static function settingsAll(): array
+ {
+ $pdo = self::pdo();
+ if ($pdo === null) {
+ return [];
+ }
+ $out = [];
+ $stmt = $pdo->query('SELECT k, v FROM ny_settings');
+ foreach ($stmt->fetchAll() as $row) {
+ $out[$row['k']] = $row['v'];
+ }
+ return $out;
+ }
+
+ public static function settingsSet(string $k, string $v): void
+ {
+ $pdo = self::pdo();
+ if ($pdo === null) {
+ return;
+ }
+ $drv = self::$driver;
+ if ($drv === 'pgsql') {
+ $sql = 'INSERT INTO ny_settings(k, v) VALUES(?, ?) ' .
+ 'ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v';
+ } elseif ($drv === 'sqlite') {
+ $sql = 'INSERT OR REPLACE INTO ny_settings(k, v) VALUES(?, ?)';
+ } else {
+ $sql = 'INSERT INTO ny_settings(k, v) VALUES(?, ?) ' .
+ 'ON DUPLICATE KEY UPDATE v = VALUES(v)';
+ }
+ $pdo->prepare($sql)->execute([$k, $v]);
+ }
+
+ /** 名单:全量读 */
+ public static function rosterAll(): array
+ {
+ $pdo = self::pdo();
+ if ($pdo === null) {
+ return [];
+ }
+ $stmt = $pdo->query('SELECT sid, name, cls, grp FROM ny_roster ORDER BY id');
+ $rows = [];
+ foreach ($stmt->fetchAll() as $r) {
+ $rows[] = [
+ 'sid' => (string)$r['sid'],
+ 'name' => (string)$r['name'],
+ 'cls' => (string)$r['cls'],
+ 'grp' => (string)$r['grp'],
+ ];
+ }
+ return $rows;
+ }
+
+ /** 名单:清空后全量写 */
+ public static function rosterReplace(array $students): void
+ {
+ $pdo = self::pdo();
+ if ($pdo === null) {
+ return;
+ }
+ $pdo->beginTransaction();
+ try {
+ $pdo->exec('DELETE FROM ny_roster');
+ $st = $pdo->prepare('INSERT INTO ny_roster(sid, name, cls, grp) VALUES(?, ?, ?, ?)');
+ foreach ($students as $s) {
+ $st->execute([
+ (string)($s['sid'] ?? ''),
+ (string)($s['name'] ?? ''),
+ (string)($s['cls'] ?? ''),
+ (string)($s['grp'] ?? ''),
+ ]);
+ }
+ $pdo->commit();
+ } catch (Throwable $e) {
+ $pdo->rollBack();
+ throw $e;
+ }
+ }
+
+ /** 名单:行数 */
+ public static function rosterCount(): int
+ {
+ $pdo = self::pdo();
+ if ($pdo === null) {
+ return 0;
+ }
+ return (int)$pdo->query('SELECT COUNT(*) FROM ny_roster')->fetchColumn();
+ }
+}
diff --git a/lib/Roster.php b/lib/Roster.php
index 98fe911..2fef0d6 100644
--- a/lib/Roster.php
+++ b/lib/Roster.php
@@ -13,9 +13,18 @@ class Roster
return NY_DATA . '/roster.json';
}
+ /** 数据库是否可作为名单后端 */
+ private static function useDb(): bool
+ {
+ return class_exists('Db') && Db::pdo() !== null;
+ }
+
/** @return array
+
+
+ NY
+
+
+
+ = htmlspecialchars(NY_APP_NAME) ?>
+ 首次安装 · 配置数据存储与系统连接
+
+
+
+
+
✅ 安装完成!即将进入登录页…
+ + 若连接信息有误或未生效,请重新访问本页配置。
+ + + + +
+
+
+
+
+
+
+ NY
+
+
+ = htmlspecialchars(NY_APP_NAME) ?>
+ 实时数据统计
+
+ 系统连接信息已配置(= htmlspecialchars(parse_url($cfg['base_url'], PHP_URL_HOST)) ?>/= htmlspecialchars(trim($cfg['api_prefix'], '/')) ?>)。请输入账号密码完成登录。
+ + += htmlspecialchars($err) ?>
+
+
+
+ 如系统连接信息已变更,请先完成重新安装/配置
+
+
+
+
+
+
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..6da246d
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,10 @@
+
+
+ NY
+
+
+ 登录成功正在进入数据平台…
+