REDis
This commit is contained in:
@@ -4,6 +4,7 @@ import { noticeForClient, noticePlainText, sanitizeNoticeContent } from '../secu
|
||||
export function createAdminRoutes(context) {
|
||||
const {
|
||||
database,
|
||||
cache,
|
||||
readDb,
|
||||
sendJson,
|
||||
sendError,
|
||||
@@ -1040,6 +1041,16 @@ export function createAdminRoutes(context) {
|
||||
await database.updateNotice(notice, log);
|
||||
return sendJson(response, 200, { ok: true, notice: noticeForClient(notice) });
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/admin/results/cache/refresh') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const refreshed = await cache.invalidate('results');
|
||||
return sendJson(response, 200, {
|
||||
ok: true,
|
||||
refreshed,
|
||||
cacheStatus: cache.status,
|
||||
message: refreshed ? '成绩 Redis 缓存已刷新,后续查询将重新生成缓存' : 'Redis 缓存当前未连接或刷新失败,成绩查询继续直接读取数据库'
|
||||
});
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/admin/results') {
|
||||
if (!requirePermission(user, response, 'results.read')) return true;
|
||||
const scopedRegistrations = db.registrations.filter(item => registrationInScope(db, user, item));
|
||||
@@ -1088,7 +1099,7 @@ export function createAdminRoutes(context) {
|
||||
const account = db.users.find(accountItem => accountItem.id === item.userId);
|
||||
return { ...view, candidateName: profile?.name || account?.displayName || '', candidateNumber: account?.candidateNumber || item.registrationNumber || '', schoolName: profile?.school || '', className: db.classes.find(classItem => classItem.id === profile?.classId)?.name || profile?.grade || '' };
|
||||
}) : [];
|
||||
return sendJson(response, 200, { ok: true, results, appeals, registrations, exams });
|
||||
return sendJson(response, 200, { ok: true, results, appeals, registrations, exams, resultCache: { enabled: cache.enabled, status: cache.status } });
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/admin/results/import') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { noticeForClient } from '../security/notice-content.mjs';
|
||||
export function createCandidateRoutes(context) {
|
||||
const {
|
||||
database,
|
||||
cache,
|
||||
resultsCacheTtlSeconds,
|
||||
readDb,
|
||||
sendJson,
|
||||
sendError,
|
||||
@@ -126,25 +128,28 @@ export function createCandidateRoutes(context) {
|
||||
return sendJson(response, 201, { ok: true, registration: examRegistrationView(db, registration), message: '考试报名已提交' });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/candidate/results') {
|
||||
const registrations = db.registrations.filter(item => item.userId === user.id);
|
||||
const results = db.results.filter(item => item.published && registrations.some(reg => reg.id === item.registrationId)).map(result => {
|
||||
const registration = registrations.find(reg => reg.id === result.registrationId);
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const subject = exam.subjects.find(item => item.id === result.subjectId);
|
||||
const appealInstance = db.workflowInstances.find(item => item.businessType === 'score_appeal' && item.businessId === result.id);
|
||||
const appeal = appealInstance ? workflowView(db, appealInstance) : null;
|
||||
const rank = resultRankInfo(db, result);
|
||||
const pass = subjectPassEvaluation(db, result, subject);
|
||||
return {
|
||||
...result, ...rank, grade: rank.grade, examId: exam.id, examName: exam.name, examCode: exam.code, examStart: exam.examStart, archivedAt: exam.archivedAt || null,
|
||||
subjectName: subject?.name || result.subjectId, fullScore: subject?.fullScore || 150,
|
||||
passRule: subject?.passRule || 'fixed_score', passValue: subject?.passValue ?? subject?.passScore,
|
||||
passScore: pass.passScore, cutoffRank: pass.cutoffRank, passText: subjectPassText(subject), qualified: pass.qualified,
|
||||
appeal: appeal ? { ...appeal, reason: appeal.actions.find(action => action.action === 'submit')?.note || '' } : null
|
||||
};
|
||||
});
|
||||
const summaries = registrations.map(registration => examResultSummary(db, registration)).filter(summary => summary?.publishedSubjects);
|
||||
return sendJson(response, 200, { ok: true, results, summaries });
|
||||
const payload = await cache.remember('results', `candidate:${encodeURIComponent(user.id)}`, async () => {
|
||||
const registrations = db.registrations.filter(item => item.userId === user.id);
|
||||
const results = db.results.filter(item => item.published && registrations.some(reg => reg.id === item.registrationId)).map(result => {
|
||||
const registration = registrations.find(reg => reg.id === result.registrationId);
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const subject = exam.subjects.find(item => item.id === result.subjectId);
|
||||
const appealInstance = db.workflowInstances.find(item => item.businessType === 'score_appeal' && item.businessId === result.id);
|
||||
const appeal = appealInstance ? workflowView(db, appealInstance) : null;
|
||||
const rank = resultRankInfo(db, result);
|
||||
const pass = subjectPassEvaluation(db, result, subject);
|
||||
return {
|
||||
...result, ...rank, grade: rank.grade, examId: exam.id, examName: exam.name, examCode: exam.code, examStart: exam.examStart, archivedAt: exam.archivedAt || null,
|
||||
subjectName: subject?.name || result.subjectId, fullScore: subject?.fullScore || 150,
|
||||
passRule: subject?.passRule || 'fixed_score', passValue: subject?.passValue ?? subject?.passScore,
|
||||
passScore: pass.passScore, cutoffRank: pass.cutoffRank, passText: subjectPassText(subject), qualified: pass.qualified,
|
||||
appeal: appeal ? { ...appeal, reason: appeal.actions.find(action => action.action === 'submit')?.note || '' } : null
|
||||
};
|
||||
});
|
||||
const summaries = registrations.map(registration => examResultSummary(db, registration)).filter(summary => summary?.publishedSubjects);
|
||||
return { ok: true, results, summaries };
|
||||
}, { ttlSeconds: resultsCacheTtlSeconds });
|
||||
return sendJson(response, 200, payload);
|
||||
}
|
||||
const scoreAppealMatch = pathname.match(/^\/api\/candidate\/results\/([^/]+)\/appeals$/);
|
||||
if (request.method === 'POST' && scoreAppealMatch) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { noticeForClient } from '../security/notice-content.mjs';
|
||||
export function createPublicRoutes(context) {
|
||||
const {
|
||||
database,
|
||||
cache,
|
||||
readDb,
|
||||
publicSiteConfig,
|
||||
sendJson,
|
||||
@@ -51,16 +52,23 @@ export function createPublicRoutes(context) {
|
||||
} = context;
|
||||
|
||||
async function handlePublic(pathname, response) {
|
||||
const db = await readDb();
|
||||
if (pathname === '/api/public/home') {
|
||||
const publishedNotices = db.notices.filter(item => item.status === 'published').sort((a, b) => Number(b.pinned) - Number(a.pinned) || new Date(b.publishAt) - new Date(a.publishAt)).map(noticeForClient);
|
||||
const exams = db.exams.filter(item => item.status === 'published' && !item.archivedAt).map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length }));
|
||||
return sendJson(response, 200, { ok: true, organization: publicSiteConfig.organization, siteCopy: { heroEyebrow: publicSiteConfig.heroEyebrow, heroTitle: publicSiteConfig.heroTitle, heroHighlight: publicSiteConfig.heroHighlight, heroDescription: publicSiteConfig.heroDescription, footerNotice: publicSiteConfig.footerNotice }, schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active), selfRegistrationEnabled: db.settings.selfRegistrationEnabled, notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: exams.length, registrations: db.registrations.length } });
|
||||
const payload = await cache.remember('public', 'home', async () => {
|
||||
const db = await readDb();
|
||||
const publishedNotices = db.notices.filter(item => item.status === 'published').sort((a, b) => Number(b.pinned) - Number(a.pinned) || new Date(b.publishAt) - new Date(a.publishAt)).map(noticeForClient);
|
||||
const exams = db.exams.filter(item => item.status === 'published' && !item.archivedAt).map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length }));
|
||||
return { ok: true, organization: publicSiteConfig.organization, siteCopy: { heroEyebrow: publicSiteConfig.heroEyebrow, heroTitle: publicSiteConfig.heroTitle, heroHighlight: publicSiteConfig.heroHighlight, heroDescription: publicSiteConfig.heroDescription, footerNotice: publicSiteConfig.footerNotice }, schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active), selfRegistrationEnabled: db.settings.selfRegistrationEnabled, notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: exams.length, registrations: db.registrations.length } };
|
||||
});
|
||||
return sendJson(response, 200, payload);
|
||||
}
|
||||
const noticeMatch = pathname.match(/^\/api\/public\/notices\/([^/]+)$/);
|
||||
if (noticeMatch) {
|
||||
const notice = db.notices.find(item => item.id === noticeMatch[1] && item.status === 'published');
|
||||
return notice ? sendJson(response, 200, { ok: true, notice: noticeForClient(notice) }) : sendError(response, 404, '通知不存在或尚未发布');
|
||||
const notice = await cache.remember('public', `notice:${encodeURIComponent(noticeMatch[1])}`, async () => {
|
||||
const db = await readDb();
|
||||
const found = db.notices.find(item => item.id === noticeMatch[1] && item.status === 'published');
|
||||
return found ? noticeForClient(found) : null;
|
||||
});
|
||||
return notice ? sendJson(response, 200, { ok: true, notice }) : sendError(response, 404, '通知不存在或尚未发布');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user