资讯动态

SpringBoot+Vue+MySQL编程训练系统设计:从判题到权限控制的全栈实战

发布时间:2026/10/16 6:50:40 来源:尧图企业网站定制
最近连续好几个学弟学妹问我毕设选题的事聊下来发现一个共同点都想做一个“看起来有技术含量、工作量适中、又不至于烂大街”的系统。编程训练管理平台恰好是这类项目里最稳的选择之一——它既有用户管理、权限控制这类常规业务又有代码提交、自动判题这种能体现技术深度的模块而且前后端分离的架构天然适合写进简历。这篇文章就把基于 SpringBoot Vue MySQL 的编程训练系统管理平台从需求设计、数据库建模、后端接口、前端联调到部署上线的完整链路拆开讲一遍尤其是判题模块和权限控制这两块很多教程都一笔带过我这边把关键逻辑和踩过的坑都说清楚。这套系统适合三类人参考一是拿它当毕设或课设的在校学生二是想完整走一遍全栈项目流程的初学者三是需要给学校或培训机构搭一个轻型刷题平台的开发者。无论你属于哪一类核心思路都是一样的——先把业务边界划清楚再把数据模型建稳最后才是堆功能。1. 项目整体设计与技术选型思路1.1 需求定位这套系统到底解决什么问题编程训练系统说白了就是一个让学习者刷题、让老师布置作业、让管理员管理资源的在线平台。市面上的开源OJ在线判题系统很多但大部分功能冗余、部署复杂不适合拿来当课程设计级别的项目。自己做一套轻量化的训练平台重点不是做得多大而是把链路走通学生登录 → 选择题目 → 编写代码 → 提交运行 → 系统判题 → 返回结果 → 老师查看统计数据。这个闭环如果完整实现了项目的深度就有了。我在实际做的时候把系统拆成三个端管理员端负责用户管理、题目审核、公告发布教师端负责题目创建、训练任务布置、查看学生提交情况学生端负责刷题、提交代码、查看判题结果和个人统计。三个角色的权限边界是这套系统的灵魂如果这里不清晰后面的接口设计一定会乱。我的做法是在设计阶段就画清楚一个矩阵哪些操作是管理员独有哪些是教师可用哪些是学生只读。比如题目表的设计就要区分“草稿”和“已发布”状态老师创建的题目必须先经过管理员审核或者老师自己确认发布学生只能看到已发布的题目。1.2 技术栈选型为什么是 SpringBoot Vue MySQL这个组合几乎是目前国内Java全栈学习路线的事实标准选它不是为了跟风而是它确实在“学习成本”和“生产可用”之间找到了最佳平衡点。SpringBoot负责后端核心优势是自动配置和生态成熟。做这种管理系统Spring Data JPA或者MyBatis-Plus都能快速搞定数据访问层Spring Security配合JWT做认证授权跑题不大。我实际用的是MyBatis-Plus理由只有一个字快。分页插件、代码生成器都是现成的做毕设最怕时间耗在重复的CRUD上用它能省下一大半时间。Vue负责前端我用的是Vue 2 Element UI的组合。虽然现在Vue 3 Element Plus已经很成熟了但考虑到很多课设要求里老师自己更熟悉Vue 2的生态而且网上的现成案例、组件库示例更多遇到问题容易搜到答案。如果你是从零开始学选Vue 3配Element Plus也完全没问题底层思路都一样只是API写法差异。MySQL做数据存储是标准操作。这里我特别强调一个点表结构设计一定要先于编码。很多人上来就写代码写到一半发现数据表字段不够用再回头改这种返工最浪费时间。我后面会给出完整的建表SQL照着拆就能理解整个业务的数据流转。2. 核心功能模块拆解与数据库建模2.1 场景驱动从用户故事倒推功能清单做管理平台不能一上来就想着“我要做哪些页面”而是要问“用户进来要完成什么动作”。我列了十几个核心场景每个场景对应一个或一组接口学生注册登录后能看到训练列表和题目列表点进某道题能查看题目描述、难度标签和样例输入输出。学生在代码编辑器里写完代码选择语言提交系统编译运行并返回结果结果状态分为通过、答案错误、编译错误、运行超时、内存超限。教师创建题目时需要配置题目描述、输入输出样例、测试点数据和判题脚本。测试点数据在课程设计阶段可以用简单文本比对实现不需要上沙箱容器。教师发布一场训练或考试设定起止时间学生只能在时间窗口内提交。管理员查看整体数据统计包括活跃用户数、提交次数、题目通过率等。这五个场景基本覆盖了系统的核心价值。剩下的什么公告、站内信、个人中心都属于锦上添花建议放到第二阶段再做先保证主线闭环跑通。2.2 数据库表结构设计与关键字段说明数据库我设计了一共十张表核心的七张表是用户表、角色表、用户角色关联表、题目表、提交记录表、训练任务表、训练题目关联表。零散的还有公告表和分类表。先看用户表我故意把用户名和密码拆开密码存储用的是BCrypt加密绝对不要存明文这一点在答辩时经常被老师问到。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) COMMENT 昵称, email VARCHAR(100), avatar VARCHAR(255), status TINYINT DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );角色表不需要多解释就三个角色编码ADMIN、TEACHER、STUDENT。用户角色关联表是一张中间表用来支持一个用户多角色的可能虽然目前业务上不需要但保留扩展性。题目表是核心中的核心字段设计直接决定后续功能好不好写CREATE TABLE exercise ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 题目标题, description TEXT COMMENT 题目描述, input_desc TEXT COMMENT 输入说明, output_desc TEXT COMMENT 输出说明, sample_input TEXT COMMENT 样例输入, sample_output TEXT COMMENT 样例输出, difficulty TINYINT COMMENT 1简单 2中等 3困难, type VARCHAR(20) COMMENT 题目类型编程题/选择题, judge_type VARCHAR(20) DEFAULT text COMMENT 判题方式text精确匹配/hidden隐藏测试点, test_case_url VARCHAR(255) COMMENT 测试数据文件路径, creator_id BIGINT COMMENT 创建人ID, status TINYINT DEFAULT 0 COMMENT 0草稿 1已发布 2下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );提交记录表是整个系统数据量增长最快的表一定要给关键字段加索引否则数据量上来之后查询会越来越慢CREATE TABLE submission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, exercise_id BIGINT NOT NULL, language VARCHAR(20) COMMENT JAVA/PYTHON/C/CPP, code TEXT COMMENT 提交的源码, status TINYINT COMMENT 0待判 1通过 2答案错误 3编译错误 4运行超时 5内存超限 6系统错误, execute_time INT COMMENT 运行耗时ms, execute_memory INT COMMENT 运行内存KB, judge_detail TEXT COMMENT 判题详细结果, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_exercise (exercise_id), INDEX idx_status (status) );训练任务表设计时要注意一个训练包含多道题题目和训练是多对多关系所以需要训练题目关联表。训练本身有起止时间、参与班级或面向所有用户、发布状态这几个核心字段。这套表结构够用在课程设计答辩时讲很久了因为每张表都能对应到一个功能点每个字段都能解释出“为什么这样设计”。当初我把训练统计表单独拆出来就是考虑到参赛要展示“排行榜、正确率、完成人数”这些聚合数据单独建表做定时统计比每次都现算SQL高效得多。3. 后端核心实现与关键代码解析3.1 统一响应结构与全局异常处理后端项目的第一个基础工程是统一响应体。我见过很多半成品项目的接口返回格式五花八门有的返回Map有的直接返回实体类导致前端解析异常时根本无从下手。我的做法是定义一个Result泛型类所有接口统一返回这个结构public class ResultT { private Integer code; // 200成功其他为失败 private String message; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }配合RestControllerAdvice做全局异常处理这样Controller层就不用每个方法都写try-catch了代码干净很多。3.2 JWT认证与权限控制认证方案我选择JWT原理很简单用户登录成功后后端生成一个包含用户ID和角色信息的签名令牌返回给前端前端存在本地每次请求通过Authorization请求头带上后端用一个拦截器验证令牌合法性并从令牌中解析用户身份。这样做的好处是服务端不需要保存会话状态对前后端分离架构特别友好。这里有一个我在实际项目中踩过的坑JWT Token不能放用户密码等敏感信息因为Token虽然签名不可篡改但载荷部分是Base64编码的任何人都能解码看到内容。我见过有人把明文密码放进Token的这是非常危险的做法。权限控制我用Spring Security 自定义过滤器实现。核心逻辑是在过滤器中解析出用户角色然后用注解PreAuthorize(hasRole(TEACHER))标注在需要教师权限的接口上。这里要注意Spring Security的角色判断默认会加上ROLE_前缀自定义角色编码时要区分好不然会出现明明用户是TEACHER却始终没有权限的问题。3.3 判题模块的实现思路判题模块是整套系统里最容易被问“你是怎么实现的”的部分也是区分项目含金量的关键。课程设计级别的判题没必要引入Docker沙箱那套方案涉及容器隔离、资源限制工作量大且部署复杂。我的实现分两步第一步是代码保存与编译。后端收到提交请求后把源码保存到服务器指定目录文件名为submission_id.扩展名然后调用本机JDK或GCC的编译命令。这里必须用ProcessBuilder不能直接用Runtime.exec因为前者能更好地控制工作目录和环境变量。第二步是运行测试用例。对于公开样例直接把样例输入写入输入文件程序运行后读取标准输出和期望输出做文本比对。文本比对有一个经验一定要先忽略行尾空格和末尾换行否则正常能过的代码会因为多一个空格被判错。我当时用了一个简单的归一化函数去掉所有空白字符再比对虽然宽松了点但课程设计阶段完全够用。判题时还要设置超时控制否则提交一个死循环代码会直接把服务器拖垮。我用Future 超时机制的思路把运行任务提交到线程池通过get(timeout)控制等待时间超时就强制杀掉进程并把状态置为运行超时。下面这段是判题运行的核心逻辑骨架public JudgeResult runTestCase(String compileDir, String testInput) { JudgeResult result new JudgeResult(); ExecutorService executor Executors.newSingleThreadExecutor(); FutureRunOutcome future executor.submit(() - { ProcessBuilder pb new ProcessBuilder(java, -cp, compileDir, Main); pb.redirectErrorStream(false); Process process pb.start(); try (OutputStream os process.getOutputStream()) { os.write(testInput.getBytes(StandardCharsets.UTF_8)); } // ... 读取输出 process.waitFor(); return outcome; }); try { RunOutcome outcome future.get(5, TimeUnit.SECONDS); result.setOutput(outcome.getOutput()); result.setTimeUsed(outcome.getTimeUsed()); } catch (TimeoutException e) { future.cancel(true); result.setStatus(4); // 运行超时 } return result; }这里有个容易忽略的细节给子进程发送输入后一定要关闭OutputStream否则子进程读不到EOF会一直阻塞。很多人在这一步卡了半天就是因为输入流没关闭。4. 前端Vue实现与前后端联调实录4.1 前端路由与状态管理设计前端我用Vue Router做页面路由分为静态路由和动态路由两部分。静态路由是登录页、注册页、首页这种所有用户都能访问的动态路由是在用户登录后根据角色动态添加的比如管理员的用户管理页面学生的训练列表页面教师的题目管理页面。状态管理用了Vuex主要存三类数据用户登录信息、角色权限标识、当前训练或答题的临时状态。有一个教训是不要把大段的题目内容或代码提交内容放进Vuex页面刷新后Vuex数据会清空会造成体验问题。正确做法是刷新后从后端重新拉取所以接口设计时提交记录和题目详情必须支持按ID查询。4.2 代码编辑器接入与判题结果展示前端最出效果的地方是代码编辑器。我接入的是CodeMirror相比Monaco EditorVS Code的内核CodeMirror体积小、集成简单、对Vue2的兼容好做课程设计完全够用。接好之后用户可以选择语言Java/Python/C编辑器会根据语言切换高亮语法。判题结果展示我用了一个结果状态组件把后端返回的状态码映射成不同的颜色和文案——通过用绿色编译错误用橙色运行超时用红色。同时展示运行耗时和内存旁边放一个“查看详情”的抽屉把判题过程中的错误堆栈或者输出内容展示出来。这个交互细节在答辩演示的时候很加分因为直观地展示了“平台真的在判题”而不是假数据。Axios请求封装同样是重点。我统一封装了一个request模块在请求拦截器里添加Token在响应拦截器里统一处理401跳转登录页、500弹错误提示。代码不长但非常关键service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error Promise.reject(error)) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { MessageBox.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response.status 401) { router.push(/login) } return Promise.reject(error) } )4.3 前后端联调踩过的坑前后端联调是我整个开发过程中耗时最长也最烦的阶段因为很多问题只有两边真正连起来才暴露。我在这里总结三个高频问题提前知道能省不少时间。第一个是跨域问题。开发环境下我用脚手架的代理功能解决在vue.config.js里配置devServer.proxy把/api前缀的请求转发到后端端口。上线部署阶段我用Nginx把/apilocation 反向代理到后端Java进程前端只负责静态资源这样生产环境也没有跨域问题。后端同时开启CORS配置兜底但原则上能不用跨域就不用跨域。第二个是时间格式化问题。Java后端返回的LocalDateTime默认序列化后是2025-01-08T12:30:00这种带T的格式前端直接展示很难看。解决办法是在后端统一配置JSON序列化格式把日期格式化为yyyy-MM-dd HH:mm:ss。这个问题虽然小但在提交记录列表、公告列表这些涉及时间显示的地方到处都会出现统一处理一次省心很多。第三个是Long类型精度丢失。后端表主键用的BIGINT转成JSON后超过JavaScript安全整数范围的数字会被截断导致前端拿到的ID和实际不一致。解决办法是在实体主键字段上加JsonSerialize(using ToStringSerializer.class)把ID转成字符串返回或者在全局配置里处理。这个问题不遇到一次根本想不到但一旦遇到就很折腾我第一次联调时查了快两小时才发现是精度问题。5. 部署上线、常见问题与毕设答辩建议5.1 本地开发到服务器部署的完整流程项目完成后的部署同样是老师爱问的环节。我把流程梳理成了四步照着操作基本不会出问题。第一步是准备环境。服务器安装JDK 8或11、MySQL 5.7或8.0。注意MySQL 8的密码加密方式默认是caching_sha2_password老版本的JDBC驱动会连不上需要在MySQL里调整加密方式或者直接用新版JDBC驱动。第二步是初始化数据库。把建表SQL在服务器MySQL里执行一遍还要注意修改数据库连接的时区配置。我遇到过一个问题本地连数据库一切正常服务器上一直报The server time zone value is unrecognized最后发现是数据库连接URL少加serverTimezoneAsia/Shanghai参数。第三步是后端打包运行。使用Maven的package命令打成Jar包然后用nohup java -jar xxx.jar app.log 21 后台启动。每个环境多多少少有点不一致所以强烈建议把数据库连接、文件上传路径这些配置放在application-prod.yml里用spring.profiles.activeprod启动时指定。第四步是前端构建。执行npm run build生成dist目录然后配置Nginx把root指向dist目录把/api反向代理到http://127.0.0.1:8080。这里有一个注意点前端路由如果用了history模式刷新会出现404需要Nginx配置try_files $uri $uri/ /index.html;来做前端路由兜底。5.2 常见问题速查表我把自己开发过程中遇到并解决的典型问题整理成了表格按“现象—原因—解决”的方式列出方便以后排查现象可能原因解决方式登录接口报401Token无效前端没在请求头携带Token在Axios请求拦截器统一添加Authorization头有权限但接口仍然拒绝访问Security权限校验顺序问题匿名请求被拦截检查配置类中放行路径是否覆盖了登录和注册接口提交代码后编译错误服务器未安装JDK/GCC安装对应编译环境并确认PATH配置正确判题结果一直显示待判中判题模块异步线程池或时间太长没返回检查是否调用了判题服务查看日志确认线程执行情况前端请求接口跨域端口不一致或未配置代理开发环境用脚手架代理生产环境用Nginx反代数据库连接超时空闲连接被MySQL主动关闭Druid连接池配置空闲回收策略或降低wait超时时间分页数据不对MyBatis-Plus分页插件未注册在MyBatis-Plus配置中增加分页插件拦截器注意版本兼容排查问题一定要养成看日志的习惯。后端启动后用tail -f app.log跟踪日志输出前端浏览器F12看重网络请求的响应体和状态码问题定位能快一半以上。5.3 给准备拿这套系统做毕设的同学几个实操建议第一工作量要提前规划。核心五个功能模块用户角色、题目管理、训练任务、代码提交、判题结果往往就能满足答辩要求不要一上来就想做得又多又全。功能多少不是关键主线闭环是否通了才是重点把一个模块做扎实比十个模块都半吊子更有价值。第二答辩演示前一定要准备预演数据。题目列表要有几道不同难度的题提交记录要有状态为通过、答案错误、编译错误的不同样例这样演示时不会出现冷场。我在真实项目演示时一般是提前准备好两道题一道简单题现场秒过一道中等题故意留个常见错误现场演示失败这样反而能展示判题的真实性。第三关于代码审查我有个经验毕设查重和老师问问题的时候重点会放在你对核心代码的解释能力上。建议把Spring Security认证过滤器、判题模块子进程调度这两块代码反复读几遍确保能清楚地讲出每一步在做什么、为什么这么做这比能背出一万个技术名词有用得多。6. 这套系统后续还能怎么扩展项目做完交付了不代表思路就结束了。从学习和展示的角度这套系统的扩展方向其实非常多而且每个方向都对应一个可以往简历上写的技能点。最实用的扩展是把判题模块升级成Docker容器隔离方案。基于容器的隔离是真实生产环境里在线判题系统的标准做法每个提交启动一个短暂容器限制CPU和内存。改动量不大但技术深度立刻提了一个档。我当时把这个版本作为进阶目标就是把原有的子进程运行方式封装成一个接口具体实现从本地进程切换成调用Docker SDK后端其余逻辑不用改动。第二个扩展方向是增加数据统计功能。目前我只做了简单的用户提交次数和通过率统计如果引入ECharts做可视化大屏展示每日活跃用户、题目通过率热力图、训练参与趋势这类功能在答辩现场会非常亮眼。数据统计不是光靠一个SQL就能做完的需要有统计访问量的设计比如用独立表记录每个提交事件或者定时聚合任务。第三个方向是支持更多题型。目前的判题只支持编程题可以扩展选择题、填空题、简答题。选择题这一块很值得做因为很多入门训练都以选择题为主做起来本身比较简单却能拓展系统的适用场景。判题模块不需要大改新增一个judge_type即可选择题的答案是字符串比对简答题则用相似度算法做参考评分。我个人在做完这套系统后的体会是平台本身不难难的是把每个环节都打通并且讲清楚。很多同学的项目经验少不是因为没写过代码而是缺少从一个完整的业务场景出发去理解系统的视角。这套系统的价值不只是“能用”而是它覆盖了从数据库建模到前后端分离、从权限控制到进程调度的完整链路认真做一遍会对完整项目的运作方式有一个非常立体的认识。最后再提醒一点如果你是用它做毕设请务必自己把代码过一遍不要抱着“能跑就行”的心态去答辩老师们最看重的是你对每一行核心逻辑的真正理解。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑