ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Flask和Vue的C语言上机考试系统设计与实现

基于Flask和Vue的C语言上机考试系统设计与实现 做C语言上机考试系统这件事听起来像是个课程设计但真上手后你会发现它其实是一个典型的“小而全”的全栈项目既要处理题库、组卷、评分这些业务逻辑又要照顾到考试场景下学生、老师、管理员三种角色的差异还得保证考试过程稳定不翻车。我选的组合是Python Flask做后端Vue做前端数据库用的MySQL整个系统做下来最大的感受是Flask足够轻Vue足够灵活两者配合非常适合这种需要快速迭代、逻辑又不太复杂的教学类系统。这篇文章把我的完整实现思路、关键代码、踩坑记录都整理出来从题库导入到在线考试再到自动评分每一步都尽量讲清楚“为什么这么做”希望对正在做类似系统或者准备做课程设计的同学有实际帮助。1. 整体设计与技术选型为什么是FlaskVue而不是其他组合1.1 技术选型背后的考量先聊技术选型。C语言上机考试系统和普通的理论考试系统最大的区别在于它不仅要管“题”还要管“代码的运行环境”。也就是说学生写完代码之后系统得能够在服务器端把代码编译运行并且用预设的测试用例来判定对错。这决定了后端不能太重也不能太“死板”必须能方便地调用系统命令、处理进程、拿到编译输出。当时我面前有几个选择。Java Spring Boot确实强大自带的东西很多但对于这种规模的系统来说Spring Boot的项目结构、依赖管理、部署成本都偏重课程设计或者实验室内部系统根本不需要那么“工业级”的复杂度。Node.js的Express也很轻但Node调用外部编译进程、处理多进程并发的时候写起来没有Python顺手。Python的Flask框架足够轻量路由和请求处理的抽象层次刚好合适配合subprocess模块调用gcc编译器天然契合“编译并运行学生代码”这个核心需求。选Vue做前端则是看中了它的组件化开发方式加上生态成熟管理后台和考试页面可以完全拆成两套视觉体系来做代码组织起来不混乱。一个有意思的细节是Flask的Jinja2模板其实也能直接渲染页面但那样会把前后端耦合在一起代码写得越长越难维护。用Vue做前后端分离之后前端通过AJAX请求后端接口后端只负责输出JSON数据。这样做的好处是题库管理界面、在线考试界面、成绩统计界面三套页面可以并行开发互不干扰而且部署时Vue打包出来的静态文件可以直接丢给Nginx托管Flask只负责API职责清晰。1.2 功能模块与角色权限拆解系统面向三类用户每类用户的“主战场”不同。管理员负责系统的基础配置、学生账号管理、题库的导入和整理老师负责组卷、发布考试、查看成绩和分析答题情况学生则是考试的主角登录后参加考试、查看自己的成绩和答题详情。这三类角色的权限边界必须清晰。我在设计数据库时用户表里直接加了一个role字段取值是admin、teacher、student三种接口层面通过装饰器统一做角色校验避免每个视图函数里都写一遍判断逻辑。考试表、试卷表、答题记录表也都带上了关联字段从“谁出的卷”到“谁答的题”到“得了多少分”整条链路都能追溯。功能上拆成几个大块题库管理支持单题录入和批量导入、试卷管理手动选题和自动组卷、在线考试倒计时、代码编辑、自动保存、禁切屏、自动评分编译运行测试用例、成绩统计班级维度、知识点维度。每个大块展开后涉及的细节都不少后面逐个讲。1.3 题库设计不只是存文本那么简单题库是整个系统的心脏设计得好不好直接决定评分算法的复杂度和出卷的灵活性。我建的question表包含以下字段id主键title题目名称比如“计算5*5矩阵的鞍点”content题目描述正文支持多行文本包含输入输出格式说明difficulty难度1到5整数knowledge_point知识点标签比如“循环”“数组”“指针”“结构体”sample_input、sample_output示例输入输出用于前端展示和基础验证test_casesJSON格式的测试用例数组每个用例包含输入和期望输出time_limit时间限制默认2000毫秒created_at创建时间为什么测试用例要单独用JSON存而不是建一张独立的表从数据库范式角度讲确实应该建test_case表做一对多关联但实际开发中我发现题目和测试用例的关联基本只会“整体读取”很少单独修改某一个用例。用JSON字段存储可以简化查询逻辑一次查出题目所有信息减少一次关联查询。代价是不能在数据库层面按用例做统计但对于这个系统规模来说完全不值得引入额外的表。知识点的标签化很重要。C语言的知识点相对固定数据类型、运算符、分支、循环、数组、函数、指针、结构体、文件操作。这些标签不仅用于组卷时按比例抽题还用于考试后的成绩分析——统计班级在“指针”这个知识点上的得分率老师就能知道哪些内容需要重点讲。成绩分析的前提是组卷时有按知识点配比的能力后面讲组卷算法时会细说。2. 核心细节解析与实操要点从题库导入到自动评分2.1 题库批量导入用Python脚本解析C语言练习题手工逐题录入效率太低尤其是题库初始化和扩充阶段。我写了一个基于Python的导入脚本支持从两种格式的文件批量导题。第一种是JSON格式字段一一对应数据库列最简单直接第二种是自定义文本格式兼容从现有文档或题库网站复制的题目。具体自定义格式是这样的以#题目分隔标题在下一行然后是描述、输入说明、输出说明、示例输入、示例输出、测试用例、知识点、难度。脚本逐行解析遇到#题目标记就说明前一道题解析完毕把数据插入数据库。整个脚本跑一遍能导入数百道题比手工录入快几个量级。解析脚本的要点在于容错。题库来源不一空行、多余空格、注释符号都可能出现。针对这些情况我做了这几件事去掉每行首尾空白字符忽略空行题目描述、示例输入输出等长文本字段统一用三引号包裹中间允许换行测试用例要求是input...和output...配对格式缺失任何一个就跳过该题并打印警告导入脚本运行一次后输出统计信息成功导入N道跳过M道每题跳过原因。这样即使试卷来源格式混乱也能快速定位问题。2.2 自动组卷算法按知识点、难度、题量配比抽题自动组卷不是简单地随机选N道题而是要满足老师设定的“知识点配比”和“难度分布”。举个例子老师可能希望一份试卷里“指针”知识点占30%、“数组”占30%、“循环”占20%、“结构体”占20%同时简单题占40%、中等题占40%、难题占20%。我是用“分层随机抽样”的思路来解决的。先把所有题按知识点分组然后对每个知识点内部按难度再次分组最后从每个“知识点-难度”格子中随机抽取指定数量的题。比如“指针-简单”这个格子需要2道题就从题库中把所有“指针且简单”的题目收集起来随机打乱后取前2道。这样做的好处是分布可控而且避免了“全凭运气”的纯随机出卷导致难易不均的问题。关键代码是三层嵌套循环最外层遍历知识点配比中层遍历难度档位内层执行随机抽取代码非常精简。有一个边界情况要处理某个格子里的题量不够比如“结构体-困难”只有1道题但要求抽2道这时给出友好提示并且整体跳过该知识点配比不让老师拿到一份缺题的试卷。2.3 在线考试分数判定的核心调用gcc编译并运行为可执行文件自动评分是上机考试系统的灵魂。学生提交的是C语言源代码系统要做的是把这份源代码编译成可执行文件然后用多个测试用例去对比运行输出和期望输出。具体流程是这样的前端把学生代码作为字符串POST到后端接口接口收到代码后先写入一个带时间戳和用户ID的文件文件名类似submit_20250321_153001_user001.c然后用subprocess调用系统gcc命令编译import subprocess, os, uuid def compile_code(code, work_dir): c_file os.path.join(work_dir, fmain_{uuid.uuid4().hex}.c) exe_file os.path.join(work_dir, fmain_{uuid.uuid4().hex}.exe) if os.name nt else os.path.join(work_dir, fmain_{uuid.uuid4().hex}) with open(c_file, w, encodingutf-8) as f: f.write(code) compile_result subprocess.run( [gcc, c_file, -o, exe_file, -stdc99, -Wall], capture_outputTrue, textTrue, timeout10 ) if compile_result.returncode ! 0: return {status: compile_error, output: compile_result.stderr} return exe_file编译成功后每个测试用例的测试方式是把输入数据写入文件然后用管道方式重定向给可执行程序运行捕获程序的输出和退出码def run_test(exe_file, test_input, time_limit2000): try: result subprocess.run( exe_file, inputtest_input, capture_outputTrue, textTrue, timeouttime_limit / 1000 ) return {returncode: result.returncode, stdout: result.stdout} except subprocess.TimeoutExpired: return {timeout: True}评分标准是三档完全没有通过编译则记0分并展示编译报错信息通过编译但测试用例有部分失败则按“通过的测试用例数/总测试用例数”折算比例得分全部用例通过得满分。如果运行超时则视为该用例未通过。这里有个我吃过亏的细节Windows系统和Linux系统下编译产物的命名规则不同Windows下gcc默认生成a.exeLinux下生成a.out虽然可以在命令行加参数指定名称但跨平台时路径分隔符和扩展名处理稍不注意就是坑。我的做法是写了一个跨平台辅助函数统一拼接可执行文件路径按os.name做分支处理。另一个教训是代码中如果包含中文注释Windows控制台默认编码是GBK学生代码整体按UTF-8保存编译时不会有问题但运行时如果学生printf输出的中文会乱码。这个属于环境问题我做法是在考试须知里要求学生编码统一使用UTF-8同时题目中尽量不用中文输出只判断标准英文输出的测试用例。2.4 考试防作弊与稳定性禁切屏、自动保存、断线续考在线考试比离线考试多了一个“实时性”的要求所以必须考虑几个场景切屏被检测到、浏览器意外关闭、网络断断续续。我的方案是禁切屏检测通过监听浏览器的visibilitychange事件实现页面一旦切换到后台就记录一次切屏日志超过设定次数直接标记为作弊嫌疑。这个检测由后端接口配合记录前端只是上报最终判定权交给老师。这道逻辑做起来不难但很有用C语言上机考试中最常见的作弊方式就是切出去查资料。自动保存针对的是代码编辑器。我用的编辑器是CodeMirror设置一个定时器每30秒自动把编辑器内容提交到后端接口保存草稿。就算学生中途浏览器崩溃重新登录后还能把草稿拉回来继续写。断线续考和自动保存配合使用后端记录“最近心跳时间”和“最近保存内容”学生重新登录后拉取最新草稿继续作答。实际考试中遇到过几次机房网络抖动的情况这个机制确实救了不少学生试卷进度没丢。3. 实操过程与核心环节实现前端Vue与后端Flask的完整交互3.1 Vue项目搭建与环境配置前端我用了Vue 3加Vite构建相比Vue 2的Webpack版本Vite的开发服务器冷启动快得多修改代码热更新也顺畅。安装Vue环境这件事其实很简单按顺序执行npm create vitelatest exam-frontend -- --template vue cd exam-frontend npm install npm install vue-router4 axios element-plus npm run devElement Plus是Vue 3配套的UI组件库表格、表单、弹窗、消息提示这些做后台管理界面时直接拿来用比自己写CSS快非常多。但我必须承认Element Plus的样式风格偏“后台感”用来做学生考试界面不是特别合适。我的做法是管理后台全部用Element Plus学生考试页单独写了一套样式白底黑字居中布局尽可能接近真实考试系统那种安静专注的氛围。路由结构方面我按角色划分了路由层级。登录页是公共路由登录成功后根据角色动态添加路由/admin题库管理、用户管理、知识点统计/teacher试卷管理、考试管理、成绩分析/student考试列表、参加考试、成绩详情Vue Router的addRoute方法可以在运行时动态挂载路由配合beforeEach钩子做登录状态和角色校验。这里要特别注意刷新页面后路由会重置所以需要在全局状态中保存当前用户角色信息刷新后再按角色重新挂载路由。3.2 Flask后端结构与接口设计Flask后端我采用了蓝图的组织方式按功能模块拆分了几个文件而不是把所有路由堆在app.py里。目录结构如下exam_api/ ├── app.py # 应用入口注册蓝图初始化扩展 ├── config.py # 配置项数据库连接、密钥、上传目录 ├── models.py # SQLAlchemy 数据模型 ├── auth.py # 登录注册、JWT鉴权 ├── admin_bp.py # 管理员相关接口 ├── teacher_bp.py # 教师相关接口 ├── student_bp.py # 学生考试相关接口 ├── judge.py # 编译运行核心模块 └── utils.py # 通用工具函数接口设计遵循RESTful风格核心接口大致如下方法路径说明POST/api/auth/login登录返回JWT令牌和角色信息GET/api/questions分页获取题库支持关键词和知识点筛选POST/api/questions/batch批量导入题目POST/api/exams/generate自动组卷POST/api/exams/id/publish发布考试GET/api/exams/id/paper获取当前学生考试的题目列表POST/api/exams/id/submit提交某道题的代码并评分POST/api/exams/id/finalize交卷结束考试GET/api/exams/id/scores获取考试成绩列表这里有个值得说一说的设计决策考试过程中前端调的是“提交单题”的接口而不是整卷一次性提交。这样每道题都能立刻获得评分结果反馈到前端学生可以看到自己哪道题通过了哪些用例。考试结束时再调一次“交卷确认”接口把答题记录状态从“作答中”改为“已交卷”。JWT鉴权用flask-jwt-extended扩展实现登录成功后生成访问令牌前端把令牌存到localStorage每次请求时通过axios拦截器自动在请求头加上Authorization: Bearer token。这比传统Session方案更适合前后端分离架构因为Flask后端可以被同一个前端多台服务器轮询调用不需要考虑Session同步的问题。3.3 前后端联调与跨域处理开发阶段前后端分别在两个端口运行Vite默认端口5173Flask默认端口5000浏览器直接跨域请求会被拦。解决方式有两种一是Flask端用flask-cors扩展开启CORS二是Vite配置server.proxy代理把/api前缀的请求转发到Flask服务。我两种都试过实际开发时用Vite代理更方便因为看起来请求完全同源不需要修改Flask的CORS规则。// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })部署时反过来需要在前端打包后的静态目录上配置反向代理让/api前缀的请求同样转发到Flask服务。Nginx配置示例如下location /api { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这么做的最大好处是浏览器只需要访问一个地址不暴露Flask的内部端口同时避免了很多因为跨域导致的前端字体、图片资源加载失败的问题。3.4 考试流程中的关键页面与交互细节学生考试页面是系统中交互最复杂的部分。页面分为三栏左侧是题目列表和知识点标签中间是CodeMirror代码编辑器右侧是题目描述、示例输入输出和“运行/提交”按钮。CodeMirror集成时要注意Vue生命周期问题。编辑器实例要在组件mounted时创建beforeUnmount时销毁否则会出现编辑器重复初始化、内容无法更新的问题。我的代码大致是这样import CodeMirror from codemirror import codemirror/mode/clike/clike import codemirror/addon/edit/matchbrackets onMounted(() { editor CodeMirror.fromTextArea(textareaRef.value, { mode: text/x-csrc, lineNumbers: true, matchBrackets: true, indentUnit: 4, tabSize: 4 }) editor.setValue(currentCode.value || defaultTemplate) }) onBeforeUnmount(() { if (editor) { editor.toTextArea() editor null } })考试倒计时是另一个需要小心处理的点。倒计时不能只在前端做本地计时因为学生刷新页面计时器就会重置。我的做法是后端记录考试开始时间和考试总时长前端每次进入页面时向后端同步剩余时间然后前端用每秒递减的方式显示同时每60秒再向后端校准一次避免因网络延迟产生时间偏差。自动保存的接口在保存成功后不返回复杂数据只告诉前端“已保存”前端配合一个提示动画让学生有“系统在帮我存着”的安全感。这个体验细节看起来不起眼但实际考试时学生非常在意自己的代码有没有被保存上。4. 常见问题与排查技巧实录4.1 编译环境搭建Windows、Linux与虚拟机的坑编译环境是这块最容易出问题的地方。学生反馈“我代码本地跑得好好的为什么系统里编译不过”大多数时候是环境差异导致的。最常见的情况有以下几种第一gcc版本和默认C标准不同。有些题目用了for (int i 0; i n; i)这种C99语法而老的gcc版本默认可能是C90标准会报“for loop initial declaration”错误。我在编译命令里显式加了-stdc99参数统一标准避免版本差异导致的误判。第二缺少limits.h等标准头文件时某些极端测试数据会出问题。比如计算5*5矩阵鞍点时常规做法是先把每行的最大值和每列的最小值求出来再逐个位置判断。如果不初始化用于比较的变量默认初值不确定可能首行首列的值恰好就是最大值导致漏判。这类问题靠编译参数解决不了只能靠题目设计时给出明确的边界说明同时在测试用例中包含边界数据比如全零矩阵、单元素矩阵、负数矩阵等。第三虚拟机里装Ubuntu配置C语言环境时容易漏装build-essential这个元包导致系统里只有编辑器没有gcc编译器。我在部署文档里专门强调了这个安装命令sudo apt update sudo apt install build-essential gcc --version如果线上一次性有几十个人同时交卷每个交卷都触发一次gcc编译服务器的CPU占用会瞬间飙升。实测下来4核8G的云服务器能承受约30个并发编译任务再多就会出现编译超时。为了解决这个问题一是把编译超时时间设置得保守一点二是给编译进程加了并发信号量限制超过并发数时让请求排队等待。4.2 判题逻辑细节标准化输出、超时控制与资源隔离判题逻辑中最容易误判的是“输出比对”的精度问题。学生代码输出的结果如果在行尾多了一个空格、结尾多了一个换行严格按字符串对比就会判为不通过。我在比对时做了规范化先把期望输出和实际输出的字符串都按行拆分去掉每行的首尾空白字符再忽略末尾的空白行最后逐行比对。超时控制同样重要。如果学生代码里有无限循环while(1)是合理的题目要求但如果是逻辑错误导致的死循环进程会一直占用CPU。subprocess.run里设置timeout参数可以强制杀掉超时进程但要注意程序运行中产生的子进程不会被自动杀掉。我见过一次学生代码偷偷fork了子进程subprocess.run超时后主进程被杀但子进程还在跑一直占着CPU。解决方法是使用start_new_sessionTrue创建新的进程组超时后通过os.killpg杀掉整个进程组import subprocess, os, signal result subprocess.run( exe_file, inputtest_input, capture_outputTrue, textTrue, timeout2, start_new_sessionTrue ) # 超时处理 try: result subprocess.run(..., timeout2, start_new_sessionTrue) except subprocess.TimeoutExpired as e: os.killpg(os.getpgid(e.pid), signal.SIGKILL)这种写法能确保即使程序自身有子进程也能一并清理干净不会在服务器上留僵尸进程。4.3 Vue前端调试路由拦截、动态路由刷新失效、编辑器取值前端遇到的问题也不少挑几个典型的说一说。Vue Router的beforeEach守卫里做登录判断时最忌讳的是只用“本地是否有token”来判断登录状态。token存在但已过期或者后端重启导致token失效都会出现前端显示已登录、后端接口全报401的情况。我的做法是axios拦截器收到401响应时清空本地token并强制跳转到登录页同时在登录页面保留一条提示信息告诉用户“登录已过期请重新登录”。动态路由在刷新后失效是一个经典问题。学生登录后本来有考试入口F5刷新一下就变成空白页或404因为动态路由是运行时挂到路由表里的刷新后路由表恢复初始状态动态路由丢失。解决的办法是把当前用户的角色信息持久化到localStorage在应用初始化时读取角色并重新挂载对应路由等路由ready后再渲染页面。代码逻辑不复杂但顺序很重要必须先挂路由再创建Vue实例。CodeMirror取值的坑在于如果你用普通的textarea绑定v-model编辑器并不会自动同步内容到textarea直接提交时拿到的是空字符串或初始值。需要显式调用editor.getValue()来取得最新内容。自动保存时也调用这个方法不能依赖v-model。4.4 考试过程中的异常处理断电、断网、误操作线上考试最怕的不是题目难而是环境出问题。我实际遇到过机房集体断电的情况学生写了一半的代码全部消失。虽然我做了自动保存但自动保存是每30秒一次断电前最后30秒的内容确实可能丢。后来又补了一个机制监听浏览器offline事件从正常上网状态切换到离线状态时立刻触发一次保存请求这样网络断开前往往还能救回最近的内容。误操作方面学生可能不小心点了交卷按钮。我的处理是交卷按钮点击后弹出二次确认框内容包含“确认交卷交卷后无法继续作答”的提示同时必须在后端记录“已交卷”状态后才能结束考试。万一学生误触交卷老师端有一个“重置考试状态”的按钮可以把考试重置为“作答中”学生重新登录就能继续考试。这个功能老师在考试结束后再决定是否开放考试中重置需要老师确认操作防止学生自己乱来。4.5 部署过程中的数据库与文件权限问题Flask后端部署时常用Gunicorn作为生产服务器官方推荐的运行方式里有一个容易被忽视的点Gunicorn默认使用多进程模型每个worker进程都是独立的如果代码里用了进程内缓存或简单的内存变量存状态多个worker之间是不共享的导致的结果就是A用户请求落在worker1上保存了状态下一次请求落在worker2上发现状态丢失。我的系统因为鉴权用了JWT不依赖内存Session所以这个问题没遇到但如果是传统Session方案的Flask应用部署时就要改成共享存储或者用flask-session的Redis后端。文件权限的问题主要集中在“上传目录可写”这一点上。编译过程中生成的临时.c文件和可执行文件都在工作目录里如果是root用户启动的Flask服务写入没问题但如果用普通用户启动Gunicorn工作目录的写权限没开好编译就会失败。而且工人进程之间互相删除临时文件的情况也需要通过带唯一后缀的文件名来避免我用uuid保证文件名的全局唯一性不会出现两个用户同时交卷时互相覆盖的情形。5. 题目设计经验与题库内容建设5.1 从C语言基础题到经典题的收集整理题库的质量决定了系统好不好用。收集题目时我的原则是“经典优先、由易到难、覆盖面广”。从基础的数据类型定义与变量分类、printf与scanf格式控制到分支结构比如九九乘法表的多种输出格式、循环结构字符串逆序、数值统计、数组与矩阵处理最典型的就是5*5矩阵鞍点问题再到函数、指针、结构体、文件操作每个知识点都保留了有代表性的经典题。浙江大学翁恺老师的C语言练习题给我提供了很好的选题思路题目的叙述风格简洁、边界条件明确非常适合作为上机考试题。比如字符串逆序那类题考察的是数组下标操作和循环边界条件学生容易在边界处出错正好作为中等难度题。5.2 测试用例设计边界、特殊值、性能测试用例是自动评分的核心依据。每个题目我至少设计5组测试用例按照“常规输入、边界输入、特殊输入、大输入、错误输入”的思路来覆盖。以55鞍点问题为例常规用例是一般矩阵边界用例是全零矩阵此时每个位置都是行最大值、列最小值需要输出特殊的“无鞍点”提示而不是错误结果特殊用例是负数矩阵大输入是10001000矩阵目的是检验时间复杂度和内存使用。这里要特别提醒的是题目的输入格式要与测试用例严格一致否则学生程序按常规写法读取不到数据会直接判错。我遇到过一道题描述里写着“输入两个整数”但测试用例的输入文件中包含多余的空行尾缀学生程序用scanf读完后无法退出循环导致超时判错。后来我在测试用例设计时新增了一条规则必须先跑一遍标准答案代码确认它能通过全部测试用例才能把测试用例录入系统。没有这个环节“判分逻辑本身出错”的问题几乎无法察觉。5.3 知识点标签与难度校准题目入库时要打上知识点标签但每个标签的覆盖范围需要统一口径。比如“指针”这个标签既包含指针基础、指针与数组、指针与函数又包含指针与结构体。如果标签口径混乱组卷时按“指针”抽题就会抽出一堆不相关的题。我参考了C语言教材的章节划分把知识点定为两级一级标签是“基础语法、分支、循环、数组、函数、指针、结构体、文件操作、综合应用”二级标签更细但题库量大时维护成本高实际只做到一级标签加一个“扩展标签”字段用于存放更细粒度的分类。难度校准是我觉得最有必要但最难以自动化的事情。一道题是否属于“简单”“中等”“困难”仅靠出题人主观判断常常不准。我的做法是先在内部小范围测试中让学生做一遍记录每个学生的答题正确率正确率高于80%的记为简单50%到80%的记为中等低于50%的记为困难。系统上线后随着考试数据积累还可以根据实际得分率动态修正难度标签让组卷的难度分布更符合实际班级水平。6. 性能优化与并发处理实录6.1 Flask接口层面的性能瓶颈与优化Flask本身性能中规中矩真正的瓶颈集中在“编译运行学生代码”这个环节。一般请求几十毫秒就返回了编译一个C程序至少几百毫秒加上运行测试用例可能超过一秒。应对并发交卷压力我做了两件事。第一把“编译运行”的耗时操作放到线程池中执行。Flask的接口处理是同步阻塞的如果并发请求都来编译进程会被占满。用ThreadPoolExecutor把编译任务提交给线程池接口本身可以快速返回一个任务ID前端再轮询查询结果。这样接口的并发能力大幅提升不会因为某个代码编译太慢而拖垮所有请求。第二限制同时编译的数量。信号量threading.Semaphore(4)控制最多同时4个编译任务在执行其余的排队等待。这个“排队”不是接口层排队而是编译任务在信号量处等待接口返回后前端拿到的是“判题中”状态等编译任务完成后由异步回调更新结果。实际效果是即使有30个学生同时交卷接口也不会超时只是每个学生的等待时间变长了一点。6.2 数据库查询优化与索引设计题库表、考试表、答题记录表、用户表的核心查询都加了索引。最常用的是question.knowledge_point和question.difficulty的联合索引因为自动组卷时大量按这两个字段筛选exam_record.student_id加索引因为学生查自己成绩记录是高频操作exam.student_id加索引用于查某学生所有考试一个容易被忽略的点是考试开始时需要批量创建答题记录如果循环一条条INSERT几百道题几百个学生就是上万条INSERT数据库性能根本扛不住。我用bulk_insert_mappings一次性插入所有记录几秒钟搞定。6.3 前端性能大题库的懒加载与虚拟滚动题库列表如果几百道题一次返回前端渲染也会卡顿。我用分页接口配合Element Plus的el-table自带分页功能每页20条。题目内容包含长文本展开详情时才从后端请求题目完整描述列表只显示标题和标签避免一次传输大量MB级JSON数据。考试时题目列表加载也做了优化学生进入考试页面后端只返回每道题的题号和标题页面先渲染出列表。点击具体某道题时再请求该题的完整描述和当前已保存代码。这样即使是500人的大考场数据库和网络的负载都被控制在合理范围内。个人心得体会是只要是做考试系统一定要把“异常处理”当成一等公民来对待。功能开发可能只占一半时间另一半时间都在处理“学生网络断了怎么办”“代码编译超时怎么办”“考试中误点了交卷怎么办”这类问题。把这些异常场景提前想清楚考试当天才不至于手忙脚乱。代码规范方面编译参数、超时时间、比对规则都在配置文件中统一维护遇到新问题只需要改配置不碰代码维护成本低很多。后续如果要扩展这个系统我觉得有两个方向很有价值。一是把判题能力推广到其他语言比如Python判题只需要调整编译运行命令核心判题模块完全可以复用。二是增加考后试卷分析与反馈比如把学生的代码保存下来老师可以回看某道题哪些学生卡在了哪个测试用例上相当于自动定位教学重难点。这套系统目前已经稳定运行了一段时间后续计划加入Python题库的支持和新版界面的适配到时候再单独写一篇分享。
返回列表