news 2026/6/27 6:43:18

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零打造一个支持小众语言的在线评测系统

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

前言

刷题平台大家见得多了,LeetCode、洛谷、Codeforces……但这些平台几乎全是围绕 C++/Java/Python 这类主流编程语言设计的。如果你是想练习 Shell 脚本、SQL 查询,甚至是 Bash 命令行操作,基本找不到一个像样的在线评测环境。

这个项目就是从这个缺口开始的。三个月业余时间,我用 PHP + MySQL 搭了一个支持Bash、SQL等小众语言的在线评测系统(OJ),并在过程中顺手实现了一套适合学校教学场景的班级管理模式。这篇文章把设计和实现过程做一个复盘。
曜渊OJ


项目定位:为什么做它

市面上 OJ 系统的主要痛点:

  • 语言支持单一:99% 的平台只支持编程语言,不支持 Shell/SQL 这类"脚本型"语言
  • 学校场景薄弱:现有平台以个人刷题为主,缺少班级、作业、教师管理等学校刚需功能
  • 判题耦合严重:很多开源 OJ 的判题逻辑和前端耦合在一起,水平扩展困难

基于这三个痛点,我给自己定了三个核心目标:

  1. 支持 Bash、SQL 等小众语言的精确评测
  2. 内置学校模式——班级、教师、作业、排名一站式解决
  3. 异步判题队列——解耦提交和判题,支持高并发

技术架构

整体采用经典的 LAMP 栈,但在几个关键点上做了针对性设计:

1. 数据库层

核心表结构设计:

职责
problems题目基础信息(支持类型、难度、标签)
submissions提交记录(含代码、状态、分数)
judge_queue异步判题队列(待评测任务缓冲)
judge_details逐测试点评测详情
organizations学校/组织信息
org_classes班级表(学校模式的扩展)
org_teachers教师表
org_assignments作业表(支持按班级发布)

2. 判题引擎

判题是整个系统的核心。针对 Bash 和 SQL 两种语言,设计了完全不同的评测策略:

Bash 评测

  • 使用轻量级沙箱执行用户脚本
  • 模拟文件系统(VFS)提供测试环境
  • 比对标准输出(逐字符 diff)

SQL 评测

  • 在隔离的 MySQL 测试库中执行用户 SQL
  • 对比查询结果的行数、列名、数据内容
  • 支持 UPDATE/DELETE 等 DML 语句的副作用验证

判题结果统一抽象为:Pending → Judging → Accepted / Wrong Answer / TLE / MLE / RE / CE

3. 异步队列

最早期版本是同步判题——用户提交后页面卡死等待。后来改成了异步队列:

用户提交 → 写入 judge_queue(pending) → 返回 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 等)
  • 代码自动补全
  • 主题切换(亮色/暗色)
  • 快捷键支持(Ctrl+Enter 提交)

一个小细节:为了适配 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 资源,但网站的 CSP(Content 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.php
  • judge_worker.php从队列中取出任务执行判题,更新数据库状态
  • 前端通过轮询获取最新状态

关键是那个 100ms 超时的设计——submit.php 不需要等待判题完成,触发就走。如果 worker 正在运行,新请求会被忽略(通过文件锁控制),避免重复启动。

functiontriggerJudgeWorker(){$ch=curl_init();curl_setopt_array($ch,[CURLOPT_URL=>url('api/judge_worker.php'),CURLOPT_POST=>true,CURLOPT_TIMEOUT_MS=>100,// 100ms 后超时,不等待响应CURLOPT_RETURNTRANSFER=>true,]);@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.phpBashSimulator.php这两个核心文件。


技术栈:PHP 8.x / MySQL 8.0 / Monaco Editor / Vanilla JS
部署环境:Linux + Nginx + PHP-FPM
代码行数:约 15k(PHP)+ 5k(JS/CSS)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/27 6:41:40

金融信贷信息流还在砸钱拍真人剧情?易元 AI 数字人 + 爆款复刻

在金融信贷信息流赛道,剧情类短视频始终是跑量主力。工薪族周转、小微企业主救急、反诈科普等场景,都依赖真人演员演绎情绪、传递信任。但随着平台审核趋严、素材衰退周期缩短、投放量级放大,传统真人拍摄模式的短板愈发明显:成本…

作者头像 李华
网站建设 2026/6/27 6:40:11

火电厂巡检报告自动生成太鸡肋?实测AI智能体,精准预判维保节点

在2026年的工业数字化浪潮中,火电厂作为能源保供的压舱石,其运维模式正经历从“抢修”到“预维”的质变。然而,许多运维主管发现,现有的火电设备巡检报告自动生成方案往往只能做到“事后记录”,难以真正实现维保节点的…

作者头像 李华
网站建设 2026/6/27 6:33:38

Rails 8 新特性全解析

Rails 8 新特性全解析:Solid Queue / Cache / Cable,抛弃 Redis 方案实战——当 Rails 决定替你砍掉整个 Redis 依赖,这不是噱头,是一场架构范式的革命。‌一、为什么 Rails 8 要"干掉" Redis?先说一句得罪人…

作者头像 李华
网站建设 2026/6/27 6:32:30

分布式管理系统:去中心化架构如何解决信号调度的老问题

前两年接触过一个市级应急指挥中心的项目。大楼里三层办公区加一个主指挥大厅,监控摄像头一百多路,业务终端几十台,还有移动应急车要随时接入。传统方案是需要把所有的信号拉到一台核心矩阵上,然后分发到各个显示终端。问题是这台…

作者头像 李华
网站建设 2026/6/27 6:30:45

Spring Boot 微服务中获取自身IP端口和注册信息(Registration)

1. 背景 在微服务架构中,每个服务实例启动后都会向注册中心(Nacos、Eureka、Consul 等)注册自己的 IP、端口和元数据。在不少业务场景下,服务需要获取自身在注册中心的信息: 构造回调 URL 或 Webhook 地址生成供其他服…

作者头像 李华