数据库

This commit is contained in:
2026-07-19 21:48:15 +08:00 Unverified
parent de4d0961e4
commit bb5d3da766
8 changed files with 1229 additions and 61 deletions
+43 -51
View File
@@ -1,11 +1,12 @@
import { createServer } from 'node:http';
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
import { readFile } from 'node:fs/promises';
import { extname, join, normalize, resolve } from 'node:path';
import { randomBytes, pbkdf2Sync, timingSafeEqual } from 'node:crypto';
import { createDatabase } from './database.mjs';
const root = resolve(process.cwd());
const port = Number(process.env.PORT || 4173);
const dbPath = resolve(process.env.EXAM_DB_PATH || join(root, 'data', 'db.json'));
const host = process.env.HOST || '127.0.0.1';
const sessions = new Map();
const staticFiles = new Set(['/index.html', '/styles.css', '/app.js']);
const mimeTypes = {
@@ -108,24 +109,8 @@ function seedDatabase() {
};
}
async function ensureDatabase() {
try {
await access(dbPath);
} catch {
await mkdir(resolve(dbPath, '..'), { recursive: true });
await writeFile(dbPath, JSON.stringify(seedDatabase(), null, 2), 'utf8');
}
}
async function readDb() {
await ensureDatabase();
return JSON.parse(await readFile(dbPath, 'utf8'));
}
async function writeDb(db) {
await mkdir(resolve(dbPath, '..'), { recursive: true });
await writeFile(dbPath, JSON.stringify(db, null, 2), 'utf8');
}
const database = await createDatabase({ root, seed: seedDatabase });
const readDb = () => database.read();
function sendJson(response, status, payload, headers = {}) {
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', ...headers });
@@ -213,8 +198,10 @@ function examRegistrationView(db, registration) {
}
function logAction(db, user, action, detail) {
db.auditLogs.unshift({ id: uid('log'), actorId: user.id, actorName: user.displayName, action, detail, createdAt: nowIso() });
const log = { id: uid('log'), actorId: user.id, actorName: user.displayName, action, detail, createdAt: nowIso() };
db.auditLogs.unshift(log);
db.auditLogs = db.auditLogs.slice(0, 200);
return log;
}
function escapeHtml(value) {
@@ -265,9 +252,7 @@ async function handleAuth(request, response, pathname) {
if (db.candidateProfiles.some(profile => profile.idNumber === idNumber)) return sendError(response, 409, '该证件号码已注册');
const user = { id: uid('usr'), username, passwordHash: hashPassword(password), role: 'candidate', displayName: name, createdAt: nowIso() };
const profile = { id: uid('profile'), userId: user.id, name, idNumber, phone, gender: cleanText(body.gender, 10), email: cleanText(body.email, 80), school: cleanText(body.school, 80), grade: cleanText(body.grade, 50), address: '', emergencyContact: '', emergencyPhone: '', status: 'pending', reviewNote: '', updatedAt: nowIso() };
db.users.push(user);
db.candidateProfiles.push(profile);
await writeDb(db);
await database.createCandidate(user, profile);
return sendJson(response, 201, { ok: true, message: '注册成功,请等待管理员审核资料' });
}
if (request.method === 'POST' && pathname === '/api/auth/login') {
@@ -310,9 +295,7 @@ async function handleCandidate(request, response, pathname) {
profile.status = 'pending';
profile.reviewNote = '';
profile.updatedAt = nowIso();
const persistedUser = db.users.find(item => item.id === user.id);
if (persistedUser) persistedUser.displayName = profile.name;
await writeDb(db);
await database.updateCandidateProfile(profile, profile.name);
return sendJson(response, 200, { ok: true, profile, message: '资料已提交,等待管理员复核' });
}
if (request.method === 'GET' && pathname === '/api/candidate/exams') {
@@ -334,8 +317,7 @@ async function handleCandidate(request, response, pathname) {
const subjectIds = [...new Set(Array.isArray(body.subjectIds) ? body.subjectIds : [])];
if (!subjectIds.length || subjectIds.some(id => !exam.subjects.some(subject => subject.id === id))) return sendError(response, 400, '请选择有效的报考科目');
const registration = { id: uid('reg'), userId: user.id, examId: exam.id, subjectIds, status: 'pending', paymentStatus: 'unpaid', createdAt: nowIso(), admitCard: null };
db.registrations.push(registration);
await writeDb(db);
await database.createRegistration(registration);
return sendJson(response, 201, { ok: true, registration: examRegistrationView(db, registration), message: '考试报名已提交' });
}
if (request.method === 'GET' && pathname === '/api/candidate/results') {
@@ -390,8 +372,8 @@ async function handleAdmin(request, response, pathname) {
profile.reviewNote = cleanText(body.reviewNote, 300);
profile.reviewedAt = nowIso();
profile.reviewerId = user.id;
logAction(db, user, body.status === 'approved' ? '通过考生资料' : '退回考生资料', `${profile.name}${profile.reviewNote || '无备注'}`);
await writeDb(db);
const log = logAction(db, user, body.status === 'approved' ? '通过考生资料' : '退回考生资料', `${profile.name}${profile.reviewNote || '无备注'}`);
await database.reviewCandidate(profile, log);
return sendJson(response, 200, { ok: true, profile });
}
if (request.method === 'GET' && pathname === '/api/admin/registrations') {
@@ -412,8 +394,8 @@ async function handleAdmin(request, response, pathname) {
registration.reviewedAt = nowIso();
if (body.status === 'approved') registration.paymentStatus = 'paid';
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
logAction(db, user, body.status === 'approved' ? '通过考试报名' : '退回考试报名', `${profile?.name || registration.userId} · ${db.exams.find(item => item.id === registration.examId)?.name}`);
await writeDb(db);
const log = logAction(db, user, body.status === 'approved' ? '通过考试报名' : '退回考试报名', `${profile?.name || registration.userId} · ${db.exams.find(item => item.id === registration.examId)?.name}`);
await database.reviewRegistration(registration, log);
return sendJson(response, 200, { ok: true, registration });
}
const admitMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)\/admit-card$/);
@@ -425,15 +407,15 @@ async function handleAdmin(request, response, pathname) {
const exam = db.exams.find(item => item.id === registration.examId);
const sequence = String(db.registrations.filter(item => item.examId === exam.id && item.admitCard).length + 1).padStart(4, '0');
registration.admitCard = {
number: `${exam.code.replace(/\D/g, '').slice(-4) || '2026'}-${sequence}`,
number: `${exam.code.replace(/[^a-z0-9]/gi, '').toUpperCase().slice(-12) || String(new Date().getFullYear())}-${sequence}`,
testCenter: cleanText((await readJson(request)).testCenter || '海州市第一中学', 80),
room: `0${Math.ceil(Number(sequence) / 30) || 1} 考场`,
seat: String(((Number(sequence) - 1) % 30) + 1).padStart(2, '0'),
generatedAt: nowIso()
};
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
logAction(db, user, '生成准考证', `${profile?.name || registration.userId} · ${registration.admitCard.number}`);
await writeDb(db);
const log = logAction(db, user, '生成准考证', `${profile?.name || registration.userId} · ${registration.admitCard.number}`);
await database.createAdmitCard(registration.id, registration.admitCard, log);
}
return sendJson(response, 200, { ok: true, admitCard: registration.admitCard });
}
@@ -446,9 +428,8 @@ async function handleAdmin(request, response, pathname) {
const subjects = subjectNames.map(name => cleanText(typeof name === 'string' ? name : name.name, 30)).filter(Boolean).map((name, index) => ({ id: uid('sub'), name, date: cleanText(body.examStart, 10), start: '09:00', end: '11:00', fee: 0, order: index + 1 }));
if (!subjects.length) return sendError(response, 400, '请至少添加一个考试科目');
const exam = { id: uid('exam'), code: cleanText(body.code, 30) || `EX-${new Date().getFullYear()}-${String(db.exams.length + 1).padStart(2, '0')}`, name, description: cleanText(body.description, 500), registrationStart: body.registrationStart, registrationEnd: body.registrationEnd, examStart: body.examStart, examEnd: body.examEnd, admitDownloadStart: body.admitDownloadStart || body.registrationEnd, admitDownloadEnd: body.admitDownloadEnd || body.examStart, location: cleanText(body.location, 100), status: body.status === 'published' ? 'published' : 'draft', subjects, createdAt: nowIso() };
db.exams.push(exam);
logAction(db, user, '创建考试', `${exam.name} · ${subjects.length} 个科目`);
await writeDb(db);
const log = logAction(db, user, '创建考试', `${exam.name} · ${subjects.length} 个科目`);
await database.createExam(exam, log);
return sendJson(response, 201, { ok: true, exam });
}
const examMatch = pathname.match(/^\/api\/admin\/exams\/([^/]+)$/);
@@ -458,8 +439,8 @@ async function handleAdmin(request, response, pathname) {
if (!exam) return sendError(response, 404, '考试不存在');
if (body.status && ['draft', 'published', 'closed'].includes(body.status)) exam.status = body.status;
['name', 'description', 'location', 'registrationStart', 'registrationEnd', 'examStart', 'examEnd', 'admitDownloadStart', 'admitDownloadEnd'].forEach(field => { if (body[field] != null) exam[field] = cleanText(body[field], 500); });
logAction(db, user, '更新考试', `${exam.name} · 状态 ${exam.status}`);
await writeDb(db);
const log = logAction(db, user, '更新考试', `${exam.name} · 状态 ${exam.status}`);
await database.updateExam(exam, log);
return sendJson(response, 200, { ok: true, exam });
}
if (request.method === 'GET' && pathname === '/api/admin/notices') return sendJson(response, 200, { ok: true, notices: db.notices.sort((a, b) => new Date(b.publishAt || b.createdAt) - new Date(a.publishAt || a.createdAt)) });
@@ -469,9 +450,8 @@ async function handleAdmin(request, response, pathname) {
const content = cleanText(body.content, 5000);
if (!title || !content) return sendError(response, 400, '通知标题和正文不能为空');
const notice = { id: uid('notice'), title, summary: cleanText(body.summary, 260) || content.slice(0, 80), content, category: cleanText(body.category, 30) || '通知公告', pinned: Boolean(body.pinned), status: body.status === 'draft' ? 'draft' : 'published', publishAt: body.status === 'draft' ? null : nowIso(), createdAt: nowIso(), author: user.displayName };
db.notices.push(notice);
logAction(db, user, notice.status === 'published' ? '发布通知' : '保存通知草稿', notice.title);
await writeDb(db);
const log = logAction(db, user, notice.status === 'published' ? '发布通知' : '保存通知草稿', notice.title);
await database.createNotice(notice, log);
return sendJson(response, 201, { ok: true, notice });
}
const noticeMatch = pathname.match(/^\/api\/admin\/notices\/([^/]+)$/);
@@ -485,8 +465,8 @@ async function handleAdmin(request, response, pathname) {
notice.status = body.status;
if (body.status === 'published' && !notice.publishAt) notice.publishAt = nowIso();
}
logAction(db, user, '更新通知', `${notice.title} · ${notice.status}`);
await writeDb(db);
const log = logAction(db, user, '更新通知', `${notice.title} · ${notice.status}`);
await database.updateNotice(notice, log);
return sendJson(response, 200, { ok: true, notice });
}
if (request.method === 'GET' && pathname === '/api/admin/results') {
@@ -508,6 +488,7 @@ async function handleAdmin(request, response, pathname) {
const score = Number(body.score);
if (!Number.isFinite(score) || score < 0 || score > 150) return sendError(response, 400, '成绩必须在 0—150 之间');
let result = db.results.find(item => item.registrationId === registration.id && item.subjectId === body.subjectId);
const isNew = !result;
if (!result) {
result = { id: uid('result'), registrationId: registration.id, subjectId: body.subjectId };
db.results.push(result);
@@ -515,8 +496,8 @@ async function handleAdmin(request, response, pathname) {
Object.assign(result, { score, grade: cleanText(body.grade, 10) || (score >= 135 ? 'A+' : score >= 120 ? 'A' : score >= 105 ? 'B+' : score >= 90 ? 'B' : score >= 60 ? 'C' : 'D'), published: Boolean(body.published), updatedAt: nowIso(), publishedAt: body.published ? nowIso() : null });
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
const subject = exam.subjects.find(item => item.id === body.subjectId);
logAction(db, user, body.published ? '发布成绩' : '保存成绩', `${profile?.name} · ${subject?.name} · ${score}`);
await writeDb(db);
const log = logAction(db, user, body.published ? '发布成绩' : '保存成绩', `${profile?.name} · ${subject?.name} · ${score}`);
await database.saveResult(result, isNew, log);
return sendJson(response, 200, { ok: true, result });
}
return sendError(response, 404, '管理功能接口不存在');
@@ -554,7 +535,18 @@ const server = createServer(async (request, response) => {
}
});
await ensureDatabase();
server.listen(port, '127.0.0.1', () => {
console.log(`衡准考试信息管理系统:http://127.0.0.1:${port}`);
server.listen(port, host, () => {
console.log(`衡准考试信息管理系统:http://${host}:${port}`);
console.log(`数据库:${database.client}${database.location}`);
});
async function shutdown(signal) {
console.log(`收到 ${signal},正在关闭服务...`);
server.close(async () => {
await database.close();
process.exit(0);
});
}
process.once('SIGINT', () => shutdown('SIGINT'));
process.once('SIGTERM', () => shutdown('SIGTERM'));