feat: 成绩明细打印 + SQLite + 每卷独立配置(满分/合格率/免考) + 导航精简
- 成绩明细打印:默认成绩降序,可选按班级/组别分组对比(段首小计) - 修复看板退出全屏无反应(exitFullscreen 挂在 document 而非 documentElement) - 导航栏精简:三页统一为单一主标题,移除 Logo 与副标题 - 优化三端布局:重排断点、竖排隐藏 spacer、触摸设备热区 - 全面启用 SQLite(data/ny.db):配置/名单/每卷配置入库,无 pdo_sqlite 自动降级文件 - 新增独立合格率配置(全局 + 每卷),合格率不再写死 60% - 新增免考配置,仅对当前试卷生效,试卷间隔离 - 试卷独立满分从设置移出,改到主页面「试卷设置」 - 测试:新增 4 条每卷配置隔离断言,全套 113 项通过
This commit is contained in:
+55
-27
@@ -20,6 +20,17 @@ const hasToken = fs.existsSync(tokenFile);
|
||||
const tokenSnap = hasToken ? fs.readFileSync(tokenFile, 'utf8') : null;
|
||||
if (hasToken) fs.renameSync(tokenFile, tokenBak);
|
||||
|
||||
// SQLite 优先:若不先移走 data/ny.db,里面的旧配置会盖掉本测试写入的 config.json,
|
||||
// 导致登录用的是上一次遗留的账号密码(表现为"登录成功但 auth_check=false")。
|
||||
// 同时清掉登录限流记录,避免上一轮失败把本轮锁住。
|
||||
const dbFile = path.join(ROOT, 'data', 'ny.db');
|
||||
const dbBak = dbFile + '.bak';
|
||||
const hasDb = fs.existsSync(dbFile);
|
||||
if (hasDb) { try { fs.renameSync(dbFile, dbBak); } catch (e) {} }
|
||||
|
||||
const throttleFile = path.join(ROOT, 'data', 'login_fail.json');
|
||||
if (fs.existsSync(throttleFile)) { try { fs.unlinkSync(throttleFile); } catch (e) {} }
|
||||
|
||||
fs.writeFileSync(cfgFile, JSON.stringify({
|
||||
base_url: 'http://127.0.0.1:19911',
|
||||
api_prefix: '/api',
|
||||
@@ -51,6 +62,14 @@ function cleanup() {
|
||||
try {
|
||||
if (hasToken && fs.existsSync(tokenBak)) fs.renameSync(tokenBak, tokenFile);
|
||||
} catch(e) { console.error('cleanup token failed:', e.message); }
|
||||
// 恢复 SQLite(本轮测试新建的 ny.db 丢弃,换回原有的)
|
||||
try {
|
||||
if (fs.existsSync(dbFile)) fs.unlinkSync(dbFile);
|
||||
if (hasDb && fs.existsSync(dbBak)) fs.renameSync(dbBak, dbFile);
|
||||
} catch(e) { console.error('cleanup db failed:', e.message); }
|
||||
try {
|
||||
if (fs.existsSync(throttleFile)) fs.unlinkSync(throttleFile);
|
||||
} catch(e) {}
|
||||
}
|
||||
process.on('exit', cleanup);
|
||||
process.on('SIGINT', () => { cleanup(); process.exit(1); });
|
||||
@@ -235,35 +254,44 @@ async function login() {
|
||||
const after = fs.existsSync(tokenFile) ? fs.readFileSync(tokenFile, 'utf8') : '';
|
||||
chk('token 保持未变', before === after, 'changed=' + (before !== after));
|
||||
|
||||
console.log('\n--- 4) 各试卷独立满分:仅当 apply_pid 显式指定时写 ---');
|
||||
// 不传 apply_pid,也不传 full_scores → full_scores 应保留原值(保持空映射)
|
||||
const cs2 = await req('POST', '/api.php?action=config_save', {
|
||||
full_score: 90,
|
||||
console.log('\n--- 4) 试卷独立配置(满分/合格率/免考)已从设置移出,改走 project_config ---');
|
||||
// 每卷满分已不在 config_save 里;改为 project_config_save(试卷间隔离)
|
||||
const pc1 = await req('POST', '/api.php?action=project_config_save&projectId=p1', {
|
||||
full_score: 150, pass_rate: 70, exempt: 'S1,S2',
|
||||
});
|
||||
const _cs2fs = cs2.body && cs2.body.data && cs2.body.data.config
|
||||
? cs2.body.data.config.full_scores : undefined;
|
||||
chk('未传 apply_pid → full_scores 保持空映射(不被覆盖为非空)',
|
||||
_cs2fs === undefined || Object.keys(_cs2fs).length === 0,
|
||||
JSON.stringify(_cs2fs));
|
||||
// 显式 apply_pid='p1', apply_score=85 → 写入
|
||||
const cs3 = await req('POST', '/api.php?action=config_save', {
|
||||
full_score: 90,
|
||||
apply_pid: 'p1',
|
||||
apply_score: 85,
|
||||
chk('project_config_save 200', pc1.status === 200, 'status=' + pc1.status + ' ' + JSON.stringify(pc1.body));
|
||||
chk('生效满分=150', pc1.body && pc1.body.data && pc1.body.data.effective
|
||||
&& pc1.body.data.effective.full_score === 150,
|
||||
JSON.stringify(pc1.body && pc1.body.data && pc1.body.data.effective));
|
||||
chk('生效合格率=70', pc1.body && pc1.body.data && pc1.body.data.effective
|
||||
&& Number(pc1.body.data.effective.pass_rate) === 70,
|
||||
JSON.stringify(pc1.body && pc1.body.data && pc1.body.data.effective));
|
||||
|
||||
// 隔离性:p2 未配置 → 跟随全局(注:上面的 config_save 已把全局满分改成 95,
|
||||
// 所以这里不能写死 100,必须与响应里的 global 比对)
|
||||
const pc2 = await req('GET', '/api.php?action=project_config&projectId=p2');
|
||||
const gFs = pc2.body && pc2.body.data && pc2.body.data.global ? pc2.body.data.global.full_score : null;
|
||||
const gPr = pc2.body && pc2.body.data && pc2.body.data.global ? pc2.body.data.global.pass_rate : null;
|
||||
chk('p2 隔离:满分跟随全局(=' + gFs + ')', pc2.body && pc2.body.data && pc2.body.data.effective
|
||||
&& pc2.body.data.effective.full_score === gFs,
|
||||
JSON.stringify(pc2.body && pc2.body.data && pc2.body.data.effective) + ' global=' + gFs);
|
||||
chk('p2 隔离:合格率跟随全局(=' + gPr + ')', pc2.body && pc2.body.data && pc2.body.data.effective
|
||||
&& Number(pc2.body.data.effective.pass_rate) === Number(gPr),
|
||||
JSON.stringify(pc2.body && pc2.body.data && pc2.body.data.effective) + ' global=' + gPr);
|
||||
|
||||
// 免考名单往返
|
||||
const pc3 = await req('GET', '/api.php?action=project_config&projectId=p1');
|
||||
const ex = pc3.body && pc3.body.data && pc3.body.data.cfg && pc3.body.data.cfg.exempt;
|
||||
chk('免考名单往返 = [S1,S2]', Array.isArray(ex) && ex.length === 2 && ex[0] === 'S1' && ex[1] === 'S2',
|
||||
JSON.stringify(ex));
|
||||
|
||||
// 清除本卷设置 → 回到全局
|
||||
const pc4 = await req('POST', '/api.php?action=project_config_save&projectId=p1', {
|
||||
full_score: '', pass_rate: '', exempt: '',
|
||||
});
|
||||
chk('apply_pid=85 → full_scores.p1=85', cs3.body && cs3.body.data && cs3.body.data.config
|
||||
&& cs3.body.data.config.full_scores && cs3.body.data.config.full_scores.p1 === 85,
|
||||
JSON.stringify(cs3.body.data && cs3.body.data.config && cs3.body.data.config.full_scores));
|
||||
// apply_pid='p1', apply_score=0 → 清除
|
||||
const cs4 = await req('POST', '/api.php?action=config_save', {
|
||||
full_score: 90,
|
||||
apply_pid: 'p1',
|
||||
apply_score: 0,
|
||||
});
|
||||
chk('apply_pid=0 → 清除 p1',
|
||||
cs4.body && cs4.body.data && cs4.body.data.config
|
||||
&& (!cs4.body.data.config.full_scores || cs4.body.data.config.full_scores.p1 === undefined),
|
||||
JSON.stringify(cs4.body.data && cs4.body.data.config && cs4.body.data.config.full_scores));
|
||||
chk('清除后 p1 回到全局满分(=' + gFs + ')', pc4.body && pc4.body.data && pc4.body.data.effective
|
||||
&& pc4.body.data.effective.full_score === gFs,
|
||||
JSON.stringify(pc4.body && pc4.body.data && pc4.body.data.effective) + ' global=' + gFs);
|
||||
|
||||
console.log('\n--- 5) 修改连接信息:应清 token + 重新登录(ping)---');
|
||||
const tokBefore = fs.existsSync(tokenFile) ? fs.readFileSync(tokenFile, 'utf8') : '';
|
||||
|
||||
Reference in New Issue
Block a user