OJ在线判题系统(一)
OJ在线判题系统(一)
项目设计
### 实现核心
- 权限校验:谁能提代码,谁不能提代码
- 代码沙箱(安全沙箱)
- 隔离藏毒的用户代码
- 资源分配-防止用户恶意疯狂占用资源占满内存
- 判题规则:判题用例的比对
- 任务调度:在服务器资源有限用户要排队的情况下如何按照顺序依次判题,而不是直接拒绝
### 核心业务流程

不编译不能运行代码
时序图:
按时间顺序、调用的顺序去画
判题服务:获取题目信息,预计的输入输出结果,返回给主业务后端:用户的答案是否正确
代码沙箱:只负责运行代码,给出结果,不管什么结果是正确的。
功能
- 题目模块
- 创建题目(管理员)
- 删除题目(管理员)
- 搜索题目(用户)
- 在线做题
- 提交题目代码
- 用户模块
- 注册
- 登录
- 判题模块
- 提交判题(结果是否正确)
- 错误处理(内存溢出、安全性、超时)
- 自主实现代码沙箱
- 开放接口
技术选型
前端:Vue3、Arco Design 组件库、手撸项目模板、在线代码编辑器、在线文档浏览
Java进程控制、Java安全管理器、部分JVM 知识点
虚拟机(云服务器)、Docker(代码沙箱实现)
Spring Cloud 微服务、消息队列、多种设计模式
架构设计
一、你要知道分哪些层;二、你要知道每一层有哪些模块

主流的OJ系统实现方案
开发原则:能用别人的轮子,就不要造轮子
这里鱼皮走的是自主开发,就不详写了,感兴趣自己看成熟项目Judge0的代码
代码:https://github.com/judge0/judge0
Judge0 APl 地址: https://rapidapi.com/judge0-official/api/judge0-ce
官方文档:https://cejudge0.com/#submissions-submission-post
前后端初始化
前端:
准备工具:nodejs + npm + vue-cli脚手架

其中vue-cli脚手架安装:
▼shell复制代码npm install -g @vue/cli
其他工具的安装问AI,nvm在这里只起到换nodejs版本的作用,nodejs版本在16/18的可不下nvm
创建项目
▼shell复制代码vue create demo-frontend-name

选Manually select features

这里多选,用空格选上
其中TypeScript是语言,Router是路由,Vuex是状态管理

选3.x
剩余设置看图片

先运行试试
- 打开
package.json点击serve左边的绿色小箭头 - 终端输入,我的是
yarn serve,具体看你初始化时的提示:

这里遇到了问题,vue-router找不到,刚初始化就报错也是难绷
试了两次都出现报错,应该是版本问题
这个是AI修复的回答:
你这个项目“其实已经启动了”,但被 TypeScript 类型检查错误拦住了热更新流程。
核心原因是版本不兼容:
- 你的项目是老模板(
vue@3.2.13+typescript~4.5.5+ vue-cli 5)- 但现在装到的是新版本
vue-router@4.6.4vue-router@4.6.4的类型声明是dist/vue-router.d.mts,对你这套旧 TS/解析配置不友好,导致报:
TS7016: Could not find a declaration file for module 'vue-router'另外它还要求
peerDependencies: vue ^3.5.0,也和你当前vue 3.2.13不一致。解决办法:
把
vue-router降到和当前项目年代一致的版本:
yarn add vue-router@4.1.6然后重新启动:
yarn serve
运行成功后继续
前端工程化配置
创建后在webStorm里开启代码美化插件
setting-Languages & Frameworks(语言和框架)-Javascript-Prettier

在vue文件中执行格式化快捷键Ctrl+Alt+L,不报错,表示配置工程化成功
引入组件
组件库:arco.design/vue
快速上手:arco.design/vue/docs/start
注意:vue版本≥3.2.0
npm install --save-dev @arco-design/web-vue
这里使用全局引入
▼ts复制代码import { createApp } from "vue"; import App from "./App.vue"; import ArcoVue from "@arco-design/web-vue"; import "@arco-design/web-vue/dist/arco.css"; import router from "./router"; import store from "./store"; createApp(App).use(ArcoVue).use(store).use(router).mount("#app");
注:如果出现报错而,有可能是不符合Pretter/ESLint代码规范导致的,按
ctrl+Alt+L即可
引入一个组件,如果显示出来,就表示引入成功
用日历组件进行实验(放那个vue都行只是看能不能引入)
▼vue复制代码<template> <a-calendar v-model="value" /> select: {{ value }} </template> <script> import { ref } from "vue"; export default { setup() { const value = ref(new Date("2023-01-01")); return { value, }; }, }; </script>
运行显示日历即可

初始化的这些组件(HelloView等)先别删,后面还有用
搞布局
创建文件夹layouts,里面创建vue文件BasicLayout.vue
将这个布局文件引入到App.vue中
▼vue复制代码<template> <div id="app"> <BasicLayout /> </div> </template>
引入完成,接下来是敲BasicLayout.vue来实现布局了,先把上中下布局编排好,然后再填充内容。
这里使用arco.design给的布局组件库
这里鱼皮是不停调整css样式(比如修改颜色让布局更清晰)的,故只附上最终源码
BasicLayout.vue:
▼vue复制代码<template> <div id="basicLayout"> <a-layout style="height: 400px"> <a-layout-header class="header"> <GlobalHeader></GlobalHeader> </a-layout-header> <a-layout-content class="content"> <router-view /> </a-layout-content> <a-layout-footer class="footer"> <a href="http://yupi.icu.com" target="_blank" >OJ判题系统 by 桃木</a ></a-layout-footer > </a-layout> </div> </template> <script setup lang="ts"> import GlobalHeader from "@/components/GlobalHeader.vue"; </script> <style scoped> #basicLayout { } #basicLayout .header { background: red; margin-bottom: 16px; } #basicLayout .content { background: linear-gradient(to right, #bbb, #fff); margin-bottom: 16px; } #basicLayout .footer { background: #efefef; padding: 14px; position: fixed; bottom: 0; right: 0; left: 0; text-align: center; } </style>
GlobalHeader.vue:
▼vue复制代码<template> <div id="globalHeader"> <a-menu mode="horizontal" :selected-keys="selectedKeys" @menu-item-click="doMenuClick" > <a-menu-item key="0" :style="{ padding: 0, marginRight: '38px' }" disabled > <div class="title-bar"> <img src="@/assets/logo.png" alt="" class="logo" /> <div class="title">桃 OJ</div> </div> </a-menu-item> <a-menu-item v-for="item in routes" :key="item.path"> {{ item.name }} </a-menu-item> </a-menu> </div> </template> <script lang="ts" setup> import { routes } from "../router/routes"; import { useRouter } from "vue-router"; import { ref } from "vue"; const router = useRouter(); //默认主页 const selectedKeys = ref(["/"]); //路由跳转后,更新激活项 router.afterEach((to, from, next) => { selectedKeys.value = [to.path]; }); const doMenuClick = (key: string) => { router.push({ path: key, }); }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .title-bar { display: flex; align-items: center; } .title { color: #444; margin-left: 16px; } .logo { height: 48px; } </style>
这时候你会发现报错,这是因为在
router-index.ts文档下,鱼皮将里面具体的路由路径与配置做了分离即:在router文件夹下创建routes.ts,将
const routes的内容搬进去routes.ts:
▼ts复制代码import { RouteRecordRaw } from "vue-router"; export const routes: Array<RouteRecordRaw> = [];(因为我把初始化自带的组件都删了,所以我这很干净……)
然后在原先的
index.ts文件中引入这个routes▼text复制代码import { routes } from "@/router/routes";
演示一下样式,如果你的routes不为空,组件还没删,能看到顶部的导航栏=。=

全局状态管理
工具:vuex
状态管理:所有页面全局共享的变量,而不是仅存在于一个页面中
比如:同步已登录的用户信息
本质上:给你提供了一套增删改查全局变量的API,只不过可能多了一些功能(比如时间旅行)
存放vuex全局管理信息的位置:store-index.ts
state:存储的状态信息,比如用户信息 mutation(尽量同步):定义了对变量进行增删改(更新)的方法 actions(支持异步):执行异步操作,并且触发mutation 的更改(actions调用mutation) modules(模块):把一个大的state(全局变量)划分为多个小模块,比如user专门存用户的状态信息
这里创建user.ts,并在index.ts的modules里定义了user
▼ts复制代码import { createStore } from "vuex"; import user from "@/store/user"; export default createStore({ state: {}, getters: {}, mutations: {}, actions: {}, modules: { user }, });
如果把store比作一个数据库,那么user此刻就是里面的一张表
user.ts:
▼ts复制代码import { StoreOptions } from "vuex"; export default { namespaced: true, state: { loginUser: { userName: "未登录" }, }, getters: {}, actions: { getLoginUser({ commit, state }, payload) { commit("updateUser", { userName: "鱼皮" }); }, }, mutations: { updateUser(state, payload) { state.loginUser = payload; }, }, } as StoreOptions<any>;
然后修改GlobalHeader测试全局管理的效果
在已有的导航栏外添加栅格布局(template)
▼vue复制代码<a-row align="center" style="margin-bottom: 16px" id="globalHeader"> <a-col flex="auto"> <a-menu mode="horizontal" :selected-keys="selectedKeys" @menu-item-click="doMenuClick" > <a-menu-item key="0" :style="{ padding: 0, marginRight: '38px' }" disabled > <div class="title-bar"> <img src="@/assets/logo.jpg" alt="" class="logo" /> <div class="title">派 OJ</div> </div> </a-menu-item> <a-menu-item v-for="item in routes" :key="item.path"> {{ item.name }} </a-menu-item> </a-menu> </a-col> <a-col flex="100px"> <div>{{ store.state.user?.loginUser?.userName ?? "未登录" }}</div> </a-col> </a-row>
在script中使用vuex进行变量的全局管理
▼vue复制代码//使用vuex const store = useStore(); //添加一个3s后启动的方法 setTimeout(() => { store.dispatch("user/getLoginUser",{userName:"鱼皮"}); }, 3000);
用store.dispatch调用action
store.dispatch(模块名.对应的方法,{传入的参数})
运行

全局权限管理
目标:能够直接以一套通用的机制,去定义哪个页面需要那些权限。而不用每个页面独立去判断权限,提高效率。 思路:
- 在路由配置文件,定义某个路由的访问权限
- 在全局页面组件 app.vue 中,绑定一个全局路由监听。每次访问页面时,根据用户要访问页面的路由信息先判断用户是否有对应的访问权限。
- 如果有,跳转到原页面;如果没有,拦截或跳转到 401鉴权或登录页
在视图(view)下添加两个页面,分别为AdminView.vue和NoAuthView.vue,并配置好路由显示在导航栏上
在路由中用meta定义每个页面需要什么权限
▼ts复制代码import { RouteRecordRaw } from "vue-router"; export const routes: Array<RouteRecordRaw> = [ { path: "/noAuth", name: "无权限", component: () => import("../views/NoAuthView.vue"), }, { path: "/admin", name: "admin", component: () => import("../views/AdminView.vue"), meta: {//注意这个meta access: "canAdmin", }, }, ];
在App.vue中加入跳转逻辑
▼ts复制代码router.beforeEach((to, from, next) => { if (to.meta.access === "canAdmin") { if (store.state.loginUser?.role !== "admin") { next("/noAuth"); return; } } next(); });
效果:点击admin后会跳转无权限页
修改页面布局
- 为了修改footer不在最底部的问题
▼vue复制代码<template> <div id="basicLayout"> //该行修改 <a-layout style="min-height: 100vh"> <a-layout-header class="header"> <GlobalHeader></GlobalHeader> </a-layout-header> <a-layout-content class="content"> <router-view /> </a-layout-content> <a-layout-footer class="footer"> <a href="http://yupi.icu.com" target="_blank" >OJ判题系统 by 桃木</a ></a-layout-footer > </a-layout> </div> </template> #basicLayout .footer { background: #efefef; padding: 16px; //该行修改 position: sticky; bottom: 0; right: 0; left: 0; text-align: center; }
- 过窄时未登录被挤压到第二行
▼vue复制代码//GlobalHeader.vue //添加wrap //为了下一步的优化布局,删掉style <a-row align="center" id="globalHeader" :wrap="false">
- 纯优化布局
▼css复制代码#basicLayout .content { background: linear-gradient(to right, #bbb, #fff); margin-bottom: 16px; //加这行 padding: 20px; } //红色背景早就可以删了,这里改成鱼皮调的阴影 #basicLayout .header { box-shadow: #eee 1px 1px 5px; margin-bottom: 16px; }
通过meta控制页面可见
阶段一 用属性来实现vue页面的展示
逻辑:在meta中添加hideInMenu属性(boolean),然后在vue展示中只展示没有hideInMenu的页面
注意,不要用v-for+v-if去条件渲染元素,这样会先循环所有的元素,导致性能的浪费
方法:先过滤再展示
▼ts复制代码//也是不得已临时创建一个HelloView组件了,引入路径不用模仿我,你们看懂了就行 { path: "/hide", name: "隐藏页面", component: () => import("../views/HelloView.vue"), meta: { hideInMenu: true, }, },
这时候你看到导航栏有更新,这里的任务是只让隐藏页面消失(不要在意那个主页)
需要在GlobalHeader引入的路由中加入过滤条件
▼js复制代码const visibleRouter = routes.filter((item, index) => { if (item.meta?.hideInMenu) { return false; } return true; }); const router = useRouter();
在渲染中将渲染范围中routes改为visibleRoutes
▼vue复制代码<a-menu-item v-for="item in visibleRoutes" :key="item.path"> {{ item.name }} </a-menu-item>
完成

阶段二 让这个属性的值能按照使用逻辑动态改变
定义权限
创建access文件夹,添加accessEnum和checkAccess
在accessEnum.ts中定义不同权限(类似枚举类的形式)
▼ts复制代码/** * 权限定义 */ const ACCESS_ENUM = { NOT_LOGIN: "notLogin", USER: "user", ADMIN: "admin", }; export default ACCESS_ENUM;
抽离权限检查功能
以后的很多功能需要验证当前用户的权限,故将检验权限功能从App.vue中抽离出来
checkAccess.ts
▼ts复制代码import ACCESS_ENUM from "@/access/accessEnum"; /** * 检查权限(判断当前登录用户是否具有某个权限) * @param loginUser 当前登录用户 * @param needAccess 需要有的权限 * @return boolean 有无权限 */ const checkAccess = (loginUser: any, needAccess = ACCESS_ENUM.NOT_LOGIN) => { // 获取当前登录用户具有的权限(如果没有 loginUser,则表示未登录) //如果loginUser.userRole不存在,则loginUserAccess=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;
调整路由携带的参数
将meta里的参数修改成ACCESS_ENUM中定义的常量
routes.ts
▼ts复制代码{ path: "/admin", name: "admin", component: () => import("../views/AdminView.vue"), meta: { access: AccessEnum.ADMIN, }, },
修改store存储的参数名称
前面有的写成role了,这里统一一下,注意getLoginUser里payload部分不要写死
user.ts
▼ts复制代码export default { namespaced: true, state: { loginUser: { userName: "未登录", userRole: AccessEnum.NOT_LOGIN }, }, getters: {}, actions: { getLoginUser({ commit, state }, payload) { commit("updateUser", payload); }, }, mutations: { updateUser(state, payload) { state.loginUser = payload; }, }, } as StoreOptions<any>;
修改导航栏组件动态更新
▼vue复制代码const visibleRoutes = computed(() => { return routes.filter((item, index) => { if (item.meta?.hideInMenu) { return false; } //根据权限过滤菜单 if (!checkAccess(store.state.user.loginUser, item.meta?.access as string)) { return false; } return true; }); });
setTimeout这部分注意将role修改成userRole
▼vue复制代码setTimeout(() => { store.dispatch("user/getLoginUser", { userName: "管理员", userRole: "admin", }); }, 3000);
若3s后用户从未登录变成管理员,且出现admin导航栏,说明此功能实现
后端项目初始化
在知识星球中下载后端源码
https://pan.baidu.com/s/1VkrwKe63VF7KLtM_Jd4kdQ?pwd=lcpu
- ctrl shift R spring-init修改为yuoj-backend
- ctrl shift R springinit 修改为 yuoj
- 包名 springinit 修改为yuoj
- 模块名 修改为yuoj-backend
- 在sql文件夹中运行create_table文件,自己定义数据库
- application.yml中配置MySQL
- 将server底下的port从8101改为8121(也可以不改但你要记住你和鱼皮后端用的端口号不一样)
运行如果显示找不到springinit就在编辑配置中将爆红的springinit修改为yuoj
初始化模板讲解
1)先阅读README.md 2)sql/create_table.sql定义了数据库的初始化建库建表语句
3)sq/post_es_mappingjson 帖子表在 ES 中的建表语句
4)aop:用于全局权限校验、全局日志记录 5)common:万用的类,比如通用响应类 6)config:用于接收application.yml 中的参数,初始化一些客户端的配置类(比如对象存储客户端)
7)constant:定义常量 8)controller:接受请求 9)esdao:类似 mybatis 的mapper,用于操作ES
10)exception:异常处理相关 11)job:任务相关(定时任务、单次任务) 12)manager:服务层(一般是定义一些公用的服务、对接第三方APl等)
13)mapper:mybatis 的数据访问层,用于操作数据库 14)model:数据模型、实体类、包装类、枚举值
15)service:服务层,用于编写业务逻辑
16)utils:工具类,各种各样公用的方法 17)wxmp:公众号相关的包
18)test:单元测试
19)MainApplication:项目启动入口
20)Dockerfile:用于构建Docker镜像
前后端联调
意思是将前端和后端的数据同步起来
- 安装请求工具类axios(里面封装的是ajax)
npm install axios(axios.get()...axios.post()...) - 编写调用后端的代码
1)
参考这个链接直接生成代码:https://github.com/ferdikoomen/openapi-typescript-codegen
npm install openapi-typescript-codegen --save-dev
后执行
openapi --input http://localhost:8121/api/v2/api-docs --output ./generated --client axios
我在这里访问不到http://localhost:8121/api/v2/api-docs,于是在本地创建json文件转存进去,让openapi依据这个json文件生成代码
这里先修改一下插件的全局配置,在generated-core-OpenAPI.ts中修改:
▼ts复制代码export const OpenAPI: OpenAPIConfig = { //删掉api BASE: "http://localhost:8121", VERSION: "1.0", WITH_CREDENTIALS: false, CREDENTIALS: "include", TOKEN: undefined, USERNAME: undefined, PASSWORD: undefined, HEADERS: undefined, ENCODE_PATH: undefined, };
在user.ts中修改action:
▼ts复制代码actions: { //async:声明这是一个异步函数,内部可以使用 await 等待接口返回。 async getLoginUser({ commit, state }, payload) { //这里调用了刚才通过工具生成的 API 服务(UserControllerService) //它会发起一个 HTTP GET 请求到后端的 /user/get/login(或其他对应路径)接口。 const res = await UserControllerService.getLoginUserUsingGet(); //如果后端返回 0(通常代表操作成功),则调用名为 updateUser 的 Mutation。 if (res.code === 0) { commit("updateUser", res.data); } else { //...state.loginUser:保留原有用户信息(如果有的话)。 //强制将 userRole 字段设置为 AccessEnum.NOT_LOGIN(未登录枚举)。 //配合之前的 checkAccess 权限校验函数,确保页面能正确识别出当前是“游客”身份,从而限制某些敏感操作。 commit("updateUser", { ...state.loginUser, userRole: AccessEnum.NOT_LOGIN, }); } }, },
效果:打开浏览器,在网络请求中看到对后端的请求


到这里前后端最基本的对接就已经完成了
让我们理一理逻辑:后端提供API接口,我们用一个github插件将api接口快捷生成前端访问后端的业务代码(services文件夹下),然后我们在vuex的user.ts文件中将数据的更新从完全的前端业务转为依据后端返回的结果而更新
因为你是访问的后端,所以考虑到网络波动的影响,它后端服务器未必立刻为你服务,所以你要加async异步访问要加await,毕竟你不太可能前端的访问刚发出,后端的服务器就立马为你返回结果
2)
直接定义axios请求库的全局参数,比如全局请求响应拦截器
创建plugins目录,添加axios.ts文件
▼ts复制代码// Add a request interceptor import axios from "axios"; axios.interceptors.request.use( function (config) { // Do something before request is sent return config; }, function (error) { // Do something with request error return Promise.reject(error); } ); // Add a response interceptor axios.interceptors.response.use( function (response) { console.log("响应", response); // Any status code that lie within the range of 2xx cause this function to trigger // Do something with response data return response; }, function (error) { // Any status codes that falls outside the range of 2xx cause this function to trigger // Do something with response error return Promise.reject(error); } );
在main.ts中引入,添加一行:
▼ts复制代码import "@/plugins/axios";
打开浏览器发现打印:

为什么我看上去没有明写get/post请求,这里却自动对准了http://localhost:8121/api/user/get/login?
因为你写的是拦截器,它捕获到了应用启动时自动发起的“获取当前登录用户”请求
无论是你手动写的
axios.get还是插件生成的UserControllerService.getLoginUserUsingGet(),只要底层使用的是同一个Axios包,都会经过定义的拦截器而根据之前的代码逻辑,
getLoginUser会在页面加载时自动执行于是触发了拦截器
用户登录功能
自动登录
业务逻辑:用户在登进你的网站后你要第一时间访问服务器获取数据并更新页面
- 在store-user.ts中编写获取远程登录用户信息的代码(vuex专门管理前端的全局变量)
- 在哪里去触发 getLoginUser 函数的执行?应当在一个全局的位置
- 可以用路由拦截、全局页面入口app.vue、全局通用布局等多种方式实现
- 此处选择第一种方案,可以直接在全局权限管理的路由拦截中判断用户是否已经登录
全局权限管理优化
在access下新建index.ts
将App.vue里的关于用户验证的代码移到index.ts中
App.vue:
▼ts复制代码<template> <div id="app"> <BasicLayout></BasicLayout> </div> </template> <style></style> <script setup lang="ts"> import BasicLayout from "@/layouts/BasicLayout.vue"; import { onMounted } from "vue"; /** * 全局初始化函数,有全局单次调用的代码。都可以写到这里 */ const doInit = () => { console.log("hello 欢迎来到我的项目"); }; onMounted(() => { doInit(); }); </script>
在main.ts中引入access
▼ts复制代码import "@/access";
创建access/index.ts(好吧看样子是重新写了逻辑)
▼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; //如果没登陆,自动登录 //守卫会检查内存中是否有用户信息。如果没有,它会阻塞(await)后续操作,先去调用你之前写的 getLoginUser 接口 //防止用户刷新浏览器时,vuex的状态会被清空 if (!loginUser || !loginUser.userRole) { await store.dispatch("user/getLoginUser"); } //如果没有配置 access,则默认该页面是公共的(NOT_LOGIN) const needAccess = (to.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN; //要跳转的页面需要登录 if (needAccess !== ACCESS_ENUM.NOT_LOGIN) { //如果没登录,跳转登录页 if (!loginUser || !loginUser.userRole) { next(`/user/login?redirect=${to.fullPath}`); return; } //已登录但没有权限 if (!checkAccess(loginUser, needAccess as string)) { next("/noAuth"); return; } } // if (to.meta.access === "canAdmin") { // if (store.state.loginUser?.role !== "admin") { // next("/noAuth"); // return; // } // } //如果跳转的页面是公共页面,则直接放行 next(); });
访问http://localhost:8080/admin,重定位到http://localhost:8080/user/login?redirect=/admin即可

笔者遇到的问题是无法重定位,依然是/noAuth
这里排查了原因:路由守卫里
await store.dispatch("user/getLoginUser")之后,代码仍然用的是dispatch 之前的旧loginUser常量,导致它被当成“已登录但无权限”(或拿到空用户去做权限判断),于是走了next("/noAuth")把
src/access/index.ts改成:dispatch 之后重新从store.state.user.loginUser取一次最新值,再做登录/权限判断,这样访问http://localhost:8080/admin未登录时会正确跳转到:/user/login?redirect=/admin也就是access/index.ts的代码修改成:
▼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) => { let loginUser = store.state.user.loginUser; // 如果没登陆,自动拉取登录态(避免刷新后 vuex 清空) if (!loginUser || !loginUser.userRole) { await store.dispatch("user/getLoginUser"); loginUser = store.state.user.loginUser; } // 如果没有配置 access,则默认该页面是公共的(NOT_LOGIN) const needAccess = (to.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN; // 要跳转的页面需要登录 if (needAccess !== ACCESS_ENUM.NOT_LOGIN) { // 如果没登录,跳转登录页 if (!loginUser || !loginUser.userRole) { next(`/user/login?redirect=${to.fullPath}`); return; } // 已登录但没有权限 if (!checkAccess(loginUser, needAccess as string)) { next("/noAuth"); return; } } next(); });在vuex中user.ts文件中的state删去userRole这一项
支持多套布局
- 在routes路由文件中添加一套用户路由
▼ts复制代码{ path: "/user", name: "用户", component: () => import("../layouts/UserLayout.vue"), children: [ { path: "/user/login", name: "登录", component: () => import("../views/user/UserLoginView.vue"), }, { path: "/user/register", name: "注册", component: () => import("../views/user/UserRegisterView.vue"), }, ], meta: { hideInMenu: true, },
-
新建UserLayout、UserLoginView、UserRegisterView页面,并在routes中引入
-
在app.vue根页面文件,根据路由区分多套布局
▼ts复制代码<template v-if="route.path.startsWith('/user')"> <router-view /> </template> <template v-else> <BasicLayout /> </template>
登录页面开发
登陆页面的核心是表单,只需要从组件库中找到表单组件,修改表单字段名称和后端完全匹配就足够了。
看个人审美吧
▼html复制代码<template> <div id="userLoginView"> <h2 style="margin-bottom: 16px">用户登录</h2> <a-form style="max-width: 480px; margin: 0 auto" label-align="left" auto-label-width :model="form" @submit="handleSubmit" > <a-form-item field="userAccount" label="账号"> <a-input v-model="form.userAccount" placeholder="请输入账号" /> </a-form-item> <a-form-item field="userPassword" tooltip="密码不少于 8 位" label="密码"> <a-input-password v-model="form.userPassword" placeholder="请输入密码" /> </a-form-item> <a-form-item> <a-button type="primary" html-type="submit" style="width: 120px"> 登录 </a-button> </a-form-item> </a-form> </div> </template> <script setup lang="ts"> import { reactive } from "vue"; import { UserControllerService, UserLoginRequest } from "../../../generated"; import message from "@arco-design/web-vue/es/message"; import { useRouter } from "vue-router"; import { useStore } from "vuex"; /** * 表单信息 */ const form = reactive({ userAccount: "", userPassword: "", } as UserLoginRequest); const router = useRouter(); const store = useStore(); /** * 提交表单 * @param data */ const handleSubmit = async () => { const res = await UserControllerService.userLoginUsingPost(form); // 登录成功,跳转到主页 if (res.code === 0) { await store.dispatch("user/getLoginUser"); router.push({ path: "/", replace: true, }); } else { message.error("登陆失败," + res.message); } }; </script>
在OpenApi.ts中修改配置
▼ts复制代码export const OpenAPI: OpenAPIConfig = { BASE: "http://localhost:8121", VERSION: "1.0", //改为true WITH_CREDENTIALS: true, CREDENTIALS: "include", TOKEN: undefined, USERNAME: undefined, PASSWORD: undefined, HEADERS: undefined, ENCODE_PATH: undefined, };
不然请求头不带cookie
修改库里的userName,再次登录,会发现用户名同步改变

注册页面开发
UserRegisterView.vue
▼vue复制代码<template> <div id="userRegisterView"> <h2 style="margin-bottom: 16px">用户注册</h2> <a-form style="max-width: 480px; margin: 0 auto" label-align="left" auto-label-width :model="form" @submit="handleSubmit" > <a-form-item field="userAccount" label="账号"> <a-input v-model="form.userAccount" placeholder="请输入账号" /> </a-form-item> <a-form-item field="userPassword" tooltip="密码不少于 8 位" label="密码"> <a-input-password v-model="form.userPassword" placeholder="请输入密码" /> </a-form-item> <a-form-item field="checkPassword" tooltip="密码不少于 8 位" label="确认密码" > <a-input-password v-model="form.checkPassword" placeholder="请与密码一致" /> </a-form-item> <a-form-item> <a-button type="primary" html-type="submit" style="width: 120px"> 登录 </a-button> </a-form-item> </a-form> </div> </template> <script setup lang="ts"> import { reactive } from "vue"; import { UserControllerService, UserRegisterRequest } from "../../../generated"; import message from "@arco-design/web-vue/es/message"; import { useRouter } from "vue-router"; /** * 表单信息 */ const form = reactive({ userAccount: "", userPassword: "", checkPassword: "", } as UserRegisterRequest); const router = useRouter(); /** * 提交表单 * @param data */ const handleSubmit = async () => { if (form.userPassword !== form.checkPassword) { message.error("两次输入的密码不一致"); return; } const res = await UserControllerService.userRegisterUsingPost(form); // 注册成功,跳转到主页 if (res.code === 0) { router.push({ path: "/user/login", replace: true, }); } else { message.error("注册失败," + res.message); } }; </script>


