本文目录导读:

搭建一个在线考试系统是一个典型的Web开发项目,涉及用户管理、题库管理、试卷生成、考试监控、自动评分等核心模块,下面我从系统架构、数据库设计、核心功能实现、安全性考虑四个维度,详细说明如何用PHP完成这个项目。
技术选型与系统架构
| 层次 | 推荐技术 | 说明 |
|---|---|---|
| 后端语言 | PHP 8.x | 推荐使用Laravel(企业级)或ThinkPHP(国内常用)框架 |
| 数据库 | MySQL 8.0 + Redis | MySQL存储核心数据,Redis实现防重复提交、缓存 |
| 前端框架 | Vue.js / Layui | 后台管理用Layui做快速搭建,考试界面用Vue实现交互 |
| 辅助工具 | Composer, Git, Nginx | 包管理、版本控制、高性能Web服务器 |
系统模块划分:
- 用户端:注册/登录、考试列表、参加考试、查看成绩
- 教师端:题库管理、试卷组卷、考试发布、成绩统计
- 管理员端:用户管理、系统设置、数据备份
核心数据库设计(MySQL)
以下是几个最核心的数据库表设计示例:
-- 1. 题库表(支持单选/多选/判断/填空) CREATE TABLE `questions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `type` tinyint(4) NOT NULL COMMENT '1:单选 2:多选 3:判断 4:填空', `content` text NOT NULL COMMENT '题目内容(支持HTML)', `options` json DEFAULT NULL COMMENT '选项JSON,如["A.北京","B.上海"]', `answer` text NOT NULL COMMENT '正确答案', `score` decimal(5,2) DEFAULT '1.00' COMMENT '分值', `category_id` int(11) DEFAULT NULL COMMENT '分类ID', `difficulty` tinyint(4) DEFAULT '1' COMMENT '难度1-5', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `category_id` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2. 试卷表 CREATE TABLE `exams` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, varchar(255) NOT NULL COMMENT '试卷名称', `duration` int(11) NOT NULL COMMENT '考试时长(分钟)', `start_time` datetime NOT NULL COMMENT '开始时间', `end_time` datetime NOT NULL COMMENT '结束时间', `pass_score` decimal(5,2) DEFAULT NULL COMMENT '及格分数', `is_random` tinyint(1) DEFAULT '0' COMMENT '是否随机抽题', `status` tinyint(4) DEFAULT '0' COMMENT '0未发布 1进行中 2已结束', `created_by` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 3. 试卷题目关联表(支持固定+随机混合组卷) CREATE TABLE `exam_questions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `exam_id` int(11) NOT NULL, `question_id` int(11) NOT NULL, `sort_order` int(11) DEFAULT '0' COMMENT '排序', `score` decimal(5,2) DEFAULT NULL COMMENT '该题在此试卷中的分值', PRIMARY KEY (`id`), UNIQUE KEY `exam_question` (`exam_id`,`question_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 4. 考生答题记录表 CREATE TABLE `exam_answers` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `exam_id` int(11) NOT NULL, `question_id` int(11) NOT NULL, `answer` text COMMENT '考生答案', `is_correct` tinyint(1) DEFAULT NULL COMMENT '是否正确', `score` decimal(5,2) DEFAULT '0.00' COMMENT '得分', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `user_exam_question` (`user_id`,`exam_id`,`question_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心功能实现代码(Laravel示例)
自动组卷(随机抽题与固定题混合)
// ExamService.php
public function generatePaper($examId)
{
$exam = Exam::findOrFail($examId);
// 如果已是随机组卷模式,删除原有题目关联
if ($exam->is_random) {
ExamQuestion::where('exam_id', $examId)->delete();
// 从题库按分类和难度随机抽取
$questions = Question::where('category_id', $exam->category_id)
->where('difficulty', $exam->difficulty)
->inRandomOrder()
->limit($exam->question_count)
->get();
foreach ($questions as $index => $q) {
ExamQuestion::create([
'exam_id' => $examId,
'question_id' => $q->id,
'sort_order' => $index + 1,
'score' => $q->score
]);
}
}
// 固定组卷逻辑:前端拖拽排序后提交ID列表
}
防作弊:考试开始与提交
// ExamController.php
public function startExam($examId)
{
$user = auth()->user();
$exam = Exam::findOrFail($examId);
// 检查考试时间
if (now() < $exam->start_time || now() > $exam->end_time) {
throw new \Exception('考试不在有效时间内');
}
// 检查是否已开始(防重复进入)
$attempt = ExamAttempt::firstOrCreate([
'user_id' => $user->id,
'exam_id' => $examId
], [
'started_at' => now(),
'status' => 'in_progress'
]);
// 生成唯一token防作弊
$attempt->token = Str::random(32);
$attempt->save();
// 把token放入Redis,设置过期时间为考试时长
Redis::setex("exam:{$examId}:user:{$user->id}:token", $exam->duration * 60, $attempt->token);
// 打乱题目顺序(随机模式)
$questions = $exam->questions()->orderBy('sort_order')->get();
if ($exam->random_questions) {
$questions = $questions->shuffle();
}
return view('exam.take', compact('exam', 'questions', 'attempt'));
}
public function submitExam(Request $request, $examId)
{
$user = auth()->user();
$attempt = ExamAttempt::where('user_id', $user->id)
->where('exam_id', $examId)
->where('status', 'in_progress')
->firstOrFail();
// 验证防作弊token
$token = Redis::get("exam:{$examId}:user:{$user->id}:token");
if ($request->token !== $token) {
return back()->with('error', '非法请求');
}
DB::beginTransaction();
try {
// 批量插入答案
foreach ($request->answers as $questionId => $answer) {
ExamAnswer::updateOrCreate([
'user_id' => $user->id,
'exam_id' => $examId,
'question_id' => $questionId
], [
'answer' => is_array($answer) ? json_encode($answer) : $answer
]);
}
// 自动评分
$totalScore = 0;
$answers = ExamAnswer::where('user_id', $user->id)
->where('exam_id', $examId)
->get();
foreach ($answers as $ans) {
$question = Question::find($ans->question_id);
$correct = $this->judgeAnswer($question, $ans->answer);
$ans->is_correct = $correct;
$ans->score = $correct ? $question->score : 0;
$ans->save();
$totalScore += $ans->score;
}
// 记录总成绩
$attempt->total_score = $totalScore;
$attempt->status = 'completed';
$attempt->completed_at = now();
$attempt->save();
// 清理Redis token
Redis::del("exam:{$examId}:user:{$user->id}:token");
DB::commit();
return redirect()->route('exam.result', $attempt->id);
} catch (\Exception $e) {
DB::rollBack();
Log::error('提交考试失败', ['error' => $e->getMessage()]);
return back()->with('error', '提交失败,请重试');
}
}
自动评分核心逻辑
private function judgeAnswer($question, $userAnswer)
{
switch ($question->type) {
case 1: // 单选
return $userAnswer === $question->answer;
case 2: // 多选(答案用逗号分隔,顺序无关)
$correct = explode(',', $question->answer);
$user = is_string($userAnswer) ? explode(',', $userAnswer) : $userAnswer;
sort($correct);
sort($user);
return $correct === $user;
case 3: // 判断
return (int)$userAnswer === (int)$question->answer;
case 4: // 填空(支持模糊匹配:去除空格、大小写)
return strtolower(trim($userAnswer)) === strtolower(trim($question->answer));
default:
return false;
}
}
安全性关键措施
- 防止重复提交:使用Redis记录每个考生的考试状态token,提交后立即销毁。
- 考试中途断网处理:每30秒自动保存答案到数据库(Ajax异步保存)。
- 防作弊机制:
- 限制IP同一账号同时登录
- 禁止复制粘贴(JavaScript
oncopy事件) - 全屏模式启动考试(
Fullscreen API) - 检测切屏次数(监听
visibilitychange事件)
- SQL注入防护:使用ORM(Eloquent)查询,避免原生SQL拼接。
- XSS防护输入时做HTML转义,输出时使用 自动转义。
部署与优化建议
-
PHP-FPM与Nginx配置:
server { listen 80; server_name exam.example.com; root /var/www/exam/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } -
性能优化:
- 使用MySQL索引(
user_id, exam_id联合索引) - 考试中实时评量的题目用Redis缓存答案,考试结束后统一落库
- 静态资源(CSS/JS)使用CDN加速
- 使用MySQL索引(
-
定时任务(Laravel Scheduler):
// 每天凌晨清理过期考试记录 $schedule->command('exam:cleanup-expired')->daily(); // 每分钟检查考试状态并自动归档 $schedule->command('exam:archive-finished')->everyMinute();
推荐学习资源与开源项目
| 类型 | 名称 | 说明 |
|---|---|---|
| 开源项目 | LaravelExam(GitHub) | 基于Laravel的完整在线考试系统 |
| 开源项目 | ExamOnline(Gitee) | 基于ThinkPHP6,支持多题型 |
| 课程 | Laracast《Building a Quiz App》 | 英文,基础实现 |
| 书籍 | 《PHP与MySQL高性能应用开发》 | 中文,架构设计参考 |
总结建议
对于初学者,建议按以下步骤迭代:
- 第一版:只用PHP原生+MySQL,实现手动组卷、单选判断自动改卷
- 第二版:引入Laravel框架,增加用户角色、多题型、防作弊
- 第三版:加入Redis缓存、分布式部署、考试监控大屏
如果你需要一个现成的快速搭建方案,可以告诉我你的具体需求(如支持多少人同时在线、需要哪些题型),我可以提供一份可运行的简化版源码架构。