免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于PHP+Vue的前后端分离在线考试系统设计与实现

基于PHP+Vue的前后端分离在线考试系统设计与实现 每年毕业季都能看到一大批做“在线考试系统”的同学要么是自选题目要么是老师指定的课程设计。基于PHPVue的课程在线考试系统算是我见过“性价比”很高的一类毕设选题后端用PHP做接口和业务逻辑前端用Vue做页面交互既有前后端分离的味道又不会把复杂度顶到Java微服务那种程度短期内能跑通、能讲清楚。从实际功能来看这套系统主要解决“老师出题、学生考试、系统判分、成绩统计”这条完整链路。老师可以管理课程和题库创建试卷并发布考试学生登录后参加考试提交后系统自动批改客观题需要人工评阅的主观题再由老师打分最终成绩以列表和图表的形式呈现。对毕设而言源码之外最重要的就是文档、讲解、调试运行这类“配套服务”因为答辩时老师不只看功能还会问设计原因和实现细节。这篇文章我就按一个真实项目的开发流程把整个系统的设计思路、数据库结构、后端接口、Vue前端、部署调试和答辩要点拆开讲一遍尽量给到可以直接参考的方案。适合正在做同类毕设的同学也适合想用PHPVue快速搭一套考试平台、后面再接业务需求的人。1. 项目整体设计与模块划分1.1 为什么选PHPVue而不选别的现在许多教程一上来就推Spring Boot但我个人觉得对于“课程在线考试系统”这种规模的毕设题目PHP反而更合适。原因不复杂PHP对MySQL的操作天然友好、部署成本低随便一台服务器装好PHP和MySQL就能跑ThinkPHP 6这类框架自带ORM、中间件、路由开发效率很高学生从“听懂”到“写出可用代码”的路径更短。Vue负责前端是因为考试页面需要大量交互倒计时、选项切换、题目跳转、答题卡状态、成绩图表。用原生jQuery写会非常痛苦而Vue的组件化能把“试题卡片”“倒计时条”“答题卡导航”拆成独立组件代码清晰后期改样式和加功能都方便。打个比方PHP就像一个坐镇后厨的老师傅快速处理登录、组卷、判分、存库这些“脏活累活”Vue则像前厅的接待员把用户操作做成流畅的界面不需要每次点按钮都刷新页面。两者通过JSON接口通信结构简单也符合当前Web开发的主流习惯。1.2 系统功能模块拆解这套系统按角色划分主要有三类用户管理员、教师、学生。管理员的职责偏“系统配置”管理教师账号、维护课程信息、查看整体数据。教师的职责是业务核心创建课程、维护题库、设计试卷、发布考试、人工评阅主观题、查看成绩统计。学生的职责最简单浏览可参加的考试、在线作答、查看成绩和错题。从业务流程上看一次完整考试大概走这几步教师创建课程并添加学生选课关系。教师在该课程下录入选择题、判断题、填空题、简答题等题目。教师设置考试名称、考试时间、考试时长、总分并配置“单选题X题每题Y分”这类组卷规则。系统根据规则从题库随机抽题生成一份试卷。学生在规定时间进入考试页面答题并提交。系统自动判分客观题主观题进入待评阅状态。教师人工打分后系统汇总成绩并生成统计图表。这个流程看起来不复杂但每个环节都有可深挖的点。例如“随机组卷”要保证题量正确、总分匹配“自动判分”要考虑多选题漏选、错选、判断题答案匹配“考试提交”要考虑重复提交和并发问题。这些点正好也是答辩时展示设计能力的关键。1.3 技术栈与目录结构规划技术选型上我建议这样搭配后端PHP 7.4或8.0框架用ThinkPHP 6数据库用MySQL 5.7或8.0。前端Vue 3 Vite Vue Router PiniaUI组件库用Element PlusHTTP请求用Axios图表用ECharts。之所以选ThinkPHP一是国内资料多、中文文档齐全出了问题搜得到二是它自带Db查询构造器和模型关联写后端业务比手写PDO省很多事。如果不想用框架原生PHP也能做但毕设项目通常时间紧框架能帮你省下造轮子的时间。目录结构可以这样规划exam-system/ ├── server/ // PHP后端 │ ├── app/ │ │ ├── controller/ // 控制器 │ │ ├── model/ // 模型 │ │ ├── middleware/ // 中间件 │ │ └── service/ // 业务逻辑 │ ├── config/ // 配置文件 │ ├── public/ // 入口文件 │ └── route/ // 路由定义 ├── web/ // Vue前端 │ ├── src/ │ │ ├── api/ // 接口请求封装 │ │ ├── views/ // 页面 │ │ ├── components/ // 公共组件 │ │ ├── router/ // 路由配置 │ │ └── store/ // 全局状态 └── sql/ └── exam_system.sql // 数据库初始化脚本前后端分离的核心是“接口约定先行”。前端不直接操作数据库所有数据都通过HTTP接口获取后端不关心页面长什么样只负责校验权限、处理业务、返回JSON。这样的好处是你可以先调试后端所有接口再写前端页面两边职责清楚出问题也好定位。2. 数据库设计与前后端交互约定2.1 核心数据表设计数据库设计是这类系统最容易出问题的地方。设计得好后面写业务逻辑很顺畅设计得不好可能出现“一张表塞太多字段”“删改数据互相冲突”的情况。我用过的核心表如下表名说明关键字段user用户表id, username, password, real_name, role, course_idscourse课程表id, course_name, teacher_id, create_timecourse_student选课关系表id, course_id, student_idquestion题库表id, course_id, type, content, options, answer, score, difficultyexam考试表id, course_id, title, start_time, end_time, duration, total_score, statusexam_question试卷题目表id, exam_id, question_id, sort_order, scoreexam_record考试记录表id, exam_id, user_id, start_time, submit_time, score, statusanswer_record答题明细表id, record_id, question_id, user_answer, is_correct, score以题库表为例type字段我习惯用1单选、2多选、3判断、4填空、5简答。options字段存JSON字符串比如[A:选项A,B:选项B]answer字段存标准答案多选题存[A,C]这样的JSON。填空和简答也统一用字符串处理便于自动判分时按类型走不同逻辑。考试表里的status用来标识考试状态0未开始、1进行中、2已结束。这个状态不能只靠前端显示后端在考题接口和提交接口里都要再次校验防止学生在考试开始前偷偷调接口拿到题目。考试记录表是整个系统的核心建立唯一索引(exam_id, user_id)保证同一个学生对同一场考试只能有一条正式记录。这样做不只是为了查成绩方便更重要的是从数据库层面挡住重复提交。2.2 接口协议与状态码规范前后端分离项目的痛点往往是“各自为战”。前端写完了等后端后端写完了等前端最后联调时才发现字段对不上。提前定好接口规范能省掉一半调试时间。我建议所有接口统一走/api前缀返回格式固定为{ code: 0, msg: success, data: {} }code为0表示成功非0表示业务错误401表示未登录或Token过期403表示没有权限500表示服务器异常。前端Axios响应拦截器里统一判断code减少每个页面单独处理错误。几个核心接口可以这样列POST /api/auth/login 用户登录 GET /api/teacher/courses 教师课程列表 POST /api/teacher/questions 新增试题 GET /api/teacher/papers/{examId} 查看试卷内容 POST /api/teacher/exams 创建考试 GET /api/student/exams 学生可参加的考试 GET /api/student/exams/{examId}/questions 获取考试题目 POST /api/student/exams/{examId}/submit 提交考试 GET /api/student/records 我的考试成绩接口设计上我特别提醒一点题目接口和试卷查看接口要分开。教师查看试卷时可以看到标准答案而学生进入考试时只返回题目和选项绝对不能在返回数据里带answer字段否则用浏览器开发者工具就能直接看到答案这是很多新手容易忽略的泄露点。2.3 登录鉴权与权限控制方案毕设项目通常不需要做得像企业级那么重但登录鉴权是基本盘中盘。我推荐用JWT而不是Session因为前后端分离后前端可能部署在另一个域名或端口Session管理跨域麻烦JWT则天然无状态。JWT的生成逻辑放在后端登录成功后返回一段加密令牌前端存在localStorage之后每次请求都在Authorization头里带上。PHP端可以用firebase/php-jwt这个库安装命令composer require firebase/php-jwt生成Token的核心代码大致是这样use Firebase\JWT\JWT; public function createToken($userId, $role) { $key your-secret-key; $payload [ iss exam-system, sub $userId, role $role, iat time(), exp time() 7200 ]; return JWT::encode($payload, $key, HS256); }后端再写一个中间件从请求头中取出Token解密并校验有效期然后把用户信息塞进请求上下文。路由分组时只对需要登录的接口挂载中间件公共接口比如登录接口直接放行。权限控制上角色字段role就够了管理员是admin教师是teacher学生是student。后端每个写接口都判断当前用户角色不允许学生调新增试题的接口。这种控制看起来简单却能让系统安全度上一个台阶。3. 后端核心功能实现3.1 PHP环境搭建与项目初始化后面的代码示例我都用ThinkPHP 6来写因为它的目录规范清楚适合做毕设项目讲解。如果你本机已经装好PHP 7.4以上和Composer可以用下面命令创建项目composer create-project topthink/think6.0.* exam-api创建完成后进入项目目录配置.env数据库连接APP_DEBUG true [DATABASE] TYPE mysql HOSTNAME 127.0.0.1 DATABASE exam_system USERNAME root PASSWORD 123456 HOSTPORT 3306 CHARSET utf8mb4 PREFIX ex_然后再导入数据库脚本。初始化完成后先跑一个简单接口验证环境namespace app\controller; use think\facade\Db; class Index { public function index() { return json([code 0, data Db::name(user)-count()]); } }这里有个容易踩的坑public目录下的伪静态配置。ThinkPHP默认入口是public/index.php使用Nginx时如果不做try_files重写访问/api/xxx会直接变成文件找不到。开发阶段用php think run启动内置服务器没问题但上线前一定要把Nginx的location /配置好。3.2 用户登录与JWT实现登录接口是最基础也最关键的部分。首先密码不能明文存储注册时用password_hash生成哈希登录时用password_verify校验。登录控制器的核心逻辑public function login() { $username input(post.username); $password input(post.password); $user Db::name(user) -where(username, $username) -find(); if (!$user || !password_verify($password, $user[password])) { return json([code 400, msg 用户名或密码错误]); } $token $this-createToken($user[id], $user[role]); unset($user[password]); return json([ code 0, data [ token $token, user $user ] ]); }注意我返回用户信息时特意unset掉密码字段。密码哈希即使泄露也不能被直接还原但能少暴露就少暴露。后端中间件解析Token的逻辑也不复杂public function handle($request, \Closure $next) { $authHeader $request-header(Authorization); if (!$authHeader || !str_starts_with($authHeader, Bearer )) { return json([code 401, msg 未登录], 401); } $token substr($authHeader, 7); try { $decoded JWT::decode($token, $key, [HS256]); $request-userId $decoded-sub; $request-userRole $decoded-role; } catch (\Exception $e) { return json([code 401, msg Token无效或已过期], 401); } return $next($request); }这里有个细节JWT的exp过期时间建议设置成2小时。考试系统里学生考试通常不止一小时时间太短会导致考试中途Token失效时间太长又有安全隐患。更好的做法是把Token过期时间设为8小时同时登录后把用户信息存到Pinia里每次请求前检查本地过期时间快到期了再自动重新登录。3.3 试题管理和组卷逻辑实现题库管理是教师端的核心功能本质上是对question表的增删改查。但“新增试题”有几个校验点content题目内容不能为空。选择题必须有选项且标准答案必须包含在选项中。score分值必须大于0。判断题答案只接受对/错或true/false。同一课程下允许相同题目但最好用course_id content做去重提示。组卷逻辑是这套系统的亮点也是答辩常问的地方。最简单的随机组卷方案是前端或后端指定“单选题10题、每题3分多选题5题、每题4分判断题10题、每题2分简答题2题、每题10分”后端根据配置从题库中随机抽取。关键代码思路public function generatePaper($examId) { $exam Db::name(exam)-where(id, $examId)-find(); $rule json_decode($exam[question_rules], true); $paper []; foreach ($rule as $item) { $questions Db::name(question) -where(course_id, $exam[course_id]) -where(type, $item[type]) -orderRaw(rand()) -limit($item[count]) -select() -toArray(); foreach ($questions as $q) { $paper[] [ exam_id $examId, question_id $q[id], sort_order count($paper) 1, score $item[score] ]; } } Db::name(exam_question)-where(exam_id, $examId)-delete(); Db::name(exam_question)-insertAll($paper); }orderRaw(rand())在数据量小的时候很好用但如果题库有几万道题这种方式性能很差因为数据库要对所有行排序。毕设规模完全够用但如果想体现一点优化意识可以在答辩时补充说“生产环境我会先用WHERE id (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM question)))这种方式随机取ID再走主键查询避免全表排序。”这句话虽然简单但面试官或答辩老师听了会觉得你有思考。组卷完成后exam_question表会生成一份“固定试卷”。为什么要把试卷题目固定下来而不是每次考试动态生成因为学生A和学生B如果进入考试的时间不同动态生成会导致题目不同成绩无法横向比较而且教师也需要在发布前预览试卷、确认题目难度和分值。固定试卷同时还能保证考试中如果学生刷新页面再次拿到的题目和第一次一致。3.4 考试提交与自动判分实现学生提交答案是最容易出“并发问题”的地方。设想一个场景学生答完题后手快连续点了两次提交按钮如果后端没有做防重处理就会生成两条考试记录成绩统计就会错乱。我的做法是提交接口进入后先检查exam_record表里是否已经有该学生该考试的记录有就直接返回“请勿重复提交”。再用数据库事务包裹整个判分和写记录的过程。大概逻辑Db::transaction(function () use ($recordId, $answers) { $record Db::name(exam_record) -where(id, $recordId) -lock(true) -find(); if ($record[status] ! 0) { throw new \Exception(请勿重复提交); } $totalScore 0; foreach ($answers as $answer) { $q Db::name(question)-where(id, $answer[question_id])-find(); $isCorrect 0; $score 0; if ($q[type] ! 5) { // 非简答题自动判分 $isCorrect $this-checkAnswer($q, $answer[content]); if ($isCorrect) { $score $answer[score] ?? $q[score]; } $totalScore $score; } Db::name(answer_record)-insert([ record_id $recordId, question_id $q[id], user_answer $answer[content], is_correct $isCorrect, score $score ]); } Db::name(exam_record)-where(id, $recordId)-update([ score $totalScore, submit_time date(Y-m-d H:i:s), status 1 ]); });自动判分时单选题和判断题直接比较答案字符串多选题需要把标准答案和学生答案都转成数组排序后再比较填空题我建议用严格匹配最多支持多个标准答案并存比如json_encode([PHP, Hypertext Preprocessor])学生答案只要命中一个就算对。这里需要提示一个非常现实的问题很多毕设项目在自动判分时把标准答案“明文”写在接口返回里导致学生看一眼网络请求就能得到正确答案。正确做法是学生获取试题的接口永远不返回answer字段判分只在后端执行。这是安全和业务的双重底线。4. 前端Vue页面与交互实现4.1 Vue项目创建与路由配置前端我建议用Vite创建Vue 3项目命令很简单npm create vuelatest exam-web创建后按提示选择Router、Pinia后续再安装Element Plus、Axios和EChartsnpm install element-plus axios echarts路由配置建议把登录页、管理员页面、教师页面、学生页面分开并用路由守卫做权限拦截router.beforeEach((to, from, next) { const token localStorage.getItem(token); const role localStorage.getItem(role); if (to.meta.public) { next(); return; } if (!token) { next(/login); return; } if (to.meta.roles !to.meta.roles.includes(role)) { next(/403); return; } next(); });meta.roles可以放在路由定义里比如教师页面{ path: /teacher/exam/create, component: () import(/views/teacher/ExamCreate.vue), meta: { roles: [admin, teacher] } }前端权限控制只是提升体验真正安全的判断一定在后端。就算有人绕过路由守卫直接访问页面没有后端接口权限照样拿不到数据。请求封装上我用Axios实例统一处理Token和错误码const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 0) { ElMessage.error(res.msg); return Promise.reject(new Error(res.msg)); } return res; }, error { if (error.response error.response.status 401) { localStorage.clear(); router.push(/login); } ElMessage.error(error.message || 请求失败); return Promise.reject(error); } );4.2 考试页面倒计时与防作弊处理考试页是整个前端最复杂的页面。布局上左侧是答题卡显示每道题的题号已答的题号高亮中间是题目内容顶部是倒计时和交卷按钮。这个页面我建议拆成三个组件CountdownBar、QuestionCard、AnswerSheet避免一个组件上千行。倒计时逻辑用一个全局定时器实现const remaining ref(exam.duration * 60); let timer null; function startTimer() { timer setInterval(() { remaining.value--; if (remaining.value 0) { clearInterval(timer); submitExam(); } }, 1000); }这里要注意一个问题如果组件销毁时不清理定时器会造成内存泄漏。onUnmounted钩子里一定要clearInterval(timer)。防作弊我做了三层第一层禁止右键菜单和复制最简单但防君子不防小人第二层监听visibilitychange事件当页面切到后台时记录一次切屏累计超过3次就自动交卷第三层记录用户ID、IP和切换时间存到exam_log表教师可以查看考试日志。document.addEventListener(visibilitychange, () { if (document.hidden) { quitCount.value; // 记录切屏日志接口 logSwitchScreen(examId); if (quitCount.value 3) { ElMessage.warning(切屏次数过多系统自动交卷); submitExam(); } } });需要说句实话纯前端的防作弊措施只是演示真正的防作弊需要摄像头监控、屏幕录制这类强方案。但毕设做到“切屏记录自动交卷”已经能体现你对安全性的思考。答题过程中我建议把答案同时存到本地存储里。万一学生不小心刷新页面还能从localStorage恢复答案不然一场考试辛苦答到一半刷新全没了体验会很糟糕。提交成功后再清除这份本地缓存。4.3 成绩统计图表展示成绩统计是教师端让学生“眼前一亮”的功能点。用ECharts画成绩分布直方图比纯数字列表直观得多。需要注意echarts体积不小不要全局引入所有模块建议按需引入。统计接口返回数据结构{ avgScore: 76.5, maxScore: 98, minScore: 45, distribution: { 90-100: 5, 80-89: 8, 70-79: 6, 60-69: 3, 60: 2 } }前端画图的核心步骤import * as echarts from echarts/core; import { BarChart } from echarts/charts; import { GridComponent, TooltipComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; echarts.use([BarChart, GridComponent, TooltipComponent, CanvasRenderer]); const chart echarts.init(document.getElementById(scoreChart)); chart.setOption({ xAxis: { type: category, data: Object.keys(distribution) }, yAxis: { type: value }, series: [{ type: bar, data: Object.values(distribution), itemStyle: { color: #409EFF } }] });别忘记在组件卸载时调用chart.dispose()释放实例否则页面切换后图表会重复渲染内存也越占越多。除了图表教师端还应该有一个成绩导出功能。最简单的是后端生成CSVpublic function export($examId) { $list Db::name(exam_record) -alias(r) -join(user u, r.user_id u.id) -where(r.exam_id, $examId) -select() -toArray(); $filename exam_ . $examId . _ . date(Ymd) . .csv; header(Content-Type: text/csv; charsetutf-8); header(Content-Disposition: attachment; filename . $filename . ); $output fopen(php://output, w); fwrite($output, \xEF\xBB\xBF); // 写入BOM防止Excel打开中文乱码 fputcsv($output, [姓名, 成绩, 提交时间]); foreach ($list as $row) { fputcsv($output, [$row[real_name], $row[score], $row[submit_time]]); } fclose($output); }很多同学第一次写导出功能时被CSV中文乱码坑过因为Excel默认用GBK解析CSV。写入UTF-8的BOM头\xEF\xBB\xBF就能解决这也是我踩过坑之后记住的。5. 部署调试与毕设答辩要点5.1 本地调试与常见报错排查本地联调时前端开发服务器的端口通常是5173后端接口跑在8000或80存在跨域。与其在后端写一堆CORS头我建议直接利用Vite的代理功能把/api请求转发到后端// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } });这样前端所有请求都写成/api/xxx开发时不会跨域部署时只要Nginx配置好同样路径前后端代码一行都不用改。我整理过一份高频问题清单对照着排查能省不少时间现象可能原因处理方式登录接口404路由没有注册或伪静态没配检查route文件检查Nginxtry_files中文乱码数据库连接编码不对.env里charsetutf8mb4建表也统一utf8mb4Token马上失效服务器时区和本地时区不一致PHP设置date_default_timezone_set(PRC)MySQL连接失败PHP缺少pdo_mysql扩展php -m查看扩展安装后重启前端页面白屏build时报错或资源路径不对Vite修改base为./检查Nginx root路径提交考试没反应网络请求被拦截或重复提交打开浏览器Network查看请求看响应里的错误码如果想快速定位后端问题ThinkPHP开启APP_DEBUGtrue后接口报错会直接返回详细异常信息比瞎猜效率高很多。生产环境再关掉免得把堆栈信息暴露给用户。5.2 部署到服务器的步骤部署这一步很多同学没做过但答辩时老师往往爱问“你的系统是怎么部署的”。我用过最简单的方式是本地把前端打包成静态文件后端代码传到服务器再用Nginx做反向代理。前端打包前改一下Vite配置确保打包后的资源能正确加载export default defineConfig({ base: ./ });打包命令npm run build执行后会生成dist目录里面是纯静态文件。把它上传到服务器比如放到/www/wwwroot/exam-web。后端代码放到/www/wwwroot/exam-api打开public目录设为网站运行目录。Nginx关键配置可以这样写server { listen 80; server_name exam.example.com; root /www/wwwroot/exam-web; index index.html; location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里location /下的try_files很重要因为Vue Router使用history模式刷新/teacher/exam/create这种地址时Nginx如果找不到对应文件就会返回404加上try_files后就会回退到index.html由前端路由接管。如果使用宝塔面板这些配置可以在后台图形化完成创建站点、设置伪静态为thinkphp、配置反向代理。但我还是建议自己手写一遍Nginx配置答辩时被问到了能讲得清。部署完成后记得修改后端的JWT密钥不要用代码里的默认值。密钥是硬编码在.env里的环境变量生产环境尤其要注意。5.3 答辩时如何讲解项目亮点很多同学代码写完了一到答辩就只会说“我用了PHP和Vue实现了登录和考试功能”。这太可惜了。同样是功能会讲和不会讲老师的印象完全不同。我的建议是把亮点集中在四个点第一点角色权限设计。系统里三套角色、三套菜单、三套接口权限讲清楚前端路由守卫和后端中间件的双层校验。第二点随机组卷和固定试卷的结合。系统按规则抽题生成固定试卷保证同一场考试所有学生题目一致且抽题时自动验证总分匹配题目配置。第三点自动判分与事务一致性。客观题按题型设计不同判分逻辑主观题留待人工评阅提交考试用事务加锁防止重复提交导致脏数据。第四点防作弊思路。切屏监控、答题日志、提交后不可修改这些细节虽然没有做到企业级那么强但整套逻辑闭环。演示时我建议按“教师建课、加题、创建考试学生参加考试教师查看成绩”这条主流程走一遍。准备三个浏览器一个登录管理员一个登录教师一个登录学生切换角色演示的效果比只截几张图强很多。答辩前把数据库设计图、接口文档、部署步骤打印出来或者放在第二屏问到哪一方面都能快速调出对应内容。老师问“为什么用JWT而不用Session”时就把“前后端分离、无状态、扩展性好”这三句话展开说。6. 常见问题与避坑经验6.1 中文乱码、时区、跨域三座大山这三个问题几乎每个项目都会遇到。中文乱码的根源是“编码不一致”数据库表是utf8mb4但PHP连接字符集没设置或者前端页面声明的是GBK或者JSON返回时头部没有标明Content-Type: application/json; charsetutf-8。我的统一方案是数据库连接、PHP文件编码、JSON输出、HTML页面全部使用UTF-8。MySQL建库时明确写CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;时区问题主要是Linux服务器默认是UTC而中国用户看到的考试时间必须按北京时间。PHP端可以在公共入口文件里加一行date_default_timezone_set(PRC);MySQL也可以设置全局时区SET GLOBAL time_zone 8:00;跨域问题我前面提过开发环境用Vite代理生产环境用Nginx反向代理这样前端和后端保持在同一个域名下既规避跨域还避免暴露后端端口。6.2 数据一致性与并发提交问题在线考试系统最容易被人忽略的坑是“学生提交答案时并发请求”。虽然学生一般不会恶意刷接口但网络波动、重复点击都会导致多条提交请求到达后端。如果后端没有做幂等处理成绩记录就会重复。我的经验是双保险第一道保险数据库层面。exam_record表对(exam_id, user_id)加唯一索引重复插入直接报错这是底线。第二道保险应用层面。提交接口使用事务并在事务里对考试记录行加锁。锁定后先查状态如果已经提交过就直接返回否则才继续算分。答题明细表answer_record也建议加唯一索引(record_id, question_id)。理论上在同一份记录里一道题只会有一条答案加索引后即便代码异常也不会插入重复答题数据。关于Redis或队列毕设阶段不强制用。但我可以在文档里提一句“当同时在线考试人数较大时可以引入Redis缓存试卷题目使用消息队列异步处理主观题提交”作为未来的优化方向不需要真正实现。6.3 如何扩展成可商用的考试平台如果以后想把这个毕设升级成能接真实业务的平台最值得做的扩展有三个一是导入导出。题库支持Excel批量导入学生名单支持批量导入考试成绩支持一键导出。企业场景里手动一条条录题目和名单根本不可接受。二是题型升级。增加组合题、操作题、文件上传题。文件上传题在后端要校验文件类型、大小并把文件存到独立的存储目录数据库只记录文件路径。三是考试策略。比如考试过程中按章节顺序答题、题目乱序、选项乱序、考试中途断网重连、试题缓存到本地。这些功能虽然开发量不小但全都是现有架构上可以扩展的不需要重构。以我个人的经验毕设项目的评分标准大多不是“功能越多越好”而是“结构是否清晰、逻辑是否自洽、是否掌握核心技术点”。你能把JWT鉴权、随机组卷、自动判分、事务防重这几件事讲清楚就已经超过大多数只背概念的同学。最后再分享一个小习惯每写完一个模块就在本地跑一遍完整链路别把问题攒到最后联调时才暴露。在线考试系统这种项目数据流程长、角色多早发现问题早解决绝对比最后通宵改Bug舒服得多。
返回列表