
计算机毕业设计做到“前端课程在线考核系统”这个题目时很多人第一反应是套个现成的管理系统模板改改登录页、加几张数据表格就完事。但我近几个月刚从零开始把这套系统完整实现并部署上线还顺利通过了答辩这里把设计和实现过程中真正值得写进论文和代码的细节拆开讲一讲。这套系统的核心不是传统的后台管理而是面向前端专业学生的在线考试场景试题里有一半是HTML/CSS还原题、JavaScript编程题需要支持代码运行、结果比对和自动判分和普通的选择题考试差异非常大。准备做类似课题的同学或者想自己搭一套在线考试平台的前端开发者都可以参考这篇文章里的设计思路和踩坑记录。1. 项目整体设计与思路拆解1.1 选题背景为什么专门做“前端课程”考核一般高校的计算机课程考核系统重点都放在客观题上单选题、多选题、填空题归根到底就是文本比对和选项匹配。但前端课程不一样学生要掌握的能力是页面还原、交互实现和代码排错这些能力很难用选择题评估。比如“如何让一个元素在父容器里水平垂直居中”这种问题学生背选项也能答对但是真正给他一个空白的index.html让他写一个居中弹窗动手能力高低立刻见分晓。所以我在做这个毕业设计时把设计目标定成一套以代码题为核心、兼顾客观题的前端在线考核系统。学生端要能在浏览器里直接写HTML/CSS/JavaScript点击运行后看到结果截图或输出日志教师端要能批量导入试题、配置考试规则、查看学生答题代码和运行结果并且支持简单的自动评分。这个需求比普通考试系统复杂但正是复杂度带来了论文的亮点和答辩时的展示素材。1.2 技术选型为什么用前后端分离而不是单体架构我最终选择的是前端Vue 3 Element Plus后端Spring Boot MyBatis Plus数据库MySQL代码执行服务单独用Node.js写了一个沙箱服务。有人会问毕业设计有必要拆这么细吗我的答案是有必要。因为代码题“在线运行”这个需求天然要求前后端分离。如果让Spring Boot直接执行学生提交的JavaScript代码会存在两个问题一是进程安全性不好控制学生代码里写个死循环整个Tomcat线程都被卡死二是执行环境和浏览器端运行结果有不一致比如浏览器里有DOM、BOM环境而后端Node.js或Java环境没有。所以我把代码执行相关逻辑单独抽出来用Node.js的child_process配合超时控制和资源限制来做后端只负责保存学生提交的代码和最终得分。前端选择Vue 3而不是Vue 2一方面组件库在持续更新另一方面Composition API写考试页面的状态管理更清晰。在线考试页面最大的难点是考试状态当前试题索引、已答题目列表、剩余时间、已标记题目、答题代码内容这些状态如果全部分散在各个组件里很容易因为某个子组件更新了数据而其他组件不同步。用defineStore或者ref/reactive做一个集中式的考试状态模块配合onMounted初始化后续处理切题、保存、提交会顺手很多。1.3 功能模块怎么划分才合理整个系统我划分成了五个核心模块用户模块学生、教师、管理员三种角色登录认证、个人信息维护、密码修改。考试管理模块教师创建考试、配置考试时长、选择试题、发布考试学生查看待参加的考试列表。试题管理模块支持单选题、多选题、判断题、HTML/CSS代码还原题、JavaScript编程题。在线考试模块学生进入考试、答题、代码运行、自动保存、倒计时结束自动交卷。成绩与统计模块自动判分、手动批阅主观题、成绩导出、班级成绩分布统计。模块划分的关键是边界。很多同学喜欢把“试题管理”和“考试管理”混在一起觉得都是教师操作。但我在设计数据库时把它们彻底分开了试题表是独立的考试表只存“考试引用了哪些试题ID”不直接复制题目内容。这样同一个题目可以被多个考试共用批量导入时也只要维护一份题目数据。类似地“在线考试”和“成绩模块”看起来都和学生相关但前者是高并发写操作后者是查询统计操作分开之后部署时可以单独调节服务器资源。2. 核心细节解析与实操要点2.1 角色权限设计前端路由守卫和后端接口鉴权要同时做角色权限在毕业设计里很容易被糊弄过去很多模板就只判断“是否登录”没区分角色。但在线考试系统不同角色之间的接口差异很明显如果不做权限控制学生完全可以调用教师端接口去创建考试。我在实际实现时前后端各做了一层。前端用Vue Router的meta字段标注页面允许访问的角色在全局前置守卫里做判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next({ path: /login }) return } if (token) { const userRole localStorage.getItem(userRole) if (to.meta.roles !to.meta.roles.includes(userRole)) { next({ path: /403 }) return } } next() })这里有个容易忽略的点路由守卫只挡住了页面跳转不挡住接口请求。如果学生直接打开浏览器控制台手动调用创建考试的API后端也会接收。所以后端接口必须用拦截器校验JWT中的角色字段不同接口标注不同的RequireRole注解。前端的路由守卫更多是提升用户体验后端鉴权才是安全底线。角色权限细节上还有一层教师只允许管理自己创建的考试不能看到其他教师的题库。这个可以通过在数据库查询时带teacher_id条件实现但很多参考代码只写了角色维度没写数据维度。答辩时如果能主动讲出“横向越权”这个概念导师会觉得你有安全意识。2.2 试题表和试卷表的数据结构设计数据库设计决定了后面写代码是顺滑还是痛苦。我最初设计的试题表很简单就一个question_type和content字段结果存代码题时发现内容既要存HTML/CSS代码还要存示例输入输出和判分规则一个字段根本装不下。最后我采用了这样的设计思路一张主表存题目的公共字段ID、类型、难度、所属课程、创建教师、创建时间一张扩展表存不同题型的细节字段。代码题需要存题目初始代码、完整参考代码、判分方式、运行超时时间选择题需要存选项列表、正确答案、解析。question_id, question_type, difficulty, course_id, teacher_id question_detail: question_id, initial_code, answer_code, compare_method, timeout question_option: question_id, option_key, option_text, is_correct这里最需要注意的是answer_code的处理。我自测时遇到一个很尴尬的情况学生提交代码后后端把代码保存到数据库但代码里有引号、换行符、特殊字符直接拼到SQL里就报错。所以使用MyBatis Plus时凡是保存代码内容必须用#{}预编译方式写入不能使用${}拼接。另外代码字段要设计成TEXT或LONGTEXT类型别用VARCHAR(255)一个完整的函数体很容易超出长度。试题批量导入我参考了常见Excel导入方案前端用el-upload把文件上传到后端后端用EasyExcel解析然后逐条插入。前端上传时需要注意大文件超时问题虽然题库Excel一般不超过几兆但如果题量特别大可以在上传组件中把on-progress事件用起来提示导入进度避免教师以为页面卡死了。2.3 在线考试核心流程倒计时、自动保存、防切屏在线考试的流程比普通CRUD要复杂因为它严格依赖时间状态。我的实现思路是考试开始时间由服务器下发倒计时由前端基于Date.now()计算不依赖setInterval的累积。原因很简单setInterval在浏览器标签页切到后台后会降频甚至暂停倒计时会出现偏差。const endTime ref(Date.now() examDuration * 60 * 1000) const remainSeconds ref(examDuration * 60) const timer setInterval(() { remainSeconds.value Math.max(0, Math.ceil((endTime.value - Date.now()) / 1000)) if (remainSeconds.value 0) { clearInterval(timer) submitExam() } }, 500)使用结束时间戳来计算剩余时间比每秒减一秒要可靠得多即使定时器偶尔延迟也能在下一次触发时用真实时间校正。这里还有一个细节考试开始接口返回的时长应该以秒为单位后端同时记录start_time和end_time前端接受的是截止时间戳这样学生刷新页面后可以从后端重新拉取截止时间倒计时不会重置。自动保存是另一个重点。我的策略是学生在切换题目或点击保存按钮时把当前题目的代码和选项保存到后端同时每隔30秒自动保存一次当前正在编辑的题目。这里用到了防抖函数避免学生在代码框里连续输入时频繁请求接口。实际使用中30秒自动保存加切题保存已经足够不需要每敲一个字符就请求一次否则高并发时服务器压力会很大。防切屏功能是很多导师喜欢问的点。我实现的方式是监听visibilitychange事件当文档从可见变为隐藏且不是正常交卷时后端记录一次切屏次数累计超过N次则标记为作弊。只记录切屏并不能完全阻止作弊因为学生可以把代码写到本地再粘贴回来但这个功能的意义在于威慑和管理约束答辩时说明白这一点就好。2.4 前端课程特殊题型HTML/CSS还原题和JavaScript编程题前端考核系统最核心的部分就是这两类特殊题型。HTML/CSS还原题我设计的流程是教师上传一张目标效果图学生进入答题页面时左侧是效果图右侧是一个预览区域学生编写HTML和CSS代码后点击运行预览区域用iframe加载代码生成的效果教师批阅时对比效果图和预览截图。这里涉及到iframe的srcdoc属性iframe :srcdociframeContent sandboxallow-scripts/iframe使用sandboxallow-scripts可以限制iframe内脚本的权限但要注意如果学生代码里需要引入外部CDN链接还需要加上allow-same-origin。实际运行中我发现如果同时用sandbox和srcdoc部分浏览器尤其老版本Edge会出现样式不应用的问题排查后发现是没有包含allow-same-origin。这个兼容性坑让我花了不少时间。JavaScript编程题则使用Node.js沙箱执行。学生提交的代码被发送到后端的执行服务执行服务把代码写入临时目录用child_process.execFile启动一个独立的Node进程并设置资源限制const { execFile } require(child_process) const child execFile(node, [/tmp/user_code_ userId .js], { timeout: 3000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) { // 返回 stdout 或 stderr 作为运行结果 })这样学生代码即使写了while(true)死循环3秒后也会被timeout杀掉不会拖垮整个考试系统。同时需要在题目中定义输入输出测试用例学生代码读取标准输入或固定参数输出结果和后端保存的期望输出做比对从而自动判断是否正确。3. 实操过程与核心环节实现3.1 项目初始化和工程化配置我建议前端和后端都用标准的工程化脚手架。前端使用Vite创建Vue 3项目选TypeScript版本虽然起步麻烦但后期维护和写论文时可以说“采用TypeScript保证类型安全”。由于毕业生普遍对TypeScript不够熟练如果时间紧张也可以用JavaScript版本但组件库和路由等核心结构要清晰。工程化配置里代理是一个必踩的坑。前后端分离开发时前端访问/api接口需要代理到后端服务。我在vite.config.js里这样配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这里要注意rewrite的作用前端请求/api/exam/list后端接口实际上只定义了/exam/list所以要去掉/api前缀。如果后端接口本身就带/api就不能写rewrite否则请求会变成/exam/list导致404。项目初始化时我还把目录结构按照功能拆分而不是按页面拆分。很多教程会把组件全放在views下按页面命名但考试系统的项目里“考试”相关功能有学生考试页、教师创建考试页、成绩查看页如果按页面拆公共逻辑无法复用。我用的结构是把每个业务模块拆成api文件夹、store文件夹、components文件夹比如src/ api/ # 接口请求封装 assets/ components/ # 公共组件 router/ store/ # Pinia状态管理 views/ exam/ # 考试相关页面 question/ # 试题管理页面 user/ # 登录、个人中心 utils/这样代码组织清晰答辩时也可以讲模块化设计思路。3.2 登录鉴权和用户状态管理登录逻辑不算复杂前端把用户名密码通过POST请求发送给后端后端校验成功后返回JWT令牌和用户角色。前端把令牌存到localStorage同时用Pinia保存用户信息。需要注意JWT的过期时间不能设置太久我设置了24小时但考试时长最多2小时所以考试中途一般不会过期。如果考试特别长需要在后端接口中给考试中的用户延长会话。考试页面的刷新恢复我是这样实现的学生进入考试时后端生成一条exam_record记录包含考试ID、学生ID、当前考试截止时间。前端每一次自动保存都会将答案同步到后端。学生如果不小心刷新页面重新进入考试接口时后端会返回是否已有进行中的考试记录如果存在则恢复考试状态而不是重新创建。这个设计比单纯把数据存在localStorage要稳。因为localStorage数据一旦被用户清空考试就丢了后端保存则能在任何终端恢复。缺点是每次自动保存都产生数据库写入但考试系统本身并发量就不会特别高完全扛得住。3.3 试卷生成与答题状态管理教师创建考试时选择试题有两种方式手动逐题添加和按条件随机抽题。手动添加适合固定试卷随机抽题适合“每次都不同”的小测验。我在后端实现了一个generatePaper方法先按题型、难度、知识点过滤出题池再用随机数抽样最后插入考试题目关联表。答题状态管理是前端开发的亮点我用Pinia定义了一个examStoreexport const useExamStore defineStore(exam, { state: () ({ examInfo: null, questions: [], currentIndex: 0, answers: {}, markedQuestions: new Set(), remainSeconds: 0 }), actions: { async loadExam(examId) { ... }, saveAnswer(questionId, answer) { ... }, toggleMark(questionId) { ... }, async submitExam() { ... } } })在考试页面中题目列表组件根据currentIndex展示当前题侧边栏卡片显示所有题号已答的题背景变成绿色标记的题显示一个三角符号。每次切换题目时自动调用saveAnswer保存当前题目的答案这样即使用户没有主动点击保存切题时也会兜底。实现时有一个小技巧代码编辑器我用的是Monaco Editor这是VS Code的编辑器内核支持代码高亮和自动补全。但Monaco体积很大直接引入会拖慢考试页加载速度。我采用按需引入只加载HTML、CSS、JavaScript三种语言的支持并且使用Vite的manualChunks把Monaco单独打包这样首屏加载时间从原来的6秒降到3秒左右。3.4 成绩统计、自动判分和手动批阅客观题自动判分比较简单前端提交答案后后端比对选项累加得分。代码题比较麻烦不能只比对输出因为学生可能只写了一部分代码。我设计了双阶段判分第一阶段自动执行测试用例根据用例通过数给基础分第二阶段把学生的参考代码和执行结果保存下来教师在后台人工批阅可以对代码风格、优化程度打印象分。成绩统计页面用到了ECharts我做了三个图表班级平均分雷达图、分数段柱状图、每道题正确率折线图。这三个图表组合起来能直观看出考试中哪些知识点掌握得差。ECharts在毕业设计里很常用但要注意的是图表组件在弹窗或Tab切换时容易宽度为0需要在nextTick后调用chart.resize()这个坑几乎每个用ECharts的人都会遇到。4. 常见问题与排查技巧实录4.1 考试页面刷新丢失答题数据这个问题出现的频率最高。最初我把答案全部放在前端内存里只在交卷时提交一刷新全没了。后来改成自动保存后端后遇到另一个问题onBeforeUnload事件里发请求浏览器的请求经常被中断数据没发出去。解决方式是除了切题保存和30秒定时保存在页面关闭前使用navigator.sendBeacon发送一次最终保存。sendBeacon能够在页面卸载后仍尽最大努力把数据发送到服务器比普通的Ajax更可靠。但sendBeacon只能发送POST请求且数据格式有限制对于简单JSON是够用的。如果答案很大还是要依靠定时保存兜底。4.2 倒计时在生产环境出现偏差本地开发时倒计时正常部署到服务器上后学生反馈考试时间比真实时间少了十几秒。排查后发现不是前端问题而是服务器时区和浏览器时区不一致。后端返回截止时间戳时我用了LocalDateTime.now()转成时间戳时没有指定时区导致解析偏差。解决办法是前端提交答案、后端生成时间戳全部统一使用时间戳后端配置spring.jackson.time-zoneGMT8数据库连接串也加上serverTimezoneAsia/Shanghai。前后端时间传输一律用时间戳不传日期字符串能彻底避免时区问题。4.3 代码执行沙箱的安全坑学生提交代码后直接执行会带来安全隐患。我在测试时用了一段代码测试是否有办法跳出沙箱结果发现如果把require作为上下文注入Node进程可能读取服务器文件。后来改用vm模块创建独立上下文但仍然存在原型链污染的风险。最终我用的是更稳妥的做法使用Docker容器作为代码执行环境。在Docker中运行Node服务通过docker exec启动容器内的代码执行进程即使被攻击也只是攻击了一个容器环境不会影响主服务器。毕业设计阶段如果服务器配置有限可以退而求其次使用child_process加超时限制但必须在论文中说明安全设计思路。4.4 跨浏览器兼容性这个系统的用户主要是学生浏览器环境五花八门有Chrome、Edge、Firefox还有一部分用学校的旧版电脑浏览器。我实测遇到几个兼容性问题Array.prototype.at方法在旧版浏览器不支持Monaco编辑器在部分Edge版本白屏CSSgap属性在Flex布局下兼容性差。解决方法是Vite构建时配置兼容目标browserslist指定“last 2 versions and not dead”使用Polyfill补充缺失API关键布局用margin代替gapMonaco编辑器如果检测到浏览器不支持降级使用文本框进行代码提交。这些兼容性处理虽然占了一定的开发时间但保证了考试的公平性——不能让学生因为浏览器问题而影响答题。4.5 成绩导入导出和前端展示成绩导出的需求很常见教师希望把班级成绩导出成Excel。我用后端Apache POI生成.xlsx文件前端用el-dialog展示下载链接。这里有一个细节文件流下载时如果后端返回的是二进制流前端需要设置responseType: blob否则会得到乱码。我最初没有设置导致Excel文件打不开排查了半天才发现是响应类型问题。后来我在前端封装了一个download方法const res await axios.post(/api/score/export, params, { responseType: blob }) const url URL.createObjectURL(new Blob([res.data])) const link document.createElement(a) link.href url link.download 成绩导出.xlsx link.click() URL.revokeObjectURL(url)这样导出功能完整可用。配套的分数查看页面我还做了班级平均分和个人最高分的对比展示用到一个简单的平均值计算方法和排序算法这部分代码适合写进论文里的“系统实现”章节因为算法过程好描述。5. 论文撰写与答辩亮点建议做完系统之后很多同学卡在论文上。这里分享一点我的体会毕业设计论文不是把系统每个功能都写一遍而是要突出“你做了哪些设计决策这些决策为什么合理”。例如在介绍在线考试流程时可以画一个时序图答辩PPT中可用正文用文字描述说明前端从进入考试到自动交卷的事件流配合代码展示关键逻辑导师会认为你真正理解了系统的实现过程。技术亮点上我建议重点写三点基于时间戳的倒计时方案和自动交卷机制。针对前端课程代码题的安全沙箱执行与自动判分设计。前后端分离架构下的角色权限控制与数据隔离。这些内容不需要写得多么高深关键在于把过程和坑讲清楚。比如沙箱执行你可以写“经测试死循环代码会在3秒内被强制终止未出现服务器宕机”这比空谈安全机制更有说服力。另外设计模式的应用也可以加进去。我的代码里用到了观察者模式答题状态变化后自动更新侧边栏题号状态、策略模式不同题型调用不同的判分器、工厂模式根据题型创建对应的编辑器组件。这三个设计模式不需要刻意引入但当你代码结构恰好合适时在论文里写出来会让导师觉得你具备软件工程素养。在答辩时我建议准备好真实演示环境不要只放截图。重点演示三个流程学生进入考试、编写代码运行并提交教师人工批阅代码题并打分管理员查看成绩统计图表。演示过程中如果能把断网后的自动保存恢复功能也展示一下答辩印象分会很高。当年我答辩时刚好有学生切屏被记录演示了切屏日志模块台下老师立刻来了兴趣追着问了五分钟最后评价是“这个系统有真实应用价值”。最后再分享一个小技巧整个项目开发时一定要用Git做版本管理每次功能完成就提交一次写论文时能准确回忆每个模块的开发时间也能展示代码迭代的完整性。哪怕只有一个人开发也建议保留提交历史这不仅是好习惯也是答辩时展示工程能力最简单直接的方式。