
简介本资源是一个面向计算机专业学生、后端与前端开发者及算法竞赛教学者的在线代码评测系统实战项目解决OJOnline Judge平台从零搭建的核心需求涵盖用户管理、题目维护与实时代码评测等典型场景。压缩包共120个文件含89个Java后端业务逻辑与控制器类如QuestionController、UserServiceImple等、8个XML配置文件、4个YML环境配置、13张界面截图PNG及5个Git忽略规则文件整体体积仅2.13MB结构紧凑、模块清晰便于快速部署与二次开发。已有80人学习下载资源完整包含Spring Boot微服务后端与Vue 3前后端分离前端提供可运行的判题流程闭环——从用户登录、题目浏览、代码提交到沙箱评测反馈附带权限分级普通用户/管理员与分页题目查询等工程级功能是理解分布式判题架构与全栈协同开发的优质参考样本。 做在线评测系统OJ这事儿我一开始是有点抗拒的。市面上的开源OJ一抓一大把从HUSTOJ到Hydro从青岛OJ到各种教学平台随便部署一个不就能用了吗但真到自己动手做项目、或者要给实验室/课程组搭一个定制化平台的时候你会发现现成方案总有那么几个让你难受的点改样式要翻老代码、加功能要动核心表结构、想接自己的判题逻辑更是难上加难。再加上SpringBoot和Vue这套组合拳在Java生态里实在太主流了后端用SpringBoot做REST API前端用Vue做单页应用几乎成了Web项目的标准配置所以“基于SpringBoot和Vue自己写一个在线代码评测系统”就成了一个既有挑战性、又非常有代表性的练手项目。这篇文章我就完整拆解一下这套系统的设计思路和核心实现包括沙箱方案选型、判题状态机设计、测试用例管理、前端编辑器选型以及我在实际开发中踩过的那些坑。无论你是想拿这个项目做毕业设计、想深入学习SpringBootVue全栈开发还是真的想给自己团队搭一套可用的OJ这篇文章都能给你一个足够清晰的参考路线。1. 系统整体设计与技术选型1.1 为什么选SpringBootVue而不是其他方案先说结论SpringBoot Vue不是在线评测系统的最佳性能方案但绝对是最适合“快速落地、团队协作、后续维护”的方案之一。如果是追求极致性能的OJ判题核心往往会用C或Go来写评测机甚至会直接用Linux的系统调用和ptrace做进程级隔离。但现实是多数场景下我们需要的不是一个能扛住全球百万用户提交的竞赛平台而是一个满足课程作业、实验室内部训练、或者毕业设计演示需求的中小型系统。这个规模下SpringBoot的成熟生态能帮你省下大量时间Spring Security做登录认证、MyBatis Plus操作数据库、Redis做缓存和队列、Spring Schedule做定时任务这些都是Java后端开发者闭着眼都能写的组合。前端选Vue就更直接了。Vue的中文文档友好社区活跃上手曲线平滑而且用Vue CLI或Vite初始化项目、配合Element UI或者Ant Design Vue搭后台管理界面效率非常高。对于在线代码评测系统这种页面逻辑不算极端复杂的场景Vue的响应式数据绑定和组件化开发模式已经足够支撑代码编辑器、提交列表、题目详情、后台管理这些功能模块了。还有一点很关键这个技术栈的招聘市场需求量大。真话就是如果你做完这个项目要去找工作SpringBootVue的项目经验写在简历上比一个单独的C判题服务更容易被HR和面试官认可。系统设计上我们把“Web服务”和“判题服务”做了逻辑分离后面我会详细讲这一点这种架构思路本身就是面试中常见的考察点。1.2 核心模块划分一个OJ系统到底包含哪些部分在线代码评测系统不是只写个页面让用户提交代码就完了。一个真正能跑起来的OJ至少包含以下模块用户模块注册、登录、个人信息维护、角色权限普通用户、管理员。这块用Spring Security JWT就能搞定需要注意的是密码加密存储推荐BCrypt。题目模块题目的CRUD、测试用例的维护、题目分类和标签、难度等级。这里最核心的是“测试用例”和“题目”的数据结构设计我后面专门用一节来讲。提交评测模块这是OJ的心脏。用户提交代码后系统要经过“编译 - 运行 - 比对输出”三个阶段并返回AC、WA、TLE、MLE、RE、CE等判题结果。竞赛模块可选如果要做比赛功能还需要考虑竞赛的创建、报名、封榜、排名等功能。很多课程设计版本的OJ不会做这块但如果你的需求说明书里提到了“在线考试”或“编程竞赛”这部分就得提前设计。后台管理模块用户管理、题目审核、判题服务监控、系统参数配置等。这部分用Vue做管理端页面配合SpringBoot的后台接口非常模式化。实际开发中我会把项目分成两个Maven模块加一个前端工程。当然把判题核心单独拆成一个服务是后话了。最初的版本先做成单体应用把判题功能作为一个Service层来实现跑通主流程后再考虑微服务化这个节奏是比较合理的。2. 评测沙箱系统的心脏2.1 绝对不能直接在主进程里执行用户代码这是整个OJ系统最核心的一条铁律永远不要在你自己的服务器主进程里直接编译运行用户提交的代码。可能有人会问不就直接Runtime.exec(java Main)一下吗问题大了去了。用户提交的代码可能是恶意代码比如while (true) { // 死循环把你服务器CPU跑满 } Runtime.getRuntime().exec(rm -rf /);即便提交代码的用户没有恶意一个不小心写了死循环或者超大数据量的递归也足以把整个评测服务拖垮。更别说有些系统为了支持多语言还需要在服务器上装各种编译器这本身就会扩大攻击面。所以沙箱隔离不是可选项是必选项。那具体怎么做隔离有几个层次最简单的是操作系统账户隔离为每个提交创建一个临时用户用该用户的权限去执行代码。这种方式能防止一些越权操作但资源限制不好控制。命令行资源限制方案用Linux的timeout命令限制运行时间用ulimit限制内存和CPU时间。这种方式实现简单但精度不高而且同一服务器上其他用户还是能看到运行进程的明细。容器隔离方案推荐用Docker为每次提交创建一个一次性容器在容器内完成编译和运行。Docker在资源限制上有天然的机制CPU限额--cpus、内存限额--memory、网络隔离--network none、文件系统隔离、进程隔离。这基本是当前主流OJ的标准方案。2.2 Docker沙箱的具体实现思路我推荐在你设计判题服务时把代码执行拆解成三步第一步根据提交记录ID创建独立的运行目录在该目录下写入用户提交的源码文件。比如用户用Java提交就在目录下创建Main.java用C提交创建main.cpp。第二步启动一个一次性Docker容器挂载该目录到容器内部的指定路径执行编译命令。编译阶段也需要限制资源避免恶意代码在编译期就搞事。第三步如果编译成功在同一个容器里运行编译产物用测试用例的标准输入喂给程序捕获它的标准输出然后比对输出结果。Docker容器的启动命令可以这样设计docker run --rm \ --network none \ --cpus 0.5 \ --memory 256m \ --ulimit nproc64:64 \ --ulimit fsize50m \ -v /tmp/judge/123:/workspace \ -w /workspace \ sandbox-image:1.0 \ bash -c g main.cpp -O2 -o main ./main input.txt output.txt注意几个关键参数--rm容器运行完自动删除避免残留。--network none禁止容器访问网络。用户代码里面写个爬虫不行。写个Socket攻击别的机器也不行。这一条直接断网非常关键。--cpus 0.5限制容器最多使用0.5个CPU核心防止死循环打满CPU。--memory 256m限制内存使用超出直接OOM对应MLE判题结果。--ulimit nproc64:64限制进程数防止用户代码fork炸弹。--ulimit fsize50m限制文件大小防止用户代码在评测机上写大文件把磁盘塞满。这里有个容易被忽略的细节你运行Docker命令的宿主进程本身也需要确保不是在容器内运行Docker命令。简单说判题服务所在的服务器上Docker命令要能直接执行并且判题服务进程需要有权限调用Docker。如果判题服务本身跑在容器里就得配置Docker-in-Docker或者挂载Docker socket这会引入额外的安全风险我在生产环境中不太推荐。2.3 判题镜像的构建Docker沙箱需要一个基础镜像里面装好要支持的编程语言的编译器和运行时。比如要支持C/C、Java、PythonDockerfile大致长这样FROM ubuntu:22.04 RUN apt-get update apt-get install -y \ gcc g \ openjdk-17-jdk \ python3 python3-pip \ --no-install-recommends \ rm -rf /var/lib/apt/lists/* RUN useradd -m judge USER judge WORKDIR /workspace这里有个关键点创建普通用户judge然后在容器里默认以这个用户运行编译和执行命令而不是root。即使容器本身已经做了隔离但如果用户代码里真的执行了什么危险命令以最小权限运行总比root好。这是纵深防御的思路。另外每个语言最好单独做镜像。如果你只支持Java就做java-sandbox只装JRE/JDK如果只支持C就只装gcc/g。镜像越小启动越快攻击面也越小。3. 从提交到出分评测流程与状态机设计3.1 判题状态设计在线评测系统的提交记录一定有一个状态流转过程。很多初学者会忽略这一点直接用一两个字段比如hasJudged、score硬记状态结果链路一复杂就乱了。正确的做法是定义一个清晰的判题状态枚举PENDING排队中。用户刚提交还没轮到判题。JUDGING判题中。评测机正在编译运行该提交。ACCEPTED通过。输出与预期完全一致。WRONG_ANSWER答案错误。程序正常结束了但输出与预期不符。TIME_LIMIT_EXCEEDED运行超时。MEMORY_LIMIT_EXCEEDED内存超限。RUNTIME_ERROR运行期错误比如数组越界、除零、抛出未捕获异常。COMPILE_ERROR编译错误。SYSTEM_ERROR系统错误。可能是沙箱出问题、判题机异常等。状态机流转是初始状态PENDING判题开始后变为JUDGING结束后落到上面任意一个终态。如果判题中沙箱或评测机本身出了问题就落到SYSTEM_ERROR。3.2 评测队列的实现用户提交判题请求后不应该同步阻塞等待结果。因为判题可能需要几秒甚至十几秒编译多组测试用例运行如果用户在HTTP请求里一直等着前端体验会很差服务端也容易超时。标准方案是用队列解耦。用户提交代码后后端先把提交记录插入数据库状态设为PENDING然后把“判题任务”丢进队列HTTP请求直接返回“提交成功”。判题服务从队列里消费任务执行完后把结果写回数据库。队列用Redis的List左进右出或者Stream就够用。如果不想额外引入消息队列中间件Kafka/RabbitMQ对于这个项目偏重Redis是性价比最高的选择。结构大概是这样的// 提交判题任务 String taskId String.valueOf(submissionId); redisTemplate.opsForList().rightPush(judge:queue, taskId);判题服务启动一个或多个后台线程阻塞式地从队列左侧取任务while (true) { String taskId redisTemplate.opsForList().leftPop(judge:queue, 30, TimeUnit.SECONDS); if (taskId null) continue; handleJudge(taskId); }这里用while循环 leftPop阻塞简单可靠。等做完了也许可以考虑用Redis Stream的消费者组做更精细的ACK管理但单机版OJ用List已经足够。判题结束后的回调有几种做法轮询前端每1~2秒请求一次提交记录详情接口查询状态。简单易实现但有点浪费请求。WebSocket长连接判题完成后后端主动推送结果。体验更好实时性更高。Vue端可以用原生WebSocket或者Socket.js封装SpringBoot端可以用Spring WebSocket。我建议会点进阶的读者学一下WebSocket方案体验感完全不一样。3.3 多组测试用例的判题策略一道题目通常有多个测试用例可能是10个、20个甚至更多。判题时是全部运行还是逐个运行常见的做法是“逐个运行、遇错即停”。按顺序执行测试用例一旦某个用例的结果是WA/TLE/MLE/RE就立即终止后续测试提交状态为这个错误结果。这样能节约评测资源用户也能快速拿到反馈。只有全部测试用例都通过状态才是ACCEPTED。但这里有个坑如果一道题是部分得分制比如有些考试系统按通过的测试点比例给分就不能遇错即停而是要全部跑完后统计通过了多少个测试点。我做课程设计版本时题目表加了一个字段score_type支持“全对才得分”和“按测试点比例得分”两种模式。完整逻辑不复杂但一开始就要想好改动数据库字段的成本比改代码高得多。4. 题目与测试用例管理4.1 数据库设计题目表和测试用例表在线评测系统里题目problem和测试用例test_case是一对多的关系。一个题目包含信息标题、描述、输入描述、输出描述、样例输入、样例输出、时间限制、内存限制、难度、标签、是否可见等。测试用例表的设计通常包含所属题目ID、测试输入内容、期望输出内容、是否为样例样例是展示给用户看的普通测试用例对用户不可见、分值权重。一个常见误区是把测试用例直接塞到题目表的某个字段里比如用分隔符拼接字符串存储。这样做的弊端是查询、更新、断点续判都很麻烦。老老实实拆一张子表用外键关联后续你想做“某个测试点单独重判”都会方便得多。CREATE TABLE test_case ( id BIGINT PRIMARY KEY AUTO_INCREMENT, problem_id BIGINT NOT NULL, input_data TEXT, output_data TEXT, is_sample BOOLEAN DEFAULT FALSE, score INT DEFAULT 1, sort_order INT DEFAULT 0 );4.2 输出比对严谨的判题结果判断评测比对就有讲究了。很多题目输出的答案可能有行尾空格、空行不同、大小写差异等问题。如何比对才算“正确”行业里普遍采用的是“去尾空白后逐行比对”的策略忽略每行末尾的空格、忽略文件末尾的空行。把程序的输出和期望输出都先按行拆分去掉每行末尾的空白字符再逐行比对。如果要求完全一致比赛中某些严格题目的SPJ再走特判逻辑。还有一个坑是Windows和Linux的换行符差异。你在Windows下编辑的期望输出文件换行符是\r\n而程序在Linux容器里运行的输出可能是\n直接比对就会错。解决方案是统一在比对前归一化换行符把\r都去掉。4.3 Special Judge不止是文本比对如果题目要求答案不唯一比如“输出任意一个合法解”“误差小于1e-6”就需要Special Judge简称SPJ。这种情况不能只比对文本而是需要运行一个特判程序来验证输出是否合法。实现SPJ的方法是把判题程序也编译进沙箱让它接收用户的输出文件和测试用例的标准输出文件作为输入然后根据特判逻辑判断通过与否。很多考试系统的“填空题”“编程题”都可以用SPJ实现。当然如果项目刚起步测试用例都是唯一答案的ACM风格题目可以先不做SPJ。但设计时要留出扩展空间比如判题机通过一个字段判断该题目是否需要特判这样后续扩充特性时不需要大改架构。5. 前端实践Vue搭建在线做题体验5.1 代码编辑器选型在线评测系统最核心的前端组件是代码编辑器。Vue生态里常见的选项有三个textarea 简单高亮用coder或highlight.js做代码高亮交互简单适合纯展示场景但写代码的体验差。CodeMirror老牌代码编辑器功能适中体积轻支持多种语言语法高亮、自动缩进、括号匹配。很多OJ在用文档全面。适合中小型项目。Monaco EditorVS Code底层的编辑器功能最强有智能提示、代码折叠、多光标等但体积很大初始化稍慢。如果项目对编辑器要求高比如上线后要面对大量写代码用户选Monaco更专业。我个人建议毕业设计或课程项目用CodeMirror就够了它和Vue有成熟的封装库vue-codemirror集成起来非常快。如果你的系统要面向真正的编程竞赛用户直接上Monaco因为用户会非常在意编辑器手感。集成Monaco到Vue的基本思路是这样import * as monaco from monaco-editor; import EditorWorker from monaco-editor/esm/vs/editor/editor.worker?worker; window.MonacoEnvironment { getWorker: () new EditorWorker() }; const editor monaco.editor.create(document.getElementById(container), { value: #include iostream\nint main() { return 0; }, language: cpp, theme: vs-dark, automaticLayout: true });注意自动布局automaticLayout: true一定要设置否则编辑器在窗口大小变化时不会自动调整尺寸页面缩放后代码区域就可能错位。5.2 提交流程与结果展示前端提交流程建议这样设计用户点击提交按钮前端判断当前编辑器内容和所选语言是否符合要求非空、语言已选择随即向后端POST /api/submission接口。后端返回提交ID后前端立即跳转到“提交详情页”或者在本页弹出结果卡片。结果展示上我倾向于用“状态 用时 内存 通过用例数”的组合。状态展示用一个带颜色的标签比如绿色AC、红色WA、黄色TLE、紫色RE、灰色CE用户一眼就能看出结果。如果是异常错误还能展示编译错误的具体日志、运行错误的堆栈信息这个对用户排查问题帮助非常大而且实现成本不高只需在结果Detail字段里存储完整日志即可。如果追求实时反馈前端在轮询或WebSocket收到最终状态后展示一个结果动画或弹窗如果是WA把具体的“期望输出 vs 实际输出”差异展示出来类似Codeforces那样会显得很专业。5.3 题目列表与详情页题目列表页的常见设计是表格展示题号、标题、难度、通过率、标签。通过率的计算公式是“该题AC人数 / 提交总人数”显示用一个百分比进度条更直观。题目详情页则是左边显示题目描述、输入输出格式、样例右边固定显示代码编辑器。这个布局类似于在线考试系统的答题界面左侧题目、右侧代码用户在写代码的同时不用滚动页面去看题目内容。Vue实现起来就是左右两栏flex布局左边占55%右边占45%中间放一个可拖拽的分隔条体验会提升一个档次。题目详情页里还应该展示该用户在这道题的提交记录历史方便用户回顾自己之前的提交和错误原因。6. 后端接口设计与性能优化6.1 RESTful API设计要点在实现后端业务逻辑时API设计直接影响前端开发和后续排查效率。典型的OJ接口如下POST /api/user/register用户注册 POST /api/user/login登录返回JWT GET /api/problems题目分页列表支持关键词搜索、难度筛选 GET /api/problems/{id}题目详情含题目描述和样例不含隐藏测试用例 POST /api/submission提交判题参数为题目ID、语言、源码 GET /api/submission/{id}查询提交详情包括状态、结果、运行时间、内存 GET /api/submission/my当前用户的提交记录列表接口整体风格统一使用REST风格返回统一结果体比如{ code: 200, message: success, data: { } }这样前端可以统一处理异常不用每个接口都写一套判断逻辑。我见过不少项目后端接口直接返回一个裸对象出错时Field都变了前端各种判断undefined维护起来想骂人。6.2 数据库层性能优化在线评测系统最重要的数据表是提交记录表submission这张表数据量增长快、查询频率高需要重点设计。一是索引。submission表的查询场景通常是根据用户ID查提交列表、根据题目ID查该题的提交、按状态筛选提交。这三个字段user_id, problem_id, status都应该建索引。联合索引可以考虑(user_id, problem_id)作为组合查询条件。二是分页查询。提交列表查询要用order by id desc limit offset, size。数据量大之后深分页会有性能问题可以使用“延迟关联”或者基于游标的方式优化但中小系统先不要过度设计合理使用LIMIT即可。三是缓存。题目列表和题目详情这种读多写少的数据非常适合用Redis做缓存。用户提交后题目通过率要更新这时候可以缓存失效并重算。如果做得细致点还可以用Caffeine做本地缓存Redis做二级缓存但那是微服务级别的玩法了单体应用先用好Redis就足够。6.3 判题并发控制判题服务要支持并发评测不然一次提交只能跑一个用户体验极差。设计时要注意两点判题队列消费端的并发度控制如果判题机和Web服务在同一台机器上线程数不宜过多否则评测时CPU资源会被抢占影响Web服务的响应。建议按CPU核数的一半设置消费线程池大小。多评测机横向扩展如果判断服务需要支持更大并发可以拆成独立的应用通过配置消费同一个Redis判题队列多个实例并行消费。这就是一个简单的水平扩展方案了不需要ZK、不需要KafkaRedis队列天然支持多消费者竞争。这里还要注意任务隔离某些提交可能需要使用特殊配置比如某题要求栈空间更大任务队列里最好带上题目级别的参数配置由判题服务读取并应用到沙箱。7. 常见问题与排查技巧实录7.1 中文输出乱码与编译器编码问题这是OJ开发中最让人崩溃的问题之一。用户程序里写了中文输出在本地运行正常判题结果却是乱码或比对失败。排查思路首先确认编译时的字符集。C用g -o main main.cpp默认情况下编译器读取源码的编码是由locale决定的。容器里的locale如果默认是POSIX吃进去UTF-8源码输出一般是UTF-8字节流但是Java程序的源码编译时javac默认使用平台默认编码容器里如果没设置环境变量很可能读取源码用GBK结果全乱了。解决办法是编译阶段显式指定编码。# Java javac -encoding UTF-8 Main.java # Python # Python3默认UTF-8一般没问题 # C # g默认读取UTF-8没问题但Windows用户上传源码到平台时如果文件被转成GBK保存就会出问题另外在容器环境变量里统一设置LANGC.UTF-8或LC_ALLC.UTF-8让所有程序默认按UTF-8处理文本可以有效减少这类问题。7.2 内存限制不准确程序没到限额就被杀了你设置--memory 256m程序用到了150MB就被OOM杀掉查了下明明离256m还很远。为什么罪魁祸首是JVM的堆内存分配。Java程序启动时如果你不对-Xmx做设置JVM默认会按容器可用内存的比例分配堆空间。当你用--memory 256m限制容器后JVM可能认为可用内存不到256MB于是堆初始值设置得很低导致程序更容易OOM。处理方案有两种在运行Java时显式传递JVM参数比如java -Xmx128m -Xss64m Main限制堆大小为128MB栈大小64MB这样能准确把内存控制在预期范围内或者在容器内设置环境变量JAVA_TOOL_OPTIONS-Xmx128m。同理Python程序的递归深度也常常导致“明明是合法递归却报RecursionError”。Python默认递归深度是1000如果一个合法算法需要3000层递归需要在提交列表页或运行指令里加sys.setrecursionlimit(...)。或者更彻底的做法在判题运行的启动脚本里预先导入并设置python3 -c import sys; sys.setrecursionlimit(1000000) -c exec(open(main.py).read())7.3 测试点并行执行导致结果不稳定如果一道题有100个测试用例为了提高效率有些开发者会并发跑多个测试用例然后合并结果。但这里有一个隐藏问题假设用户代码用随机数生成结果或者程序内部依赖全局状态那么并发执行测试用例时每个用例都是独立的新进程你无法保证几次执行结果一致。更严重的是并发执行多个测试用例每个用例单独计时会导致总评测时间膨胀还可能因为共享磁盘IO导致某些用例执行时间飘忽不定。我的建议是对一个提交的多个测试用例按照顺序串行执行但不同提交之间可以并行。这样可以保证评测结果的确定性减少偶发性的WA或TLE。7.4 Docker镜像拉取和容器启动慢判题服务每判一个提交就创建一个新容器Docker的镜像如果没提前拉取到本地首次运行会非常慢。更麻烦的是容器每次启动都要几秒钟如果再加上镜像初始化用户体验会非常差。优化方案提前在服务器上手动docker pull把需要的沙箱镜像都拉取到本地。不要每次创建新的容器都走Dockerfile重新build直接用已存在的镜像run。对于冷启动延迟可以在判题服务启动时预热预先启动一批容器或者用Docker的apt镜像预加载库。但最简单有效的还是提前拉取镜像 精简镜像体积让镜像启动时间控制在几百毫秒内。实际上还有一些更极端的做法比如用gVisorrunsc做更轻量级的沙箱或者预创建一组常驻容器轮流复用但这些都属于进阶优化等基础功能稳了再说别一上来就给自己加难度。7.5 用户提交恶意代码的应对虽然沙箱已经做了网络隔离和资源限制但线上OJ还是要做一些基础的安全加固。注册和提交接口要加验证码或频率限制防止恶意用户刷接口、大量提交拖垮判题队列。Docker镜像不装多余工具。能装编译器就不要装curl/wget/bash的网络工具如果能不装vim就不要装vim最小化原则减少攻击面。Docker守护进程本身要配置好。不要让Web服务有直接操作Docker socket的权限判题模块最好独立部署或者至少在专用API层做权限控制。敏感数据不要出现在判题日志中。用户代码里如果写了数据库密码判题日志打印出来就泄露了所以判题模块最好不打印完整源码。实际上对于很多课程设计级别的OJ完全没有必要把安全问题想得太夸张但基本的“沙箱隔离资源限制接口限流”三个手段做到位就可以挡住绝大部分恶意提交了。8. 实操经验与优化方向8.1 开发顺序建议如果你准备照着这个思路自己写一版我建议开发顺序是这样的第一步先做通最简单的“提交-判题-显示结果”链路。不要急着美化界面、加更多语言支持。一块输入框、一个下拉框、一个提交按钮、一个结果显示区先把核心链路跑通。第二步加入题目管理和测试用例管理。没有测试用例系统就不能算OJ只能算代码提交工具。第三步用户系统和完善的权限管理。第四步优化判题体验和前端效果代码高亮、实时结果推送、结果差异展示。第五步并发压测、安全加固、部署上线。这个顺序能让你在前三步完成后就得到一个可演示的版本即使中途卡住也随时能拿出一版能跑的东西。8.2 判题机扩展方向在线评测系统如果想继续做深有几个很好的扩展方向支持更多编程语言Go、Rust、JavaScriptNode.js、C#.NET等。每种语言一个沙箱镜像判题机里做一个语言配置表。支持Remote Judge从远程OJ拉取题目提交时转发到远程OJ。这个功能比较复杂但很有亮点。支持AI辅助分析解析用户错误代码给出提示或修改建议。这属于进阶玩法利用大语言模型接口可以实现。支持可视化监控大屏用Vue Socket或WebSocket展示判题机的实时运行状态、队列积压情况、用户提交趋势。如果做比赛平台这个功能非常适合展示。8.3 部署上的一些经验项目开发完成后部署方案也是面试官常问的点。我建议用Docker Compose一键部署整个项目服务1MySQL 8存储业务数据。 服务2Redis缓存 判题队列。 服务3SpringBoot后端应用。 服务4Nginx部署前端构建产物并反向代理后端API。 服务5可选判题沙箱镜像不常驻但提前拉取到宿主。docker-compose.yml大致是这个思路但要注意判题服务所在容器的Docker权限问题。最简单的方案是判题模块不放在容器里而是直接部署在宿主机上这样它可以直接调用宿主机的Docker命令避免Docker-in-Docker的复杂性。前端构建完成后静态文件放到Nginx的html目录下Nginx配置一个/api前缀代理到后端的8080端口的服务这样前后端就通了。如果做比赛或课程演示建议提前压测一下并发场景。用JMeter或者wrk模拟多个用户同时提交观察判题队列的积压情况和平均判题耗时及时调整消费线程数。9. 写在最后几个让我印象深刻的坑做这个项目我前前后后踩过不少坑有些特别值得拿出来提醒大家。第一个坑是Windows开发环境与Linux生产环境的换行符问题。我花了一个下午排查“为什么本地写着正确服务器上判题全WA”最后发现是Windows下创建的期望输出文件带了\r\n而容器里程序输出是\n导致比对不一致。从那以后我再也不在本地直接手写期望输出文件了全部通过管理页面录入后端统一做换行符归一化。第二个坑是JVM内存限制。刚开始用Docker做沙箱时Java题目的MLE率特别高不管怎么调整--memory都感觉不对。后来才意识到是JVM默认堆参数在作怪。我给Java运行命令加上了-xmx参数后内存控制精准多了。第三个坑是Redis队列的可靠性。如果判题服务在处理某个任务时崩溃了这条任务就从队列里丢了提交会永远卡在PENDING状态。后来我改用Redis Stream的消费者组消息处理后会携带一个Pending状态可以通过XAUTOACK机制来做消息恢复。简单实现是判题服务启动时把数据库中所有PENDING状态的提交重新入队。第四个坑是CodeMirror在弹窗中的显示问题。做了竞赛模式下的“题目预览弹窗”时弹窗打开时编辑器高度算不对要等好几秒才自动调整。后来发现是没调用editor.refresh()加上就解决了。最后再说一个方向上的建议如果这个项目是给毕业设计用的你不需要把评测做的多复杂但一定要把“遇到问题、分析问题、解决问题”的过程完整记录、展示出来。面试官和答辩老师并不指望你做出来一个Codeforces他们更想看到你的思考过程和工程落地能力。这套基于SpringBoot和Vue的在线代码评测系统做完之后我觉得收获最大的不是学会了多少API怎么调而是理解了在线编程平台的本质一定要把“用户提交”和“代码执行”隔离得足够远把“评测流程”设计得足够清晰才能保证系统在复杂场景下稳定可用。希望这篇文章的拆解能帮你把同一个项目做得更稳、更强。本文还有配套的精品资源点击获取