Vue + Koa 搭建 ACM OJ - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Kerminate

Vue + Koa 搭建 ACM OJ

  •  
  •   Kerminate 2018 年 6 月 1 日 7212 次点击
    这是一个创建于 2884 天前的主题,其中的信息可能已经有所发展或是发生改变。

    花了两个多月时间,我与 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 = => 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 条回复    2020-09-10 10:11:28 +08:00
    0915240
        1
    0915240  
       2018 年 6 月 1 日 via iPhone
    滋持下。
    simple11
        2
    simple11  
       2018 年 6 月 1 日
    mark 一下
    LeungJZ
        3
    LeungJZ  
       2018 年 6 月 1 日
    马克一下。

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

    这是我之前写的,没那么多花里胡哨的前端技巧,就是干。
    mengrenzi
        9
    mengrenzi  
       2018 年 6 月 1 日 via iPhone
    yoho,学长加油
    Menci
        10
    Menci  
       2018 年 6 月 1 日
    那我也把我的 OJ 发出来吧,https://github.com/syzoj/syzoj https://loj.ac
    superlks
        11
    superlks  
       2018 年 6 月 2 日 via iPhone
    mark 一下,我们目前内部使用的也是一个开源 oj 魔改的
    fan123199
        12
    fan123199  
       2018 年 6 月 2 日
    支持一下, 有点厉害,最近在学 nodejs, 学习一下。
    chenlaocong
        13
    chenlaocong  
       2018 年 6 月 2 日
    卧槽 一看 url,妈的校友啊 cjlu
    jinya
        14
    jinya  
       2018 年 6 月 2 日
    你们的测试数据哪里来的?
    Kerminate
        15
    Kerminate  
    OP
       2018 年 6 月 2 日
    @jinya 自己出的
    Kerminate
        16
    Kerminate  
    OP
       2018 年 6 月 2 日
    @chenlaocong 喔爪。
    Oz2011
        17
    Oz2011  
       2018 年 6 月 11 日
    哈哈 最近开始学 web 开发,也准备写一个 OJ 来练练手,mark
    zxCoder
        18
    zxCoder  
       2020 年 9 月 10 日
    判题用 shell 去调用效率怎么样
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1570 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 68ms UTC 16:41 PVG 00:41 LAX 09:41 JFK 12:41
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86