项目迁移

This commit is contained in:
2026-07-22 18:22:52 +08:00 Unverified
commit ecb3dc63ed
68 changed files with 21894 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
import assert from 'node:assert/strict';
import { admissionCutoffRows, admissionPlanProgress, admissionRoundPublications, assignAdmissionNoticeNumbers, buildVolunteerPlacements, candidateAdmissionScore, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus, supplementarySchoolIds } from '../src/services/volunteer-admission.mjs';
import { candidateEligibleForCategory, specialtyLabel } from '../src/data/specialty-types.mjs';
import { systemNotificationItems } from '../src/services/system-notifications.mjs';
const now = '2026-07-21T08:00:00.000Z';
let sequence = 0;
const db = {
users: [
{ id: 'u-high', role: 'candidate', active: true, candidateNumber: '20260001', displayName: '高分考生' },
{ id: 'u-low', role: 'candidate', active: true, candidateNumber: '20260002', displayName: '次高考生' },
{ id: 'u-sport', role: 'candidate', active: true, candidateNumber: '20260003', displayName: '特长考生' }
],
candidateProfiles: [
{ userId: 'u-high', name: '高分考生', schoolId: 'source-a', profileCompleted: true, idNumber: '320101200901011234', phone: '13812345678', specialtyTypes: [] },
{ userId: 'u-low', name: '次高考生', schoolId: 'source-b', profileCompleted: true, idNumber: '320101200902021234', phone: '13912345678', specialtyTypes: [] },
{ userId: 'u-sport', name: '特长考生', schoolId: 'source-b', profileCompleted: true, idNumber: '320101200903031234', phone: '13712345678', specialtyCategory: 'sports', specialtyType: 'track_field', specialtyTypes: ['track_field'] }
],
schools: [
{ id: 'source-a', name: '生源学校 A' }, { id: 'source-b', name: '生源学校 B' },
{ id: 'target-a', name: '第一中学' }, { id: 'target-b', name: '第二中学' }
],
registrations: [
{ id: 'r-high', examId: 'exam', userId: 'u-high', status: 'approved', subjectIds: ['cn', 'math'] },
{ id: 'r-low', examId: 'exam', userId: 'u-low', status: 'approved', subjectIds: ['cn', 'math'] },
{ id: 'r-sport', examId: 'exam', userId: 'u-sport', status: 'approved', subjectIds: ['cn', 'math'], featureScore: 88.5 }
],
results: [
{ registrationId: 'r-high', subjectId: 'cn', score: 120, published: true }, { registrationId: 'r-high', subjectId: 'math', score: 130, published: true },
{ registrationId: 'r-low', subjectId: 'cn', score: 118, published: true }, { registrationId: 'r-low', subjectId: 'math', score: 126, published: true },
{ registrationId: 'r-sport', subjectId: 'cn', score: 105, published: true }, { registrationId: 'r-sport', subjectId: 'math', score: 110, published: true }
],
admissionRecords: [
{ id: 'plan-a', kind: 'plan', examId: 'exam', schoolId: 'target-a', status: 'approved', payload: { categories: [{ code: 'general', name: '普通生', quota: 1, specialtyType: '', indicatorAllocations: [] }] } },
{ id: 'plan-b', kind: 'plan', examId: 'exam', schoolId: 'target-b', status: 'approved', payload: { categories: [
{ code: 'general', name: '普通生', quota: 1, specialtyType: '', indicatorAllocations: [] },
{ code: 'sport', name: '田径特长生', quota: 1, specialtyCategory: 'sports', specialtyType: 'track_field', indicatorAllocations: [{ sourceSchoolId: 'source-b', quota: 1 }] }
] } },
{ id: 'qual-low', kind: 'indicator_qualification', examId: 'exam', userId: 'u-low', schoolId: 'source-b', status: 'confirmed', payload: { eligible: false } },
{ id: 'qual-sport', kind: 'indicator_qualification', examId: 'exam', userId: 'u-sport', schoolId: 'source-b', status: 'confirmed', payload: { eligible: true } },
{ id: 'pref-high', kind: 'preference', examId: 'exam', userId: 'u-high', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'general', preferenceType: 'general' }, { schoolId: 'target-a', categoryCode: 'general', preferenceType: 'general' }] } },
{ id: 'pref-low', kind: 'preference', examId: 'exam', userId: 'u-low', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'general', preferenceType: 'general' }, { schoolId: 'target-a', categoryCode: 'general', preferenceType: 'general' }] } },
{ id: 'pref-sport', kind: 'preference', examId: 'exam', userId: 'u-sport', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'sport', preferenceType: 'indicator' }] } }
]
};
const setting = { examId: 'exam', payload: { round: 1 } };
const placements = buildVolunteerPlacements(db, setting, { uid: prefix => `${prefix}-${++sequence}`, nowIso: () => now });
assert.equal(candidateTotalScore(db, 'exam', 'u-high'), 250, '投档总分应取当次全部已发布科目之和');
assert.equal(candidateAdmissionScore(db, 'exam', 'u-sport', { code: 'general' }), 215, '普通招生类别不得加入特征分');
assert.equal(candidateAdmissionScore(db, 'exam', 'u-sport', { specialtyCategory: 'sports', specialtyType: 'track_field' }), 303.5, '特长生招生类别应使用文化课总分加特征分');
assert.equal(placements.length, 3, '三个符合条件且计划充足的考生都应投档');
assert.equal(placements.find(item => item.userId === 'u-high').schoolId, 'target-b', '最高分考生应优先满足第一志愿');
assert.equal(placements.find(item => item.userId === 'u-low').schoolId, 'target-a', '第一志愿已满时应继续遵循下一志愿');
assert.equal(placements.find(item => item.userId === 'u-sport').payload.quotaBucket, 'indicator:source-b', '特长生指标应使用对应生源学校指标名额');
assert.equal(placements.find(item => item.userId === 'u-sport').payload.featureScore, 88.5, '特征分应随投档材料发送');
assert.equal(placements.find(item => item.userId === 'u-sport').payload.culturalScore, 215, '特长生投档材料应保留文化课原始总分');
assert.equal(placements.find(item => item.userId === 'u-sport').payload.totalScore, 303.5, '特长生类别投档总分应加入特征分');
assert.equal(specialtyLabel('sports', 'track_field'), '体育·田径', '特长资格应显示大类和小类');
assert.equal(candidateEligibleForCategory(db.candidateProfiles[2], { specialtyCategory: 'arts', specialtyType: 'fine_arts' }), false, '体育资格考生不得填报艺术类计划');
const specialtyPriorityDb = structuredClone(db);
specialtyPriorityDb.users.push({ id: 'u-sport-rival', role: 'candidate', active: true, candidateNumber: '20260004', displayName: '特长竞争考生' });
specialtyPriorityDb.candidateProfiles.push({ userId: 'u-sport-rival', name: '特长竞争考生', schoolId: 'source-b', profileCompleted: true, specialtyCategory: 'sports', specialtyType: 'track_field', specialtyTypes: ['track_field'] });
specialtyPriorityDb.registrations.push({ id: 'r-sport-rival', examId: 'exam', userId: 'u-sport-rival', status: 'approved', subjectIds: ['cn', 'math'], featureScore: 0 });
specialtyPriorityDb.results.push({ registrationId: 'r-sport-rival', subjectId: 'cn', score: 125, published: true }, { registrationId: 'r-sport-rival', subjectId: 'math', score: 125, published: true });
specialtyPriorityDb.admissionRecords.push(
{ id: 'qual-sport-rival', kind: 'indicator_qualification', examId: 'exam', userId: 'u-sport-rival', schoolId: 'source-b', status: 'confirmed', payload: { eligible: true } },
{ id: 'pref-sport-rival', kind: 'preference', examId: 'exam', userId: 'u-sport-rival', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'sport', preferenceType: 'indicator' }] } }
);
const specialtyPriorityPlacements = buildVolunteerPlacements(specialtyPriorityDb, setting, { uid: prefix => `${prefix}-priority-${++sequence}`, nowIso: () => now });
assert.equal(specialtyPriorityPlacements.find(item => item.payload.categoryCode === 'sport').userId, 'u-sport', '特长类别应按文化课加特征分排序,而不是只按文化课排序');
db.admissionRecords.push(...placements.map(item => ({ ...item, status: 'final' })));
const remaining = remainingPlanQuota(db, db.admissionRecords.find(item => item.id === 'plan-b'));
assert.equal(remaining.find(item => item.code === 'general').remaining, 0, '普通生计划占用应准确统计');
assert.equal(remaining.find(item => item.code === 'sport').remaining, 0, '特长生计划占用应准确统计');
const supplementDb = structuredClone(db);
const supplementSetting = { id: 'setting-supplement', kind: 'setting', examId: 'exam', status: 'supplementary', payload: { round: 2 } };
supplementDb.admissionRecords.push(
supplementSetting,
{ id: 'report-a', kind: 'notification', examId: 'exam', schoolId: 'target-a', userId: null, status: 'approved', payload: { type: 'admission_reporting', round: 1, supplementDecision: 'supplement' } },
{ id: 'report-b', kind: 'notification', examId: 'exam', schoolId: 'target-b', userId: null, status: 'approved', payload: { type: 'admission_reporting', round: 1, supplementDecision: 'supplement' } }
);
for (const placement of supplementDb.admissionRecords.filter(item => item.kind === 'placement' && item.payload?.categoryCode === 'general')) placement.status = 'forfeited';
for (const [suffix, schoolId, score] of [['a', 'target-a', 230], ['b', 'target-b', 225]]) {
supplementDb.users.push({ id: `u-supp-${suffix}`, role: 'candidate', active: true, candidateNumber: `2026010${suffix === 'a' ? 1 : 2}`, displayName: `补录考生${suffix.toUpperCase()}` });
supplementDb.candidateProfiles.push({ userId: `u-supp-${suffix}`, name: `补录考生${suffix.toUpperCase()}`, schoolId: 'source-a', profileCompleted: true, specialtyTypes: [] });
supplementDb.registrations.push({ id: `r-supp-${suffix}`, examId: 'exam', userId: `u-supp-${suffix}`, status: 'approved', subjectIds: ['cn'] });
supplementDb.results.push({ registrationId: `r-supp-${suffix}`, subjectId: 'cn', score, published: true });
supplementDb.admissionRecords.push({ id: `pref-supp-${suffix}`, kind: 'preference', examId: 'exam', userId: `u-supp-${suffix}`, status: 'submitted', payload: { round: 2, choices: [{ schoolId, categoryCode: 'general', preferenceType: 'general' }] } });
}
assert.deepEqual([...supplementarySchoolIds(supplementDb, supplementSetting)].sort(), ['target-a', 'target-b'], '同轮多所学校获批补录时应完整保留学校集合');
const supplementPlacements = buildVolunteerPlacements(supplementDb, supplementSetting, { uid: prefix => `${prefix}-supp-${++sequence}`, nowIso: () => now });
assert.deepEqual(supplementPlacements.map(item => item.schoolId).sort(), ['target-a', 'target-b'], '放弃考生不得继续占用缺额,两所获批学校都应进入补录投档');
const publicRows = publicAdmissionRows(db, 'exam');
const highPublicRow = publicRows.find(item => item.registrationNumber === '20260001');
assert.equal(highPublicRow.registrationNumber, '20260001', '公示必须公开报名号');
assert.equal(highPublicRow.name, '高分考生', '公示必须公开姓名');
assert.equal(highPublicRow.totalScore, 250, '普通类别公示总成绩不得加入特征分');
assert.equal(highPublicRow.admittedSchool, '第二中学', '公示必须公开录取学校');
assert.ok(highPublicRow.idNumberMasked.includes('*') && !highPublicRow.idNumberMasked.includes('20090101'), '重要身份信息必须脱敏');
assert.equal(publicRows.find(item => item.registrationNumber === '20260003').totalScore, 303.5, '特长生类别公示总成绩应包含特征分');
const cutoffs = admissionCutoffRows(db, 'exam');
assert.equal(cutoffs.find(item => item.schoolId === 'target-b' && item.categoryCode === 'general').cutoffScore, 250, '录取分数线应取学校招生类别最终录取最低总分');
const qualificationStatus = sourceSchoolQualificationStatus(db, 'exam', 'source-b');
assert.equal(qualificationStatus.complete, true, '生源校全部考生确认后应达到自动公示条件');
assert.equal(qualificationStatus.rows.find(item => item.userId === 'u-sport').specialtyLabel, '体育·田径', '资格公示应包含对应特长类型');
const documentDb = structuredClone(db);
documentDb.exams = [{ id: 'exam', code: 'EX-2026-ZK', name: '中考' }];
documentDb.schools.find(item => item.id === 'target-b').code = 'AD02';
const targetPlacements = documentDb.admissionRecords.filter(item => item.kind === 'placement' && item.schoolId === 'target-b');
const numbered = assignAdmissionNoticeNumbers(documentDb, targetPlacements);
assert.deepEqual(numbered.map(item => item.payload.noticeNumber), ['AD02-EX-2026-ZK-000001', 'AD02-EX-2026-ZK-000002'], '通知书编号应按学校与考试独立生成连续流水号');
const reportingPlan = documentDb.admissionRecords.find(item => item.id === 'plan-b');
documentDb.admissionRecords = documentDb.admissionRecords.map(item => numbered.find(numberedItem => numberedItem.id === item.id) || item);
const legacyRoundDb = structuredClone(documentDb);
legacyRoundDb.admissionRecords.push({ id: 'setting-exam', kind: 'setting', examId: 'exam', status: 'reporting', updatedAt: now, payload: { enabled: true, autoPublish: true, round: 1, roundPublishedAt: now } });
const legacyRoundPublication = admissionRoundPublications(legacyRoundDb).find(item => item.examId === 'exam' && item.round === 1);
assert.ok(legacyRoundPublication?.virtual, '历史报到中数据缺少轮次公示记录时应自动兼容回显');
assert.equal(legacyRoundPublication.rows.length, publicAdmissionRows(legacyRoundDb, 'exam', { round: 1 }).length, '历史轮次公示应恢复该轮全部正式录取名单');
documentDb.admissionRecords.push({ id: 'reporting-b', kind: 'notification', examId: 'exam', schoolId: 'target-b', userId: null, status: 'draft', payload: { type: 'admission_reporting', round: 1, rows: [{ placementId: numbered[0].id, status: 'reported' }, { placementId: numbered[1].id, status: 'not_reported' }] } });
const progress = admissionPlanProgress(documentDb, reportingPlan);
assert.equal(progress.totalQuota, 2, '计划完成率分母应来自学校审核通过的计划人数');
assert.equal(progress.reportedCount, 1, '实际报到人数应来自学校报到暂存台账');
assert.equal(progress.reportingRate, 50, '实际报到完成率应实时按计划人数计算');
documentDb.admissionRecords.at(-1).status = 'approved';
documentDb.admissionRecords.at(-1).payload.supplementDecision = 'no_supplement';
documentDb.admissionRecords.at(-1).payload.decisionNote = '学校研究决定不进行补录。';
documentDb.admissionRecords.at(-1).payload.statistics = progress;
const reportingNotice = systemNotificationItems(documentDb).find(item => item.sourceType === 'reporting');
assert.ok(reportingNotice.title.includes('报到情况公示') && !reportingNotice.title.includes('补录'), '计划完成或决定不补录时公告标题不应出现“补录”');
console.log('志愿投档、指标名额与脱敏公示测试通过');
+25
View File
@@ -0,0 +1,25 @@
import assert from 'node:assert/strict';
import { api } from '../src/client/api.mjs';
const originalFetch = globalThis.fetch;
let calls = 0;
let release;
globalThis.fetch = async () => {
calls += 1;
await new Promise(resolve => { release = resolve; });
return new Response(JSON.stringify({ ok: true, calls }), { status: 200, headers: { 'Content-Type': 'application/json' } });
};
try {
const first = api('/api/large-ledger');
const second = api('/api/large-ledger');
await Promise.resolve();
assert.equal(calls, 1, '并发的相同 GET 请求应只发送一次');
release();
assert.deepEqual(await first, { ok: true, calls: 1 });
assert.deepEqual(await second, { ok: true, calls: 1 });
} finally {
globalThis.fetch = originalFetch;
}
console.log('API read deduplication tests passed');
+209
View File
@@ -0,0 +1,209 @@
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { createAuthStateStore } from '../src/security/auth-state.mjs';
class FakeRedisClient extends EventEmitter {
constructor({ connectError = null } = {}) {
super();
this.connectError = connectError;
this.isOpen = false;
this.values = new Map();
this.sets = new Map();
this.hashes = new Map();
}
async connect() {
if (this.connectError) throw this.connectError;
this.isOpen = true;
}
async get(key) {
return this.values.has(key) ? this.values.get(key) : null;
}
async set(key, value) {
this.values.set(key, String(value));
return 'OK';
}
async del(key) {
const deleted = Number(this.values.delete(key)) + Number(this.sets.delete(key)) + Number(this.hashes.delete(key));
return deleted ? 1 : 0;
}
async sAdd(key, value) {
if (!this.sets.has(key)) this.sets.set(key, new Set());
const before = this.sets.get(key).size;
this.sets.get(key).add(value);
return this.sets.get(key).size - before;
}
async sRem(key, value) {
return Number(this.sets.get(key)?.delete(value) || false);
}
async sMembers(key) {
return [...(this.sets.get(key) || [])];
}
async expire() {
return 1;
}
async hSet(key, entries) {
if (!this.hashes.has(key)) this.hashes.set(key, new Map());
for (const [field, value] of Object.entries(entries)) this.hashes.get(key).set(field, String(value));
return Object.keys(entries).length;
}
async hGetAll(key) {
return Object.fromEntries(this.hashes.get(key) || []);
}
async eval(_script, { keys, arguments: scriptArguments }) {
const hash = this.hashes.get(keys[0]);
if (!hash) return -1;
const attempts = Number(hash.get('attempts') || 0) + 1;
hash.set('attempts', String(attempts));
if (attempts >= Number(scriptArguments[0])) this.hashes.delete(keys[0]);
return attempts;
}
multi() {
const operations = [];
const transaction = {};
for (const method of ['set', 'del', 'sAdd', 'sRem', 'expire', 'hSet']) {
transaction[method] = (...args) => {
operations.push(() => this[method](...args));
return transaction;
};
}
transaction.exec = async () => Promise.all(operations.map(operation => operation()));
return transaction;
}
async quit() {
this.isOpen = false;
}
destroy() {
this.isOpen = false;
}
}
const silentLogger = { error() {} };
{
const state = await createAuthStateStore({ env: {} });
assert.equal(state.status, 'disabled');
assert.equal(state.backend, 'memory');
await state.createSession('session-1', 'user-1');
assert.deepEqual(await state.getSession('session-1'), { userId: 'user-1' });
await state.createLoginChallenge('challenge-1', 'user-1');
assert.deepEqual(await state.getLoginChallenge('challenge-1'), { userId: 'user-1', attempts: 0 });
await state.createTotpSetup('session-1', 'user-1', 'SECRET');
assert.deepEqual(await state.getTotpSetup('session-1'), { userId: 'user-1', secret: 'SECRET' });
assert.equal(await state.deleteUserSessions('user-1'), 1);
assert.equal(await state.getSession('session-1'), null);
await state.close();
}
{
const client = new FakeRedisClient();
let clientOptions;
const state = await createAuthStateStore({
env: { REDIS_URL: 'redis://cache.example:6379/0', REDIS_SESSION_PREFIX: 'test:auth' },
logger: silentLogger,
clientFactory(options) {
clientOptions = options;
return client;
}
});
assert.equal(state.status, 'ready');
assert.equal(state.backend, 'redis');
assert.equal(state.database, 1, '普通缓存使用 DB 0 时,认证状态应自动使用 DB 1');
assert.equal(clientOptions.database, 1);
assert.equal(clientOptions.url, 'redis://cache.example:6379/0');
await state.createSession('session-1', 'user-1');
await state.createSession('session-2', 'user-1');
assert.deepEqual(await state.getSession('session-1'), { userId: 'user-1' });
assert.equal(await state.deleteUserSessions('user-1'), 2);
assert.equal(await state.getSession('session-1'), null);
assert.equal(await state.getSession('session-2'), null);
await state.createLoginChallenge('challenge-1', 'user-1');
for (let attempts = 1; attempts <= 4; attempts += 1) {
assert.deepEqual(await state.recordLoginChallengeFailure('challenge-1', 5), { attempts, exhausted: false });
}
assert.deepEqual(await state.recordLoginChallengeFailure('challenge-1', 5), { attempts: 5, exhausted: true });
assert.equal(await state.getLoginChallenge('challenge-1'), null);
await state.createTotpSetup('session-3', 'user-1', 'SECRET');
assert.deepEqual(await state.getTotpSetup('session-3'), { userId: 'user-1', secret: 'SECRET' });
await state.deleteTotpSetup('session-3');
assert.equal(await state.getTotpSetup('session-3'), null);
await state.close();
assert.equal(client.isOpen, false);
}
{
const client = new FakeRedisClient();
let options;
const state = await createAuthStateStore({
env: { REDIS_URL: 'redis://cache.example:6379/0', REDIS_SESSION_URL: 'rediss://sessions.example:6380/4' },
logger: silentLogger,
clientFactory(clientOptions) {
options = clientOptions;
return client;
}
});
assert.equal(options.url, 'rediss://sessions.example:6380/4');
assert.equal(options.database, 4);
await state.close();
}
{
const client = new FakeRedisClient();
let options;
const state = await createAuthStateStore({
env: { REDIS_URL: 'redis://cache.example:6379/1', REDIS_SESSION_DB: '0' },
logger: silentLogger,
clientFactory(clientOptions) {
options = clientOptions;
return client;
}
});
assert.equal(options.database, 0, '应允许显式选择 DB 0,只要普通缓存使用不同的 DB');
await state.close();
}
await assert.rejects(
createAuthStateStore({
env: { REDIS_URL: 'redis://same.example:6379/0', REDIS_SESSION_URL: 'redis://same.example:6379/0' },
logger: silentLogger,
clientFactory: () => new FakeRedisClient()
}),
/必须使用与普通缓存不同的逻辑数据库/
);
await assert.rejects(
createAuthStateStore({
env: { REDIS_URL: 'redis://cache-user@same.example:6379/0', REDIS_SESSION_URL: 'redis://session-user@same.example:6379/0' },
logger: silentLogger,
clientFactory: () => new FakeRedisClient()
}),
/必须使用与普通缓存不同的逻辑数据库/
);
await assert.rejects(
createAuthStateStore({
env: { REDIS_URL: 'redis://unavailable.example:6379/0' },
logger: silentLogger,
clientFactory: () => new FakeRedisClient({ connectError: new Error('connection refused') })
}),
/认证状态存储连接失败/
);
console.log('✓ 认证状态本机回退、独立 Redis DB、会话失效与 TOTP 临时状态');
+129
View File
@@ -0,0 +1,129 @@
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { createRedisCache, withCacheInvalidation } from '../src/cache/redis-cache.mjs';
class FakeRedisClient extends EventEmitter {
constructor({ connectError = null } = {}) {
super();
this.connectError = connectError;
this.isOpen = false;
this.isReady = false;
this.values = new Map();
}
async connect() {
if (this.connectError) throw this.connectError;
this.isOpen = true;
this.isReady = true;
this.emit('ready');
}
async get(key) {
return this.values.has(key) ? this.values.get(key) : null;
}
async set(key, value, options = {}) {
if (options.NX && this.values.has(key)) return null;
this.values.set(key, value);
return 'OK';
}
async incr(key) {
const next = Number(this.values.get(key) || 0) + 1;
this.values.set(key, String(next));
return next;
}
async quit() {
this.isReady = false;
this.isOpen = false;
}
destroy() {
this.isReady = false;
this.isOpen = false;
}
}
const silentLogger = { warn() {} };
{
const client = new FakeRedisClient();
const cache = await createRedisCache({
env: { REDIS_URL: 'redis://test', REDIS_CACHE_PREFIX: 'test', REDIS_CACHE_TTL_SECONDS: '30' },
logger: silentLogger,
clientFactory: () => client
});
let loads = 0;
const load = async () => ({ version: ++loads });
assert.deepEqual(await cache.remember('public', 'home', load), { version: 1 });
assert.deepEqual(await cache.remember('public', 'home', load), { version: 1 });
assert.equal(loads, 1, '相同缓存键应只读取一次数据源');
await cache.invalidate('public');
assert.deepEqual(await cache.remember('public', 'home', load), { version: 2 });
assert.equal(loads, 2, '命名空间失效后应重新读取数据源');
let resultLoads = 0;
const loadResults = async () => ({ version: ++resultLoads });
assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 1 });
const database = withCacheInvalidation({
client: 'test',
async read() { return {}; },
async save() { return 'saved'; },
async close() {}
}, cache, method => method === 'save' ? ['public', 'results'] : ['public']);
assert.equal(await database.save(), 'saved');
assert.deepEqual(await cache.remember('public', 'home', load), { version: 3 });
assert.equal(loads, 3, '数据库写入后应让公开缓存失效');
assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 2 });
assert.equal(resultLoads, 2, '成绩相关写入后应让成绩缓存失效');
await cache.invalidate('results');
assert.deepEqual(await cache.remember('results', 'candidate:1', loadResults), { version: 3 });
assert.equal(resultLoads, 3, '后台手动刷新后应重新生成成绩缓存');
client.isReady = false;
let fallbackLoads = 0;
assert.equal(await cache.remember('public', 'runtime-fallback', async () => ++fallbackLoads), 1);
assert.equal(await cache.remember('public', 'runtime-fallback', async () => ++fallbackLoads), 1);
assert.equal(fallbackLoads, 1, 'Redis 运行中断开后,相同热点读取应由本机缓存合并');
client.isReady = true;
await cache.close();
}
{
const cache = await createRedisCache({ env: {}, logger: silentLogger });
let loads = 0;
assert.equal(await cache.remember('public', 'home', async () => ++loads), 1);
assert.equal(await cache.remember('public', 'home', async () => ++loads), 1);
assert.equal(loads, 1, '未配置 Redis 时应使用有界本机缓存,避免重复回源');
await cache.invalidate('public');
assert.equal(await cache.remember('public', 'home', async () => ++loads), 2);
assert.equal(loads, 2, '本机缓存应在数据库写入后立即失效');
assert.equal(cache.status, 'disabled');
let releaseOld;
const oldLoad = cache.remember('public', 'race', () => new Promise(resolve => { releaseOld = () => resolve('old'); }));
await cache.invalidate('public');
const newLoad = cache.remember('public', 'race', async () => 'new');
releaseOld();
assert.equal(await oldLoad, 'old');
assert.equal(await newLoad, 'new', '失效后不得等待失效前仍在运行的加载');
assert.equal(await cache.remember('public', 'race', async () => 'unexpected'), 'new', '旧加载完成后不得覆盖新缓存');
}
{
const client = new FakeRedisClient({ connectError: new Error('connection refused') });
const cache = await createRedisCache({
env: { REDIS_URL: 'redis://unavailable' },
logger: silentLogger,
clientFactory: () => client
});
assert.equal(cache.status, 'unavailable');
assert.equal(await cache.remember('public', 'home', async () => 'database'), 'database');
assert.equal(await cache.remember('public', 'home', async () => 'unexpected'), 'database', 'Redis 故障时本机缓存应继续承接重复读取');
}
console.log('Redis 缓存测试通过');
+57
View File
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict';
import { createAdminViews } from '../src/client/admin-views.mjs';
import { createAdmissionViews } from '../src/client/admission-views.mjs';
import { api } from '../src/client/api.mjs';
import { createCandidateViews } from '../src/client/candidate-views.mjs';
import { createPublicViews } from '../src/client/public-views.mjs';
function protectedViewContext() {
let loginRequests = 0;
const context = {
state: { user: null },
app: { classList: { add() {}, remove() {} } },
requireLogin() { loginRequests += 1; }
};
return { context, loginRequests: () => loginRequests };
}
for (const createView of [createAdminViews, createCandidateViews, createAdmissionViews]) {
const fixture = protectedViewContext();
const views = createView(fixture.context);
const render = views.renderAdmin || views.renderCandidate || views.renderAdmission;
await render('dashboard');
assert.equal(fixture.loginRequests(), 1, '未登录访问受保护视图时应交给统一登录处理');
}
{
const app = { classList: { remove() {} }, innerHTML: '' };
const state = {
user: null,
authNotice: '登录状态已失效,请重新登录。',
publicData: { selfRegistrationEnabled: false }
};
const { renderAuth } = createPublicViews({
state,
app,
h: value => String(value ?? ''),
icons: { arrow: '', menu: '' }
});
renderAuth('login');
assert.match(app.innerHTML, /需要重新登录/);
assert.match(app.innerHTML, /登录状态已失效,请重新登录/);
}
{
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response(JSON.stringify({ message: '请先登录' }), {
status: 401,
headers: { 'content-type': 'application/json' }
});
try {
await assert.rejects(() => api('/api/protected'), error => error.status === 401 && error.message === '请先登录');
} finally {
globalThis.fetch = originalFetch;
}
}
console.log('客户端登录失效处理测试通过');
+44
View File
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict';
import { admissionNoticeCode, resolveDocumentVerificationSecret, safeCodeEqual, scoreReportCode } from '../src/security/document-verification.mjs';
const secret = 'test-document-verification-secret-32-characters';
const exam = { id: 'exam_1' };
const registration = { id: 'registration_1', userId: 'candidate_1' };
const results = [
{ subjectId: 'math', score: 118, publishedAt: '2026-07-20T08:00:00.000Z' },
{ subjectId: 'chinese', score: 112, publishedAt: '2026-07-20T08:00:00.000Z' }
];
const scoreCode = scoreReportCode(secret, registration, exam, results);
const reorderedCode = scoreReportCode(secret, registration, exam, [...results].reverse());
const changedScoreCode = scoreReportCode(secret, registration, exam, [{ ...results[0], score: 119 }, results[1]]);
assert.match(scoreCode, /^SR-[A-F0-9]{24}$/);
assert.equal(scoreCode, reorderedCode, '科目返回顺序不应改变同一成绩单的防伪码');
assert.notEqual(scoreCode, changedScoreCode, '成绩变化必须使旧防伪码失效');
assert.equal(safeCodeEqual(scoreCode, scoreCode.toLowerCase()), true);
assert.equal(safeCodeEqual(scoreCode, `${scoreCode}0`), false);
const placement = {
id: 'placement_1', userId: 'candidate_1', schoolId: 'school_1',
payload: { categoryCode: 'general', noticeNumber: 'AD01-EX-2026-000001' }, updatedAt: '2026-07-21T08:00:00.000Z'
};
const noticeCode = admissionNoticeCode(secret, placement, exam);
const movedSchoolCode = admissionNoticeCode(secret, { ...placement, schoolId: 'school_2' }, exam);
const changedNumberCode = admissionNoticeCode(secret, { ...placement, payload: { ...placement.payload, noticeNumber: 'AD01-EX-2026-000002' } }, exam);
assert.match(noticeCode, /^AN-[A-F0-9]{24}$/);
assert.notEqual(noticeCode, movedSchoolCode, '录取学校变化必须使旧通知书防伪码失效');
assert.notEqual(noticeCode, changedNumberCode, '录取通知书编号变化必须使旧防伪码失效');
assert.throws(
() => resolveDocumentVerificationSecret({ NODE_ENV: 'production', DOCUMENT_VERIFICATION_SECRET: 'too-short' }),
/至少 32 个字符/,
'生产环境不得静默使用弱密钥或开发回退值'
);
assert.equal(
resolveDocumentVerificationSecret({ NODE_ENV: 'production', DOCUMENT_VERIFICATION_SECRET: secret }),
secret
);
console.log('✓ 文书防伪码稳定性、篡改失效与安全比较测试通过');
+48
View File
@@ -0,0 +1,48 @@
import assert from 'node:assert/strict';
import { createSeedDatabase } from '../src/data/seed.mjs';
const state = createSeedDatabase({ nowIso: () => '2026-07-21T00:00:00.000Z', hashPassword: password => `test-${password}` });
const mainExam = state.exams.find(exam => exam.id === 'exam_autumn_2026');
const mainRegistrations = state.registrations.filter(registration => registration.examId === mainExam.id);
const mainRegistrationIds = new Set(mainRegistrations.map(registration => registration.id));
const mainResults = state.results.filter(result => mainRegistrationIds.has(result.registrationId));
const mainPreferences = state.admissionRecords.filter(record => record.kind === 'preference' && record.examId === mainExam.id && Number(record.payload?.round || 1) === 1);
const mainPlans = state.admissionRecords.filter(record => record.kind === 'plan' && record.examId === mainExam.id && record.status === 'approved');
const specialtyProfiles = state.candidateProfiles.filter(profile => profile.specialtyCategory && profile.specialtyType);
const specialtyUserIds = new Set(specialtyProfiles.map(profile => profile.userId));
const profilesByUserId = new Map(state.candidateProfiles.map(profile => [profile.userId, profile]));
assert.ok(state.exams.length >= 2, '演示数据至少包含两场考试');
assert.equal(state.schools.filter(school => school.isSourceSchool).length, 5, '演示数据应包含 5 所生源校');
assert.equal(state.schools.filter(school => school.isAdmissionSchool).length, 3, '演示数据应包含 3 所招生校');
assert.deepEqual(Object.fromEntries(mainExam.subjects.map(subject => [subject.name, subject.fullScore])), {
语文: 120, 数学: 120, 外语: 120, 历史: 75, 政治: 75, 物理: 80, 化学: 70, 实验: 20, 信息技术: 10
});
assert.equal(mainRegistrations.length, 1200, '主考试应有 1200 名考生');
assert.ok(mainRegistrations.every(registration => registration.status === 'approved' && registration.subjectIds.length === 9), '主考试报名应全部审核通过并包含 9 科');
assert.equal(mainResults.length, 1200 * 9, '每名主考试考生都应有完整的 9 科成绩');
assert.ok(mainResults.every(result => result.published), '主考试成绩应全部发布');
assert.equal(mainPreferences.length, 1200, '主考试每名考生都应完成第一轮志愿');
assert.ok(mainPreferences.every(record => record.status === 'submitted' && record.payload.submissionCount === 1 && record.payload.choices.length === 3), '第一轮志愿应提交并填满 3 个招生校');
assert.equal(mainPlans.length, 3, '三所招生校都应有已审核通过的招生计划');
assert.ok(mainPlans.every(plan => plan.payload.categories.find(category => category.code === 'general')?.quota === 350), '每所招生校普通类计划应为 350 人');
assert.ok(mainPlans.every(plan => plan.payload.categories.filter(category => category.specialtyCategory).reduce((sum, category) => sum + category.quota, 0) === 2), '每所招生校特长生计划合计应为 2 人');
assert.equal(specialtyProfiles.length, 150, '应有 150 名特长生');
assert.ok(mainRegistrations.filter(registration => specialtyUserIds.has(registration.userId)).every(registration => registration.featureScore >= 80 && registration.featureScore <= 100), '特长生特征分应分布在 80-100 分');
assert.ok(mainPreferences.every(preference => {
const profile = profilesByUserId.get(preference.userId);
const categories = preference.payload.choices.map(choice => choice.categoryCode);
return profile.specialtyCategory ? categories[0] === profile.specialtyCategory && categories.slice(1).every(code => code === 'general') : categories.every(code => code === 'general');
}), '特长生第一志愿应匹配本人特长类别,其余志愿及普通考生志愿应填报普通类');
assert.deepEqual(new Set(state.users.map(user => user.passwordHash)), new Set(['test-12345678']), '所有预置账号密码应统一为 12345678');
for (const subject of mainExam.subjects) {
const scores = mainResults.filter(result => result.subjectId === subject.id).map(result => result.score);
const mean = scores.reduce((sum, score) => sum + score, 0) / scores.length;
const standardDeviation = Math.sqrt(scores.reduce((sum, score) => sum + (score - mean) ** 2, 0) / scores.length);
assert.ok(mean > subject.fullScore * 0.66 && mean < subject.fullScore * 0.78, `${subject.name}平均分应符合正态样本预期`);
assert.ok(standardDeviation > subject.fullScore * 0.09 && standardDeviation < subject.fullScore * 0.18, `${subject.name}标准差应符合正态样本预期`);
assert.ok(scores.every(score => score >= 0 && score <= subject.fullScore), `${subject.name}成绩不得超出满分`);
}
console.log('演示数据规模、学校角色、科目、志愿、特长生、成绩分布与密码校验通过');
+41
View File
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict';
import { createStateCache } from '../src/database/state-cache.mjs';
let loads = 0;
let release;
const gate = new Promise(resolve => { release = resolve; });
const coalesced = createStateCache({
maxAgeMs: 30000,
async load() {
loads += 1;
await gate;
return { load: loads };
}
});
const firstPending = coalesced.read();
const secondPending = coalesced.read();
release();
const [first, second] = await Promise.all([firstPending, secondPending]);
assert.equal(loads, 1, '并发冷读取应合并为一次全量加载');
assert.strictEqual(first, second, '并发读取应共享同一份快照');
assert.strictEqual(await coalesced.read(), first, '有效期内应直接复用内存快照');
coalesced.invalidate();
const afterInvalidation = await coalesced.read();
assert.equal(loads, 2, '应用写入失效后应重新加载');
assert.notStrictEqual(afterInvalidation, first, '失效后不得继续返回旧快照');
let version = 1;
let versionLoads = 0;
const versioned = createStateCache({
version: () => version,
load: () => ({ load: ++versionLoads })
});
const versionOne = await versioned.read();
assert.strictEqual(await versioned.read(), versionOne, '数据库版本未变化时应复用快照');
version += 1;
const versionTwo = await versioned.read();
assert.equal(versionLoads, 2, '外部数据库版本变化后应重新加载');
assert.notStrictEqual(versionTwo, versionOne, '外部写入后不得返回旧快照');
console.log('State cache tests passed');
File diff suppressed because it is too large Load Diff