当前位置: 首页 > news >正文

高校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()虚函数,而具体任务如CompileTaskRunTask都继承于此。这让学生能直观理解“任务即对象”的设计思想,而不是调用一个黑盒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点击“提交”,输入一段冒泡排序代码。整个流程如下:

  1. 前端触发index.js收集代码、题目ID、语言选项,通过fetch('/api/submit', {method: 'POST', body: JSON.stringify({...})})发送请求;
  2. 服务端接收:OJServer的handle_request()解析HTTP POST,提取JSON字段,校验token(简单session机制,token存在内存map中,超时30分钟);
  3. 任务创建:构建CompileTask对象,包含代码字符串、题目ID、临时文件路径(如/tmp/oj_20240520_142345_789.cpp);
  4. 线程池调度OJThreadPool::instance()->add_task(std::make_shared<CompileTask>(...)),任务被push()std::queue<std::shared_ptr<Task>>
  5. 编译执行:工作线程从队列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;
  6. 运行判定:若编译成功,创建RunTaskfork()子进程,在沙箱中执行/tmp/oj_20240520_142345_789.out < input.txt > output.txt
  7. 结果比对OJCore::judge_output()读取output.txt和题目预设的answer.txt,逐行比较(忽略行末空格和多余空行),调用diff -wB命令生成差异摘要;
  8. 持久化OJSql::insert_result()将结果(AC/RE/TLE等)、耗时(clock_gettime(CLOCK_MONOTONIC))、内存占用(getrusage(RUSAGE_CHILDREN))、diff摘要插入MySQLsubmission表;
  9. 前端通知: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,然后OJServernobody用户身份运行,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,但做了三层防护:

  1. 连接池抽象OJSql::get_connection()std::vector<MYSQL*>中取一个空闲连接,用完后mysql_close()归还,避免频繁建连开销;
  2. 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);
  3. 事务保障:判题结果插入和题目统计更新(如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,脚本返回ACWA。这样,复杂判题逻辑不用硬编码进C++,教师用Python写个脚本就能扩展。

实操心得:SPECIAL模式下,我们限制Python脚本执行时间不超过5秒(通过timeout 5s python3 judge.py),并禁止其访问网络(unshare --user --net /bin/sh -c 'python3 judge.py')。这些在OJExec.ccspecial_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-dev

4.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 serverMySQL服务未运行或端口被占sudo systemctl status mysql
sudo netstat -tuln \| grep :3306
sudo systemctl start mysql
修改OJSql.hDB_PORT
提交后卡在“判题中…”,日志无新记录线程池阻塞或沙箱目录权限错误ps aux \| grep oj_server
ls -ld /tmp/oj_sandbox
检查workers数量是否为0
sudo chown nobody:nogroup /tmp/oj_sandbox
返回CE但日志里stderr为空g++未安装或不在PATHwhich 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.cccompile_command修改compile_commandg++ -std=c++11gcc -std=gnu99,根据语言动态选择
前端显示Network ErrorOJServer未监听外部IPsudo ss -tuln \| grep :8080修改OJServer.ccbind()addr.sin_addr.s_addr = INADDR_ANY;

5.2 独家避坑技巧

技巧1:日志分级调试法
当遇到诡异问题(如偶发TLE),不要只看最终状态。在OJExec.ccexecute_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.ccreturn_connection()是否被所有分支调用,特别是异常路径。

技巧4:前端缓存干扰排除
学生总看到旧结果?强制清除浏览器缓存:
- Chrome:Ctrl+Shift+R(硬刷新)
- 或在index.jsfetch()中加时间戳:
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二次定制。


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

http://www.cnnetsun.cn/news/2841961.html

相关文章:

  • 零API零GPU本地对话系统:规则+检索+轻量推理架构
  • WELearn网课助手:终极指南,5分钟实现英语学习自由
  • Excel时间数据处理:从‘4.00E+00’到清晰秒数的完整避坑指南
  • 别再到处找日志了!Hadoop YARN日志聚合(Log Aggregation)配置与查看全攻略
  • MATLAB多源航迹融合工具包:含卡尔曼滤波主程序、平滑后处理与多场景测试数据
  • ViGEmBus驱动终极指南:5步轻松实现Windows游戏控制器模拟
  • 音频合并工具怎么选?2026 年主流方案对比与操作指南
  • PHP软文推广平台源码:支持自助发稿、在线交易、支付宝充值与媒体站群对接
  • 同济软院数据结构实战包:10个即跑实验+区间优化课程设计+国际跳棋AI实现
  • SAP Retail 商品季节管理,Season 如何关联 Article,Generic Article 与 Structured Article
  • WinUI 3项目创建保姆级教程:Visual Studio 2022组件勾选与避坑指南(附离线补丁)
  • 原神帧率解锁终极指南:轻松突破60FPS限制的完整解决方案
  • 想做网站改版?这3个问题没搞懂,千万别动工
  • 告别CNN/RNN统治:高光谱分类新宠SpectralFormer,实测在三个经典数据集上表现如何?
  • 概率思维:AI工程师的不确定性建模实战指南
  • STM32F4上跑通SOEM主站控制伺服电机:我的踩坑记录与内存优化心得
  • Java 编译与反编译 完整详解
  • AI 实时推理流式预热实战:首字符延迟从 800ms 砍到 200ms
  • HuggingFace Downloader——批量自动化的仓库项目下载软件
  • 动态基数保持图Transformer在分子预测中的应用
  • MAA明日方舟助手:一键解放双手的智能自动辅助工具完全指南
  • GTA5线上小助手:免费开源工具,彻底改变你的洛圣都体验
  • STM32F103驱动MS41929双路步进电机的可直接烧录Keil工程
  • 告别踩坑:用PHPStudy在Win11一键部署MySQL 8,顺便学学手动配置原理
  • TUM RGBD数据集工具包全解析:从associate.py到evaluate_ate.py,你的SLAM评测工具箱
  • CoppeliaSim仿真提速秘籍:如何把复杂的STL机械臂模型简化成‘凸面体’并搭建运动树
  • RAG精度提升实战手册:检索校准、上下文压缩与生成约束
  • 孤能子视角:分析钉钉内网的《置身钉内》,顺看AI+背景下社会组织的“关系”处理
  • 私密文件共享工具怎么选?主流 4 大阵营对比与企业级避坑指南
  • 进销存软件和生产管理工具,差别不在表面