title: Portable OJ 一期工程技术方案 date: 2021-10-31 20:09:29 math: true mermaid: true
采用三端分离的架构
所有模块的需要索引的信息和部分短字段均存储至 MySQL,非短字段或者非需要索引字段均存储至 Mongo,Redis 仅用于缓存且不采用持久化
判断模块从属规则:基于哪个实体的数据进行逻辑判断
^[a-zA-Z0-9_\-]{4,15}$
,通常情况下不允许更新^[a-zA-Z0-9_\-@#$%^&*~',./?:]{6,16}$
NORMAL
,表示用户的类型权限 | 名称 | 功能 |
---|---|---|
CHANGE_ORGANIZATION | 修改用户组织权 | 能够让一个受你管理的组织下的一个用户的组织转为一个新的受你管理的组织下 |
GRANT | 分发权限 | 将自己拥有的权限同时授权给一个受你管理的组织下的一个用户,或者从他手里收回此权限 |
VIEW_HIDDEN_PROBLEM | 查看隐藏题目权 | 拥有题库查看、题库提交、比赛链接隐藏题目的权利,同时也可以下载公开、隐藏题目的数据 |
CREATE_AND_EDIT_PROBLEM | 创建编辑自己拥有的题目 | 可以创建题目,同时可以编辑自己拥有的题目 |
EDIT_NOT_OWNER_PROBLEM | 编辑可访问的题目 | 可以随意编辑任何可以查看的题目 |
VIEW_PUBLIC_SOLUTION | 查看公开的所有提交 | 可以查看公开的所有提交 |
VIEW_SOLUTION_MESSAGE | 查看提交的运行信息 | 查看提交的运行信息 |
MANAGER_JUDGE | 管理判题系统 | 管理判题系统 |
状态 | 名称 | 描述 |
---|---|---|
NORMAL | 正常 | 标准的题目,仅此状态可以进行提交 |
UNTREATED | 未处理 | 未进行生成 output 文件 |
TREATING | 处理中 | 正在生成 output 文件 |
UNCHECK | 未校验 | 未校验标准代码 |
CHECKING | 校验中 | 正在校验标准代码 |
TREAT_FAILED | 处理失败 | 处理失败,无法生成 output 文件 |
CHECK_FAILED | 校验失败 | 校验失败,不满足期望的标准代码通过情况 |
graph LR
a([NORMAL]) -- 未处理 --> b([UNTREATED])
a([NORMAL]) -- 未校验 --> d([UNCHECK])
b([UNTREATED]) -- 未处理 --> b([UNTREATED])
b([UNTREATED]) -- 未校验 --> b([UNTREATED])
d([UNCHECK]) -- 未处理 --> b([UNTREATED])
d([UNCHECK]) -- 未校验 --> d([UNCHECK])
b([UNTREATED]) -- 执行处理 --> c([TREATING])
d([UNCHECK]) -- 执行处理 --> e([CHECKING])
c([TREATING]) -- 处理失败 --> f([TREAT_FAILED])
c([TREATING]) -- 处理成功 --> d([UNCHECK])
e([CHECKING]) -- 校验失败 --> g([CHECK_FAILED])
e([CHECKING]) -- 校验成功 --> a([NORMAL])
f([TREAT_FAILED]) -- 未处理 --> b([UNTREATED])
f([TREAT_FAILED]) -- 未校验 --> b([UNTREATED])
g([CHECK_FAILED]) -- 未处理 --> b([UNTREATED])
g([CHECK_FAILED]) -- 未校验 --> d([UNCHECK])
f([TREAT_FAILED]) -- 执行处理 --> c([TREATING])
g([CHECK_FAILED]) -- 执行处理 --> e([CHECKING])
accessType: 题目的访问权限
| 访问权限 | 名称 | 在题库中查看和提交 | 比赛链接 | | :-: | :-: | :-: | :-: | | PUBLIC | 公开的 | 均可 | 均可 | | HIDDEN | 隐藏的 | 仅拥有查看权限的人 | 仅拥有查看权限的人 | | PRIVATE | 私有的 | 仅拥有者 | 仅拥有者 |
submissionCount: 总提交数,历史上所有的提交数
Normal
,用于后续增加交互题的能力STD
STD
CREATE TABLE account
(
id BIGINT AUTO_INCREMENT NOT NULL COMMENT '用户 ID',
data_id VARCHAR(32) NULL COMMENT '用户数据主键',
handle VARCHAR(64) NOT NULL COMMENT '用户名',
password VARCHAR(64) NOT NULL COMMENT '用户密码',
type VARCHAR(64) NOT NULL COMMENT '用户类型',
PRIMARY KEY (`id`),
UNIQUE KEY uk_handle(`handle`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mp4 COLLATION=utf8mb4_general_ci COMMENT '用户基本信息表';
public class NormalUserData {
/**
* 数据库主键
*/
@Id
private String _id;
/**
* 所属组织
*/
private OrganizationType organization;
/**
* 总提交数量
*/
private Integer submission;
/**
* 总通过数量
*/
private Integer accept;
/**
* 权限列表
*/
private Set<PermissionType> permissionTypeSet;
/**
* 邮箱
*/
private String email;
}
CREATE TABLE problem
(
id BIGINT AUTO_INCREMENT NOT NULL COMMENT '题目 ID',
data_id VARCHAR(32) NULL COMMENT '题目数据主键',
title VARCHAR(256) NOT NULL COMMENT '题目的标题',
status_type VARCHAR(64) NOT NULL COMMENT '题目状态',
access_type VARCHAR(64) NOT NULL COMMENT '访问权限',
submission_count INT DEFAULT 0 NOT NULL COMMENT '总提交数',
accept_count INT DEFAULT 0 NOT NULL COMMENT '通过的提交数',
owner BIGINT NOT NULL COMMENT '题目拥有者',
PRIMARY KEY (`id`),
UNIQUE KEY uk_title(`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mp4 COLLATION=utf8mb4_general_ci COMMENT '题目基本信息表';
public class ProblemData {
@Id
private String _id;
/**
* 首次关联至的比赛 ID
*/
private Long contestId;
/**
* 默认的耗时限制,单位(ms)
*/
private Integer defaultTimeLimit;
/**
* 默认的内存限制,单位(mb)
*/
private Integer defaultMemoryLimit;
/**
* 部分语言的特殊时间限制
*/
private Map<LanguageType, Integer> specialTimeLimit;
/**
* 部分语言的特殊内存限制
*/
private Map<LanguageType, Integer> specialMemoryLimit;
/**
* 允许使用的语言类型
*/
private List<LanguageType> supportLanguage;
/**
* 题目描述
*/
private String description;
/**
* 输入描述
*/
private String input;
/**
* 输出描述
*/
private String output;
/**
* 输入输出样例
*/
private List<Example> example;
/**
* 题目类型
*/
private ProblemType type;
/**
* judge 模式
*/
private JudgeCodeType judgeCodeType;
/**
* DIY judge code
*/
private String judgeCode;
/**
* 测试样例的名称,允许给不同的样例准备不同的名称,输入为 XXX.in,输出为 XXX.out,交互题只有输入
*/
private List<String> testName;
/**
* 是否允许下载样例
*/
private Boolean shareTest;
/**
* 标准代码,必定为通过
*/
private StdCode stdCode;
/**
* 测试代码(并不一定是通过,可能是故意错误的,但是一定有一份是通过的)
*/
private List<StdCode> testCodeList;
/**
* 题目版本号
*/
private Integer version;
/**
* 题目最后更新时间
*/
private Date gmtModifyTime;
@Data
@Builder
public static class Example {
/**
* 样例输入(原始文件格式 \n 表示换行)
*/
private String in;
/**
* 样例输出(原始文件格式 \n 表示换行)
*/
private String out;
}
@Data
@Builder
public static class StdCode {
/**
* 文件名
*/
private String name;
/**
* 代码
*/
private String code;
/**
* 期望结果
*/
private SolutionStatusType expectResultType;
/**
* 所使用的语言
*/
private LanguageType languageType;
/**
* 最终的测试 ID
*/
private Long solutionId;
public void reset() {
this.solutionId = null;
}
}
}
CREATE TABLE solution
(
id BIGINT AUTO_INCREMENT NOT NULL COMMENT '题目 ID',
data_id VARCHAR(32) NULL COMMENT '题目数据主键',
submit_time date NOT NULL COMMENT '题目的标题',
user_id BIGINT NULL COMMENT '提交的用户 ID',
problem_id BIGINT NOT NULL COMMENT '提交至的问题 ID',
contest_id BIGINT NULL COMMENT '提交至的比赛 ID',
language_type VARCHAR(64) NOT NULL COMMENT '提交的语言类型',
status VARCHAR(64) NOT NULL COMMENT '当前状态',
solution_type VARCHAR(64) NOT NULL COMMENT '提交的类型。公开的提交,在比赛中提交,系统测试程序',
time_cost INT NULL COMMENT '耗时',
memory_cost INT NULL COMMENT '内存消耗',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mp4 COLLATION=utf8mb4_general_ci COMMENT '提交基本信息';
public class SolutionData {
/**
* Mongo ID
*/
@Id
private String _id;
/**
* 代码内容
*/
private String code;
/**
* 编译信息
*/
private String compileMsg;
/**
* 运行中 judge 反馈信息
*/
private Map<String, String> runningMsg;
/**
* 运行的版本号
*/
private Integer runOnVersion;
}
Judge 和 Service 通过超长时间的 TCP 连接,并通过应答的方式,由 Judge 向 Service 发送请求,Service 应答。Judge 通过向 Service 发送注册指令在 Service 上完成注册,然后可以获取到一个唯一表示当前 Judge 机的编码,之后 Judge 可以使用多个新增 TCP 连接的方式实现建立多条 TCP 连接并绑定至同一个 Judge 上
BEGIN
${METHOD}
${DATA}
END
${CODE}
${DATA}
其中 DATA 数据使用分块传输编码的方式进行编码
每一个块都以一个该块包含的字节数(以十进制整数表示)开始,跟随一个 LF 换行符,然后是数据本身,最后以一个 LF 结束,且结束的 LF 不在字节数长度内。最后一个块必定是一个字节数为 0 的块,消息最后以 LF 结尾。即类似如下内容
${len1}
${data1}
${len2}
${data2}
0
将所有块的数据拼接后可以得到完整的数据内容,数据有三种格式,为前后端约定,分别为 单数据 简单数据 和 复杂数据,同时规定在后续开发中,尽量避免使用复杂数据结构
${value}
直接显示值,此方式适用于返回值仅有一个值的情况,或者是仅有一个数组/集合的情况,仅用于返回值
${key} ${value}
为一个 key
和一个 value
,表示 key
的值是 value
,中间使用空格分割,最后以 LF 作为结束符号。此时,value
部分长度不应该超过 200 个字符
${key} ${len}
${value}
首先是一个 key
和 len
,表示这个 key
的值长度为 len
,然后是一个 LF 换行符,再紧接着的是整个值的信息,最后以 LF 作为结束符号。通常用在简单数据中存在换行、空格的数据情况下使用,仅用于请求值
Response 返回时还会携带状态码,若状态码为非 0 则为错误
标题即为请求调用的方法
向目标 Service 注册本 Judge 机
参数 | 描述 | 使用的数据类型 |
---|---|---|
serverCode | 目标服务器给出的随机服务串,用来认定 Judge 是来自可信任的人启动并连接至此的 | 简单数据 |
maxThreadCore | 最大任务线程池大小 | 简单数据 |
maxSocketCore | 最大通信线程池大小 | 简单数据 |
maxWorkCore | 最大任务线程池大小 | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
judgeCode | 表示当前 Judge 的口令 | 单数据 |
通知服务器新增加一个 TCP 连接,并与现有的 TCP 所指向的 Judge 绑定
参数 | 描述 | 使用的数据类型 |
---|---|---|
judgeCode | 表示当前 Judge 的口令 | 简单数据 |
无返回信息
心跳数据包
参数 | 描述 | 使用的数据类型 |
---|---|---|
threadAccumulation | 堆积的小任务数据量 | 简单数据 |
socketAccumulation | 堆积的通信数据量 | 简单数据 |
workAccumulation | 堆积的任务数据量 | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
judgeTask | 需要进行 judge 的编号,可重复出现,表示多个任务 | 简单数据 |
testTask | 需要生成 test 数据的 problem 编号,可重复出现,表示多个任务 | 简单数据 |
threadCore | 更新最大线程池数量,为更新至的值,可以为空 | 简单数据 |
socketCore | 更新最大连接池数量,为更新至的值,可以为空 | 简单数据 |
workCore | 更新最大任务线程池数量,为更新至的值,可以为空 | 简单数据 |
cleanProblem | 需要删除的题目数据 | 简单数据 |
获取 Judge 任务的信息
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 提交的 ID | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
problemId | 提交对应的题目 ID | 简单数据 |
language | 使用的语言 | 简单数据 |
judgeName | 使用的 judge 模式 | 简单数据 |
testNum | 测试数据量 | 简单数据 |
timeLimit | 时间限制 | 简单数据 |
memoryLimit | 内存限制 | 简单数据 |
获取评测 Solution 的下一组测试名称
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 对应提交 ID | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
name | 下一组测试数据名称 | 单数据 |
提交评测 Solution 的信息
参数 | 描述 | 使用的数据类型 |
---|---|---|
value | 对应运行的结果 | 简单数据 |
testName | 运行的测试名称 | 简单数据 |
msg | 对应运行产生的信息 | 复杂数据 |
id | 对应提交 ID | 简单数据 |
timeCost | 对应提交的耗时 | 简单数据 |
memoryCost | 对应提交的内存消耗 | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
continue | 是否结束评测 | 单数据 |
提交评测 Solution 的编译信息
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 对应提交 ID | 简单数据 |
value | 对应 code 编译是否成功的信息 | 简单数据 |
judge | 对应 judge 编译是否成功的信息 | 简单数据 |
data | 编译信息 | 复杂数据 |
无返回信息
提交的代码
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 提交的 ID | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
code | 提交的代码 | 单数据 |
获取默认的 Judge 列表
无请求参数
参数 | 描述 | 使用的数据类型 |
---|---|---|
judgeNameList | 默认的 Judge 名称列表 | 单数据 |
获取对应的默认 Judge 代码
参数 | 描述 | 使用的数据类型 |
---|---|---|
name | 默认的 Judge 名称 | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
code | 默认的 Judge 代码 | 单数据 |
获取对应题目的自定义 Judge 代码
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目 ID | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
code | 对应题目的 DIY Judge 代码 | 单数据 |
获取对应题目的输入文件
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目 ID | 简单数据 |
name | 测试数据名称 | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
test | 对应题目的测试输入数据 | 单数据 |
获取对应题目的输出文件
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目 ID | 简单数据 |
name | 测试数据名称 | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
test | 对应题目的测试输出数据 | 单数据 |
获取 Test 任务的信息
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目的 ID | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
language | 使用的语言 | 简单数据 |
testNum | 测试数据量 | 简单数据 |
timeLimit | 时间限制 | 简单数据 |
memoryLimit | 内存限制 | 简单数据 |
获取生成测试的用的标准代码
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目的 ID | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
code | 标准代码 | 单数据 |
获取评测 Test 的下一组测试名称
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | problem ID | 简单数据 |
参数 | 描述 | 使用的数据类型 |
---|---|---|
name | 下一组测试数据名称 | 单数据 |
上传输出文件
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目的 ID | 简单数据 |
flag | 是否成功 | 简单数据 |
name | 数据的名称 | 简单数据 |
pos | 输出的偏移量 | 简单数据 |
value | 输出的片段内容 | 复杂数据 |
无
通知服务器 stdCode 编译出现编译错误
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目的 ID | 简单数据 |
无
通知服务器,本题目的 test 生成已经成功完成
参数 | 描述 | 使用的数据类型 |
---|---|---|
id | 题目的 ID | 简单数据 |
无