☰
Vue+Node.js+ElementUI构建在线考试系统:从设计到部署全流程实战
2026/10/18 13:25:44 网站建设 项目流程

去年帮某高校教研室做的高校在线考试系统,技术栈就是标题里的 Vue + Node.js + ElementUI,从数据库到前端页面再到部署上线,整个链路走完后我最大的感受是:在线考试系统的难点从来不是“能考试”,而是“考得准、交得稳、判得对”。这套系统做下来,覆盖了管理员、教师、学生三种角色,包含题库管理、自动组卷、在线答题、倒计时交卷、自动阅卷、成绩统计这些模块。如果你正准备做同类项目,或者在公司里需要快速搭建一套内部考试/培训考核平台,这篇文章应该能帮你少走不少弯路。

我不打算只列功能清单,而是按真实开发顺序,把每一步的设计理由和踩坑过程讲清楚,尽量做到看完就能照着自己的项目复现。

1. 项目背景与方案选型:在线考试系统到底要解决哪些问题

1.1 一场线上考试的真实业务链路

刚开始和某高校的对接老师聊需求时,对方给的需求文档写得很简单:“能发布试卷、学生能在线作答、自动算出成绩”。但真正坐到一起梳理业务流程,才发现问题远没有那么简单。一场考试从创建到出成绩,至少要经历 7 个环节:教师录入题库、按规则组卷、设置考试时间与时长、发布考试、学生登录考试页答题、系统自动判客观题、教师人工批主观题,最后还要支持成绩导出和统计分析。

其中最容易在设计阶段被忽略的,是“同卷不同序”和“考试中途异常离开”这两个场景。前者要求每个学生看到的题目顺序、选项顺序可以不同,目的是降低抄袭概率;后者要求学生在断网、刷新、误关页面后重新回来,还能回到原来的答题进度,而不是重新考一遍。

这两个需求直接影响数据库设计和前端状态管理方案。我在项目初期就是因为在文档里没把这些场景写全,导致后端表结构返工了一次,后面会详细说。

1.2 技术选型:为什么是 Vue + Node.js + ElementUI

这套技术栈在今天看来不算新潮,但放在高校内部系统的场景里非常合适,理由很实在:

第一,团队语言栈统一。前后端都用 JavaScript,后端用 Node.js 做接口服务,前端用 Vue 做页面,中间不需要切换语言上下文。对一个小型开发团队来说,这能把协作成本压到最低,连接口数据结构都能直接在代码里共享。

第二,ElementUI 的后台管理组件太成熟了。考试系统有大量的后台管理页面——试题列表、试卷配置、考试记录、成绩表格,这些页面高度依赖表格、表单、弹窗、分页、级联选择等组件,ElementUI 基本全是现成的,不需要自己造轮子。虽然界面风格偏“中后台”,但高校项目反而吃这一套:朴素、稳定、功能一目了然。

第三,Vue 的单页应用特性适合考试流程。考试过程是一个典型的“填表 + 状态切换”交互,从进入考试页到答题卡标记、倒计时更新、交卷确认,基本都在同一个页面视图里完成,不需要频繁切换路由。Vue 的响应式状态管理配合 Vuex,处理这种高交互场景非常顺手。

对比一下其他方案:Java + Spring Boot 是高校老系统的常客,但在这个项目里没必要为了“传统”而引入更重的框架,因为业务量级就是几百人同时在线;直接用 jQuery + 后端模板渲染也能做,但考试页面的实时状态同步和组件复用会别扭很多。综合下来,Vue + Node.js + ElementUI 是投入产出比最高的组合。

1.3 功能模块与角色权限划分

系统按角色拆分成三个端:

管理员端:管理院系、班级、教师账号,负责系统基础数据维护,查看全局考试统计数据。

教师端:核心使用方。管理题库(单选、多选、判断、简答四种题型)、手动组卷或随机组卷、发布考试、查看考试成绩、人工批改主观题、导出成绩单。

学生端:参加考试。查看待考列表、进入考试、答题、交卷、查看成绩和答案解析。

权限模型我用的是最经典的三张表:用户表、角色表、用户角色关联表。细粒度权限没有做得太过,因为高校内部系统的角色是固定的三种,不需要像互联网系统那样做菜单粒度权限。但接口层面必须做角色鉴权,不能只在菜单上做隐藏。

前端路由用 Vue Router 的全局守卫做了登录态校验和角色页面隔离,后端接口用 JWT 中间件校验身份,每个接口显式声明允许的角色。

2. 数据库建模:把“一张试卷”拆成哪几张表才合理

2.1 用户、角色与考试组织的表结构设计

先把基础表列出来,这套结构基本可以平移到任何内部考试系统:

users 表(用户):id、username、password(bcrypt 加密后的哈希)、real_name、role(admin/teacher/student)、department_id、class_id、created_at。

departments 表(院系):id、name。classes 表(班级):id、department_id、name。这两个表主要是为了按班级群发考试,以及后续按院系维度做成绩统计。

角色我一开始用独立表roles+user_roles关联表,后来想想,这个系统短期内不可能出现动态角色配置,就直接在 users 表里加了role字段。做这类内部管理系统时,过度设计是最大的敌人,简简单单用一个字段解决问题更实用。如果后续要扩展角色,再拆表也来得及,数据迁移成本并不大。

2.2 题库、试卷与答题流水的关系建模

这是整个系统里最值得花心思的部分。我把它拆成四张核心表:

questions 表(题目表):id、type(single/multiple/judge/short)、content(题干)、options(JSON 数组,如["A.xxx","B.xxx"])、answer、difficulty、score、course_id。这里有一个关键点:选项用 JSON 字段存储,而不是单独建一张选项表。

一开始我纠结过要不要把选项拆成子表,后来想明白了:考试系统的题目一旦被引用进某份试卷,就要求选项和答案是固定快照,不允许后面题目被修改而影响历史考试。所以题目的options和answer在组卷时会复制一份到试卷题目表,而不是实时关联。

papers 表(试卷表):id、name、course_id、total_score、creator_id、create_time。这只是试卷的壳,具体题目关系在下一张表。

paper_questions 表(试卷题目表):id、paper_id、question_id、score、sort_order、options(组卷时复制的题目选项快照)、answer(复制的答案快照)。这张表是整张试卷的核心,因为它同时记录了题目顺序、分值和过期快照。随机组卷时,就是向这张表批量插入题目。

exams 表(考试记录表):id、paper_id、title、start_time、end_time、duration(分钟)、status(0未发布/1进行中/2已结束)、created_by。一场考试对应一份试卷,考试和试卷分离是因为同一份试卷可能在不同时间被多个班级考。

2.3 答题记录与成绩表的幂等设计

学生作答记录是整个系统数据量最大、并发最高的表,设计时主要考虑“怎么保证第一次交卷就是唯一一次”。

answer_records 表(答题流水表):id、exam_id、student_id、paper_question_id、answer_content、score、is_correct、submit_time。每个学生每道题一条记录,主观题的分数字段先留着,等教师批改后回填。

exam_results 表(考试成绩表):id、exam_id、student_id、paper_snapshot(选择题答案快照,JSON)、objective_score(客观题得分)、subjective_score(主观题得分)、total_score、submit_time、status(0未交/1已交/2待批改)。

这里做了两个冗余设计:一是paper_snapshot存整个试卷的题目顺序和选项序,因为同一场考试可能有不同的乱序版本,成绩复核时必须知道学生当时看到的是哪一版卷子;二是成绩表里同时冗余了objective_score和subjective_score,避免每次查成绩都要临时聚合答题流水表。不做冗余的话,一旦答题记录因为某种原因被变更,历史成绩就没法看了。

“交卷幂等”是在exam_results表上建了exam_id + student_id唯一索引,后端交卷接口先查后插,走到插入那步再用唯一索引兜底,具体逻辑第三章讲。

3. Node.js 后端核心:考试倒计时、自动阅卷与防重复交卷

3.1 Express 接口分层与 JWT 鉴权中间件

后端我用的是 Express,没有上 NestJS,因为系统以简单 REST 接口为主,没必要用重框架增加认知负担。目录结构长这样:

server/ routes/ // 路由定义 controllers/ // 控制器:取参、校验、调用 service services/ // 业务逻辑层 models/ // 数据库操作 middlewares/ // auth、error handler

路由层只做 URL 分发,controller 只做参数校验和结果包装,真正的业务逻辑全部下沉到 service。这样做的最大收益是:自动阅卷这种复杂逻辑可以单独写成 service 函数,交给定时任务调用和接口调用都不冲突。

鉴权中间件是每个接口绕不开的一环。登录成功后发 JWT,前端存 localStorage,每次请求带Authorization: Bearer <token>。中间件里解析 token、校验角色,然后挂到req.user上:

// server/middlewares/auth.js const jwt = require('jsonwebtoken'); module.exports = function auth(roles = []) { return (req, res, next) => { const token = req.headers['authorization']?.split(' ')[1]; if (!token) return res.status(401).json({ code: 401, msg: '未登录' }); try { const payload = jwt.verify(token, process.env.JWT_SECRET); if (roles.length && !roles.includes(payload.role)) { return res.status(403).json({ code: 403, msg: '无权限' }); } req.user = payload; next(); } catch (e) { return res.status(401).json({ code: 401, msg: '登录已过期' }); } }; }

使用时就特别简单,比如只有教师和管理员能访问试卷列表接口:

router.get('/papers', auth(['admin', 'teacher']), paperController.list);

3.2 考试倒计时的三种方案,我最终选了哪一种

考试倒计时是前端必做、后端更要管的功能。我对比过三种方案,各有各的坑:

方案一:前端计算倒计时。进入考试页时从接口拿到end_time,然后前端用setInterval每秒减 1。这个方案实现最简单,但致命问题是前端时间可以被用户篡改——改系统时间、改浏览器时间,倒计时立刻失真。高校考试虽然不至于有人恶意作弊,但监考老师有硬性要求,所有考生必须以服务器时间为准。

方案二:后端存截止时间,前端定时校准。进入考试时下发end_time,前端倒计时显示用本地计时器驱动,但每 5 秒调一次接口校准服务器时间,差值超过阈值就强制刷新本地倒计时。这个方案能防住大部分时间作弊,但接口请求太频繁,几万人考试时会对服务造成无意义压力。

方案三(最终采用):后端时间 + 前端本地预估。进入考试时后端一次性下发server_time和end_time,前端算出时间差offset = end_time - server_time,倒计时展示就用offset - 已过本地时间。整个考试过程中不额外请求时间校准接口,交卷时后端必须校验当前服务器时间是否超过end_time,超过直接拒绝并强制交卷。

为什么最终选方案三?因为高校考试场景里,学生电脑时间不对是常态,但截断网络把浏览器时间改到考试结束前才交卷的极端场景很少。我们靠交卷时的服务端校验兜底,考试过程中的倒计时即使有微小偏差,也不会影响判定结果。实现代码大致是这样的:

// 进入考试时接口返回 { serverTime: 1699999999999, // 后端当前时间戳 endTime: 1700000000000 // 考试结束时间戳 } // 前端计算 const remainMs = endTime - serverTime;

3.3 自动阅卷的判分逻辑,最少要处理这四种题型

客观题自动判分是逻辑最直接但也最容易出细节问题的地方,我把它拆成四个分支处理:

单选题:答案存的就是单个选项字符(如"A"),判分时直接比对answer_content === answer。

判断题:答案存"T"或"F",判分同样是一对一比对,逻辑和单选题一样。

多选题:这里有个很容易踩的坑——多选答案在数据库里存的是["A","C","D"]数组,但如果存成 JSON 字符串,比对时用===绝对会出问题,因为["A","C"]和["A","C"]在 JS 里是引用比较,必须拆成数组然后排序后 join 再比对。还有评分策略分成“漏选得一半分”和“多选不得分”两种,需要在试卷里配置一个score_rule字段,否则不同老师对同一道多选判分要求不一样,逻辑就写死了。

我们最终实现的判分核心函数长这样:

// server/services/scoreService.js function scoreQuestion(question, answerContent) { const type = question.type; const correctAnswer = JSON.parse(question.answer); // 已存为数组或字符串 if (type === 'single' || type === 'judge') { return answerContent === correctAnswer ? question.score : 0; } if (type === 'multiple') { const userArr = JSON.parse(answerContent).sort(); const correctArr = correctAnswer.sort(); if (userArr.join(',') === correctArr.join(',')) return question.score; // 漏选给一半分:用户选中的每个选项都是正确答案的一部分 const allCorrect = userArr.every(opt => correctArr.includes(opt)); if (allCorrect) return Math.floor(question.score / 2); return 0; } // 简答题返回 0,等待人工批改 return 0; }

自动阅卷的触发时机与人工批改流程

自动阅卷不能放在交卷接口的同步流程里做,因为多道题判分虽然快,但在高并发交卷时会拖慢交卷响应时间。我的做法是:交卷接口只做一件事——把答题记录和试卷快照写入数据库,标记状态为“待判分”,然后立即返回。判分操作由一个延迟任务或者队列消费端去执行。

管理员在教师后台能看到两类成绩状态:客观题 0 分,主观题待批改。教师进入批改页面后,左侧是客观题得分预览,右侧按简答题逐题人工打分,打分完成后保存,系统自动汇总objective_score + subjective_score。

这里有个细节:即使简答题答案为空,也必须生成一条答题记录,否则教师在批改页面会漏看那些一个字没写的学生。

3.4 防重复交卷:唯一索引 + Redis SetNX 双重保险

交卷接口是并发风险最高的时候。学生点交卷之后,前端按钮会立刻置灰,但这只能防住正常用户,防不住恶意请求重复 POST。我的策略是两重防护:第一重,exam_results表建唯一索引uk_exam_student(exam_id, student_id),第二次交卷时插入直接报 duplicate key 错误;第二重,在交卷接口开头用 Redis 的SETNX key value做分布式锁,key 为submit:examId:studentId,成功拿到锁才继续处理,否则直接返回“已交卷”。

// server/services/examService.js const lockKey = `submit:${examId}:${studentId}`; const lockAcquired = await redis.set(lockKey, '1', 'EX', 120, 'NX'); if (!lockAcquired) { return { code: 400, msg: '试卷已提交,请勿重复操作' }; } try { // 检查考试是否已结束、是否已提交 // 写入 answer_records、更新 exam_results } finally { await redis.del(lockKey); }

这套方案实测下来,同时 500 人交卷没有出现一条重复记录。

3.5 突然断网、刷新、关页面的恢复机制

在线考试最怕学生考到一半断网,刷新后看到空白页。我在后端设计了“考试状态恢复”接口:进入考试页时调一次GET /api/exams/:id/status,后端根据exam_results的状态返回:

  • 0:未开始考试,前端跳到考试介绍页;
  • 1:已在考试中,返回已提交的答题记录、剩余时间、试卷题目数据,前端恢复到上次状态;
  • 2:已交卷但仍在考试时间内,返回已交卷状态,前端提示“你已完成考试”。

这个接口同时解决了“刷新丢进度”和“考试中途换设备不丢进度”的问题,理论上只要后端答题流水还在,前端随时可以重建试卷状态。

4. Vue + ElementUI 前端重点:考试页面和管理后台的实现套路

4.1 前端路由守卫与考试状态恢复

前端路由用 Vue Router,登录页和考试页要特殊处理。全局守卫里做两件事:先判断是否登录,未登录一律跳到登录页;再判断路由的meta.role是否包含当前用户角色,不匹配就跳 403 页。

考试页面/exam/:examId的守卫逻辑特殊一点:进入前先调后端状态接口,拿回考试是否还在有效期内;如果考试已结束,直接跳成绩页;如果正在考试,就恢复答题记录,这一步必须放在路由守卫里做,否则用户刷新后答题状态会先闪一下空白,体验很糟糕。

// router/index.js router.beforeEach(async (to, from, next) => { const user = store.state.user; if (!user) { if (to.path === '/login') return next(); return next('/login'); } if (to.meta.role && !to.meta.role.includes(user.role)) { return next('/403'); } next(); });

4.2 考试页答题卡设计:为什么不用每道题都发请求

考试页的交互核心是答题卡。左侧是题目列表,点击切换题目,右侧显示试卷信息、倒计时、交卷按钮,中间是当前题目的题干和选项。我用了 Vuex 存“当前试卷状态”,结构大概是:

  • currentQuestionIndex:当前定位到第几题;
  • answers:对象,key 为paperQuestionId,value 为选中的答案;
  • answeredCount:已答题目数,驱动顶部进度条。

为什么用 Vuex 而不是直接在组件里存?因为考试页里存在大量跨组件状态传递:顶部倒计时组件要影响交卷按钮的可用状态,答题卡侧边栏要实时显示已答题目列表,题目切换组件要同时更新 Vuex 的索引。如果用组件自己的 data 去管理,这套联动逻辑会变得特别混乱。

用户每点一个选项,答案只更新 Vuex,不发请求。只有交卷时才把所有答案一次性提交。这样做一是减少接口调用量,二是避免用户切题时答案闪烁。防断网的策略是浏览器beforeunload事件里把 Vuex 里的answers序列化进 localStorage,用户刷新后从 localStorage 恢复。

答题卡交互还有一个细节:多选题用 checkbox,单选题和判断题用 radio,简答题用 textarea。这几个表单元素在 ElementUI 里全部现成,但需要统一封装一个QuestionCard.vue组件,根据question.type渲染不同控件,组件内部只负责展示和触发store.commit('setAnswer', ...),不关心业务逻辑。

4.3 ElementUI 在后台管理页面的组合玩法

后台管理页面大量依赖 ElementUI,真正效率拉满的地方是这几个组件的组合:

el-table + el-pagination + el-select:教师题库列表页就是标准的三组件组合。表格展示题干和题型,分页器控制翻页,顶部的类型筛选器用 select 绑定查询参数。这个组合要注意的是筛选条件变化时,必须把当前页重置为 1,否则筛选后还在第 5 页,界面会显得很奇怪。

el-dialog + el-form:新增和编辑题目共用一个弹窗表单,通过dialogType区分新增还是编辑。vue 的响应式特性在这里有个坑:打开编辑弹窗时,表单回填直接赋值form = row会导致修改原表格数据,必须用深拷贝JSON.parse(JSON.stringify(row))。

el-upload + el-table:批量导入题库用的是 el-upload 上传 Excel 文件,后端解析后返回成功与失败列表,前端用 el-table 展示导入结果。这里建议在上传成功后再调一次列表接口刷新数据,否则用户会一直停留在旧数据界面。

4.4 axios 拦截器统一处理登录态与错误提示

接口请求统一封装在utils/request.js,axios 实例挂上请求拦截器和响应拦截器。请求拦截器做两件事:从 localStorage 取 token 加到请求头;如果是交卷这种关键请求,给按钮加 loading 状态防止用户重复点击。

响应拦截器统一处理三件事:返回码为 200 时直接返回data字段;返回 401 时清除本地登录态并跳转登录页;其他错误码用 ElementUI 的Message.error弹出后端返回的报错信息。

// utils/request.js service.interceptors.response.use( (response) => { const res = response.data; if (res.code === 200) return res.data; if (res.code === 401) { store.dispatch('logout'); router.push('/login'); return Promise.reject(new Error('未登录')); } Message.error(res.msg || '请求失败'); return Promise.reject(new Error(res.msg)); }, (error) => { Message.error('网络异常,请稍后重试'); return Promise.reject(error); } );

这套防御在考试页特别重要,交卷时如果网络断了,用户至少能看到一个明确的“网络异常”提示,而不是在毫无反馈的情况下看着页面卡死。

5. 上线前后踩过的坑:时钟、多选题与并发交卷

5.1 学生电脑时间不准引发的倒计时错误

这个问题上线第一天就暴露了。当时有一批学生的电脑系统时间比北京时间快了 3 分钟,导致考试页显示的剩余时间比实际少了 3 分钟,学生还没答完就显示考试结束,全场炸锅。

排查后确认问题出在前端计算倒计时的公式上。最初我是这么写的:

// 错误写法 const remainMs = new Date(endTime).getTime() - Date.now();

Date.now()读的是本地系统时间,一旦学生电脑时间不准就出错。后来改成以服务器时间戳为基准:

// 正确写法 const offset = new Date(endTime).getTime() - serverTime; // 服务器剩余毫秒 const remainMs = offset - (Date.now() - localStartTime); // 减去本地已过时间

这里localStartTime是进入页面时本地记录的时间戳。因为学生在考试期间电脑时间发生变化的情况极少,所以只需保证进来那一刻的基准是对的即可。如果你要做得更严谨,可以在倒计时只剩最后 5 分钟时,额外调一次时间校准接口,把服务器时间拉下来重算 offset。

这个坑给我们的教训是:一切时间判断都必须以服务器时间为准,前端能显示倒计时,但永远不要相信前端时间。

5.2 多选题判分:JSON 数组的顺序陷阱

自动阅卷上线前测试时,我们造了几套模拟数据,测出来十道多选题错了一半。查了半天才发现是判分函数里数组比较出了问题。

我第一版写的多选判分是:

if (JSON.parse(userAnswer).join() === JSON.parse(correctAnswer).join()) { return score; }

看起来没问题,但 JSON.parse 出来的是数组,数组的顺序在存储和读取过程中并不保证一致。如果后台录入答案时存的顺序是["A","C","D"],前端传上来的用户答案恰好是["A","D","C"],字符串 compare 就失败了。

修复方案就是前面提到的先排序再拼接:

const u = JSON.parse(userAnswer).sort().join(','); const c = JSON.parse(correctAnswer).sort().join(','); if (u === c) return score;

同一个坑还会出现在后台批量导入题目的场景:Excel 里多选题答案写的是ACD而不是A,C,D,导入解析时就得统一转成数组再排序存储。建议在题库导入导出模块做一次答案格式规范,别让脏数据流进正式库。

5.3 高并发交卷时,答案记录写入顺序不一致

有次模拟 1000 人同时交卷,出现了一个奇怪的现象:部分学生的客观题成绩比预想的少了几分。查日志发现,这些学生的答题记录有一部分在“判分任务”执行时才刚插入,另一部分已经插完,判分逻辑读取的数据自然不全。

问题根因是交卷接口为了追求响应速度,使用了“先写一句话记录,再异步判分”的模式。但数据库写入并不是原子的,多条记录被拆成了多个 INSERT,异步判分读库时只能读到一部分。解决方法是把整个交卷写库操作放在一个事务里,所有答题记录一次性写入,再标记成绩表的待判分状态:

// server/services/submitService.js const transaction = await db.sequelize.transaction(); try { await AnswerRecord.bulkCreate(recordList, { transaction }); await ExamResult.update( { status: 'pending', submit_time: new Date() }, { where: { exam_id, student_id }, transaction } ); await transaction.commit(); } catch (e) { await transaction.rollback(); throw e; }

事务提交完成后,判分任务再来读答题记录就是完整的了。这个改动之后,模拟压测 1500 人交卷,客观题判分都稳定准确。

5.4 部署与反向代理:静态资源分离和 HTTPS

部署环境是一台 4 核 8G 的云服务器,前端打包成静态文件交给 Nginx,Node.js 后端用 PM2 守护进程监听 3000 端口,Nginx 做反向代理,把/api路径转发到本地 3000。

# nginx.conf 关键配置 server { listen 80; server_name exam.example.com; root /var/www/exam-frontend/dist; location /api/ { proxy_pass http://127.0.0.1:3000/; 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 $uri $uri/ /index.html; } }

前端打包时要特别注意 Vite 的base配置,如果部署在子目录就要把base改成对应路径,否则静态资源全部 404。还有,开发环境和线上环境的接口地址不要写死在代码里,用环境变量做区分:开发环境.env.development指向本地接口,生产环境.env.production指向 Nginx 反代地址。

防作弊方面我做了三层:一是切屏检测,考试页面监听visibilitychange事件,切出 5 秒以上记录并提示一次,超过三次标记为风险;二是选项顺序随机,组卷时同一道题的选项顺序在paper_questions表里存一份乱序快照;三是题目顺序随机,同一份试卷在生成考试时按不同分发给不同学生。这套组合拳下来,虽然没有完全杜绝作弊,但至少挡住了大多数靠截图和传答案的低成本手段。

5.5 移动端适配和浏览器兼容

高校学生现在大量用手机考试,这个需求最初被忽略了。ElementUI 默认是为桌面端设计的,表格和弹窗在手机上的体验很一般,所以考试页单独做了一套移动端样式:答题选项的 padding 加大、字号调大、交卷按钮固定在底部、顶部倒计时缩小。用 CSS 媒体查询配合触发断点,不需要引入额外的移动端 UI 库。

浏览器兼容主要盯住两个旧版本的坑:一是旧版 Safari 不支持Date.now()之外的某些时间格式化写法,倒计时字符串解析时容易被坑;二是 IE 已经完全不用管,但 Chrome 和 Edge 的老版本要注意Array.prototype.sort在数组元素为字符串时是字典序排序,这在多选题判分时会引发问题,所以判断用了显式的localeCompare兜底。

写在最后:做在线考试系统最容易被低估的,是流程完整度

这整套系统从设计到上线大约用了三周,工作量最大的不是代码本身,而是把“考试”这件线下很自然的事情翻译成线上系统能处理的确定性逻辑。倒计时以谁为准、多选题怎么算分、断网后怎么恢复、交卷怎么保证唯一——这些才是项目真正的复杂度所在。

我个人真实的体会是:如果你现在准备做类似的系统,建议先别急着写代码,找一位带过真实考试的教师聊一小时,把你设计的流程从头到尾给他讲一遍,他大概率会告诉你三件你没想到的事:学生没看到考试通知怎么办、有人在考试中途换了电脑怎么办、试卷题目有错别字需要临时修改怎么办。这三个问题的答案,才是你系统真正区别于“Demo”的分界线。

技术上 Vue + Node.js + ElementUI 这套组合足够承担这样的业务,团队小、迭代快、后台管理页面省心。后续如果想扩展更多能力,比如人脸识别监考、AI 查重、基于大数据的学情分析,也都是在现有架构上做增量,不会推倒重来。但不管加什么,先把考试流程里那些“异常情况”处理好,系统才算立得住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询