在线考试系统架构
在线考试系统需要处理高并发场景,同时保证数据安全和考试公平性。系统架构通常分为以下几个核心模块:
| 模块 | 核心功能 | 技术要点 |
|---|---|---|
| 题库管理 | 试题CRUD、分类、标签 | 富文本编辑、LaTeX公式 |
| 组卷系统 | 手动/智能组卷、抽题 | 随机算法、约束求解 |
| 考试引擎 | 计时、答题、提交 | 实时同步、断点续答 |
| 防作弊 | 切屏检测、人脸识别 | 前端监控、AI识别 |
| 评分系统 | 自动批改、人工复核 | 规则引擎、NLP |
| 数据分析 | 成绩统计、试题分析 | 统计学、可视化 |
题库数据模型
试题结构设计
// 基础试题接口
interface Question {
id: string
type: QuestionType
content: RichText // 题干(支持图片、公式)
difficulty: 1 | 2 | 3 | 4 | 5 // 难度等级
points: number // 分值
subjectId: string // 所属科目
knowledgePoints: string[] // 关联知识点
tags: string[] // 标签
explanation?: RichText // 答案解析
metadata: QuestionMetadata
}
type QuestionType =
| 'single-choice' // 单选题
| 'multiple-choice' // 多选题
| 'true-false' // 判断题
| 'fill-blank' // 填空题
| 'short-answer' // 简答题
| 'essay' // 论述题
| 'coding' // 编程题
// 选择题特有字段
interface ChoiceQuestion extends Question {
type: 'single-choice' | 'multiple-choice'
options: Option[]
correctAnswers: string[] // 正确选项ID
shuffleOptions: boolean // 是否打乱选项顺序
}
// 填空题特有字段
interface FillBlankQuestion extends Question {
type: 'fill-blank'
blanks: Blank[] // 多个空
}
interface Blank {
id: string
acceptedAnswers: string[] // 可接受的答案列表
caseSensitive: boolean // 是否区分大小写
partialCredit: boolean // 是否给部分分
}
题目难度与区分度
interface QuestionMetadata {
createdAt: Date
updatedAt: Date
usageCount: number // 被使用次数
averageScore: number // 平均得分率
discrimination: number // 区分度 (0-1)
lastUsedAt?: Date
}
// 根据实际考试数据更新试题统计
async function updateQuestionStats(
questionId: string,
responses: QuestionResponse[]
): Promise<void> {
const scores = responses.map(r => r.scoreRatio) // 0-1
// 计算平均得分率
const averageScore = mean(scores)
// 计算区分度(高分组与低分组得分率差异)
const sorted = scores.sort((a, b) => b - a)
const topGroup = sorted.slice(0, Math.floor(sorted.length * 0.27))
const bottomGroup = sorted.slice(-Math.floor(sorted.length * 0.27))
const discrimination = mean(topGroup) - mean(bottomGroup)
await db.questions.update({
where: { id: questionId },
data: {
metadata: {
usageCount: { increment: 1 },
averageScore,
discrimination
}
}
})
}
关键说明: 区分度指标反映试题区分高低水平考生的能力。区分度大于0.3的题目通常被认为是优质题目;低于0.2的题目可能需要修改或淘汰。
智能组卷算法
约束条件定义
interface PaperConfig {
title: string
duration: number // 考试时长(分钟)
totalPoints: number // 总分
// 题型分布
sections: SectionConfig[]
// 约束条件
constraints: {
difficultyRange: [number, number] // 难度范围
targetDifficulty: number // 目标平均难度
knowledgePointCoverage: string[] // 必须覆盖的知识点
excludeQuestions: string[] // 排除的题目
}
}
interface SectionConfig {
name: string
questionType: QuestionType
count: number
pointsPerQuestion: number
}
随机抽题实现
async function generatePaper(
config: PaperConfig
): Promise<GeneratedPaper> {
const sections: GeneratedSection[] = []
const usedQuestionIds: Set<string> = new Set()
for (const sectionConfig of config.sections) {
const candidates = await db.questions.findMany({
where: {
type: sectionConfig.questionType,
subjectId: config.subjectId,
difficulty: {
gte: config.constraints.difficultyRange[0],
lte: config.constraints.difficultyRange[1]
},
id: { notIn: [...usedQuestionIds, ...config.constraints.excludeQuestions] }
}
})
// 使用加权随机选择,优先选择区分度高的题目
const selected = weightedRandomSelect(
candidates,
sectionConfig.count,
(q) => q.metadata.discrimination * 0.6 + (1 - Math.abs(q.difficulty - config.constraints.targetDifficulty) / 5) * 0.4
)
selected.forEach(q => usedQuestionIds.add(q.id))
sections.push({
name: sectionConfig.name,
questions: selected,
totalPoints: sectionConfig.count * sectionConfig.pointsPerQuestion
})
}
// 验证知识点覆盖
const coveredPoints = new Set(
sections.flatMap(s => s.questions.flatMap(q => q.knowledgePoints))
)
const missingPoints = config.constraints.knowledgePointCoverage.filter(
p => !coveredPoints.has(p)
)
if (missingPoints.length > 0) {
console.warn('未覆盖的知识点:', missingPoints)
}
return { sections, config }
}
考试引擎设计
考试状态管理
interface ExamSession {
id: string
examId: string
studentId: string
startTime: Date
endTime?: Date
status: ExamStatus
answers: Record<string, Answer> // questionId -> answer
events: ExamEvent[] // 考试过程事件
remainingTime: number // 剩余时间(秒)
}
type ExamStatus =
| 'not-started'
| 'in-progress'
| 'paused' // 断点暂停
| 'submitted'
| 'auto-submitted' // 超时自动提交
| 'grading'
| 'graded'
// 考试事件记录
interface ExamEvent {
type: 'start' | 'answer' | 'switch-question' | 'blur' | 'focus' | 'submit'
timestamp: Date
data?: any
}
答案自动保存
// 使用防抖保存答案
const debouncedSave = useDebounceFn(async (answer: Answer) => {
await saveAnswer(sessionId, answer)
}, 1000)
// 监听答案变化
watch(currentAnswer, (newAnswer) => {
// 本地立即更新
localAnswers.value[currentQuestionId.value] = newAnswer
// 服务器延迟保存
debouncedSave(newAnswer)
})
// 定期强制同步
useIntervalFn(async () => {
await syncAllAnswers(sessionId, localAnswers.value)
}, 30000) // 每30秒同步一次
防作弊机制
前端监控
// 切屏检测
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
recordEvent({
type: 'blur',
timestamp: new Date(),
data: { reason: 'visibility-change' }
})
blurCount.value++
if (blurCount.value >= config.maxBlurCount) {
showWarning('多次切换窗口,系统已记录')
}
}
})
// 禁用复制粘贴
document.addEventListener('copy', (e) => {
e.preventDefault()
recordEvent({ type: 'copy-attempt', timestamp: new Date() })
})
// 全屏要求
async function enterFullscreen() {
try {
await document.documentElement.requestFullscreen()
isFullscreen.value = true
} catch (error) {
showWarning('请允许全屏模式以继续考试')
}
}
服务端验证
// 验证提交时间合理性
function validateSubmissionTime(session: ExamSession): ValidationResult {
const errors: string[] = []
const actualDuration = (session.endTime - session.startTime) / 1000 / 60
const expectedDuration = session.exam.duration
// 提交时间过短(可能作弊)
if (actualDuration < expectedDuration * 0.1) {
errors.push('答题时间异常短')
}
// 检查答题间隔是否合理
const answerIntervals = calculateAnswerIntervals(session.events)
const suspiciouslyFast = answerIntervals.filter(i => i < 5) // 小于5秒
if (suspiciouslyFast.length > session.exam.questionCount * 0.3) {
errors.push('多道题目答题速度异常')
}
return { valid: errors.length === 0, errors }
}
自动评分系统
客观题评分
function gradeObjectiveQuestion(
question: Question,
answer: Answer
): GradeResult {
switch (question.type) {
case 'single-choice':
return {
score: answer.selected === question.correctAnswers[0]
? question.points
: 0,
feedback: answer.selected === question.correctAnswers[0]
? '正确'
: `正确答案: ${question.correctAnswers[0]}`
}
case 'multiple-choice':
const selected = new Set(answer.selected || [])
const correct = new Set(question.correctAnswers)
// 完全正确
if (setsEqual(selected, correct)) {
return { score: question.points, feedback: '正确' }
}
// 部分正确(选对但不全)
const intersection = setIntersection(selected, correct)
const hasWrong = selected.size > intersection.size
if (!hasWrong && intersection.size > 0) {
return {
score: question.points * (intersection.size / correct.size) * 0.5,
feedback: '部分正确,答案不完整'
}
}
return { score: 0, feedback: `正确答案: ${[...correct].join(', ')}` }
case 'fill-blank':
return gradeFillBlank(question as FillBlankQuestion, answer)
}
}
编程题自动判题
interface CodeJudgeResult {
status: 'accepted' | 'wrong-answer' | 'time-limit' | 'runtime-error' | 'compile-error'
score: number
passedTestCases: number
totalTestCases: number
details: TestCaseResult[]
}
async function judgeCodingQuestion(
question: CodingQuestion,
code: string,
language: string
): Promise<CodeJudgeResult> {
// 1. 编译代码
const compileResult = await compile(code, language)
if (!compileResult.success) {
return {
status: 'compile-error',
score: 0,
passedTestCases: 0,
totalTestCases: question.testCases.length,
details: [{ error: compileResult.error }]
}
}
// 2. 运行测试用例
const results: TestCaseResult[] = []
for (const testCase of question.testCases) {
const result = await runWithTimeout(
compileResult.executable,
testCase.input,
question.timeLimit
)
results.push({
input: testCase.isHidden ? '[隐藏]' : testCase.input,
expectedOutput: testCase.isHidden ? '[隐藏]' : testCase.expectedOutput,
actualOutput: result.output,
passed: result.output.trim() === testCase.expectedOutput.trim(),
executionTime: result.time
})
}
const passed = results.filter(r => r.passed).length
return {
status: passed === results.length ? 'accepted' : 'wrong-answer',
score: (passed / results.length) * question.points,
passedTestCases: passed,
totalTestCases: results.length,
details: results
}
}
考试数据分析
成绩统计报告
| 分析维度 | 指标 | 应用价值 |
|---|---|---|
| 分数分布 | 平均分、标准差、中位数 | 评估整体难度 |
| 题目分析 | 正确率、区分度 | 优化题库质量 |
| 知识点分析 | 各知识点得分率 | 发现教学薄弱点 |
| 时间分析 | 各题耗时统计 | 评估题目难度 |
async function generateExamReport(examId: string): Promise<ExamReport> {
const sessions = await db.examSessions.findMany({
where: { examId, status: 'graded' }
})
const scores = sessions.map(s => s.totalScore)
return {
examId,
participantCount: sessions.length,
scoreStats: {
average: mean(scores),
median: median(scores),
stdDev: standardDeviation(scores),
min: Math.min(...scores),
max: Math.max(...scores),
passRate: scores.filter(s => s >= 60).length / scores.length
},
distribution: calculateDistribution(scores),
questionAnalysis: await analyzeQuestions(examId, sessions)
}
}
最佳实践
- 题库建设:持续积累高质量题目,定期淘汰低区分度题目
- 模拟考试:正式考试前进行技术演练
- 应急预案:准备网络故障、服务器宕机的处理方案
- 数据备份:考试数据实时备份,确保不丢失
- 公平公正:防作弊措施与人性化体验平衡
通过以上设计,可以构建一个安全、可靠、智能的在线考试系统。


