后台重构为一级业务域,并将招生录取拆为 5 个二级菜单。 修复成绩等级排名遮挡科目。 成绩单、录取通知书均支持带防伪码和二维码的 PDF 下载。 录取通知书移除“录取专用章”,支持学校自定义模板。 通知书编号采用“学校代码 + 考试代码 + 学校独立流水号”。 完成招生学校报到台账:Y/N/P 状态、暂存、Excel 导入导出、二维码扫描确认。 完成报到提交、计划完成率、补录决定、超级管理员审批和自动公开闭环。 自动公告现已进入公开首页、考生通知及招生学校工作台。 验证结果: 完整 npm test 全部通过。 浏览器端三个角色工作台验收通过。 Excel 模板已实际导出并渲染验证。 两类 PDF 已实际生成、转图检查,二维码、编号和无印章版式正常。
29 lines
1.5 KiB
JavaScript
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);
|
|
}
|