一、前言
1、什么是若依?
若依框架(RuoYi)是一套基于Java开发的快速开发框架,它提供了许多常用的功能模块和工具 ,包括用户管理、部门管理、角色管理、菜单管理、字典管理、系统监控、定时任务等。若依框架采用了MVC(Model-View-Controller)的架构模式,使用了Spring Boot、MyBatis等流行的开源框架,可以帮助开发者快速搭建企业级的后台管理系统。若依框架还提供了许多可视化的操作界面,使得开发者可以方便地进行系统配置和管理。
二、验证码
1、验证码前端实现
在运行成功之后前端会自动进入到后台管理系统界面,然后发送一个验证码的一个请求"captchaImage",打开前端代码login.vue里面的created方法。
编辑
之后执行getCodeImg生成一张图片显示在网页上。
验证码的验证规则是怎样的呢?
在后端会直接生成一个表达式,例如:1+1=2,那么就会将这个格式转换为 1+1=?@2 ,1+1=? 转成图片,传到前端进行展示,而将2 存入 Reids里面,我们看看后端代码。
2、验证码后端实现
在CaptchaController控制器类中的getCode方法大哥断点,这里就显示的将1+1=2的格式转为1+1=?@2。
编辑
然后将key(就是verifyKey)与value(是code)还有设置验证码有效期(分钟)、存储到redis里面,前端验证验证码就直接从redis里面进行验证。
编辑
编辑
3、反向代理
前端代码是80端口,后台代码是8080端口,但是我们F12打开控制台来查看,为什么localhost没有带端口号呢?为什么又多了一个dev-api的路径呢?
编辑
在前端的vue.config.js文件当中,这里的作用是反向代理,路径重写。
csharp
proxy: { // 反向代理,8080 -> 80
// 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]: '' // 替换为空,然后再映射
}
}
},
其中里面有一块是VUE_APP_BASE_API,这个开发环境,在其后面添加路径为 /dev-api 。
编辑
三、登录后端实现
找到login方法,首先它会进行验证码校验,对当期的用户名进行验证,首先先判断验证码是否为空,然后转载redis里面拿出键值,进行用户输入的验证码与redis取出来的进行对比,输入错误抛出异常,然后发出一个异步请求来记录登录信息(记录打印信息到日志、获取客户端操作系统、获取客户端浏览器、对象)然后将数据插入进去。
编辑
编辑
然后登录前置校验,检验(用户名或密码为空、密码如果不在指定范围内、IP黑名单校验)。
登录操作查询出来用户存在(账号与密码都正确),然后插入当前用户的操作、记录登录信息、生成token(返回给前端)。
编辑
四、获取用户角色和权限
在输入完正确的用户名、密码、验证码,我们可以看到一个名为getInfo的请求。
1、getInfo前端实现
编辑
这其实是个全局获取当用户登录之后会立即发送,作用是获取当前用户的角色和权限信息,并且存储到 Vuex 中,这个在permission.js 文件当中。
编辑
扩:vue中 router.beforeEach() 的作用------router.beforeEach 是全局钩子函数,它是在路由跳转之前所调用的函数,在实际开发中页面进度条的加载、设置页面标题、判断用户是否已经登录过了等等都可以在该函数中执行。
点击GetInfo进去看看,GetInfo是一个方法其作用是获取用户信息。
编辑
货到最上面getInfo是来自/api/login文件下的,进去看看,这里就是发起请求的路径了,方式为get。
编辑
编辑
2、getInfo后端实现
进入到控制层getInfo方法。
编辑
getRolePermission:查询当前用户的角色信息,是管理员add("admin"),不是的话,去数据库里面查找用户。
编辑
getMenuPermission:查询用户的权限信息。
编辑
如果是管理员就是 ::* (获取全部的权限,菜单:目录:按钮),不是的话去数据库里面查找权限。
然后将获取到的user、roles、permissions封装到AjaxResult里面返回给前端。
前端如何解析后端返回的数据呢?
在之前的GetInfo方法里面的commit(将值存储到vuex里面),将信息解析出来,渲染到页面上面。
编辑
五、获取动态菜单路由
我们看到左边的菜单栏是动态生成的,不是直接写死在页面上面,不同的用户有不同的权限,就有不同的菜单,这个是如何实现的呢?
编辑
1、前端实现
控制台有一个getRouters请求,很明显这个就是实现动态菜单的请求了,而且这个貌似与上面的getInfo一样,是全局路由?
打开permission.js文件很明显GenerateRoutes这个就是我们要找的,进去看看,同样的生成路由,从后端获取信息解析。
编辑
但是getRouters是在/api/menu目录下的,去看看,一个发起请求的路径。
编辑
2、后端实现
这里猜一下,菜单实现的方法是什么?------递归,我们找的getRouters,来看看。
编辑
selectMenuTreeAll: 查找管理员的权限对应的菜单数据。
selectMenuTreeByUserId: 查找不是管理员的权限对应的菜单数据。
编辑
一直追踪selectMenuTreeAll 一直到mapper文件,找到sql,直接cv到navicat 运行看看啥效果。
编辑
其中sys_menu 这张表有menu_id与parent_id,这两个就是分辨哪个是哪个的子菜单,parent_id为0的就是父菜单,最最顶上面的。
将查出来的值menus与0(0的值就是parentId,先查找父菜单)传到getChildPerms方法里面。
迭代list里面的值的getParentId,如果等于parentId值,那就是父菜单。
编辑
然后开始递归(方法为recursionFn),思路一样,如果list里面的值的getParentId,如果等于parentId值,那就是找到了对应的父菜单;这样一级一级的组成一个类似链表的一个结构,最后返回给控制层一个List类型的数据,封装成AjaxResult,返回给前端。
前端将AjaxResult一个一个组装成路由字符串,转换为组件对象。
编辑
六、首页数据加载
登录完成之后,会直接进入到首页这个是怎么实现的呢?
编辑
在handleLogin方法里面,当用户登录成功之后直接重定向到"/"。
编辑
这个路径对应到哪呢?
其实是router里面的index.js文件。
编辑
点击进去看看
导航栏
首页
编辑
找到Sidebar文件的index.js,里面可以看到有一个v-for,这个就是之前的,动态菜单后端传来的数据通过循环一个一个渲染到页面上面的。
编辑
七、用户数据分页
进入到页面之后,我们点击用户管理,其中会发送一个list请求加上分页信息pageNum=1&pageSize=10,这个是如何实现的呢?
编辑
找到对应的index文件。
编辑
数据在页面加载的时候就出来了,这肯定一初始化有关(created()方法)。
this.getList(): 获取用户管理数据。
this.getDeptTree(): 获取旁边的部门信息。
listUser向后端发起请求,后端接收。
编辑
找到list。
startPage(): 设置请求分页数据。
selectUserList(): 查找用户信息。
getDataTable(): 响应请求分页数据
编辑
利用分页插件(PageHelper)将响应请求分页数据返回给前端。
其中:
1、startPage()
PageHelper 中的 reasonable 对参数进行逻辑处理,保 证参数的正确性, pageNum = 0/-1,pageNum = 1
2、userService.selectUserList(user);
注解 @DataScope(deptAlias = "d", userAlias = "u") 给表设置别名的,sys_dept d,sys_user u
八、部门树状图
在前面写到,当页面初始化的时候,会直接加载三个方法,其中getDeptTree就是查出所有的部门数据。
编辑
编辑
进入user.js里面,路径是/system/user/deptTree,直接到后端查找。
编辑
进入控制层进入到selectDeptTreeList里面看看。
编辑
其中selectDeptList方法是查询部门管理数据,buildDeptTreeSelect是构建前端所需要下拉树结构,将selectDeptList查询出来的结果depts传给buildDeptTreeSelect。
编辑
编辑
其中buildDeptTree是构建前端所需要树结构,里面看到了老面孔------recursionFn,逻辑与前面一样,如果父节点等于子节点,那么子节点就是父节点的子菜单。
recursionFn: 递归列表。
编辑
然后将结果返回给前端,前端再解析出来。
九、添加用户数据
1、回显数据
在user/index.vue里面找到新增按钮的方法,其中里面有一个名为reset,他的作用是在打开的新增表单重置。
编辑
编辑
编辑
然后里面getUser方法,点进去,很熟悉前面讲过的,只是后面加了个参数(userId),路径为/system/user/userId,传给后端。
编辑
后端控制层找到方法GetMapping,接受无路径与有路径为userId
编辑
checkUserDataScope: 校验用户是否有数据权限。
selectRoleAll: 查询所有角色。
selectPostAll: 查询所有岗位。
将这些数据回显到页面上去,也就是现在我们能看到的归属部门、岗位、角色所选的下拉列表。
然后再根据用户ID查询用户(selectUserById),然后将数据封装返回给前端。
2、新增操作
将基本信息填写完成之后,点击确定按钮数据提交给后端。
编辑
通过点击事件找到方法
编辑
新增与修改请求路径如下。
编辑
控制层先做判断(登录账号已存在、手机号码已存在、邮箱账号已存在),然后设置登录用户名、对密码进行加密(密码默认的是123456)。
编辑
然后直接向DB(sys_user表)插入数据。
编辑
然后新增用户岗位关联(insertUserPost)、新增用户与角色管理(insertUserRole)
编辑
编辑
其实就是对三张表进行新增操作(sys_user、sys_user_post、sys_user_role)。
编辑
编辑
编辑
十、修改用户信息
找到修改按钮然后找到对应的点击事件方法------updateUser,同样的数据回显查询用户详细数据,然后数据回显在页面上,这里就不做过多的描述了。
编辑
点击确定,触发updateUser,携带数据到后端路径为/system/user。
编辑
点击updateUser,进入到业务层,发现这个不是和新增操作一样吗?对修改的逻辑就是先删除=后进行新增操作。
编辑
十一、删除用户
找到删除的点击事件------handleDelete,路径为/system/user/userId,发送给后端,后端接收请求。
编辑
首先先判断是否为当前正在操作的用户(也就是本人,本人操作肯定是不能删除的了)。
编辑
如果没有问题执行业务层,然后校验用户是否允许操作(checkUserAllowed 结果是 不允许操作超级管理员用户)、校验用户是否有数据权限(checkUserDataScope 结果是 没有权限访问用户数据)、然后删除用户与角色关联(deleteUserRole)、删除用户与岗位关联(deleteUserPost)、批量删除用户信息(deleteUserByIds)。
注意这里的批量删除用户信息不是真的删除而是一个逻辑删除,也就是修改他的状态del_flag为2。
十二、异步任务管理器
打开后端找到login的控制层点击login进入到业务层,在业务层中有一行这样的代码。
less
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
编辑
编辑
一看就很高级,那么他有什么作用呢?
给出结论: 通过异步任务管理器记录登录日志。
首先AsyncManager进去看看里面有啥? 一个异步任务管理器。找到me(),是个静态方法,return 了一个me,看看上面 懒汉式单例模式,分配异步任务。
编辑
那么就是AsyncManager.me() 获取一个 AsycnManager 对象。
然后执行 execute 方法,执行任务,传入的是一个 Task 对象(就是AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")),这么大长一段,并设置操作延迟10毫秒。
编辑
TimerTask实现了TimerTask接口,由线程 Thread 去执行。
编辑
那么recordLogininfor的作用是记录登录信息,将代码cv出来。
scss
/**
* 记录登录信息
*
* @param username 用户名
* @param status 状态
* @param message 消息
* @param args 列表
* @return 任务task
*/
public static TimerTask recordLogininfor(final String username, final String status, final String message,
final Object... args)
{
final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
final String ip = IpUtils.getIpAddr();
return new TimerTask()
{
@Override
public void run()
{
String address = AddressUtils.getRealAddressByIP(ip);
StringBuilder s = new StringBuilder();
s.append(LogUtils.getBlock(ip));
s.append(address);
s.append(LogUtils.getBlock(username));
s.append(LogUtils.getBlock(status));
s.append(LogUtils.getBlock(message));
// 打印信息到日志
sys_user_logger.info(s.toString(), args);
// 获取客户端操作系统
String os = userAgent.getOperatingSystem().getName();
// 获取客户端浏览器
String browser = userAgent.getBrowser().getName();
// 封装对象
SysLogininfor logininfor = new SysLogininfor();
logininfor.setUserName(username);
logininfor.setIpaddr(ip);
logininfor.setLoginLocation(address);
logininfor.setBrowser(browser);
logininfor.setOs(os);
logininfor.setMsg(message);
// 日志状态
if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
{
logininfor.setStatus(Constants.SUCCESS);
}
else if (Constants.LOGIN_FAIL.equals(status))
{
logininfor.setStatus(Constants.FAIL);
}
// 插入数据
SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
}
};
}
其实这串代码就是封装了当前登录用户的信息(获取客户端操作系统、获取客户端浏览器等)然后封装对象。插入数据(insertLogininfor)。
但是!这里不会执行,而是将任务交给线程对象来执行,这里只是个任务而已。
找到execute方法,点击进去,然后点击executor,executor是一个成员变量(异步操作任务调度线程池),将我们上面封装的对象交给线程池来执行。
编辑
编辑
敲两下shift键,将scheduledExecutorService cv到搜索框中找到scheduledExecutorService的bean。
编辑
编辑
也就是说AsyncManager 的executor其实就是scheduledExecutorService返回的对象,那么ScheduledThreadPoolExecutor就是在创建线程池,大小为50(corePoolSize)。
编辑
那么逻辑就是:异步任务管理器,内部定义了一个线程池,然后根据业务创建添加日志的任务,交给线程池来处理,这样做到日志和业务的抽象,解耦合,日志全部统一处理。
举个例子,用户登录操作将登录的信息存储日志,同步的话用户登录请求完成之后等待日志插入请求完成,然后再执行用户登录后操作,异步的话将插入日志分离了出来,这样的话,不管是啥操作,都可以通过异步任务管理器,创建一个任务,交给线程池直接去执行(就不用我们去管了,线程池分配了他线程,他就会执行)大大提高了效率。
十三、代码自动生成器
首先先创建数据表
sql
create table test_user(
id int primary key auto_increment,
name varchar(11),
password varchar(11)
);
建完之后打开若依运行的web,在系统工具里面的代码生成,点击导入按钮。
编辑
然后编辑基本信息(带*号的一定要填)。
编辑
编辑
然后点击生成代码。
编辑
将下载好的压缩包解压出来,main(Java 后端代码),vue(Vue 前端代码),SQL(菜单 SQL),导入代码,重启项目。
注:如果后端抛出 404 异常,rebuild project,重新启动。
编辑
效果展示
编辑
不积跬步无以至千里,趁年轻,使劲拼,给未来的自己一个交代!向着明天更好的自己前进吧!
编辑