Files
ny-exam-platform/tools/mock-server.js
T
nxxy335 94ac381579 完成批次 L-R:限流移除/白名单驱动/打印增强/全屏/表格对齐/主题变量/滚动闪烁修复
- 移除登录失败限流;fix-config.php 修正 e2e 串味的 127.0.0.1 连接

- 白名单驱动缺/待考名单(DataService + SurveyKingClient::getWhitelist,复用已取 survey)

- 打印成绩明细增强(范围/合格列默认关/去页脚);看板全屏占满;表格 vertical-align:middle;隐藏登陆账号;导入默认追加

- 主题变量收口(:root 新增 brand/primary-2/ok-2 等,替换硬编码渐变);删死规则

- 表头/数据列对齐修复(index 未匹配表、dashboard 成员榜、draw 结果/历史/排除表);看板占位卡片配色一致

- 修复滚动闪烁:row-drop-in 动画经 _dropAnim 门控,仅强制重绘时播放

- 测试:新增 test-whitelist.js;run-all.sh 全绿 132
2026-09-13 10:25:48 +08:00

143 lines
5.7 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',
surveySetting: {
// 答题白名单:待考名单来源(键为题目 ID,值为允许的答案列表;这里用学号作标识)
whiteLists: { q1: ['90001', '90002', '90003', '90004', '90005'] },
},
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);
});