# GSOJ **Repository Path**: YonagiKei/gsoj ## Basic Information - **Project Name**: GSOJ - **Description**: 为美好的算法献上oj - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-24 - **Last Updated**: 2026-07-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 介绍 ## 一、什么是GOJ GSOJ(原VOJ)是基于SpringCloud+SpringBoot开发的分布式架构的在线评测系统(OJ),支持弹性增加评测机保证评测服务的高可用 区别于其他OJ,本系统定制开发了爬虫服务,在爬虫的加持下OJ可以: - 爬取**Leetcode、Nowcoder、Atcoder、Codeforces**的近期比赛 - 根据名单爬取**Leetcode、Nowcoder、Atcoder、Codeforces**周赛的排名情况 - 爬取**Leetcode、Nowcoder、Atcoder、Codeforces、HDOJ、YBT**的题目信息 - 根据用户绑定的平台账户,爬取该用户在对应平台的做题数量 - 远程提交(开发中) ## 二、为什么开发 1、兴趣使然 2、毕业设计 # 系统设计 ​ 该项目最初是由@Mαi从零开始独立开发的,承担了包括数据库设计、后端与前端开发、UI设计以及运维部署在内的多项任务,一个人的能力是有限的,后来得到了@xiaoma和@yun的加入和支持,共同推进项目的开发维护 ​ 此外,集训队的同学们也积极参与到了系统的测试使用过程中。他们不仅提供了大量的题面和评测数据,还提出了许多宝贵的改进建议。这些反馈帮助项目团队及时发现了潜在的问题,并根据用户需求进行了针对性的优化。 ## 技术选型 ### 前端 - Vue3+TypeScript+Pinia+ElementPlus - 支持不同设备的响应式布局 - 以Ace Editor作为代码高亮组件,在此基础上封装了DraggableCodeEditor(可拖拽式代码编辑器)和DocPipCodeEditor(基于文档画中画实现的可脱离浏览器的代码编辑器,尚有BUG) - 以Mavon Editor作为富文本编辑器,使用`vue-katex-auto-render`和`markdown-it`渲染数学公式和markdown语法 ### 后端 - Web框架以SpringBoot为主 - Nacos作为注册中心及配置中心 - MyBatis-Plus作为ORM框架的增强工具,负责实体类与数据库之间的映射及数据交互。 - 以SaToken作为鉴权框架,支持用户角色权限管理 - Redis作为缓存中间件 - RabbitMQ作为评测任务等待队列和结果推送队列 - 使用SSE进行评测结果的推送,保证实时性的同时足够轻量! - 使用Go-Judge(沙箱)编译运行代码 - 使用MinIO作为对象存储服务 ### 爬虫 早期使用Python的requests编写,后来因为不够稳定而使用Java的Jsoup进行重构 思路一致,都是发送请求然后使用xpath或正则表达式进行数据提取 ### 运维 使用Docker 和 Docker-Compose 进行服务编排与部署 使用jenkins进行CICD(弃用,服务器配置不够,构建时资源占用飙升会导致其他服务崩溃) 使用rsync同步多台服务器之间的评测数据 ## 架构设计 ### 基础架构 OJ系统与其他系统最大的不同是,服务器的压力大多来自于评测服务,为了保证评测机的可靠性,本OJ采用分布式架构,支持弹性增加评测机保证评测服务的高可用。![image-20240627150214803](assets/image-20240627150214803.png) 架构演进过程: 1. 起初所有代码杂糅在一个单体架构中 2. 随着使用人数增多,评测机的速度变慢,便将评测模块抽离成一个单独的服务**(judge-server)**,部署在多台服务器上,通过MQ进行通信,有了分布式的雏形 3. 考虑到使用人数增多并不会对管理模块造成太大影响,将管理模块抽离成一个单独的服务**(admin-server)**,部署**admin-server**的服务器成为主服务器,其他服务器成为从服务器,**admin-server**负责维护管理员上传的评测数据,从服务器使用rsync自动同步主服务器的评测数据 后来发现这玩意只能算是分布式单体,一点点改进 ![image-20240923000812084](assets/image-20240923000812084.png) 评测数据需要先上传到一台主服务器,然后在在多台从服务器之间同步 ![image-20240923001316763](assets/image-20240923001316763.png) ### 重构感想 虽然暑假工作的时候也重构过部分代码,但那段时间新用户激增,新功能上线和修复新BUG导致版本差距过大,重构速度跟不上更新速度,遂放弃。 现在人员稳定下来,基础功能基本完成,终于开始着手重构屎山项目了。 这个项目最大的问题是,web-server和admin-server重合度过高,题目、比赛、题单等功能都集成了在这两个服务中,做不到每个服务都有独立的数据库,这就陷入了划分陷阱:**分布式单体**。通过拥有独立的数据库,每个服务可以根据自己的需求独立地进行数据库模式的更改,而不需要协调整个应用程序中的其他服务。后面考虑上读写分离的mysql集群 划分前几个模块较为轻松,无非就是代码搬砖。我在重构评测功能时就头疼了。**评测功能由评测调度服务(生产者)**、**评测工作服务(消费者)**、**执行沙箱(执行代码的隔离环境)** 想了半天有如下几个方案: 1. 用户提交代码时,先请求到题目服务,题目服务会对本次评测中与题目相关的内容进行校验,比如是否允许提交本题,代码语言是否符合预期,确认无误后封装成评测任务放入消息队列。评测调度服务从消息队列中取出评测任务,保存到数据库中,使用Feign通过自定义的负载均衡器调用评测工作者。 2. 用户提交代码时,先请求到评测调度服务,评测调度服务先通过Feign从题目服务请求到相关数据,进行校验后将评测请求封装成评测任务放入消息队列,评测工作者从消息队列中取出评测任务。 3. 评测调度服务和评测工作者合并成评测服务,评测仍然是先经过题目服务,随后放入消息队列。 4. 评测调度服务和评测工作者合并成评测服务,评测先经过评测服务,经过校验后放入消息队列,评测服务自己又作为消费者从消息队列取出任务进行消费 斟酌了许久,选了方案二,这让每个服务的职责更加明确。 ### 踩坑 国庆那几天已经重构完成了,因为Docker Swarm的原因迟迟没有测试上线,搞了一个多星期才发现,云服务器不仅ban掉了4789端口,还ban掉了vxlan,不支持自建集群,跨主机服务发现是用不了了。 ## 评测沙箱的选择 ### 1、自己编写编译程序 **优点**:灵活,可以定制化开发,可以加深自己对操作系统理解 **缺点**:学习曲线陡峭,容易出现考虑不周的情况 如果有同学时间充足,愿意在此领域钻研,为ZGUC-OJ单独开发评测沙箱,最好不过 ```java import java.io.BufferedReader; import java.io.InputStreamReader; public class RunCppFromJava { public static void main(String[] args) { try { // 编译C++代码 ProcessBuilder compileProcessBuilder = new ProcessBuilder("g++", "-o", "hello", "hello.cpp"); Process compileProcess = compileProcessBuilder.start(); compileProcess.waitFor(); // 检查编译是否成功 if (compileProcess.exitValue() != 0) { System.out.println("C++ code compilation failed."); printProcessOutput(compileProcess); return; } // 运行编译后的程序 ProcessBuilder runProcessBuilder = new ProcessBuilder("./hello"); Process runProcess = runProcessBuilder.start(); runProcess.waitFor(); // 打印程序输出 printProcessOutput(runProcess); } catch (Exception e) { e.printStackTrace(); } } private static void printProcessOutput(Process process) throws Exception { try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { String line; while ((line = reader.readLine()) != null) { System.err.println(line); } } } } ``` ### 2、使用开源的评测沙箱(go-judge) 创建容器 centos7 想要正常启动sandbox最终方案: **需要开启user命名空间** 1. 永久性设置操作 `echo user.max_user_namespaces=10000 >> /etc/sysctl.d/98-userns.conf` `reboot` // 重启机器生效 2. 临时开启操作 `echo 10000 > /proc/sys/user/max_user_namespaces` 设置完后,重新启动`hoj-judgeserver`docker容器即可正常评测。 ```bash docker run -d -it --privileged --shm-size=512m -p 5050:5050 --name=go-judge -v /judge:/judge criyle/go-judge ``` 更换docker镜像源 ``` vim /etc/docker/daemon.json { "registry-mirrors": ["https://docker.foreverlink.love"] } ``` ##### 更换镜像源 ```bash # 华为云 Huawei Cloud sed -i 's@deb.debian.org@repo.huaweicloud.com@g' /etc/apt/sources.list.d/debian.sources # 阿里云 Aliyun(限速) sed -i 's@deb.debian.org@mirrors.aliyun.com@g' /etc/apt/sources.list.d/debian.sources # 腾讯云 Tencent Cloud(限速) sed -i 's@deb.debian.org@mirrors.cloud.tencent.com@g' /etc/apt/sources.list.d/debian.sources # 清华大学 TSINGHUA TUNA(北京地区推荐) sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources # 中科大 USTC sed -i 's@deb.debian.org@mirrors.ustc.edu.cn@g' /etc/apt/sources.list.d/debian.sources # 南京大学 NJU sed -i 's@deb.debian.org@mirror.nju.edu.cn@g' /etc/apt/sources.list.d/debian.sources # 上海交大 SJTU(上海地区推荐) sed -i 's@deb.debian.org@mirror.sjtu.edu.cn@g' /etc/apt/sources.list.d/debian.sources # 香港 xTom(仅限香港特别行政区) sed -i 's@deb.debian.org@mirror.xtom.com.hk@g' /etc/apt/sources.list.d/debian.sources ``` ##### 安装编译器 g++ ```bash sudo apt-get update && apt-get install build-essential ``` python3 ``` apt install make build-essential libssl-dev zlib1g-dev apt install libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm apt install libncurses5-dev libncursesw5-dev xz-utils tk-dev cd opt wget https://www.python.org/ftp/python/3.10.6/Python-3.10.6.tgz tar xf Python-3.10.6.tgz ./configure prefix=/usr/local/python3 --with-openssl=/usr --with-http_ssl_module --enable-optimizations make make install ln -s /usr/local/python3/bin/python3 /usr/bin/python ``` python3.10 ```bash #移除现有的python apt-get remove --purge python3 apt-get autoremove #更新系统并安装依赖 apt-get update apt-get upgrade apt-get install -y build-essential libssl-dev zlib1g-dev libncurses5-dev libncursesw5-dev libreadline-dev libsqlite3-dev libgdbm-dev libdb5.3-dev libbz2-dev libexpat1-dev liblzma-dev tk-dev #解压并安装 tar -xf Python-3.10.4.tgz cd Python-3.10.4 ./configure --enable-optimizations make -j $(nproc) make altinstall #验证安装 python3.10 --version apt install python3.11 ``` jdk17 ```bash apt-get install openjdk-17-jdk #设置环境变量 export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 export PATH=$PATH:$JAVA_HOME/bin ``` 安装java可能会遇到java.lang.InternalError: Error loading java.security file ```bash #将/opt/mount.yaml复制到容器外 docker cp voj-judge:/opt/mount.yaml ./mount.yaml #在 target: /.env 行后加入 - type: bind source: /etc/java-17-openjdk target: /etc/java-17-openjdk readonly: true #返回容器 docker cp ./mount.yaml voj-judge:/opt/mount.yaml ``` ##### 快速实践 通过postman调用`ip:5050/run`接口编译一个C++文件 ```json { "cmd": [{ //命令行参数 "args": ["/usr/bin/g++", "a.cc", "-o", "a"], //环境变量 "env": ["PATH=/usr/bin:/bin"], //指定输入输出异常文件 "files": [{ "content": "" }, { "name": "stdout", "max": 10240 }, { "name": "stderr", "max": 10240 }], "cpuLimit": 10000000000, "memoryLimit": 104857600, "procLimit": 50, //执行程序前将代码写入a.cc文件 "copyIn": { "a.cc": { "content": "#include \nusing namespace std;\nint main() {\nint a, b;\ncin >> a >> b;\ncout << a + b << endl;\n}" } }, //指定输出文件 "copyOut": ["stdout", "stderr"], //于copyOut相同,a文件可以通过 /file/:fileId 接口下载 "copyOutCached": ["a"] }] } //结果为: [ { "status": "Accepted", "exitStatus": 0, "time": 402767892, "memory": 41672704, "runTime": 414094480, "files": { "stderr": "", "stdout": "" }, "fileIds": { "a": "TGJ3KGVL" } } ] ``` ##### 运行python ```bash { "cmd": [{ //命令行参数 "args": ["/usr/bin/python3", "-m","py_compile","main.py"], //环境变量 "env": ["PATH=/usr/bin:/bin"], //指定输入输出异常文件 "files": [{ "content": "HelloWorld" }, { "name": "stdout", "max": 10240 }, { "name": "stderr", "max": 10240 }], "cpuLimit": 10000000000, "memoryLimit": 104857600, "procLimit": 50, //执行程序前将代码写入a.cc文件 "copyIn": { "main.py": { "content": "a=input()\nprint(a)" } }, //指定输出文件 "copyOut": ["stdout", "stderr"], //于copyOut相同,a文件可以通过 /file/:fileId 接口下载 "copyOutCached": ["./__pycache__/main.cpython-310.pyc"] }] } ``` ##### 运行java ```bash { "cmd": [{ //命令行参数 "args": ["/usr/bin/javac", "-encoding","utf-8","Main.java"], //环境变量 "env": ["PATH=/usr/bin:/bin"], //指定输入输出异常文件 "files": [{ "content": "" }, { "name": "stdout", "max": 10240 }, { "name": "stderr", "max": 10240 }], "cpuLimit": 10000000000, "memoryLimit": 104857600, "procLimit": 50, //执行程序前将代码写入a.cc文件 "copyIn": { "Main.java": { "content": "public class Main {\npublic static void main(String[] args) {\nSystem.out.println(\"Hello, World!\");\n}\n}" } }, //指定输出文件 "copyOut": ["stdout", "stderr"], //于copyOut相同,a文件可以通过 /file/:fileId 接口下载 "copyOutCached": ["Main.class"] }] } //运行 { "cmd": [{ "args": ["/usr/bin/java","-Dfile.encoding=UTF-8","Main"], "env": ["PATH=/usr/bin:/bin"], "files": [{ "content": "" }, { "name": "stdout", "max": 10240 }, { "name": "stderr", "max": 10240 }], "cpuLimit": 10000000000, "memoryLimit": 104857600, "procLimit": 50, "copyIn": { "Main.class": { "fileId": "CIEVBTCR" // 这个缓存文件的 ID 来自上一个请求返回的 fileIds } } }] } ``` # 开发清单 ## 评测模块 ### 1、判题功能 1. ACM模式(仅对比结果输出,已实现) 2. IO模式(赛后出成绩,没有罚时,按最后一次提交算结果) 3. 交互判题 4. 特殊判题(多种答案) 5. 扩展ICPC模式(类似于codeforces的hack功能) | 贡献者 | 贡献内容 | 时间 | | ------ | -------------------- | ---------- | | Mai | 完成判题基础功能开发 | 2024.06.27 | | | | | | | | | ## 爬虫模块 ### 1、通用爬虫 目前的爬虫使用Python实现,爬取题目、远程提交也只实现了Atcoder和Codeforces,后续最好使用java重构 | 贡献者 | 贡献内容 | 时间 | | ------ | ------------------------------------------------------------ | ------- | | Mai | Python编写Atcoder、cf的远程拉取题目、提交题目爬虫 | 2024.06 | | Mai | 使用java重构Python代码,完成周赛列表爬取、比赛结果爬取、完成题数爬取 | 2024.09 | | | | | ### 2、用户数据分析 根据用户提供的平台ID,爬取用户的提交数据,保存至数据库,并能够生成一份用户数据分析报表 | 贡献者 | 贡献内容 | 时间 | | ------ | ----------------------------------------------- | ---------- | | Mai | Python编写Atcoder、cf、力扣、牛客的做题记录爬虫 | 2024.06.27 | | | | | | | | | ## 集训队官网建设 1、GitHub Pages 静态站点 使用Hexo或vuepress在github上建立一个ZGUC-ACM主站点,展示集训队比赛风采,并能在导航到OJ或其他分站点 | 贡献者 | 贡献内容 | 时间 | | ------ | ------------------------------ | ------- | | Mai | 使用vuePress生成一份主站点模板 | 2024.08 | | | | | | | | | ## UI设计 ### 通用UI 仍需改进的地方: 1、亮色调下配色不佳 2、题目列表、评测结果列表表格UI风格可以改进 | 贡献者 | 贡献内容 | 时间 | | ------ | -------------------------------------- | ---------- | | Mai | PC端布局 | 2024.06.27 | | Mai | 移动端适配、UI响应式布局、暗亮色调切换 | 2024.08 | | | | | ### 网页代码编辑器 仍需改进的地方: 1、考虑使用画中画技术将代码编辑器独立于浏览器 2、代码编辑器的大小目前是按视窗百分比写死的,用户无法调节 | 贡献者 | 贡献内容 | 时间 | | ------ | ------------------------------------------------ | ------- | | Mai | 封装代码编辑器组件 | 2024.06 | | Mai | 完成对代码编辑器的拖拽范围限制,本地缓存用户代码 | 2024.09 | | | | | | | | | ## 积分系统 仍需改进的地方: 1、当前系统未开放该功能。用户参加比赛后会根据表现加上一定的分数 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ## 信息集成系统 仍需改进的地方: 1、该功能尚未开发。每次比赛报名都需要手动收集同学们的信息,较为繁琐,用户可以将自己的信息记录在本系统中,教练需要时,可以在后台一键导出。 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ## 公告模块 仍需改进的地方: | 贡献者 | 贡献内容 | 时间 | | -------- | ---------------------- | ------- | | 公告功能 | 完成公告的基础功能开发 | 2024.06 | | | | | | | | | ## 题单模块 该功能尚未开发。用户可以在某道题、某场比赛发布对应的题解内容 | 贡献者 | 贡献内容 | 时间 | | ------ | ---------------------- | ------- | | xiaoma | 题单模块后端接口开发 | 2024-08 | | mai | 题单模块前端页面绘制 | 2024-08 | | xiaoma | 题单模块排行榜功能开发 | 2024-09 | | | | | ## 题目模块 ### 题解功能 该功能尚未开发。用户可以在某道题、某场比赛发布对应的题解内容 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ### 题目导入 该模块尚未开发。需要支持批量导入 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ### 评测数据下载 该功能尚未开发。获取评测数据的方式仍然是手动下载 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ### 题目跳转功能 点击题目标题跳转到题目详情页 该功能尚未开发。获取评测数据的方式仍然是手动下载 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ### 当前题目提交记录 在题目详情页面可以查看当前用户在本题的所有提交记录 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ## 比赛模块 ### 团队参赛功能 该功能尚未开发。用户可以以个人身份或团队身份参加比赛,支持创建团队、加入团队、审批加入、团队首页、团队列表、团队成员列表等功能 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | ## 反馈功能 该功能尚未开发。为更好建设网站,开放意见反馈入口,集思广益,用户直接对话网站运营者。普通用户仅能查看自己的反馈记录,管理员可以在后台查看所有的反馈信息。 | 贡献者 | 贡献内容 | 时间 | | ------ | -------- | ---- | | | | | | | | | | | | | # 重大BUG日志 | 日期 | BUG场景 | 原因及解决方案 | 更新者 | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------ | | 2024-06-28 | web-server报错:Couldn‘t connect to host, port: smtp.163.com, 25 | 云厂商会默认把25端口禁掉,可以向云厂商提交申请开放端口但比较麻烦,最简单的是把端口换成ssl465端口 | Mai | | 2024-06-28 | 分布式部署judge-server和web-server,可能会丢失评测结果 | 多个消费者监听一个队列造成竞争,结果未被凭证持有者接收。将队列名改成评测机名,保证一个消费者监听一个队列,交换机设置为fanout广播模式 | Mai | | 2024-06-28 | vite打包部署后白屏 | 原因复杂,尝试将base改成:"/"、nginx添加`try_files $uri$ uri/ /index.html; `、使用`()=>import("xxx")`异步导入组件 | Mai | | 2024-07-31 | 前端精度丢失 | 前端的number无法表示雪花类型的数,可以在后端序列化或者前端引入bigint,这里采用的是bigint | Mai | | 2024-08-08 | latex无法正常渲染 | 和`<`有关,被认为是html标签了,手动在`<`旁边加空格即可 | Mai | | 2024-08-10 | 标程无法AC | windows下的回车符为`\r\n`,Linux下为`\n`。在上传评测数据时,替换调所有的`\r\n` | Mai | | 2024-09-02 | 无法查看评测列表 | 触发场景:该账户可以查看所有私有题目时无法查看评测列表。不可看列表为空,mybatisPlus的in传入了空数组后报错。
解决:对数组判空 | Mai | | 2024-09-10 | 修复比赛不显示题目BUG | 原因:题目列表根据缓存中的题目生成,比赛开始不存在提交,所有返回了空的题目列表
解决:该比赛的评测列表为空时,从数据库查询 | Mai | | 2024-09-12 | 题目未超时却显示TLE | 评测请求多次发送,时间限制确实按单个的计算,所以超时了。
评测请求依次发送即可 | Mai | | 2024-09-12 | 评测机大量TLE | 原因:加入了新功能,导致消费者抛异常,没有ack消息,导致消息堆积
final ack | Mai | | 2024-09-20 | 评测机异常TLE | 原因:编译代码时使用了运行代码的cpuTimeLimit和memoryLimit,导致无法编译通过。解决方案:编译代码使用固定的时限和内存限制 | Mai | | 2024-09-29 | 重构代码前Gateway可正常启动,重构引入新模块后无法启动,配置正确,报错:**No beans of ‘RedisConnectionFactory‘ type found** | 依赖冲突,具体依赖尚未排查出。解决方案,引入voj-common-satoken时,排除voj-common-core | Mai |