项目|若依Ruoyi-Vue学习笔记


文章目录

  • 0. 前言
    • 目标
    • 功能的基本流程
    • 环境要求
  • 1. 运行Ruoyi
    • 1.1 下载
    • 1.2 配置数据库
    • 1.3 配置Redis
    • 1.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
参考视频:【开源项目学习】若依前后端分离版,通俗易懂,快速上手
目标 学习开源项目的目标:
  1. 用,减少自己的工作量
  2. 学习优秀开源项目的底层编程思想、设计思路,提升自己的编程能力
使用、学习开源项目的流程:
  1. 下载并运行
  2. 看懂业务流程
  3. 进行二次开发
功能的基本流程
  1. 加载Vue页面
  2. 请求后端
环境要求
  1. JDK1.8+
  2. MySQL8+
  3. Redis
  4. Maven
  5. Vue
1. 运行Ruoyi 1.1 下载 从Gitee官网复制url在IDEA中打开(后端),注意前端Vue项目ruoyi-ui需要额外使用一个idea打开。
1.2 配置数据库 表:直接执行/sql下的两个sql文件,在本地创建表
项目|若依Ruoyi-Vue学习笔记
文章图片

数据源:修改配置文件中数据源配置
项目|若依Ruoyi-Vue学习笔记
文章图片

1.3 配置Redis 使用Docker启动Redis
修改Redis配置
项目|若依Ruoyi-Vue学习笔记
文章图片

项目|若依Ruoyi-Vue学习笔记
文章图片

1.4 日志 需要在ruoyi-admin/src/main/resources/logback.xml中修改日志存放位置:
项目|若依Ruoyi-Vue学习笔记
文章图片

1.5 启动后端 启动admin中的springboot启动类
(????)??若依启动成功?(′?`?)? .-------.______ |_ _\\\// | ( ' )|\_. /' |(_ o _) /_( )_ .' | (_,_).' _____(_ o _)' ||\ \||||(_,_)' || \ `'/|`-'/ ||\/\/ ''-'`'-'`-..-'

项目|若依Ruoyi-Vue学习笔记
文章图片

1.6 启动前端 根据ruoyi-ui项目中的README.md文件进行配置安装依赖,然后启动
# 克隆项目 git clone https://gitee.com/y_project/RuoYi-Vue# 进入项目目录 cd ruoyi-ui# 安装依赖 npm install# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 npm install --registry=https://registry.npmmirror.com# 启动服务 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客户端查看验证码答案。
项目|若依Ruoyi-Vue学习笔记
文章图片

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

项目|若依Ruoyi-Vue学习笔记
文章图片

前端实现
请求的封装 验证码的代码实现在ruoyi-ui/src/views/login.vue中。
基本流程概括:打开登陆页面,向后端请求验证码图片和一个uuid(Redis的key)
项目|若依Ruoyi-Vue学习笔记
文章图片

前端Vue和后端Springboot交互时通常使用axios(ajax),而这里看不到axios的调用是因为进行了多次封装。如果再深入追溯,可进入getCodeImg()方法,然后发现还有封装:
项目|若依Ruoyi-Vue学习笔记
文章图片

进入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: https://cli.vuejs.org/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.password const code = userInfo.code const uuid = userInfo.uuid return 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 = https://www.it610.com/article/{ 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.loadUserByUsername authentication = 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()); // 生成token return tokenService.createToken(loginUser); }

前后端不分离版使用的安全框架时Shiro;而分离版使用的是Spring Security。
注意最后的recordLoginInfo方法:记录用户最近的登陆信息。后台的数据表会记录用户登陆的ip和时间。
登陆的日志信息会存入sys_logininfor
项目|若依Ruoyi-Vue学习笔记
文章图片

用户最近登陆信息则存入(更新)sys_user
项目|若依Ruoyi-Vue学习笔记
文章图片

/** * 校验验证码 * * @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-Vue学习笔记
文章图片
这两个请求可以在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 = false store.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.user const 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 roles = permissionService.getRolePermission(user); // 权限集合 Set permissions = permissionService.getMenuPermission(user); AjaxResult ajax = AjaxResult.success(); ajax.put("user", user); ajax.put("roles", roles); ajax.put("permissions", permissions); return ajax; }

测试获取角色和权限
项目|若依Ruoyi-Vue学习笔记
文章图片

查看数据库中表关系可以发现:
  • 每个用户有一个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 = false store.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 = https://www.it610.com/article/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 menus = menuService.selectMenuTreeByUserId(userId); return AjaxResult.success(menuService.buildMenus(menus)); }

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

mapper:
id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult"> select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, 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_time from sys_menu m left join sys_role_menu rm on m.menu_id = rm.menu_id left join sys_user_role ur on rm.role_id = ur.role_id left join sys_role ro on ur.role_id = ro.role_id left join sys_user u on ur.user_id = u.user_id where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0AND ro.status = 0 order by m.parent_id, m.order_num

项目|若依Ruoyi-Vue学习笔记
文章图片

以上就是以树形嵌套结构查询出的结果
注意mapper层并未实现嵌套关系,而是在Java层面实现(使用递归实现;但是个人感觉使用Map存储是最快的;当然如果把这个工作交给前端来实现也是完全可以的),这里的父子菜单嵌套非常类似我之前写的博客的父子评论嵌套设计(树形结构):博客-评论系统数据库设计及实现
/** * 根据父节点的ID获取所有子节点 * * @param list 分类表 * @param parentId 传入的父节点ID * @return String */ public List getChildPerms(List list, int parentId) { List returnList = new ArrayList(); for (Iterator 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 list, SysMenu t) { // 得到子节点列表 List 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:
...

3.2 用户管理(PageHelper分页) 项目|若依Ruoyi-Vue学习笔记
文章图片

这里省略前端的代码,直接贴后端的处理
Controller:
  1. 设置分页
  2. 进行正常的查询(PageHelper中的拦截器会拦截数据库sql查询语句,并加入分页的sql语句,完成分页)
  3. 把查询结果封装后返回
/** * 获取用户列表 */ @PreAuthorize("@ss.hasPermi('system:user:list')") @GetMapping("/list") public TableDataInfo list(SysUser user) { startPage(); // 设置请求分页数据 List list = userService.selectUserList(user); return getDataTable(list); // 封装返回结果 }

/** * 响应请求分页数据 */ @SuppressWarnings({ "rawtypes", "unchecked" }) protected TableDataInfo getDataTable(List list) { TableDataInfo rspData = https://www.it610.com/article/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 部分树状图 跟菜单列表是一样的逻辑
项目|若依Ruoyi-Vue学习笔记
文章图片

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

这里特别注意buildTreeSelect方法,他把查询到的Tree中的SysDept通过stream转化成TreeSelect结构,即从后端数据转化成给前端显示的数据。
/** * 构建前端所需要下拉树结构 * * @param depts 部门列表 * @return 下拉树结构列表 */ @Override public List buildDeptTreeSelect(List depts) { List deptTrees = buildDeptTree(depts); return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); }

从包含很多无关信息的dept
项目|若依Ruoyi-Vue学习笔记
文章图片

变到了只剩部门id、部名名和children的树形结构,这样前端解析就很方便和舒服
项目|若依Ruoyi-Vue学习笔记
文章图片

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();

通过异步任务管理器记录(登陆)日志:
  1. AsyncManager.me()获取一个AsyncManager对象(单例模式)
  2. 执行execute方法,执行任务,传入的是一个TimerTask对象
/** * 执行任务 * * @param task 任务 */ public void execute(TimerTask task) { executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); }

  1. TimerTask对象实现了Runnable接口,是一个任务,由一个线程Thread去执行,注意这里的executors就是一个线程池
/** * 异步操作任务调度线程池 */ private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

总结:
异步任务管理器,内部定义了一个线程池,然后根据业务,创建添加日志的任务,交给线程池来执行,这样就做到了日志和业务的抽象与解耦合。
6. 代码自动生成 B站视频
Ruoyi提供了代码自动生成功能(MyBatisPlus只能自动生成后端代码,Ruoyi生成前后端+数据库的代码),我们只需要在数据库创建数据表,在管理页面即可自动生成增删改查的代码。
从Ruoyi为我们生成的代码文件结构也可以很清楚的了解到整个项目的结构(前后端+数据库)
  1. 先在mysql数据库中创建一张实体类的表test_user
use `ruoyi-vue`; create table test_user( id int primary key auto_increment, name varchar(20), password varchar(20) );

  1. 在后台页面的系统工具->代码生成->导入新表
项目|若依Ruoyi-Vue学习笔记
文章图片

3. 预览代码,Ruoyi自动为我们生成了从数据库到后端到前端的代码
项目|若依Ruoyi-Vue学习笔记
文章图片

项目|若依Ruoyi-Vue学习笔记
文章图片

5. 编辑:基本信息、字段信息、生成信息
项目|若依Ruoyi-Vue学习笔记
文章图片
项目|若依Ruoyi-Vue学习笔记
文章图片

6. 点击生成代码,生成zip压缩包,解压复制到Ruoyi项目中,解压后可以看到三部分:main(Java后端), vue(Vue前端), sql(菜单的SQL语句)
7. CV导入代码,执行sql,重启(rebuild后端项目)前后端项目。可以看到“测试代码生成”页面,拥有基本的增删查改
【项目|若依Ruoyi-Vue学习笔记】项目|若依Ruoyi-Vue学习笔记
文章图片

    推荐阅读