# 插件集成

为了让开发者更加方便和快速的满足需求,提供了各种插件集成实现方案。

# 集成redis实现集群会话

目前的会话信息通过ehcache存储在本地,不方便集群会话管理,由于不少小伙伴需要,所以抽时间整合了一下。如果有需要可以参考我的步骤去集成。改动比较多,请根据实际情况调整。

1、由于切换成redis,可以删除一些处理类(不在同步到数据库表)和ehcache相关内容。

// 删除的java类
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\service\SysShiroService.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\session\OnlineSessionDAO.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\filter\online\OnlineSessionFilter.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\filter\sync\SyncOnlineSessionFilter.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\session\OnlineWebSessionManager.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\session\SpringSessionValidationScheduler.java
ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysUserOnlineMapper.java
ruoyi-system\src\main\java\com\ruoyi\system\service\ISysUserOnlineService.java
ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysUserOnlineServiceImpl.java

// 删除mybatis的数据库操作
ruoyi-system\src\main\resources\mapper\system\SysUserOnlineMapper.xml

// 删除ehcache配置
ruoyi-admin\src\main\resources\ehcache\ehcache-shiro.xml

// 删除ruoyi-common\pom.xml中的shiro-ehcache依赖
<!-- Shiro使用EhCache缓存框架 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-ehcache</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

2、ruoyi-common\pom.xml模块添加整合依赖

<!-- shiro整合redis -->
<dependency>
	<groupId>org.crazycake</groupId>
	<artifactId>shiro-redis</artifactId>
	<version>3.3.1</version>
	<exclusions>
	    <exclusion>
	        <groupId>org.apache.velocity</groupId>
	        <artifactId>velocity</artifactId>
	    </exclusion>
	</exclusions>
</dependency>

<!-- springboot整合redis -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

3、ruoyi-admin文件application-druid.yml,添加redis配置

# 数据源配置
spring:
    # redis配置
    redis:
      database: 0
      host: 127.0.0.1
      port: 6379
      password: 
      timeout: 6000ms           # 连接超时时长(毫秒)
      lettuce:
        pool:
          max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
          max-wait: -1ms    # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-idle: 10      # 连接池中的最大空闲连接
          min-idle: 5       # 连接池中的最小空闲连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

4、下载插件相关包和代码实现覆盖到工程中

提示

插件相关包和代码实现ruoyi/集成redis实现集群会话管理.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

5、测试验证会话集群,在线用户,缓存监控等功能是否正常。

# 集成jwt实现登录授权访问

jwt适用于前后端分离,但是不分离版本对外提供接口有时候也需要。不少小伙伴有提过要求,最近抽空整合了一下方案,参考步骤如下。

1、ruoyi-framework\pom.xml添加jwt依赖

<!-- jwt jar-->
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.4.0</version>
</dependency>
1
2
3
4
5
6

2、下载插件相关包和代码实现覆盖到工程中

提示

插件相关包和代码实现ruoyi/集成jwt实现权限登录授权.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

3、添加测试接口类

ruoyi-admin\ApiController.java

package com.ruoyi.web.controller.system;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.domain.AjaxResult;

@RestController
@RequestMapping("/api")
public class ApiController
{
    /**
     * 无权限访问
     * 
     * @return
     */
    @GetMapping("/list")
    public AjaxResult list()
    {
        return AjaxResult.success("list success");
    }

    /**
     * 菜单权限 system:user:list
     */
    @GetMapping("/user/list")
    @RequiresPermissions("system:user:list")
    public AjaxResult userlist()
    {
        return AjaxResult.success("user list success");
    }

    /**
     * 角色权限 admin
     */
    @GetMapping("/role/list")
    @RequiresRoles("admin")
    public AjaxResult rolelist()
    {
        return AjaxResult.success("role list success");
    }
}
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
38
39
40
41
42
43
44

4、测试权限登录访问请求

登录访问(返回token) POST / http://localhost:80/jwt/login?username=ry&password=admin123

测试任意权限(header携带token) GET / http://localhost:80/api/list

测试菜单权限(header携带token) GET / http://localhost:80/api/user/list

测试角色权限(header携带token) GET / http://localhost:80/api/role/list

# 集成cas实现单点登录认证

单点登录(Single Sign On),简称为SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

1、下载cas-overlay-template搭建cas服务器

下载项目https://github.com/apereo/cas-overlay-template.git

# 构建项目(需要安装gradle环境)
gradlew.bat clean build

# 解压
gradlew.bat explodeWar
1
2
3
4
5

此时将会在bulid目录下生成一个cas-resources文件夹,我们把里面的文件全部拷贝到cas-overlay-template/src/main/resources,将/etc/cas/thekeystore也拷贝到该目录下

修改配置application.properties

server.ssl.key-store=classpath:thekeystore
1

为了方便测试直接屏蔽了ssl,端口改成了8080

server.ssl.enabled=false
server.port=8080
1
2

在内嵌的Tomcat中运行cas

gradlew.bat run
1

启动完成后浏览器中打开(http://localhost:8080/cas/login (opens new window))就可以访问了。

在登录也面输入用户名和密码:casuser/Mellon,出现界面表明cas已经部署成功。

2、cas服务端整合Mysql数据库,添加service-registry依赖

修改build.gradle文件,加入mysql驱动配置














 
 
 



dependencies {
    // Add modules in format compatible with overlay casModules property
    if (project.hasProperty("casModules")) {
        def dependencies = project.getProperty("casModules").split(",")
        dependencies.each {
            def projectsToAdd = rootProject.subprojects.findAll {project ->
                project.name == "cas-server-core-${it}" || project.name == "cas-server-support-${it}"
            }
            projectsToAdd.each {implementation it}
        }
    }
    // CAS dependencies/modules may be listed here statically...
    implementation "org.apereo.cas:cas-server-webapp-init:${casServerVersion}"
    implementation "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
    implementation "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"
    implementation "org.apereo.cas:cas-server-support-jdbc-drivers:${casServerVersion}"
    implementation "mysql:mysql-connector-java:8.0.22"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

修改resources/application.properties文件,加入数据库连接配置

# 取消静态配置
# cas.authn.accept.users=casuser::Mellon
# cas.authn.accept.name=Static Credentials

# 本地的数据库配置信息
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/ry?serverTimezone=UTC&allowMultiQueries=true
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=password
cas.authn.jdbc.query[0].sql=select password from sys_user where login_name= ?
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3、设置允许http访问

修改resources/application.properties开启识别json

# 开启识别json文件配置
cas.tgc.secure=false
cas.service-registry.init-from-json=true
cas.service-registry.json.location=classpath:/services
1
2
3
4

修改services/HTTPSandIMAPS-10000001.json,加入http



 






{
  "@class": "org.apereo.cas.services.RegexRegisteredService",
  "serviceId": "^(https|http|imaps)://.*",
  "name": "HTTPS and IMAPS",
  "id": 10000001,
  "description": "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
  "evaluationOrder": 10000
}
1
2
3
4
5
6
7
8

4、ruoyi-framework\pom.xml添加pac4j依赖

<!-- pac4j安全引擎 -->
<dependency>
	<groupId>org.pac4j</groupId>
	<artifactId>pac4j-cas</artifactId>
	<version>3.0.2</version>
</dependency>

<dependency>
	<groupId>io.buji</groupId>
	<artifactId>buji-pac4j</artifactId>
	<version>4.0.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12

5、下载插件相关包和代码实现覆盖到工程中

提示

插件相关包和代码实现ruoyi/集成cas实现单点登录认证.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

6、测试单点登录访问请求,是否正常登陆以及退出,同时能访问多个不同系统。

# 集成docker实现一键部署

Docker是一个虚拟环境容器,可以将你的开发环境、代码、配置文件等一并打包到这个容器中,最终只需要一个命令即可打包发布应用到任意平台中。

1、安装docker

yum install https://download.docker.com/linux/fedora/30/x86_64/stable/Packages/containerd.io-1.2.6-3.3.fc30.x86_64.rpm
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce
curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
1
2
3
4
5

2、检查dockerdocker-compose是否安装成功

docker version
docker-compose --version
1
2

3、文件授权

chmod +x /usr/local/bin/docker-compose
1

4、下载若依docker插件,上传到自己的服务器目录

插件相关脚本实现ruoyi/集成docker实现一键部署.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

  • 其中db目录存放ruoyi数据库脚本
  • 其中jar目录存放打包好的jar应用文件
  • 数据库mysql地址需要修改成ruoyi-mysql
  • 数据库脚本头部需要添加SET NAMES 'utf8';(防止乱码)

5、启动docker

systemctl start docker
1

6、构建docker服务

docker-compose build
1

7、启动docker容器

docker-compose up -d
1

8、访问应用地址

打开浏览器,输入:(http://localhost:80 (opens new window)),若能正确展示页面,则表明环境搭建成功。

启动服务的容器docker-compose up ruoyi-mysql ruoyi-server

停止服务的容器docker-compose stop ruoyi-mysql ruoyi-server

时区设置

如果服务器的时区不正确,可以在dockerfile文件中添加ENV TZ=Asia/Shanghai

# 升级springboot到最新版本3.x

Spring Boot 3.x要求使用Java 17或更高版本,所以需要确保项目使用的Java版本符合要求。

1、修改pom.xml文件,version版本根据实际情况配置最新。

<!-- java.version版本8更换为17 -->
<java.version>17</java.version>

<!-- 新增mybatis节点,版本为3.0.2 -->
<mybatis-spring-boot.version>3.0.2</mybatis-spring-boot.version>

<!-- spring-boot版本2.5.15更换为3.1.5 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-dependencies</artifactId>
	<version>3.1.5</version>
	<type>pom</type>
	<scope>import</scope>
</dependency>

<!-- shiro-core & shiro-spring 修改为`jakarta`依赖,排除 shiro-core & shiro-web -->
<!-- Shiro核心框架 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<classifier>jakarta</classifier>
	<version>${shiro.version}</version>
	<!-- 排除仍使用了javax.servlet的依赖 -->
	<exclusions>
		<exclusion>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
		</exclusion>
		<exclusion>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-web</artifactId>
		</exclusion>
	</exclusions>
</dependency>

<!-- Shiro使用Spring框架 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<classifier>jakarta</classifier>
	<version>${shiro.version}</version>
	<!-- 排除仍使用了javax.servlet的依赖 -->
	<exclusions>
		<exclusion>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
		</exclusion>
		<exclusion>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-web</artifactId>
		</exclusion>
	</exclusions>
</dependency>


<!-- 新增四个配置依赖 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-web</artifactId>
	<classifier>jakarta</classifier>
	<version>${shiro.version}</version>
	<exclusions>
		<exclusion>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>${mybatis-spring-boot.version}</version>
</dependency>

<dependency>
	<groupId>com.mysql</groupId>
	<artifactId>mysql-connector-j</artifactId>
	<version>8.1.0</version>
</dependency>

<dependency>
	<groupId>jakarta.servlet</groupId>
	<artifactId>jakarta.servlet-api</artifactId>
	<version>6.0.0</version>
</dependency>
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

2、修改ruoyi-admin/pom.xml文件mysql依赖。



 
 


<!-- Mysql驱动包 -->
<dependency>
	<groupId>com.mysql</groupId>
	<artifactId>mysql-connector-j</artifactId>
</dependency>
1
2
3
4
5

3、修改ruoyi-framework/pom.xml文件kaptcha & shiro依赖,新增shiro-core & shiro-web依赖。








 








 


 
 
 
 
 
 
 
 
 
 
 
 

<!-- 验证码 -->
<dependency>
	<groupId>pro.fessional</groupId>
	<artifactId>kaptcha</artifactId>
	<exclusions>
		<exclusion>
			<artifactId>servlet-api</artifactId>
			<groupId>jakarta.servlet</groupId>
		</exclusion>
	</exclusions>
</dependency>

<!-- Shiro使用Spring框架 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<classifier>jakarta</classifier>
</dependency>

<!-- Shiro核心框架 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<classifier>jakarta</classifier>
</dependency>

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-web</artifactId>
	<classifier>jakarta</classifier>
</dependency>
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

4、修改ruoyi-common/pom.xml文件servlet依赖为jakarta





 




 
 


<!--Shiro核心框架 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<classifier>jakarta</classifier>
</dependency>

<!-- servlet包 -->
<dependency>
	<groupId>jakarta.servlet</groupId>
	<artifactId>jakarta.servlet-api</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12

5、Java EEJakarta EE
Spring Boot 3.0将所有底层依赖项从Java EE迁移到了Jakarta EE,会对一些使用了Java EE的方法造成影响,需要进行相应的修改和调整。

javax.xxxx替换成jakarta.xxxx,例如

javax.annotation 替换成 jakarta.validation
javax.servlet    替换成 jakarta.servlet
javax.validation 替换成 jakarta.validation
javax.xxxxxxxxxx 替换成 jakarta.xxxxxxxxxx
1
2
3
4

但是有些原生方法是不需要去进行修改的,例如项目中的这几个方法,包不需要替换成jakarta.xxxx

import javax.imageio.ImageIO;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.sql.DataSource
1
2
3
4
5
6
7
8

PS:如果嫌麻烦可以使用idea自带的转换功能

6、修改DruidConfig.java,添加数据库监听功能配置Bean







 
 
 
 
 




package com.ruoyi.framework.config;

public class DruidConfig
{
    .......

    @Bean
    public DruidStatProperties druidStatProperties()
    {
        return new DruidStatProperties();
    }

    .......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

7、到此就对springboot3做了全部的兼容,提供全部改后代码供大家参考。

提示

插件相关包和代码实现ruoyi/升级springboot到最新版本3.x.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

# 集成websocket实现实时通信

WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

1、ruoyi-framework/pom.xml文件添加websocket依赖。

<!-- SpringBoot Websocket -->
<dependency>  
   <groupId>org.springframework.boot</groupId>  
   <artifactId>spring-boot-starter-websocket</artifactId>  
</dependency>
1
2
3
4
5

2、配置匿名访问(可选)

// 如果需要不登录也可以访问,需要在`ShiroConfig.java`中设置匿名访问
filterChainDefinitionMap.put("/websocket/**", "anon");
1
2

3、下载插件相关包和代码实现覆盖到工程中

提示

插件相关包和代码实现ruoyi/集成websocket实现实时通信.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

4、测试验证

如果要测试验证可以把websocket.html内容复制到login.html,点击连接发送消息测试返回结果。

# 集成atomikos实现分布式事务

在一些复杂的应用开发中,一个应用可能会涉及到连接多个数据源,所谓多数据源这里就定义为至少连接两个及以上的数据库了。 对于这种多数据的应用中,数据源就是一种典型的分布式场景,因此系统在多个数据源间的数据操作必须做好事务控制。在SpringBoot的官网推荐我们使用Atomikos (opens new window)。 当然分布式事务的作用并不仅仅应用于多数据源。例如:在做数据插入的时候往一个kafka消息队列写消息,如果信息很重要同样需要保证分布式数据的一致性。

若依框架已经通过Druid实现了多数据源切换,但是Spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。所以我们需要Atomikos解决多数据源事务的一致性问题

1、ruoyi-framework/pom.xml文件添加atomikos依赖。

<!-- atomikos分布式事务 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
1
2
3
4
5

2、下载插件相关包和代码实现覆盖到工程中

提示

插件相关包和代码实现ruoyi/集成atomikos实现分布式事务.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

3、测试验证

加入多数据源,如果不会使用可以参考多数据源实现

对应需要操作多数据源方法加入@Transactional测试一致性,例如。

@Transactional
public void insert()
{
	SpringUtils.getAopProxy(this).insertA();
	SpringUtils.getAopProxy(this).insertB();
}

@DataSource(DataSourceType.MASTER)
public void insertA()
{
	return xxxxMapper.insertXxxx();
}

@DataSource(DataSourceType.SLAVE)
public void insertB()
{
	return xxxxMapper.insertXxxx();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

到此我们项目多个数据源的事务控制生效了

# 集成minio实现分布式文件存储

框架默认存储使用的本地磁盘,对于一些文件较大较多且有数据备份、数据安全、分布式等等就满足不了我们的要求,对于这种情况我们可以集成OSS对象存储服务。 minio是目前githubstar最多的数据存储框架。minio可以用来搭建分布式存储服务,可以很好的和机器学习相结合。

1、ruoyi-common/pom.xml文件添加minio依赖。

<!-- Minio 文件存储 -->
<dependency>
	<groupId>io.minio</groupId>
	<artifactId>minio</artifactId>
	<version>8.2.1</version>
</dependency>
1
2
3
4
5
6

2、ruoyi-admin文件application.yml,添加minio配置

# Minio配置
minio:
  url: http://localhost:9000
  accessKey: minioadmin
  secretKey: minioadmin
  bucketName: ruoyi
1
2
3
4
5
6

3、CommonController.java自定义Minio服务器上传请求

/**
 * 自定义 Minio 服务器上传请求
 */
@PostMapping("/uploadMinio")
@ResponseBody
public AjaxResult uploadFileMinio(MultipartFile file) throws Exception
{
	try
	{
		// 上传并返回新文件名称
		String fileName = FileUploadUtils.uploadMinio(file);
		AjaxResult ajax = AjaxResult.success();
		ajax.put("url", fileName);
		ajax.put("fileName", fileName);
		ajax.put("newFileName", FileUtils.getName(fileName));
		ajax.put("originalFilename", file.getOriginalFilename());
		return ajax;
	}
	catch (Exception e)
	{
		return AjaxResult.error(e.getMessage());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

4、下载插件相关包和代码实现覆盖到工程中

提示

插件相关包和代码实现ruoyi/集成minio实现分布式文件存储.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

5、测试验证文件存储的功能

代码测试可以将自己的FileUploadUtils.upload修改为FileUploadUtils.uploadMinio,返回值为文件的url路径。

页面测试可以在通知公告新增和修改页面将文件上传的路径common/upload修改为common/uploadMinio,然后上传图片测试验证结果。

# 使用undertow来替代tomcat容器

SpringBoot中我们既可以使用Tomcat作为Http服务,也可以用Undertow来代替。Undertow在高并发业务场景中,性能优于Tomcat。所以,如果我们的系统是高并发请求,不妨使用一下Undertow,你会发现你的系统性能会得到很大的提升。

1、ruoyi-framework\pom.xml模块修改web容器依赖,使用undertow来替代tomcat容器

 <!-- SpringBoot Web容器 -->
 <dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	 <exclusions>
		 <exclusion>
			 <artifactId>spring-boot-starter-tomcat</artifactId>
			 <groupId>org.springframework.boot</groupId>
		 </exclusion>
	 </exclusions>
</dependency>

<!-- web 容器使用 undertow -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

2、修改application.yml,使用undertow来替代tomcat容器

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为80
  port: 80
  servlet:
    # 应用的访问路径
    context-path: /
  # undertow 配置
  undertow:
    # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
    max-http-post-size: -1
    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
    # 每块buffer的空间大小,越小的空间被利用越充分
    buffer-size: 512
    # 是否分配的直接内存
    direct-buffers: true
    threads:
      # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
      io: 8
      # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
      worker: 256
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

3、修改文件上传工具类FileUploadUtils.java

private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
{
	File desc = new File(uploadDir + File.separator + fileName);

	if (!desc.getParentFile().exists())
	{
		desc.getParentFile().mkdirs();
	}
	// undertow文件上传,因底层实现不同,无需创建新文件
	// if (!desc.exists())
	// {
	//    desc.createNewFile();
	// }
	return desc;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 集成actuator实现优雅关闭应用

优雅停机主要应用在版本更新的时候,为了等待正在工作的线程全部执行完毕,然后再停止。我们可以使用SpringBoot提供的Actuator

1、pom.xml中引入actuator依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
3
4

2、配置文件中endpoint开启shutdown

management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "shutdown"
      base-path: /monitor
1
2
3
4
5
6
7
8
9

3、在ShiroConfig中设置filterChainDefinitionMap配置url=anon

filterChainDefinitionMap.put("/monitor/shutdown", "anon");
1

4、Post请求测试验证优雅停机 curl -X POST http://localhost:80/monitor/shutdown

# 集成aj-captcha实现滑块验证码

集成以AJ-Captcha滑块验证码为例,不需要键盘手动输入,极大优化了传统验证码用户体验不佳的问题。目前对外提供两种类型的验证码,其中包含滑动拼图、文字点选。

1、ruoyi-framework\pom.xml添加依赖

<!-- 滑块验证码  -->
<dependency>
	<groupId>com.github.anji-plus</groupId>
	<artifactId>captcha-spring-boot-starter</artifactId>
	<version>1.2.7</version>
</dependency>
1
2
3
4
5
6

2、修改application.yml,加入aj-captcha配置

# 滑块验证码
aj:
   captcha:
      # blockPuzzle滑块 clickWord文字点选  default默认两者都实例化
      type: blockPuzzle
      # 右下角显示字
      water-mark: ruoyi.vip
      # 校验滑动拼图允许误差偏移量(默认5像素)
      slip-offset: 5
      # aes加密坐标开启或者禁用(true|false)
      aes-status: true
      # 滑动干扰项(0/1/2)
      interference-options: 2
1
2
3
4
5
6
7
8
9
10
11
12
13

3、下载插件相关包和代码实现覆盖到工程中

提示

下载前端插件相关包和代码实现ruoyi/集成滑动验证码.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

4、测试验证登录和注册页面滑块验证使用是否正常。

# 集成sharding-jdbc实现分库分表

sharding-jdbc是由当当捐入给apache的一款分布式数据库中间件,支持垂直分库、垂直分表、水平分库、水平分表、读写分离、分布式事务和高可用等相关功能。

1、ruoyi-framework\pom.xml模块添加sharding-jdbc整合依赖

<!-- sharding-jdbc分库分表 -->
<dependency>
	<groupId>org.apache.shardingsphere</groupId>
	<artifactId>sharding-jdbc-core</artifactId>
	<version>4.1.1</version>
</dependency>
1
2
3
4
5
6

2、创建两个测试数据库

create database `ry-order1`;
create database `ry-order2`;
1
2

3、创建两个测试订单表

-- ----------------------------
-- 订单信息表sys_order_0
-- ----------------------------
drop table if exists sys_order_0;
create table sys_order_0
(
  order_id      bigint(20)      not null                   comment '订单ID',
  user_id       bigint(64)      not null                   comment '用户编号',
  status        char(1)         not null                   comment '状态(0交易成功 1交易失败)',
  order_no      varchar(64)     default null               comment '订单流水',
  primary key (order_id)
) engine=innodb comment = '订单信息表';

-- ----------------------------
-- 订单信息表sys_order_1
-- ----------------------------
drop table if exists sys_order_1;
create table sys_order_1
(
  order_id      bigint(20)      not null                   comment '订单ID',
  user_id       bigint(64)      not null                   comment '用户编号',
  status        char(1)         not null                   comment '状态(0交易成功 1交易失败)',
  order_no      varchar(64)     default null               comment '订单流水',
  primary key (order_id)
) engine=innodb comment = '订单信息表';

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

4、配置文件application-druid.yml添加测试数据源












 
 
 
 
 
 
 
 
 
 
 
 


# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: password
            # 订单库1
            order1:
                enabled: true
                url: jdbc:mysql://localhost:3306/ry-order1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: password
            # 订单库2
            order2:
                enabled: true
                url: jdbc:mysql://localhost:3306/ry-order2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: password
            ...................
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

5、下载插件相关包和代码实现覆盖到工程中

提示

下载插件相关包和代码实现ruoyi/集成sharding-jdbc实现分库分表.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

6、测试验证

访问http://localhost/order/add/1入库到ry-order2

访问http://localhost/order/add/2入库到ry-order1

同时根据订单号order_id % 2入库到sys_order_0或者sys_order_1

# 集成just-auth实现第三方授权登录

对于一些想使用第三方平台授权登录可以使用JustAuth,支持Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么和推特等第三方平台的授权登录。

1、ruoyi-common\pom.xml模块添加整合依赖

<!-- 第三方授权登录 -->
<dependency>
	<groupId>me.zhyd.oauth</groupId>
	<artifactId>JustAuth</artifactId>
	<version>1.15.6</version>
</dependency>

<!-- HttpClient -->
<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12

2、新建第三方登录授权表

-- ----------------------------
-- 第三方授权表
-- ----------------------------
drop table if exists sys_auth_user;
create table sys_auth_user (
  auth_id           bigint(20)      not null auto_increment    comment '授权ID',
  uuid              varchar(500)    not null                   comment '第三方平台用户唯一ID',
  user_id           bigint(20)      not null                   comment '系统用户ID',
  login_name        varchar(30)     not null                   comment '登录账号',
  user_name         varchar(30)     default ''                 comment '用户昵称',
  avatar            varchar(500)    default ''                 comment '头像地址',
  email             varchar(255)    default ''                 comment '用户邮箱',
  source            varchar(255)    default ''                 comment '用户来源',
  create_time       datetime                                   comment '创建时间',
  primary key (auth_id)
) engine=innodb auto_increment=100 comment = '第三方授权表';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

3、下载插件相关包和代码实现覆盖到工程中

提示

下载前端插件相关包和代码实现ruoyi/集成JustAuth实现第三方授权登录.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

4、测试登录页面第三方授权登录,个人中心授权及取消功能是否正常使用。

# 集成mybatis-plus实现mybatis增强

Mybatis-Plus是在Mybatis的基础上进行扩展,只做增强不做改变,可以兼容Mybatis原生的特性。同时支持通用CRUD操作、多种主键策略、分页、性能分析、全局拦截等。极大帮助我们简化开发工作。

1、ruoyi-common\pom.xml模块添加整合依赖

<!-- mybatis-plus 增强CRUD -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.5.1</version>
</dependency>
1
2
3
4
5
6

2、ruoyi-admin文件application.yml,修改mybatis配置为mybatis-plus

# MyBatis Plus配置
mybatis-plus:
  # 搜索指定包别名
  typeAliasesPackage: com.ruoyi.**.domain
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml
1
2
3
4
5
6
7
8

3、添加Mybatis Plus配置MybatisPlusConfig.javaPS:原来的MyBatisConfig.java需要删除掉

package com.ruoyi.framework.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * Mybatis Plus 配置
 * 
 * @author ruoyi
 */
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig
{
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor()
    {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        // 阻断插件
        interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
        return interceptor;
    }

    /**
     * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
     */
    public PaginationInnerInterceptor paginationInnerInterceptor()
    {
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        // 设置数据库类型为mysql
        paginationInnerInterceptor.setDbType(DbType.MYSQL);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInnerInterceptor.setMaxLimit(-1L);
        return paginationInnerInterceptor;
    }

    /**
     * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html
     */
    public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor()
    {
        return new OptimisticLockerInnerInterceptor();
    }

    /**
     * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html
     */
    public BlockAttackInnerInterceptor blockAttackInnerInterceptor()
    {
        return new BlockAttackInnerInterceptor();
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

4、添加测试表和菜单信息

drop table if exists sys_student;
create table sys_student (
  student_id           int(11)         auto_increment    comment '编号',
  student_name         varchar(30)     default ''        comment '学生名称',
  student_age          int(3)          default null      comment '年龄',
  student_hobby        varchar(30)     default ''        comment '爱好(0代码 1音乐 2电影)',
  student_sex          char(1)         default '0'       comment '性别(0男 1女 2未知)',
  student_status       char(1)         default '0'       comment '状态(0正常 1停用)',
  student_birthday     datetime                          comment '生日',
  primary key (student_id)
) engine=innodb auto_increment=1 comment = '学生信息表';

-- 菜单 sql
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息', '3', '1', '/system/student', 'c', '0', 'system:student:view', '#', 'admin', sysdate(), '', null, '学生信息菜单');

-- 按钮父菜单id
select @parentid := last_insert_id();

-- 按钮 sql
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息查询', @parentid, '1',  '#',  'f', '0', 'system:student:list',         '#', 'admin', sysdate(), '', null, '');

insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息新增', @parentid, '2',  '#',  'f', '0', 'system:student:add',          '#', 'admin', sysdate(), '', null, '');

insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息修改', @parentid, '3',  '#',  'f', '0', 'system:student:edit',         '#', 'admin', sysdate(), '', null, '');

insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息删除', @parentid, '4',  '#',  'f', '0', 'system:student:remove',       '#', 'admin', sysdate(), '', null, '');

insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息导出', @parentid, '5',  '#',  'f', '0', 'system:student:export',       '#', 'admin', sysdate(), '', null, '');
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

5、新增测试代码验证 新增 ruoyi-system\com\ruoyi\system\controller\SysStudentController.java

package com.ruoyi.system.controller;

import java.util.Arrays;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.SysStudent;
import com.ruoyi.system.service.ISysStudentService;

/**
 * 学生信息Controller
 * 
 * @author ruoyi
 */
@Controller
@RequestMapping("/system/student")
public class SysStudentController extends BaseController
{
    private String prefix = "system/student";

    @Autowired
    private ISysStudentService sysStudentService;

    @RequiresPermissions("system:student:view")
    @GetMapping()
    public String student()
    {
        return prefix + "/student";
    }

    /**
     * 查询学生信息列表
     */
    @RequiresPermissions("system:student:list")
    @PostMapping("/list")
    @ResponseBody
    public TableDataInfo list(SysStudent sysStudent)
    {
        startPage();
        List<SysStudent> list = sysStudentService.queryList(sysStudent);
        return getDataTable(list);
    }

    /**
     * 导出学生信息列表
     */
    @RequiresPermissions("system:student:export")
    @Log(title = "学生信息", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    @ResponseBody
    public AjaxResult export(SysStudent sysStudent)
    {
        List<SysStudent> list = sysStudentService.queryList(sysStudent);
        ExcelUtil<SysStudent> util = new ExcelUtil<SysStudent>(SysStudent.class);
        return util.exportExcel(list, "student");
    }

    /**
     * 新增学生信息
     */
    @GetMapping("/add")
    public String add()
    {
        return prefix + "/add";
    }

    /**
     * 新增保存学生信息
     */
    @RequiresPermissions("system:student:add")
    @Log(title = "学生信息", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @ResponseBody
    public AjaxResult addSave(SysStudent sysStudent)
    {
        return toAjax(sysStudentService.save(sysStudent));
    }

    /**
     * 修改学生信息
     */
    @GetMapping("/edit/{studentId}")
    public String edit(@PathVariable("studentId") Long studentId, ModelMap mmap)
    {
        SysStudent sysStudent = sysStudentService.getById(studentId);
        mmap.put("sysStudent", sysStudent);
        return prefix + "/edit";
    }

    /**
     * 修改保存学生信息
     */
    @RequiresPermissions("system:student:edit")
    @Log(title = "学生信息", businessType = BusinessType.UPDATE)
    @PostMapping("/edit")
    @ResponseBody
    public AjaxResult editSave(SysStudent sysStudent)
    {
        return toAjax(sysStudentService.updateById(sysStudent));
    }

    /**
     * 删除学生信息
     */
    @RequiresPermissions("system:student:remove")
    @Log(title = "学生信息", businessType = BusinessType.DELETE)
    @PostMapping("/remove")
    @ResponseBody
    public AjaxResult remove(String ids)
    {
        return toAjax(sysStudentService.removeByIds(Arrays.asList(ids)));
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

新增 ruoyi-system\com\ruoyi\system\domain\SysStudent.java

package com.ruoyi.system.domain;

import java.io.Serializable;
import java.util.Date;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;

/**
 * 学生信息对象 sys_student
 * 
 * @author ruoyi
 */
@TableName(value = "sys_student")
public class SysStudent implements Serializable
{
    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

    /** 编号 */
    @TableId(type = IdType.AUTO)
    private Long studentId;

    /** 学生名称 */
    @Excel(name = "学生名称")
    private String studentName;

    /** 年龄 */
    @Excel(name = "年龄")
    private Integer studentAge;

    /** 爱好(0代码 1音乐 2电影) */
    @Excel(name = "爱好", readConverterExp = "0=代码,1=音乐,2=电影")
    private String studentHobby;

    /** 性别(0男 1女 2未知) */
    @Excel(name = "性别", readConverterExp = "0=男,1=女,2=未知")
    private String studentSex;

    /** 状态(0正常 1停用) */
    @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
    private String studentStatus;

    /** 生日 */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "生日", width = 30, dateFormat = "yyyy-MM-dd")
    private Date studentBirthday;

    public void setStudentId(Long studentId) 
    {
        this.studentId = studentId;
    }

    public Long getStudentId() 
    {
        return studentId;
    }
    public void setStudentName(String studentName) 
    {
        this.studentName = studentName;
    }

    public String getStudentName() 
    {
        return studentName;
    }
    public void setStudentAge(Integer studentAge) 
    {
        this.studentAge = studentAge;
    }

    public Integer getStudentAge() 
    {
        return studentAge;
    }
    public void setStudentHobby(String studentHobby) 
    {
        this.studentHobby = studentHobby;
    }

    public String getStudentHobby() 
    {
        return studentHobby;
    }
    public void setStudentSex(String studentSex) 
    {
        this.studentSex = studentSex;
    }

    public String getStudentSex() 
    {
        return studentSex;
    }
    public void setStudentStatus(String studentStatus) 
    {
        this.studentStatus = studentStatus;
    }

    public String getStudentStatus() 
    {
        return studentStatus;
    }
    public void setStudentBirthday(Date studentBirthday) 
    {
        this.studentBirthday = studentBirthday;
    }

    public Date getStudentBirthday() 
    {
        return studentBirthday;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("studentId", getStudentId())
            .append("studentName", getStudentName())
            .append("studentAge", getStudentAge())
            .append("studentHobby", getStudentHobby())
            .append("studentSex", getStudentSex())
            .append("studentStatus", getStudentStatus())
            .append("studentBirthday", getStudentBirthday())
            .toString();
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

新增 ruoyi-system\com\ruoyi\system\mapper\SysStudentMapper.java

package com.ruoyi.system.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.domain.SysStudent;

/**
 * 学生信息Mapper接口
 * 
 * @author ruoyi
 */
public interface SysStudentMapper extends BaseMapper<SysStudent>
{

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

新增 ruoyi-system\com\ruoyi\system\service\ISysStudentService.java

package com.ruoyi.system.service;

import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.system.domain.SysStudent;

/**
 * 学生信息Service接口
 * 
 * @author ruoyi
 */
public interface ISysStudentService extends IService<SysStudent>
{
    /**
     * 查询学生信息列表
     * 
     * @param sysStudent 学生信息
     * @return 学生信息集合
     */
    public List<SysStudent> queryList(SysStudent sysStudent);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

新增 ruoyi-system\com\ruoyi\system\service\impl\SysStudentServiceImpl.java

package com.ruoyi.system.service.impl;

import java.util.List;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysStudent;
import com.ruoyi.system.mapper.SysStudentMapper;
import com.ruoyi.system.service.ISysStudentService;

/**
 * 学生信息Service业务层处理
 * 
 * @author ruoyi
 */
@Service
public class SysStudentServiceImpl extends ServiceImpl<SysStudentMapper, SysStudent> implements ISysStudentService
{
    @Override
    public List<SysStudent> queryList(SysStudent sysStudent)
    {
        // 注意:mybatis-plus lambda 模式不支持 eclipse 的编译器
        // LambdaQueryWrapper<SysStudent> queryWrapper = Wrappers.lambdaQuery();
        // queryWrapper.eq(SysStudent::getStudentName, sysStudent.getStudentName());
        QueryWrapper<SysStudent> queryWrapper = Wrappers.query();
        if (StringUtils.isNotEmpty(sysStudent.getStudentName()))
        {
            queryWrapper.eq("student_name", sysStudent.getStudentName());
        }
        if (StringUtils.isNotNull(sysStudent.getStudentAge()))
        {
            queryWrapper.eq("student_age", sysStudent.getStudentAge());
        }
        if (StringUtils.isNotEmpty(sysStudent.getStudentHobby()))
        {
            queryWrapper.eq("student_hobby", sysStudent.getStudentHobby());
        }
        return this.list(queryWrapper);
    }
}
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
38
39
40
41
42

新增 ruoyi-system\templates\system\student\add.html

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
    <th:block th:include="include :: header('新增学生信息')" />
    <th:block th:include="include :: datetimepicker-css" />
</head>
<body class="white-bg">
    <div class="wrapper wrapper-content animated fadeInRight ibox-content">
        <form class="form-horizontal m" id="form-student-add">
            <div class="form-group">    
                <label class="col-sm-3 control-label">学生名称:</label>
                <div class="col-sm-8">
                    <input name="studentName" class="form-control" type="text">
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">年龄:</label>
                <div class="col-sm-8">
                    <input name="studentAge" class="form-control" type="text">
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">爱好:</label>
                <div class="col-sm-8">
                    <input name="studentHobby" class="form-control" type="text">
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">性别:</label>
                <div class="col-sm-8">
                    <select name="studentSex" class="form-control m-b">
                        <option value="">所有</option>
                    </select>
                    <span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">状态:</label>
                <div class="col-sm-8">
                    <div class="radio-box">
                        <input type="radio" name="studentStatus" value="">
                        <label th:for="studentStatus" th:text="未知"></label>
                    </div>
                    <span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">生日:</label>
                <div class="col-sm-8">
                    <div class="input-group date">
                        <input name="studentBirthday" class="form-control" placeholder="yyyy-MM-dd" type="text">
                        <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
                    </div>
                </div>
            </div>
        </form>
    </div>
    <th:block th:include="include :: footer" />
    <th:block th:include="include :: datetimepicker-js" />
    <script th:inline="javascript">
        var prefix = ctx + "system/student"
        $("#form-student-add").validate({
            focusCleanup: true
        });

        function submitHandler() {
            if ($.validate.form()) {
                $.operate.save(prefix + "/add", $('#form-student-add').serialize());
            }
        }

        $("input[name='studentBirthday']").datetimepicker({
            format: "yyyy-mm-dd",
            minView: "month",
            autoclose: true
        });
    </script>
</body>
</html>
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

新增 ruoyi-system\templates\system\student\edit.html

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
    <th:block th:include="include :: header('修改学生信息')" />
    <th:block th:include="include :: datetimepicker-css" />
</head>
<body class="white-bg">
    <div class="wrapper wrapper-content animated fadeInRight ibox-content">
        <form class="form-horizontal m" id="form-student-edit" th:object="${sysStudent}">
            <input name="studentId" th:field="*{studentId}" type="hidden">
            <div class="form-group">    
                <label class="col-sm-3 control-label">学生名称:</label>
                <div class="col-sm-8">
                    <input name="studentName" th:field="*{studentName}" class="form-control" type="text">
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">年龄:</label>
                <div class="col-sm-8">
                    <input name="studentAge" th:field="*{studentAge}" class="form-control" type="text">
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">爱好:</label>
                <div class="col-sm-8">
                    <input name="studentHobby" th:field="*{studentHobby}" class="form-control" type="text">
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">性别:</label>
                <div class="col-sm-8">
                    <select name="studentSex" class="form-control m-b">
                        <option value="">所有</option>
                    </select>
                    <span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">状态:</label>
                <div class="col-sm-8">
                    <div class="radio-box">
                        <input type="radio" name="studentStatus" value="">
                        <label th:for="studentStatus" th:text="未知"></label>
                    </div>
                    <span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
                </div>
            </div>
            <div class="form-group">    
                <label class="col-sm-3 control-label">生日:</label>
                <div class="col-sm-8">
                    <div class="input-group date">
                        <input name="studentBirthday" th:value="${#dates.format(sysStudent.studentBirthday, 'yyyy-MM-dd')}" class="form-control" placeholder="yyyy-MM-dd" type="text">
                        <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
                    </div>
                </div>
            </div>
        </form>
    </div>
    <th:block th:include="include :: footer" />
    <th:block th:include="include :: datetimepicker-js" />
    <script th:inline="javascript">
        var prefix = ctx + "system/student";
        $("#form-student-edit").validate({
            focusCleanup: true
        });

        function submitHandler() {
            if ($.validate.form()) {
                $.operate.save(prefix + "/edit", $('#form-student-edit').serialize());
            }
        }

        $("input[name='studentBirthday']").datetimepicker({
            format: "yyyy-mm-dd",
            minView: "month",
            autoclose: true
        });
    </script>
</body>
</html>
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

新增 ruoyi-system\templates\system\student\student.html

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <th:block th:include="include :: header('学生信息列表')" />
</head>
<body class="gray-bg">
     <div class="container-div">
        <div class="row">
            <div class="col-sm-12 search-collapse">
                <form id="formId">
                    <div class="select-list">
                        <ul>
                            <li>
                                <label>学生名称:</label>
                                <input type="text" name="studentName"/>
                            </li>
                            <li>
                                <label>年龄:</label>
                                <input type="text" name="studentAge"/>
                            </li>
                            <li>
                                <label>爱好:</label>
                                <input type="text" name="studentHobby"/>
                            </li>
                            <li>
                                <label>性别:</label>
                                <select name="studentSex">
                                    <option value="">所有</option>
                                    <option value="-1">代码生成请选择字典属性</option>
                                </select>
                            </li>
                            <li>
                                <label>状态:</label>
                                <select name="studentStatus">
                                    <option value="">所有</option>
                                    <option value="-1">代码生成请选择字典属性</option>
                                </select>
                            </li>
                            <li>
                                <label>生日:</label>
                                <input type="text" class="time-input" placeholder="请选择生日" name="studentBirthday"/>
                            </li>
                            <li>
                                <a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i>&nbsp;搜索</a>
                                <a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i>&nbsp;重置</a>
                            </li>
                        </ul>
                    </div>
                </form>
            </div>

            <div class="btn-group-sm" id="toolbar" role="group">
                <a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="system:student:add">
                    <i class="fa fa-plus"></i> 添加
                </a>
                <a class="btn btn-primary single disabled" onclick="$.operate.edit()" shiro:hasPermission="system:student:edit">
                    <i class="fa fa-edit"></i> 修改
                </a>
                <a class="btn btn-danger multiple disabled" onclick="$.operate.removeAll()" shiro:hasPermission="system:student:remove">
                    <i class="fa fa-remove"></i> 删除
                </a>
                <a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="system:student:export">
                    <i class="fa fa-download"></i> 导出
                </a>
            </div>
            <div class="col-sm-12 select-table table-striped">
                <table id="bootstrap-table"></table>
            </div>
        </div>
    </div>
    <th:block th:include="include :: footer" />
    <script th:inline="javascript">
        var editFlag = [[${@permission.hasPermi('system:student:edit')}]];
        var removeFlag = [[${@permission.hasPermi('system:student:remove')}]];
        var prefix = ctx + "system/student";

        $(function() {
            var options = {
                url: prefix + "/list",
                createUrl: prefix + "/add",
                updateUrl: prefix + "/edit/{id}",
                removeUrl: prefix + "/remove",
                exportUrl: prefix + "/export",
                modalName: "学生信息",
                columns: [{
                    checkbox: true
                },
                {
                    field: 'studentId',
                    title: '编号',
                    visible: false
                },
                {
                    field: 'studentName',
                    title: '学生名称'
                },
                {
                    field: 'studentAge',
                    title: '年龄'
                },
                {
                    field: 'studentHobby',
                    title: '爱好'
                },
                {
                    field: 'studentSex',
                    title: '性别'
                },
                {
                    field: 'studentStatus',
                    title: '状态'
                },
                {
                    field: 'studentBirthday',
                    title: '生日'
                },
                {
                    title: '操作',
                    align: 'center',
                    formatter: function(value, row, index) {
                        var actions = [];
                        actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.studentId + '\')"><i class="fa fa-edit"></i>编辑</a> ');
                        actions.push('<a class="btn btn-danger btn-xs ' + removeFlag + '" href="javascript:void(0)" onclick="$.operate.remove(\'' + row.studentId + '\')"><i class="fa fa-remove"></i>删除</a>');
                        return actions.join('');
                    }
                }]
            };
            $.table.init(options);
        });
    </script>
</body>
</html>
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

6、登录系统测试学生菜单增删改查功能。

提示

下载相关代码实现示例 ruoyi/集成mybatisplus实现mybatis增强.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

# 集成easyexcel实现excel表格增强

如果默认的excel注解已经满足不了你的需求,可以使用excel的增强解决方案easyexcel,它是阿里巴巴开源的一个excel处理框架,使用简单、功能特性多、以节省内存著称。

1、ruoyi-common\pom.xml模块添加整合依赖

<!-- easyexcel -->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>easyexcel</artifactId>
	<version>2.2.6</version>
</dependency>
1
2
3
4
5
6

2、ExcelUtil.java新增easyexcel导出导入方法

import com.alibaba.excel.EasyExcel;

/**
 * 对excel表单默认第一个索引名转换成list(EasyExcel)
 * 
 * @param is 输入流
 * @return 转换后集合
 */
public List<T> importEasyExcel(InputStream is) throws Exception
{
	return EasyExcel.read(is).head(clazz).sheet().doReadSync();
}

/**
 * 对list数据源将其里面的数据导入到excel表单(EasyExcel)
 * 
 * @param list 导出数据集合
 * @param sheetName 工作表的名称
 * @return 结果
 */
public AjaxResult exportEasyExcel(List<T> list, String sheetName)
{
	String filename = encodingFilename(sheetName);
	EasyExcel.write(getAbsoluteFile(filename), clazz).sheet(sheetName).doWrite(list);
	return AjaxResult.success(filename);
}
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

3、模拟测试,以操作日志为例,修改相关类。

SysOperlogController.java改为exportEasyExcel









 


@Log(title = "操作日志", businessType = BusinessType.EXPORT)
@RequiresPermissions("monitor:operlog:export")
@PostMapping("/export")
@ResponseBody
public AjaxResult export(SysOperLog operLog)
{
	List<SysOperLog> list = operLogService.selectOperLogList(operLog);
	ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
	return util.exportEasyExcel(list, "操作日志");
}
1
2
3
4
5
6
7
8
9
10

SysOperLog.java修改为@ExcelProperty注解

package com.ruoyi.system.domain;

import java.util.Date;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.HeadFontStyle;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.system.domain.read.BusiTypeStringNumberConverter;
import com.ruoyi.system.domain.read.OperTypeConverter;
import com.ruoyi.system.domain.read.StatusConverter;

/**
 * 操作日志记录表 oper_log
 * 
 * @author ruoyi
 */
@ExcelIgnoreUnannotated
@ColumnWidth(16)
@HeadRowHeight(14)
@HeadFontStyle(fontHeightInPoints = 11)
public class SysOperLog extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 日志主键 */
    @ExcelProperty(value = "操作序号")
    private Long operId;

    /** 操作模块 */
    @ExcelProperty(value = "操作模块")
    private String title;

    /** 业务类型(0其它 1新增 2修改 3删除) */
    @ExcelProperty(value = "业务类型", converter = BusiTypeStringNumberConverter.class)
    private Integer businessType;

    /** 业务类型数组 */
    private Integer[] businessTypes;

    /** 请求方法 */
    @ExcelProperty(value = "请求方法")
    private String method;

    /** 请求方式 */
    @ExcelProperty(value = "请求方式")
    private String requestMethod;

    /** 操作类别(0其它 1后台用户 2手机端用户) */
    @ExcelProperty(value = "操作类别", converter = OperTypeConverter.class)
    private Integer operatorType;

    /** 操作人员 */
    @ExcelProperty(value = "操作人员")
    private String operName;

    /** 部门名称 */
    @ExcelProperty(value = "部门名称")
    private String deptName;

    /** 请求url */
    @ExcelProperty(value = "请求地址")
    private String operUrl;

    /** 操作地址 */
    @ExcelProperty(value = "操作地址")
    private String operIp;

    /** 操作地点 */
    @ExcelProperty(value = "操作地点")
    private String operLocation;

    /** 请求参数 */
    @ExcelProperty(value = "请求参数")
    private String operParam;

    /** 返回参数 */
    @ExcelProperty(value = "返回参数")
    private String jsonResult;

    /** 操作状态(0正常 1异常) */
    @ExcelProperty(value = "状态", converter = StatusConverter.class)
    private Integer status;

    /** 错误消息 */
    @ExcelProperty(value = "错误消息")
    private String errorMsg;

    /** 操作时间 */
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    @ExcelProperty(value = "操作时间")
    private Date operTime;

    public Long getOperId()
    {
        return operId;
    }

    public void setOperId(Long operId)
    {
        this.operId = operId;
    }

    public String getTitle()
    {
        return title;
    }

    public void setTitle(String title)
    {
        this.title = title;
    }

    public Integer getBusinessType()
    {
        return businessType;
    }

    public void setBusinessType(Integer businessType)
    {
        this.businessType = businessType;
    }

    public Integer[] getBusinessTypes()
    {
        return businessTypes;
    }

    public void setBusinessTypes(Integer[] businessTypes)
    {
        this.businessTypes = businessTypes;
    }

    public String getMethod()
    {
        return method;
    }

    public void setMethod(String method)
    {
        this.method = method;
    }

    public String getRequestMethod()
    {
        return requestMethod;
    }

    public void setRequestMethod(String requestMethod)
    {
        this.requestMethod = requestMethod;
    }

    public Integer getOperatorType()
    {
        return operatorType;
    }

    public void setOperatorType(Integer operatorType)
    {
        this.operatorType = operatorType;
    }

    public String getOperName()
    {
        return operName;
    }

    public void setOperName(String operName)
    {
        this.operName = operName;
    }

    public String getDeptName()
    {
        return deptName;
    }

    public void setDeptName(String deptName)
    {
        this.deptName = deptName;
    }

    public String getOperUrl()
    {
        return operUrl;
    }

    public void setOperUrl(String operUrl)
    {
        this.operUrl = operUrl;
    }

    public String getOperIp()
    {
        return operIp;
    }

    public void setOperIp(String operIp)
    {
        this.operIp = operIp;
    }

    public String getOperLocation()
    {
        return operLocation;
    }

    public void setOperLocation(String operLocation)
    {
        this.operLocation = operLocation;
    }

    public String getOperParam()
    {
        return operParam;
    }

    public void setOperParam(String operParam)
    {
        this.operParam = operParam;
    }

    public String getJsonResult()
    {
        return jsonResult;
    }

    public void setJsonResult(String jsonResult)
    {
        this.jsonResult = jsonResult;
    }

    public Integer getStatus()
    {
        return status;
    }

    public void setStatus(Integer status)
    {
        this.status = status;
    }

    public String getErrorMsg()
    {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg)
    {
        this.errorMsg = errorMsg;
    }

    public Date getOperTime()
    {
        return operTime;
    }

    public void setOperTime(Date operTime)
    {
        this.operTime = operTime;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("operId", getOperId())
            .append("title", getTitle())
            .append("businessType", getBusinessType())
            .append("businessTypes", getBusinessTypes())
            .append("method", getMethod())
            .append("requestMethod", getRequestMethod())
            .append("operatorType", getOperatorType())
            .append("operName", getOperName())
            .append("deptName", getDeptName())
            .append("operUrl", getOperUrl())
            .append("operIp", getOperIp())
            .append("operLocation", getOperLocation())
            .append("operParam", getOperParam())
            .append("status", getStatus())
            .append("errorMsg", getErrorMsg())
            .append("operTime", getOperTime())
            .toString();
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289

添加字符串翻译内容

ruoyi-system\com\ruoyi\system\domain\read\BusiTypeStringNumberConverter.java

package com.ruoyi.system.domain.read;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

/**
 * 业务类型字符串处理
 *
 * @author ruoyi
 */
@SuppressWarnings("rawtypes")
public class BusiTypeStringNumberConverter implements Converter<Integer>
{
    @Override
    public Class supportJavaTypeKey()
    {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey()
    {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
            GlobalConfiguration globalConfiguration)
    {
        Integer value = 0;
        String str = cellData.getStringValue();
        if ("新增".equals(str))
        {
            value = 1;
        }
        else if ("修改".equals(str))
        {
            value = 2;
        }
        else if ("删除".equals(str))
        {
            value = 3;
        }
        else if ("授权".equals(str))
        {
            value = 4;
        }
        else if ("导出".equals(str))
        {
            value = 5;
        }
        else if ("导入".equals(str))
        {
            value = 6;
        }
        else if ("强退".equals(str))
        {
            value = 7;
        }
        else if ("生成代码".equals(str))
        {
            value = 8;
        }
        else if ("清空数据".equals(str))
        {
            value = 9;
        }
        return value;
    }

    @Override
    public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty,
            GlobalConfiguration globalConfiguration)
    {
        String str = "其他";
        if (1 == value)
        {
            str = "新增";
        }
        else if (2 == value)
        {
            str = "修改";
        }
        else if (3 == value)
        {
            str = "删除";
        }
        else if (4 == value)
        {
            str = "授权";
        }
        else if (5 == value)
        {
            str = "导出";
        }
        else if (6 == value)
        {
            str = "导入";
        }
        else if (7 == value)
        {
            str = "强退";
        }
        else if (8 == value)
        {
            str = "生成代码";
        }
        else if (9 == value)
        {
            str = "清空数据";
        }
        return new CellData(str);
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

ruoyi-system\com\ruoyi\system\domain\read\OperTypeConverter.java

package com.ruoyi.system.domain.read;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

/**
 * 操作类别字符串处理
 *
 * @author ruoyi
 */
@SuppressWarnings("rawtypes")
public class OperTypeConverter implements Converter<Integer>
{
    @Override
    public Class supportJavaTypeKey()
    {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey()
    {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
            GlobalConfiguration globalConfiguration)
    {
        Integer value = 0;
        String str = cellData.getStringValue();
        if ("后台用户".equals(str))
        {
            value = 1;
        }
        else if ("手机端用户".equals(str))
        {
            value = 2;
        }
        return value;
    }

    @Override
    public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty,
            GlobalConfiguration globalConfiguration)
    {
        String str = "其他";
        if (1 == value)
        {
            str = "后台用户";
        }
        else if (2 == value)
        {
            str = "手机端用户";
        }
        return new CellData(str);
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

ruoyi-system\com\ruoyi\system\domain\read\StatusConverter.java

package com.ruoyi.system.domain.read;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

/**
 * 状态字符串处理
 *
 * @author ruoyi
 */
@SuppressWarnings("rawtypes")
public class StatusConverter implements Converter<Integer>
{
    @Override
    public Class supportJavaTypeKey()
    {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey()
    {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
            GlobalConfiguration globalConfiguration)
    {
        return "正常".equals(cellData.getStringValue()) ? 0 : 1;
    }

    @Override
    public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty,
            GlobalConfiguration globalConfiguration)
    {
        return new CellData(0 == value ? "正常" : "异常");
    }
}
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
38
39
40
41
42

4、登录系统,进入系统管理-日志管理-操作日志-执行导出功能

# 集成knife4j实现swagger文档增强

如果不习惯使用swagger可以使用前端UI的增强解决方案knife4j,对比swagger相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。

1、ruoyi-admin\pom.xml模块添加整合依赖

<!-- knife4j -->
<dependency>
	<groupId>com.github.xiaoymin</groupId>
	<artifactId>knife4j-spring-boot-starter</artifactId>
	<version>3.0.3</version>
</dependency>
1
2
3
4
5
6

2、SwaggerController.java修改跳转访问地址

// 默认swagger-ui.html前端ui访问地址
public String index()
{
	return redirect("/swagger-ui.html");
}
// 修改成knife4j前端ui访问地址doc.html
public String index()
{
	return redirect("/doc.html");
}
1
2
3
4
5
6
7
8
9
10

3、登录系统,访问菜单系统工具/系统接口,出现如下图表示成功。

knife4j

提示

引用knife4j-spring-boot-starter依赖,项目中的swagger依赖可以删除。

# 集成ueditor实现富文本编辑器增强

UEditor是由百度前端研发部开发所见即所得富文本web编辑器,具有轻量、可定制、注重用户体验等特点。可以很好的满足国内用户的需求。

1、下载UEditor前端插件

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

ruoyi/集成ueditor实现富文本编辑器增强.zip

ruoyi-admin\src\main\resources\static\ajax\libs\ueditor 复制插件文件到自己的项目

2、ruoyi-admin\include.html添加ueditor

<!-- ueditor富文本编辑器插件 -->
<div th:fragment="ueditor-js">
	<script th:src="@{/ajax/libs/ueditor/ueditor.config.js}"></script>
	<script th:src="@{/ajax/libs/ueditor/ueditor.all.min.js}"></script>
	<script th:src="@{/ajax/libs/ueditor/lang/zh-cn/zh-cn.js}"></script>
</div>
1
2
3
4
5
6

3、修改通知公告相关页面

修改 templates\system\notice\add.html

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
	<th:block th:include="include :: header('新增通知公告')" />
</head>
<body class="white-bg">
    <div class="wrapper wrapper-content animated fadeInRight ibox-content">
        <form class="form-horizontal m" id="form-notice-add">
			<div class="form-group">	
				<label class="col-sm-2 control-label is-required">公告标题:</label>
				<div class="col-sm-10">
					<input id="noticeTitle" name="noticeTitle" class="form-control" type="text" required>
				</div>
			</div>
			<div class="form-group">
				<label class="col-sm-2 control-label">公告类型:</label>
				<div class="col-sm-10">
					<select name="noticeType" class="form-control m-b" th:with="type=${@dict.getType('sys_notice_type')}">
	                    <option th:each="dict : ${type}" th:text="${dict.dictLabel}" th:value="${dict.dictValue}"></option>
	                </select>
				</div>
			</div>
			<div class="form-group">	
				<label class="col-sm-2 control-label">公告内容:</label>
				<div class="col-sm-10">
				    <script id="editor" name="noticeContent" type="text/plain" style="height: 300px;"></script>
				</div>
			</div>
			<div class="form-group">
				<label class="col-sm-2 control-label">公告状态:</label>
				<div class="col-sm-10">
				    <div class="radio-box" th:each="dict : ${@dict.getType('sys_notice_status')}">
						<input type="radio" th:id="${dict.dictCode}" name="status" th:value="${dict.dictValue}" th:checked="${dict.default}">
						<label th:for="${dict.dictCode}" th:text="${dict.dictLabel}"></label>
					</div>
				</div>
			</div>
		</form>
	</div>
    <th:block th:include="include :: footer" />
    <th:block th:include="include :: ueditor-js" />
    <script type="text/javascript">
        var prefix = ctx + "system/notice";
        
        var ue = UE.getEditor('editor');

        function getContentTxt() {
            return UE.getEditor('editor').getContentTxt();
        }
        
		$("#form-notice-add").validate({
			focusCleanup: true
		});
		
		function submitHandler() {
	        if ($.validate.form()) {
	        	var text = getContentTxt();
	            if (text == '' || text.length == 0) {
	                $.modal.alertWarning("请输入公告内容!");
	                return;
	            }
				$.operate.save(prefix + "/add", $('#form-notice-add').serialize());
	        }
	    }
	</script>
</body>
</html>
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

修改 templates\system\notice\edit.html

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
	<th:block th:include="include :: header('修改通知公告')" />
</head>
<body class="white-bg">
    <div class="wrapper wrapper-content animated fadeInRight ibox-content">
        <form class="form-horizontal m" id="form-notice-edit" th:object="${notice}">
            <input id="noticeId" name="noticeId" th:field="*{noticeId}"  type="hidden">
            <div class="form-group">	
                <label class="col-sm-2 control-label is-required">公告标题:</label>
                <div class="col-sm-10">
                    <input id="noticeTitle" name="noticeTitle" th:field="*{noticeTitle}" class="form-control" type="text" required>
                </div>
            </div>
            <div class="form-group">
				<label class="col-sm-2 control-label">公告类型:</label>
				<div class="col-sm-10">
					<select name="noticeType" class="form-control m-b" th:with="type=${@dict.getType('sys_notice_type')}">
	                    <option th:each="dict : ${type}" th:text="${dict.dictLabel}" th:value="${dict.dictValue}" th:field="*{noticeType}"></option>
	                </select>
				</div>
			</div>
            <div class="form-group">	
                <label class="col-sm-2 control-label">公告内容:</label>
                <div class="col-sm-10">
                    <script id="editor" name="noticeContent" type="text/plain" style="height: 300px;"></script>
                    <textarea id="noticeContent" style="display: none;">[[*{noticeContent}]]</textarea>
                </div>
            </div>
            <div class="form-group">
				<label class="col-sm-2 control-label">公告状态:</label>
				<div class="col-sm-10">
					<div class="radio-box" th:each="dict : ${@dict.getType('sys_notice_status')}">
						<input type="radio" th:id="${dict.dictCode}" name="status" th:value="${dict.dictValue}" th:field="*{status}">
						<label th:for="${dict.dictCode}" th:text="${dict.dictLabel}"></label>
					</div>
				</div>
			</div>
		</form>
    </div>
    <th:block th:include="include :: footer" />
    <th:block th:include="include :: ueditor-js" />
    <script type="text/javascript">
        var prefix = ctx + "system/notice";
        
        $(function () {
            var text = $("#noticeContent").text();
            var ue = UE.getEditor('editor');
            ue.ready(function () {
                ue.setContent(text);
            });
        })

        function getContentTxt() {
            return UE.getEditor('editor').getContentTxt();
        }
	    
		$("#form-notice-edit").validate({
			focusCleanup: true
		});
		
		function submitHandler() {
	        if ($.validate.form()) {
	        	var text = getContentTxt();
	            if (text == '' || text.length == 0) {
	                $.modal.alertWarning("请输入通知内容!");
	                return;
	            }
				$.operate.save(prefix + "/edit", $('#form-notice-edit').serialize());
	        }
	    }
	</script>
</body>
</html>
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

4、添加配置文件到ruoyi-admin\src\main\resources

新增 ueditor-config.json

/* 前后端通信相关的配置,注释只允许使用多行方式 */
{
    /* 上传图片配置项 */
    "imageActionName": "uploadimage", /* 执行上传图片的action名称 */
    "imageFieldName": "upfile", /* 提交的图片表单名称 */
    "imageMaxSize": 2048000, /* 上传大小限制,单位B */
    "imageAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 上传图片格式显示 */
    "imageCompressEnable": true, /* 是否压缩图片,默认是true */
    "imageCompressBorder": 1600, /* 图片压缩最长边限制 */
    "imageInsertAlign": "none", /* 插入的图片浮动方式 */
    "imageUrlPrefix": "", /* 图片访问路径前缀 */
    "imagePathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
                                /* {filename} 会替换成原文件名,配置这项需要注意中文乱码问题 */
                                /* {rand:6} 会替换成随机数,后面的数字是随机数的位数 */
                                /* {time} 会替换成时间戳 */
                                /* {yyyy} 会替换成四位年份 */
                                /* {yy} 会替换成两位年份 */
                                /* {mm} 会替换成两位月份 */
                                /* {dd} 会替换成两位日期 */
                                /* {hh} 会替换成两位小时 */
                                /* {ii} 会替换成两位分钟 */
                                /* {ss} 会替换成两位秒 */
                                /* 非法字符 \ : * ? " < > | */
                                /* 具请体看线上文档: fex.baidu.com/ueditor/#use-format_upload_filename */

    /* 涂鸦图片上传配置项 */
    "scrawlActionName": "uploadscrawl", /* 执行上传涂鸦的action名称 */
    "scrawlFieldName": "upfile", /* 提交的图片表单名称 */
    "scrawlPathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "scrawlMaxSize": 2048000, /* 上传大小限制,单位B */
    "scrawlUrlPrefix": "", /* 图片访问路径前缀 */
    "scrawlInsertAlign": "none",

    /* 截图工具上传 */
    "snapscreenActionName": "uploadimage", /* 执行上传截图的action名称 */
    "snapscreenPathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "snapscreenUrlPrefix": "", /* 图片访问路径前缀 */
    "snapscreenInsertAlign": "none", /* 插入的图片浮动方式 */

    /* 抓取远程图片配置 */
    "catcherLocalDomain": ["127.0.0.1", "localhost", "img.baidu.com"],
    "catcherActionName": "catchimage", /* 执行抓取远程图片的action名称 */
    "catcherFieldName": "source", /* 提交的图片列表表单名称 */
    "catcherPathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "catcherUrlPrefix": "", /* 图片访问路径前缀 */
    "catcherMaxSize": 2048000, /* 上传大小限制,单位B */
    "catcherAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 抓取图片格式显示 */

    /* 上传视频配置 */
    "videoActionName": "uploadvideo", /* 执行上传视频的action名称 */
    "videoFieldName": "upfile", /* 提交的视频表单名称 */
    "videoPathFormat": "/ueditor/jsp/upload/video/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "videoUrlPrefix": "", /* 视频访问路径前缀 */
    "videoMaxSize": 102400000, /* 上传大小限制,单位B,默认100MB */
    "videoAllowFiles": [
        ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
        ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid"], /* 上传视频格式显示 */

    /* 上传文件配置 */
    "fileActionName": "uploadfile", /* controller里,执行上传视频的action名称 */
    "fileFieldName": "upfile", /* 提交的文件表单名称 */
    "filePathFormat": "/ueditor/jsp/upload/file/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "fileUrlPrefix": "", /* 文件访问路径前缀 */
    "fileMaxSize": 51200000, /* 上传大小限制,单位B,默认50MB */
    "fileAllowFiles": [
        ".png", ".jpg", ".jpeg", ".gif", ".bmp",
        ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
        ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
        ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
        ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
    ], /* 上传文件格式显示 */

    /* 列出指定目录下的图片 */
    "imageManagerActionName": "listimage", /* 执行图片管理的action名称 */
    "imageManagerListPath": "/ueditor/jsp/upload/image/", /* 指定要列出图片的目录 */
    "imageManagerListSize": 20, /* 每次列出文件数量 */
    "imageManagerUrlPrefix": "", /* 图片访问路径前缀 */
    "imageManagerInsertAlign": "none", /* 插入的图片浮动方式 */
    "imageManagerAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 列出的文件类型 */

    /* 列出指定目录下的文件 */
    "fileManagerActionName": "listfile", /* 执行文件管理的action名称 */
    "fileManagerListPath": "/ueditor/jsp/upload/file/", /* 指定要列出文件的目录 */
    "fileManagerUrlPrefix": "", /* 文件访问路径前缀 */
    "fileManagerListSize": 20, /* 每次列出文件数量 */
    "fileManagerAllowFiles": [
        ".png", ".jpg", ".jpeg", ".gif", ".bmp",
        ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
        ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
        ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
        ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
    ] /* 列出的文件类型 */

}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

5、新增Ueditor请求处理控制器

新增 ruoyi-admin\controller\common\UeditorController.java

package com.ruoyi.web.controller.common;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.config.ServerConfig;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.utils.file.FileUploadUtils;

/**
 * Ueditor 请求处理
 *
 * @author ruoyi
 */
@SuppressWarnings("serial")
@Controller
@RequestMapping("/ajax/libs/ueditor")
public class UeditorController extends BaseController
{
    private final String METHOD_HEAD = "ueditor";
    private final String IMGE_PATH = "/ueditor/images/";
    private final String VIDEO_PATH = "/ueditor/videos/";
    private final String FILE_PATH = "/ueditor/files/";

    @Autowired
    private ServerConfig serverConfig;

    /**
     * ueditor
     */
    @ResponseBody
    @RequestMapping(value = "/ueditor/controller")
    public Object ueditor(HttpServletRequest request, @RequestParam(value = "action", required = true) String action,
            MultipartFile upfile) throws Exception
    {
        List<Object> param = new ArrayList<Object>()
        {
            {
                add(action);
                add(upfile);
            }
        };
        Method method = this.getClass().getMethod(METHOD_HEAD + action, List.class, String.class);
        return method.invoke(this.getClass().newInstance(), param, serverConfig.getUrl());
    }

    /**
     * 读取配置文件
     */
    public JSONObject ueditorconfig(List<Object> param, String fileSuffixUrl) throws Exception
    {
        ClassPathResource classPathResource = new ClassPathResource("ueditor-config.json");
        String jsonString = new BufferedReader(new InputStreamReader(classPathResource.getInputStream())).lines().parallel().collect(Collectors.joining(System.lineSeparator()));
        JSONObject json = JSON.parseObject(jsonString, JSONObject.class);
        return json;
    }

    /**
     * 上传图片
     */
    public JSONObject ueditoruploadimage(List<Object> param, String fileSuffixUrl) throws Exception
    {
        JSONObject json = new JSONObject();
        json.put("state", "SUCCESS");
        json.put("url", ueditorcore(param, IMGE_PATH, false, fileSuffixUrl));
        return json;
    }

    /**
     * 上传视频
     */
    public JSONObject ueditoruploadvideo(List<Object> param, String fileSuffixUrl) throws Exception
    {
        JSONObject json = new JSONObject();
        json.put("state", "SUCCESS");
        json.put("url", ueditorcore(param, VIDEO_PATH, false, fileSuffixUrl));
        return json;
    }

    /**
     * 上传附件
     */
    public JSONObject ueditoruploadfile(List<Object> param, String fileSuffixUrl) throws Exception
    {
        JSONObject json = new JSONObject();
        json.put("state", "SUCCESS");
        json.put("url", ueditorcore(param, FILE_PATH, true, fileSuffixUrl));
        return json;
    }

    public String ueditorcore(List<Object> param, String path, boolean isFileName, String fileSuffixUrl)
            throws Exception
    {
        MultipartFile upfile = (MultipartFile) param.get(1);
        // 上传文件路径
        String filePath = RuoYiConfig.getUploadPath();
        String fileName = FileUploadUtils.upload(filePath, upfile);
        String url = fileSuffixUrl + fileName;
        return url;
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

6、登录系统,进入通知公告菜单测试富文本操作。

# 集成ip2region实现离线IP地址定位

离线IP地址定位库主要用于内网或想减少对外访问http带来的资源消耗。(代码已兼容支持jar包部署)

1、引入依赖

<!-- 离线IP地址定位库 -->
<dependency>
	<groupId>org.lionsoul</groupId>
	<artifactId>ip2region</artifactId>
	<version>1.7.2</version>
</dependency>
1
2
3
4
5
6

2、添加工具类RegionUtil.java

package com.ruoyi.common.utils;

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Method;
import org.apache.commons.io.FileUtils;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.lionsoul.ip2region.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * 根据ip离线查询地址
 *
 * @author ruoyi
 */
public class RegionUtil
{
    private static final Logger log = LoggerFactory.getLogger(RegionUtil.class);

    private static final String JAVA_TEMP_DIR = "java.io.tmpdir";

    static DbConfig config = null;
    static DbSearcher searcher = null;

    /**
     * 初始化IP库
     */
    static
    {
        try
        {
            // 因为jar无法读取文件,复制创建临时文件
            String dbPath = RegionUtil.class.getResource("/ip2region/ip2region.db").getPath();
            File file = new File(dbPath);
            if (!file.exists())
            {
                String tmpDir = System.getProperties().getProperty(JAVA_TEMP_DIR);
                dbPath = tmpDir + "ip2region.db";
                file = new File(dbPath);
                ClassPathResource cpr = new ClassPathResource("ip2region" + File.separator + "ip2region.db");
                InputStream resourceAsStream = cpr.getInputStream();
                if (resourceAsStream != null)
                {
                    FileUtils.copyInputStreamToFile(resourceAsStream, file);
                }
            }
            config = new DbConfig();
            searcher = new DbSearcher(config, dbPath);
            log.info("bean [{}]", config);
            log.info("bean [{}]", searcher);
        }
        catch (Exception e)
        {
            log.error("init ip region error:{}", e);
        }
    }

    /**
     * 解析IP
     *
     * @param ip
     * @return
     */
    public static String getRegion(String ip)
    {
        try
        {
            // db
            if (searcher == null || StringUtils.isEmpty(ip))
            {
                log.error("DbSearcher is null");
                return StringUtils.EMPTY;
            }
            long startTime = System.currentTimeMillis();
            // 查询算法
            int algorithm = DbSearcher.MEMORY_ALGORITYM;
            Method method = null;
            switch (algorithm)
            {
                case DbSearcher.BTREE_ALGORITHM:
                    method = searcher.getClass().getMethod("btreeSearch", String.class);
                    break;
                case DbSearcher.BINARY_ALGORITHM:
                    method = searcher.getClass().getMethod("binarySearch", String.class);
                    break;
                case DbSearcher.MEMORY_ALGORITYM:
                    method = searcher.getClass().getMethod("memorySearch", String.class);
                    break;
            }

            DataBlock dataBlock = null;
            if (Util.isIpAddress(ip) == false)
            {
                log.warn("warning: Invalid ip address");
            }
            dataBlock = (DataBlock) method.invoke(searcher, ip);
            String result = dataBlock.getRegion();
            long endTime = System.currentTimeMillis();
            log.debug("region use time[{}] result[{}]", endTime - startTime, result);
            return result;

        }
        catch (Exception e)
        {
            log.error("error:{}", e);
        }
        return StringUtils.EMPTY;
    }

}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

3、修改AddressUtils.java

package com.ruoyi.common.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.config.RuoYiConfig;

/**
 * 获取地址类
 * 
 * @author ruoyi
 */
public class AddressUtils
{
    private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);

    // 未知地址
    public static final String UNKNOWN = "XX XX";

    public static String getRealAddressByIP(String ip)
    {
        String address = UNKNOWN;
        // 内网不查询
        if (IpUtils.internalIp(ip))
        {
            return "内网IP";
        }
        if (RuoYiConfig.isAddressEnabled())
        {
            try
            {
                String rspStr = RegionUtil.getRegion(ip);
                if (StringUtils.isEmpty(rspStr))
                {
                    log.error("获取地理位置异常 {}", ip);
                    return UNKNOWN;
                }
                String[] obj = rspStr.split("\\|");
                String region = obj[2];
                String city = obj[3];

                return String.format("%s %s", region, city);
            }
            catch (Exception e)
            {
                log.error("获取地理位置异常 {}", e);
            }
        }
        return address;
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50

4、添加离线IP地址库插件

下载前端插件相关包和代码实现ruoyi/集成ip2region离线地址定位.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

5、添加离线IP地址库

src/main/resources下新建ip2region复制文件ip2region.db到目录下。

# 集成jsencrypt实现密码加密传输方式

目前登录接口密码是明文传输,如果安全性有要求,可以调整成加密方式传输。参考如下

1、修改前端login.js对密码进行rsa加密。

// 密钥对生成 http://web.chacuo.net/netrsakeypair

const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
  'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='

// 加密
function encrypt(txt) {
  const encryptor = new JSEncrypt()
  encryptor.setPublicKey(publicKey) // 设置公钥
  return encryptor.encrypt(txt) // 对数据进行加密
}

$(function() {
    validateKickout();
    validateRule();
    $('.imgcode').click(function() {
        var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random();
        $(".imgcode").attr("src", url);
    });
});

$.validator.setDefaults({
    submitHandler: function() {
        login();
    }
});

function login() {
    $.modal.loading($("#btnSubmit").data("loading"));
    var username = $.common.trim($("input[name='username']").val());
    var password = $.common.trim($("input[name='password']").val());
    var validateCode = $("input[name='validateCode']").val();
    var rememberMe = $("input[name='rememberme']").is(':checked');
    $.ajax({
        type: "post",
        url: ctx + "login",
        data: {
            "username": username,
            "password": encrypt(password),
            "validateCode": validateCode,
            "rememberMe": rememberMe
        },
        success: function(r) {
            if (r.code == web_status.SUCCESS) {
                location.href = ctx + 'index';
            } else {
            	$('.imgcode').click();
            	$(".code").val("");
            	$.modal.msg(r.msg);
            }
            $.modal.closeLoading();
        }
    });
}

function validateRule() {
    var icon = "<i class='fa fa-times-circle'></i> ";
    $("#signupForm").validate({
        rules: {
            username: {
                required: true
            },
            password: {
                required: true
            }
        },
        messages: {
            username: {
                required: icon + "请输入您的用户名",
            },
            password: {
                required: icon + "请输入您的密码",
            }
        }
    })
}

function validateKickout() {
    if (getParam("kickout") == 1) {
        layer.alert("<font color='red'>您已在别处登录,请您修改密码或重新登录</font>", {
            icon: 0,
            title: "系统提示"
        },
        function(index) {
            //关闭弹窗
            layer.close(index);
            if (top != self) {
                top.location = self.location;
            } else {
                var url = location.search;
                if (url) {
                    var oldUrl = window.location.href;
                    var newUrl = oldUrl.substring(0, oldUrl.indexOf('?'));
                    self.location = newUrl;
                }
            }
        });
    }
}

function getParam(paramName) {
    var reg = new RegExp("(^|&)" + paramName + "=([^&]*)(&|$)");
    var r = window.location.search.substr(1).match(reg);
    if (r != null) return decodeURI(r[2]);
    return null;
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

2、修改login.html文件,引入jsencrypt插件

<script src="../static/js/jsencrypt.min.js" th:src="@{/js/jsencrypt.min.js}"></script>
1

3、工具类security包下添加RsaUtils.java,用于RSA加密解密。

package com.ruoyi.common.utils.security;

import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * RSA加密解密
 * 
 * @author ruoyi
 **/
public class RsaUtils
{
    // Rsa 私钥
    public static String privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY"
            + "7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN"
            + "PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA"
            + "kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow"
            + "cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv"
            + "DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh"
            + "YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3" + "UP8iWi1Qw0Y=";

    /**
     * 私钥解密
     *
     * @param privateKeyString 私钥
     * @param text 待解密的文本
     * @return 解密后的文本
     */
    public static String decryptByPrivateKey(String text) throws Exception
    {
        return decryptByPrivateKey(privateKey, text);
    }

    /**
     * 公钥解密
     *
     * @param publicKeyString 公钥
     * @param text 待解密的信息
     * @return 解密后的文本
     */
    public static String decryptByPublicKey(String publicKeyString, String text) throws Exception
    {
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }

    /**
     * 私钥加密
     *
     * @param privateKeyString 私钥
     * @param text 待加密的信息
     * @return 加密后的文本
     */
    public static String encryptByPrivateKey(String privateKeyString, String text) throws Exception
    {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }

    /**
     * 私钥解密
     *
     * @param privateKeyString 私钥
     * @param text 待解密的文本
     * @return 解密后的文本
     */
    public static String decryptByPrivateKey(String privateKeyString, String text) throws Exception
    {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }

    /**
     * 公钥加密
     *
     * @param publicKeyString 公钥
     * @param text 待加密的文本
     * @return 加密后的文本
     */
    public static String encryptByPublicKey(String publicKeyString, String text) throws Exception
    {
        X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }

    /**
     * 构建RSA密钥对
     *
     * @return 生成后的公私钥信息
     */
    public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException
    {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
        String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
        String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
        return new RsaKeyPair(publicKeyString, privateKeyString);
    }

    /**
     * RSA密钥对对象
     */
    public static class RsaKeyPair
    {
        private final String publicKey;
        private final String privateKey;

        public RsaKeyPair(String publicKey, String privateKey)
        {
            this.publicKey = publicKey;
            this.privateKey = privateKey;
        }

        public String getPublicKey()
        {
            return publicKey;
        }

        public String getPrivateKey()
        {
            return privateKey;
        }
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

4、登录方法SysLoginController.java,对密码进行rsa解密。










 
















@Controller
public class SysLoginController extends BaseController
{
    @PostMapping("/login")
    @ResponseBody
    public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe)
    {
        try
        {
            UsernamePasswordToken token = new UsernamePasswordToken(username, RsaUtils.decryptByPrivateKey(password), rememberMe);
            Subject subject = SecurityUtils.getSubject();
            subject.login(token);
            return success();
        }
        catch (Exception e)
        {
            String msg = "用户或密码错误";
            if (StringUtils.isNotEmpty(e.getMessage()))
            {
                msg = e.getMessage();
            }
            return error(msg);
        }
    }
}
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

4、测试访问验证

访问 http://localhost/login 登录页面。提交时检查密码是否为加密传输,且后台也能正常解密。

下载前端插件相关包和代码实现ruoyi/集成jsencrypt实现密码加密传输方式.zip

链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt

# 集成druid实现数据库密码加密功能

数据库密码直接写在配置中,对运维安全来说,是一个很大的挑战。可以使用Druid为此提供一种数据库密码加密的手段ConfigFilter。项目已经集成druid所以只需按要求配置即可。

1、执行命令加密数据库密码

java -cp druid-1.2.4.jar com.alibaba.druid.filter.config.ConfigTools password
1

password输入你的数据库密码,输出的是加密后的结果,版本号视情况而定。

privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuLMVAFmcew+mPfVnzI6utEvhHWO2s6e4R1bVW3a9IpH+pEypeNV6KtZ/w9PuysPfdPxW5fN3BmnKFZUAIMvWhQIDAQABAkA6rnsfr1juKFyzFsMx1KthETKmucWUctczoz0KYEFbN+joNsd/ApQqsS/2MVG1QWbDJLUsSLWkchvRbtiqOlVJAiEA6KmgVeLR2qUU9gv6DJfuWk4Ol1M9GJnTamgyDttsSGcCIQDLOdjcht29s954vApG1fiPTP/kMvZ5aLrccw1lEuEGMwIhAKoe3c3u++MTsi/2se9jaDU/vguIIbRLRfsYFQIoDxUhAiAnCm/cvZPvk5RTgVxAC276qIIoJpou7K2pF/kkx6Gu/QIgKUVFiM8GVZkOWZC+nUm3UIfpGjrKXjvGrlHNvt89uBA=
publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==
password:gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA==
1
2
3

2、配置数据源,提示Druid数据源需要对数据库密码进行解密。

# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA==
            # 从库数据源
            slave:
                # 从数据源开关/默认关闭
                enabled: false
                url: 
                username: 
                password: 
            # 初始连接数
            initialSize: 5
            # 最小连接池数量
            minIdle: 10
            # 最大连接池数量
            maxActive: 20
            # 配置获取连接等待超时的时间
            maxWait: 60000
            # 配置连接超时时间
            connectTimeout: 30000
            # 配置网络超时时间
            socketTimeout: 60000
            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            timeBetweenEvictionRunsMillis: 60000
            # 配置一个连接在池中最小生存的时间,单位是毫秒
            minEvictableIdleTimeMillis: 300000
            # 配置一个连接在池中最大生存的时间,单位是毫秒
            maxEvictableIdleTimeMillis: 900000
            # 配置检测连接是否有效
            validationQuery: SELECT 1 FROM DUAL
            testWhileIdle: true
            testOnBorrow: false
            testOnReturn: false
            connectProperties: config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==
            webStatFilter: 
                enabled: true
            statViewServlet:
                enabled: true
                # 设置白名单,不填则允许所有访问
                allow:
                url-pattern: /druid/*
                # 控制台管理用户名和密码
                login-username: ruoyi
                login-password: 123456
            filter:
                config:
                    # 是否配置加密
                    enabled: true
                stat:
                    enabled: true
                    # 慢SQL记录
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: true
                wall:
                    config:
                        multi-statement-allow: true
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

3、DruidProperties配置connectProperties属性

package com.ruoyi.framework.config.properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;

/**
 * druid 配置属性
 * 
 * @author ruoyi
 */
@Configuration
public class DruidProperties
{
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.druid.connectTimeout}")
    private int connectTimeout;

    @Value("${spring.datasource.druid.socketTimeout}")
    private int socketTimeout;

    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    @Value("${spring.datasource.druid.connectProperties}")
    private String connectProperties;

    public DruidDataSource dataSource(DruidDataSource datasource)
    {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);
        
        /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */
        datasource.setConnectTimeout(connectTimeout);
        
        /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */
        datasource.setSocketTimeout(socketTimeout);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /**
         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
         */
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);

        /** 为数据库密码提供加密功能 */
        datasource.setConnectionProperties(connectProperties);
        return datasource;
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

4、启动应用程序测试验证加密结果

提示

如若忘记密码可以使用工具类解密(传入生成的公钥+密码)

public static void main(String[] args) throws Exception
{
	String password = ConfigTools.decrypt(
			"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==",
			"gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA==");
	System.out.println("解密密码:" + password);
}
1
2
3
4
5
6
7

# 集成yuicompressor实现(CSS/JS压缩)

Maven打包的时候可以使用YUI Compressor(压缩CSS/JS)文件,使用yuicompressor-maven-plugin插件进行压缩后会减小体积,提高请求速度。

pom.xml文件中增加该插件的定义,示例如下:

<build>
	<plugins>
		<!-- YUI Compressor (CSS/JS压缩) -->
		<plugin>
			<groupId>net.alchim31.maven</groupId>
			<artifactId>yuicompressor-maven-plugin</artifactId>
			<version>1.5.1</version>
			<executions>
				<execution>
					<phase>prepare-package</phase>
					<goals>
						<goal>compress</goal>
					</goals>
				</execution>
			</executions>
			<configuration>
				<!-- 读取js,css文件采用UTF-8编码 -->
				<encoding>UTF-8</encoding>
				<!-- 是否忽略警告 -->
				<jswarn>false</jswarn>
				<!-- 是否添加.min后缀 -->
				<nosuffix>true</nosuffix>
				<!-- 压缩多少字节换行 -->
				<linebreakpos>50000</linebreakpos>
				<!-- 源目录,即需压缩的根目录 -->
				<sourceDirectory>src/main/resources/static</sourceDirectory>
				<!-- 若存在已压缩的文件,会先对比源文件是否有改动。有改动便压缩,无改动就不压缩 -->
				<force>true</force>
				<includes>
					<include>**/*.js</include>
					<include>**/*.css</include>
				</includes>
				<excludes>
					<exclude>**/*.min.js</exclude>
					<exclude>**/*.min.css</exclude>
					<exclude>**/fileinput.js</exclude>
					<exclude>**/bootstrap-treetable.js</exclude>
				</excludes>
			</configuration>
		</plugin> 
	</plugins>
</build>
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
38
39
40
41
42

# 集成watermark实现页面添加水印

在网站浏览中,常常需要网页水印,以便防止用户截图或录屏暴露敏感信息后,方便追踪用户来源。

1、在ry-ui.js文件通用方法中增加watermark方法,示例如下:

// 为网页添加文字水印
watermark: function(settings) {
	// 默认设置
	var defaultSettings = {
		watermark_txt: "text",
		watermark_x: 20,
		// 水印起始位置x轴坐标
		watermark_y: 20,
		// 水印起始位置Y轴坐标
		watermark_rows: 100,
		// 水印行数
		watermark_cols: 20,
		// 水印列数
		watermark_x_space: 10,
		// 水印x轴间隔
		watermark_y_space: 10,
		// 水印y轴间隔
		watermark_color: '#aaa',
		// 水印字体颜色
		watermark_alpha: 0.3,
		// 水印透明度
		watermark_fontsize: '15px',
		// 水印字体大小
		watermark_font: '微软雅黑',
		// 水印字体
		watermark_width: 150,
		// 水印宽度
		watermark_height: 80,
		// 水印长度
		watermark_angle: 15 // 水印倾斜度数
	};
	// 采用配置项替换默认值,作用类似jquery.extend
	if (arguments.length === 1 && typeof arguments[0] === "object") {
		var src = arguments[0] || {};
		for (key in src) {
			if (src[key] && defaultSettings[key] && src[key] === defaultSettings[key]) continue;
			else if (src[key]) defaultSettings[key] = src[key];
		}
	}

	var oTemp = document.createDocumentFragment();

	// 获取页面最大宽度
	var page_width = Math.max(document.body.scrollWidth, document.body.clientWidth);
	var cutWidth = page_width * 0.0150;
	page_width = page_width - cutWidth;
	// 获取页面最大高度
	var page_height = Math.max(document.body.scrollHeight - 80, document.body.clientHeight - 40);
	// var page_height = document.body.scrollHeight+document.body.scrollTop;
	// 如果将水印列数设置为0,或水印列数设置过大,超过页面最大宽度,则重新计算水印列数和水印x轴间隔
	if (defaultSettings.watermark_cols == 0 || (parseInt(defaultSettings.watermark_x + defaultSettings.watermark_width * defaultSettings.watermark_cols + defaultSettings.watermark_x_space * (defaultSettings.watermark_cols - 1)) > page_width)) {
		defaultSettings.watermark_cols = parseInt((page_width - defaultSettings.watermark_x + defaultSettings.watermark_x_space) / (defaultSettings.watermark_width + defaultSettings.watermark_x_space));
		defaultSettings.watermark_x_space = parseInt((page_width - defaultSettings.watermark_x - defaultSettings.watermark_width * defaultSettings.watermark_cols) / (defaultSettings.watermark_cols - 1));
	}
	// 如果将水印行数设置为0,或水印行数设置过大,超过页面最大长度,则重新计算水印行数和水印y轴间隔
	if (defaultSettings.watermark_rows == 0 || (parseInt(defaultSettings.watermark_y + defaultSettings.watermark_height * defaultSettings.watermark_rows + defaultSettings.watermark_y_space * (defaultSettings.watermark_rows - 1)) > page_height)) {
		defaultSettings.watermark_rows = parseInt((defaultSettings.watermark_y_space + page_height - defaultSettings.watermark_y) / (defaultSettings.watermark_height + defaultSettings.watermark_y_space));
		defaultSettings.watermark_y_space = parseInt(((page_height - defaultSettings.watermark_y) - defaultSettings.watermark_height * defaultSettings.watermark_rows) / (defaultSettings.watermark_rows - 1));
	}
	var x;
	var y;
	for (var i = 0; i < defaultSettings.watermark_rows; i++) {
		y = defaultSettings.watermark_y + (defaultSettings.watermark_y_space + defaultSettings.watermark_height) * i;
		for (var j = 0; j < defaultSettings.watermark_cols; j++) {
			x = defaultSettings.watermark_x + (defaultSettings.watermark_width + defaultSettings.watermark_x_space) * j;
			var mask_div = document.createElement('div');
			mask_div.id = 'mask_div' + i + j;
			mask_div.className = 'mask_div';
			mask_div.appendChild(document.createTextNode(defaultSettings.watermark_txt));
			// 设置水印div倾斜显示
			mask_div.style.webkitTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
			mask_div.style.MozTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
			mask_div.style.msTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
			mask_div.style.OTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
			mask_div.style.transform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
			mask_div.style.visibility = "";
			mask_div.style.position = "fixed";
			mask_div.style.left = x + 'px';
			mask_div.style.top = y + 'px';
			mask_div.style.overflow = "hidden";
			mask_div.style.zIndex = "19920219";
			mask_div.style.pointerEvents = 'none'; // pointer-events:none  让水印不遮挡页面的点击事件
			// mask_div.style.border="solid #eee 1px";
			mask_div.style.opacity = defaultSettings.watermark_alpha;
			mask_div.style.fontSize = defaultSettings.watermark_fontsize;
			mask_div.style.fontFamily = defaultSettings.watermark_font;
			mask_div.style.color = defaultSettings.watermark_color;
			mask_div.style.textAlign = "center";
			mask_div.style.width = defaultSettings.watermark_width + 'px';
			mask_div.style.height = defaultSettings.watermark_height + 'px';
			mask_div.style.display = "block";
			// 交叉网格显示
			if ((i % 2 == 0) && (j % 2 == 0)) {
				oTemp.appendChild(mask_div);
			}
			if ((i % 2 == 1) && (j % 2 == 1)) {
				oTemp.appendChild(mask_div);
			}
		};
	};
	document.body.appendChild(oTemp);
},
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

2、在index.htmlindex-topnav.html文件调用watermark方法,示例如下:

$(function() {
	var loginName = [[${@permission.getPrincipalProperty('loginName')}]];
	$.common.watermark({ "watermark_txt": loginName + "水印" })
});
1
2
3
4

3、访问页面,检查页面水印是否显示。

注意

如需Excel导出时添加水印参考 - 参考如何Excel导出时添加水印

# 集成browscap读取浏览器用户代理

由于项目使用的UserAgentUtils早在18年就停止维护了,对于目前市面上的新版本浏览器及系统没有进行区分,所以可以选择更换为browscap-java,但是browscap-java有一个缺点就是首次加载会很慢,大概10秒左右,根据机器的性能决定。 因为这个原因所以项目目前没有采纳,等待后续在看browscap-java有没有对这个进行算法优化,如果觉得不是什么大问题可以参考如下流程进行升级,如果发现有更好的其他插件也可以反馈给我。

1、修改pom.xml,将bitwalker替换成browscap-java

<browscap.version>1.3.12</browscap.version>

<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
	<groupId>com.blueconic</groupId>
	<artifactId>browscap-java</artifactId>
	<version>${browscap.version}</version>
</dependency>
1
2
3
4
5
6
7
8

2、ruoyi-framework/pom.xml删除bitwalker

3、ruoyi-common/pom.xml新增browscap-java

<dependency>
	<groupId>com.blueconic</groupId>
	<artifactId>browscap-java</artifactId>
</dependency>
1
2
3
4

4、新增用户代理解析类UserAgent.java

package com.ruoyi.common.utils.http;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.blueconic.browscap.Capabilities;
import com.blueconic.browscap.UserAgentParser;
import com.blueconic.browscap.UserAgentService;
import com.ruoyi.common.utils.AddressUtils;

/**
 * 浏览器用户代理解析
 * 
 * @author ruoyi
 */
public class UserAgent
{
    private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);

    /** 浏览器 */
    public String browser = "";

    /** 操作系统 */
    public String operatingSystem = "";

    /** 解析器 */
    private static UserAgentParser parser = null;

    static
    {
        try
        {
            parser = new UserAgentService().loadParser();
        }
        catch (Exception e)
        {
            log.error("获取用户代理异常 {}", e);
        }
    }

    public UserAgent(String userAgentString)
    {
        if (parser != null)
        {
            String userAgentLowercaseString = userAgentString == null ? null : userAgentString.toLowerCase();
            Capabilities capabilities = parser.parse(userAgentLowercaseString);
            this.browser = String.format("%s %s", capabilities.getBrowser(), capabilities.getBrowserMajorVersion());
            this.operatingSystem = capabilities.getPlatform();
        }
    }

    public static UserAgent parseUserAgentString(String userAgentString)
    {
        return new UserAgent(userAgentString);
    }

    public String getBrowser()
    {
        return browser;
    }

    public String getOperatingSystem()
    {
        return operatingSystem;
    }
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

5、AsyncFactoryOnlineSessionFactory修改UserAgent包路径并修改获取方法。

// bitwalker 获取浏览器/操作系统方法
String os = userAgent.getOperatingSystem().getName();
String browser = userAgent.getBrowser().getName();

// ======== 修改为 ======== 	

// browscap-java 获取浏览器/操作系统方法		
String os = userAgent.getOperatingSystem();
String browser = userAgent.getBrowser();
1
2
3
4
5
6
7
8
9

# 集成dynamic-datasource实现多数据源增强

dynamic-datasource是一个基于springboot的快速集成多数据源的启动器。同时支持数据源分组、数据库敏感配置信息加密、自定义注解、动态增加移除数据源、读写分离、本地多数据源事务方案、基于Seata的分布式事务方案等等。

  • 提供并简化对DruidHikariCpBeeCpDbcp2的快速集成。
  • 提供对Mybatis-PlusQuartzShardingJdbcP6spyJndi等组件的集成方案。

集成多数据源dynamic-datasource,可以删除原先的默认多数据源处理类

ruoyi-framework\src\main\java\com\ruoyi\framework\config\DruidConfig.java
ruoyi-framework\src\main\java\com\ruoyi\framework\config\properties\DruidProperties.java
ruoyi-framework\src\main\java\com\ruoyi\framework\datasource\DynamicDataSource.java
ruoyi-framework\src\main\java\com\ruoyi\framework\datasource\DynamicDataSourceContextHolder.java
ruoyi-framework\src\main\java\com\ruoyi\framework\aspectj\DataSourceAspect.java

1、ruoyi-common\pom.xml模块添加整合依赖

<!-- 动态数据源 -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
	<version>3.5.2</version>
</dependency>
1
2
3
4
5
6

2、ruoyi-admin文件application-druid.yml,修改spirng.datasource配置

# spring配置
spring:
  datasource:
    druid:
      stat-view-servlet:
        enabled: true
        loginUsername: ruoyi
        loginPassword: 123456
    dynamic:
      druid:
        initial-size: 5
        min-idle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,slf4j
        connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      datasource:
          # 主库数据源
          master:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
            username: root
            password: password
          # 测试数据源
          test:
            driver-class-name: com.mysql.jdbc.Driver
            url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
            username: root
            password: password
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

3、测试验证,修改参数管理《增删改查》切换到test数据源。

package com.ruoyi.system.mapper;

import java.util.List;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.ruoyi.system.domain.SysConfig;

/**
 * 参数配置 数据层
 * 
 * @author ruoyi
 */
public interface SysConfigMapper
{
    /**
     * 查询参数配置信息
     * 
     * @param config 参数配置信息
     * @return 参数配置信息
     */
    @DS("test")
    public SysConfig selectConfig(SysConfig config);

    /**
     * 查询参数配置列表
     * 
     * @param config 参数配置信息
     * @return 参数配置集合
     */
    @DS("test")
    public List<SysConfig> selectConfigList(SysConfig config);

    /**
     * 根据键名查询参数配置信息
     * 
     * @param configKey 参数键名
     * @return 参数配置信息
     */
    @DS("test")
    public SysConfig checkConfigKeyUnique(String configKey);

    /**
     * 新增参数配置
     * 
     * @param config 参数配置信息
     * @return 结果
     */
    @DS("test")
    public int insertConfig(SysConfig config);

    /**
     * 修改参数配置
     * 
     * @param config 参数配置信息
     * @return 结果
     */
    @DS("test")
    public int updateConfig(SysConfig config);

    /**
     * 删除参数配置
     * 
     * @param configId 参数主键
     * @return 结果
     */
    @DS("test")
    public int deleteConfigById(Long configId);

    /**
     * 批量删除参数配置
     * 
     * @param configIds 需要删除的数据ID
     * @return 结果
     */
    @DS("test")
    public int deleteConfigByIds(String[] configIds);
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76