1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > 若依Ruoyi-Vue学习笔记

若依Ruoyi-Vue学习笔记

时间:2023-08-03 22:25:01

相关推荐

若依Ruoyi-Vue学习笔记

文章目录

0. 前言目标功能的基本流程环境要求1. 运行Ruoyi1.1 下载1.2 配置数据库1.3 配置Redis1.4 日志1.5 启动后端1.6 启动前端2. 登陆功能2.1 验证码基本思路前端实现请求的封装反向代理后端实现2.2 登陆前端实现后端实现控制层业务层2.3 获取用户角色和权限前端实现后端实现2.4 获取动态菜单路由3. 数据加载3.1 首页数据加载3.2 用户管理(PageHelper分页)3.3 部分树状图4. 用户增删查改5. 异步任务管理器6. 代码自动生成

0. 前言

Ruoyi前后端分离版:SpringBoot + Vue

官网:https://ruoyi.vip

参考视频:【开源项目学习】若依前后端分离版,通俗易懂,快速上手

目标

学习开源项目的目标:

用,减少自己的工作量学习优秀开源项目的底层编程思想、设计思路,提升自己的编程能力

使用、学习开源项目的流程:

下载并运行看懂业务流程进行二次开发

功能的基本流程

加载Vue页面请求后端

环境要求

JDK1.8+MySQL8+RedisMavenVue

1. 运行Ruoyi

1.1 下载

从Gitee官网复制url在IDEA中打开(后端),注意前端Vue项目ruoyi-ui需要额外使用一个idea打开。

1.2 配置数据库

表:直接执行/sql下的两个sql文件,在本地创建表

数据源:修改配置文件中数据源配置

1.3 配置Redis

使用Docker启动Redis

修改Redis配置

1.4 日志

需要在ruoyi-admin/src/main/resources/logback.xml中修改日志存放位置:

1.5 启动后端

启动admin中的springboot启动类

(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ .-------. ______ | _ _ \\ \ / / | ( ' ) | \ _. / ' |(_ o _) / _( )_ .' | (_,_).' __ ___(_ o _)'| |\ \ | || |(_,_)' | | \ `' /| `-' / | | \ / \/ ''-' `'-' `-..-'

1.6 启动前端

根据ruoyi-ui项目中的README.md文件进行配置安装依赖,然后启动

# 克隆项目git clone /y_project/RuoYi-Vue# 进入项目目录cd ruoyi-ui# 安装依赖npm install# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题npm install --registry=# 启动服务npm run dev

2. 登陆功能

2.1 验证码

基本思路

简而言之:前端让后端出一道算术题,后端把题目告诉前端,并把答案放入后端的Redis中,前端计算完结果后去后端的Redis中比对答案。

每次需要登录时,会在后端自动生成验证码,如“1+1=?@2”,验证码“1+1=?”会被转成图片传到前端登陆页面,答案“2”会被存储进Redis中(@是用于分割问题和答案的标记符号)。当前端输入完账号密码和验证码后,系统会拿验证码“2”和Redis中的答案“2”进行比较,成功则再验证账号密码。Redis中的key值会被传到前端,如果有多人登陆,每个客户端可根据自己的key值查询redis中的value值(答案)。

如果通过Docker启动的Redis,可通过交互模式进入Redis容器,然后进入Redis客户端查看验证码答案。

# 通过交互模式进入Redis容器docker exec -it 6ce bash# 进入Redisredis-cli# 查看所有keykeys *# 查看验证码答案127.0.0.1:6379> get captcha_codes:aecd3ba23ab94614b2a7840e2625107c"\"35\""

前端实现

请求的封装

验证码的代码实现在ruoyi-ui/src/views/login.vue中。

基本流程概括:打开登陆页面,向后端请求验证码图片和一个uuid(Redis的key)

前端Vue和后端Springboot交互时通常使用axios(ajax),而这里看不到axios的调用是因为进行了多次封装。如果再深入追溯,可进入getCodeImg()方法,然后发现还有封装:

进入login.js找到getCodeImg(),发现了ajax的基本写法:url、请求类型、超时时间,注意至此还是在request封装中,依旧没有看到axios。

// 获取验证码export function getCodeImg() {return request({url: '/captchaImage',headers: {isToken: false},method: 'get',timeout: 20000})}

接着进入ruoyi-ui/src/utils/request.js,找到axios:

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'// 创建axios实例const service = axios.create({// axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: process.env.VUE_APP_BASE_API,// 超时timeout: 10000})

其中VUE_APP_BASE_API定义在了配置文件.env.development中:

# 若依管理系统/开发环境VUE_APP_BASE_API = '/dev-api'

这样任何请求都添加前缀’/dev-api’。

统一前缀是为了区分开发环境和生产环境。

反向代理

此时注意一个点:Vue获取图片时,前端项目的端口是1024;后端项目的端口是8080。理论上对验证码的信息的请求应该是对后端发起请求,但是url中实际还是对前端1024端口请求。

原因:反向代理,url在前端请求前端,进行代理,映射到后端,如此操作是为了解决跨域问题。跨域问题在后端的解决方式是Springboot添加一个配置类;前端的解决方式是反向代理。跨域问题在前端或者后端解决都可。

反向代理的配置在ruoyi-ui/vue.config.js中:

// webpack-dev-server 相关配置devServer: {host: '0.0.0.0',port: port,open: true,proxy: {// detail: /config/#devserver-proxy[process.env.VUE_APP_BASE_API]: {target: `http://localhost:8080`,changeOrigin: true,pathRewrite: {['^' + process.env.VUE_APP_BASE_API]: ''}}},disableHostCheck: true},

上面的pathRewrite里,会把前面的请求前缀替换为空,即’',再映射到后端的端口,即target。如此请求url从http://localhost:1024/dev-api/captchaImage变成了http://localhost:8080/captchaImage

后端实现

首先先定位到验证码功能的控制器,使用全局搜索(ctrl+shift+F)对admin项目搜索captchaImage,找到CaptchaController

/*** 生成验证码*/@GetMapping("/captchaImage")public AjaxResult getCode(HttpServletResponse response) throws IOException{// 最终需要返回给前端的ajax结果(封装版)AjaxResult ajax = AjaxResult.success();// 检查是否开启验证码boolean captchaOnOff = configService.selectCaptchaOnOff();ajax.put("captchaOnOff", captchaOnOff);if (!captchaOnOff){return ajax;}// 保存验证码信息String uuid = IdUtils.simpleUUID();// 拼接一个key,用于放入redis,如“captcha_codes:aecd3ba23ab94614b2a7840e2625107c”String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;String capStr = null, code = null;BufferedImage image = null;// 生成验证码String captchaType = RuoYiConfig.getCaptchaType();if ("math".equals(captchaType)){String capText = captchaProducerMath.createText();capStr = capText.substring(0, capText.lastIndexOf("@"));code = capText.substring(capText.lastIndexOf("@") + 1);image = captchaProducerMath.createImage(capStr);}else if ("char".equals(captchaType)){capStr = code = captchaProducer.createText();image = captchaProducer.createImage(capStr);}// 将key和value存入redis,并设置缓存时间redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);// 转换流信息写出FastByteArrayOutputStream os = new FastByteArrayOutputStream();try{ImageIO.write(image, "jpg", os);}catch (IOException e){return AjaxResult.error(e.getMessage());}ajax.put("uuid", uuid);ajax.put("img", Base64.encode(os.toByteArray()));return ajax;}

这个AjaxResult就是后端给前端返回的数据对象,通常称为VO或ResultVO或R(前端与后端交互时的统一数据模型)。

2.2 登陆

前端实现

登陆的前端实现和验证码一样,依旧使用了前端反向代理。

登陆功能的前端实现主要是由handleLogin()方法实现的。

handleLogin() {this.$refs.loginForm.validate(valid => {if (valid) {this.loading = true;if (this.loginForm.rememberMe) {Cookies.set("username", this.loginForm.username, {expires: 30 });Cookies.set("password", encrypt(this.loginForm.password), {expires: 30 });Cookies.set('rememberMe', this.loginForm.rememberMe, {expires: 30 });} else {Cookies.remove("username");Cookies.remove("password");Cookies.remove('rememberMe');}this.$store.dispatch("Login", this.loginForm).then(() => {this.$router.push({path: this.redirect || "/" }).catch(()=>{});}).catch(() => {this.loading = false;if (this.captchaOnOff) {this.getCode();}});}});}

登陆使用的是表单,有“记住密码”功能,如果勾选,则将用户名密码和记住我选项存入cookie中,否则移除。其中登陆是由Login实现,它是一个action;获取到用户信息后构建并返回一个Promise,它是es6提供的异步处理的对象。

actions: {// 登录Login({commit }, userInfo) {const username = userInfo.username.trim()const password = userInfo.passwordconst code = userInfo.codeconst uuid = userInfo.uuidreturn new Promise((resolve, reject) => {login(username, password, code, uuid).then(res => {setToken(res.token)commit('SET_TOKEN', res.token)resolve()}).catch(error => {reject(error)})})},

后端校验成功后,将后端返回的token保存起来(令牌是加密后的用户信息)其中login方法又是封装定义好的方法…,最终还是ajax

// 登录方法export function login(username, password, code, uuid) {const data = {username,password,code,uuid}return request({url: '/login',headers: {isToken: false},method: 'post',data: data})}

后端实现

控制层

登陆的后端实现在ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java,逻辑很简单:生成需要返回的AjaxResult对象,调用Service层的login方法(需要username,password和验证码),生成令牌,放入ajax返回。

关于密码加密:密码是不会在前端或者后端中加密(传输时https协议会进行加密解密;http不会),而是在数据库中(持久层)进行加密存储。

/*** 登录方法* * @param loginBody 登录信息* @return 结果*/@PostMapping("/login")public AjaxResult login(@RequestBody LoginBody loginBody){AjaxResult ajax = AjaxResult.success();// 生成令牌String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),loginBody.getUuid());ajax.put(Constants.TOKEN, token);return ajax;}

业务层

大体流程与验证码生成类似:首先验证验证码,然后验证账号和密码。

/*** 登录验证* * @param username 用户名* @param password 密码* @param code 验证码* @param uuid 唯一标识* @return 结果*/public String login(String username, String password, String code, String uuid){boolean captchaOnOff = configService.selectCaptchaOnOff();// 验证码开关if (captchaOnOff){validateCaptcha(username, code, uuid);}// 用户验证Authentication authentication = null;try{// 该方法会去调用UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));}catch (Exception e){if (e instanceof BadCredentialsException){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}else{// 其他任何异常 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}// 记录用户登陆日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));LoginUser loginUser = (LoginUser) authentication.getPrincipal();// 记录用户最近登陆信息recordLoginInfo(loginUser.getUserId());// 生成tokenreturn tokenService.createToken(loginUser);}

前后端不分离版使用的安全框架时Shiro;而分离版使用的是Spring Security。

注意最后的recordLoginInfo方法:记录用户最近的登陆信息。后台的数据表会记录用户登陆的ip和时间。

登陆的日志信息会存入sys_logininfor

用户最近登陆信息则存入(更新)sys_user

/*** 校验验证码* * @param username 用户名* @param code 验证码* @param uuid 唯一标识* @return 结果*/public void validateCaptcha(String username, String code, String uuid){// 拼接Redis的key值String verifyKey = Constants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");// 去Redis中验证key值是否存在(可能因为长时间未使用而过期)String captcha = redisCache.getCacheObject(verifyKey);// key已经使用过,及时删除redisCache.deleteObject(verifyKey);if (captcha == null){// 如果key不存在,则“异步记录日志”AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));// 抛出异常throw new CaptchaExpireException();}if (!code.equalsIgnoreCase(captcha)){// 如果code(value)验证不正确,同样异步记录日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));throw new CaptchaException();}}

代码亮点/难点就是使用了异步任务管理器(后面会说),当redis的key不存在时,异步记录日志。这么做的好处就是用到了异步分离,可以避免线程阻塞,让主线程运行快一些(但日志使用异步的意义不大)。关于抛出异常:大型项目和复杂业务直接return的情况少(理想状况才是直接return),通常都是自定义异常处理给系统抓取,前端展示异常信息。而如果只用return(包括错误情况),那么上层的调用方还需要对return的值作判断处理;而使用异常当前调用直接结束,相当于短路处理。

2.3 获取用户角色和权限

前端实现

通过查看浏览器的请求可以发现每次登陆除了login还有getInfogetRouters

这两个请求可以在ruoyi-ui/src/permission.js中找到:

router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) {to.meta.title && store.dispatch('settings/setTitle', to.meta.title)/* has token*/if (to.path === '/login') {next({path: '/' })NProgress.done()} else {if (store.getters.roles.length === 0) {isRelogin.show = true// 判断当前用户是否已拉取完user_info信息store.dispatch('GetInfo').then(() => {isRelogin.show = falsestore.dispatch('GenerateRoutes').then(accessRoutes => {// 根据roles权限生成可访问的路由表router.addRoutes(accessRoutes) // 动态添加可访问路由表next({...to, replace: true }) // hack方法 确保addRoutes已完成})}).catch(err => {store.dispatch('LogOut').then(() => {Message.error(err)next({path: '/' })})})} else {next()}}...

上面这段代码的意思是:前端每个页面进行跳转时,都会进入到这个方法(获取信息、路由…),是Vue router的请求拦截器(全局路由管理器、路由前置守卫)

进一步查看GetInfo方法:

// 获取用户信息GetInfo({commit, state }) {return new Promise((resolve, reject) => {getInfo().then(res => {const user = res.userconst avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;if (res.roles && res.roles.length > 0) {// 验证返回的roles是否是一个非空数组commit('SET_ROLES', res.roles)commit('SET_PERMISSIONS', res.permissions)} else {commit('SET_ROLES', ['ROLE_DEFAULT'])}commit('SET_NAME', user.userName)commit('SET_AVATAR', avatar)resolve(res)}).catch(error => {reject(error)})})},

发现里面还是封装了一个Promise对象,进行异步调用。

commit是对roles和permissions进行全局存储,这样之后在页面内就可以直接使用,而不用每次都进行查询。

同时里面又封装了getInfo方法:

// 获取用户详细信息export function getInfo() {return request({url: '/getInfo',method: 'get'})}

后端实现

这里集成了Spring Security,可以直接获取当前登陆的user。

/*** 获取用户信息* * @return 用户信息*/@GetMapping("getInfo")public AjaxResult getInfo(){SysUser user = SecurityUtils.getLoginUser().getUser();// 角色集合Set<String> roles = permissionService.getRolePermission(user);// 权限集合Set<String> permissions = permissionService.getMenuPermission(user);AjaxResult ajax = AjaxResult.success();ajax.put("user", user);ajax.put("roles", roles);ajax.put("permissions", permissions);return ajax;}

测试获取角色和权限

查看数据库中表关系可以发现:

每个用户有一个user_id每个角色对应一个role_id第三张中间表维护user_id和role_id的对应关系,多对多

2.4 获取动态菜单路由

前面分析了GetInfo,下面分析GenerateRoutes,看看Ruoyi是怎么动态获取菜单路由的。

router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) {to.meta.title && store.dispatch('settings/setTitle', to.meta.title)/* has token*/if (to.path === '/login') {next({path: '/' })NProgress.done()} else {if (store.getters.roles.length === 0) {isRelogin.show = true// 判断当前用户是否已拉取完user_info信息store.dispatch('GetInfo').then(() => {isRelogin.show = falsestore.dispatch('GenerateRoutes').then(accessRoutes => {// 根据roles权限生成可访问的路由表router.addRoutes(accessRoutes) // 动态添加可访问路由表next({...to, replace: true }) // hack方法 确保addRoutes已完成})}).catch(err => {store.dispatch('LogOut').then(() => {Message.error(err)next({path: '/' })})})} else {next()}}...

进入GenerateRoutes方法,

actions: {// 生成路由GenerateRoutes({commit }) {return new Promise(resolve => {// 向后端请求路由数据getRouters().then(res => {const sdata = JSON.parse(JSON.stringify(res.data))const rdata = JSON.parse(JSON.stringify(res.data))const sidebarRoutes = filterAsyncRouter(sdata)const rewriteRoutes = filterAsyncRouter(rdata, false, true)const asyncRoutes = filterDynamicRoutes(dynamicRoutes);rewriteRoutes.push({path: '*', redirect: '/404', hidden: true })router.addRoutes(asyncRoutes);commit('SET_ROUTES', rewriteRoutes)commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))commit('SET_DEFAULT_ROUTES', sidebarRoutes)commit('SET_TOPBAR_ROUTES', sidebarRoutes)resolve(rewriteRoutes)})})}}

// 获取路由export const getRouters = () => {return request({url: '/getRouters',method: 'get'})}

后端Controller逻辑和前面功能一样,依旧使用SpringSecurity获取用户(用户id),然后通过菜单service获取权限对应的菜单,最终通过Ajax对象返回:

/*** 获取路由信息* * @return 路由信息*/@GetMapping("getRouters")public AjaxResult getRouters(){Long userId = SecurityUtils.getUserId();List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);return AjaxResult.success(menuService.buildMenus(menus));}

service:

/*** 根据用户ID查询菜单* * @param userId 用户名称* @return 菜单列表*/@Overridepublic List<SysMenu> selectMenuTreeByUserId(Long userId){List<SysMenu> menus = null;if (SecurityUtils.isAdmin(userId)){menus = menuMapper.selectMenuTreeAll();}else{menus = menuMapper.selectMenuTreeByUserId(userId);}return getChildPerms(menus, 0);}

mapper:

<select id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult">select distinct m.menu_id, m.parent_id, m.menu_name, m.path, ponent, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_timefrom sys_menu mleft join sys_role_menu rm on m.menu_id = rm.menu_idleft join sys_user_role ur on rm.role_id = ur.role_idleft join sys_role ro on ur.role_id = ro.role_idleft join sys_user u on ur.user_id = u.user_idwhere u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0 AND ro.status = 0order by m.parent_id, m.order_num</select>

以上就是以树形嵌套结构查询出的结果

注意mapper层并未实现嵌套关系,而是在Java层面实现(使用递归实现;但是个人感觉使用Map存储是最快的;当然如果把这个工作交给前端来实现也是完全可以的),这里的父子菜单嵌套非常类似我之前写的博客的父子评论嵌套设计(树形结构):博客-评论系统数据库设计及实现

/*** 根据父节点的ID获取所有子节点* * @param list 分类表* @param parentId 传入的父节点ID* @return String*/public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId){List<SysMenu> returnList = new ArrayList<SysMenu>();for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();){SysMenu t = (SysMenu) iterator.next();// 一、根据传入的某个父节点ID,遍历该父节点的所有子节点if (t.getParentId() == parentId){recursionFn(list, t);returnList.add(t);}}return returnList;}/*** 递归列表* * @param list* @param t*/private void recursionFn(List<SysMenu> list, SysMenu t){// 得到子节点列表List<SysMenu> childList = getChildList(list, t);t.setChildren(childList);for (SysMenu tChild : childList){if (hasChild(list, tChild)){recursionFn(list, tChild);}}}

3. 数据加载

3.1 首页数据加载

前端有路由控制,登陆完成后跳转到/即index主页面:

this.$store.dispatch("Login", this.loginForm).then(() => {this.$router.push({path: this.redirect || "/" }).catch(()=>{});}).catch(() => {this.loading = false;if (this.captchaOnOff) {this.getCode();}});

关于Vue路由的控制都在ruoyi-ui/src/router/index.js:

// index主页面{path: '',component: Layout,redirect: 'index',children: [{path: 'index',component: () => import('@/views/index'),name: 'Index',meta: {title: '首页', icon: 'dashboard', affix: true }}]},

关于Vue的页面布局在ruoyi-ui/src/layout/index.vue:

<template><div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}"><div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/><sidebar v-if="!sidebar.hide" class="sidebar-container" /><div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container"><div :class="{'fixed-header':fixedHeader}"><navbar /><tags-view v-if="needTagsView" /></div><app-main /><right-panel><settings /></right-panel></div></div></template>...

3.2 用户管理(PageHelper分页)

这里省略前端的代码,直接贴后端的处理

Controller:

设置分页进行正常的查询(PageHelper中的拦截器会拦截数据库sql查询语句,并加入分页的sql语句,完成分页)把查询结果封装后返回

/*** 获取用户列表*/@PreAuthorize("@ss.hasPermi('system:user:list')")@GetMapping("/list")public TableDataInfo list(SysUser user){startPage();// 设置请求分页数据List<SysUser> list = userService.selectUserList(user);return getDataTable(list);// 封装返回结果}

/*** 响应请求分页数据*/@SuppressWarnings({"rawtypes", "unchecked" })protected TableDataInfo getDataTable(List<?> list){TableDataInfo rspData = new TableDataInfo();rspData.setCode(HttpStatus.SUCCESS);rspData.setMsg("查询成功");rspData.setRows(list);rspData.setTotal(new PageInfo(list).getTotal());return rspData;}

上面代码本质与直接使用PageHelper一样,但是作者进行了非常多的抽象与封装,包括把PageHelper封装成自己的工具类PageUtils,把返回的结果封装成TableDataInfo(类似于Map)。

/*** 设置请求分页数据*/public static void startPage(){PageDomain pageDomain = TableSupport.buildPageRequest();Integer pageNum = pageDomain.getPageNum();Integer pageSize = pageDomain.getPageSize();String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());Boolean reasonable = pageDomain.getReasonable();PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);}

下面封装非常多,这里就不一一分析,主要看我们是怎么获取到前端传递过来的pageNumpageSize的:从HttpServletRequest中获取(作者又在这里对Servlet的工具类进行封装)

public class TableSupport{/*** 当前记录起始索引*/public static final String PAGE_NUM = "pageNum";/*** 每页显示记录数*/public static final String PAGE_SIZE = "pageSize";/*** 排序列*/public static final String ORDER_BY_COLUMN = "orderByColumn";/*** 排序的方向 "desc" 或者 "asc".*/public static final String IS_ASC = "isAsc";/*** 分页参数合理化*/public static final String REASONABLE = "reasonable";/*** 封装分页对象*/public static PageDomain getPageDomain(){PageDomain pageDomain = new PageDomain();pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));return pageDomain;}public static PageDomain buildPageRequest(){return getPageDomain();}}

3.3 部分树状图

跟菜单列表是一样的逻辑

/*** 获取部门下拉树列表*/@GetMapping("/treeselect")public AjaxResult treeselect(SysDept dept){List<SysDept> depts = deptService.selectDeptList(dept);return AjaxResult.success(deptService.buildDeptTreeSelect(depts));}

这里特别注意buildTreeSelect方法,他把查询到的Tree中的SysDept通过stream转化成TreeSelect结构,即从后端数据转化成给前端显示的数据。

/*** 构建前端所需要下拉树结构* * @param depts 部门列表* @return 下拉树结构列表*/@Overridepublic List<TreeSelect> buildDeptTreeSelect(List<SysDept> depts){List<SysDept> deptTrees = buildDeptTree(depts);return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList());}

从包含很多无关信息的dept

变到了只剩部门id、部名名和children的树形结构,这样前端解析就很方便和舒服

4. 用户增删查改

增删查改比较基础这里不作详细记录。

在前端的表单里,因为逻辑简单,用户新增修改是使用同一张表单,判断具体是新增还是修改的条件是是否有userId字段。

/** 提交按钮 */submitForm: function() {this.$refs["form"].validate(valid => {if (valid) {if (this.form.userId != undefined) {updateUser(this.form).then(response => {this.$modal.msgSuccess("修改成功");this.open = false;this.getList();});} else {addUser(this.form).then(response => {this.$modal.msgSuccess("新增成功");this.open = false;this.getList();});}}});},

而后端的处理则是将修改和新增分开

5. 异步任务管理器

以登陆时账号密码不匹配为例:

AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();

通过异步任务管理器记录(登陆)日志:

AsyncManager.me()获取一个AsyncManager对象(单例模式)执行execute方法,执行任务,传入的是一个TimerTask对象

/*** 执行任务* * @param task 任务*/public void execute(TimerTask task){executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);}

TimerTask对象实现了Runnable接口,是一个任务,由一个线程Thread去执行,注意这里的executors就是一个线程池

/*** 异步操作任务调度线程池*/private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

总结:

异步任务管理器,内部定义了一个线程池,然后根据业务,创建添加日志的任务,交给线程池来执行,这样就做到了日志和业务的抽象与解耦合。

6. 代码自动生成

B站视频

Ruoyi提供了代码自动生成功能(MyBatisPlus只能自动生成后端代码,Ruoyi生成前后端+数据库的代码),我们只需要在数据库创建数据表,在管理页面即可自动生成增删改查的代码。

从Ruoyi为我们生成的代码文件结构也可以很清楚的了解到整个项目的结构(前后端+数据库)

先在mysql数据库中创建一张实体类的表test_user

use `ruoyi-vue`;create table test_user(id int primary key auto_increment,name varchar(20),password varchar(20));

在后台页面的系统工具->代码生成->导入新表

3. 预览代码,Ruoyi自动为我们生成了从数据库到后端到前端的代码

5. 编辑:基本信息、字段信息、生成信息

6. 点击生成代码,生成zip压缩包,解压复制到Ruoyi项目中,解压后可以看到三部分:main(Java后端), vue(Vue前端), sql(菜单的SQL语句)

7. CV导入代码,执行sql,重启(rebuild后端项目)前后端项目。可以看到“测试代码生成”页面,拥有基本的增删查改

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。