分布式在线判题系统(分布式OJ系统)服务端

yangyi 19e9b95cc8 # feat: 本地JVM代码沙箱(编译执行/超时控制/异步IO/输出归一化/并发限流) il y a 1 mois
gradle 23cbe4ab8d # project init il y a 7 mois
sql 43a70d0fe8 feat:数据表增加逻辑删除字段和相关的注释 il y a 2 mois
src 19e9b95cc8 # feat: 本地JVM代码沙箱(编译执行/超时控制/异步IO/输出归一化/并发限流) il y a 1 mois
.gitattributes 23cbe4ab8d # project init il y a 7 mois
.gitignore 23cbe4ab8d # project init il y a 7 mois
AGENTS.md 19e9b95cc8 # feat: 本地JVM代码沙箱(编译执行/超时控制/异步IO/输出归一化/并发限流) il y a 1 mois
README.md 19e9b95cc8 # feat: 本地JVM代码沙箱(编译执行/超时控制/异步IO/输出归一化/并发限流) il y a 1 mois
build.gradle.kts 5ed5d189c0 # feat: 配置分页插件、开放公开浏览接口并暴露当前用户ID il y a 2 mois
gradlew 19e9b95cc8 # feat: 本地JVM代码沙箱(编译执行/超时控制/异步IO/输出归一化/并发限流) il y a 1 mois
gradlew.bat 23cbe4ab8d # project init il y a 7 mois
settings.gradle.kts 23cbe4ab8d # project init il y a 7 mois

README.md

serve_template

在线评测系统(OJ)后端服务模板,基于 Spring Boot 构建,提供统一的认证鉴权、用户管理与通用响应封装,可直接作为后续功能(题目、评测、比赛等)的开发底座。

技术栈

分类 选型 版本
语言 Java 17
框架 Spring Boot 3.5.13
构建工具 Gradle (Wrapper) 8.14.4
安全 Spring Security + JWT -
ORM MyBatis-Plus 3.5.15
数据库 PostgreSQL + Druid 连接池 -
接口文档 springdoc-openapi (Swagger UI) 2.8.16

已实现功能

  • 用户认证:注册、登录,密码 BCrypt 加密,登录后签发 JWT(有效期 24 小时)
  • 接口鉴权:基于角色的方法级鉴权(user / coach / admin),未认证、无权限统一 JSON 返回
  • 用户管理:分页查询、详情查询、新增、编辑、批量删除,以及状态 / 头像 / 密码更新
  • 统一响应:Response<T> 通用响应体 + 全局异常处理(参数校验、业务异常)
  • 请求日志:基于 CommonsRequestLoggingFilter 记录请求入参
  • 接口文档:Swagger UI 在线调试
  • 本地代码沙箱:基于本地 JVM 进程的判题执行(当前支持 Java),覆盖编译、逐用例执行、超时控制、异步 IO、输出归一化判定与并发限流

项目结构

serve_template
├── sql/postgersql.sql            # 数据库初始化脚本(表结构、索引与触发器)
├── src/main/java/space/anyi
│   ├── serve                     # 业务工程包
│   │   ├── ServeApplication.java # 启动类
│   │   ├── config                # SecurityConfig / WebConfig(CORS/日志) / OpenApiConfig
│   │   ├── controller            # AuthController(登录注册) / UserController(用户管理)
│   │   ├── entity                # Response(响应体) / PageVo(分页) / auth / user(DTO、VO、实体)
│   │   ├── filter                # JwtAuthenticationFilter(JWT 解析过滤器)
│   │   ├── handler               # GlobalExceptionHandler + security(JWT 工具、异常入口)
│   │   ├── mapper                # MyBatis-Plus Mapper
│   │   └── service               # UserService 接口 + impl 实现
│   └── codesandbox               # 本地JVM代码沙箱(判题执行)
└── src/main/resources
    ├── application.yaml          # 应用配置
    └── mapper/UserMapper.xml     # Mapper XML

快速开始

1. 环境要求

  • JDK 17+
  • PostgreSQL(本地或远程均可)

2. 初始化数据库

psql -U <postgres用户> -f sql/postgersql.sql

脚本会创建 oj 数据库、dev schema,以及用户、题目、评测用例、提交记录、分类、比赛等表(含注释与索引)。

3. 配置环境变量

application.yaml 中数据库连接通过环境变量注入:

环境变量 说明 默认值
databaseHost 数据库地址 -
databaseUsername 数据库用户名 -
databasePassword 数据库密码 -

4. 启动

./gradlew bootRun

服务默认运行在 http://localhost:8080。

接口文档

启动后访问 Swagger UI:

http://localhost:8080/swagger-ui/index.html

auth/login、auth/register 及 Swagger 相关路径无需认证,其余接口需在请求头携带 Authorization: Bearer <token>。

数据库设计

表均位于 dev schema 下:

  • user:用户(账号、密码、用户名、角色、头像、启用状态、逻辑删除标记)
  • problem:题目(标题、描述、时间/内存限制、提交与通过次数、公开状态、难度)
  • problem_case:题目样例(展示给用户)
  • problem_test:评测用例(后台判题,可隐藏)
  • submission:提交记录(代码、语言、判题状态、耗时/内存、得分、用例统计)
  • category / problem_category_rel:题目类别及多对多关联
  • contest / contest_problem / contest_registration:比赛、比赛题目、比赛报名

各表之间使用逻辑外键(仅记录 ID),便于水平扩展。

时间戳自动维护

  • 创建时间:所有表的 created_at 由数据库列默认值 DEFAULT NOW() 在插入时自动填充。
  • 更新时间:所有表的 updated_at 由 BEFORE UPDATE 触发器自动刷新。触发器函数 set_updated_at() 将每行更新前的 updated_at 置为当前时间,各表对应触发器如下(均在 sql/postgersql.sql 中定义):
表 触发器
user trg_user_updated_at
problem trg_problem_updated_at
problem_case trg_problem_case_updated_at
problem_test trg_problem_test_updated_at
submission trg_submission_updated_at
category trg_category_updated_at
problem_category_rel trg_problem_category_rel_updated_at
contest trg_contest_updated_at
contest_problem trg_contest_problem_updated_at
contest_registration trg_contest_registration_updated_at
-- 触发器函数(定义一次,所有表共用)
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 每个表单独建触发器,例如:
CREATE TRIGGER trg_user_updated_at
BEFORE UPDATE ON dev.user
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

应用层(MyBatis-Plus 实体)无需维护 created_at / updated_at,插入与更新均由数据库自动处理。

E-R 图

erDiagram
    user ||--o{ problem : "创建(created_by)"
    user ||--o{ contest : "创建(created_by)"
    user ||--o{ submission : "提交(user_id)"
    user ||--o{ contest_registration : "报名(user_id)"
    problem ||--o{ problem_case : "样例(problem_id)"
    problem ||--o{ problem_test : "评测用例(problem_id)"
    problem ||--o{ submission : "提交记录(problem_id)"
    problem ||--o{ problem_category_rel : "关联(problem_id)"
    category ||--o{ problem_category_rel : "关联(category_id)"
    contest ||--o{ submission : "包含(contest_id)"
    contest ||--o{ contest_problem : "题目(contest_id)"
    problem ||--o{ contest_problem : "被纳入(problem_id)"
    contest ||--o{ contest_registration : "报名(contest_id)"

本地代码沙箱

基于本地 JVM 进程实现的判题沙箱(src/main/java/space/anyi/codesandbox/),当前仅支持 Java 语言。通过子进程编译并运行提交代码,与宿主服务隔离。

核心类

类 说明
CodeSandBox 判题沙箱接口,定义 process(CodeContext)
JVMNativeCodeSandBox 基于本地 JVM 子进程的实现
CodeContext 判题上下文(record):id、code、language、maxTime、maxMemory、testCases
TestCase 测试用例(record):id、input、output
CodeResponse 判题结果(record):id、status、usedTime、usedMemory、testCaseResponses、message
TestCaseResponse 单用例结果(record):id、status、usedTime、usedMemory

状态值复用 Submission 实体的常量(STATUS_AC / STATUS_WA / STATUS_TLE / STATUS_MLE / STATUS_RE / STATUS_CE / STATUS_SE)。

执行流程

  1. 将提交代码写入临时目录 code/<uuid>/Main.java
  2. 调用 javac -encoding utf8 Main.java 编译,编译超时(COMPILE_TIMEOUT_MS,默认 10s)判 SE
  3. 逐用例执行 java -Xmx<maxMemory>k -Dfile.encoding=utf8 Main:
    • stdout / stderr 异步读取、stdin 异步写入(CompletableFuture + 守护线程池),主线程不阻塞
    • process.waitFor(maxTime, ms) 超时则 destroyForcibly() 并判 TLE,防止死循环耗尽资源
  4. finally 中清理临时目录,所有返回路径均保证删除
  5. 统计结果:usedTime 取各用例最大值,usedMemory 取最小值(实际内存统计尚未实现,标注为 todo)

输出判定

  • 按行归一化比较(outputMatches):忽略每行行尾空白(空格 / \t / \r)与末尾空行,行首与中间空白敏感
  • 程序输出上限 MAX_OUTPUT_LENGTH(1MB 字符),防止恶意输出耗尽内存

并发控制

  • Semaphore 限制最大并发判题数(MAX_CONCURRENT,默认 10)
  • 达到上限后 tryAcquire(5s) 超时,返回 SE「系统繁忙」,不排队阻塞线程

资源限制约定

  • maxTime:毫秒,作为 waitFor 的进程存活上限
  • maxMemory:KB(与 submission.memory_used_kb 单位一致),作为 -Xmx 参数传给子 JVM
  • CodeContext 缺省值:maxTime = 1000ms、maxMemory = 256MB(262144 KB)

注意事项

  • javac / java 路径取 JAVA_HOME,未设置时回退到当前运行时 java.home;需指向 JDK(非 JRE)
  • 本次提交尚未接入业务接口(JudgeController / SubmissionService),沙箱为独立可调用的能力,测试床见 JVMNativeCodeSandBoxTest

测试

JAVA_HOME=/path/to/jdk17 ./gradlew test --tests "space.anyi.codesandbox.JVMNativeCodeSandBoxTest"

覆盖:AC/WA/TLE/RE、多行输出与行尾空白归一化、死循环超时、10 并发、自定义内存上限(-Xmx<kb>k 格式)。

主要依赖

依赖 用途
spring-boot-starter-web Web MVC
spring-boot-starter-security 认证与鉴权
spring-boot-starter-validation 参数校验
mybatis-plus-spring-boot3-starter ORM
druid-spring-boot-3-starter 数据库连接池
postgresql PostgreSQL 驱动
jjwt JWT 生成与解析
springdoc-openapi-starter-webmvc-ui Swagger 接口文档
spring-boot-starter-actuator 健康检查与监控(端点全量暴露)