Files
Exam-Information-System/src/security/document-verification.mjs
T
biss 3e1bced990 全站大数据台账补齐跨页搜索、筛选和分页,包括投档监督、报到审批、成绩复议、公开录取名单等。
后台重构为一级业务域,并将招生录取拆为 5 个二级菜单。
修复成绩等级排名遮挡科目。
成绩单、录取通知书均支持带防伪码和二维码的 PDF 下载。
录取通知书移除“录取专用章”,支持学校自定义模板。
通知书编号采用“学校代码 + 考试代码 + 学校独立流水号”。
完成招生学校报到台账:Y/N/P 状态、暂存、Excel 导入导出、二维码扫描确认。
完成报到提交、计划完成率、补录决定、超级管理员审批和自动公开闭环。
自动公告现已进入公开首页、考生通知及招生学校工作台。
验证结果:
完整 npm test 全部通过。
浏览器端三个角色工作台验收通过。
Excel 模板已实际导出并渲染验证。
两类 PDF 已实际生成、转图检查,二维码、编号和无印章版式正常。
2026-07-22 09:50:52 +08:00

29 lines
1.5 KiB
JavaScript

import { createHmac, timingSafeEqual } from 'node:crypto';
function signature(secret, type, parts) {
return createHmac('sha256', secret).update([type, ...parts].join('\u001f')).digest('hex').slice(0, 24).toUpperCase();
}
export function resolveDocumentVerificationSecret(env = process.env) {
const configured = String(env.DOCUMENT_VERIFICATION_SECRET || '');
if (env.NODE_ENV === 'production' && configured.length < 32) {
throw new Error('生产环境必须设置至少 32 个字符的 DOCUMENT_VERIFICATION_SECRET');
}
return configured || String(env.SESSION_SECRET || '') || 'development-document-verification-secret';
}
export function scoreReportCode(secret, registration, exam, results = []) {
const scores = [...results].sort((a, b) => String(a.subjectId).localeCompare(String(b.subjectId))).map(item => `${item.subjectId}:${Number(item.score)}:${item.publishedAt || item.updatedAt || ''}`);
return `SR-${signature(secret, 'score-report', [registration.id, registration.userId, exam.id, ...scores])}`;
}
export function admissionNoticeCode(secret, placement, exam) {
return `AN-${signature(secret, 'admission-notice', [placement.id, placement.userId, placement.schoolId, exam.id, placement.payload?.categoryCode || '', placement.payload?.noticeNumber || '', placement.updatedAt || ''])}`;
}
export function safeCodeEqual(left, right) {
const a = Buffer.from(String(left || '').toUpperCase());
const b = Buffer.from(String(right || '').toUpperCase());
return a.length === b.length && timingSafeEqual(a, b);
}