MuOJ搭建

做了这么久的算法题突然突发奇想自己搭建一个OJ系统

预计技术栈:Vue3 + Spring Boot + Spring Cloud 微服务 + Docker,争取暑假结束之前搭建出来,祝我好运吧~

搭建完成后,会上传到Github上,还请大家多多给点Star呀⭐,谢谢大家啦


开发日记

Day 1:

基本完成前端框架的搭建,学习了如何使用routes管理,更新用户信息
其中的user.ts:

// initial state
import { StoreOptions } from "vuex";

export default {
  namespaced: true,
  state: () => ({
    loginUser: {
      userName: "未登录",
      role: "notLogin",
    },
  }),
  actions: {
    getLoginUser({ commit, state }, payload) {
      commit("updateUser", { userName: "MuQYY" });
    },
  },
  mutations: {
    updateUser(state, payload) {
      state.loginUser = payload;
    },
  },
} as StoreOptions<any>

大概就是长酱紫啦~
图片[1]-MuOJ搭建-MuQYY的博客

Day 2:

感觉Day 1的笔记写的太简陋了,所以今天的笔记边写代码边做哈哈哈😄

前端

优化页面布局

1、底部footer布局优化
将之前的absolute布局改为sticky

#basic-layout .footer {
  background: #efefef;
  padding: 16px;
  position: sticky;
  bottom: 0;
  left: 0;
  right: 0;
  text-align: center;
}

2、优化用户名称在屏幕折叠时的换行问题

  <a-row
    id="globalHeader"
    class="grid-demo"
    style="margin-bottom: 16px"
    align="center"
    :wrap="false" // 这里关闭换行
  >

3、优化content、globalHeader的样式

根据配置控制菜单的显隐

1)routes.ts给路由新增一个标志位,用于判断路由是否显隐

  {
    path: "/hide",
    name: "隐藏页面",
    component: HomeView,
    meta: {
      hideInMenu: true,
    },
  },

2)不要用v-for + v-if去条件渲染元素,这样会先循环所有的元素,导致性能的浪费
推荐:先过滤只需要展示的元素数组

// 展示在菜单的路由数组
const visibleRoutes = routes.filter((item, index) => {
  if (item.meta?.hideInMenu) {
    return false;
  }
  return true;
});

然后我们只需要遍历visibleRoutes就可以了

        <a-menu-item v-for="item in visibleRoutes" :key="item.path">
          {{ item.name }}
        </a-menu-item>

根据权限隐藏菜单

我们只想给用户提供具有权限的菜单
类似上面的控制路由显示隐藏,只要判断用户没有这个权限,直接过滤掉

全局权限管理
1)定义权限

/**
 * 权限定义
 */
const ACCESS_ENUM = {
  NOT_LOGIN: "notLogin",
  USER: "user",
  ADMIN: "admin",
};

export default ACCESS_ENUM;

2)定义公用的权限校验方法
抽离成抽象方法,方便维护和使用

创建checkAccess.ts文件,专门定义检测权限的函数

import ACCESS_ENUM from "@/access/accessEnum";

/**
 * 检查权限(判断当前登录用户是否具有每个权限)
 * @param loginUser 当前登录用户
 * @param needAccess 需要的权限
 * @return boolean 有无权限
 */
const checkAccess = (loginUser: any, needAccess = ACCESS_ENUM.NOT_LOGIN) => {
  // 获取当前用户具有的权限
  const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN;
  if (needAccess === ACCESS_ENUM.NOT_LOGIN) {
    return true;
  }
  // 如果需要用户登录才能访问
  if (needAccess === ACCESS_ENUM.USER) {
    // 如果用户没登陆,表述没有权限
    if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {
      return false;
    }
  }
  if (needAccess === ACCESS_ENUM.ADMIN) {
    if (loginUserAccess !== ACCESS_ENUM.ADMIN) {
      return false;
    }
  }
  return true;
};

export default checkAccess;

3)修改GlobalHeader动态菜单组件,根据权限来过滤菜单
注意这里要使用计算属性,是为了当登录用户信息发生变更时,触发菜单栏的重新渲染,展示新增权限的菜单栏

const visibleRoutes = computed(() => {
  const loginUser = store.state.user.loginUser;
  return routes.filter((item, index) => {
    if (item.meta?.hideInMenu) {
      return false;
    }
    // 根据权限过滤菜单
    if (!checkAccess(loginUser, item?.meta?.access as string)) {
      return false;
    }
    return true;
  });
});

全局项目入口

app.vue中预留一个可以编写全局初始化逻辑的代码:

/**
 * 全局初始化函数,有全局单次调用的代码都可以写在这里
 */
const doInit = () => {
  console.log("hello!");
};

onMounted(() => {
  doInit();
});

Day 3:

后端项目初始化

1)使用万用模板,建立数据库,试运行查看api文档
图片[2]-MuOJ搭建-MuQYY的博客
2)sql/create_table.sql:定义数据库的初始化建库建表语句
3)sql/post_es_mapping.json:帖子表在ES中的建表语句
4)aop:用于全局权限校验、全局日志记录
5)common:万用的类,比如通用相应类
6)config:用于接收application.yml中的参数,初始化一些客户端的配置类(比如对象存储客户端)
7)constant:定义常量
8)controller:接受请求
9)esdao:类似mybatis的mapper,用于操作ES
10)exception:异常处理相关
11)job:任务相关(定时任务、单次任务)
12)manager:服务层(一般是定义一些公用的服务,对接第三方API等)
13)mapper:mybatis的数据访问层,用于操作数据库
14)model:数据模型、实体类、包装类、枚举值
15)service:服务层,用于编写业务逻辑
16)utils:工具类,各种各样公用的方法
17)wxmp:公众号相关包
18)test:单元测试
19)MainApplication:项目启动入口
20)Dockerfile:用于构建Docker镜像

前后端联调

前端发送请求调用后端接口
使用openapi-typescript-codegen
https://github.com/ferdikoomen/openapi-typescript-codegen
生成generated目录,将接口接入前端
这里要注意的是,此处前端会报一个错误
图片[3]-MuOJ搭建-MuQYY的博客
我们需要修改OpenAPI.ts中的BASE,将/api去掉

图片[4]-MuOJ搭建-MuQYY的博客
这样就不会报错啦~

直接使用生成的Service代码,直接调用函数发送请求即可

actions: {
    async getLoginUser({ commit, state }, payload) {
      const res = await UserControllerService.getLoginUserUsingGet();
      // 从远程请求获取登录信息
      if (res.code === 0) {
        commit("updaterUser", res.data);
      } else
        commit("updateUser", {
          ...state.loginUser,
          userRole: ACCESS_ENUM.NOT_LOGIN,
        });
    },
  },

用户登录功能

自动登录

1)在store\user.ts编写获取远程登录用户信息的代码

actions: {
    async getLoginUser({ commit, state }, payload) {
      const res = await UserControllerService.getLoginUserUsingGet();
      // 从远程请求获取登录信息
      if (res.code === 0) {
        commit("updaterUser", res.data);
      } else
        commit("updateUser", {
          ...state.loginUser,
          userRole: ACCESS_ENUM.NOT_LOGIN,
        });
    },
  },

2)在哪里去触发getLoginUser函数的执行呢?应当在一个全局的位置
options:

  1. 全局拦截
  2. 全局页面入口 app.vue
  3. 全局通用布局(所以页面都共享的组件)

全局权限管理优化

1)新建access\index.ts文件,把原有的路由拦截、权限校验逻辑放在独立的文件中,只要不引入就不会对项目有影响

2)编写权限管理和自动登录逻辑
如果没登陆过就自动登录

const loginUser = store.state.user.loginUser;
  // 如果之前没登陆过,自动登录
  if (!loginUser || !loginUser.userRole) {
    // 加await是为了等用户登陆成功之后再执行
    await store.dispatch("user/getLoginUser");
  }

如果用户访问的页面不需要登录,是否需要强制跳转到登录页?
答:不需要!
access/index.ts

import router from "@/router";
import store from "@/store";
import ACCESS_ENUM from "@/access/accessEnum";
import checkAccess from "@/access/checkAccess";

router.beforeEach(async (to, from, next) => {
  console.log("登录用户信息", store.state.user.loginUser);

  const loginUser = store.state.user.loginUser;
  // // 如果之前没登陆过,自动登录
  // if (!loginUser || !loginUser.userRole) {
  //   // 加await是为了等用户登陆成功之后再执行
  //   await store.dispatch("user/getLoginUser");
  // }
  const needAccess = (to.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN;
  // 要跳转的页面必须要登录
  if (needAccess !== ACCESS_ENUM.NOT_LOGIN) {
    // 如果没登录,则跳转到登录页面
    if (!loginUser || loginUser.userRole === ACCESS_ENUM.NOT_LOGIN) {
      next(`/user/login?redirect=${to.fullPath}`);
      return;
    }
    // 如果已经登陆了,检查权限
    if (!checkAccess(loginUser, needAccess)) {
      next("/noAuth");
      return;
    }
  }
  next();
});

支持多套布局

1)在routes路由文件中新建一套用户路由,使用vue-router自带的子路由机制,实现布局和嵌套路由

export const routes: Array<RouteRecordRaw> = [
  {
    path: "/user",
    name: "用户",
    component: UserLayout,
    children: [
      {
        path: "/user/login",
        name: "用户登录",
        component: UserLoginView,
      },
      {
        path: "/user/register",
        name: "用户注册",
        component: UserRegisterView,
      },
    ],
  },
]

2)新建UserLayout、UserLoginView、UserRegister页面,并在routes中引入
3)在app.vue根页面文件,根据路由区分多套布局

    <template v-if="route.path.startsWith('/user')">
      <router-view />
    </template>
    <template v-else>
      <BasicLayout />
    </template>

更好的优化思路:直接读取routes.ts,在这个文件中定义多套布局,然后自动使用页面布局

登录页面开发

完成!
图片[5]-MuOJ搭建-MuQYY的博客

注册页面开发

完善后端的register接口,加上userName信息
完成!
图片[6]-MuOJ搭建-MuQYY的博客

Day 4:

创建题目表配置

-- 题目表
create table if not exists question
(
    id         bigint auto_increment comment 'id' primary key,
    title      varchar(512)                       null comment '标题',
    content    text                               null comment '内容',
    tags       varchar(1024)                      null comment '标签列表(json 数组)',
    answer     text                               null comment '题目答案',
    submitNum  int default 0                      not null comment '题目提交数',
    acceptedNum int default 0                     not null comment '题目通过数',
    judgeCase  text                               null comment '判题用例(json数组)',
    judgeConfig  text                             null comment '判题配置(json对象)',
    thumbNum   int      default 0                 not null comment '点赞数',
    favourNum  int      default 0                 not null comment '收藏数',
    userId     bigint                             not null comment '创建用户 id',
    createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete   tinyint  default 0                 not null comment '是否删除',
    index idx_userId (userId)
) comment '题目' collate = utf8mb4_unicode_ci;

创建题目提交表

-- 题目提交表
create table if not exists question_submit
(
    id         bigint auto_increment comment 'id' primary key,
    language   varchar(128)                       not null comment '编程语言',
    code       text                               not null comment '用户代码',
    judgeInfo  text                               null comment '判题信息(json对象)',
    status     int      default 0                 not null comment '判题状态(0 - 待判题、1 - 判题中、2 - 成功、3 - 失败)',
    questionId bigint                             not null comment '题目 id',
    userId     bigint                             not null comment '提交用户 id',
    createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    isDelete   tinyint  default 0                 not null comment '是否删除',
    index idx_questionId (questionId),
    index idx_userId (userId)
) comment '题目提交';

编写后端代码

updateRequest 和 editRequest 的区别:前者是给管理员更新用的,可以指定更多字段;后者是给用户用的,只能指定部分字段。

为了更方便地处理json字段中的某个字段,需要给对应的json字段编写一个独立的类。

示例代码:

package com.yupi.muoj.model.dto.question;

import lombok.Data;

/**
 * 题目用例
 */
@Data
public class JudgeCase {
    /**
     * 输入用例
     */
    private String input;

    /**
     * 输出用例
     */
    private String output;

}

定义VO类:作用是专门给前端返回对象,可以节约网络传输大小,或者过滤字段,保障安全性

编写枚举类

package com.yupi.muoj.model.enums;

import org.apache.commons.lang3.ObjectUtils;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 题目提交编程语言枚举
 *
 */
public enum QuestionSubmitLanguageEnum {

    JAVA("Java", "java"),
    CPLUSPLUS("C++", "c++"),
    GOLANG("Golang", "golang"),
    PYTHON("Python", "python"),
    JAVASCRIPT("JavaScript", "javascript"),
    PHP("PHP", "php"),
    RUST("Rust", "rust"),
    SWIFT("Swift", "swift"),
    KOTLIN("Kotlin", "kotlin"),
    RUBY("Ruby", "ruby"),
    CSHARP("C#", "c#");

    private final String text;

    private final String value;

    QuestionSubmitLanguageEnum(String text, String value) {
        this.text = text;
        this.value = value;
    }

    /**
     * 获取值列表
     *
     * @return
     */
    public static List getValues() {
        return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
    }

    /**
     * 根据 value 获取枚举
     *
     * @param value
     * @return
     */
    public static QuestionSubmitLanguageEnum getEnumByValue(String value) {
        if (ObjectUtils.isEmpty(value)) {
            return null;
        }
        for (QuestionSubmitLanguageEnum anEnum : QuestionSubmitLanguageEnum.values()) {
            if (anEnum.value.equals(value)) {
                return anEnum;
            }
        }
        return null;
    }

    public String getValue() {
        return value;
    }

    public String getText() {
        return text;
    }
}

查询提交信息接口

功能:能够根据用户id、编程语言、题目id、提交状态去查询提交记录
注意事项:仅本人和管理员能看见自己(提交userId和用户Id不同)的提交代码

实现方案:先查询,后脱敏!

    @Override
    public QuestionSubmitVO getQuestionSubmitVO(QuestionSubmit questionSubmit, User loginUser) {
        QuestionSubmitVO questionSubmitVO = QuestionSubmitVO.objToVo(questionSubmit);
        // 脱敏:仅本人和管理员能看见自己(提交userId和用户Id不同)的代码
        long userId = loginUser.getId();
        if (userId != questionSubmit.getUserId() && !userService.isAdmin(loginUser)) {
            questionSubmitVO.setCode(null);
        }
        return questionSubmitVO;
    }

图片[7]-MuOJ搭建-MuQYY的博客

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 共2条

请登录后发表评论

    暂无评论内容