Vue + Koa 搭建 ACM OJ

花了兩個多月時間,我與 lazzzis 完成了第二版本的Putong OJ,由於中間忙着春招以及畢業設計等,項目最近才正式上線。javascript

項目線上地址:acm.cjlu.edu.cn/css

項目前端地址:github.com/acm309/Puto…html

項目後端地址:github.com/acm309/Puto…前端

這裏求一下star啊(^o^)/~vue

本OJ前端架構爲 Vue2.5 + vue-router + vuex + axios + iview + stylus + webpack3.6 後端架構爲 Koa2 + MongoDB + redisjava

開發背景

咱們學校 acm 起步較晚,最先的 OJ 是由 Hust OJ 魔改而來,界面寫的比較粗糙。2年前,那屆的 acm 隊長原本決定使用 Vue + Go 重寫一下 OJ,可是由於一些緣由,他跑路了,最後只 fork 了一個開源 OJ。一年前,lazzzis 開始重構 OJ,採用了 Vue + node,開發出了 Putong OJ 的第一個版本。今年,因爲老師增長了功能上的一些需求,再加上後端數據結構又發生了一些變化,以及對第一個版本不太滿意,我與 lazzzis 再次重構,開發出了 Putong OJ V2 版本。node

技術選型

考慮過要用 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 差很少大)。webpack

後端其實咱們不太喜歡 Java,最後用了輕量又方便的 node。數據庫用了 MongoDB,主要是方便 js 操做,同時用 redis 作數據緩存,並作了簡單的消息隊列。ios

預覽

主題色採用了 lazzzis 特別喜歡的騷紫。git

實現功能

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 ⭐️ 鼓勵一下吧~

相關文章
相關標籤/搜索