通过在交题时抓包, 可以发现会发送一个POST请求包, 发给 /submit-problem-judge api, 直接在源码中查找即可。

JudgeController 文件内是获取前端提交的代码以及是否远程提交的类型

    /**
     * @MethodName submitProblemJudge
     * @Description 核心方法 判题就此开始
     * @Return CommonResult
     * @Since 2020/10/30
     */
    @RequiresAuthentication
    @RequiresPermissions("submit")
    @RequestMapping(value = "/submit-problem-judge", method = RequestMethod.POST)
    public CommonResult<Judge> submitProblemJudge(@RequestBody SubmitJudgeDTO judgeDto) {
        return judgeService.submitProblemJudge(judgeDto);
    }
 

从前端传进来的 json 文件:

{
   "cid" : 0,
   "code" : "#include ...",
   "gid" : null,
   "isRemote" : true,
   "language" : "C++",
   "pid" : "HDU-1002",
   "tid" : null
}
 

最后api返回的 json 数据包:

{
   "data" : {
      "cid" : 0,
      "code" : "#include ...",
      "cpid" : 0,
      "displayPid" : "HDU-1002",
      "errorMessage" : null,
      "gid" : null,
      "gmtCreate" : "2023-06-02T14:42:45.557+0000",
      "gmtModified" : "2023-06-02T14:42:45.557+0000",
      "ip" : "218.28.152.180",
      "isManual" : null,
      "judger" : null,
      "language" : "C++",
      "length" : 1022,
      "memory" : null,
      "oiRankScore" : null,
      "pid" : 1005,
      "score" : null,
      "share" : false,
      "status" : 5,
      "submitId" : 56,
      "submitTime" : "2023-06-02T14:42:45.556+0000",
      "time" : null,
      "uid" : "0d20b3693c1a422ba6784e4964309b43",
      "username" : "Aze",
      "version" : 0,
      "vjudgePassword" : null,
      "vjudgeSubmitId" : null,
      "vjudgeUsername" : null
   },
   "msg" : "success",
   "status" : 200
}
 

这是调用的 judgeService.submitProblemJudge(judgeDto)

在 JudgeServicempl.java 中, 其中 JudgeServicempl 继承自 抽象类 JudgeService
    @Override
    public CommonResult<Judge> submitProblemJudge(SubmitJudgeDTO judgeDto) {
        try {
            return CommonResult.successResponse(judgeManager.submitProblemJudge(judgeDto));
        } catch (StatusForbiddenException | AccessException e) {
            return CommonResult.errorResponse(e.getMessage(), ResultStatus.FORBIDDEN);
        } catch (StatusNotFoundException e) {
            return CommonResult.errorResponse(e.getMessage(), ResultStatus.NOT_FOUND);
        } catch (StatusAccessDeniedException e) {
            return CommonResult.errorResponse(e.getMessage(), ResultStatus.ACCESS_DENIED);
        } catch (StatusFailException e) {
            return CommonResult.errorResponse(e.getMessage());
        }
    }

而该函数时封装后用来判断提交情况的, 实际执行还得再剥一层。

而这位更是重量级, JudgeManager.submitProblemJudge 就是实现代码, 咱们来慢慢分析一下:

/**
     * @MethodName submitProblemJudge
     * @Description 核心方法 判题通过openfeign调用判题系统服务
     * @Since 2020/10/30
     */
    public Judge submitProblemJudge(SubmitJudgeDTO judgeDto) throws StatusForbiddenException, StatusFailException, StatusNotFoundException, StatusAccessDeniedException, AccessException {
 
        judgeValidator.validateSubmissionInfo(judgeDto);
 
        // 需要获取一下该token对应用户的数据
        AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
 
        boolean isContestSubmission = judgeDto.getCid() != null && judgeDto.getCid() != 0;
        boolean isTrainingSubmission = judgeDto.getTid() != null && judgeDto.getTid() != 0;
 
        SwitchConfig switchConfig = nacosSwitchConfig.getSwitchConfig();
        if (!isContestSubmission && switchConfig.getDefaultSubmitInterval() > 0) { // 非比赛提交有限制限制
            String lockKey = Constants.Account.SUBMIT_NON_CONTEST_LOCK.getCode() + userRolesVo.getUid();
            long count = redisUtils.incr(lockKey, 1);
            if (count > 1) {
                throw new StatusForbiddenException("对不起,您的提交频率过快,请稍后再尝试!");
            }
            redisUtils.expire(lockKey, switchConfig.getDefaultSubmitInterval());
        }
 
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        // 将提交先写入数据库,准备调用判题服务器
        Judge judge = new Judge();
        judge.setShare(false) // 默认设置代码为单独自己可见
                .setCode(judgeDto.getCode())
                .setCid(judgeDto.getCid())
                .setGid(judgeDto.getGid())
                .setLanguage(judgeDto.getLanguage())
                .setLength(judgeDto.getCode().length())
                .setUid(userRolesVo.getUid())
                .setUsername(userRolesVo.getUsername())
                .setStatus(Constants.Judge.STATUS_PENDING.getStatus()) // 开始进入判题队列
                .setSubmitTime(new Date())
                .setVersion(0)
                .setIp(IpUtils.getUserIpAddr(request));
 
        // 如果比赛id不等于0,则说明为比赛提交
        if (isContestSubmission) {
            beforeDispatchInitManager.initContestSubmission(judgeDto.getCid(), judgeDto.getPid(), userRolesVo, judge);
        } else if (isTrainingSubmission) {
            beforeDispatchInitManager.initTrainingSubmission(judgeDto.getTid(), judgeDto.getPid(), userRolesVo, judge);
        } else { // 如果不是比赛提交和训练提交
            beforeDispatchInitManager.initCommonSubmission(judgeDto.getPid(), judgeDto.getGid(), judge);
 
        }
 
        // 将提交加入任务队列
        if (judgeDto.getIsRemote()) { // 如果是远程oj判题
            remoteJudgeDispatcher.sendTask(judge.getSubmitId(),
                    judge.getPid(),
                    judge.getDisplayPid(),
                    isContestSubmission,
                    false);
        } else {
            judgeDispatcher.sendTask(judge.getSubmitId(),
                    judge.getPid(),
                    isContestSubmission);
        }
 
        return judge;
    }
 

judge.java 是一个提交状态类, 也是之前的 api 返回结果 json。注意到从前端获取的 json 包只有代码和题号有用, 接下来应该是通过题号判断是否为远程评测。

judge.setshare() 只是获取用户信息, 接下来要看 beforeDispatchInitManager 类的东西, 会在里面更新 judge 对象的题目信息。直接看默认提交也就是 initCommonSubmission 的内容:

    public void initCommonSubmission(String problemId, Long gid, Judge judge) throws StatusForbiddenException {
        AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
 
        QueryWrapper<Problem> problemQueryWrapper = new QueryWrapper<>();
        problemQueryWrapper.select("id", "problem_id", "auth", "is_group", "gid");
        problemQueryWrapper.eq("problem_id", problemId);
        Problem problem = problemEntityService.getOne(problemQueryWrapper, false);
 
        if (problem == null){
            throw new StatusForbiddenException("错误!当前题目已不存在,不可提交!");
        }
 
        if (problem.getAuth() == 2) {
            throw new StatusForbiddenException("错误!当前题目不可提交!");
        }
 
        boolean isRoot = SecurityUtils.getSubject().hasRole("root");
 
        if (problem.getIsGroup()) {
            if (gid == null){
                throw new StatusForbiddenException("提交失败,该题目为团队所属,请你前往指定团队内提交!");
            }
            if (!isRoot && !groupValidator.isGroupMember(userRolesVo.getUid(), problem.getGid())) {
                throw new StatusForbiddenException("对不起,您并非该题目所属的团队内成员,无权进行提交!");
            }
            judge.setGid(problem.getGid());
        }
 
        judge.setCpid(0L)
                .setPid(problem.getId())
                .setDisplayPid(problem.getProblemId());
 
        // 将新提交数据插入数据库
        judgeEntityService.save(judge);
 
        trainingManager.checkAndSyncTrainingRecord(problem.getId(), judge.getSubmitId(), judge.getUid());
    }

映入眼帘的有一个 problemQueryWrapper, 哥们直接猜测是与数据库进行交互, 虽然不知道干嘛的, 应该 .select 是查询返回字段, eq 大概是要查的 key。而接受是用 Problem 类, 简单看一下应该是直接对应数据库里的problem表

列名实体属性类型备注
idlongprimary keyauto_increment 1000开始
judge_modeString默认为default、其他值有spj、interactive
problem_idString题目展示id
titleString题目标题
authorString默认可为无
typeint题目类型 0为ACM,1为OI
time_limitint时间限制(ms),默认为c/c++限制,其它语言为2倍
memory_limitint空间限制(mb),默认为c/c++限制,其它语言为2倍
stack_limitint栈限制(mb),默认为128
descriptionString内容描述
inputString输入描述
outputString输出描述
examplesSrting题面输入输出样例,不纳入评测数据
sourceint题目来源(比赛id),默认为hoj,可能为爬虫vj
difficultyint题目难度,0简单,1中等,2困难
hintString备注 提醒
authint默认为1公开,2为私有,3为比赛中。
io_scoreint当该题目为io题目时的分数 默认为100
code_shareboolean该题目对应的相关提交代码,用户是否可用分享
spj_codeString特判或交互程序代码
spj_languageString特判或交互程序的语言
user_extra_fileString选手程序的额外文件 json key:文件名 value:文件内容
judge_extra_fileString特判或交互程序的额外文件 json key:文件名 value:文件内容
is_remove_end_blankboolean是否默认去除用户代码的文末空格
open_case_resultboolean是否默认开启该题目的测试样例结果查看
caseVersionString题目测试数据的版本号
is_upload_caseboolean是否是上传zip评测数据的
modified_userString最新修改题目的用户
gmt_createdatetime创建时间
gmt_modifieddatetime修改时间

这里还只是获取信息阶段, 我们如果想导入新的远程提交网站应该得在提交远程题目那去看看。不过先继续看判题流程吧。

查询完之后给对应信息传入 judge 对象, 并将该对象同样记录到数据库中。

回到 beforeDispathInitManager , 简单查看一下其他的 init 方法, 可以发现都是类似的。

接下来是重头戏:

        // 将提交加入任务队列
        if (judgeDto.getIsRemote()) { // 如果是远程oj判题
            remoteJudgeDispatcher.sendTask(judge.getSubmitId(),
                    judge.getPid(),
                    judge.getDisplayPid(),
                    isContestSubmission,
                    false);
        } else {...}

表明我们下一步的检索对象是 remoteJudgeDispatcher

remoteJudgeDispatcher 远程评测类

里面只有一个 sendTask 方法:

public void sendTask(Long judgeId, Long pid, String remoteJudgeProblem, Boolean isContest, Boolean isHasSubmitIdRemoteReJudge) {
        JSONObject task = new JSONObject();
        task.set("judgeId", judgeId);
        task.set("remoteJudgeProblem", remoteJudgeProblem);
        task.set("token", judgeToken);
        task.set("isContest", isContest);
        task.set("isHasSubmitIdRemoteReJudge", isHasSubmitIdRemoteReJudge);
        try {
            boolean isOk;
            if (isContest) {
                isOk = redisUtils.llPush(Constants.Queue.CONTEST_REMOTE_JUDGE_WAITING_HANDLE.getName(), JSONUtil.toJsonStr(task));
            } else {
                isOk = redisUtils.llPush(Constants.Queue.GENERAL_REMOTE_JUDGE_WAITING_HANDLE.getName(), JSONUtil.toJsonStr(task));
            }
            if (!isOk) {
                judgeEntityService.updateById(new Judge()
                        .setSubmitId(judgeId)
                        .setStatus(Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus())
                        .setErrorMessage("Call Redis to push task error. Please try to submit again!")
                );
            }
            remoteJudgeReceiver.processWaitingTask();
        } catch (Exception e) {
            log.error("调用redis将判题纳入判题等待队列异常,此次判题任务判为系统错误--------------->", e);
            judgeEntityService.failToUseRedisPublishJudge(judgeId, pid, isContest);
        }
    }

看起来是定义 JSONObject task 来传入任务到 redis 缓存中依次执行。而 isOK 是判断是否成功装入 redis 中, 如果 isOK = false 就执行 .updateById 操作, 这里咱先不看, 如果成果的话就会转入 remoteJudgeReceiver 类, 进行等待远程 OJ的返回。

但有点问题, 传入只有在 HOJ 上的提交ID, 题号如HDU-1100, 和HOJ展示题号,以及是否为比赛提交这些参数, 如何构造出一个远程提交的封包且检测实时结果呢?不知道是不是封装进 装入redis 的操作中了, 先看 remoteJudgeReceiver 类吧。

    @Async("judgeTaskAsyncPool")
    public void processWaitingTask() {
        // 优先处理比赛的提交
        // 其次处理普通提交的提交
        handleWaitingTask(Constants.Queue.CONTEST_REMOTE_JUDGE_WAITING_HANDLE.getName(),
                Constants.Queue.GENERAL_REMOTE_JUDGE_WAITING_HANDLE.getName());
    }

不懂 redis, 什么玩意这是(

但也能看懂一些, 这部分是循环对队列中的评测任务进行处理, 其中 handleJudgeMsg 是主要的处理方法。

  @Override
    public void handleJudgeMsg(String taskStr, String queueName) {
        JSONObject task = JSONUtil.parseObj(taskStr);
        String token = task.getStr("token");
        String remoteJudgeProblem = task.getStr("remoteJudgeProblem");
        Boolean isHasSubmitIdRemoteReJudge = task.getBool("isHasSubmitIdRemoteReJudge");
        String remoteOJName = remoteJudgeProblem.split("-")[0].toUpperCase();
        Long judgeId = task.getLong("judgeId");
        Judge judge = judgeEntityService.getById(judgeId);
        if (judge != null) {
            if (Objects.equals(judge.getStatus(), Constants.Judge.STATUS_CANCELLED.getStatus())) {
                if (judge.getCid() != 0) {
                    UpdateWrapper<ContestRecord> updateWrapper = new UpdateWrapper<>();
                    // 取消评测,不罚时也不算得分
                    updateWrapper.set("status", Constants.Contest.RECORD_NOT_AC_NOT_PENALTY.getCode());
                    updateWrapper.eq("submit_id", judge.getSubmitId()); // submit_id一定只有一个
                    contestRecordEntityService.update(updateWrapper);
                }
            } else {
                dispatchRemoteJudge(judge,
                        token,
                        remoteJudgeProblem,
                        isHasSubmitIdRemoteReJudge,
                        remoteOJName);
            }
        }
    }

显然是将 task 拆包, 然后判断是否提交失败, 如果失败就更新数据库。

如果getbyid能从数据库中获取到 评测对象 judge, 说明已经在评测中,然后判断是否被取消评测, 是则更新数据库, 不是则正常提交, 转入 dispatchRemoteJudge 方法处理。

    private void dispatchRemoteJudge(Judge judge, String token, String remoteJudgeProblem,
                                     Boolean isHasSubmitIdRemoteReJudge, String remoteOJName) {
 
        ToJudgeDTO toJudgeDTO = new ToJudgeDTO();
        toJudgeDTO.setJudge(judge)
                .setToken(token)
                .setRemoteJudgeProblem(remoteJudgeProblem);
        Constants.RemoteOJ remoteOJ = Constants.RemoteOJ.getRemoteOJ(remoteOJName);
        if (!checkExistedAccountByOJ(remoteOJ)) {
            judge.setStatus(Constants.Judge.STATUS_SYSTEM_ERROR.getStatus());
            judge.setErrorMessage("System Error! Cause: The System does not have [" + remoteOJ + "] account configured. " +
                    "Please report the matter to the administrator!");
            judgeEntityService.updateById(judge);
        } else {
            if (remoteOJName.equals(Constants.RemoteOJ.CODEFORCES.getName())
                    || remoteOJName.equals(Constants.RemoteOJ.GYM.getName())) {
                if (ChooseUtils.openCodeforcesFixServer) {
                    fixServerCFJudge(isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
                } else {
                    commonJudge(Constants.RemoteOJ.CODEFORCES.getName(), isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
                }
            } else if (remoteOJName.equals(Constants.RemoteOJ.POJ.getName())) {
                pojJudge(isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
            } else {
                commonJudge(remoteOJName, isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
            }
        }
        // 如果队列中还有任务,则继续处理
        processWaitingTask();
    }

定义了一个 ToJudgeDTO 类, 去看看是什么成分, 也是评测的东西, 一个抽象类, 只存了有关远程评测账号啥的。

接着用 remoteOJ 存储对应的远程OJ名字, 看来如果想加OJ得在这修改一下 HOJtodo

public enum RemoteOJ {
        HDU("HDU"),
        CODEFORCES("CF"),
        GYM("GYM"),
        POJ("POJ"),
        SPOJ("SPOJ"),
        ATCODER("AC");
 
        private final String name;
 
        private RemoteOJ(String name) {
            this.name = name;
        }
 
        public String getName() {
            return name;
        }
 
        public static Boolean isRemoteOJ(String name) {
            for (RemoteOJ remoteOJ : RemoteOJ.values()) {
                if (remoteOJ.getName().equals(name)) {
                    return true;
                }
            }
            return false;
 
        }
 
        public static RemoteOJ getRemoteOJ(String name) {
            for (RemoteOJ remoteOJ : RemoteOJ.values()) {
                if (remoteOJ.getName().equals(name)) {
                    return remoteOJ;
                }
            }
            return null;
        }
    }

接下来是判断找到的 OJ 是否存在对应账号 checkExistedAccountByOJ , 里面用 SwitchConfig 访问在后台管理中 “系统开关” 配置的账号密码, 这里估计得修改一下前端。 HOJtodo

如果存在就继续判断是哪个OJ, 如果是CF和POJ要有特殊处理。 对于CF, 会判断 ChooseUtils.openCodefrocesFixServer , 但这里始终都是 False, 应该是用于调试的。

接下来看 commonJudge。 困了, 睡觉去。