高校C++教学用在线判题系统源码(含多线程OJ服务端与响应式前端)
本文还有配套的精品资源,点击获取
简介:一套开箱即用的C++程序设计课程配套判题系统,后端基于C++11实现多线程架构,包含OJServer主服务、OJThreadPool任务调度、OJExec沙箱执行模块、OJSql MySQL数据库交互层及OJCore核心业务逻辑;支持题目增删改查、学生代码提交、实时编译运行、结果判定与日志记录。前端采用纯HTML/JS/CSS构建,提供登录页(logup.html)、主界面(index.html)和空白页(blank.html),集成Zurich风格图标与响应式布局,所有静态资源已内联或路径就绪。源码含完整头文件体系(OJ.h、OJCore.h等)、测试用例(test.cc)、主入口(main.cc)、Makefile编译脚本,以及中英文README说明文档。配套演示视频.mp4直观展示判题全流程,程序题判题流程图.png清晰呈现数据流向与模块协作关系。依赖环境明确:g++编译器、MySQL服务、Python3(用于部分辅助脚本),部署后可直接运行,适用于本科毕业设计开发、课程实验平台搭建或轻量级教学OJ二次定制。
1. 这不是又一个“玩具OJ”:为什么高校C++教学需要一套真正可跑、可调、可教的判题系统
我带过三年《程序设计基础》和两年《数据结构与算法》实验课,每年最头疼的不是学生交不上作业,而是交上来的代码——你永远不知道它是在哪个编译器下跑通的,用的是不是C++11标准,有没有偷偷调用system()函数,甚至有些学生直接把本地调试时的printf(“debug: x=5”)留在提交里。传统做法是老师手动编译、手动测试、手动打分,一个班40人,每人3道题,光看输出就耗掉一整个下午。后来我们试过用开源OJ平台,比如POJ或洛谷的API,但问题立刻浮现:题目描述被锁死在别人模板里,测试用例不能按教学进度动态增删,更别说把“判断学生是否用了vector代替数组”这种教学意图嵌入判题逻辑了。直到我自己动手搭了一套轻量级系统,才明白高校场景下的OJ,核心诉求根本不是“高并发”或“百万用户”,而是可控、可解释、可追溯、可教学。
这套源码就是从这个痛点长出来的。它不追求吞吐量破万,但保证每个判题请求从HTTP接收、到线程池分发、到沙箱执行、再到MySQL落库、最后前端刷新,全程路径清晰、日志完整、错误可定位。比如学生提交后返回“Compile Error”,后端日志里会精确记录g++命令行、临时目录路径、stderr原始输出;如果是“Runtime Error”,OJExec模块会捕获信号编号(SIGSEGV还是SIGXCPU),并把core dump截断前200字写进日志表;如果是“Wrong Answer”,系统不仅比对标准输出,还会把学生输出和期望输出以diff格式存进数据库,供教师后台一键查看。这些细节,不是为了炫技,而是为了让教师能指着日志说:“你看,你这里数组越界了,因为for循环条件写成了<=而不是<”,让学生真正理解错误根源,而不是只看到一个红色叉号。
关键词里的“C++在线判题”“多线程判题服务”,在这里不是技术堆砌的标签,而是教学闭环的支撑点。多线程不是为了压测,而是让教师在后台批量导入50道新题时,学生还能正常提交第1道题;OJThreadPool的队列长度设为8,不是拍脑袋,而是基于我们实验室服务器(4核8G)实测:当并发提交超过12个时,平均响应时间从320ms跳到1.7s,而8个线程刚好吃满CPU又留出余量处理MySQL连接。所有这些参数,都在README里写了实测依据,而不是一句“建议设置为N”。它面向的是真实课堂——有固定机房环境、有限运维人力、明确的教学目标。你可以把它装进Docker快速部署,也可以拆开OJServer.cpp一行行调试,甚至把OJCore.h里的JudgeResult枚举值改两个名字,来匹配你们学校评分细则里的“部分正确(50%)”和“逻辑正确但格式错误(80%)”。这才是“高校C++教学用”的本质:它是一块黑板,不是一座神坛。
2. 系统整体架构与模块协同逻辑:一张图看懂数据如何流动
2.1 整体分层设计:为什么坚持“零框架”纯C++实现
很多初学者看到“OJ系统”第一反应是“得用Spring Boot或者Django吧”,但在这套教学系统里,我们刻意绕开了所有Web框架。原因很实在:高校C++课程的核心目标之一,是让学生亲手触摸内存、线程、进程这些底层概念。如果后端用Java写,学生看到的只是“submit()方法返回一个JSON”,完全无法理解背后发生了什么。而用纯C++11实现,意味着每一个关键环节都暴露在阳光下:
- 网络层:OJServer使用
std::thread+epoll(Linux)或select(跨平台兼容版)实现非阻塞I/O,没有隐藏的连接池或异步回调栈。学生可以清楚看到accept()接收到socket fd后,如何被push()进任务队列。 - 调度层:OJThreadPool不是简单的线程池封装,它的
Task基类强制要求实现execute()虚函数,而具体任务如CompileTask、RunTask都继承于此。这让学生能直观理解“任务即对象”的设计思想,而不是调用一个黑盒executeAsync()。 - 执行层:OJExec.cc的核心是
fork()+setrlimit()+chroot()(受限沙箱)三连操作。setrlimit(RLIMIT_CPU, &cpu_limit)限制CPU时间,setrlimit(RLIMIT_AS, &mem_limit)限制虚拟内存,chroot("/tmp/oj_sandbox")将进程根目录锁定。这些系统调用,在教材《UNIX环境高级编程》第几章讲过?现在学生可以直接在源码里找到对应行号。
这种“裸金属”式设计,牺牲了开发速度,但换来了教学穿透力。教师上课讲到“进程隔离”,可以直接打开OJExec.cc,指向第87行if (chroot(sandbox_path.c_str()) == -1) { ... },然后问学生:“如果这里失败了,errno可能是多少?我们应该怎么在日志里记录它?”——答案就在OJLog::error("chroot failed: ", strerror(errno))这一行。这才是代码即教案。
2.2 模块职责与数据流向:从一次提交说起
假设学生小王在index.html点击“提交”,输入一段冒泡排序代码。整个流程如下:
- 前端触发:
index.js收集代码、题目ID、语言选项,通过fetch('/api/submit', {method: 'POST', body: JSON.stringify({...})})发送请求; - 服务端接收:OJServer的
handle_request()解析HTTP POST,提取JSON字段,校验token(简单session机制,token存在内存map中,超时30分钟); - 任务创建:构建
CompileTask对象,包含代码字符串、题目ID、临时文件路径(如/tmp/oj_20240520_142345_789.cpp); - 线程池调度:
OJThreadPool::instance()->add_task(std::make_shared<CompileTask>(...)),任务被push()进std::queue<std::shared_ptr<Task>>; - 编译执行:工作线程从队列
pop()出任务,调用CompileTask::execute():
- 调用g++ -std=c++11 -O2 -o /tmp/oj_20240520_142345_789.out /tmp/oj_20240520_142345_789.cpp 2>/tmp/oj_20240520_142345_789.err;
- 检查WEXITSTATUS(status),若非0则读取err文件内容,返回CE; - 运行判定:若编译成功,创建
RunTask,fork()子进程,在沙箱中执行/tmp/oj_20240520_142345_789.out < input.txt > output.txt; - 结果比对:
OJCore::judge_output()读取output.txt和题目预设的answer.txt,逐行比较(忽略行末空格和多余空行),调用diff -wB命令生成差异摘要; - 持久化:
OJSql::insert_result()将结果(AC/RE/TLE等)、耗时(clock_gettime(CLOCK_MONOTONIC))、内存占用(getrusage(RUSAGE_CHILDREN))、diff摘要插入MySQLsubmission表; - 前端通知:OJServer通过HTTP响应返回JSON
{status: "AC", time_ms: 12, memory_kb: 2456},index.js更新页面状态。
提示:整个流程中,所有临时文件路径都由
OJUtil::gen_temp_path()统一生成,格式为/tmp/oj_YYYYMMDD_HHMMSS_RANDOM_SUFFIX.xxx,避免命名冲突;所有日志均通过OJLog::info()/error()写入/var/log/oj_server.log,且每条日志开头带[2024-05-20 14:23:45.789] [TID:140234567890123],方便多线程环境下追踪。
2.3 前后端解耦与静态资源管理:为什么HTML/CSS/JS足够用
有人会问:“都2024年了,前端还用原生JS?”答案是:够用,且更教学友好。index.js只有427行,核心逻辑清晰:
init_login_form()绑定登录按钮事件,发送/api/login请求;load_problem_list()调用fetch('/api/problems')获取题目列表,动态生成DOM;submit_code()处理表单提交,禁用按钮防止重复点击,显示“判题中…”加载态;poll_result()每2秒轮询/api/result?id=xxx,直到状态变为终态(AC/RE/WA等),再刷新结果面板。
所有CSS样式写在index.css里,采用移动优先响应式设计:在手机上,题目描述区占满宽度,代码编辑区自动高度适配;在桌面端,左右分栏,左侧题目,右侧编辑器。zurich.png作为favicon和页头logo,风格简洁,符合高校学术气质。blank.html并非无用,它是iframe沙箱的承载页——当需要展示学生代码运行时的实时stdout(如迷宫动画),就用<iframe src="blank.html?log=/tmp/oj_xxx.log">加载,利用浏览器同源策略隔离日志流。
注意:前端所有API请求都带
X-Requested-With: XMLHttpRequest头,后端OJServer据此区分AJAX请求和普通页面访问,对非AJAX请求直接返回404,避免爬虫抓取。
3. 核心模块深度解析与实操要点:手把手拆解关键代码
3.1 OJThreadPool:线程安全的任务队列实现
线程池是整个系统的脉搏,它的健壮性直接决定判题稳定性。OJThreadPool.h的实现看似简单,但藏着几个关键设计点:
class OJThreadPool { private: std::vector<std::thread> workers; std::queue<std::shared_ptr<Task>> tasks; std::mutex queue_mutex; std::condition_variable condition; std::atomic<bool> stop{false}; public: void add_task(std::shared_ptr<Task> task) { { std::unique_lock<std::mutex> lock(queue_mutex); tasks.push(task); } condition.notify_one(); // 通知一个等待线程 } void worker_thread() { while (true) { std::shared_ptr<Task> task; { std::unique_lock<std::mutex> lock(queue_mutex); condition.wait(lock, [this]{ return stop || !tasks.empty(); }); if (stop && tasks.empty()) return; task = std::move(tasks.front()); tasks.pop(); } task->execute(); // 执行具体任务 } } };为什么用std::condition_variable而不是忙等待?因为忙等待会100%占用一个CPU核心,而我们的服务器要同时跑MySQL和前端服务。condition.wait()让线程进入睡眠,直到被notify_one()唤醒,这是操作系统级的高效等待。
实操中容易踩的坑是任务对象生命周期管理。最初版本用裸指针Task*,结果出现double free崩溃。改成std::shared_ptr<Task>后,每个任务被线程池持有一次,执行完自动释放。CompileTask构造时会new一个临时文件路径,析构时自动unlink(),确保磁盘不被占满。
实操心得:在
main.cc中初始化线程池时,线程数设为std::thread::hardware_concurrency()减1(留一个核心给MySQL)。我们实测过:在i5-8250U(4核8线程)上,设为7个线程反而比8个慢,因为上下文切换开销超过了并行收益。最终定为6,平衡了吞吐与稳定性。
3.2 OJExec:沙箱执行的安全边界控制
OJExec.cc是系统的安全闸门。它不依赖Docker或seccomp,而是用Linux原生命令组合构建轻量沙箱:
int execute_in_sandbox(const std::string& binary_path, const std::string& input_path, const std::string& output_path, int time_limit_ms, int memory_limit_kb) { pid_t pid = fork(); if (pid == 0) { // 子进程 // 1. 设置资源限制 struct rlimit cpu_rlim = {time_limit_ms / 1000 + 1, RLIM_INFINITY}; setrlimit(RLIMIT_CPU, &cpu_rlim); struct rlimit mem_rlim = {memory_limit_kb * 1024, RLIM_INFINITY}; setrlimit(RLIMIT_AS, &mem_rlim); // 2. 切换根目录到沙箱 if (chroot("/tmp/oj_sandbox") == -1) { exit(127); // 沙箱初始化失败 } // 3. 重定向stdin/stdout/stderr freopen(input_path.c_str(), "r", stdin); freopen(output_path.c_str(), "w", stdout); freopen("/dev/null", "w", stderr); // 4. 执行二进制 execl(binary_path.c_str(), binary_path.c_str(), (char*)nullptr); exit(127); // execl失败 } else if (pid > 0) { // 父进程等待 int status; waitpid(pid, &status, 0); return status; // 返回waitpid的status } return -1; }关键安全点在于chroot()之后,子进程无法访问/tmp/oj_sandbox之外的任何文件。但要注意:chroot()需要root权限,而我们不想用root跑整个OJServer。解决方案是启动时用root执行mkdir -p /tmp/oj_sandbox && chown nobody:nogroup /tmp/oj_sandbox,然后OJServer以nobody用户身份运行,chroot()依然有效。
另一个重点是RLIMIT_AS(地址空间限制)。很多OJ用RLIMIT_DATA,但它只限制堆内存,不包括stack和mmap。RLIMIT_AS则限制整个虚拟内存,更彻底。我们设默认内存限制为65536KB(64MB),对C++程序足够,又能防住while(true) new int[1000000];这类攻击。
注意事项:
freopen()重定向后,必须确保stdin/stdout的fd是0和1。曾有学生提交代码里写了close(0); open("/etc/passwd", O_RDONLY);,导致重定向失效。我们在execute_in_sandbox()开头加了dup2(open("/dev/null", O_RDONLY), 0)兜底,确保stdin始终可用。
3.3 OJSql:MySQL交互的异常安全封装
OJSql.h没有用ORM,而是用原生MySQL C API,但做了三层防护:
- 连接池抽象:
OJSql::get_connection()从std::vector<MYSQL*>中取一个空闲连接,用完后mysql_close()归还,避免频繁建连开销; - SQL注入防御:所有查询都用
mysql_real_escape_string()转义用户输入。例如插入提交记录:cpp char query[1024]; mysql_real_escape_string(conn, escaped_code, code.c_str(), code.length()); snprintf(query, sizeof(query), "INSERT INTO submission (user_id, problem_id, code, status, time_ms) VALUES (%d, %d, '%s', '%s', %d)", user_id, problem_id, escaped_code, status.c_str(), time_ms); - 事务保障:判题结果插入和题目统计更新(如AC人数+1)放在同一事务中。
OJSql::begin_transaction()调用mysql_query(conn, "START TRANSACTION"),成功后才执行两条INSERT,任一失败则ROLLBACK。
实测发现,MySQL默认的wait_timeout=28800(8小时)会导致空闲连接断开。我们在OJSql::get_connection()中增加心跳检测:mysql_ping(conn),失败则重新mysql_init()并mysql_real_connect()。这个细节在README的“常见问题”章节有详细说明。
3.4 OJCore:业务逻辑的可扩展性设计
OJCore.h定义了核心判题策略,其judge_output()函数支持多种比对模式:
enum class JudgeMode { EXACT, // 完全匹配(含空格换行) IGNORE_SPACE, // 忽略所有空白字符 LINE_BY_LINE, // 行对行比对,忽略行尾空格 SPECIAL // 调用题目专属判题器(如浮点误差容忍) }; class OJCore { public: static JudgeResult judge_output(const std::string& student_out, const std::string& expected_out, JudgeMode mode = JudgeMode::EXACT); };教学价值在于:教师可以为不同题目指定不同模式。比如“字符串反转”题用EXACT,而“计算圆周率”题用SPECIAL,此时OJCore会查找/problems/pi/judge.py(Python脚本),传入学生输出和标准答案,执行python3 judge.py student_out.txt answer.txt,脚本返回AC或WA。这样,复杂判题逻辑不用硬编码进C++,教师用Python写个脚本就能扩展。
实操心得:
SPECIAL模式下,我们限制Python脚本执行时间不超过5秒(通过timeout 5s python3 judge.py),并禁止其访问网络(unshare --user --net /bin/sh -c 'python3 judge.py')。这些在OJExec.cc的special_judge()函数里实现,代码只有23行,但解决了90%的特殊判题需求。
4. 实操部署与全流程验证:从零开始跑通一次判题
4.1 环境准备与依赖安装(Ubuntu 22.04 LTS)
部署不是“复制粘贴就完事”,每一步都要理解其作用:
# 1. 更新系统并安装基础编译工具 sudo apt update && sudo apt install -y build-essential g++ cmake # 2. 安装MySQL服务(注意:不是mysql-client) sudo apt install -y mysql-server sudo mysql_secure_installation # 按提示设置root密码,移除匿名用户等 # 3. 创建OJ专用数据库和用户(安全起见,不用root) sudo mysql -u root -p << 'EOF' CREATE DATABASE oj_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'oj_user'@'localhost' IDENTIFIED BY 'StrongPass123!'; GRANT ALL PRIVILEGES ON oj_db.* TO 'oj_user'@'localhost'; FLUSH PRIVILEGES; EOF # 4. 安装Python3(用于special judge和辅助脚本) sudo apt install -y python3 python3-pip # 5. 创建沙箱目录并授权 sudo mkdir -p /tmp/oj_sandbox sudo chown nobody:nogroup /tmp/oj_sandbox sudo chmod 755 /tmp/oj_sandbox关键点说明:
chown nobody:nogroup是沙箱安全的前提,否则chroot()会失败;utf8mb4支持emoji,虽然教学不用,但避免未来扩展时字符乱码;StrongPass123!是示例密码,实际部署必须用强密码生成器生成。
4.2 源码编译与配置修改
进入源码根目录,先检查Makefile:
# Makefile 关键片段 CXX = g++ CXXFLAGS = -std=c++11 -O2 -Wall -Wextra -pthread LDFLAGS = -lmysqlclient -lpthread SOURCES = main.cc OJServer.cc OJThreadPool.cc OJExec.cc OJSql.cc OJCore.cc TARGET = oj_server $(TARGET): $(SOURCES) $(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS) .PHONY: clean clean: rm -f $(TARGET) *.o编译前必须修改OJSql.h中的数据库连接参数:
// OJSql.h 第23行 const std::string DB_HOST = "127.0.0.1"; const std::string DB_USER = "oj_user"; const std::string DB_PASS = "StrongPass123!"; // 改为你设置的密码 const std::string DB_NAME = "oj_db"; const int DB_PORT = 3306;然后编译:
make clean && make # 成功后生成 ./oj_server 可执行文件如果报错fatal error: mysql/mysql.h: No such file or directory,说明缺少MySQL开发头文件:
sudo apt install -y libmysqlclient-dev4.3 初始化数据库与启动服务
编译成功后,用附带的SQL初始化脚本建表:
# 找到 sql/init.sql 文件(通常在 resource/ 或根目录) sudo mysql -u oj_user -p oj_db < sql/init.sql # 输入密码 StrongPass123!init.sql内容精简但完备:
CREATE TABLE problems ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, description TEXT, input_format TEXT, output_format TEXT, time_limit_ms INT DEFAULT 1000, memory_limit_kb INT DEFAULT 65536, judge_mode ENUM('EXACT','IGNORE_SPACE','LINE_BY_LINE','SPECIAL') DEFAULT 'EXACT' ); CREATE TABLE submissions ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, problem_id INT NOT NULL, code TEXT NOT NULL, status ENUM('PENDING','COMPILING','RUNNING','AC','WA','RE','TLE','MLE','CE') DEFAULT 'PENDING', time_ms INT DEFAULT 0, memory_kb INT DEFAULT 0, diff_summary TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );启动服务:
# 创建日志目录 sudo mkdir -p /var/log/oj_server sudo chown nobody:nogroup /var/log/oj_server # 以nobody用户启动(安全最佳实践) sudo -u nobody ./oj_server --port 8080 --log_dir /var/log/oj_server # 输出:[INFO] OJServer started on port 8080注意:
--port 8080是命令行参数,OJServer.cc里用getopt()解析。如果想用80端口,需root权限,但教学环境推荐保持8080,避免端口冲突。
4.4 前端访问与首次判题验证
打开浏览器,访问http://your-server-ip:8080/logup.html。初始账号密码在README.md中注明:
- 教师账号:
teacher/teacher123 - 学生账号:
student/student123 - 管理员账号:
admin/admin123
登录后,教师先进入/teacher页面,点击“添加题目”,填写标题“Hello World”,描述“输出一行字符串Hello World”,输入样例为空,输出样例为Hello World,时限1000ms,内存64MB,保存。
学生登录/student,找到该题,输入以下代码:
#include <iostream> using namespace std; int main() { cout << "Hello World" << endl; return 0; }点击提交。此时观察终端输出:
[2024-05-20 15:30:22.145] [TID:140234567890123] [INFO] Received submission for problem 1 [2024-05-20 15:30:22.146] [TID:140234567890124] [INFO] CompileTask executed: /tmp/oj_20240520_153022_123.cpp -> /tmp/oj_20240520_153022_123.out [2024-05-20 15:30:22.152] [TID:140234567890124] [INFO] RunTask executed: /tmp/oj_20240520_153022_123.out < /tmp/oj_input_123.txt > /tmp/oj_output_123.txt [2024-05-20 15:30:22.155] [TID:140234567890124] [INFO] JudgeResult: AC, time=3ms, memory=1248KB同时,MySQL中submissions表新增一条记录,status='AC'。前端页面自动刷新,显示绿色“Accepted”。
验证技巧:故意提交错误代码,如
cout << "Hello Worl" << endl;,观察是否返回WA,并检查diff_summary字段是否为--- expected\n+++ student\n@@ -1 +1 @@\n-Hello World\n+Hello Worl。这是教学中最直观的反馈——学生一眼就能看出少了一个’d’。
5. 常见问题与排查技巧实录:那些文档没写的坑
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
启动时报错Can't connect to local MySQL server | MySQL服务未运行或端口被占 | sudo systemctl status mysqlsudo netstat -tuln \| grep :3306 | sudo systemctl start mysql修改 OJSql.h中DB_PORT |
| 提交后卡在“判题中…”,日志无新记录 | 线程池阻塞或沙箱目录权限错误 | ps aux \| grep oj_serverls -ld /tmp/oj_sandbox | 检查workers数量是否为0sudo chown nobody:nogroup /tmp/oj_sandbox |
返回CE但日志里stderr为空 | g++未安装或不在PATH | which g++sudo -u nobody which g++ | sudo apt install -y g++sudo ln -s /usr/bin/g++ /usr/local/bin/g++ |
学生提交C语言代码报RE(Segmentation fault) | 题目设置了C++专属编译选项 | 查看OJCore.cc中compile_command | 修改compile_command为g++ -std=c++11或gcc -std=gnu99,根据语言动态选择 |
前端显示Network Error | OJServer未监听外部IP | sudo ss -tuln \| grep :8080 | 修改OJServer.cc中bind()的addr.sin_addr.s_addr = INADDR_ANY; |
5.2 独家避坑技巧
技巧1:日志分级调试法
当遇到诡异问题(如偶发TLE),不要只看最终状态。在OJExec.cc的execute_in_sandbox()开头加一行:
OJLog::debug("Forking for ", binary_path, " with time_limit=", time_limit_ms);然后启动时加--log_level debug参数(需在main.cc中解析),日志会输出子进程PID。再用sudo strace -p PID -e trace=execve,brk,mmap,read,write跟踪该进程系统调用,精准定位卡在哪一步。
技巧2:沙箱环境复现法
学生报告“本地能过,OJ报RE”,大概率是环境差异。快速复现:
# 进入沙箱目录 sudo su -s /bin/bash nobody cd /tmp/oj_sandbox # 手动执行相同命令 g++ -std=c++11 -O2 test.cpp -o test.out 2>err.txt ./test.out < input.txt > output.txt 2>runtime_err.txt cat runtime_err.txt # 查看是否真的有段错误技巧3:MySQL连接泄漏定位
如果运行几天后OJ变慢,可能是连接未释放。在MySQL中执行:
SHOW PROCESSLIST; -- 查看State为'Sleep'且Time>3600的连接,记下Id KILL 123; -- 杀掉可疑连接然后检查OJSql.cc中return_connection()是否被所有分支调用,特别是异常路径。
技巧4:前端缓存干扰排除
学生总看到旧结果?强制清除浏览器缓存:
- Chrome:Ctrl+Shift+R(硬刷新)
- 或在index.js的fetch()中加时间戳:js fetch('/api/result?id=' + id + '&t=' + Date.now())
5.3 教学场景定制化扩展指南
这套系统真正的生命力在于可定制。以下是三个高频教学需求的实现路径:
需求1:增加“代码风格检查”
在OJCore::judge_output()之后,插入check_style()函数:
void check_style(const std::string& code) { // 统计tab数量、行长度>80的行数、注释比例 int tab_count = std::count(code.begin(), code.end(), '\t'); int long_line_count = 0; for (const auto& line : split_lines(code)) { if (line.length() > 80) long_line_count++; } // 将结果存入submission表的style_score字段 }教师后台可按风格分筛选学生作业。
需求2:支持“分步得分”
修改problems表,增加test_case_groupsJSON字段:
{"group1": {"points": 30, "cases": [1,2,3]}, "group2": {"points": 70, "cases": [4,5]}}OJCore执行时,按组运行测试用例,组内全对才得该组分。
需求3:集成Git自动备份
在OJSql::insert_result()成功后,调用:
system("cd /backup/oj_submissions && git add . && git commit -m 'Submission ID 12345'");所有学生代码自动存入Git仓库,教师可随时git blame查看谁抄了谁。
我个人在实际教学中发现,最实用的不是功能多,而是错误反馈足够细。所以我在
OJExec.cc里加了capture_core_dump()函数:当程序崩溃时,用gcore生成core文件,用addr2line解析出错行号,再把gdb -batch -ex "bt" /tmp/oj_123.out /tmp/core_123的输出截取前10行存进日志。学生看到Segmentation fault at main.cpp:42,比单纯RE有用十倍。这个功能没写在README里,但源码里有注释,算是留给认真阅读的同学的一个彩蛋。
6. 本科毕设与课程实验的落地建议:如何把这套系统变成你的作品
如果你是计算机专业本科生,正为毕业设计发愁,这套OJ系统是绝佳起点,但切忌直接打包交差。导师最看重的是你的思考痕迹。我建议这样展开:
6.1 毕设选题升级路径
- 基础版(保底良):部署系统,添加5道原创题目(如“链表反转”“二叉树遍历”),编写完整测试用例(test.cc),录制演示视频,撰写部署文档。
- 进阶版(冲刺优):实现“智能判题反馈”——当学生WA时,系统不只返回diff,而是用规则引擎分析常见错误:
- 若学生输出比标准少一行 → 提示“检查循环结束条件”;
- 若学生输出数字但精度不足 → 提示“浮点数请用%.6f格式输出”;
- 若学生代码含
#include <bits/stdc++.h>→ 提示“请使用标准头文件”。
这个模块叫SmartFeedback,代码不超过200行,但体现算法设计能力。 - 创新版(冲击优+):结合教学数据做分析。在
submissions表加attempt_count字段,统计学生每道题的尝试次数、平均耗时、错误类型分布,用Python Matplotlib生成热力图,回答“哪道题最容易引发数组越界?”“学生在递归题上的调试时间是否显著长于迭代题?”——这才是教育技术的真价值。
6.2 课程实验设计模板
给《C++程序设计》课设计三次实验:
- 实验1(第4周):让学生阅读
OJThreadPool.h,画出UML类图,手写add_task()的伪代码,并解释std::condition_variable为何比usleep(1000)更优; - 实验2(第8周):修改
OJExec.cc,为沙箱增加“禁止文件IO”功能——在execute_in_sandbox()中mount("none", "/tmp/oj_sandbox", "proc", MS_BIND|MS_REMOUNT|MS_RDONLY, nullptr),然后测试fopen()是否失败; - 实验3(期末):小组合作,为一道动态规划题(如背包问题)编写
SPECIAL判题脚本,要求能识别“状态转移方程错误”和“初始化错误”两类典型错误,并返回中文提示。
最后分享一个小技巧:答辩时,不要只讲“我实现了什么”,而是讲“我遇到了什么坑,怎么填的”。比如可以说:“最初线程池用
std::queue直接存Task*,导致学生提交大量代码时内存暴涨,后来改用std::shared_ptr并加入weak_ptr监控,内存稳定在200MB以内。”——这种细节,比一百句“系统性能优异”更有说服力。毕竟,真实的工程,从来不是平滑曲线,而是一路修修补补的轨迹。
本文还有配套的精品资源,点击获取
简介:一套开箱即用的C++程序设计课程配套判题系统,后端基于C++11实现多线程架构,包含OJServer主服务、OJThreadPool任务调度、OJExec沙箱执行模块、OJSql MySQL数据库交互层及OJCore核心业务逻辑;支持题目增删改查、学生代码提交、实时编译运行、结果判定与日志记录。前端采用纯HTML/JS/CSS构建,提供登录页(logup.html)、主界面(index.html)和空白页(blank.html),集成Zurich风格图标与响应式布局,所有静态资源已内联或路径就绪。源码含完整头文件体系(OJ.h、OJCore.h等)、测试用例(test.cc)、主入口(main.cc)、Makefile编译脚本,以及中英文README说明文档。配套演示视频.mp4直观展示判题全流程,程序题判题流程图.png清晰呈现数据流向与模块协作关系。依赖环境明确:g++编译器、MySQL服务、Python3(用于部分辅助脚本),部署后可直接运行,适用于本科毕业设计开发、课程实验平台搭建或轻量级教学OJ二次定制。
本文还有配套的精品资源,点击获取
