做了这么久的算法题突然突发奇想自己搭建一个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>
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的博客](https://s3.bmp.ovh/imgs/2024/08/17/a9555aef56f2954a.png)
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的博客](https://s3.bmp.ovh/imgs/2024/08/17/b9accaa0d5c3b8ee.png)
我们需要修改OpenAPI.ts中的BASE,将/api去掉
![图片[4]-MuOJ搭建-MuQYY的博客](https://s3.bmp.ovh/imgs/2024/08/17/9afd7fb25837e05f.png)
这样就不会报错啦~
直接使用生成的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:
- 全局拦截
- 全局页面入口 app.vue
- 全局通用布局(所以页面都共享的组件)
全局权限管理优化
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的博客](https://s3.bmp.ovh/imgs/2024/08/18/fed48732dc30c924.png)
注册页面开发
完善后端的register接口,加上userName信息
完成!
![图片[6]-MuOJ搭建-MuQYY的博客](https://s3.bmp.ovh/imgs/2024/08/18/6c32c06a5e81a416.png)
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的博客](https://s3.bmp.ovh/imgs/2024/08/19/7613961b2eaf9a48.png)
- 1本网站名称:MuQYY
- 2本站永久网址:www.muqyy.top
- 3本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长 微信:bwj-1215 进行删除处理。
- 4本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
- 5本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
- 6本站资源大多存储在云盘,如发现链接失效,请联系我们我们会在第一时间更新。



![图片[1]-MuOJ搭建-MuQYY的博客](http://www.muqyy.top/wp-content/uploads/2024/08/1723469331-屏幕截图-2024-08-12-212824.png)




暂无评论内容