在线 OJ 项目详解

项目介绍
普通用户登录后, 可见题目列表, 每个题目都包含序号, 标题, 难度和通过状态, 点击题目标题后即可跳转到刷题界面, 该页面对题目有一个详细的介绍, 以及一些可选的操作, 有执行该次编写的代码, 查看上次提交的代码, 查看参考答案等操作. 用户执行不同的操作后, 服务器会返回对应的结果. 管理用户登录后, 会有新的操作权限, 即增加题目和删除题目.

项目开发

  • 开发技术

    SpringBoot, Spring MVC, MyBatis, Redis, MySQL, PageHelper, fastjson, lombok
    
  • 编译运行模块
    前提介绍
    后端获取到用户提交的代码后, 要将代码在服务器上运行, 然后返回代码运行的结果.
    每一个题目都有自己的代码源文件, 编译代码后产生的文件, 编译结果, 运行结果, 输出结果. 这里创建一个目录来存放它们, 为了防止存在不同题目目录相同的情况, 这里用 UUID 来作为题目的目录, 为了更好的管理所有的题目, 将这些 UUID 目录全部放在一个 当前项目的 tmp 目录下. 并且约定文件名为 Solution , 然后创建 compile_stderr.txt 的文件来存放代码编译的结果, 创建 run_stderr.txt 的文件来存放代码运行的结果, 创建 stdout.txt 的文件来存放代码输出的结果. 为了保证每个题目之间的独立性, 这里采取用进程的方式来编译运行每个题目, 而不是线程, 以防止单个题目挂了或者出现意料外的问题而影响别的题目继续运行. 下面是题目所在的目录和每个题目下存在的文件:

    题目目录 WORD_DIR = "./tmp/" + UUID.randomUUID().toString() + "/"
    
    Solution.java   	代码源文件
    Solution.class  	代码编译后产生的文件
    compile_stderr.txt  存放代码编译结果的文件(编译代码后, 该文件存在内容说明编译出错)
    run_stderr.txt		存放代码运行结果的文件(运行代码后, 该文件存在内容说明运行出错)
    stdout.txt			存在代码输出信息的文件(该文件存在内容, 说明编译运行代码都没有出错)
    

    1.得到用户提交代码并进行校验
    后端得到用户提交的代码后, 进行一个安全校验, 如果存在一些影响安全性的代码, 例如: exec , destroy … 这些词, 则直接返回给用户一个警告, 再进行一些代码合格性之类的校验, 例如用户提交了一个空代码之类的操作.

    2.拼接测试代码
    提交的代码并没有 main 方法, 这里提前就准备好了每个题目的测试代码, 根据提交的题目在数据库中查到对应题目的测试代码进行拼接, 得到完整代码.

    3. 编译代码, 命令如下:

    javac -encoding utf8 文件名.java -d 编译后的文件存放位置(在本目录下, tmp 目录下的 UUID 目录下存放): javac -encoding utf8 Solution.java -d WORD_DIR
    

    编译时, 用 process.getErrorStream() 方法读取到编译出错的结果, 并放到 compile_stderr.txt 这个文件下, 然后读取这个文件, 如果不为空, 说明编译出错, 由于展示编译出错的原因包含文件名, 而文件名又存在于 tmp 目录下随机的 UUID 目录下, 这里为了更好的展示编译出错的原因, 直接将前面的文件名截取掉. 如果存放编译结果的文件为空, 说明编译没有出错, 继续进行下一步的操作.

    4. 运行代码, 命令如下:

    java -classpath 文件所在目录 文件名
    即: java -classpath WORD_DIR Solution
    

    运行时, 进行计时, 如果代码运行 3 秒后还未出结果, 那么认为代码可能存在无线递归或者死循环的情况, 直接杀死这个进程, 返回运行超时的警告. 运行代码未超时, 就用 process.getErrorStream() 方法读取到运行出错结果, 并放到 run_stderr.txt 这个文件下, 然后读取这个文件, 如果不为空, 说明运行出错, 可能存在抛异常的情况, 直接返回给用户该题目的运行出错的原因. 如果为空, 说明运行没有出错, 则将代码输出信息保存到 stdout.txt 这个文件中, 再继续进行下一步的操作.

    5. 将代码输出信息返回给用户
    到这里说明代码编译运行都没有问题, 直接读取 stdout.txt 这个文件, 将代码的输出信息返回给用户. 同时记录该用户提交的代码, 保存到数据库中, 并检查该提交代码测试用例通过的个数, 若全部通过, 则修改该用户对于该题的状态为通过, 后续该用户在前端题目页面展示的时候就根据该用户的每一个题目的通过状态来展示.

  • 后端逻辑模块

    1.关于缓存设计

    展示题目页面:

    • 每一页的题目通过状态:
      每一页有 10 个题目, 每一个用户的题目列表相同, 分页借助 PageHelper 插件来完成, 1 条 SQL 即可,
      但是每一个用户对于每一页所有题目的通过状态需要查询 10 条 SQL, 这里一个页面就要查询 11 条 SQL,
      为了提高响应速度, 这里就用缓存来保存每一个用户对于每一页题目的通过状态. 用一个数组来存储,
      1 为通过, 0 为未通过. 由于 SQL 过多, 直接设置为永不过期.

    • 每一页的题目:
      每一页的题目对于所有的用户是一样的, 只是通过状态不同, 这里只缓存题目的id, 标题, 难度,
      后续再与该用户该页的通过状态一一拼接返回给前端. 设置过期时间.

    题目详情页面: 每一个题目都加入缓存, 设置过期时间.

    用户信息: 保存所有的用户信息, 由于用户不多而且为了方便管理用户, 直接设置为永不过期.

    2.项目启动模块
    项目启动时, 把后续一些需要的对象加入到容器中, 比如线程池, 再建立一些需要的缓存(所有用户加入到缓存中, 所有题目详情加入到缓存中, 每一页的题目加入到缓存中, 所有用户的每一页的通过状态加入到缓存中).

    登录操作
    用户登录时, 先校验验证码, 若验证码不正确则直接返回, 验证码正确再进行后续的密码比对. 再从 redis 中拿到所有的用户, 依次遍历这些用户, 如果不存在该用户, 则直接返回, 若存在该用户, 再对登录密码进行 MD5 加密, 再与缓存中已经记录的用户的密码进行比对, 密码不正确也直接返回. 密码正确就判断该用户是否为管理用户, 再返回给前端.

    注册操作
    用户注册时, 会对用户名和密码进行校验, 用户名的长度和密码长度必须合理, 并且密码要尽量复杂. 通过了这些校验, 则会判断注册的用户的用户名是否已经存在, 先从 redis 中 拿到所有的用户, 依次比对这些用户, 若注册的用户名已经存在, 则直接返回, 若用户名不存在, 则对密码进行 MD5 加密后再则进行注册. 把该用户存入到数据库中, 并把该用户加入到 redis 的用户列表中, 并初始化该用户对于所有的题目的通过状态为未通过, 这里将每一个用户的每一页的通过状态加入到缓存, 可能有些耗时, 不过不要担心, 这里所有的缓存操作都将由一个新的线程去操作, 并且这个线程池在项目启动的时候就已经注入到容器中了. 完全不会耽误用户的时间, 毕竟用户从注册到登录已经有好几秒了.

    题目列表展示
    用户登录后, 来到题目列表界面, 直接从 redis 中拿到该页的题目列表和该用户该页的题目通过状态, 这里的题目列表缓存存在过期时间, 若题目列表缓存不存在, 则借助 PageHelper 分页技术查询, 再建立缓存并传给前端, 前端在展示序号, 题目标题, 题目难度和通过状态, 点击题目即可跳转到对应题目的详情界面. 如果是管理用户, 还会有增加和删除题目两个按钮. 每一个操按钮都有对应的操作.

    题目详情展示
    点击题目标题后, 来到题目详情界面, 直接从 redis 中拿到该题目, 若存在该题目的缓存, 则直接返回给前端, 若不存在该题目的缓存, 则查询 SQL建立缓存并返回给前端. 前端再展示该题目的序号, 名称, 难度, 描述, 模板代码. 同时该界面存在着多个按钮, 提交代码, 查看上次提交的代码, 查看参考答案. 如果是管理用户, 还会有增加和删除题目两个按钮. 每一个操按钮都有对应的操作.

    提交代码
    代码提交到后端后, 先进行安全校验, 通过安全校验后, 从 redis 中拿到该题目, 拼接测试用例, 再进行编译, 保存编译结果到一个文件中, 若该文件不为空说明编译出错, 直接返回, 若该文件为空, 说明编译未出错. 再运行代码, 先进行一个时间记录, 若运行超过 3 秒还未出结果, 则认为代码可能存在无线递归或者死循环, 直接返回一个警告给前端. 运行未超时, 则将运行的结果保存到一个文件中, 若该文件不为空说明运行出错, 直接返回, 若该文件为空, 说明运行未出错. 直接返回该题目的输出结果给前端. 同时保存这次提交的代码, 记录该题目的通过状态, 若该题的测试用例全部通过, 则修改数据库中该用户对于该题的通过状态, 并修改缓存该用户该题目的通过状态.

    查看上次提交代码
    进行一个查询, 并返回给前端, 如果用户还没提交过代码, 则直接返回.

    查看查看参考答案
    直接从 redis 中拿到该题目, 返回该题目的参考答案给前端.

    添加题目
    进行一个 insert 操作, 并跳转到题目展示界面. 增加题目要修改最后一页的缓存, 还要判断是否要新建一页缓存, 再把这题加入到缓存中.

    和删除题目
    进行一个 delete 操作, 要更新该题目所在页以及后续页的全部缓存, 并删除所有用户对于该题目的通过状态和已经提交的代码, 再删除该题目的缓存.

3.前端模块

登录操作
访问登录页面时, 会先触发一个索要验证码图片的请求, 后端会把这张图片传给前端, 同时后端会记录验证码以方便后续登录时校验验证码. 登录时, 根据后端传来的状态码进行相应的操作, 这里约定 0 是登录成功, -1 是用户名或密码错误, -2 是验证码错误, 500是服务器出现未知的异常. 登录成功, 跳转到题目展示界面, 登录失败这里直接 alert 出 message 的信息. 用户信息有一个 isAdmin 字段, 根据这个来判断是否为管理用户, 如果是管理用户, 就跳转到管理用户的界面.

注册操作
根据后端传来的状态码进行相应的操作, 这里约定 1 是注册成功, -1 是已存在该用户名. 500是服务器出现未知的异常, 注册成功, 跳转到登录界面, 注册失败这里直接 alert 出 message 的信息.

题目展示界面
直接解析后端出来的 Json 格式的数据, 并加载到对应的 div 中. 该界面存在翻页按钮, 根据后端 PageHelper 分页的页数, 来展示本页的前后页, 第一页不展示前一页, 最后一页不展示后一页.

题目详情界面, 查看上次提交代码和查看参考答案
直接解析后端出来的 Json 格式的数据, 并加载到对应的 div 中.

提交代码
根据后端传来的状态码和 message 进行不同的操作, 这里约定 -1 是编译出错, -2 是运行异常, -3 是意料外的错误, 0 是编译运行成功. 状态码是 0 则输出 message内的信息, 状态码不是 0 则输出 reason 内的信息.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值