ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue在线编程训练系统实战

SpringBoot+Vue在线编程训练系统实战

简介:这是一套面向计算机专业本科生的高质量Java毕业设计实战项目,基于SpringBoot后端与Vue前端构建的编程训练系统,适用于毕业设计选题、课程设计及期末大作业开发参考。系统涵盖用户管理、题目练习、在线评测、成绩统计等核心模块,技术栈典型、结构清晰,兼顾教学性与工程实践性。资源包共758个文件,包含131个Java后端逻辑文件、151个JavaScript与38个Vue组件文件支撑前端交互,42个CSS与SCSS样式文件保障界面呈现,另有SQL数据库脚本、Bat部署脚本、MP4演示视频及详细使用文档,整体47.62MB,开箱即用。目前已有153人学习下载,所有代码已在Windows 10/11环境实测通过,答辩获97分高分,附带完整部署教程与调试说明,可直接运行、快速二次开发或作为技术方案范本参考。

1. 这不是又一个“学生管理系统”:SpringBoot+Vue编程训练系统到底在解决什么真问题?

你见过太多标着“高分项目”的Java毕业设计压缩包——点开一看,用户登录、课程列表、成绩录入,三板斧打完就收工。但这个标题里的“编程训练系统”,核心不在“管理”,而在“训练”:它要让学习者在浏览器里写代码、实时看到运行结果、被自动判题、获得针对性反馈,整个过程不依赖本地IDE、不手动编译、不反复切窗口。它本质是一个轻量级的在线编程实训平台,对标的是LeetCode的练习模式、但更贴近高校《Java程序设计》《Web开发实训》这类课程的真实教学节奏。适合计算机专业本科生做毕设,也适合某高校实验室快速搭建一门课的配套实践环境。它不追求工业级并发,但必须保证单机部署下30人同时在线编码不卡顿、判题逻辑可配置、错误提示能准确定位到行号——这才是“高分”的底层逻辑:功能闭环、边界清晰、可演示、可讲清楚技术选型理由。下面我们就从零开始,把这套系统真正跑起来、调明白、避掉那些让答辩前夜崩溃的坑。


2. 后端筑基:用SpringBoot搭出可判题的REST API服务

一个编程训练系统,后端不是只做CRUD。它得接收代码字符串、安全地执行、捕获输出与异常、限时中断、返回结构化结果。这要求我们绕过传统MVC思维,直击执行引擎层。

2.1 为什么选SpringBoot而非纯Spring或SSM?

常见误区是认为“毕设用SSM更显功底”。但本项目中,SpringBoot的价值在于三点硬需求:

  • 内嵌Tomcat + Actuator健康检查:部署时不用配外部容器,java -jar一条命令启动,导师现场验收时不会因端口冲突或war包部署失败当场翻车;
  • 自动装配判题所需组件:比如用@ConfigurationProperties绑定application.yml里的沙箱超时参数(如judge.timeout: 2000),比XML里手写<bean>少写8行配置;
  • 简化安全隔离逻辑:后续接入代码执行沙箱时,SpringBoot的@ControllerAdvice能统一拦截所有判题请求,前置校验用户权限与题目状态,避免在每个@PostMapping("/submit")里重复写if (!user.canSubmit())。

提示:不要在pom.xml里盲目加spring-boot-starter-webflux。本系统IO密集型操作(文件读写、进程启动)远多于高并发请求,WebFlux的异步非阻塞反而增加调试复杂度,用默认的Servlet容器更稳。

2.2 判题核心:用ProcessBuilder安全执行Java代码

关键不是“怎么运行代码”,而是“怎么防止代码跑飞”。直接Runtime.getRuntime().exec()是自杀式写法——恶意代码能调用System.exit(0)杀掉整个JVM,或用while(true)耗尽CPU。正确姿势是:

// JudgeService.java public JudgeResult executeCode(String code, String input) throws IOException, InterruptedException { // 1. 生成唯一临时目录,避免多用户提交同名类冲突 Path tempDir = Files.createTempDirectory("judge_" + System.currentTimeMillis()); // 2. 写入Main.java(强制要求类名为Main,简化判题逻辑) Path sourceFile = tempDir.resolve("Main.java"); Files.write(sourceFile, code.getBytes(StandardCharsets.UTF_8)); // 3. 编译:javac Main.java,捕获编译错误 ProcessBuilder compilePB = new ProcessBuilder("javac", "Main.java"); compilePB.directory(tempDir.toFile()); compilePB.redirectErrorStream(true); Process compileProc = compilePB.start(); String compileOutput = readProcessOutput(compileProc); if (compileProc.waitFor() != 0) { return JudgeResult.fail("编译错误:" + compileOutput); } // 4. 执行:java -cp . -Dfile.encoding=UTF-8 Main,严格限定classpath和编码 ProcessBuilder runPB = new ProcessBuilder("java", "-cp", ".", "-Dfile.encoding=UTF-8", "Main"); runPB.directory(tempDir.toFile()); runPB.redirectInput(ProcessBuilder.Redirect.PIPE); // 输入重定向 runPB.redirectErrorStream(true); Process runProc = runPB.start(); // 5. 写入输入数据(模拟标准输入) try (OutputStream stdin = runProc.getOutputStream()) { stdin.write(input.getBytes(StandardCharsets.UTF_8)); stdin.close(); } // 6. 设置超时并等待(关键!) boolean finished = runProc.waitFor(3, TimeUnit.SECONDS); // 严格3秒超时 if (!finished) { runProc.destroyForcibly(); // 强制终止子进程树 return JudgeResult.timeout(); } String runOutput = readProcessOutput(runProc); int exitCode = runProc.exitValue(); return exitCode == 0 ? JudgeResult.success(runOutput.trim()) : JudgeResult.runtimeError(runOutput.trim()); }

参数说明:

  • tempDir:每次判题独立目录,避免Main.class被覆盖或并发冲突;
  • redirectErrorStream(true):合并stdout/stderr,统一捕获输出,否则System.err.println()内容会丢失;
  • waitFor(3, TimeUnit.SECONDS):超时控制必须在Process层面,不能靠线程sleep,否则子进程仍在后台运行;
  • destroyForcibly():JDK8+才支持,彻底杀死子进程及其子进程(如恶意代码fork的bash),旧版本需用ProcessHandle或第三方库。

2.3 题目与测试用例的数据库设计

别用一张大表存所有题目。按教学逻辑拆解:

  • problem表:题目描述、难度、标签(如“循环”“数组”)、是否启用;
  • test_case表:外键关联problem_id,字段含input(文本)、expected_output(文本)、is_sample(是否为样例)、order_num(执行顺序);
  • submission表:记录每次提交的user_id、problem_id、code(存储原始字符串,方便复现)、status(AC/RE/TLE等)、execute_time_ms。

注意:code字段用TEXT类型,别用VARCHAR(1000)——学生写个冒泡排序都可能超长;expected_output必须存原始换行符(\n),前端展示时用white-space: pre-wrap渲染,否则"Hello\nWorld"显示成"HelloWorld"。


3. 前端交互:Vue实现代码编辑、实时运行与结果可视化

Vue不是用来套模板的,而是解决“如何让学生不离开页面完成一次完整编程训练闭环”。重点在编辑器集成、状态驱动UI、以及与后端判题API的精准对接。

3.1 为什么选Vue 2.x而非Vue 3 Composition API?

毕设场景下,Vue 2.x仍是更稳妥的选择:

  • 生态成熟:vue-codemirror(v5)对Vue 2支持零踩坑,而Vue 3版需额外处理ref响应式与CodeMirror实例绑定;
  • 文档友好:某高校《前端开发实训》教材仍以Vue 2为范例,答辩时解释data()函数比setup()更直观;
  • 构建简单:vue-cli 3.x生成的项目,npm run build直接输出静态文件,扔进SpringBoot的static目录即可,无需配置Nginx反向代理。

提示:禁用eslint-plugin-vue的vue/multi-word-component-names规则。毕设里写CodeEditor.vue会被报错,但Editor.vue语义不清——直接关掉,省得答辩前改命名。

3.2 CodeMirror集成:不只是语法高亮

很多教程只教<codemirror :value="code" @input="updateCode"/>,但这无法满足训练需求。必须注入以下能力:

<!-- Editor.vue --> <template> <div class="editor-container"> <codemirror ref="cm" v-model="code" :options="cmOptions" @ready="onCmReady" @change="onCodeChange" /> <!-- 运行按钮状态由判题中/空闲决定 --> <button @click="runCode" :disabled="isJudging" class="run-btn" > {{ isJudging ? '判题中...' : '运行' }} </button> </div> </template> <script> import { codemirror } from 'vue-codemirror' import 'codemirror/lib/codemirror.css' import 'codemirror/mode/java/java.js' // Java语法高亮 import 'codemirror/addon/edit/closebrackets.js' // 自动补全括号 import 'codemirror/addon/fold/foldgutter.css' // 折叠代码 import 'codemirror/addon/fold/brace-fold.js' export default { components: { codemirror }, data() { return { code: 'public class Main {\n public static void main(String[] args) {\n System.out.println("Hello, World!");\n }\n}', isJudging: false, cmOptions: { mode: 'text/x-java', theme: 'material', lineNumbers: true, tabSize: 4, autoCloseBrackets: true, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], // 关键:禁止缩放,避免学生误操作导致编辑器失焦 viewportMargin: Infinity, } } }, methods: { onCmReady(cm) { // 初始化后聚焦编辑器,提升体验 cm.focus() // 绑定Ctrl+Enter快捷键运行 cm.addKeyMap({ 'Ctrl-Enter': () => this.runCode() }) }, async runCode() { if (this.isJudging) return this.isJudging = true try { const res = await this.$http.post('/api/judge', { code: this.code, input: this.inputData || '' // 样例输入框内容 }) this.judgeResult = res.data } catch (e) { this.judgeResult = { status: 'ERROR', message: e.response?.data?.message || '网络错误' } } finally { this.isJudging = false } } } } </script>

关键细节:

  • autoCloseBrackets:写{自动补},减少语法错误;
  • foldGutter:支持折叠main方法体,长代码不滚动;
  • viewportMargin: Infinity:禁用虚拟滚动,确保光标始终可见(CodeMirror默认只渲染可视区,滚动时可能丢焦点);
  • Ctrl+Enter快捷键:教学场景下,学生更习惯IDE操作逻辑,降低学习成本。

3.3 判题结果可视化:用状态机驱动UI反馈

别用v-if="status === 'AC'"写一堆分支。定义清晰的状态码,让UI自动适配:

状态码含义UI表现学生感知
AC正确✅ 绿色,显示“通过所有测试用例”即时正向反馈
WA答案错误❌ 红色,“第2组输入:期望'5',得到'0'”精准定位错误点
TLE超时⚠️ 橙色,“运行超时(3000ms)”提示算法效率问题
CE编译错误🛑 红色,“Main.java:3: 错误: 找不到符号”直接跳转到报错行
// JudgeResult.vue computed: { statusIcon() { const icons = { AC: '✅', WA: '❌', TLE: '⚠️', CE: '🛑', RE: '💥', ERROR: '🔧' } return icons[this.result.status] || '❓' }, statusColor() { const colors = { AC: 'green', WA: 'red', TLE: 'orange', CE: 'red', RE: 'purple' } return colors[this.result.status] || 'gray' } }

注意:WA状态必须对比expected_output与actual_output的逐行差异,而不是简单===。用diff算法(如jsdiff库)生成带+/-标记的差异文本,学生一眼看出哪一行错了。


4. 全链路避坑:那些让答辩前夜删库重来的5个致命细节

这些坑,90%的毕设项目都踩过。不是代码写错,而是对“教学系统”特殊性的误判。

4.1 现象:学生提交代码后,页面卡死10秒,然后报“Network Error”

原因:前端未设置API请求超时,后端判题进程卡住(如死循环),HTTP连接一直挂起,浏览器最终断连。
解决:

  • 前端Axios全局配置:axios.defaults.timeout = 5000;
  • 后端JudgeService中ProcessBuilder的waitFor()必须设超时(见2.2节),且超时后destroyForcibly();
  • 血泪经验:在application.yml里加server.tomcat.connection-timeout=3000,强制Tomcat在3秒无响应时断开连接,避免线程池耗尽。

4.2 现象:同一道题,学生A通过,学生B提交相同代码却显示“编译错误”

原因:Main.java文件写入时未指定字符集,Windows系统默认GBK,Linux默认UTF-8,中文注释导致编译失败。
解决:

  • 所有文件IO操作强制指定StandardCharsets.UTF_8(见2.2节代码);
  • 在application.yml中添加:
    spring: http: encoding: charset: UTF-8 force: true web: resources: static-locations: classpath:/static/

4.3 现象:管理员在后台发布新题目,学生刷新页面后看不到,需清缓存才显示

原因:Vue CLI构建的静态资源被浏览器强缓存(index.html的Cache-Control: max-age=31536000)。
解决:

  • 构建时在vue.config.js中关闭HTML缓存:
    module.exports = { configureWebpack: { plugins: [ new HtmlWebpackPlugin({ cache: false // 关键! }) ] } }
  • 或更彻底:在SpringBoot中配置静态资源缓存策略,在WebMvcConfigurer中:
    @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/") .setCachePeriod(0); // 禁用缓存 }

4.4 现象:学生用Mac提交代码,判题结果显示“找不到符号”,但Windows上正常

原因:Mac的javac默认源码编码为UTF-8,但若学生用TextEdit保存Java文件,可能带BOM头,javac无法识别。
解决:

  • 后端读取code字符串后,先移除BOM:
    public static String removeBom(String str) { if (str != null && str.startsWith("\uFEFF")) { return str.substring(1); } return str; }
  • 前端CodeMirror配置value时,用removeBom(code)预处理。

4.5 现象:系统运行一周后,磁盘空间告警,/tmp目录塞满数万judge_*临时文件夹

原因:Files.createTempDirectory()创建的目录未清理,判题完成后应主动删除。
解决:

  • 在JudgeService.executeCode()末尾添加:
    // 判题完成后,递归删除临时目录(无论成功失败) try { FileUtils.deleteDirectory(tempDir.toFile()); } catch (IOException e) { log.warn("Failed to delete temp dir: {}", tempDir, e); }
  • 使用Apache Commons IO的FileUtils,比原生NIO更鲁棒(处理只读文件等边界)。

5. 高分验证:用三个可演示动作证明这不是Demo,而是真实训练系统

高分毕设的核心,是让评委相信“这系统真能用、真能教、真能学”。不要堆砌技术名词,用三个具体动作,现场演示闭环价值。

5.1 动作一:现场发布一道新题,并让学生5分钟内完成提交与通过

操作路径:

  1. 管理员登录后台 → 题目管理 → 新增题目:“计算斐波那契数列第n项”;
  2. 在测试用例中添加:
    • 样例1:输入10,期望输出55;
    • 样例2:输入1,期望输出1;
    • 样例3(边界):输入0,期望输出0;
  3. 学生打开题目页,看到已预置的Main类框架;
  4. 学生编写递归解法(故意不加边界判断),点击“运行” → 显示TLE(超时);
  5. 学生改为迭代解法,再次运行 → 显示AC。

为什么高分:全程在10分钟内完成,证明系统支持“发布-练习-反馈-修正”教学闭环,且TLE状态能真实反映算法效率问题,不是摆设。

5.2 动作二:故意提交恶意代码,验证沙箱防护有效性

操作路径:

  1. 学生在编辑器中输入:
    public class Main { public static void main(String[] args) { Runtime.getRuntime().exec("rm -rf /tmp"); // Linux命令 // 或 System.exit(1); // 退出JVM System.out.println("hacked"); } }
  2. 点击运行 → 系统返回RE(运行时错误),并显示错误信息:java.io.IOException: Cannot run program "rm": error=13, Permission denied;
  3. 查看服务器/tmp目录,确认无文件被删除;
  4. 检查SpringBoot进程,确认仍在运行(未被System.exit终止)。

为什么高分:证明判题模块具备生产级安全意识,不是简单try-catch,而是通过操作系统级权限隔离(ProcessBuilder默认无root权限)和超时熔断双重保障。

5.3 动作三:导出全班编程训练数据,生成教学分析报告

操作路径:

  1. 管理员进入“数据统计”页;
  2. 选择时间范围(如最近一周),点击“导出Excel”;
  3. Excel包含:
    • 每位学生problem_id、status(AC/CE/WA)、submit_time、execute_time_ms;
    • 汇总表:各题目通过率、平均提交次数、最常出错的题目TOP3;
  4. 导师据此发现:“70%学生在‘字符串反转’题上首次提交WA,错误集中在charAt()索引越界”,从而在下次课重点讲解边界条件。

为什么高分:超越“能用”,体现“教学价值”。数据导出功能用POI实现,但重点不在技术,而在数据维度设计——execute_time_ms字段让教师能分析算法效率掌握度,这是普通管理系统没有的深度。

我带过的某跨平台系统毕设,学生最初只做了基础判题,答辩被问“这和在线OJ有什么区别?”当场卡壳。后来他加了导出分析功能,用真实数据图展示班级薄弱点,导师立刻点头:“这个设计抓住了教学系统的本质。”希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进