V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
Kerminate
V2EX  ›  Node.js

Vue + Koa 搭建 ACM OJ

  •  
  •   Kerminate · Jun 1, 2018 · 7223 views
    This topic created in 2889 days ago, the information mentioned may be changed or developed.

    花了两个多月时间,我与 lazzzis 完成了第二版本的 Putong OJ,因为中间忙着春招以及毕业设计等,项目最近才正式上线。

    项目线上地址:http://acm.cjlu.edu.cn/

    项目前端地址:https://github.com/acm309/PutongOJ-FE

    项目后端地址:https://github.com/acm309/PutongOJ

    这里求一下 star 啊(^o^)/~

    本 OJ 前端架构为 Vue2.5 + vue-router + vuex + axios + iview + stylus + webpack3.6 后端架构为 Koa2 + MongoDB + redis

    开发背景

    我们学校 acm 起步较晚,最早的 OJ 是由 Hust OJ 魔改而来,界面写的比较粗糙。2 年前,那届的 acm 队长本来决定使用 Vue + Go 重写一下 OJ,但是因为一些原因,他跑路了,最后只 fork 了一个开源 OJ。一年前,lazzzis 开始重构 OJ,采用了 Vue + node,开发出了 Putong OJ 的第一个版本。今年,由于老师增加了功能上的一些需求,再加上后端数据结构又发生了一些变化,以及对第一个版本不太满意,我与 lazzzis 再次重构,开发出了 Putong OJ V2 版本。

    技术选型

    考虑过要用 React 开发,(好吧,说实话写 OJ 的时候我还不会 React),但是 Vue 上手简单且中文资源丰富,所以决定使用 Vue 全家桶。最初 vue1.0 时官方推荐 vue-resource,后来在 2.0 时 Vue 官方不再推荐 vue-resource, 而是推荐使用 axios 作前后端通信。开发初期一开始用了 element 作为 vue 的 UI 库,后来转而使用了 iview。其实这两个 UI 库相当像,都是 ant-design 风格的,api 也比较一致,在我眼里比较打的区别是 element 的组件更大,iview 的更小巧(视觉上的大小,element small 的跟 iview 的 default 差不多大)。

    后端其实我们不太喜欢 Java,最后用了轻量又方便的 node。数据库用了 MongoDB,主要是方便 js 操作,同时用 redis 做数据缓存,并做了简单的消息队列。

    预览

    主题色采用了 lazzzis 比较喜欢的骚紫。

    实现功能

    OJ 分为 web 端和判题端,这边主要分析 web 端,判题端 由 Acdream 的判题端 魔改而来。web 端共有消息模块,题目模块,讨论模块,状态模块,排行模块,比赛模块与管理员模块七大模块。 本 OJ 提供了两种用户,普通用户和管理员用户。顾名思义,普通用户只能答题,参加比赛,发帖,查看信息等,管理员用户拥有对信息,题目,比赛等增删改查的权限。

    • 消息模块 就是 OJ 的首页,含有列表页和消息详情页,主要就是管理员发布的消息。
    • 题目模块 OJ 的核心模块之一,含有题目列表页和题目详情页,题目详情页里有 6 个 tab 页,题目描述,提交,我的提交,统计,编辑,测试数据。其中编辑和测试数据两个 tab 页仅管理员可见。
    • 讨论模块 其实就是讨论区,用户可在上面发帖评论。
    • 状态模块 用户提交题目的判题结果。
    • 排行模块 用户排名,有分组功能,便于老师统计结果
    • 比赛模块 核心模块之一,含有比赛列表和比赛详情页,比赛详情页有 6 个 tab 页,总览,题目,提交,状态,排名,编辑。其中编辑页仅管理员可见。
    • 管理员模块 核心模块之一,含有创建消息,创建题目,创建比赛,用户管理四个功能页。

    给大家提前注册了一个普通用户账号,账号 123456,密码 123456,欢迎去试用一下。

    前端

    先来看看前端的项目结构,通过脚手架 vue-cli 构建

    ├── dist // 生成打包好的文件
    │   ├── static
    │   │   ├── css
    │   │   ├── fonts
    │   │   ├── img
    │   │   └── js  
    │   └── index.html
    └── src
        ├── main.js // 项目入口
        ├── router // 路由文件,说明了各个路由将会使用的组件
        │   ├── index.js // router 的配置以及引用组件
        │   └── routes.js // 定义各个路由
        ├── assets // 网站 logo 图资源
        ├── components // 一些小组件
        ├── store // vuex 文件
        │   └── modules // 子模块
        ├── utils // js 工具方法
        └── views // 路由对应的组件 (这些组件在 router.js 中都被引入)
            ├── Admin
            ├── Contest
            ├── News
            └── Problem
    
    

    前端一共有三十多张页面,但其实大多数都是只有图表,页面逻辑并不复杂。 iview 按需加载,减小前端打包大小。 为了保证首屏加载的速度,对部分路由进行懒加载。

    // 路由懒加载
    const ProblemStatistics = r => require.ensure([], () => r(require('@/views/Problem/Statistics')), 'statistics')
    const ProblemEdit = r => require.ensure([], () => r(require('@/views/Problem/ProblemEdit')), 'admin')
    const Testcase = r => require.ensure([], () => r(require('@/views/Problem/Testcase')), 'admin')
    const ContestEdit = r => require.ensure([], () => r(require('@/views/Contest/ContestEdit')), 'admin')
    const NewsEdit = r => require.ensure([], () => r(require('@/views/News/NewsEdit')), 'admin')
    const ProblemCreate = r => require.ensure([], () => r(require('@/views/Admin/ProblemCreate')), 'admin')
    const ContestCreate = r => require.ensure([], () => r(require('@/views/Admin/ContestCreate')), 'admin')
    const NewsCreate = r => require.ensure([], () => r(require('@/views/Admin/NewsCreate')), 'admin')
    const UserManage = r => require.ensure([], () => r(require('@/views/Admin/UserManage/Usermanage')), 'admin')
    const UserEdit = r => require.ensure([], () => r(require('@/views/Admin/UserManage/UserEdit')), 'admin')
    const GroupEdit = r => require.ensure([], () => r(require('@/views/Admin/UserManage/GroupEdit')), 'admin')
    const AdminEdit = r => require.ensure([], () => r(require('@/views/Admin/UserManage/AdminEdit')), 'admin')
    const TagEdit = r => require.ensure([], () => r(require('@/views/Admin/UserManage/TagEdit')), 'admin')
    

    同时前端用了不少第三方组件实现小的需求。

    • vue-echarts: 基于 Vue 的 Echarts 组件,在项目中用于展示提交结果的统计分析图。
    • vue2-editor: 基于 Vue 的富文本编辑器,用于题目内容的编辑,支持图片上传等基本功能。
    • Vue.Draggable: 基于 Vue 的拖拽组件,方便管理员对比赛题目顺序做改动。
    • vue-clipboard2: 基于 Vue 的剪切板,方便用户复制代码。
    • vuex-router-sync: 使 vue-router 的 $route 能够在 vuex 中的 state 访问到。
    • highlight.js: 页面里代码高亮。

    后端

    ├── config // 项目配置(数据库等)
    ├── model // 数据库 model
    ├── routes // 后端路由
    ├── controllers // 主要功能实现
    ├── services // 主要服务(判题、邮件提醒、更新)
    ├── utils // js 工具函数
    ├── test // 测试
    ├── app.js
    └── manage.js
    
    

    后端使用 koa2 开发,使用 async/await 代替回调,避免 callback hell. 主要数据都保存在 MongoDB 中,使用的 node 的 mongoose 包。为了避免多人同时提交题目,造成的高并发问题,接口遵循 RESTful 设计,使用 redis 对判题做了队列缓存。用户提交的题目会进入 redis 中,再一个个弹出队列交给判题端处理。正常 ACM 比赛最后一小时会进行封榜(不再进行排名和 ac 题目的更新,但是会更新用户的提交次数),在这里也用了 redis 对比赛排行榜进行更新,比赛过程中只将数据保存在 redis 中,并实现封榜,赛后再将比赛所有信息保存到 mongo 中。

    // 比赛时返回比赛排行榜
    const ranklist = async (ctx) => {
      const contest = ctx.state.contest
      const ranklist = ctx.state.contest.ranklist
      let res
      const deadline = 60 * 60 * 1000
      await Promise.all(Object.keys(ranklist).map((uid) =>
        User
          .findOne({ uid })
          .exec()
          .then(user => { ranklist[user.uid].nick = user.nick })))
    
      if (Date.now() + deadline < contest.end) {
        // 若比赛未进入最后一小时,最新的 ranklist 推到 redis 里
        const str = JSON.stringify(ranklist)
        await redis.set(`oj:ranklist:${contest.cid}`, str) // 更新该比赛的最新排名信息
        res = ranklist
      } else if (!isAdmin(ctx.session.profile) &&
        Date.now() + deadline > contest.end &&
        Date.now() < contest.end) {
        // 比赛最后一小时封榜,普通用户只能看到题目提交的变化
        const mid = await redis.get(`oj:ranklist:${contest.cid}`) // 获取 redis 中该比赛的排名信息
        res = JSON.parse(mid)
        Object.entries(ranklist).map(([uid, problems]) => {
          Object.entries(problems).map(([pid, sub]) => {
            if (sub.wa < 0) {
              res[uid][pid] = {
                wa: sub.wa
              }
            }
          })
        })
        const str = JSON.stringify(res)
        await redis.set(`oj:ranklist:${contest.cid}`, str) // 将更新后的 ranklist 更新到 redis
        // 比赛结束
        res = ranklist
      }
      ctx.body = {
        ranklist: res
      }
    }
    

    项目使用 docker 进行一键部署。写了 Dockerfile 对 web 端进行镜像定制,在 docker-compose 中配置项目所需的所有镜像。部署过程

    最后

    篇幅有限,无法展现更多的内容,有兴趣的话可以进入项目地址阅读源码,当然,如果觉得项目还不错的话 👏,就给个 star ⭐️ 鼓励一下吧~

    18 replies    2020-09-10 10:11:28 +08:00
    0915240
        1
    0915240  
       Jun 1, 2018 via iPhone
    滋持下。
    simple11
        2
    simple11  
       Jun 1, 2018
    mark 一下
    LeungJZ
        3
    LeungJZ  
       Jun 1, 2018
    马克一下。

    顺便插下嘴,vue 的懒加载貌似可以简写成
    component: () => import('xxxx')
    yuankui
        4
    yuankui  
       Jun 1, 2018
    不错,支持一下
    rannie
        5
    rannie  
       Jun 1, 2018
    zhichi xia
    murmur
        6
    murmur  
       Jun 1, 2018
    本来想进来看怎么判题的
    感觉这地方才是最屌的
    能执行各种语言
    还要控制超时和内存
    还要执行各种用例
    光用 bash 写我都认为好牛逼了
    Yinnfeng
        7
    Yinnfeng  
       Jun 1, 2018
    刷一道 A+B 表示滋辞
    holyghost
        8
    holyghost  
       Jun 1, 2018
    @murmur https://github.com/justice-oj

    这是我之前写的,没那么多花里胡哨的前端技巧,就是干。
    mengrenzi
        9
    mengrenzi  
       Jun 1, 2018 via iPhone
    yoho,学长加油
    Menci
        10
    Menci  
       Jun 1, 2018
    那我也把我的 OJ 发出来吧,https://github.com/syzoj/syzoj https://loj.ac
    superlks
        11
    superlks  
       Jun 2, 2018 via iPhone
    mark 一下,我们目前内部使用的也是一个开源 oj 魔改的
    fan123199
        12
    fan123199  
       Jun 2, 2018
    支持一下, 有点厉害,最近在学 nodejs, 学习一下。
    chenlaocong
        13
    chenlaocong  
       Jun 2, 2018
    卧槽 一看 url,妈的校友啊 cjlu
    jinya
        14
    jinya  
       Jun 2, 2018
    你们的测试数据哪里来的?
    Kerminate
        15
    Kerminate  
    OP
       Jun 2, 2018
    @jinya 自己出的
    Kerminate
        16
    Kerminate  
    OP
       Jun 2, 2018
    @chenlaocong 喔爪。
    Oz2011
        17
    Oz2011  
       Jun 11, 2018
    哈哈 最近开始学 web 开发,也准备写一个 OJ 来练练手,mark
    zxCoder
        18
    zxCoder  
       Sep 10, 2020
    判题用 shell 去调用效率怎么样
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3476 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 95ms · UTC 04:22 · PVG 12:22 · LAX 21:22 · JFK 00:22
    ♥ Do have faith in what you're doing.