本文最后更新于:1 个月前
                  
                
              
            
            
              
                
                技术选型 架构设计 编码开发 前端 很好啊,前后端项目模板跑起来了:
2024年5月5日
 
框架构建 安装 | Vue CLI (vuejs.org) 
Arco Design Vue 
1 2 3 4 5 6 7 8 9 10 11 12 13 <template>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template>
1 2 3 4 5 6 7 8 9 10 11 12 <template>
路由跳转 router 下的 index.ts:
1 2 3 4 5 6 7 8 9 10 import  { createRouter, createWebHistory } from  "vue-router" ;import  { routes } from  "@/router/routes" ;const  router = createRouter ({history : createWebHistory (process.env .BASE_URL ),export  default  router;
router 下的 router.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import  { RouteRecordRaw  } from  "vue-router" ;import  HomeView  from  "@/views/HomeView.vue" ;export  const  routes : Array <RouteRecordRaw > = [path : "/" ,name : "首页" ,component : HomeView ,path : "/about" ,name : "关于" ,component : () => import ( "../views/AboutView.vue" ),
动态导航栏:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <a-menu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import  { ref } from  "vue" ;import  { useRouter } from  "vue-router" ;import  { routes } from  "@/router/routes" ;const  router = useRouter ();const  selectedKeys = ref (["/" ]);afterEach ((to, from , failure ) =>  {value  = [to.path ];const  doMenuClick  = (key: string  ) => {push ({path : key,
效果如下: 
全局状态管理 vuex/examples/classic/shopping-cart/store/index.js at main · vuejs/vuex (github.com) 
开始 | Vuex (vuejs.org) 
store 下的 user.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import  { StoreOptions  } from  "vuex" ;export  default  {namespaced : true ,state : () =>  ({loginUser : {userName : "未登录" ,actions : {async  getLoginUser ({ commit, state }, payload ) {commit ("updateUser" , { userName : "memory"  });mutations : {updateUser (state, payload ) {loginUser  = payload;as  StoreOptions <any >;
index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 import  { createStore } from  "vuex" ;import  user from  "@/store/user" ;export  default  createStore ({state : {},getters : {},mutations : {},actions : {},modules : {
main.ts
1 2 3 4 import  store from  "./store" ;createApp (App ).use (ArcoVue ).use (store).use (router).mount ("#app" );
store 原理(执行流程) 
1 2 3 4 5 6 setTimeout (() =>  {dispatch ("user/getLoginUser" , {userName : "回忆如初" ,3000 );
这样引入 store,执行 dispatch方法,根据 actions 下的路径,提供参数,执行 mutations 下的方法。
1 2 3 4 5 actions : {async  getLoginUser ({ commit, state }, payload ) {commit ("updateUser" , payload);
1 2 3 4 5 mutations : {updateUser (state, payload ) {loginUser  = payload;
mutations 改变了 state 的值,根据传入的参数改变了。
我们尝试在页面获取 store 值,并展示:
1 2 3 4 5 <a-col flex="100px">
效果如下:
另外,action 这里可以写死,不接受参数:
1 2 3 4 5 actions : {async  getLoginUser ({ commit, state }, payload ) {commit ("updateUser" , { userName : "memory"  });
全局权限管理 关键在于这段逻辑,App.vue 下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import  router from  "@/router" ;import  store from  "@/store" ;beforeEach ((to, from , next ) =>  {if  (to.meta ?.access  === "canAdmin" ) {if  (store.state .user ?.loginUser ?.role  !== "admin" ) {next ("/noAuth" );return ;next ();
设置路由访问权限,必须为管理员可见:
1 2 3 4 5 6 7 8 {path : "/auth" ,name : "管理员可见" ,component : AuthView ,meta : {access : "canAdmin" ,
默认用户权限,测试用:
1 2 3 4 5 6 state : () =>  ({loginUser : {userName : "未登录" ,role : "admin" ,
我们发现能正常跳转页面,但这样做:
1 2 3 4 5 6 7 setTimeout (() =>  {dispatch ("user/getLoginUser" , {userName : "回忆如初" ,role : "noAdmin" ,3000 );
三秒过后,该用户不是管理员权限,访问一个管理员可见的页面,直接重定向:
隐藏菜单 构造通用的导航栏组件,根据配置控制菜单栏的显隐
1 2 3 4 5 6 7 8 {path : "/hide" ,name : "隐藏页面" ,component : noAuthView,meta : {hideInMenu : true ,
过滤,仅展示显示在菜单上的路由数组
1 2 3 <a-menu-item v-for="item in visibleRoutes" :key="item.path">
1 2 3 4 5 6 import  { routes } from  "@/router/routes" ;const  visibleRoutes = routes.filter ((item, index ) =>  {return  !item.meta ?.hideInmenu ;
除了 根据配置权限隐藏菜单 ,还需要根据用户权限,只有具有相关权限的用户,才能看到该菜单。
检测用户权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import  ACCESS_ENUM  from  "@/access/accessEnum" ;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;
使用计算属性,使得用户信息发生变更时,触发菜单栏的重新渲染,。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ;
全局项目入口 1 2 3 4 5 6 7 const  doInit  = (console .log ("项目全局入口" );onMounted (() =>  {doInit ();
库表设计 题目表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 create  table  if not  exists  questionbigint  auto_increment comment 'id'  primary  key,varchar (512 )                       null  comment '标题' ,null  comment '内容' ,varchar (1024 )                      null  comment '标签列表(json 数组)' ,null  comment '题目答案' ,int       default  0                  not  null  comment '题目提交数' ,int       default  0                  not  null  comment '题目通过数' ,null  comment '判题用例(json 数组)' ,null  comment '判题配置(json 对象)' ,int       default  0                  not  null  comment '点赞数' ,int       default  0                  not  null  comment '收藏数' ,bigint                              not  null  comment '创建用户 id' ,default  CURRENT_TIMESTAMP  not  null  comment '创建时间' ,default  CURRENT_TIMESTAMP  not  null  on  update  CURRENT_TIMESTAMP  comment '更新时间' ,default  0                  not  null  comment '是否删除' ,'题目'  collate  =  utf8mb4_unicode_ci;
数据库字段存 json 字符串,便于扩展:判题用例 、判题配置 、判题信息 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data public  class  JudgeConfig  {private  Long timeLimit;private  Long memoryLimit;private  Long stackLimit;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Data public  class  JudgeCase  {private  String input;private  String output;
题目提交表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 create  table  if not  exists  question_submitbigint  auto_increment comment 'id'  primary  key,language    varchar (128 )                       not  null  comment '编程语言' ,not  null  comment '用户代码' ,null  comment '判题信息(json 对象)' ,int       default  0                  not  null  comment '判题状态(0 - 待判题、1 - 判题中、2 - 成功、3 - 失败)' ,bigint                              not  null  comment '题目 id' ,bigint                              not  null  comment '创建用户 id' ,default  CURRENT_TIMESTAMP  not  null  comment '创建时间' ,default  CURRENT_TIMESTAMP  not  null  on  update  CURRENT_TIMESTAMP  comment '更新时间' ,default  0                  not  null  comment '是否删除' ,'题目提交' ;
枚举类构建 构建枚举类:判题信息 、判题状态 、编程语言 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public  enum  JudgeInfoMessageEnum  {"成功" , "Accepted" ),"答案错误" , "Wrong Answer" ),"Compile Error" , "编译错误" ),"" , "内存溢出" ),"Time Limit Exceeded" , "超时" ),"Presentation Error" , "展示错误" ),"Waiting" , "等待中" ),"Output Limit Exceeded" , "输出溢出" ),"Dangerous Operation" , "危险操作" ),"Runtime Error" , "运行错误" ),"System Error" , "系统错误" );
1 2 3 4 5 6 7 8 public  enum  QuestionSubmitLanguageEnum  {"java" , "java" ),"cpp" , "cpp" ),"go" , "go" );
1 2 3 4 5 6 7 8 9 10 public  enum  QuestionSubmitStatusEnum  {"等待中" , 0 ),"判题中" , 1 ),"成功" , 2 ),"失败" , 3 );
校验题目是否合法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Override public  void  validQuestion (Question question, boolean  add)  {if  (question == null ) {throw  new  BusinessException (ErrorCode.PARAMS_ERROR);String  title  =  question.getTitle();String  content  =  question.getContent();String  tags  =  question.getTags();String  answer  =  question.getAnswer();String  judgeCase  =  question.getJudgeCase();String  judgeConfig  =  question.getJudgeConfig();if  (add) {if  (StringUtils.isNotBlank(title) && title.length() > 80 ) {throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "标题过长" );if  (StringUtils.isNotBlank(content) && content.length() > 8192 ) {throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "内容过长" );if  (StringUtils.isNotBlank(answer) && answer.length() > 8192 ) {throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "答案过长" );if  (StringUtils.isNotBlank(judgeCase) && judgeCase.length() > 8192 ) {throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "判题用例过长" );if  (StringUtils.isNotBlank(judgeConfig) && judgeConfig.length() > 8192 ) {throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "判题配置过长" );
QuestionVO 封装了脱敏 Question 的题目答案 answer 和判题用例 judgeCase 字段,以下是包装类 QuestionVO 和原类Question 的字段属性对比:
1 2 3 4 5 6 7 8 9 10 private  List<String> tags;private  JudgeConfig judgeConfig;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private  String tags;private  String judgeCase;private  String judgeConfig;
QuestionVO 内置了包装类与原对象互相转换的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public  static  QuestionVO objToVo (Question question)  {if  (question == null ) {return  null ;QuestionVO  questionVO  =  new  QuestionVO ();String  judgeConfigStr  =  question.getJudgeConfig();return  questionVO;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public  static  Question voToObj (QuestionVO questionVO)  {if  (questionVO == null ) {return  null ;Question  question  =  new  Question ();if  (tagList != null ) {JudgeConfig  voJudgeConfig  =  questionVO.getJudgeConfig();if  (voJudgeConfig != null ) {return  question;
封装查询请求参数 QuestionQueryRequest,封装获取查询包装类,动态判断用户根据哪些字段查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 "title" , title);"content" , content);"answer" , answer);if  (CollectionUtils.isNotEmpty(tags)) {for  (String tag : tags) {"tags" , "\""  + tag + "\"" );"id" , id);"userId" , userId);"isDelete" , false );return  queryWrapper;
阶段性总结 通用的的业务校验流程:
添加新题目,进行类成员变量字段 -> json 的转换,封装了 validQuestion 方法判断参数是否合法。为新题目设置 userId 等字段属性,执行 SQL 语句插入该新纪录并校验是否执行成功。
删除题目,封装 DeleteRequest 删除请求参数,携带删除题目 id,首先根据题目 id 查询数据库,判断该题目是否存在;根据查询得到的题目记录的 userId,判断执行该操作的用户是否为出题用户或者管理员,否则不予执行删除操作。执行 SQL 语句删除记录并校验是否执行成功。
更新题目(管理员),进行类成员变量字段 -> json 的转换,封装了 validQuestion 方法判断参数是否合法。在更新题目记录之前,根据题目 id 检查该题目是否存在,再执行 SQL 语句更新该记录并校验是否执行成功。
编辑题目(普通用户),在管理员更新题目信息中,添加了用户信息校验,只有出题用户和管理员可编辑:
 
1 2 3 4 5 6 7 8 9 User  loginUser  =  userService.getLoginUser(request);long  id  =  questionEditRequest.getId();Question  oldQuestion  =  questionService.getById(id);null , ErrorCode.NOT_FOUND_ERROR);if  (!oldQuestion.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {throw  new  BusinessException (ErrorCode.NO_AUTH_ERROR);
根据 id 获取题目,校验用户是否为本人或管理员,否则拿不到题目信息 
 
1 2 3 4 @Override public  boolean  isAdmin (User user)  {return  user != null  && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
根据 id 获取题目信息(脱敏),并关联查询用户信息 
 
1 return  ResultUtils.success(questionService.getQuestionVO(question, request));
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public  QuestionVO getQuestionVO (Question question, HttpServletRequest request)  {QuestionVO  questionVO  =  QuestionVO.objToVo(question);Long  userId  =  question.getUserId();User  user  =  null ;if  (userId != null  && userId > 0 ) {UserVO  userVO  =  userService.getUserVO(user);return  questionVO;
封装查询请求参数类 QuestionQueryRequest,封装获取查询包装类方法 getQueryWrapper,批量分页获取题目列表(脱敏),并关联查询用户信息: 
 
1 2 3 Page<Question> questionPage = questionService.page(new  Page <>(current, size),return  ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 QuestionVO  questionVO  =  QuestionVO.objToVo(question);Long  userId  =  question.getUserId();User  user  =  null ;if  (userIdUserListMap.containsKey(userId)) {0 );return  questionVO;
获取用户创建的题目列表,基本同上,查询请求参数 QuestionQueryRequest 携带该用户信息: 
 
1 2 User  loginUser  =  userService.getLoginUser(request);
简单的获取所有题目信息列表,仅管理员可用 
用户提交题目,封装提交题目请求参数 QuestionSubmitAddRequest,判断编程语言是否合法、题目是否存在,直接存入题目提交信息,再异步执行判题服务: 
 
1 2 3 4 5 6 7 8 9 10 11 12 QuestionSubmit  questionSubmit  =  new  QuestionSubmit ();"{}" );boolean  save  =  this .save(questionSubmit);if  (!save){throw  new  BusinessException (ErrorCode.SYSTEM_ERROR, "数据插入失败" );
1 2 3 4 5 Long  questionSubmitId  =  questionSubmit.getId();
封装提交题目查询参数 QuestionSubmitQueryRequest,分页查询已提交的题目,只有提交者本人和管理员才可以看到提交代码 
 
1 2 3 4 5 6 long  userId  =  loginUser.getId();if  (userId != questionSubmit.getUserId() && !userService.isAdmin(loginUser)) {null );
1 2 3 4 
2024年5月4日
 
核心功能:提交代码,权限校验;代码沙箱,判题服务;判题规则,结果比对验证;任务调度
核心业务流程:时序图
报错解决指南 前端生成请求接口后报错 
这是因为 OpenAPI 生成的请求接口是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public  static  getLoginUserUsingGet (): CancelablePromise <BaseResponse _LoginUserVO_> {return  __request (OpenAPI , {method : 'GET' ,url : '/api/user/get/login' ,errors : {401 : `Unauthorized` ,403 : `Forbidden` ,404 : `Not Found` ,
修改 OpenAPIConfig 即可:
1 2 3 4 5 6 7 8 9 10 11 export  const  OpenAPI : OpenAPIConfig  = {BASE : 'http://localhost:8101/' ,VERSION : '1.0' ,WITH_CREDENTIALS : false ,CREDENTIALS : 'include' ,TOKEN : undefined ,USERNAME : undefined ,PASSWORD : undefined ,HEADERS : undefined ,ENCODE_PATH : undefined ,