Files
ny-exam-platform/tools/mock-server.js
T
nxxy335 eb5d06d392 修复考试白名单驱动缺/待考名单:改用卷王项目参与者(Project Partners)真实模型
旧代码误读 surveySetting.whiteLists(实为题目选项白名单),导致线上从未触发。现改为读取 /api/project/partner/list 的 RESPONDENT_SYS_USER/IMP_USER 参与者作为应考名单,并保留 whiteLists 作为极老版本兜底。

dashboard.php 移除未实现的提交率卡片;全_score 默认 100 已在 data 层修正。e2e 测试 mock 与断言同步改为真实模型,全量 132 测试通过。
2026-09-13 11:02:34 +08:00

158 lines
6.3 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: {},
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 },
];
// p1 的「受访者」参与者 = 应考名单(真实卷王模型:t_project_partner type=RESPONDENT_IMP_USER=3)
// status: 0未访问 / 1已访问未答 / 2已答题
const PARTNERS = {
p1: [
{ id: 'pp1', type: 3, userName: '90001', userId: '90001', status: 0 },
{ id: 'pp2', type: 3, userName: '90002', userId: '90002', status: 0 },
{ id: 'pp3', type: 3, userName: '90003', userId: '90003', status: 0 },
{ id: 'pp4', type: 3, userName: '90004', userId: '90004', status: 0 },
{ id: 'pp5', type: 3, userName: '90005', userId: '90005', status: 0 },
],
p2: [],
};
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 } });
}
if (p === '/api/project/partner/list') {
const pid = u.searchParams.get('projectId');
const list = PARTNERS[pid] || [];
return send(res, 200, { code: 0, data: { total: list.length, list } });
}
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);
});