从零打造一个支持小众语言的在线评测系统

从零打造一个支持小众语言的在线评测系统 从零打造一个支持小众语言的在线评测系统前言刷题平台大家见得多了LeetCode、洛谷、Codeforces……但这些平台几乎全是围绕 C/Java/Python 这类主流编程语言设计的。如果你是想练习 Shell 脚本、SQL 查询甚至是 Bash 命令行操作基本找不到一个像样的在线评测环境。这个项目就是从这个缺口开始的。三个月业余时间我用 PHP MySQL 搭了一个支持Bash、SQL等小众语言的在线评测系统OJ并在过程中顺手实现了一套适合学校教学场景的班级管理模式。这篇文章把设计和实现过程做一个复盘。曜渊OJ项目定位为什么做它市面上 OJ 系统的主要痛点语言支持单一99% 的平台只支持编程语言不支持 Shell/SQL 这类脚本型语言学校场景薄弱现有平台以个人刷题为主缺少班级、作业、教师管理等学校刚需功能判题耦合严重很多开源 OJ 的判题逻辑和前端耦合在一起水平扩展困难基于这三个痛点我给自己定了三个核心目标支持 Bash、SQL 等小众语言的精确评测内置学校模式——班级、教师、作业、排名一站式解决异步判题队列——解耦提交和判题支持高并发技术架构整体采用经典的 LAMP 栈但在几个关键点上做了针对性设计1. 数据库层核心表结构设计表职责problems题目基础信息支持类型、难度、标签submissions提交记录含代码、状态、分数judge_queue异步判题队列待评测任务缓冲judge_details逐测试点评测详情organizations学校/组织信息org_classes班级表学校模式的扩展org_teachers教师表org_assignments作业表支持按班级发布2. 判题引擎判题是整个系统的核心。针对 Bash 和 SQL 两种语言设计了完全不同的评测策略Bash 评测使用轻量级沙箱执行用户脚本模拟文件系统VFS提供测试环境比对标准输出逐字符 diffSQL 评测在隔离的 MySQL 测试库中执行用户 SQL对比查询结果的行数、列名、数据内容支持 UPDATE/DELETE 等 DML 语句的副作用验证判题结果统一抽象为Pending → Judging → Accepted / Wrong Answer / TLE / MLE / RE / CE3. 异步队列最早期版本是同步判题——用户提交后页面卡死等待。后来改成了异步队列用户提交 → 写入 judge_queuepending → 返回 submission_id ↓ judge_worker 消费队列 ← 异步判题 ↓ 更新 submissions 状态 ← 用户轮询或 SSE 推送judge_worker.php支持两种启动方式CLI 模式php judge_worker.php --run持续消费HTTP 触发模式提交后自动触发无需常驻进程核心功能亮点1. Monaco Editor 代码编辑器放弃了简单的 textarea集成了 VS Code 同源的 Monaco Editor语法高亮Bash、SQL、Python 等代码自动补全主题切换亮色/暗色快捷键支持CtrlEnter 提交一个小细节为了适配 CSP 策略CDN 资源需要加到白名单中否则会被浏览器拦截加载。2. 学校模式——班级系统这是为教学场景专门设计的模块层级结构是学校organization └── 班级org_classes ├── 教师org_teachers ├── 学生org_class_members └── 作业org_assignments绑定 class_id教师可以创建班级并指定班主任按班级发布作业而非全校统一查看班级内学生的作业完成率和排名一键导出作业统计报表学生视角查看自己的作业列表和截止时间提交后自动判题实时查看排名班级内解题数排行榜3. 题解系统每道题目支持用户发布题解支持 Markdown 格式。题解有独立评分和排序算法优质题解会优先展示。这个设计参考了 LeetCode 的题解区但更轻量。4. 题目导入导出支持批量导入题目JSON 格式包含题目描述、输入输出格式测试用例标准输入/输出标签、难度、类型也支持导出为 JSON 备份方便题库迁移。踩过的坑1. Bash 脚本的换行符陷阱Bash 对换行符极其敏感。用户写echo hello和echo -n hello的输出完全不同。早期测试用例里因为没有处理末尾换行导致大量 WA答案错误。后来强制规定除非题目特殊说明所有输出必须严格匹配末尾换行。2. SQL 评测的状态污染如果多个评测任务共享同一个测试库前面的 UPDATE/DELETE 会影响后面的测试。解决方案是每个测试用例执行前重置数据库状态——用事务回滚或重建表。3. 前端 CSP 策略与 Monaco 的冲突Monaco Editor 需要从 CDN 加载大量 JS 资源但网站的 CSPContent Security Policy默认只允许同域资源。报错信息很隐晦loader.min.js: Loading ... violates Content Security Policy。最后解法是把cdn.jsdelivr.net加到script-src白名单里。4. 移动端菜单的 className 不一致这是个小 bug 但很有意思。CSS 里菜单展开用的是.nav-menu.open但 JS 里 toggle 的是.nav-menu.active。小屏幕下点击汉堡菜单没有任何反应排查了半天才发现是类名没对上。这种前后端不一致的问题在快速迭代时很容易出现。实现细节异步判题的两种模式判题队列的实现其实经历了两次迭代。第一版同步阻塞用户点击提交 → 后端直接调用判题引擎 → 等待结果 → 返回页面。简单直接但用户体验极差——提交后页面卡死而且并发量一上来直接崩。第二版异步队列 HTTP 触发用户提交后代码写入submissions表同时在judge_queue插入一条待处理记录submit.php通过curl发起一个 100ms 超时的异步 HTTP 请求到judge_worker.phpjudge_worker.php从队列中取出任务执行判题更新数据库状态前端通过轮询获取最新状态关键是那个 100ms 超时的设计——submit.php 不需要等待判题完成触发就走。如果 worker 正在运行新请求会被忽略通过文件锁控制避免重复启动。functiontriggerJudgeWorker(){$chcurl_init();curl_setopt_array($ch,[CURLOPT_URLurl(api/judge_worker.php),CURLOPT_POSTtrue,CURLOPT_TIMEOUT_MS100,// 100ms 后超时不等待响应CURLOPT_RETURNTRANSFERtrue,]);curl_exec($ch);curl_close($ch);}学校模式从组织到班级的扩展最初的organizations表只是一个简单的成员集合。为了支持教学场景我新增了三个表org_classes班级信息名称、班主任、状态org_class_members班级成员学生/班长角色org_teachers学校教师带科目信息作业表org_assignments增加了一个class_id字段——如果为空就是全校作业如果有值就是班级作业。这样兼容了原有数据不需要迁移。权限模型设计为三层学校 owner/admin拥有所有班级管理权限班级教师管理自己班级添加学生、发布作业、查看统计学生提交作业、查看排名、退出学校退出学校的逻辑也做了边界处理owner 不能直接退出必须先转让所有权或解散学校避免学校变成孤儿。写在最后这个项目没有用什么新技术栈PHP MySQL 原生 JS但解决了很多实际痛点。最大的收获不是技术深度而是把多个零散功能整合成一个完整产品的能力——从判题引擎到班级管理从 Monaco 编辑器到题解社区每个模块单独看都不复杂但拼在一起要能流畅运转需要对边界条件和异常流程有充分的预判。如果你也在做类似的教育/评测类系统欢迎交流。项目代码会逐步整理开源对 Bash/SQL 评测引擎感兴趣的可以重点关注JudgeEngine.php和BashSimulator.php这两个核心文件。技术栈PHP 8.x / MySQL 8.0 / Monaco Editor / Vanilla JS部署环境Linux Nginx PHP-FPM代码行数约 15kPHP 5kJS/CSS