ruoyi学习笔记

1,036 阅读14分钟

本文已参与「博主入驻有礼」活动, 一起开启掘金创作之路。

ruoyi前后端不分离版本4.4.0

对一个开源web项目要学习什么?

  • 需求分析

  • 权限管理

    • 数据权限
    • 操作权限
  • 数据库设计

  • UI设计

  • 日志系统

  • 文档书写

properties

ruoyi是后缀为Properties的类,用于读取配置文件中的配置,而不是java中的properties类
这种类加上@Configuration、@ConfigurationProperties注解,用@Value注解配合spel(或在类上指定配置中的前缀)获取配置文件中的属性

  • 跨模块的配置文件也可以读取,因为依赖?还是因为读取classpath下的文件?
  • maven依赖是单向的(毕竟会报循环依赖)
    在配置类用@EnableConfigurationProperties({MySqlBinlogConnectJavaProperties.class})注解读取获取到的属性

动态数据源配置

  • 什么是“动态数据源”?
  • 似乎要先系统学学mysql主从,,话说单纯mysql主从不需要改代码吧。
public class DruidConfig
{
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

Maven多模块开发

曾经我们在一个应用(一个war or jar)中划分出包(层是以包的层次结构划分的)。随着项目体积的逐渐增大,项目结构逐渐横向扩张变得臃肿(短粗):

  • 单个java文件逐渐变得很长,同一包下的文件变得很多,但项目结构已定,改变结构很费功夫;

  • pom.xml中的依赖列表也越来越长,到后来你根本就不清楚哪个依赖是谁需要的,渐渐的,很多不必要的依赖被引入。甚至出现了一个依赖有多个版本存在。

  • 这个应用可能需要有一个前台和一个后台管理端,你发现大部分dao,一些service,和大部分util在其他应用中可以用;

    • 如果其他项目不能引用这些代码,则不得不重写一遍;
  • build整个项目的时间越来越长,尽管你只是一直在web层工作,但你不得不build整个项目。

  • 某个模块,比如util,你只想让一些经验丰富的人来维护,可是,现在这种情况,每个开发者都能修改,这导致关键模块的代码质量不能达到你的要求。

  • 你不得不新建一个项目依赖这个WAR,这变得非常的恶心,因为在Maven中配置对WAR的依赖远不如依赖JAR那样简单明了,而且你根本不需要org.myorg.app.web。
    项目发展到如此庞大的水平,老一套结构已经不符合高内聚、低耦合的标准了。为了给项目“减肥”,同时进一步粒化拆分模块,我们需要“增高”,即经典的“加一层”思想。现在是以包的形式分层,再加一层就是应用之间能够互相引用(或者说依赖),也就是jar包级别之间的依赖。同时,由于传统的jar包引入方式不利于维护,pom.xml文件太长也需要拆分,我们使用maven进行分模块开发,顺便还解决了编写部分代买需要编译全部项目的问题。
    一个简单的Maven模块结构是这样的:

---- app-parent
             |-- pom.xml (pom)
             |
             |-- app-util
             |        |-- pom.xml (jar)
             |
             |-- app-dao
             |        |-- pom.xml (jar)
             |
             |-- app-service
             |        |-- pom.xml (jar)
             |
             |-- app-web
                      |-- pom.xml (war)   

较好的描述
配置文件随意,会在作用域内按优先级生效。我猜是就近原则

  • 变量的就近原则和编译原理有关系吗?是在内存中顺序寻找最近的么?但内存是可以随即存取的把。

springboot多模块简单来说,就是把按包分模块的模式,借助maven升级到jar的方式,抽象性更加强了,假如jar再升级到到war或者多个集合jar,就成微服务了,在多模块jar模式下可以将某个jar拿出来对外共用,能大大提高代码复用率与开发效率。
zhuanlan.zhihu.com/p/345682526

父模块

创建时选择:
maven pom
不以springboot为父项目的子模块需要加入这个依赖

            <!-- SpringBoot的依赖配置-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.5.9</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

声明包含的子模块,并把自己打包成pom

    <modules>
        <module>ruoyi-admin</module>
        <module>ruoyi-framework</module>
        <module>ruoyi-system</module>
        <module>ruoyi-quartz</module>
        <module>ruoyi-generator</module>
        <module>ruoyi-common</module>
    </modules>
    <packaging>pom</packaging>

属性

定义属性,如版本号等,可以自定义标签。

    <properties>
        <ruoyi.version>4.7.2</ruoyi.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
      <!-- 等等等等 -->
    </properties>

在后面用${}取出来用。

<plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.1.RELEASE</version>
                <configuration>
                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
 </plugin>

子模块

创建时选择: maven project
一个通用模块,其他模块需要的时候在pom.xml中依赖他就行;实体类、注解、配置类、异常、util等都在这里

模块版本modelVersion和父工程版本不一样(4.0.0和4.7.2)

若依里,通用的在common里,主要业务在framework里。admin就光controller层?其他模块依赖common模块,common没有依赖其他,不然会循环依赖。admin不需要指明依赖common,因为依赖了依赖common的许多模块。(父工程下的大哥)

日志学习

sys_oper_log 操作日志记录
其他日志由logback保存在日志文件中,操作日志和登录日志保存在数据库中

handleLog方法中,user,dept,deptName真是每一步都要判空啊
记录日志:用单例模式的异步管理类
异步任务工厂AsyncFactory生产异步任务,由异步任务管理类AsyncManager执行(管理)
在 RuoYi-Vue 脚手架中,使用了 Java JUC 中的 ScheduledExecutorService 去完成这个延时任务,AsyncManager.me().execute 方法实际上就是去执行一个延时任务,这个延时任务就是一个往数据库中写入一条记录。
如果请求类型是 GET 或者 DELETE,则请求参数就直接从请求对象提取了。是如果请求类型是 PUT 或者 POST,就意味着请求参数是在请求体中,请求参数有可能是二进制数据(例如上传的文件),二进制数据就不好保存了,所以对于 POST 和 PUT 还是从接口参数中提取,然后过滤掉二进制数据即可。

操作日志

LogAspect

ServletUtils.getRequest().getParameterMap()不为空
否则 使用joinPoint的参数
根据Druid,每次更新Dept的时候应该时把状态单独执行的

业务类型

0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据

授权角色:先删除user_role再插入user_role

  • controller参数: id,ids
    删除都是状态改变,可能有定时任务清理?businessType还是删除

实践:改造操作日志->记录数据变化

思路:

  • 提前取出老数据再上下文中?

  • 比较同一个类两个对象?

  • 删除/导出可以多选,怎么办?

    • 传递参数时逗号分隔的字符串
    • 导出的数据是什么?导出是导出全部,传入一个空数据

binlog学习

client.connect() is blocking (meaning that client will listen for events in the current thread).
因为阻塞所以在前台吗
mysql的binlog有三种模式(级别),Row(输出变化)、Statement(默认,输出Sql语句)、Mixed
show global variables like '%binlog_format%';
BinlogMiner:离线挖掘模式,将日志发到其他机器上分析(脱机?)

系统日志

# 日志配置
logging:
  level:
    com.ruoyi: debug
    org.springframework: warn

这个日志级别配置和ems_vue一样么?

权限学习

shiro使用

知乎上一篇教程
Realm负责Autherication和Authorizztion
shiro和slf4j的关系?
shiro认证
认真和授权流程相似
注:该实例中的类全部来自Shiro

//新建Realm
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加账户,并使他具有admin和user两个角色(可以不设置角色)
//在自定义的Realm中,应该由Subject传过来的信息中提取用户名,再根据用户名从数据库中提取权限、角色、凭证等信息
simpleAccountRealm.addAccount("wmyskxz", "123456", "admin", "user");

        // 1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
        Subject subject = SecurityUtils.getSubject(); // 获取当前主体
		//将用户输入的账号密码包装成token,并尝试登录看看是否能被认证
        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登录

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
        subject.logout(); // 登出
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出false
        
        // 判断subject是否具有admin和user两个角色权限,如没有则会报错
        subject.checkRoles("admin","user");
    }

自定义Realm

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.*;

public class MyRealm extends AuthorizingRealm {

    /**
     * 模拟从数据库中取得所有需要的用户名、密码
     */
    Map<String, String> userMap = new HashMap<>(16);
    {
        userMap.put("wmyskxz", "123456");
        super.setName("myRealm"); // 设置自定义Realm的名称,取什么无所谓..
    }

    /**
     * 授权
     * 根据用户名,获取用户的权限和角色。类似上例addAccount中的功能
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 从数据库获取角色和权限数据,
        Set<String> roles = dao.getRolesByUserName(userName);
        Set<String> permissions = dao.getPermissionsByUserName(userName);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 认证
     * @param authenticationToken 主体传过来的认证信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.从主体传过来的认证信息中,获得用户名
        String userName = (String) authenticationToken.getPrincipal();

        // 2.通过用户名到数据库中获取凭证(此处为密码,找得到就返回认证信息)
        String password = getPasswordByUserName(userName);
        if (password == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
        return authenticationInfo;
    }
}

然后将之前例子中的Realm改成自己的就好,也不用手动添加addAccount了

springboot配置-ruoyi示例

# Shiro
shiro:
  user:
    # 登录地址
    loginUrl: /login
    # 权限认证失败地址
    unauthorizedUrl: /unauth
    # 首页地址
    indexUrl: /index
    # 验证码开关
    captchaEnabled: true
    # 验证码类型 math 数组计算 char 字符
    captchaType: math
  cookie:
    # 设置Cookie的域名 默认空,即当前访问的域名
    domain: 
    # 设置cookie的有效访问路径
    path: /
    # 设置HttpOnly属性
    httpOnly: true
    # 设置Cookie的过期时间,天为单位
    maxAge: 30
    # 设置密钥,务必保持唯一性(生成方式,直接拷贝到main运行即可)KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecretKey deskey = keygen.generateKey(); System.out.println(Base64.encodeToString(deskey.getEncoded()));
    cipherKey: zSyK5Kp6PZAAjlT+eeNMlg==
  session:
    # Session超时时间,-1代表永不过期(默认30分钟)
    expireTime: 30
    # 同步session到数据库的周期(默认1分钟)
    dbSyncPeriod: 1
    # 相隔多久检查一次session的有效性,默认就是10分钟
    validationInterval: 10
    # 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
    maxSession: -1
    # 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
    kickoutAfter: false

操作权限

通过shiro,在controller层添加@RequiresPermissions,在访问前认证;通过shiro标签控制是否显示;
删除角色前需要查看角色操作权限?和数据权限,且如果角色已被分配则不能删除

            checkRoleAllowed(new SysRole(roleId));
            checkRoleDataScope(roleId);

自定义UserRealm类认证、授权,

认证

SysLoginController

    @PostMapping("/login")
    @ResponseBody
    public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe)
    {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
        Subject subject = SecurityUtils.getSubject();
        try
        {
            subject.login(token);
            return success();
        }
        catch (AuthenticationException e)
        {
            String msg = "用户或密码错误";
            if (StringUtils.isNotEmpty(e.getMessage()))
            {
                msg = e.getMessage();
            }
            return error(msg);
        }
    }
  • get的login是干什么的?
  • shiro配置login是哪个?还是说都是?

UserRealm

从token中取出password、username,tryloginService.login(username, password),逐渐catch认证;没出错则包装返回

授权

ShiroUtils中获得当前User,根据userId,从Sys_menu表中取出perms字段,从sys_role表中取出SysRole对象(当前用户的角色、权限),放在授权信息中。
doGetAuthorizationInfo方法触发机制
在需要进行认证、授权的时候,SecurityManager调用Realm中的方法

数据权限

按照部门分配
角色与部门关联表
通过注解、切面、拼接sql字符串来动态拼接查询语句。
只有角色和数据权限直接相关,用户通过自己的角色来获得权限

@DataScope

common模块中的annotion包中定义DataScope注解,两个属性用于指定别名;指定别名是为了在切面类拼接sql语句时和Mapper.xml中的别名一致
在Service层

使用该注解的方法:

  • 查询user(分页查询、未分配、已分配)
  • 查询role(只能看到自己数据权限内的角色)
  • 查询部门列表
  • 查询部门管理树
  • 查询部门管理树(排除下级)
    后两个方法就是把第三个全查出来再在Service中剔除、排列
    校验部门是否有数据权限(checkDeptDataScope)没用上?校验角色的倒是在删除角色用上了
   /**
     * 修改数据权限信息
     */
    @Override
    @Transactional
    public int authDataScope(SysRole role)
    {
        // 修改角色信息(role中有datascope字段?ORM一对多要求的么)
        roleMapper.updateRole(role);
        
        //先删后增而不是直接修改,因为修改前后条数不一定一样(多对多关联表修改都是这样么?)
        // 删除角色与部门关联
        roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId());
        // 新增角色和部门信息(数据权限)
        return insertRoleDept(role);
    }

文档

  • 设置了数据权限后,角色、用户、部门树等只能看到自己数据权限给的部门内的。
    SysUser类的orm对象包含着roles的对象引用列表,而不是像数据库一样只有外键;在从数据库查询用户的时候就已经将roles读取到内存,而不是读取sys_user_role中的roles_id,在内存中不是通过id查找的。
    此时类和数据库不是一一对应的,SysUser类中的roles需要在其他表中查找

DataScopeAspect

framework模块中的aspectj包实现切面、注解
@Before匹配的注解是下面自己的参数的

  • 没有配置切入点

在执行@DataScope标注的方法之前,用ShiroUtils.getSysUser()获取当前用户,for循环处理用户的roles,获取每个role唯一的dataScope,然后if-else判断拼接哪句sql;
然后用joinPoint.getArgs()[0]获取参数(SysUser或SysDept),获取为Object,判断后转换为BaseEntity类,放入params并开始查询

SysUser类继承自BaseEntity类,父类中有Map类型的params属性,在mapper中调用,用于获取dataScope。虽然是私有属性,但是可以通过继承的共有方法调用??还是说,其实是SysUser->Object->BaseEntity?
为什么是subString(4)?

  • 全部权限就直接为空,不用筛选
  • 自定义数据权限的角色从sys_role_dept表里查dept_id,获得这个role能看的部门id
  • 只看自己部门就d.depi_id = user的deptId
  • 能看子部门就从部门表中选择自己部门 or 祖先有自己部门的
  • 只看自己就筛选按user_id,没有别名就为空(什么时候有这种情况呢?)
    基本是查看d.dept_id是否in一个子查询中

aop中指定切点为某个包?则该包下只能由接口,实现类要转移至业务层

  • 切点只能是接口吗?

相关表(sys_前缀)

理解
user用户
role角色
menu菜单权限表菜单是前端出现的选项,有些菜单需要权限才渲染
role_menu角色权限多对多
user_role角色用户多对多

菜单里有权限字段,user通过连接表查询自己的所有权限,有权限则显示菜单、访问controller接口

不需要用户-权限直接配置?因为用户的权限通过给角色配置过了吧(被范式优化掉了?因为shiro需要“角色”?spring security还需要角色不,数据库还这么搞不?)

  • 没分配角色侧边栏什么也看不到;

  • 父子联动:

    • 关掉父子联动,有父权限才能在任务栏看到父标签,有子权限父权限标签下才有东西
    • 选父会全选子,选一个子会自动选父;子没了自动取消

在角色中有这个权限才可以看到这个选项

相关类

  • role中这个属性干嘛的?
    /** 部门组(数据权限) */
    private Long[] deptIds;
  • @RequiresRoles写在哪里?

controller

  • system/dept/treeData 公司部门树
  • /system/user/list

数据库设计

表名加上前缀以区分
用户和岗位关联表,角色和部门关联

ancestors字段保存树形结构的父节点,然后用find_in_set(#{deptId}, ancestors) 查找子节点
当前时间用sysdate()

接口的概念:编程语言中和业务中不一样,做范围区分图

  • Fernflower decompiler / x jad

判断方法:
if (table == null? false: table.startsWith(“sys”))
从canal学到的

Java中定义Map常量,List常量
一般的方式的使用静态代码块。比如:

public final static Map map = new HashMap();  
static {  
    map.put("key1", "value1");  
    map.put("key2", "value2");  
}

下面为一种简单定义Map常量的方式

public final static Map<String, Fragment> NAV_ITEM_ADPTER = new HashMap<String, Fragment>() {
    {
        put("拍录传", new CameraFragment());
        put("集群对讲", new GroupTalkFragment());
        put("视通", new VideoCallFragment());
        put("位置", new PositionFragment());
        put("浏览", new BrowseFragment());
        put("消息", new MsgFragment());
        put("群组", new GroupFragment());
        put("设置", null);
        put("退出", null);
    }
};
        mv.addObject("pageInfo",pageInfo);
        mv.setViewName("orders-page-list");

modelView是把页面和页面需要的数据当成一个对象来操作,这种思想可以借鉴。如果需要的数据是一个对象,就addObject,把需要的数据包装成对象。似乎前后端分离也可以这么封装。

  • 这点儿可以看Spring的解释证明一下
  • 字典数据是干嘛的?
  • 内连接的on只能写主键与外键?