It took lazzzis and I more than two months to complete the second version of Putong OJ, which was recently launched due to busy spring recruitment and graduation projects.

Project online address: ACm.cjlu.edu.cn/

Front-end address: github.com/acm309/Puto…

Project backend address: github.com/acm309/Puto…

So let’s figure out the star over here (^o^)/~

The front-end architecture of OJ is Vue2.5 + VUE-Router + VUex + AXIos + iview + Stylus + webpack3.6 and the back-end architecture is Koa2 + MongoDB + Redis

Development background

Acm in our school started late. The earliest OJ was modified from Hust OJ, and the interface was rough. 2 years ago, the acm captain decided to rewrite OJ using Vue + Go, but for some reason he ran away and ended up forking an open source OJ. A year ago, Lazzzis started refactoring OJ, adopting Vue + Node and developing the first version of Putong OJ. This year, due to the teacher’s increased functional requirements, some changes in the back-end data structure and dissatisfaction with the first version, LAzzzis and I reconstructed again and developed Putong OJ V2 version.

Technology selection

I considered using React, but Vue is easy to use and has a lot of Chinese resources, so I decided to use Vue family buckets. Vue-resource was originally recommended by VUE 1.0, but in vUE 2.0, vue-Resource was no longer recommended, and axios was instead recommended for back and forth communication. We started with Element as the UI library for Vue, and later switched to iView. In fact, the two UI libraries are very similar, both of which are Ant-Design style and have the same API. In my eyes, the big difference is that Element’s components are larger and iView’s components are smaller (visually, Element Small is about the same size as iView default.

On the back end, we didn’t really like Java, so we ended up with a lightweight and convenient Node. The database uses MongoDB, mainly to facilitate JS operation, at the same time using Redis to do data cache, and do a simple message queue.

preview

The theme color is purple, which Lazzzis particularly likes.

Realize the function

OJ is divided into web side and problem solving side. This side mainly analyzes the Web side, and the problem solving side is changed from the problem solving side of Acdream. The Web terminal has seven modules: message module, topic module, discussion module, status module, ranking module, competition module and administrator module. This OJ provides two types of users, ordinary users and administrator users. As the name implies, ordinary users can only answer questions, participate in competitions, post, view information, etc., administrator users have the rights to add, delete, change and check information, topics, competitions, etc.

  • Message module is the home page of OJ, including the list page and message details page, which is mainly the message released by the administrator.
  • Topic module OJ one of the core modules, containing topic list page and topic details page, topic details page has 6 TAB pages, topic description, submit, my submission, statistics, edit, test data. Edit and test data two TAB pages are visible only to the administrator.
  • The discussion module is essentially a discussion area where users can post comments.
  • The status module user submits the result of the question.
  • Ranking module user ranking, with grouping function, easy for the teacher statistics results
  • Tournament module one of the core modules, contains the tournament list and the tournament details page, the tournament details page has 6 TAB pages, overview, title, submission, status, ranking, edit. The edit page is visible only to the administrator.
  • Administrator module one of the core modules, containing create message, create topic, create competition, user management four function pages.

I have pre-registered a common user account, account 123456, password 123456, welcome to try it.

The front end

Take a look at the project structure of the front end, built through scaffolding vue-CLI

├ ─ ─ dist / / generated packed file │ ├ ─ ─ the static │ │ ├ ─ ─ CSS │ │ ├ ─ ─ fonts │ │ ├ ─ ─ img │ │ └ ─ ─ js │ └ ─ ─ index. The HTML └ ─ ─ the SRC ├ ─ ─ the main, js // Project Entry ├─ Router // Shows each route will use the components of │ ├ ─ ─ index. The js / / router configuration and reference component │ └ ─ ─ routes. The js / / define each routing ├ ─ ─ assets/logo/website resources ├ ─ ─ the components / / Small components ├ ─ ─ store / / vuex file │ └ ─ ─ modules / / child modules ├ ─ ─ utils/tools/js method └ ─ ─ views / / routing the corresponding component (these components in the router. Were introduced in js) ├ ─ ─ Admin ├── Contest ├─ News ├─ ProblemCopy the code

There are more than 30 pages in the front end, but most of them are charts, and the page logic is not complicated. Iview loads on demand, reducing front-end package size. To ensure the loading speed of the first screen, some routes are loaded lazily.

// Route lazy loading
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')
Copy the code

At the same time, the front end uses many third-party components to achieve small requirements.

  • Vue-echarts: A VUe-based ECharts component used in a project to display a statistical analysis diagram of submitted results.
  • Vue2-editor: vuue-based rich text editor, used for editing topic content, supporting basic functions such as uploading pictures.
  • Vue.Draggable: VUe-based Draggable component for administrators to make changes to the order of contest questions.
  • Vue-clipboard 2: Vue-based clipboard for easy code copying.
  • Vuex-router-sync: enables $route of vue-router to be accessed in the state of vuex.
  • Highlight.js: code in the page is highlighted.

The back-end

│ ├─ Config │ ├─ Mode │ routes │ ├─ controllers │ ├─ services │ ├─ mode │ routes │ ├─ controllers │ Main services (Exercises, Email Reminders, Updates) ├─ Utills // JS Tool functions ├─test // 测试
├── app.js
└── manage.js

Copy the code

The back end is developed using KOA2 and uses async/await instead of callbacks, avoiding callback hell. The main data are stored in MongoDB, using the Node Mongoose package. In order to avoid the problem of high concurrency caused by multiple people submitting questions at the same time, the interface follows RESTful design, and redis is used to make queue cache for the judgment questions. Questions submitted by users will be entered into Redis, and then queue up one by one and handed to the question examiner for processing. Normally, the ranking will be closed at the last hour of ACM competition (ranking and AC title will not be updated, but the submission times of users will be updated). Here, Redis is also used to update the ranking. During the competition, only data will be saved in Redis and the ranking will be closed. After the game, all the information will be saved to Mongo.

// Return to the leaderboard during a match
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) {
    // If the match is not in the last hour, the latest ranklist will be pushed to redis
    const str = JSON.stringify(ranklist)
    await redis.set(`oj:ranklist:${contest.cid}`, str) // Update the latest ranking information of the competition
    res = ranklist
  } else if(! isAdmin(ctx.session.profile) &&Date.now() + deadline > contest.end &&
    Date.now() < contest.end) {
    // During the last hour of the contest, ordinary users can only see the change of the submitted topic
    const mid = await redis.get(`oj:ranklist:${contest.cid}`) // Get the ranking information of the tournament in 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) // Update the updated rankList to redis
    // Match over
    res = ranklist
  }
  ctx.body = {
    ranklist: res
  }
}
Copy the code

The project uses Docker for one-click deployment. Dockerfile is written to customize the image on the Web end, and all the images required by the project are configured in docker-compose. The deployment process

The last

Space is limited, can not show more content, if you are interested in the project address to read the source code, of course, if you feel that the project is good 👏, give a star ⭐️ to encourage it ~