花了兩個多月時間,我與 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提供了兩種用戶,普通用戶和管理員用戶。顧名思義,普通用戶只能答題,參加比賽,發帖,查看信息等,管理員用戶擁有對信息,題目,比賽等增刪改查的權限。
給你們提早註冊了一個普通用戶帳號,帳號 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')
複製代碼
同時前端用了很多第三方組件實現小的需求。
├── 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 ⭐️ 鼓勵一下吧~