Files
ny-exam-platform/tools/mock-server.js
T
nxxy335 6cd17b9cb2 fix: 修复保存满分后清登录;配置双写;设置显示账号/密码;抽取比例改按人数加权
- 修复:仅当连接信息(base_url/api_prefix/username/password/verify_ssl)真变化时才清 token + 重连;
  保存满分/布局等不应触发登录卷王(bug:之前一律 unlink token.json,导致保存后所有用户都被踢)
- 修复:默认满分不再误写当前卷;改为显式 apply_pid + apply_score 才会改 full_scores
- 配置双写(DB+文件):避免 DB 不可用时静默回退到出厂默认 admin/123456;
  ny_save_config 同时写文件+DB;ny_load_config 在 DB 可用但无记录时从文件自动迁回
- 设置弹窗新增:登录账号可编辑、密码 type=password 不回显、留空表示不修改;
  保存后根据 ping 自动给出成功/失败反馈(错误密码不再静默)
- 抽取比例改为按组人数加权(默认)+ 各组均分 + 自定义比例 + 不分组,最大余额法守恒分配
- 抽出比例 UI:删除 5:5 复选框、改 #balanceMode 下拉 + 按组渲染比例输入框
- Db::lastError() 暴露 DB 错误用于诊断(config_get 返回 db_driver / db_connected / db_error)
- e2e:test-draw-alloc 18→29 测试用例(加权/自定义/守恒 fuzz);
  新增 test-config-save e2e 27 用例全过(连接信息变化检测 + 自动 ping + 密码不回显 + apply_pid)
- mock-server:账户校验从 'admin' 改为 'mock' 接受 e2e 账号
2026-09-09 13:06:47 +08:00

139 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 模拟卷王(SurveyKing)社区版 API,用于端到端测试
const http = require('http');
const crypto = require('crypto');
const fs = require('fs');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 1024,
publicKeyEncoding: { type: 'spki', format: 'der' },
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
});
const PUB_B64 = Buffer.from(publicKey).toString('base64');
const PASSWORD = 'surveyking';
let TOKEN = '';
const roster = JSON.parse(fs.readFileSync(require('path').join(__dirname, '..', 'data', 'roster.json'), 'utf8')).students;
// 构造答卷:前 30 人提交,其中 2 人姓名有空格/错别字
const answers = roster.slice(0, 30).map((s, i) => {
let name = s.name;
if (i === 3) name = ' ' + s.name + ' '; // 带空格
if (i === 7) name = s.name; // 正常
const start = Date.parse('2026-09-08T09:00:00+08:00') + i * 1000;
const end = start + (600 + i * 37) * 1000;
return {
id: 'a' + (i + 1),
projectId: 'p1',
answer: { q1: name, q2: '高一(1)班', q3: 'B' },
examScore: [100, 92, 88, 76, 61, 59, 95, 83, 72, 68][i % 10] + (i % 3),
tempSave: i === 5 ? 0 : 1,
metaInfo: { answerInfo: { startTime: start, endTime: end }, clientInfo: { browser: 'Chrome' } },
createAt: new Date(end).toISOString().replace('T', ' ').slice(0, 19),
createByName: '',
};
});
// 一个不在名单里的答卷
answers.push({
id: 'ax', projectId: 'p1', answer: { q1: '路过的同学', q2: '其他', q3: 'A' },
examScore: 55, tempSave: 1,
metaInfo: { answerInfo: { startTime: Date.parse('2026-09-08T09:30:00+08:00'), endTime: Date.parse('2026-09-08T09:41:20+08:00') } },
createAt: '2026-09-08 09:41:20', createByName: '',
});
const SURVEY = {
id: 'root', title: '高一数学月考', type: 'survey',
children: [
{ id: 'pg1', type: 'pagination', title: '基本信息', children: [
{ id: 'q1', type: 'text', title: '<p>姓名</p>' },
{ id: 'q2', type: 'text', title: '班级' },
]},
{ id: 'pg2', type: 'pagination', title: '试题', children: [
{ id: 'q3', type: 'radio', title: '1+1=?', children: [{ id: 'o1', title: 'A' }, { id: 'o2', title: 'B' }] },
]},
],
};
const PROJECTS = [
{ id: 'p1', name: '高一数学月考', mode: 'exam', status: 1, total: answers.length },
{ id: 'p2', name: '课后满意度调查', mode: 'survey', status: 1, total: 5 },
];
function send(res, code, obj, headers) {
res.writeHead(code, Object.assign({ 'Content-Type': 'application/json; charset=utf-8' }, headers || {}));
res.end(JSON.stringify(obj));
}
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const p = u.pathname;
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
// 鉴权:模拟社区版 v1.13 —— 只读 sk-token Cookie,不读 Authorization 头
if (p.startsWith('/api/') && !p.startsWith('/api/public/') && p !== '/api/system') {
const jar = {};
(req.headers.cookie || '').split(';').forEach(kv => {
const i = kv.indexOf('=');
if (i > 0) jar[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();
});
if (!jar['sk-token'] || jar['sk-token'] !== TOKEN) {
return send(res, 200, {
code: 401, data: null,
msg: 'Full authentication is required to access this resource'
});
}
}
if (p === '/api/system') {
return send(res, 200, { code: 0, data: { name: '卷王', version: '1.13.0', publicKey: PUB_B64 } });
}
if (p === '/api/public/login') {
let j = {};
try { j = JSON.parse(body); } catch (e) {}
let pwd = j.password;
let plain = pwd;
try {
plain = crypto.privateDecrypt(
{ key: privateKey, format: 'der', type: 'pkcs8', padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(pwd, 'base64')
).toString('utf8');
} catch (e) { plain = '<<解密失败>>'; }
if (j.username !== 'mock' || plain !== PASSWORD) {
return send(res, 200, { code: 500, data: null, msg: '账号或密码错误(收到明文=' + plain + ')' });
}
TOKEN = 'mock.jwt.token.' + Date.now();
// 真实 v1.13:Authorization 头和 Set-Cookie 都下发,但服务端鉴权只读 Cookie
return send(res, 200, { code: 0, data: { userId: '1' } }, {
'Set-Cookie': 'sk-token=' + TOKEN + '; Path=/; HttpOnly; SameSite=Lax',
Authorization: TOKEN,
});
}
if (p === '/api/project/list') {
return send(res, 200, { code: 0, data: { total: PROJECTS.length, list: PROJECTS } });
}
if (p === '/api/project') {
const id = u.searchParams.get('id');
const proj = PROJECTS.find(x => x.id === id);
if (!proj) return send(res, 200, { code: 404, data: null, msg: 'not found' });
return send(res, 200, { code: 0, data: Object.assign({}, proj, { survey: id === 'p1' ? SURVEY : null }) });
}
if (p === '/api/answer/list') {
const pid = u.searchParams.get('projectId');
const cur = parseInt(u.searchParams.get('current') || '1', 10);
const size = parseInt(u.searchParams.get('pageSize') || '200', 10);
const all = pid === 'p1' ? answers : [];
const slice = all.slice((cur - 1) * size, cur * size);
return send(res, 200, { code: 0, data: { total: all.length, list: slice } });
}
return send(res, 200, { code: 404, data: null, msg: 'no route ' + p });
});
});
server.listen(19911, '127.0.0.1', () => {
console.log('mock 卷王已启动 http://127.0.0.1:19911 密码=' + PASSWORD);
});