فهرست منبع

!67 同步dev分支
Merge pull request !67 from 疯狂的狮子Li/dev

疯狂的狮子Li 4 سال پیش
والد
کامیت
5632278688
90فایلهای تغییر یافته به همراه3513 افزوده شده و 1475 حذف شده
  1. 18 3
      README.md
  2. 92 0
      docker/deploy.sh
  3. 119 0
      docker/docker-compose.yml
  4. 77 0
      docker/nginx/nginx.conf
  5. 14 7
      pom.xml
  6. 14 0
      ruoyi-admin/Dockerfile
  7. 22 3
      ruoyi-admin/pom.xml
  8. 29 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java
  9. 3 6
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java
  10. 56 1
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
  11. 27 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
  12. 27 0
      ruoyi-admin/src/main/resources/application-dev.yml
  13. 29 2
      ruoyi-admin/src/main/resources/application-prod.yml
  14. 25 30
      ruoyi-admin/src/main/resources/application.yml
  15. 33 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  16. 36 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  17. 1 5
      ruoyi-common/pom.xml
  18. 4 1
      ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
  19. 4 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
  20. 3 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/mybatisplus/cache/MybatisPlusRedisCache.java
  21. 27 4
      ruoyi-common/src/main/java/com/ruoyi/common/core/mybatisplus/methods/InsertAll.java
  22. 3 3
      ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java
  23. 2 2
      ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java
  24. 28 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java
  25. 1072 1072
      ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
  26. 2 2
      ruoyi-demo/pom.xml
  27. 70 0
      ruoyi-demo/src/main/java/com/ruoyi/demo/controller/RedisCacheController.java
  28. 3 0
      ruoyi-demo/src/main/java/com/ruoyi/demo/feign/fallback/FeignTestFallback.java
  29. 18 0
      ruoyi-extend/pom.xml
  30. 13 0
      ruoyi-extend/ruoyi-monitor-admin/Dockerfile
  31. 73 0
      ruoyi-extend/ruoyi-monitor-admin/pom.xml
  32. 19 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/com/ruoyi/monitor/admin/MonitorAdminApplication.java
  33. 31 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/com/ruoyi/monitor/admin/config/AdminServerConfig.java
  34. 48 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/java/com/ruoyi/monitor/admin/config/SecurityConfig.java
  35. 11 0
      ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml
  36. 1 1
      ruoyi-framework/pom.xml
  37. 0 63
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/AdminServerConfig.java
  38. 36 0
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/FeignConfig.java
  39. 7 1
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
  40. 3 9
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
  41. 33 1
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/RedissonProperties.java
  42. 26 15
      ruoyi-framework/src/main/java/com/ruoyi/framework/mybatisplus/CreateAndUpdateMetaObjectHandler.java
  43. 1 1
      ruoyi-generator/pom.xml
  44. 6 6
      ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
  45. 0 2
      ruoyi-generator/src/main/resources/vm/java/mapper.java.vm
  46. 6 40
      ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm
  47. 6 42
      ruoyi-generator/src/main/resources/vm/vue/index.vue.vm
  48. 1 1
      ruoyi-quartz/pom.xml
  49. 1 1
      ruoyi-system/pom.xml
  50. 23 1
      ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java
  51. 16 0
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
  52. 37 1
      ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java
  53. 27 3
      ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java
  54. 34 4
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java
  55. 68 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
  56. 64 3
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
  57. 42 3
      ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
  58. 3 0
      ruoyi-ui/.env.development
  59. 3 0
      ruoyi-ui/.env.production
  60. 3 0
      ruoyi-ui/.env.staging
  61. 1 1
      ruoyi-ui/package.json
  62. 47 1
      ruoyi-ui/src/api/system/role.js
  63. 17 0
      ruoyi-ui/src/api/system/user.js
  64. 18 0
      ruoyi-ui/src/assets/styles/ruoyi.scss
  65. 7 6
      ruoyi-ui/src/components/Editor/index.vue
  66. 48 29
      ruoyi-ui/src/components/FileUpload/index.vue
  67. 3 1
      ruoyi-ui/src/components/HeaderSearch/index.vue
  68. 154 44
      ruoyi-ui/src/components/ImageUpload/index.vue
  69. 2 2
      ruoyi-ui/src/components/TopNav/index.vue
  70. 64 0
      ruoyi-ui/src/directive/dialog/drag.js
  71. 5 2
      ruoyi-ui/src/directive/index.js
  72. 2 2
      ruoyi-ui/src/directive/permission/hasPermi.js
  73. 2 2
      ruoyi-ui/src/directive/permission/hasRole.js
  74. 1 1
      ruoyi-ui/src/layout/components/AppMain.vue
  75. 27 0
      ruoyi-ui/src/layout/components/InnerLink/index.vue
  76. 11 2
      ruoyi-ui/src/main.js
  77. 27 0
      ruoyi-ui/src/router/index.js
  78. 3 0
      ruoyi-ui/src/store/modules/permission.js
  79. 4 2
      ruoyi-ui/src/views/demo/demo/index.vue
  80. 4 2
      ruoyi-ui/src/views/demo/tree/index.vue
  81. 26 0
      ruoyi-ui/src/views/index.vue
  82. 5 15
      ruoyi-ui/src/views/monitor/admin/index.vue
  83. 0 4
      ruoyi-ui/src/views/system/notice/index.vue
  84. 213 0
      ruoyi-ui/src/views/system/role/authUser.vue
  85. 30 8
      ruoyi-ui/src/views/system/role/index.vue
  86. 142 0
      ruoyi-ui/src/views/system/role/selectUser.vue
  87. 117 0
      ruoyi-ui/src/views/system/user/authRole.vue
  88. 30 9
      ruoyi-ui/src/views/system/user/index.vue
  89. 1 1
      ruoyi-ui/vue.config.js
  90. 2 2
      sql/ry_20210210.sql

+ 18 - 3
README.md

@@ -4,12 +4,17 @@
 [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/blob/master/LICENSE)
 [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 <br>
-[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-2.4.0-success.svg)](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus)
+[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-2.5.0-success.svg)](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus)
 [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-2.4-blue.svg)]()
 [![JDK-8+](https://img.shields.io/badge/JDK-8+-green.svg)]()
 [![JDK-11](https://img.shields.io/badge/JDK-11-green.svg)]()
 
-基于 RuoYi-Vue 集成 Mybatis-Plus Lombok Hutool 等便捷开发工具 适配重写相关业务 便于开发 定期与 RuoYi-Vue 同步
+RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 `分布式集群` 场景升级 定期与 RuoYi-Vue 同步
+
+集成 Lock4j dynamic-datasource 等分布式场景解决方案
+
+集成 Mybatis-Plus Lombok Hutool 等便捷开发工具 适配重写相关业务 便于开发 
+
 * 前端开发框架 Vue、Element UI
 * 后端开发框架 Spring Boot、Redis
 * 容器框架 Undertow 基于 Netty 的高性能容器
@@ -27,6 +32,7 @@
 * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构
 * Redis客户端 采用 Redisson 性能更强
 * 分布式锁 Lock4j 注解锁、工具锁 多种多样
+* 部署方式 Docker 容器编排 一键部署业务集群
 
 ## 参考文档
 
@@ -34,6 +40,8 @@
 <br>
 >[初始化项目 必看](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/关于初始化项目?sort_id=4164117)
 > 
+>[部署项目 必看](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/关于应用部署?sort_id=4219382)
+> 
 >[参考文档 Wiki](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages)
 
 ## 提问四部曲
@@ -58,6 +66,12 @@
 ### 四、加群
 以上三点已经能解决大家绝大部分问题了,如果还有问题没能通过这几种方式解决,那么加群,大家一起在群里探讨一下
 
+## 贡献代码
+
+欢迎各路英雄豪杰 `PR` 代码 请提交到 `dev` 开发分支 统一测试发版
+
+框架定位为 `通用后台管理系统(分布式集群强化)` 原则上不接受业务 `PR` 
+
 ## 修改RuoYi功能
 ### 依赖改动
 
@@ -74,6 +88,7 @@
 * 移除 fastjson 统一使用 jackson 序列化
 * 集成 dynamic-datasource 多数据源(默认支持MySQL,其他种类需自行适配)
 * 集成 Lock4j 实现分布式 注解锁、工具锁 多种多样
+* 增加 Docker 容器编排 打包插件与部署脚本
 
 ### 代码改动
 
@@ -90,7 +105,7 @@
 
 ### 其他
 
-* 同步升级 RuoYi-Vue 3.5.0
+* 同步升级 RuoYi-Vue
 * GitHub 地址 [RuoYi-Vue-Plus-github](https://github.com/JavaLionLi/RuoYi-Vue-Plus)
 * 单模块 fast 分支 [RuoYi-Vue-Plus-fast](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/tree/fast/)
 * Oracle 模块 oracle 分支 [RuoYi-Vue-Plus-oracle](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/tree/oracle/)

+ 92 - 0
docker/deploy.sh

@@ -0,0 +1,92 @@
+#!/bin/bash
+
+#使用说明,用来提示输入参数
+usage() {
+	echo "Usage: sh 执行脚本.sh [port|mount|monitor|base|start|stop|stopall|rm|rmiNoneTag]"
+	exit 1
+}
+
+#开启所需端口
+port(){
+	firewall-cmd --add-port=3306/tcp --permanent
+	firewall-cmd --add-port=6379/tcp --permanent
+	service firewalld restart
+}
+
+##放置挂载文件
+mount(){
+	#挂载配置文件
+	if test ! -f "/docker/nginx/conf/nginx.conf" ;then
+		mkdir -p /docker/nginx/conf
+		cp nginx/nginx.conf /docker/nginx/conf/nginx.conf
+	fi
+}
+
+#启动基础模块
+base(){
+	docker-compose up -d mysql nginx-web redis
+}
+
+#启动基础模块
+monitor(){
+	docker-compose up -d ruoyi-monitor-admin
+}
+
+#启动程序模块
+start(){
+	docker-compose up -d ruoyi-server1 ruoyi-server2
+}
+
+#停止程序模块
+stop(){
+	docker-compose stop ruoyi-server1 ruoyi-server2
+}
+
+#关闭所有模块
+stopall(){
+	docker-compose stop
+}
+
+#删除所有模块
+rm(){
+	docker-compose rm
+}
+
+#删除Tag为空的镜像
+rmiNoneTag(){
+	docker images|grep none|awk '{print $3}'|xargs docker rmi -f
+}
+
+#根据输入参数,选择执行对应方法,不输入则执行使用说明
+case "$1" in
+"port")
+	port
+;;
+"mount")
+	mount
+;;
+"base")
+	base
+;;
+"monitor")
+	monitor
+;;
+"start")
+	start
+;;
+"stop")
+	stop
+;;
+"stopall")
+	stopall
+;;
+"rm")
+	rm
+;;
+"rmiNoneTag")
+	rmiNoneTag
+;;
+*)
+	usage
+;;
+esac

+ 119 - 0
docker/docker-compose.yml

@@ -0,0 +1,119 @@
+version: '3'
+
+services:
+  mysql:
+    image: mysql:8.0.24
+    container_name: mysql
+    environment:
+      # 时区上海
+      TZ: Asia/Shanghai
+      # root 密码
+      MYSQL_ROOT_PASSWORD: root
+      # 初始化数据库(后续的初始化sql会在这个库执行)
+      MYSQL_DATABASE: ry-vue
+    ports:
+      - 3306:3306
+    volumes:
+      # 数据挂载
+      - /docker/mysql/data/:/var/lib/mysql/
+      # 配置挂载
+      - /docker/mysql/conf/:/etc/mysql/conf.d/
+    command:
+      # 将mysql8.0默认密码策略 修改为 原先 策略 (mysql8.0对其默认策略做了更改 会导致密码无法匹配)
+      --default-authentication-plugin=mysql_native_password
+      --character-set-server=utf8mb4
+      --collation-server=utf8mb4_general_ci
+      --explicit_defaults_for_timestamp=true
+      --lower_case_table_names=1
+    privileged: true
+    restart: always
+    networks:
+      ruoyi_net:
+        ipv4_address: 172.30.0.36
+
+  nginx-web:
+    # 如果需要指定版本 就把 latest 换成版本号
+    image: nginx:latest
+    container_name: nginx-web
+    ports:
+      - 80:80
+      - 443:443
+    volumes:
+      # 证书映射
+      - /docker/nginx/cert:/etc/nginx/cert
+      # 配置文件映射
+      - /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
+      # 页面目录
+      - /docker/nginx/html:/usr/share/nginx/html
+      # 日志目录
+      - /docker/nginx/log:/var/log/nginx
+      # 主机本机时间文件映射 与本机时间同步
+      - /etc/localtime:/etc/localtime:ro
+    privileged: true
+    restart: always
+    networks:
+      - ruoyi_net
+
+  redis:
+    image: redis:6.2.1
+    container_name: redis
+    ports:
+      - 6379:6379
+    environment:
+      # 设置环境变量 时区上海 编码UTF-8
+      TZ: Asia/Shanghai
+      LANG: en_US.UTF-8
+    volumes:
+      # 配置文件
+      - /docker/redis/conf/redis.conf:/redis.conf:rw
+      # 数据文件
+      - /docker/redis/data:/data:rw
+    command: "redis-server --appendonly yes"
+    privileged: true
+    restart: always
+    networks:
+      ruoyi_net:
+        ipv4_address: 172.30.0.48
+
+  ruoyi-server1:
+    image: "ruoyi/ruoyi-server:2.5.0"
+    environment:
+      - TZ=Asia/Shanghai
+    volumes:
+      # 配置文件
+      - /docker/server1/logs/:/ruoyi/server/logs/
+    privileged: true
+    restart: always
+    networks:
+      ruoyi_net:
+        ipv4_address: 172.30.0.60
+
+  ruoyi-server2:
+    image: "ruoyi/ruoyi-server:2.5.0"
+    environment:
+      - TZ=Asia/Shanghai
+    volumes:
+      # 配置文件
+      - /docker/server2/logs/:/ruoyi/server/logs/
+    privileged: true
+    restart: always
+    networks:
+      ruoyi_net:
+        ipv4_address: 172.30.0.61
+
+  ruoyi-monitor-admin:
+    image: "ruoyi/ruoyi-monitor-admin:2.5.0"
+    environment:
+      - TZ=Asia/Shanghai
+    privileged: true
+    restart: always
+    networks:
+      ruoyi_net:
+        ipv4_address: 172.30.0.90
+
+networks:
+  ruoyi_net:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.30.0.0/16

+ 77 - 0
docker/nginx/nginx.conf

@@ -0,0 +1,77 @@
+worker_processes  1;
+
+error_log  /var/log/nginx/error.log warn;
+pid        /var/run/nginx.pid;
+
+events {
+    worker_connections  1024;
+}
+
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+    sendfile        on;
+    keepalive_timeout  65;
+    # 限制body大小
+    client_max_body_size 100m;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                          '$status $body_bytes_sent "$http_referer" '
+                          '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /var/log/nginx/access.log  main;
+
+	upstream server {
+		server 172.30.0.60:8080;
+		server 172.30.0.61:8080;
+	}
+
+    upstream monitor-admin {
+        server 172.30.0.90:9090;
+    }
+
+    server {
+        listen       80;
+        server_name  localhost;
+
+        # https配置参考 start
+        #listen       443 ssl;
+
+        # 证书直接存放 /docker/nginx/cert/ 目录下即可 更改证书名称即可 无需更改证书路径
+        #ssl on;
+        #ssl_certificate      /etc/nginx/cert/xxx.local.crt; # /etc/nginx/cert/ 为docker映射路径 不允许更改
+        #ssl_certificate_key  /etc/nginx/cert/xxx.local.key; # /etc/nginx/cert/ 为docker映射路径 不允许更改
+        #ssl_session_timeout 5m;
+        #ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
+        #ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+        #ssl_prefer_server_ciphers on;
+        # https配置参考 end
+
+		location / {
+            root   /usr/share/nginx/html;
+			try_files $uri $uri/ /index.html;
+            index  index.html index.htm;
+        }
+
+		location /prod-api/ {
+			proxy_set_header Host $http_host;
+			proxy_set_header X-Real-IP $remote_addr;
+			proxy_set_header REMOTE-HOST $remote_addr;
+			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+			proxy_pass http://server/;
+		}
+
+		location /admin/ {
+			proxy_set_header Host $http_host;
+			proxy_set_header X-Real-IP $remote_addr;
+			proxy_set_header REMOTE-HOST $remote_addr;
+			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+			proxy_pass http://monitor-admin/admin/;
+		}
+
+        error_page   500 502 503 504  /50x.html;
+        location = /50x.html {
+            root   html;
+        }
+    }
+}

+ 14 - 7
pom.xml

@@ -6,32 +6,38 @@
 
     <groupId>com.ruoyi</groupId>
     <artifactId>ruoyi-vue-plus</artifactId>
-    <version>${ruoyi-vue-plus.version}</version>
+    <version>2.5.0</version>
 
     <name>RuoYi-Vue-Plus</name>
     <url>https://gitee.com/JavaLionLi/RuoYi-Vue-Plus</url>
     <description>RuoYi-Vue-Plus后台管理系统</description>
 
     <properties>
-        <ruoyi-vue-plus.version>2.4.0</ruoyi-vue-plus.version>
-        <spring-boot.version>2.4.7</spring-boot.version>
+        <ruoyi-vue-plus.version>2.5.0</ruoyi-vue-plus.version>
+        <spring-boot.version>2.4.8</spring-boot.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>1.8</java.version>
         <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
         <druid.version>1.2.6</druid.version>
-        <knife4j.version>3.0.2</knife4j.version>
+        <knife4j.version>3.0.3</knife4j.version>
         <poi.version>4.1.2</poi.version>
         <velocity.version>1.7</velocity.version>
         <jwt.version>0.9.1</jwt.version>
         <mybatis-plus.version>3.4.3</mybatis-plus.version>
-        <hutool.version>5.7.2</hutool.version>
+        <hutool.version>5.7.4</hutool.version>
         <feign.version>3.0.3</feign.version>
         <feign-okhttp.version>11.0</feign-okhttp.version>
-        <spring-boot-admin.version>2.4.1</spring-boot-admin.version>
-        <redisson.version>3.15.2</redisson.version>
+        <spring-boot-admin.version>2.4.3</spring-boot-admin.version>
+        <redisson.version>3.16.0</redisson.version>
         <lock4j.version>2.2.1</lock4j.version>
         <datasource.version>3.4.0</datasource.version>
+
+        <!-- docker 配置 -->
+        <docker.registry.url>localhost</docker.registry.url>
+        <docker.registry.host>http://${docker.registry.url}:2375</docker.registry.host>
+        <docker.namespace>ruoyi</docker.namespace>
+        <docker.plugin.version>1.2.0</docker.plugin.version>
     </properties>
 
     <!-- 依赖声明 -->
@@ -192,6 +198,7 @@
         <module>ruoyi-generator</module>
         <module>ruoyi-common</module>
         <module>ruoyi-demo</module>
+        <module>ruoyi-extend</module>
     </modules>
     <packaging>pom</packaging>
 

+ 14 - 0
ruoyi-admin/Dockerfile

@@ -0,0 +1,14 @@
+FROM anapsix/alpine-java:8_server-jre_unlimited
+
+MAINTAINER Lion Li
+
+RUN mkdir -p /ruoyi/server
+RUN mkdir -p /ruoyi/server/logs
+
+WORKDIR /ruoyi/server
+
+EXPOSE 8080
+
+ADD ./target/ruoyi-admin.jar ./app.jar
+
+ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]

+ 22 - 3
ruoyi-admin/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>${ruoyi-vue-plus.version}</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <packaging>jar</packaging>
@@ -24,7 +24,7 @@
             <optional>true</optional> <!-- 表示依赖不会传递 -->
         </dependency>
 
-         <!-- Mysql驱动包 -->
+        <!-- Mysql驱动包 -->
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
@@ -82,7 +82,26 @@
                     <failOnMissingWebXml>false</failOnMissingWebXml>
                     <warName>${project.artifactId}</warName>
                 </configuration>
-           </plugin>
+            </plugin>
+            <plugin>
+                <groupId>com.spotify</groupId>
+                <artifactId>docker-maven-plugin</artifactId>
+                <version>${docker.plugin.version}</version>
+                <configuration>
+                    <imageName>${docker.namespace}/ruoyi-server:${project.version}</imageName>
+                    <dockerDirectory>${project.basedir}</dockerDirectory>
+                    <dockerHost>${docker.registry.host}</dockerHost>
+                    <registryUrl>${docker.registry.url}</registryUrl>
+                    <serverId>${docker.registry.url}</serverId>
+                    <resources>
+                        <resource>
+                            <targetPath>/</targetPath>
+                            <directory>${project.build.directory}</directory>
+                            <include>${project.build.finalName}.jar</include>
+                        </resource>
+                    </resources>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 

+ 29 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java

@@ -0,0 +1,29 @@
+package com.ruoyi.web.controller.system;
+
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.config.RuoYiConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 首页
+ *
+ * @author ruoyi
+ */
+@RestController
+public class SysIndexController
+{
+    /** 系统基础配置 */
+    @Autowired
+    private RuoYiConfig ruoyiConfig;
+
+    /**
+     * 访问首页,提示语
+     */
+    @RequestMapping("/")
+    public String index()
+    {
+        return StrUtil.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
+    }
+}

+ 3 - 6
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java

@@ -1,8 +1,6 @@
 package com.ruoyi.web.controller.system;
 
-import cn.hutool.core.util.StrUtil;
 import com.ruoyi.common.annotation.Log;
-import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
@@ -11,6 +9,7 @@ import com.ruoyi.common.core.domain.model.LoginUser;
 import com.ruoyi.common.enums.BusinessType;
 import com.ruoyi.common.utils.SecurityUtils;
 import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.framework.web.service.TokenService;
 import com.ruoyi.system.service.ISysMenuService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -98,8 +97,7 @@ public class SysMenuController extends BaseController
         {
             return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
         }
-        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
-                && !StrUtil.startWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS))
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
         {
             return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
         }
@@ -119,8 +117,7 @@ public class SysMenuController extends BaseController
         {
             return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
         }
-        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
-                && !StrUtil.startWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS))
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
         {
             return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
         }

+ 56 - 1
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java

@@ -6,6 +6,7 @@ import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.core.domain.model.LoginUser;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.enums.BusinessType;
@@ -14,6 +15,7 @@ import com.ruoyi.common.utils.ServletUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.framework.web.service.SysPermissionService;
 import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.domain.SysUserRole;
 import com.ruoyi.system.service.ISysRoleService;
 import com.ruoyi.system.service.ISysUserService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -25,7 +27,7 @@ import java.util.List;
 
 /**
  * 角色信息
- * 
+ *
  * @author ruoyi
  */
 @RestController
@@ -171,4 +173,57 @@ public class SysRoleController extends BaseController
     {
         return AjaxResult.success(roleService.selectRoleAll());
     }
+
+    /**
+     * 查询已分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/allocatedList")
+    public TableDataInfo allocatedList(SysUser user)
+    {
+		return userService.selectAllocatedList(user);
+    }
+
+    /**
+     * 查询未分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/unallocatedList")
+    public TableDataInfo unallocatedList(SysUser user)
+    {
+        return userService.selectUnallocatedList(user);
+    }
+
+    /**
+     * 取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancel")
+    public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole)
+    {
+        return toAjax(roleService.deleteAuthUser(userRole));
+    }
+
+    /**
+     * 批量取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancelAll")
+    public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
+    {
+        return toAjax(roleService.deleteAuthUsers(roleId, userIds));
+    }
+
+    /**
+     * 批量选择用户授权
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/selectAll")
+    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
+    {
+        return toAjax(roleService.insertAuthUsers(roleId, userIds));
+    }
 }

+ 27 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java

@@ -196,4 +196,31 @@ public class SysUserController extends BaseController
         user.setUpdateBy(SecurityUtils.getUsername());
         return toAjax(userService.updateUserStatus(user));
     }
+
+    /**
+     * 根据用户编号获取授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping("/authRole/{userId}")
+    public AjaxResult authRole(@PathVariable("userId") Long userId)
+    {
+        SysUser user = userService.selectUserById(userId);
+        List<SysRole> roles = roleService.selectRolesByUserId(userId);
+		Map<String, Object> ajax = new HashMap<>();
+        ajax.put("user", user);
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+        return AjaxResult.success(ajax);
+    }
+
+    /**
+     * 用户授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authRole")
+    public AjaxResult insertAuthRole(Long userId, Long[] roleIds)
+    {
+        userService.insertUserAuth(userId, roleIds);
+        return success();
+    }
 }

+ 27 - 0
ruoyi-admin/src/main/resources/application-dev.yml

@@ -110,3 +110,30 @@ redisson:
     subscriptionsPerConnection: 5
     # DNS监测时间间隔,单位:毫秒
     dnsMonitoringInterval: 5000
+
+--- # 监控配置
+spring:
+  boot:
+    admin:
+      # Spring Boot Admin Client 客户端的相关配置
+      client:
+        # 设置 Spring Boot Admin Server 地址
+        url: http://localhost:9090/admin
+        instance:
+          prefer-ip: true # 注册实例时,优先使用 IP
+        username: ruoyi
+        password: 123456
+
+# Actuator 监控端点的配置项
+management:
+  endpoints:
+    web:
+      # Actuator 提供的 API 接口的根目录。默认为 /actuator
+      base-path: /actuator
+      exposure:
+        # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
+        # 生产环境不建议放开所有 根据项目需求放开即可
+        include: '*'
+  endpoint:
+    logfile:
+      external-file: ./logs/sys-console.log

+ 29 - 2
ruoyi-admin/src/main/resources/application-prod.yml

@@ -12,7 +12,7 @@ spring:
         # 主库数据源
         master:
           driverClassName: com.mysql.cj.jdbc.Driver
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true
+          url: jdbc:mysql://172.30.0.36:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true
           username: root
           password: root
         # 从库数据源
@@ -66,7 +66,7 @@ spring:
   # redis 配置
   redis:
     # 地址
-    host: localhost
+    host: 172.30.0.48
     # 端口,默认为6379
     port: 6379
     # 数据库索引
@@ -110,3 +110,30 @@ redisson:
     subscriptionsPerConnection: 5
     # DNS监测时间间隔,单位:毫秒
     dnsMonitoringInterval: 5000
+
+--- # 监控配置
+spring:
+  boot:
+    admin:
+      # Spring Boot Admin Client 客户端的相关配置
+      client:
+        # 设置 Spring Boot Admin Server 地址
+        url: http://172.30.0.90:9090/admin
+        instance:
+          prefer-ip: true # 注册实例时,优先使用 IP
+        username: ruoyi
+        password: 123456
+
+# Actuator 监控端点的配置项
+management:
+  endpoints:
+    web:
+      # Actuator 提供的 API 接口的根目录。默认为 /actuator
+      base-path: /actuator
+      exposure:
+        # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
+        # 生产环境不建议放开所有 根据项目需求放开即可
+        include: health,info
+  endpoint:
+    logfile:
+      external-file: ./logs/sys-console.log

+ 25 - 30
ruoyi-admin/src/main/resources/application.yml

@@ -64,6 +64,8 @@ logging:
 
 # Spring配置
 spring:
+  application:
+    name: ${ruoyi.name}
   # 资源信息
   messages:
     # 国际化资源文件路径
@@ -110,6 +112,8 @@ token:
 # MyBatisPlus配置
 # https://baomidou.com/config/
 mybatis-plus:
+  # 不支持多包, 如有需要可在注解配置 或 提升扫包等级
+  # 例如 com.**.**.mapper
   mapperPackage: com.ruoyi.**.mapper
   # 对应的 XML 文件位置
   mapperLocations: classpath*:mapper/**/*Mapper.xml
@@ -156,7 +160,9 @@ mybatis-plus:
     # STATEMENT 关闭一级缓存
     localCacheScope: SESSION
     # 开启Mybatis二级缓存,默认为 true
-    cacheEnabled: true
+    cacheEnabled: false
+    # 更详细的日志输出 会有性能损耗
+    # logImpl: org.apache.ibatis.logging.stdout.StdOutImpl
   global-config:
     # 是否打印 Logo banner
     banner: true
@@ -203,7 +209,7 @@ swagger:
   # 请求前缀
   pathMapping: /dev-api
   # 标题
-  title: '标题:RuoYi-Vue-Plus后台管理系统_接口文档'
+  title: '标题:${ruoyi.name}后台管理系统_接口文档'
   # 描述
   description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
   # 版本
@@ -244,6 +250,8 @@ thread-pool:
 
 # feign 相关配置
 feign:
+  # 不支持多包, 如有需要可在注解配置 或 提升扫包等级
+  # 例如 com.**.**.feign
   package: com.ruoyi.**.feign
   # 开启压缩
   compression:
@@ -256,6 +264,21 @@ feign:
   circuitbreaker:
     enabled: true
 
+--- # redisson 缓存配置
+redisson:
+  cacheGroup:
+    # 用例: @Cacheable(cacheNames="groupId", key="#XXX") 方可使用缓存组配置
+    - groupId: redissonCacheMap
+      # 组过期时间(脚本监控)
+      ttl: 60000
+      # 组最大空闲时间(脚本监控)
+      maxIdleTime: 60000
+      # 组最大长度
+      maxSize: 0
+    - groupId: testCache
+      ttl: 1000
+      maxIdleTime: 500
+
 --- # 分布式锁 lock4j 全局配置
 lock4j:
   # 获取分布式锁超时时间,默认为 3000 毫秒
@@ -293,31 +316,3 @@ spring:
             tablePrefix: QRTZ_
             # sqlserver 启用
             # selectWithLockSQL: SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?
-
---- # 监控配置
-spring:
-  application:
-    name: ruoyi-vue-plus
-  boot:
-    admin:
-      # Spring Boot Admin Client 客户端的相关配置
-      client:
-        # 设置 Spring Boot Admin Server 地址
-        url: http://localhost:${server.port}${spring.boot.admin.context-path}
-        instance:
-          prefer-ip: true # 注册实例时,优先使用 IP
-      # Spring Boot Admin Server 服务端的相关配置
-      context-path: /admin # 配置 Spring
-
-# Actuator 监控端点的配置项
-management:
-  endpoints:
-    web:
-      # Actuator 提供的 API 接口的根目录。默认为 /actuator
-      base-path: /actuator
-      exposure:
-        # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
-        include: '*'
-  endpoint:
-    logfile:
-      external-file: ./logs/sys-console.log

+ 33 - 0
ruoyi-admin/src/main/resources/i18n/messages_en_US.properties

@@ -0,0 +1,33 @@
+#错误消息
+not.null=
+user.jcaptcha.error=
+user.jcaptcha.expire=
+user.not.exists=
+user.password.not.match=
+user.password.retry.limit.count=
+user.password.retry.limit.exceed=
+user.password.delete=
+user.blocked=
+role.blocked=
+user.logout.success=
+length.not.valid=
+user.username.not.valid=
+user.password.not.valid=
+user.email.not.valid=
+user.mobile.phone.number.not.valid=
+user.login.success=
+user.notfound=
+user.forcelogout=
+user.unknown.error=
+
+##文件上传消息
+upload.exceed.maxSize=
+upload.filename.exceed.length=
+
+##权限
+no.permission=
+no.create.permission=
+no.update.permission=
+no.delete.permission=
+no.export.permission=
+no.view.permission=

+ 36 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties

@@ -0,0 +1,36 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 1 - 5
ruoyi-common/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>${ruoyi-vue-plus.version}</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -116,10 +116,6 @@
             <artifactId>feign-okhttp</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>de.codecentric</groupId>
-            <artifactId>spring-boot-admin-starter-server</artifactId>
-        </dependency>
         <dependency>
             <groupId>de.codecentric</groupId>
             <artifactId>spring-boot-admin-starter-client</artifactId>

+ 4 - 1
ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java

@@ -2,7 +2,7 @@ package com.ruoyi.common.constant;
 
 /**
  * 用户常量信息
- * 
+ *
  * @author ruoyi
  */
 public class UserConstants
@@ -57,6 +57,9 @@ public class UserConstants
     /** ParentView组件标识 */
     public final static String PARENT_VIEW = "ParentView";
 
+    /** InnerLink组件标识 */
+    public final static String INNER_LINK = "InnerLink";
+
     /** 校验返回结果码 */
     public final static String UNIQUE = "0";
     public final static String NOT_UNIQUE = "1";

+ 4 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java

@@ -148,6 +148,10 @@ public class SysUser implements Serializable
     @TableField(exist = false)
     private Long[] postIds;
 
+	/** 角色ID */
+	@TableField(exist = false)
+	private Long roleId;
+
     public SysUser(Long userId)
     {
         this.userId = userId;

+ 3 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/mybatisplus/cache/MybatisPlusRedisCache.java

@@ -15,6 +15,9 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
 /**
  * mybatis-redis 二级缓存
  *
+ * 使用方法 配置文件开启 mybatis-plus 二级缓存
+ * 在 XxxMapper.java 类上添加注解 @CacheNamespace(implementation = MybatisPlusRedisCache.class, eviction = MybatisPlusRedisCache.class)
+ *
  * @author Lion Li
  */
 @Slf4j

+ 27 - 4
ruoyi-common/src/main/java/com/ruoyi/common/core/mybatisplus/methods/InsertAll.java

@@ -1,13 +1,17 @@
 package com.ruoyi.common.core.mybatisplus.methods;
 
 import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.core.enums.SqlMethod;
+import com.baomidou.mybatisplus.core.injector.AbstractMethod;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
+import org.apache.ibatis.executor.keygen.KeyGenerator;
 import org.apache.ibatis.executor.keygen.NoKeyGenerator;
 import org.apache.ibatis.mapping.MappedStatement;
 import org.apache.ibatis.mapping.SqlSource;
 
-import com.baomidou.mybatisplus.core.injector.AbstractMethod;
-import com.baomidou.mybatisplus.core.metadata.TableInfo;
-
 /**
  * 单sql批量插入
  *
@@ -20,9 +24,28 @@ public class InsertAll extends AbstractMethod {
 		final String sql = "<script>insert into %s %s values %s</script>";
 		final String fieldSql = prepareFieldSql(tableInfo);
 		final String valueSql = prepareValuesSqlForMysqlBatch(tableInfo);
+		KeyGenerator keyGenerator = new NoKeyGenerator();
+		SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
+		String keyProperty = null;
+		String keyColumn = null;
+		// 表包含主键处理逻辑,如果不包含主键当普通字段处理
+		if (StrUtil.isNotBlank(tableInfo.getKeyProperty())) {
+			if (tableInfo.getIdType() == IdType.AUTO) {
+				/** 自增主键 */
+				keyGenerator = new Jdbc3KeyGenerator();
+				keyProperty = tableInfo.getKeyProperty();
+				keyColumn = tableInfo.getKeyColumn();
+			} else {
+				if (null != tableInfo.getKeySequence()) {
+					keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), tableInfo, builderAssistant);
+					keyProperty = tableInfo.getKeyProperty();
+					keyColumn = tableInfo.getKeyColumn();
+				}
+			}
+		}
 		final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);
 		SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
-		return this.addInsertMappedStatement(mapperClass, modelClass, "insertAll", sqlSource, new NoKeyGenerator(), null, null);
+		return this.addInsertMappedStatement(mapperClass, modelClass, "insertAll", sqlSource, keyGenerator, keyProperty, keyColumn);
 	}
 
 	private String prepareFieldSql(TableInfo tableInfo) {

+ 3 - 3
ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java

@@ -205,9 +205,9 @@ public class RedisCache {
 	 * @param hKeys Hash键集合
 	 * @return Hash对象集合
 	 */
-	public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
-		RListMultimap rListMultimap = redissonClient.getListMultimap(key);
-		return rListMultimap.getAll(hKeys);
+	public <K,V> Map<K,V> getMultiCacheMapValue(final String key, final Set<K> hKeys) {
+		RMap<K,V>  rMap = redissonClient.getMap(key);
+		return rMap.getAll(hKeys);
 	}
 
 	/**

+ 2 - 2
ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java

@@ -88,7 +88,7 @@ public class DictUtils
         StringBuilder propertyString = new StringBuilder();
         List<SysDictData> datas = getDictCache(dictType);
 
-        if (StrUtil.containsAny(separator, dictValue) && CollUtil.isNotEmpty(datas))
+        if (StrUtil.containsAny(dictValue, separator) && CollUtil.isNotEmpty(datas))
         {
             for (SysDictData dict : datas)
             {
@@ -128,7 +128,7 @@ public class DictUtils
         StringBuilder propertyString = new StringBuilder();
         List<SysDictData> datas = getDictCache(dictType);
 
-        if (StrUtil.containsAny(separator, dictLabel) && CollUtil.isNotEmpty(datas))
+        if (StrUtil.containsAny(dictLabel, separator) && CollUtil.isNotEmpty(datas))
         {
             for (SysDictData dict : datas)
             {

+ 28 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java

@@ -0,0 +1,28 @@
+package com.ruoyi.common.utils;
+
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.constant.Constants;
+
+/**
+ * 字符串工具类
+ *
+ * @author ruoyi
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+	/** 空字符串 */
+	private static final String NULLSTR = "";
+
+	/** 下划线 */
+	private static final char SEPARATOR = '_';
+
+	/**
+	 * 是否为http(s)://开头
+	 *
+	 * @param link 链接
+	 * @return 结果
+	 */
+	public static boolean ishttp(String link) {
+		return StrUtil.startWithAny(link, Constants.HTTP, Constants.HTTPS);
+	}
+
+}

+ 1072 - 1072
ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java

@@ -1,1072 +1,1072 @@
-package com.ruoyi.common.utils.poi;
-
-import cn.hutool.core.convert.Convert;
-import cn.hutool.core.lang.Validator;
-import cn.hutool.core.util.StrUtil;
-import com.ruoyi.common.annotation.Excel;
-import com.ruoyi.common.annotation.Excel.ColumnType;
-import com.ruoyi.common.annotation.Excel.Type;
-import com.ruoyi.common.annotation.Excels;
-import com.ruoyi.common.config.RuoYiConfig;
-import com.ruoyi.common.core.domain.AjaxResult;
-import com.ruoyi.common.exception.CustomException;
-import com.ruoyi.common.utils.DateUtils;
-import com.ruoyi.common.utils.DictUtils;
-import com.ruoyi.common.utils.file.FileTypeUtils;
-import com.ruoyi.common.utils.file.ImageUtils;
-import com.ruoyi.common.utils.reflect.ReflectUtils;
-import org.apache.poi.ss.usermodel.*;
-import org.apache.poi.ss.util.CellRangeAddressList;
-import org.apache.poi.xssf.streaming.SXSSFWorkbook;
-import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
-import org.apache.poi.xssf.usermodel.XSSFDataValidation;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.*;
-import java.lang.reflect.Field;
-import java.math.BigDecimal;
-import java.text.DecimalFormat;
-import java.util.*;
-import java.util.stream.Collectors;
-
-/**
- * Excel相关处理
- * 
- * @author ruoyi
- */
-public class ExcelUtil<T>
-{
-    private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);
-
-    /**
-     * Excel sheet最大行数,默认65536
-     */
-    public static final int sheetSize = 65536;
-
-    /**
-     * 工作表名称
-     */
-    private String sheetName;
-
-    /**
-     * 导出类型(EXPORT:导出数据;IMPORT:导入模板)
-     */
-    private Type type;
-
-    /**
-     * 工作薄对象
-     */
-    private Workbook wb;
-
-    /**
-     * 工作表对象
-     */
-    private Sheet sheet;
-
-    /**
-     * 样式列表
-     */
-    private Map<String, CellStyle> styles;
-
-    /**
-     * 导入导出数据列表
-     */
-    private List<T> list;
-
-    /**
-     * 注解列表
-     */
-    private List<Object[]> fields;
-
-    /**
-     * 最大高度
-     */
-    private short maxHeight;
-
-    /**
-     * 统计列表
-     */
-    private Map<Integer, Double> statistics = new HashMap<Integer, Double>();
-    
-    /**
-     * 数字格式
-     */
-    private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");
-    
-    /**
-     * 实体对象
-     */
-    public Class<T> clazz;
-
-    public ExcelUtil(Class<T> clazz)
-    {
-        this.clazz = clazz;
-    }
-
-    public void init(List<T> list, String sheetName, Type type)
-    {
-        if (list == null)
-        {
-            list = new ArrayList<T>();
-        }
-        this.list = list;
-        this.sheetName = sheetName;
-        this.type = type;
-        createExcelField();
-        createWorkbook();
-    }
-
-    /**
-     * 对excel表单默认第一个索引名转换成list
-     * 
-     * @param is 输入流
-     * @return 转换后集合
-     */
-    public List<T> importExcel(InputStream is) throws Exception
-    {
-        return importExcel(StrUtil.EMPTY, is);
-    }
-
-    /**
-     * 对excel表单指定表格索引名转换成list
-     * 
-     * @param sheetName 表格索引名
-     * @param is 输入流
-     * @return 转换后集合
-     */
-    public List<T> importExcel(String sheetName, InputStream is) throws Exception
-    {
-        this.type = Type.IMPORT;
-        this.wb = WorkbookFactory.create(is);
-        List<T> list = new ArrayList<T>();
-        Sheet sheet = null;
-        if (Validator.isNotEmpty(sheetName))
-        {
-            // 如果指定sheet名,则取指定sheet中的内容.
-            sheet = wb.getSheet(sheetName);
-        }
-        else
-        {
-            // 如果传入的sheet名不存在则默认指向第1个sheet.
-            sheet = wb.getSheetAt(0);
-        }
-
-        if (sheet == null)
-        {
-            throw new IOException("文件sheet不存在");
-        }
-
-        int rows = sheet.getPhysicalNumberOfRows();
-
-        if (rows > 0)
-        {
-            // 定义一个map用于存放excel列的序号和field.
-            Map<String, Integer> cellMap = new HashMap<String, Integer>();
-            // 获取表头
-            Row heard = sheet.getRow(0);
-            for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
-            {
-                Cell cell = heard.getCell(i);
-                if (Validator.isNotNull(cell))
-                {
-                    String value = this.getCellValue(heard, i).toString();
-                    cellMap.put(value, i);
-                }
-                else
-                {
-                    cellMap.put(null, i);
-                }
-            }
-            // 有数据时才处理 得到类的所有field.
-            Field[] allFields = clazz.getDeclaredFields();
-            // 定义一个map用于存放列的序号和field.
-            Map<Integer, Field> fieldsMap = new HashMap<Integer, Field>();
-            for (int col = 0; col < allFields.length; col++)
-            {
-                Field field = allFields[col];
-                Excel attr = field.getAnnotation(Excel.class);
-                if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
-                {
-                    // 设置类的私有字段属性可访问.
-                    field.setAccessible(true);
-                    Integer column = cellMap.get(attr.name());
-                    if (column != null)
-                    {
-                        fieldsMap.put(column, field);
-                    }
-                }
-            }
-            for (int i = 1; i < rows; i++)
-            {
-                // 从第2行开始取数据,默认第一行是表头.
-                Row row = sheet.getRow(i);
-                if(row == null)
-                {
-                    continue;
-                }
-                T entity = null;
-                for (Map.Entry<Integer, Field> entry : fieldsMap.entrySet())
-                {
-                    Object val = this.getCellValue(row, entry.getKey());
-
-                    // 如果不存在实例则新建.
-                    entity = (entity == null ? clazz.newInstance() : entity);
-                    // 从map中得到对应列的field.
-                    Field field = fieldsMap.get(entry.getKey());
-                    // 取得类型,并根据对象类型设置值.
-                    Class<?> fieldType = field.getType();
-                    if (String.class == fieldType)
-                    {
-                        String s = Convert.toStr(val);
-                        if (StrUtil.endWith(s, ".0"))
-                        {
-                            val = StrUtil.subBefore(s, ".0",false);
-                        }
-                        else
-                        {
-                            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
-                            if (Validator.isNotEmpty(dateFormat))
-                            {
-                                val = DateUtils.parseDateToStr(dateFormat, (Date) val);
-                            }
-                            else
-                            {
-                                val = Convert.toStr(val);
-                            }
-                        }
-                    }
-                    else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && Validator.isNumber(Convert.toStr(val)))
-                    {
-                        val = Convert.toInt(val);
-                    }
-                    else if (Long.TYPE == fieldType || Long.class == fieldType)
-                    {
-                        val = Convert.toLong(val);
-                    }
-                    else if (Double.TYPE == fieldType || Double.class == fieldType)
-                    {
-                        val = Convert.toDouble(val);
-                    }
-                    else if (Float.TYPE == fieldType || Float.class == fieldType)
-                    {
-                        val = Convert.toFloat(val);
-                    }
-                    else if (BigDecimal.class == fieldType)
-                    {
-                        val = Convert.toBigDecimal(val);
-                    }
-                    else if (Date.class == fieldType)
-                    {
-                        if (val instanceof String)
-                        {
-                            val = DateUtils.parseDate(val);
-                        }
-                        else if (val instanceof Double)
-                        {
-                            val = DateUtil.getJavaDate((Double) val);
-                        }
-                    }
-                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
-                    {
-                        val = Convert.toBool(val, false);
-                    }
-                    if (Validator.isNotNull(fieldType))
-                    {
-                        Excel attr = field.getAnnotation(Excel.class);
-                        String propertyName = field.getName();
-                        if (Validator.isNotEmpty(attr.targetAttr()))
-                        {
-                            propertyName = field.getName() + "." + attr.targetAttr();
-                        }
-                        else if (Validator.isNotEmpty(attr.readConverterExp()))
-                        {
-                            val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
-                        }
-                        else if (Validator.isNotEmpty(attr.dictType()))
-                        {
-                            val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator());
-                        }
-                        ReflectUtils.invokeSetter(entity, propertyName, val);
-                    }
-                }
-                list.add(entity);
-            }
-        }
-        return list;
-    }
-
-    /**
-     * 对list数据源将其里面的数据导入到excel表单
-     * 
-     * @param list 导出数据集合
-     * @param sheetName 工作表的名称
-     * @return 结果
-     */
-    public AjaxResult exportExcel(List<T> list, String sheetName)
-    {
-        this.init(list, sheetName, Type.EXPORT);
-        return exportExcel();
-    }
-
-    /**
-     * 对list数据源将其里面的数据导入到excel表单
-     * 
-     * @param sheetName 工作表的名称
-     * @return 结果
-     */
-    public AjaxResult importTemplateExcel(String sheetName)
-    {
-        this.init(null, sheetName, Type.IMPORT);
-        return exportExcel();
-    }
-
-    /**
-     * 对list数据源将其里面的数据导入到excel表单
-     * 
-     * @return 结果
-     */
-    public AjaxResult exportExcel()
-    {
-        OutputStream out = null;
-        try
-        {
-            // 取出一共有多少个sheet.
-            double sheetNo = Math.ceil(list.size() / sheetSize);
-            for (int index = 0; index <= sheetNo; index++)
-            {
-                createSheet(sheetNo, index);
-
-                // 产生一行
-                Row row = sheet.createRow(0);
-                int column = 0;
-                // 写入各个字段的列头名称
-                for (Object[] os : fields)
-                {
-                    Excel excel = (Excel) os[1];
-                    this.createCell(excel, row, column++);
-                }
-                if (Type.EXPORT.equals(type))
-                {
-                    fillExcelData(index, row);
-                    addStatisticsRow();
-                }
-            }
-            String filename = encodingFilename(sheetName);
-            out = new FileOutputStream(getAbsoluteFile(filename));
-            wb.write(out);
-            return AjaxResult.success(filename);
-        }
-        catch (Exception e)
-        {
-            log.error("导出Excel异常{}", e.getMessage());
-            throw new CustomException("导出Excel失败,请联系网站管理员!");
-        }
-        finally
-        {
-            if (wb != null)
-            {
-                try
-                {
-                    wb.close();
-                }
-                catch (IOException e1)
-                {
-                    e1.printStackTrace();
-                }
-            }
-            if (out != null)
-            {
-                try
-                {
-                    out.close();
-                }
-                catch (IOException e1)
-                {
-                    e1.printStackTrace();
-                }
-            }
-        }
-    }
-
-    /**
-     * 填充excel数据
-     * 
-     * @param index 序号
-     * @param row 单元格行
-     */
-    public void fillExcelData(int index, Row row)
-    {
-        int startNo = index * sheetSize;
-        int endNo = Math.min(startNo + sheetSize, list.size());
-        for (int i = startNo; i < endNo; i++)
-        {
-            row = sheet.createRow(i + 1 - startNo);
-            // 得到导出对象.
-            T vo = (T) list.get(i);
-            int column = 0;
-            for (Object[] os : fields)
-            {
-                Field field = (Field) os[0];
-                Excel excel = (Excel) os[1];
-                // 设置实体类私有属性可访问
-                field.setAccessible(true);
-                this.addCell(excel, row, vo, field, column++);
-            }
-        }
-    }
-
-    /**
-     * 创建表格样式
-     * 
-     * @param wb 工作薄对象
-     * @return 样式列表
-     */
-    private Map<String, CellStyle> createStyles(Workbook wb)
-    {
-        // 写入各条记录,每条记录对应excel表中的一行
-        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
-        CellStyle style = wb.createCellStyle();
-        style.setAlignment(HorizontalAlignment.CENTER);
-        style.setVerticalAlignment(VerticalAlignment.CENTER);
-        style.setBorderRight(BorderStyle.THIN);
-        style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
-        style.setBorderLeft(BorderStyle.THIN);
-        style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
-        style.setBorderTop(BorderStyle.THIN);
-        style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
-        style.setBorderBottom(BorderStyle.THIN);
-        style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
-        Font dataFont = wb.createFont();
-        dataFont.setFontName("Arial");
-        dataFont.setFontHeightInPoints((short) 10);
-        style.setFont(dataFont);
-        styles.put("data", style);
-
-        style = wb.createCellStyle();
-        style.cloneStyleFrom(styles.get("data"));
-        style.setAlignment(HorizontalAlignment.CENTER);
-        style.setVerticalAlignment(VerticalAlignment.CENTER);
-        style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
-        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
-        Font headerFont = wb.createFont();
-        headerFont.setFontName("Arial");
-        headerFont.setFontHeightInPoints((short) 10);
-        headerFont.setBold(true);
-        headerFont.setColor(IndexedColors.WHITE.getIndex());
-        style.setFont(headerFont);
-        styles.put("header", style);
-        
-        style = wb.createCellStyle();
-        style.setAlignment(HorizontalAlignment.CENTER);
-        style.setVerticalAlignment(VerticalAlignment.CENTER);
-        Font totalFont = wb.createFont();
-        totalFont.setFontName("Arial");
-        totalFont.setFontHeightInPoints((short) 10);
-        style.setFont(totalFont);
-        styles.put("total", style);
-
-        style = wb.createCellStyle();
-        style.cloneStyleFrom(styles.get("data"));
-        style.setAlignment(HorizontalAlignment.LEFT);
-        styles.put("data1", style);
-
-        style = wb.createCellStyle();
-        style.cloneStyleFrom(styles.get("data"));
-        style.setAlignment(HorizontalAlignment.CENTER);
-        styles.put("data2", style);
-
-        style = wb.createCellStyle();
-        style.cloneStyleFrom(styles.get("data"));
-        style.setAlignment(HorizontalAlignment.RIGHT);
-        styles.put("data3", style);
-
-        return styles;
-    }
-
-    /**
-     * 创建单元格
-     */
-    public Cell createCell(Excel attr, Row row, int column)
-    {
-        // 创建列
-        Cell cell = row.createCell(column);
-        // 写入列信息
-        cell.setCellValue(attr.name());
-        setDataValidation(attr, row, column);
-        cell.setCellStyle(styles.get("header"));
-        return cell;
-    }
-
-    /**
-     * 设置单元格信息
-     * 
-     * @param value 单元格值
-     * @param attr 注解相关
-     * @param cell 单元格信息
-     */
-    public void setCellVo(Object value, Excel attr, Cell cell)
-    {
-        if (ColumnType.STRING == attr.cellType())
-        {
-            cell.setCellValue(Validator.isNull(value) ? attr.defaultValue() : value + attr.suffix());
-        }
-        else if (ColumnType.NUMERIC == attr.cellType())
-        {
-            if (Validator.isNotNull(value))
-            {
-                cell.setCellValue(StrUtil.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value));
-            }
-        }
-        else if (ColumnType.IMAGE == attr.cellType())
-        {
-            ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1),
-                    cell.getRow().getRowNum() + 1);
-            String imagePath = Convert.toStr(value);
-            if (Validator.isNotEmpty(imagePath))
-            {
-                byte[] data = ImageUtils.getImage(imagePath);
-                getDrawingPatriarch(cell.getSheet()).createPicture(anchor,
-                        cell.getSheet().getWorkbook().addPicture(data, getImageType(data)));
-            }
-        }
-    }
-    
-    /**
-     * 获取画布
-     */
-    public static Drawing<?> getDrawingPatriarch(Sheet sheet)
-    {
-        if (sheet.getDrawingPatriarch() == null)
-        {
-            sheet.createDrawingPatriarch();
-        }
-        return sheet.getDrawingPatriarch();
-    }
-
-    /**
-     * 获取图片类型,设置图片插入类型
-     */
-    public int getImageType(byte[] value)
-    {
-        String type = FileTypeUtils.getFileExtendName(value);
-        if ("JPG".equalsIgnoreCase(type))
-        {
-            return Workbook.PICTURE_TYPE_JPEG;
-        }
-        else if ("PNG".equalsIgnoreCase(type))
-        {
-            return Workbook.PICTURE_TYPE_PNG;
-        }
-        return Workbook.PICTURE_TYPE_JPEG;
-    }
-
-    /**
-     * 创建表格样式
-     */
-    public void setDataValidation(Excel attr, Row row, int column)
-    {
-        if (attr.name().indexOf("注:") >= 0)
-        {
-            sheet.setColumnWidth(column, 6000);
-        }
-        else
-        {
-            // 设置列宽
-            sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256));
-        }
-        // 如果设置了提示信息则鼠标放上去提示.
-        if (Validator.isNotEmpty(attr.prompt()))
-        {
-            // 这里默认设了2-101列提示.
-            setXSSFPrompt(sheet, "", attr.prompt(), 1, 100, column, column);
-        }
-        // 如果设置了combo属性则本列只能选择不能输入
-        if (attr.combo().length > 0)
-        {
-            // 这里默认设了2-101列只能选择不能输入.
-            setXSSFValidation(sheet, attr.combo(), 1, 100, column, column);
-        }
-    }
-
-    /**
-     * 添加单元格
-     */
-    public Cell addCell(Excel attr, Row row, T vo, Field field, int column)
-    {
-        Cell cell = null;
-        try
-        {
-            // 设置行高
-            row.setHeight(maxHeight);
-            // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
-            if (attr.isExport())
-            {
-                // 创建cell
-                cell = row.createCell(column);
-                int align = attr.align().value();
-                cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : "")));
-
-                // 用于读取对象中的属性
-                Object value = getTargetValue(vo, field, attr);
-                String dateFormat = attr.dateFormat();
-                String readConverterExp = attr.readConverterExp();
-                String separator = attr.separator();
-                String dictType = attr.dictType();
-                if (Validator.isNotEmpty(dateFormat) && Validator.isNotNull(value))
-                {
-                    cell.setCellValue(DateUtils.parseDateToStr(dateFormat, (Date) value));
-                }
-                else if (Validator.isNotEmpty(readConverterExp) && Validator.isNotNull(value))
-                {
-                    cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator));
-                }
-                else if (Validator.isNotEmpty(dictType) && Validator.isNotNull(value))
-                {
-                    cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator));
-                }
-                else if (value instanceof BigDecimal && -1 != attr.scale())
-                {
-                    cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString());
-                }
-                else
-                {
-                    // 设置列类型
-                    setCellVo(value, attr, cell);
-                }
-                addStatisticsData(column, Convert.toStr(value), attr);
-            }
-        }
-        catch (Exception e)
-        {
-            log.error("导出Excel失败{}", e);
-        }
-        return cell;
-    }
-
-    /**
-     * 设置 POI XSSFSheet 单元格提示
-     * 
-     * @param sheet 表单
-     * @param promptTitle 提示标题
-     * @param promptContent 提示内容
-     * @param firstRow 开始行
-     * @param endRow 结束行
-     * @param firstCol 开始列
-     * @param endCol 结束列
-     */
-    public void setXSSFPrompt(Sheet sheet, String promptTitle, String promptContent, int firstRow, int endRow,
-            int firstCol, int endCol)
-    {
-        DataValidationHelper helper = sheet.getDataValidationHelper();
-        DataValidationConstraint constraint = helper.createCustomConstraint("DD1");
-        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
-        DataValidation dataValidation = helper.createValidation(constraint, regions);
-        dataValidation.createPromptBox(promptTitle, promptContent);
-        dataValidation.setShowPromptBox(true);
-        sheet.addValidationData(dataValidation);
-    }
-
-    /**
-     * 设置某些列的值只能输入预制的数据,显示下拉框.
-     * 
-     * @param sheet 要设置的sheet.
-     * @param textlist 下拉框显示的内容
-     * @param firstRow 开始行
-     * @param endRow 结束行
-     * @param firstCol 开始列
-     * @param endCol 结束列
-     * @return 设置好的sheet.
-     */
-    public void setXSSFValidation(Sheet sheet, String[] textlist, int firstRow, int endRow, int firstCol, int endCol)
-    {
-        DataValidationHelper helper = sheet.getDataValidationHelper();
-        // 加载下拉列表内容
-        DataValidationConstraint constraint = helper.createExplicitListConstraint(textlist);
-        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
-        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
-        // 数据有效性对象
-        DataValidation dataValidation = helper.createValidation(constraint, regions);
-        // 处理Excel兼容性问题
-        if (dataValidation instanceof XSSFDataValidation)
-        {
-            dataValidation.setSuppressDropDownArrow(true);
-            dataValidation.setShowErrorBox(true);
-        }
-        else
-        {
-            dataValidation.setSuppressDropDownArrow(false);
-        }
-
-        sheet.addValidationData(dataValidation);
-    }
-
-    /**
-     * 解析导出值 0=男,1=女,2=未知
-     * 
-     * @param propertyValue 参数值
-     * @param converterExp 翻译注解
-     * @param separator 分隔符
-     * @return 解析后值
-     */
-    public static String convertByExp(String propertyValue, String converterExp, String separator)
-    {
-        StringBuilder propertyString = new StringBuilder();
-        String[] convertSource = converterExp.split(",");
-        for (String item : convertSource)
-        {
-            String[] itemArray = item.split("=");
-            if (StrUtil.containsAny(separator, propertyValue))
-            {
-                for (String value : propertyValue.split(separator))
-                {
-                    if (itemArray[0].equals(value))
-                    {
-                        propertyString.append(itemArray[1] + separator);
-                        break;
-                    }
-                }
-            }
-            else
-            {
-                if (itemArray[0].equals(propertyValue))
-                {
-                    return itemArray[1];
-                }
-            }
-        }
-        return StrUtil.strip(propertyString.toString(), null,separator);
-    }
-
-    /**
-     * 反向解析值 男=0,女=1,未知=2
-     * 
-     * @param propertyValue 参数值
-     * @param converterExp 翻译注解
-     * @param separator 分隔符
-     * @return 解析后值
-     */
-    public static String reverseByExp(String propertyValue, String converterExp, String separator)
-    {
-        StringBuilder propertyString = new StringBuilder();
-        String[] convertSource = converterExp.split(",");
-        for (String item : convertSource)
-        {
-            String[] itemArray = item.split("=");
-            if (StrUtil.containsAny(separator, propertyValue))
-            {
-                for (String value : propertyValue.split(separator))
-                {
-                    if (itemArray[1].equals(value))
-                    {
-                        propertyString.append(itemArray[0] + separator);
-                        break;
-                    }
-                }
-            }
-            else
-            {
-                if (itemArray[1].equals(propertyValue))
-                {
-                    return itemArray[0];
-                }
-            }
-        }
-        return StrUtil.strip(propertyString.toString(), null,separator);
-    }
-    
-    /**
-     * 解析字典值
-     * 
-     * @param dictValue 字典值
-     * @param dictType 字典类型
-     * @param separator 分隔符
-     * @return 字典标签
-     */
-    public static String convertDictByExp(String dictValue, String dictType, String separator)
-    {
-        return DictUtils.getDictLabel(dictType, dictValue, separator);
-    }
-
-    /**
-     * 反向解析值字典值
-     * 
-     * @param dictLabel 字典标签
-     * @param dictType 字典类型
-     * @param separator 分隔符
-     * @return 字典值
-     */
-    public static String reverseDictByExp(String dictLabel, String dictType, String separator)
-    {
-        return DictUtils.getDictValue(dictType, dictLabel, separator);
-    }
-    
-    /**
-     * 合计统计信息
-     */
-    private void addStatisticsData(Integer index, String text, Excel entity)
-    {
-        if (entity != null && entity.isStatistics())
-        {
-            Double temp = 0D;
-            if (!statistics.containsKey(index))
-            {
-                statistics.put(index, temp);
-            }
-            try
-            {
-                temp = Double.valueOf(text);
-            }
-            catch (NumberFormatException e)
-            {
-            }
-            statistics.put(index, statistics.get(index) + temp);
-        }
-    }
-
-    /**
-     * 创建统计行
-     */
-    public void addStatisticsRow()
-    {
-        if (statistics.size() > 0)
-        {
-            Cell cell = null;
-            Row row = sheet.createRow(sheet.getLastRowNum() + 1);
-            Set<Integer> keys = statistics.keySet();
-            cell = row.createCell(0);
-            cell.setCellStyle(styles.get("total"));
-            cell.setCellValue("合计");
-            
-            for (Integer key : keys)
-            {
-                cell = row.createCell(key);
-                cell.setCellStyle(styles.get("total"));
-                cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key)));
-            }
-            statistics.clear();
-        }
-    }
-
-    /**
-     * 编码文件名
-     */
-    public String encodingFilename(String filename)
-    {
-        filename = UUID.randomUUID().toString() + "_" + filename + ".xlsx";
-        return filename;
-    }
-
-    /**
-     * 获取下载路径
-     * 
-     * @param filename 文件名称
-     */
-    public String getAbsoluteFile(String filename)
-    {
-        String downloadPath = RuoYiConfig.getDownloadPath() + filename;
-        File desc = new File(downloadPath);
-        if (!desc.getParentFile().exists())
-        {
-            desc.getParentFile().mkdirs();
-        }
-        return downloadPath;
-    }
-
-    /**
-     * 获取bean中的属性值
-     * 
-     * @param vo 实体对象
-     * @param field 字段
-     * @param excel 注解
-     * @return 最终的属性值
-     * @throws Exception
-     */
-    private Object getTargetValue(T vo, Field field, Excel excel) throws Exception
-    {
-        Object o = field.get(vo);
-        if (Validator.isNotEmpty(excel.targetAttr()))
-        {
-            String target = excel.targetAttr();
-            if (target.contains("."))
-            {
-                String[] targets = target.split("[.]");
-                for (String name : targets)
-                {
-                    o = getValue(o, name);
-                }
-            }
-            else
-            {
-                o = getValue(o, target);
-            }
-        }
-        return o;
-    }
-
-    /**
-     * 以类的属性的get方法方法形式获取值
-     * 
-     * @param o
-     * @param name
-     * @return value
-     * @throws Exception
-     */
-    private Object getValue(Object o, String name) throws Exception
-    {
-        if (Validator.isNotNull(o) && Validator.isNotEmpty(name))
-        {
-            Class<?> clazz = o.getClass();
-            Field field = clazz.getDeclaredField(name);
-            field.setAccessible(true);
-            o = field.get(o);
-        }
-        return o;
-    }
-
-    /**
-     * 得到所有定义字段
-     */
-    private void createExcelField()
-    {
-        this.fields = new ArrayList<Object[]>();
-        List<Field> tempFields = new ArrayList<>();
-        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
-        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
-        for (Field field : tempFields)
-        {
-            // 单注解
-            if (field.isAnnotationPresent(Excel.class))
-            {
-                putToField(field, field.getAnnotation(Excel.class));
-            }
-
-            // 多注解
-            if (field.isAnnotationPresent(Excels.class))
-            {
-                Excels attrs = field.getAnnotation(Excels.class);
-                Excel[] excels = attrs.value();
-                for (Excel excel : excels)
-                {
-                    putToField(field, excel);
-                }
-            }
-        }
-        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
-        this.maxHeight = getRowHeight();
-    }
-    
-    /**
-     * 根据注解获取最大行高
-     */
-    public short getRowHeight()
-    {
-        double maxHeight = 0;
-        for (Object[] os : this.fields)
-        {
-            Excel excel = (Excel) os[1];
-            maxHeight = maxHeight > excel.height() ? maxHeight : excel.height();
-        }
-        return (short) (maxHeight * 20);
-    }
-
-    /**
-     * 放到字段集合中
-     */
-    private void putToField(Field field, Excel attr)
-    {
-        if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
-        {
-            this.fields.add(new Object[] { field, attr });
-        }
-    }
-
-    /**
-     * 创建一个工作簿
-     */
-    public void createWorkbook()
-    {
-        this.wb = new SXSSFWorkbook(500);
-    }
-
-    /**
-     * 创建工作表
-     * 
-     * @param sheetNo sheet数量
-     * @param index 序号
-     */
-    public void createSheet(double sheetNo, int index)
-    {
-        this.sheet = wb.createSheet();
-        this.styles = createStyles(wb);
-        // 设置工作表的名称.
-        if (sheetNo == 0)
-        {
-            wb.setSheetName(index, sheetName);
-        }
-        else
-        {
-            wb.setSheetName(index, sheetName + index);
-        }
-    }
-
-    /**
-     * 获取单元格值
-     * 
-     * @param row 获取的行
-     * @param column 获取单元格列号
-     * @return 单元格值
-     */
-    public Object getCellValue(Row row, int column)
-    {
-        if (row == null)
-        {
-            return row;
-        }
-        Object val = "";
-        try
-        {
-            Cell cell = row.getCell(column);
-            if (Validator.isNotNull(cell))
-            {
-                if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA)
-                {
-                    val = cell.getNumericCellValue();
-                    if (DateUtil.isCellDateFormatted(cell))
-                    {
-                        val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
-                    }
-                    else
-                    {
-                        if ((Double) val % 1 != 0)
-                        {
-                            val = new BigDecimal(val.toString());
-                        }
-                        else
-                        {
-                            val = new DecimalFormat("0").format(val);
-                        }
-                    }
-                }
-                else if (cell.getCellType() == CellType.STRING)
-                {
-                    val = cell.getStringCellValue();
-                }
-                else if (cell.getCellType() == CellType.BOOLEAN)
-                {
-                    val = cell.getBooleanCellValue();
-                }
-                else if (cell.getCellType() == CellType.ERROR)
-                {
-                    val = cell.getErrorCellValue();
-                }
-
-            }
-        }
-        catch (Exception e)
-        {
-            return val;
-        }
-        return val;
-    }
-}
+package com.ruoyi.common.utils.poi;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.annotation.Excel.Type;
+import com.ruoyi.common.annotation.Excels;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.exception.CustomException;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.DictUtils;
+import com.ruoyi.common.utils.file.FileTypeUtils;
+import com.ruoyi.common.utils.file.ImageUtils;
+import com.ruoyi.common.utils.reflect.ReflectUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Excel相关处理
+ *
+ * @author ruoyi
+ */
+public class ExcelUtil<T>
+{
+    private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);
+
+    /**
+     * Excel sheet最大行数,默认65536
+     */
+    public static final int sheetSize = 65536;
+
+    /**
+     * 工作表名称
+     */
+    private String sheetName;
+
+    /**
+     * 导出类型(EXPORT:导出数据;IMPORT:导入模板)
+     */
+    private Type type;
+
+    /**
+     * 工作薄对象
+     */
+    private Workbook wb;
+
+    /**
+     * 工作表对象
+     */
+    private Sheet sheet;
+
+    /**
+     * 样式列表
+     */
+    private Map<String, CellStyle> styles;
+
+    /**
+     * 导入导出数据列表
+     */
+    private List<T> list;
+
+    /**
+     * 注解列表
+     */
+    private List<Object[]> fields;
+
+    /**
+     * 最大高度
+     */
+    private short maxHeight;
+
+    /**
+     * 统计列表
+     */
+    private Map<Integer, Double> statistics = new HashMap<Integer, Double>();
+
+    /**
+     * 数字格式
+     */
+    private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");
+
+    /**
+     * 实体对象
+     */
+    public Class<T> clazz;
+
+    public ExcelUtil(Class<T> clazz)
+    {
+        this.clazz = clazz;
+    }
+
+    public void init(List<T> list, String sheetName, Type type)
+    {
+        if (list == null)
+        {
+            list = new ArrayList<T>();
+        }
+        this.list = list;
+        this.sheetName = sheetName;
+        this.type = type;
+        createExcelField();
+        createWorkbook();
+    }
+
+    /**
+     * 对excel表单默认第一个索引名转换成list
+     *
+     * @param is 输入流
+     * @return 转换后集合
+     */
+    public List<T> importExcel(InputStream is) throws Exception
+    {
+        return importExcel(StrUtil.EMPTY, is);
+    }
+
+    /**
+     * 对excel表单指定表格索引名转换成list
+     *
+     * @param sheetName 表格索引名
+     * @param is 输入流
+     * @return 转换后集合
+     */
+    public List<T> importExcel(String sheetName, InputStream is) throws Exception
+    {
+        this.type = Type.IMPORT;
+        this.wb = WorkbookFactory.create(is);
+        List<T> list = new ArrayList<T>();
+        Sheet sheet = null;
+        if (Validator.isNotEmpty(sheetName))
+        {
+            // 如果指定sheet名,则取指定sheet中的内容.
+            sheet = wb.getSheet(sheetName);
+        }
+        else
+        {
+            // 如果传入的sheet名不存在则默认指向第1个sheet.
+            sheet = wb.getSheetAt(0);
+        }
+
+        if (sheet == null)
+        {
+            throw new IOException("文件sheet不存在");
+        }
+
+        int rows = sheet.getPhysicalNumberOfRows();
+
+        if (rows > 0)
+        {
+            // 定义一个map用于存放excel列的序号和field.
+            Map<String, Integer> cellMap = new HashMap<String, Integer>();
+            // 获取表头
+            Row heard = sheet.getRow(0);
+            for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
+            {
+                Cell cell = heard.getCell(i);
+                if (Validator.isNotNull(cell))
+                {
+                    String value = this.getCellValue(heard, i).toString();
+                    cellMap.put(value, i);
+                }
+                else
+                {
+                    cellMap.put(null, i);
+                }
+            }
+            // 有数据时才处理 得到类的所有field.
+            Field[] allFields = clazz.getDeclaredFields();
+            // 定义一个map用于存放列的序号和field.
+            Map<Integer, Field> fieldsMap = new HashMap<Integer, Field>();
+            for (int col = 0; col < allFields.length; col++)
+            {
+                Field field = allFields[col];
+                Excel attr = field.getAnnotation(Excel.class);
+                if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
+                {
+                    // 设置类的私有字段属性可访问.
+                    field.setAccessible(true);
+                    Integer column = cellMap.get(attr.name());
+                    if (column != null)
+                    {
+                        fieldsMap.put(column, field);
+                    }
+                }
+            }
+            for (int i = 1; i < rows; i++)
+            {
+                // 从第2行开始取数据,默认第一行是表头.
+                Row row = sheet.getRow(i);
+                if(row == null)
+                {
+                    continue;
+                }
+                T entity = null;
+                for (Map.Entry<Integer, Field> entry : fieldsMap.entrySet())
+                {
+                    Object val = this.getCellValue(row, entry.getKey());
+
+                    // 如果不存在实例则新建.
+                    entity = (entity == null ? clazz.newInstance() : entity);
+                    // 从map中得到对应列的field.
+                    Field field = fieldsMap.get(entry.getKey());
+                    // 取得类型,并根据对象类型设置值.
+                    Class<?> fieldType = field.getType();
+                    if (String.class == fieldType)
+                    {
+                        String s = Convert.toStr(val);
+                        if (StrUtil.endWith(s, ".0"))
+                        {
+                            val = StrUtil.subBefore(s, ".0",false);
+                        }
+                        else
+                        {
+                            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
+                            if (Validator.isNotEmpty(dateFormat))
+                            {
+                                val = DateUtils.parseDateToStr(dateFormat, (Date) val);
+                            }
+                            else
+                            {
+                                val = Convert.toStr(val);
+                            }
+                        }
+                    }
+                    else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && Validator.isNumber(Convert.toStr(val)))
+                    {
+                        val = Convert.toInt(val);
+                    }
+                    else if (Long.TYPE == fieldType || Long.class == fieldType)
+                    {
+                        val = Convert.toLong(val);
+                    }
+                    else if (Double.TYPE == fieldType || Double.class == fieldType)
+                    {
+                        val = Convert.toDouble(val);
+                    }
+                    else if (Float.TYPE == fieldType || Float.class == fieldType)
+                    {
+                        val = Convert.toFloat(val);
+                    }
+                    else if (BigDecimal.class == fieldType)
+                    {
+                        val = Convert.toBigDecimal(val);
+                    }
+                    else if (Date.class == fieldType)
+                    {
+                        if (val instanceof String)
+                        {
+                            val = DateUtils.parseDate(val);
+                        }
+                        else if (val instanceof Double)
+                        {
+                            val = DateUtil.getJavaDate((Double) val);
+                        }
+                    }
+                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
+                    {
+                        val = Convert.toBool(val, false);
+                    }
+                    if (Validator.isNotNull(fieldType))
+                    {
+                        Excel attr = field.getAnnotation(Excel.class);
+                        String propertyName = field.getName();
+                        if (Validator.isNotEmpty(attr.targetAttr()))
+                        {
+                            propertyName = field.getName() + "." + attr.targetAttr();
+                        }
+                        else if (Validator.isNotEmpty(attr.readConverterExp()))
+                        {
+                            val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
+                        }
+                        else if (Validator.isNotEmpty(attr.dictType()))
+                        {
+                            val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator());
+                        }
+                        ReflectUtils.invokeSetter(entity, propertyName, val);
+                    }
+                }
+                list.add(entity);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param list 导出数据集合
+     * @param sheetName 工作表的名称
+     * @return 结果
+     */
+    public AjaxResult exportExcel(List<T> list, String sheetName)
+    {
+        this.init(list, sheetName, Type.EXPORT);
+        return exportExcel();
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @param sheetName 工作表的名称
+     * @return 结果
+     */
+    public AjaxResult importTemplateExcel(String sheetName)
+    {
+        this.init(null, sheetName, Type.IMPORT);
+        return exportExcel();
+    }
+
+    /**
+     * 对list数据源将其里面的数据导入到excel表单
+     *
+     * @return 结果
+     */
+    public AjaxResult exportExcel()
+    {
+        OutputStream out = null;
+        try
+        {
+            // 取出一共有多少个sheet.
+            double sheetNo = Math.ceil(list.size() / sheetSize);
+            for (int index = 0; index <= sheetNo; index++)
+            {
+                createSheet(sheetNo, index);
+
+                // 产生一行
+                Row row = sheet.createRow(0);
+                int column = 0;
+                // 写入各个字段的列头名称
+                for (Object[] os : fields)
+                {
+                    Excel excel = (Excel) os[1];
+                    this.createCell(excel, row, column++);
+                }
+                if (Type.EXPORT.equals(type))
+                {
+                    fillExcelData(index, row);
+                    addStatisticsRow();
+                }
+            }
+            String filename = encodingFilename(sheetName);
+            out = new FileOutputStream(getAbsoluteFile(filename));
+            wb.write(out);
+            return AjaxResult.success(filename);
+        }
+        catch (Exception e)
+        {
+            log.error("导出Excel异常{}", e.getMessage());
+            throw new CustomException("导出Excel失败,请联系网站管理员!");
+        }
+        finally
+        {
+            if (wb != null)
+            {
+                try
+                {
+                    wb.close();
+                }
+                catch (IOException e1)
+                {
+                    e1.printStackTrace();
+                }
+            }
+            if (out != null)
+            {
+                try
+                {
+                    out.close();
+                }
+                catch (IOException e1)
+                {
+                    e1.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     * 填充excel数据
+     *
+     * @param index 序号
+     * @param row 单元格行
+     */
+    public void fillExcelData(int index, Row row)
+    {
+        int startNo = index * sheetSize;
+        int endNo = Math.min(startNo + sheetSize, list.size());
+        for (int i = startNo; i < endNo; i++)
+        {
+            row = sheet.createRow(i + 1 - startNo);
+            // 得到导出对象.
+            T vo = (T) list.get(i);
+            int column = 0;
+            for (Object[] os : fields)
+            {
+                Field field = (Field) os[0];
+                Excel excel = (Excel) os[1];
+                // 设置实体类私有属性可访问
+                field.setAccessible(true);
+                this.addCell(excel, row, vo, field, column++);
+            }
+        }
+    }
+
+    /**
+     * 创建表格样式
+     *
+     * @param wb 工作薄对象
+     * @return 样式列表
+     */
+    private Map<String, CellStyle> createStyles(Workbook wb)
+    {
+        // 写入各条记录,每条记录对应excel表中的一行
+        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
+        CellStyle style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderRight(BorderStyle.THIN);
+        style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderLeft(BorderStyle.THIN);
+        style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderTop(BorderStyle.THIN);
+        style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        Font dataFont = wb.createFont();
+        dataFont.setFontName("Arial");
+        dataFont.setFontHeightInPoints((short) 10);
+        style.setFont(dataFont);
+        styles.put("data", style);
+
+        style = wb.createCellStyle();
+        style.cloneStyleFrom(styles.get("data"));
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+        Font headerFont = wb.createFont();
+        headerFont.setFontName("Arial");
+        headerFont.setFontHeightInPoints((short) 10);
+        headerFont.setBold(true);
+        headerFont.setColor(IndexedColors.WHITE.getIndex());
+        style.setFont(headerFont);
+        styles.put("header", style);
+
+        style = wb.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        Font totalFont = wb.createFont();
+        totalFont.setFontName("Arial");
+        totalFont.setFontHeightInPoints((short) 10);
+        style.setFont(totalFont);
+        styles.put("total", style);
+
+        style = wb.createCellStyle();
+        style.cloneStyleFrom(styles.get("data"));
+        style.setAlignment(HorizontalAlignment.LEFT);
+        styles.put("data1", style);
+
+        style = wb.createCellStyle();
+        style.cloneStyleFrom(styles.get("data"));
+        style.setAlignment(HorizontalAlignment.CENTER);
+        styles.put("data2", style);
+
+        style = wb.createCellStyle();
+        style.cloneStyleFrom(styles.get("data"));
+        style.setAlignment(HorizontalAlignment.RIGHT);
+        styles.put("data3", style);
+
+        return styles;
+    }
+
+    /**
+     * 创建单元格
+     */
+    public Cell createCell(Excel attr, Row row, int column)
+    {
+        // 创建列
+        Cell cell = row.createCell(column);
+        // 写入列信息
+        cell.setCellValue(attr.name());
+        setDataValidation(attr, row, column);
+        cell.setCellStyle(styles.get("header"));
+        return cell;
+    }
+
+    /**
+     * 设置单元格信息
+     *
+     * @param value 单元格值
+     * @param attr 注解相关
+     * @param cell 单元格信息
+     */
+    public void setCellVo(Object value, Excel attr, Cell cell)
+    {
+        if (ColumnType.STRING == attr.cellType())
+        {
+            cell.setCellValue(Validator.isNull(value) ? attr.defaultValue() : value + attr.suffix());
+        }
+        else if (ColumnType.NUMERIC == attr.cellType())
+        {
+            if (Validator.isNotNull(value))
+            {
+                cell.setCellValue(StrUtil.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value));
+            }
+        }
+        else if (ColumnType.IMAGE == attr.cellType())
+        {
+            ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1),
+                    cell.getRow().getRowNum() + 1);
+            String imagePath = Convert.toStr(value);
+            if (Validator.isNotEmpty(imagePath))
+            {
+                byte[] data = ImageUtils.getImage(imagePath);
+                getDrawingPatriarch(cell.getSheet()).createPicture(anchor,
+                        cell.getSheet().getWorkbook().addPicture(data, getImageType(data)));
+            }
+        }
+    }
+
+    /**
+     * 获取画布
+     */
+    public static Drawing<?> getDrawingPatriarch(Sheet sheet)
+    {
+        if (sheet.getDrawingPatriarch() == null)
+        {
+            sheet.createDrawingPatriarch();
+        }
+        return sheet.getDrawingPatriarch();
+    }
+
+    /**
+     * 获取图片类型,设置图片插入类型
+     */
+    public int getImageType(byte[] value)
+    {
+        String type = FileTypeUtils.getFileExtendName(value);
+        if ("JPG".equalsIgnoreCase(type))
+        {
+            return Workbook.PICTURE_TYPE_JPEG;
+        }
+        else if ("PNG".equalsIgnoreCase(type))
+        {
+            return Workbook.PICTURE_TYPE_PNG;
+        }
+        return Workbook.PICTURE_TYPE_JPEG;
+    }
+
+    /**
+     * 创建表格样式
+     */
+    public void setDataValidation(Excel attr, Row row, int column)
+    {
+        if (attr.name().indexOf("注:") >= 0)
+        {
+            sheet.setColumnWidth(column, 6000);
+        }
+        else
+        {
+            // 设置列宽
+            sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256));
+        }
+        // 如果设置了提示信息则鼠标放上去提示.
+        if (Validator.isNotEmpty(attr.prompt()))
+        {
+            // 这里默认设了2-101列提示.
+            setXSSFPrompt(sheet, "", attr.prompt(), 1, 100, column, column);
+        }
+        // 如果设置了combo属性则本列只能选择不能输入
+        if (attr.combo().length > 0)
+        {
+            // 这里默认设了2-101列只能选择不能输入.
+            setXSSFValidation(sheet, attr.combo(), 1, 100, column, column);
+        }
+    }
+
+    /**
+     * 添加单元格
+     */
+    public Cell addCell(Excel attr, Row row, T vo, Field field, int column)
+    {
+        Cell cell = null;
+        try
+        {
+            // 设置行高
+            row.setHeight(maxHeight);
+            // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
+            if (attr.isExport())
+            {
+                // 创建cell
+                cell = row.createCell(column);
+                int align = attr.align().value();
+                cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : "")));
+
+                // 用于读取对象中的属性
+                Object value = getTargetValue(vo, field, attr);
+                String dateFormat = attr.dateFormat();
+                String readConverterExp = attr.readConverterExp();
+                String separator = attr.separator();
+                String dictType = attr.dictType();
+                if (Validator.isNotEmpty(dateFormat) && Validator.isNotNull(value))
+                {
+                    cell.setCellValue(DateUtils.parseDateToStr(dateFormat, (Date) value));
+                }
+                else if (Validator.isNotEmpty(readConverterExp) && Validator.isNotNull(value))
+                {
+                    cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator));
+                }
+                else if (Validator.isNotEmpty(dictType) && Validator.isNotNull(value))
+                {
+                    cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator));
+                }
+                else if (value instanceof BigDecimal && -1 != attr.scale())
+                {
+                    cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString());
+                }
+                else
+                {
+                    // 设置列类型
+                    setCellVo(value, attr, cell);
+                }
+                addStatisticsData(column, Convert.toStr(value), attr);
+            }
+        }
+        catch (Exception e)
+        {
+            log.error("导出Excel失败{}", e);
+        }
+        return cell;
+    }
+
+    /**
+     * 设置 POI XSSFSheet 单元格提示
+     *
+     * @param sheet 表单
+     * @param promptTitle 提示标题
+     * @param promptContent 提示内容
+     * @param firstRow 开始行
+     * @param endRow 结束行
+     * @param firstCol 开始列
+     * @param endCol 结束列
+     */
+    public void setXSSFPrompt(Sheet sheet, String promptTitle, String promptContent, int firstRow, int endRow,
+            int firstCol, int endCol)
+    {
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        DataValidationConstraint constraint = helper.createCustomConstraint("DD1");
+        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+        DataValidation dataValidation = helper.createValidation(constraint, regions);
+        dataValidation.createPromptBox(promptTitle, promptContent);
+        dataValidation.setShowPromptBox(true);
+        sheet.addValidationData(dataValidation);
+    }
+
+    /**
+     * 设置某些列的值只能输入预制的数据,显示下拉框.
+     *
+     * @param sheet 要设置的sheet.
+     * @param textlist 下拉框显示的内容
+     * @param firstRow 开始行
+     * @param endRow 结束行
+     * @param firstCol 开始列
+     * @param endCol 结束列
+     * @return 设置好的sheet.
+     */
+    public void setXSSFValidation(Sheet sheet, String[] textlist, int firstRow, int endRow, int firstCol, int endCol)
+    {
+        DataValidationHelper helper = sheet.getDataValidationHelper();
+        // 加载下拉列表内容
+        DataValidationConstraint constraint = helper.createExplicitListConstraint(textlist);
+        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
+        // 数据有效性对象
+        DataValidation dataValidation = helper.createValidation(constraint, regions);
+        // 处理Excel兼容性问题
+        if (dataValidation instanceof XSSFDataValidation)
+        {
+            dataValidation.setSuppressDropDownArrow(true);
+            dataValidation.setShowErrorBox(true);
+        }
+        else
+        {
+            dataValidation.setSuppressDropDownArrow(false);
+        }
+
+        sheet.addValidationData(dataValidation);
+    }
+
+    /**
+     * 解析导出值 0=男,1=女,2=未知
+     *
+     * @param propertyValue 参数值
+     * @param converterExp 翻译注解
+     * @param separator 分隔符
+     * @return 解析后值
+     */
+    public static String convertByExp(String propertyValue, String converterExp, String separator)
+    {
+        StringBuilder propertyString = new StringBuilder();
+        String[] convertSource = converterExp.split(",");
+        for (String item : convertSource)
+        {
+            String[] itemArray = item.split("=");
+            if (StrUtil.containsAny(propertyValue, separator))
+            {
+                for (String value : propertyValue.split(separator))
+                {
+                    if (itemArray[0].equals(value))
+                    {
+                        propertyString.append(itemArray[1] + separator);
+                        break;
+                    }
+                }
+            }
+            else
+            {
+                if (itemArray[0].equals(propertyValue))
+                {
+                    return itemArray[1];
+                }
+            }
+        }
+        return StrUtil.strip(propertyString.toString(), null,separator);
+    }
+
+    /**
+     * 反向解析值 男=0,女=1,未知=2
+     *
+     * @param propertyValue 参数值
+     * @param converterExp 翻译注解
+     * @param separator 分隔符
+     * @return 解析后值
+     */
+    public static String reverseByExp(String propertyValue, String converterExp, String separator)
+    {
+        StringBuilder propertyString = new StringBuilder();
+        String[] convertSource = converterExp.split(",");
+        for (String item : convertSource)
+        {
+            String[] itemArray = item.split("=");
+            if (StrUtil.containsAny(propertyValue, separator))
+            {
+                for (String value : propertyValue.split(separator))
+                {
+                    if (itemArray[1].equals(value))
+                    {
+                        propertyString.append(itemArray[0] + separator);
+                        break;
+                    }
+                }
+            }
+            else
+            {
+                if (itemArray[1].equals(propertyValue))
+                {
+                    return itemArray[0];
+                }
+            }
+        }
+        return StrUtil.strip(propertyString.toString(), null,separator);
+    }
+
+    /**
+     * 解析字典值
+     *
+     * @param dictValue 字典值
+     * @param dictType 字典类型
+     * @param separator 分隔符
+     * @return 字典标签
+     */
+    public static String convertDictByExp(String dictValue, String dictType, String separator)
+    {
+        return DictUtils.getDictLabel(dictType, dictValue, separator);
+    }
+
+    /**
+     * 反向解析值字典值
+     *
+     * @param dictLabel 字典标签
+     * @param dictType 字典类型
+     * @param separator 分隔符
+     * @return 字典值
+     */
+    public static String reverseDictByExp(String dictLabel, String dictType, String separator)
+    {
+        return DictUtils.getDictValue(dictType, dictLabel, separator);
+    }
+
+    /**
+     * 合计统计信息
+     */
+    private void addStatisticsData(Integer index, String text, Excel entity)
+    {
+        if (entity != null && entity.isStatistics())
+        {
+            Double temp = 0D;
+            if (!statistics.containsKey(index))
+            {
+                statistics.put(index, temp);
+            }
+            try
+            {
+                temp = Double.valueOf(text);
+            }
+            catch (NumberFormatException e)
+            {
+            }
+            statistics.put(index, statistics.get(index) + temp);
+        }
+    }
+
+    /**
+     * 创建统计行
+     */
+    public void addStatisticsRow()
+    {
+        if (statistics.size() > 0)
+        {
+            Cell cell = null;
+            Row row = sheet.createRow(sheet.getLastRowNum() + 1);
+            Set<Integer> keys = statistics.keySet();
+            cell = row.createCell(0);
+            cell.setCellStyle(styles.get("total"));
+            cell.setCellValue("合计");
+
+            for (Integer key : keys)
+            {
+                cell = row.createCell(key);
+                cell.setCellStyle(styles.get("total"));
+                cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key)));
+            }
+            statistics.clear();
+        }
+    }
+
+    /**
+     * 编码文件名
+     */
+    public String encodingFilename(String filename)
+    {
+        filename = UUID.randomUUID().toString() + "_" + filename + ".xlsx";
+        return filename;
+    }
+
+    /**
+     * 获取下载路径
+     *
+     * @param filename 文件名称
+     */
+    public String getAbsoluteFile(String filename)
+    {
+        String downloadPath = RuoYiConfig.getDownloadPath() + filename;
+        File desc = new File(downloadPath);
+        if (!desc.getParentFile().exists())
+        {
+            desc.getParentFile().mkdirs();
+        }
+        return downloadPath;
+    }
+
+    /**
+     * 获取bean中的属性值
+     *
+     * @param vo 实体对象
+     * @param field 字段
+     * @param excel 注解
+     * @return 最终的属性值
+     * @throws Exception
+     */
+    private Object getTargetValue(T vo, Field field, Excel excel) throws Exception
+    {
+        Object o = field.get(vo);
+        if (Validator.isNotEmpty(excel.targetAttr()))
+        {
+            String target = excel.targetAttr();
+            if (target.contains("."))
+            {
+                String[] targets = target.split("[.]");
+                for (String name : targets)
+                {
+                    o = getValue(o, name);
+                }
+            }
+            else
+            {
+                o = getValue(o, target);
+            }
+        }
+        return o;
+    }
+
+    /**
+     * 以类的属性的get方法方法形式获取值
+     *
+     * @param o
+     * @param name
+     * @return value
+     * @throws Exception
+     */
+    private Object getValue(Object o, String name) throws Exception
+    {
+        if (Validator.isNotNull(o) && Validator.isNotEmpty(name))
+        {
+            Class<?> clazz = o.getClass();
+            Field field = clazz.getDeclaredField(name);
+            field.setAccessible(true);
+            o = field.get(o);
+        }
+        return o;
+    }
+
+    /**
+     * 得到所有定义字段
+     */
+    private void createExcelField()
+    {
+        this.fields = new ArrayList<Object[]>();
+        List<Field> tempFields = new ArrayList<>();
+        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
+        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+        for (Field field : tempFields)
+        {
+            // 单注解
+            if (field.isAnnotationPresent(Excel.class))
+            {
+                putToField(field, field.getAnnotation(Excel.class));
+            }
+
+            // 多注解
+            if (field.isAnnotationPresent(Excels.class))
+            {
+                Excels attrs = field.getAnnotation(Excels.class);
+                Excel[] excels = attrs.value();
+                for (Excel excel : excels)
+                {
+                    putToField(field, excel);
+                }
+            }
+        }
+        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
+        this.maxHeight = getRowHeight();
+    }
+
+    /**
+     * 根据注解获取最大行高
+     */
+    public short getRowHeight()
+    {
+        double maxHeight = 0;
+        for (Object[] os : this.fields)
+        {
+            Excel excel = (Excel) os[1];
+            maxHeight = maxHeight > excel.height() ? maxHeight : excel.height();
+        }
+        return (short) (maxHeight * 20);
+    }
+
+    /**
+     * 放到字段集合中
+     */
+    private void putToField(Field field, Excel attr)
+    {
+        if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
+        {
+            this.fields.add(new Object[] { field, attr });
+        }
+    }
+
+    /**
+     * 创建一个工作簿
+     */
+    public void createWorkbook()
+    {
+        this.wb = new SXSSFWorkbook(500);
+    }
+
+    /**
+     * 创建工作表
+     *
+     * @param sheetNo sheet数量
+     * @param index 序号
+     */
+    public void createSheet(double sheetNo, int index)
+    {
+        this.sheet = wb.createSheet();
+        this.styles = createStyles(wb);
+        // 设置工作表的名称.
+        if (sheetNo == 0)
+        {
+            wb.setSheetName(index, sheetName);
+        }
+        else
+        {
+            wb.setSheetName(index, sheetName + index);
+        }
+    }
+
+    /**
+     * 获取单元格值
+     *
+     * @param row 获取的行
+     * @param column 获取单元格列号
+     * @return 单元格值
+     */
+    public Object getCellValue(Row row, int column)
+    {
+        if (row == null)
+        {
+            return row;
+        }
+        Object val = "";
+        try
+        {
+            Cell cell = row.getCell(column);
+            if (Validator.isNotNull(cell))
+            {
+                if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA)
+                {
+                    val = cell.getNumericCellValue();
+                    if (DateUtil.isCellDateFormatted(cell))
+                    {
+                        val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
+                    }
+                    else
+                    {
+                        if ((Double) val % 1 != 0)
+                        {
+                            val = new BigDecimal(val.toString());
+                        }
+                        else
+                        {
+                            val = new DecimalFormat("0").format(val);
+                        }
+                    }
+                }
+                else if (cell.getCellType() == CellType.STRING)
+                {
+                    val = cell.getStringCellValue();
+                }
+                else if (cell.getCellType() == CellType.BOOLEAN)
+                {
+                    val = cell.getBooleanCellValue();
+                }
+                else if (cell.getCellType() == CellType.ERROR)
+                {
+                    val = cell.getErrorCellValue();
+                }
+
+            }
+        }
+        catch (Exception e)
+        {
+            return val;
+        }
+        return val;
+    }
+}

+ 2 - 2
ruoyi-demo/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>${ruoyi-vue-plus.version}</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
@@ -25,4 +25,4 @@
 
     </dependencies>
 
-</project>
+</project>

+ 70 - 0
ruoyi-demo/src/main/java/com/ruoyi/demo/controller/RedisCacheController.java

@@ -0,0 +1,70 @@
+package com.ruoyi.demo.controller;
+
+import com.ruoyi.common.core.domain.AjaxResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * spring-cache 演示案例
+ *
+ * @author Lion Li
+ */
+// 类级别 缓存统一配置
+//@CacheConfig(cacheNames = "redissonCacheMap")
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+@RestController
+@RequestMapping("/demo/cache")
+public class RedisCacheController {
+
+	/**
+	 * 测试 @Cacheable
+	 *
+	 * 表示这个方法有了缓存的功能,方法的返回值会被缓存下来
+	 * 下一次调用该方法前,会去检查是否缓存中已经有值
+	 * 如果有就直接返回,不调用方法
+	 * 如果没有,就调用方法,然后把结果缓存起来
+	 * 这个注解「一般用在查询方法上」
+	 *
+	 * cacheNames 为配置文件内 groupId
+	 */
+	@Cacheable(cacheNames = "redissonCacheMap", key = "#key", condition = "#key != null")
+	@GetMapping("/test1")
+	public AjaxResult<String> test1(String key, String value){
+		return AjaxResult.success("操作成功", value);
+	}
+
+	/**
+	 * 测试 @CachePut
+	 *
+	 * 加了@CachePut注解的方法,会把方法的返回值put到缓存里面缓存起来,供其它地方使用
+	 * 它「通常用在新增方法上」
+	 *
+	 * cacheNames 为 配置文件内 groupId
+	 */
+	@CachePut(cacheNames = "redissonCacheMap", key = "#key", condition = "#key != null")
+	@GetMapping("/test2")
+	public AjaxResult<String> test2(String key, String value){
+		return AjaxResult.success("操作成功", value);
+	}
+
+	/**
+	 * 测试 @CacheEvict
+	 *
+	 * 使用了CacheEvict注解的方法,会清空指定缓存
+	 * 「一般用在更新或者删除的方法上」
+	 *
+	 * cacheNames 为 配置文件内 groupId
+	 */
+	@CacheEvict(cacheNames = "redissonCacheMap", key = "#key", condition = "#key != null")
+	@GetMapping("/test3")
+	public AjaxResult<String> test3(String key, String value){
+		return AjaxResult.success("操作成功", value);
+	}
+
+}

+ 3 - 0
ruoyi-demo/src/main/java/com/ruoyi/demo/feign/fallback/FeignTestFallback.java

@@ -7,7 +7,10 @@ import org.springframework.stereotype.Component;
 
 /**
  * feign测试fallback
+ * 自定义封装结构体熔断
+ * 需重写解码器 根据自定义实体 自行解析熔断
  *
+ * @see {com.ruoyi.framework.config.FeignConfig#errorDecoder()}
  * @author Lion Li
  */
 @Slf4j

+ 18 - 0
ruoyi-extend/pom.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ruoyi-vue-plus</artifactId>
+        <groupId>com.ruoyi</groupId>
+        <version>2.5.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>ruoyi-extend</artifactId>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>ruoyi-monitor-admin</module>
+    </modules>
+
+</project>

+ 13 - 0
ruoyi-extend/ruoyi-monitor-admin/Dockerfile

@@ -0,0 +1,13 @@
+FROM anapsix/alpine-java:8_server-jre_unlimited
+
+MAINTAINER Lion Li
+
+RUN mkdir -p /ruoyi/monitor
+
+WORKDIR /ruoyi/monitor
+
+EXPOSE 9090
+
+ADD ./target/ruoyi-monitor-admin.jar ./app.jar
+
+ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]

+ 73 - 0
ruoyi-extend/ruoyi-monitor-admin/pom.xml

@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ruoyi-extend</artifactId>
+        <groupId>com.ruoyi</groupId>
+        <version>2.5.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+    <artifactId>ruoyi-monitor-admin</artifactId>
+
+    <dependencies>
+        <!-- SpringWeb模块 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- spring security 安全认证 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>de.codecentric</groupId>
+            <artifactId>spring-boot-admin-starter-server</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.spotify</groupId>
+                <artifactId>docker-maven-plugin</artifactId>
+                <version>${docker.plugin.version}</version>
+                <configuration>
+                    <imageName>${docker.namespace}/${project.artifactId}:${project.version}</imageName>
+                    <dockerDirectory>${project.basedir}</dockerDirectory>
+                    <dockerHost>${docker.registry.host}</dockerHost>
+                    <registryUrl>${docker.registry.url}</registryUrl>
+                    <serverId>${docker.registry.url}</serverId>
+                    <resources>
+                        <resource>
+                            <targetPath>/</targetPath>
+                            <directory>${project.build.directory}</directory>
+                            <include>${project.build.finalName}.jar</include>
+                        </resource>
+                    </resources>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 19 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/java/com/ruoyi/monitor/admin/MonitorAdminApplication.java

@@ -0,0 +1,19 @@
+package com.ruoyi.monitor.admin;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Admin 监控启动程序
+ *
+ * @author Lion Li
+ */
+@SpringBootApplication
+public class MonitorAdminApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(MonitorAdminApplication.class, args);
+		System.out.println("Admin 监控启动成功" );
+	}
+
+}

+ 31 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/java/com/ruoyi/monitor/admin/config/AdminServerConfig.java

@@ -0,0 +1,31 @@
+package com.ruoyi.monitor.admin.config;
+
+import de.codecentric.boot.admin.server.config.EnableAdminServer;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
+import org.springframework.boot.task.TaskExecutorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+
+/**
+ * springboot-admin server配置类
+ *
+ * @author Lion Li
+ */
+@Configuration
+@EnableAdminServer
+public class AdminServerConfig {
+
+    @Lazy
+    @Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
+    @ConditionalOnMissingBean(Executor.class)
+    public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
+        return builder.build();
+    }
+
+
+}

+ 48 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/java/com/ruoyi/monitor/admin/config/SecurityConfig.java

@@ -0,0 +1,48 @@
+package com.ruoyi.monitor.admin.config;
+
+import de.codecentric.boot.admin.server.config.AdminServerProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
+
+/**
+ * spring security配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+	private final String adminContextPath;
+
+	public SecurityConfig(AdminServerProperties adminServerProperties) {
+		this.adminContextPath = adminServerProperties.getContextPath();
+	}
+
+	@Override
+	protected void configure(HttpSecurity httpSecurity) throws Exception {
+		SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
+		successHandler.setTargetUrlParameter("redirectTo");
+		successHandler.setDefaultTargetUrl(adminContextPath + "/");
+
+		httpSecurity.authorizeRequests()
+			//授予对所有静态资产和登录页面的公共访问权限。
+			.antMatchers(adminContextPath + "/assets/**").permitAll()
+			.antMatchers(adminContextPath + "/login").permitAll()
+			//必须对每个其他请求进行身份验证
+			.anyRequest().authenticated().and()
+			//配置登录和注销
+			.formLogin().loginPage(adminContextPath + "/login")
+			.successHandler(successHandler).and()
+			.logout().logoutUrl(adminContextPath + "/logout").and()
+			//启用HTTP-Basic支持。这是Spring Boot Admin Client注册所必需的
+			.httpBasic().and().csrf().disable()
+			.headers().frameOptions().disable();
+	}
+
+}

+ 11 - 0
ruoyi-extend/ruoyi-monitor-admin/src/main/resources/application.yml

@@ -0,0 +1,11 @@
+server:
+  port: 9090
+
+spring:
+  security:
+    user:
+      name: ruoyi
+      password: 123456
+  boot:
+    admin:
+      context-path: /admin

+ 1 - 1
ruoyi-framework/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>${ruoyi-vue-plus.version}</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 0 - 63
ruoyi-framework/src/main/java/com/ruoyi/framework/config/AdminServerConfig.java

@@ -1,63 +0,0 @@
-package com.ruoyi.framework.config;
-
-import de.codecentric.boot.admin.server.config.EnableAdminServer;
-import org.springframework.beans.factory.ObjectProvider;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
-import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
-import org.springframework.boot.task.TaskExecutorBuilder;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
-import org.thymeleaf.dialect.IDialect;
-import org.thymeleaf.spring5.ISpringTemplateEngine;
-import org.thymeleaf.spring5.SpringTemplateEngine;
-import org.thymeleaf.templateresolver.ITemplateResolver;
-
-import java.util.Comparator;
-import java.util.LinkedHashSet;
-import java.util.Set;
-import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
-
-/**
- * springboot-admin server配置类
- *
- * @author Lion Li
- */
-@Configuration
-@EnableAdminServer
-public class AdminServerConfig {
-
-    @Lazy
-    @Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
-    @ConditionalOnMissingBean(Executor.class)
-    public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
-        return builder.build();
-    }
-
-    /**
-     * 解决 admin 与 项目 页面的交叉引用 将 admin 的路由放到最后
-     * @param properties
-     * @param templateResolvers
-     * @param dialects
-     * @return
-     */
-    @Bean
-    @ConditionalOnMissingBean(ISpringTemplateEngine.class)
-    SpringTemplateEngine templateEngine(ThymeleafProperties properties,
-                                        ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
-        SpringTemplateEngine engine = new SpringTemplateEngine();
-        engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
-        engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
-        templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
-        dialects.orderedStream().forEach(engine::addDialect);
-        Set<ITemplateResolver> templateResolvers1 = engine.getTemplateResolvers();
-        templateResolvers1 = templateResolvers1.stream()
-                .sorted(Comparator.comparing(ITemplateResolver::getOrder))
-                .collect(Collectors.toCollection(LinkedHashSet::new));
-        engine.setTemplateResolvers(templateResolvers1);
-        return engine;
-    }
-}

+ 36 - 0
ruoyi-framework/src/main/java/com/ruoyi/framework/config/FeignConfig.java

@@ -54,4 +54,40 @@ public class FeignConfig {
         return new Retryer.Default();
     }
 
+//	/**
+//	 * 自定义异常解码器
+//	 * 用于自定义返回体异常熔断
+//	 */
+//	@Bean
+//	public ErrorDecoder errorDecoder() {
+//		return new CustomErrorDecoder();
+//	}
+//
+//
+//	/**
+//	 * 自定义返回体解码器
+//	 */
+//	@Slf4j
+//	public static class CustomErrorDecoder implements ErrorDecoder {
+//
+//		@Override
+//		public Exception decode(String methodKey, Response response) {
+//			Exception exception = null;
+//			try {
+//				// 获取原始的返回内容
+//				String json = JsonUtils.toJsonString(response.body().asReader(StandardCharsets.UTF_8));
+//				exception = new RuntimeException(json);
+//				// 将返回内容反序列化为Result,这里应根据自身项目作修改
+//				AjaxResult result = JsonUtils.parseObject(json, AjaxResult.class);
+//				// 业务异常抛出简单的 RuntimeException,保留原来错误信息
+//				if (result.getCode() != 200) {
+//					exception = new RuntimeException(result.getMsg());
+//				}
+//			} catch (IOException e) {
+//				log.error(e.getMessage(), e);
+//			}
+//			return exception;
+//		}
+//	}
+
 }

+ 7 - 1
ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java

@@ -19,6 +19,7 @@ import org.springframework.context.annotation.Configuration;
 
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -78,8 +79,13 @@ public class RedisConfig extends CachingConfigurerSupport {
 	 */
 	@Bean
 	public CacheManager cacheManager(RedissonClient redissonClient) {
+		List<RedissonProperties.CacheGroup> cacheGroup = redissonProperties.getCacheGroup();
 		Map<String, CacheConfig> config = new HashMap<>();
-		config.put("redissonCacheMap", new CacheConfig(30*60*1000, 10*60*1000));
+		for (RedissonProperties.CacheGroup group : cacheGroup) {
+			CacheConfig cacheConfig = new CacheConfig(group.getTtl(), group.getMaxIdleTime());
+			cacheConfig.setMaxSize(group.getMaxSize());
+			config.put(group.getGroupId(), cacheConfig);
+		}
 		return new RedissonSpringCacheManager(redissonClient, config, JsonJacksonCodec.INSTANCE);
 	}
 

+ 3 - 9
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -3,7 +3,6 @@ package com.ruoyi.framework.config;
 import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
 import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
 import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
-import de.codecentric.boot.admin.server.config.AdminServerProperties;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.http.HttpMethod;
@@ -57,9 +56,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
     @Autowired
     private CorsFilter corsFilter;
 
-    @Autowired
-    private AdminServerProperties adminServerProperties;
-
     /**
      * 解决 无法直接注入 AuthenticationManager
      *
@@ -104,12 +100,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/login", "/captchaImage").anonymous()
                 .antMatchers(
                         HttpMethod.GET,
+                        "/",
                         "/*.html",
                         "/**/*.html",
                         "/**/*.css",
-                        "/**/*.js"
+                        "/**/*.js",
+                        "/profile/**"
                 ).permitAll()
-                .antMatchers("/profile/**").anonymous()
                 .antMatchers("/common/download**").anonymous()
                 .antMatchers("/common/download/resource**").anonymous()
                 .antMatchers("/doc.html").anonymous()
@@ -117,9 +114,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/webjars/**").anonymous()
                 .antMatchers("/*/api-docs").anonymous()
                 .antMatchers("/druid/**").anonymous()
-                // Spring Boot Admin Server 的安全配置
-                .antMatchers(adminServerProperties.getContextPath()).anonymous()
-                .antMatchers(adminServerProperties.getContextPath() + "/**").anonymous()
                 // Spring Boot Actuator 的安全配置
                 .antMatchers("/actuator").anonymous()
                 .antMatchers("/actuator/**").anonymous()

+ 33 - 1
ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/RedissonProperties.java

@@ -2,11 +2,12 @@ package com.ruoyi.framework.config.properties;
 
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.redisson.client.codec.Codec;
 import org.redisson.config.TransportMode;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
+import java.util.List;
+
 /**
  * Redisson 配置属性
  *
@@ -37,6 +38,11 @@ public class RedissonProperties {
 	 */
 	private SingleServerConfig singleServerConfig;
 
+	/**
+	 * 缓存组
+	 */
+	private List<CacheGroup> cacheGroup;
+
 	@Data
 	@NoArgsConstructor
 	public static class SingleServerConfig {
@@ -98,4 +104,30 @@ public class RedissonProperties {
 
 	}
 
+	@Data
+	@NoArgsConstructor
+	public static class CacheGroup {
+
+		/**
+		 * 组id
+		 */
+		private String groupId;
+
+		/**
+		 * 组过期时间
+		 */
+		private long ttl;
+
+		/**
+		 * 组最大空闲时间
+		 */
+		private long maxIdleTime;
+
+		/**
+		 * 组最大长度
+		 */
+		private int maxSize;
+
+	}
+
 }

+ 26 - 15
ruoyi-framework/src/main/java/com/ruoyi/framework/mybatisplus/CreateAndUpdateMetaObjectHandler.java

@@ -1,6 +1,8 @@
 package com.ruoyi.framework.mybatisplus;
 
+import cn.hutool.http.HttpStatus;
 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.ruoyi.common.exception.CustomException;
 import com.ruoyi.common.utils.SecurityUtils;
 import org.apache.ibatis.reflection.MetaObject;
 
@@ -8,6 +10,7 @@ import java.util.Date;
 
 /**
  * MP注入处理器
+ *
  * @author Lion Li
  * @date 2021/4/25
  */
@@ -15,30 +18,38 @@ public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
 
 	@Override
 	public void insertFill(MetaObject metaObject) {
-		//根据属性名字设置要填充的值
-		if (metaObject.hasGetter("createTime")) {
-			if (metaObject.getValue("createTime") == null) {
-				this.setFieldValByName("createTime", new Date(), metaObject);
+		try {
+			//根据属性名字设置要填充的值
+			if (metaObject.hasGetter("createTime")) {
+				if (metaObject.getValue("createTime") == null) {
+					this.setFieldValByName("createTime", new Date(), metaObject);
+				}
 			}
-		}
-		if (metaObject.hasGetter("createBy")) {
-			if (metaObject.getValue("createBy") == null) {
-				this.setFieldValByName("createBy", SecurityUtils.getUsername(), metaObject);
+			if (metaObject.hasGetter("createBy")) {
+				if (metaObject.getValue("createBy") == null) {
+					this.setFieldValByName("createBy", SecurityUtils.getUsername(), metaObject);
+				}
 			}
+		} catch (Exception e) {
+			throw new CustomException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
 		}
 	}
 
 	@Override
 	public void updateFill(MetaObject metaObject) {
-		if (metaObject.hasGetter("updateBy")) {
-			if (metaObject.getValue("updateBy") == null) {
-				this.setFieldValByName("updateBy", SecurityUtils.getUsername(), metaObject);
+		try {
+			if (metaObject.hasGetter("updateBy")) {
+				if (metaObject.getValue("updateBy") == null) {
+					this.setFieldValByName("updateBy", SecurityUtils.getUsername(), metaObject);
+				}
 			}
-		}
-		if (metaObject.hasGetter("updateTime")) {
-			if (metaObject.getValue("updateTime") == null) {
-				this.setFieldValByName("updateTime", new Date(), metaObject);
+			if (metaObject.hasGetter("updateTime")) {
+				if (metaObject.getValue("updateTime") == null) {
+					this.setFieldValByName("updateTime", new Date(), metaObject);
+				}
 			}
+		} catch (Exception e) {
+			throw new CustomException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
 		}
 	}
 

+ 1 - 1
ruoyi-generator/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>${ruoyi-vue-plus.version}</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 6 - 6
ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java

@@ -182,9 +182,9 @@ public class GenTableServiceImpl extends ServicePlusImpl<GenTableMapper, GenTabl
                     List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);
                     for (GenTableColumn column : genTableColumns) {
                         GenUtils.initColumnField(column, table);
-                        genTableColumnMapper.insert(column);
                     }
-                }
+					genTableColumnMapper.insertAll(genTableColumns);
+				}
             }
         } catch (Exception e) {
             throw new CustomException("导入失败:" + e.getMessage());
@@ -258,7 +258,7 @@ public class GenTableServiceImpl extends ServicePlusImpl<GenTableMapper, GenTabl
         // 获取模板列表
         List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
         for (String template : templates) {
-            if (!StrUtil.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) {
+            if (!StrUtil.containsAny("sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm", template)) {
                 // 渲染模板
                 StringWriter sw = new StringWriter();
                 Template tpl = Velocity.getTemplate(template, Constants.UTF8);
@@ -290,9 +290,9 @@ public class GenTableServiceImpl extends ServicePlusImpl<GenTableMapper, GenTabl
         dbTableColumns.forEach(column -> {
             if (!tableColumnNames.contains(column.getColumnName())) {
                 GenUtils.initColumnField(column, table);
-                genTableColumnMapper.insert(column);
-            }
-        });
+			}
+		});
+		genTableColumnMapper.insertAll(tableColumns);
 
         List<GenTableColumn> delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList());
         if (CollUtil.isNotEmpty(delColumns)) {

+ 0 - 2
ruoyi-generator/src/main/resources/vm/java/mapper.java.vm

@@ -11,8 +11,6 @@ import org.apache.ibatis.annotations.CacheNamespace;
  * @author ${author}
  * @date ${datetime}
  */
-// 如使需切换数据源 请勿使用缓存 会造成数据不一致现象
-@CacheNamespace(implementation = MybatisPlusRedisCache.class, eviction = MybatisPlusRedisCache.class)
 public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}> {
 
 }

+ 6 - 40
ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm

@@ -256,52 +256,16 @@
 import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName}, export${BusinessName} } from "@/api/${moduleName}/${businessName}";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "imageUpload")
-import ImageUpload from '@/components/ImageUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "fileUpload")
-import FileUpload from '@/components/FileUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "editor")
-import Editor from '@/components/Editor';
-#break
-#end
-#end
 
 export default {
   name: "${BusinessName}",
   components: {
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "imageUpload")
-    ImageUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "fileUpload")
-    FileUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "editor")
-    Editor,
-#break
-#end
-#end
     Treeselect
   },
   data() {
     return {
-	  //按钮loading
-	  buttonLoading: false,
+      // 按钮loading
+      buttonLoading: false,
       // 遮罩层
       loading: true,
       // 显示搜索条件
@@ -510,17 +474,19 @@ export default {
 #end
           if (this.form.${pkColumn.javaField} != null) {
             update${BusinessName}(this.form).then(response => {
-			  this.buttonLoading = false;
               this.msgSuccess("修改成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           } else {
             add${BusinessName}(this.form).then(response => {
-			  this.buttonLoading = false;
               this.msgSuccess("新增成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           }
         }

+ 6 - 42
ruoyi-generator/src/main/resources/vm/vue/index.vue.vm

@@ -308,51 +308,13 @@
 
 <script>
 import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName}, export${BusinessName} } from "@/api/${moduleName}/${businessName}";
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "imageUpload")
-import ImageUpload from '@/components/ImageUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "fileUpload")
-import FileUpload from '@/components/FileUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "editor")
-import Editor from '@/components/Editor';
-#break
-#end
-#end
 
 export default {
   name: "${BusinessName}",
-  components: {
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "imageUpload")
-    ImageUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "fileUpload")
-    FileUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.pk && $column.htmlType == "editor")
-    Editor,
-#break
-#end
-#end
-  },
   data() {
     return {
-	  //按钮loading
-	  buttonLoading: false,
+      // 按钮loading
+      buttonLoading: false,
       // 遮罩层
       loading: true,
       // 导出遮罩层
@@ -567,17 +529,19 @@ export default {
 #end
           if (this.form.${pkColumn.javaField} != null) {
             update${BusinessName}(this.form).then(response => {
-              this.buttonLoading = false;
               this.msgSuccess("修改成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           } else {
             add${BusinessName}(this.form).then(response => {
-              this.buttonLoading = false;
               this.msgSuccess("新增成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           }
         }

+ 1 - 1
ruoyi-quartz/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>${ruoyi-vue-plus.version}</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
ruoyi-system/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi-vue-plus</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>${ruoyi-vue-plus.version}</version>
+        <version>2.5.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 23 - 1
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java

@@ -1,6 +1,8 @@
 package com.ruoyi.system.domain.vo;
 
-import lombok.*;
+import com.ruoyi.common.utils.StringUtils;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 import lombok.experimental.Accessors;
 
 /**
@@ -28,6 +30,11 @@ public class MetaVo {
      */
     private boolean noCache;
 
+	/**
+	 * 内链地址(http(s)://开头)
+	 */
+	private String link;
+
     public MetaVo(String title, String icon) {
         this.title = title;
         this.icon = icon;
@@ -39,4 +46,19 @@ public class MetaVo {
         this.noCache = noCache;
     }
 
+	public MetaVo(String title, String icon, String link) {
+		this.title = title;
+		this.icon = icon;
+		this.link = link;
+	}
+
+	public MetaVo(String title, String icon, boolean noCache, String link) {
+		this.title = title;
+		this.icon = icon;
+		this.noCache = noCache;
+		if (StringUtils.ishttp(link)) {
+			this.link = link;
+		}
+	}
+
 }

+ 16 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java

@@ -24,6 +24,22 @@ public interface SysUserMapper extends BaseMapperPlus<SysUser> {
      */
     public List<SysUser> selectUserList(SysUser sysUser);
 
+    /**
+     * 根据条件分页查询未已配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    public Page<SysUser> selectAllocatedList(@Param("page") Page<SysUser> page, @Param("user") SysUser user);
+
+    /**
+     * 根据条件分页查询未分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    public Page<SysUser> selectUnallocatedList(@Param("page") Page<SysUser> page, @Param("user") SysUser user);
+
     /**
      * 通过用户名查询用户
      *

+ 37 - 1
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java

@@ -3,6 +3,8 @@ package com.ruoyi.system.service;
 import com.ruoyi.common.core.domain.entity.SysRole;
 import com.ruoyi.common.core.mybatisplus.core.IServicePlus;
 import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.system.domain.SysUserRole;
 
 import java.util.List;
 import java.util.Set;
@@ -26,7 +28,15 @@ public interface ISysRoleService extends IServicePlus<SysRole> {
     public List<SysRole> selectRoleList(SysRole role);
 
     /**
-     * 根据用户ID查询角色
+     * 根据用户ID查询角色列表
+     *
+     * @param userId 用户ID
+     * @return 角色列表
+     */
+    public List<SysRole> selectRolesByUserId(Long userId);
+
+    /**
+     * 根据用户ID查询角色权限
      *
      * @param userId 用户ID
      * @return 权限列表
@@ -134,4 +144,30 @@ public interface ISysRoleService extends IServicePlus<SysRole> {
      * @return 结果
      */
     public int deleteRoleByIds(Long[] roleIds);
+
+    /**
+     * 取消授权用户角色
+     *
+     * @param userRole 用户和角色关联信息
+     * @return 结果
+     */
+    public int deleteAuthUser(SysUserRole userRole);
+
+    /**
+     * 批量取消授权用户角色
+     *
+     * @param roleId 角色ID
+     * @param userIds 需要取消授权的用户数据ID
+     * @return 结果
+     */
+    public int deleteAuthUsers(Long roleId, Long[] userIds);
+
+    /**
+     * 批量选择授权用户角色
+     *
+     * @param roleId 角色ID
+     * @param userIds 需要删除的用户数据ID
+     * @return 结果
+     */
+    public int insertAuthUsers(Long roleId, Long[] userIds);
 }

+ 27 - 3
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java

@@ -24,6 +24,22 @@ public interface ISysUserService extends IServicePlus<SysUser> {
      */
     public List<SysUser> selectUserList(SysUser user);
 
+    /**
+     * 根据条件分页查询已分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    public TableDataInfo<SysUser> selectAllocatedList(SysUser user);
+
+    /**
+     * 根据条件分页查询未分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    public TableDataInfo<SysUser> selectUnallocatedList(SysUser user);
+
     /**
      * 通过用户名查询用户
      *
@@ -103,6 +119,14 @@ public interface ISysUserService extends IServicePlus<SysUser> {
      */
     public int updateUser(SysUser user);
 
+    /**
+     * 用户授权角色
+     *
+     * @param userId 用户ID
+     * @param roleIds 角色组
+     */
+    public void insertUserAuth(Long userId, Long[] roleIds);
+
     /**
      * 修改用户状态
      *
@@ -123,7 +147,7 @@ public interface ISysUserService extends IServicePlus<SysUser> {
      * 修改用户头像
      *
      * @param userName 用户名
-     * @param avatar   头像地址
+     * @param avatar 头像地址
      * @return 结果
      */
     public boolean updateUserAvatar(String userName, String avatar);
@@ -164,9 +188,9 @@ public interface ISysUserService extends IServicePlus<SysUser> {
     /**
      * 导入用户数据
      *
-     * @param userList        用户数据列表
+     * @param userList 用户数据列表
      * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
-     * @param operName        操作用户
+     * @param operName 操作用户
      * @return 结果
      */
     public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName);

+ 34 - 4
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java

@@ -3,6 +3,7 @@ package com.ruoyi.system.service.impl;
 import cn.hutool.core.lang.Validator;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.domain.TreeSelect;
 import com.ruoyi.common.core.domain.entity.SysMenu;
@@ -10,6 +11,7 @@ import com.ruoyi.common.core.domain.entity.SysRole;
 import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.core.mybatisplus.core.ServicePlusImpl;
 import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.system.domain.SysRoleMenu;
 import com.ruoyi.system.domain.vo.MetaVo;
 import com.ruoyi.system.domain.vo.RouterVo;
@@ -135,7 +137,7 @@ public class SysMenuServiceImpl extends ServicePlusImpl<SysMenuMapper, SysMenu>
             router.setName(getRouteName(menu));
             router.setPath(getRouterPath(menu));
             router.setComponent(getComponent(menu));
-            router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StrUtil.equals("1", menu.getIsCache())));
+            router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StrUtil.equals("1", menu.getIsCache()), menu.getPath()));
             List<SysMenu> cMenus = menu.getChildren();
             if (!cMenus.isEmpty() && UserConstants.TYPE_DIR.equals(menu.getMenuType())) {
                 router.setAlwaysShow(true);
@@ -148,7 +150,19 @@ public class SysMenuServiceImpl extends ServicePlusImpl<SysMenuMapper, SysMenu>
                 children.setPath(menu.getPath());
                 children.setComponent(menu.getComponent());
                 children.setName(StrUtil.upperFirst(menu.getPath()));
-                children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StrUtil.equals("1", menu.getIsCache())));
+                children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StrUtil.equals("1", menu.getIsCache()), menu.getPath()));
+				childrenList.add(children);
+				router.setChildren(childrenList);
+			} else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
+				router.setMeta(null);
+				router.setPath("/inner");
+				List<RouterVo> childrenList = new ArrayList<RouterVo>();
+				RouterVo children = new RouterVo();
+				String routerPath = StringUtils.replaceEach(menu.getPath(), new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" });
+				children.setPath(routerPath);
+				children.setComponent(UserConstants.INNER_LINK);
+				children.setName(StringUtils.capitalize(routerPath));
+				children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
                 childrenList.add(children);
                 router.setChildren(childrenList);
             }
@@ -305,6 +319,10 @@ public class SysMenuServiceImpl extends ServicePlusImpl<SysMenuMapper, SysMenu>
      */
     public String getRouterPath(SysMenu menu) {
         String routerPath = menu.getPath();
+        // 内链打开外网方式
+        if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
+            routerPath = StringUtils.replaceEach(routerPath, new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" });
+        }
         // 非外链并且是一级目录(类型为目录)
         if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
                 && UserConstants.NO_FRAME.equals(menu.getIsFrame())) {
@@ -327,7 +345,9 @@ public class SysMenuServiceImpl extends ServicePlusImpl<SysMenuMapper, SysMenu>
         String component = UserConstants.LAYOUT;
         if (StrUtil.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) {
             component = menu.getComponent();
-        } else if (StrUtil.isEmpty(menu.getComponent()) && isParentView(menu)) {
+		} else if (StrUtil.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
+			component = UserConstants.INNER_LINK;
+		} else if (StrUtil.isEmpty(menu.getComponent()) && isParentView(menu)) {
             component = UserConstants.PARENT_VIEW;
         }
         return component;
@@ -344,6 +364,16 @@ public class SysMenuServiceImpl extends ServicePlusImpl<SysMenuMapper, SysMenu>
                 && menu.getIsFrame().equals(UserConstants.NO_FRAME);
     }
 
+    /**
+     * 是否为内链组件
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isInnerLink(SysMenu menu) {
+        return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath());
+    }
+
     /**
      * 是否为parent_view组件
      *
@@ -357,7 +387,7 @@ public class SysMenuServiceImpl extends ServicePlusImpl<SysMenuMapper, SysMenu>
     /**
      * 根据父节点的ID获取所有子节点
      *
-     * @param list     分类表
+     * @param list 分类表
      * @param parentId 传入的父节点ID
      * @return String
      */

+ 68 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java

@@ -59,6 +59,27 @@ public class SysRoleServiceImpl extends ServicePlusImpl<SysRoleMapper, SysRole>
         return baseMapper.selectRoleList(role);
     }
 
+    /**
+     * 根据用户ID查询角色
+     *
+     * @param userId 用户ID
+     * @return 角色列表
+     */
+    @Override
+    public List<SysRole> selectRolesByUserId(Long userId) {
+        List<SysRole> userRoles = baseMapper.selectRolePermissionByUserId(userId);
+        List<SysRole> roles = selectRoleAll();
+        for (SysRole role : roles) {
+            for (SysRole userRole : userRoles) {
+                if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) {
+                    role.setFlag(true);
+                    break;
+                }
+            }
+        }
+        return roles;
+    }
+
     /**
      * 根据用户ID查询权限
      *
@@ -305,4 +326,51 @@ public class SysRoleServiceImpl extends ServicePlusImpl<SysRoleMapper, SysRole>
         roleDeptMapper.delete(new LambdaQueryWrapper<SysRoleDept>().in(SysRoleDept::getRoleId, ids));
         return baseMapper.deleteBatchIds(ids);
     }
+
+    /**
+     * 取消授权用户角色
+     *
+     * @param userRole 用户和角色关联信息
+     * @return 结果
+     */
+    @Override
+    public int deleteAuthUser(SysUserRole userRole) {
+        return userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
+			.eq(SysUserRole::getRoleId, userRole.getRoleId())
+			.eq(SysUserRole::getUserId, userRole.getUserId()));
+    }
+
+    /**
+     * 批量取消授权用户角色
+     *
+     * @param roleId 角色ID
+     * @param userIds 需要取消授权的用户数据ID
+     * @return 结果
+     */
+    @Override
+    public int deleteAuthUsers(Long roleId, Long[] userIds) {
+		return userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
+			.eq(SysUserRole::getRoleId, roleId)
+			.in(SysUserRole::getUserId, Arrays.asList(userIds)));
+    }
+
+    /**
+     * 批量选择授权用户角色
+     *
+     * @param roleId 角色ID
+     * @param userIds 需要删除的用户数据ID
+     * @return 结果
+     */
+    @Override
+    public int insertAuthUsers(Long roleId, Long[] userIds) {
+        // 新增用户与角色管理
+        List<SysUserRole> list = new ArrayList<SysUserRole>();
+        for (Long userId : userIds) {
+            SysUserRole ur = new SysUserRole();
+            ur.setUserId(userId);
+            ur.setRoleId(roleId);
+            list.add(ur);
+        }
+        return userRoleMapper.insertAll(list);
+    }
 }

+ 64 - 3
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java

@@ -69,6 +69,30 @@ public class SysUserServiceImpl extends ServicePlusImpl<SysUserMapper, SysUser>
         return baseMapper.selectUserList(user);
     }
 
+    /**
+     * 根据条件分页查询已分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    @Override
+    @DataScope(deptAlias = "d", userAlias = "u", isUser = true)
+    public TableDataInfo<SysUser> selectAllocatedList(SysUser user) {
+		return PageUtils.buildDataInfo(baseMapper.selectAllocatedList(PageUtils.buildPage(), user));
+    }
+
+    /**
+     * 根据条件分页查询未分配用户角色列表
+     *
+     * @param user 用户信息
+     * @return 用户信息集合信息
+     */
+    @Override
+    @DataScope(deptAlias = "d", userAlias = "u", isUser = true)
+    public TableDataInfo<SysUser> selectUnallocatedList(SysUser user) {
+		return PageUtils.buildDataInfo(baseMapper.selectUnallocatedList(PageUtils.buildPage(), user));
+    }
+
     /**
      * 通过用户名查询用户
      *
@@ -231,6 +255,21 @@ public class SysUserServiceImpl extends ServicePlusImpl<SysUserMapper, SysUser>
         return baseMapper.updateById(user);
     }
 
+    /**
+     * 用户授权角色
+     *
+     * @param userId 用户ID
+     * @param roleIds 角色组
+     */
+    @Override
+    @Transactional
+    public void insertUserAuth(Long userId, Long[] roleIds)
+    {
+        userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
+			.eq(SysUserRole::getUserId, userId));
+        insertUserRole(userId, roleIds);
+    }
+
     /**
      * 修改用户状态
      *
@@ -257,7 +296,7 @@ public class SysUserServiceImpl extends ServicePlusImpl<SysUserMapper, SysUser>
      * 修改用户头像
      *
      * @param userName 用户名
-     * @param avatar   头像地址
+     * @param avatar 头像地址
      * @return 结果
      */
     @Override
@@ -338,6 +377,28 @@ public class SysUserServiceImpl extends ServicePlusImpl<SysUserMapper, SysUser>
         }
     }
 
+    /**
+     * 新增用户角色信息
+     *
+     * @param userId 用户ID
+     * @param roleIds 角色组
+     */
+    public void insertUserRole(Long userId, Long[] roleIds) {
+        if (Validator.isNotNull(roleIds)) {
+            // 新增用户与角色管理
+            List<SysUserRole> list = new ArrayList<SysUserRole>();
+            for (Long roleId : roleIds) {
+                SysUserRole ur = new SysUserRole();
+                ur.setUserId(userId);
+                ur.setRoleId(roleId);
+                list.add(ur);
+            }
+            if (list.size() > 0) {
+                userRoleMapper.insertAll(list);
+            }
+        }
+    }
+
     /**
      * 通过用户ID删除用户
      *
@@ -377,9 +438,9 @@ public class SysUserServiceImpl extends ServicePlusImpl<SysUserMapper, SysUser>
     /**
      * 导入用户数据
      *
-     * @param userList        用户数据列表
+     * @param userList 用户数据列表
      * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
-     * @param operName        操作用户
+     * @param operName 操作用户
      * @return 结果
      */
     @Override

+ 42 - 3
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml

@@ -75,9 +75,9 @@
                r.data_scope,
                r.status as role_status
         from sys_user u
-                 left join sys_dept d on u.dept_id = d.dept_id
-                 left join sys_user_role ur on u.user_id = ur.user_id
-                 left join sys_role r on r.role_id = ur.role_id
+		    left join sys_dept d on u.dept_id = d.dept_id
+		    left join sys_user_role ur on u.user_id = ur.user_id
+		    left join sys_role r on r.role_id = ur.role_id
     </sql>
 
     <select id="selectPageUserList" parameterType="SysUser" resultMap="SysUserResult">
@@ -142,6 +142,45 @@
         </if>
     </select>
 
+    <select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">
+        select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
+        from sys_user u
+             left join sys_dept d on u.dept_id = d.dept_id
+             left join sys_user_role ur on u.user_id = ur.user_id
+             left join sys_role r on r.role_id = ur.role_id
+        where u.del_flag = '0' and r.role_id = #{user.roleId}
+        <if test="user.userName != null and user.userName != ''">
+            AND u.user_name like concat('%', #{user.userName}, '%')
+        </if>
+        <if test="user.phonenumber != null and user.phonenumber != ''">
+            AND u.phonenumber like concat('%', #{user.phonenumber}, '%')
+        </if>
+        <!-- 数据范围过滤 -->
+        <if test="user.params.dataScope != null and user.params.dataScope != ''">
+            AND ( ${user.params.dataScope} )
+        </if>
+    </select>
+
+    <select id="selectUnallocatedList" parameterType="SysUser" resultMap="SysUserResult">
+        select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
+        from sys_user u
+             left join sys_dept d on u.dept_id = d.dept_id
+             left join sys_user_role ur on u.user_id = ur.user_id
+             left join sys_role r on r.role_id = ur.role_id
+        where u.del_flag = '0' and (r.role_id != #{user.roleId} or r.role_id IS NULL)
+        and u.user_id not in (select u.user_id from sys_user u inner join sys_user_role ur on u.user_id = ur.user_id and ur.role_id = #{user.roleId})
+        <if test="user.userName != null and user.userName != ''">
+            AND u.user_name like concat('%', #{user.userName}, '%')
+        </if>
+        <if test="user.phonenumber != null and user.phonenumber != ''">
+            AND u.phonenumber like concat('%', #{user.phonenumber}, '%')
+        </if>
+        <!-- 数据范围过滤 -->
+        <if test="user.params.dataScope != null and user.params.dataScope != ''">
+            AND ( ${user.params.dataScope} )
+        </if>
+    </select>
+
     <select id="selectUserByUserName" parameterType="String" resultMap="SysUserResult">
         <include refid="selectUserVo"/>
         where u.user_name = #{userName}

+ 3 - 0
ruoyi-ui/.env.development

@@ -7,5 +7,8 @@ ENV = 'development'
 # 若依管理系统/开发环境
 VUE_APP_BASE_API = '/dev-api'
 
+# 监控地址
+VUE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/login'
+
 # 路由懒加载
 VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 3 - 0
ruoyi-ui/.env.production

@@ -4,5 +4,8 @@ VUE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
 # 生产环境配置
 ENV = 'production'
 
+# 监控地址
+VUE_APP_MONITRO_ADMIN = '/admin/login'
+
 # 若依管理系统/生产环境
 VUE_APP_BASE_API = '/prod-api'

+ 3 - 0
ruoyi-ui/.env.staging

@@ -6,5 +6,8 @@ NODE_ENV = production
 # 测试环境配置
 ENV = 'staging'
 
+# 监控地址
+VUE_APP_MONITRO_ADMIN = '/admin/login'
+
 # 若依管理系统/测试环境
 VUE_APP_BASE_API = '/stage-api'

+ 1 - 1
ruoyi-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "ruoyi-vue-plus",
-  "version": "2.4.0",
+  "version": "2.5.0",
   "description": "RuoYi-Vue-Plus后台管理系统",
   "author": "LionLi",
   "license": "MIT",

+ 47 - 1
ruoyi-ui/src/api/system/role.js

@@ -72,4 +72,50 @@ export function exportRole(query) {
     method: 'get',
     params: query
   })
-}
+}
+
+
+// 查询角色已授权用户列表
+export function allocatedUserList(query) {
+  return request({
+    url: '/system/role/authUser/allocatedList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询角色未授权用户列表
+export function unallocatedUserList(query) {
+  return request({
+    url: '/system/role/authUser/unallocatedList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 取消用户授权角色
+export function authUserCancel(data) {
+  return request({
+    url: '/system/role/authUser/cancel',
+    method: 'put',
+    data: data
+  })
+}
+
+// 批量取消用户授权角色
+export function authUserCancelAll(data) {
+  return request({
+    url: '/system/role/authUser/cancelAll',
+    method: 'put',
+    params: data
+  })
+}
+
+// 授权用户选择
+export function authUserSelectAll(data) {
+  return request({
+    url: '/system/role/authUser/selectAll',
+    method: 'put',
+    params: data
+  })
+}

+ 17 - 0
ruoyi-ui/src/api/system/user.js

@@ -125,3 +125,20 @@ export function importTemplate() {
     method: 'get'
   })
 }
+
+// 查询授权角色
+export function getAuthRole(userId) {
+  return request({
+    url: '/system/user/authRole/' + userId,
+    method: 'get'
+  })
+}
+
+// 保存授权角色
+export function updateAuthRole(data) {
+  return request({
+    url: '/system/user/authRole',
+    method: 'put',
+    params: data
+  })
+}

+ 18 - 0
ruoyi-ui/src/assets/styles/ruoyi.scss

@@ -53,6 +53,13 @@
 	margin-left: 20px;
 }
 
+.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
+	font-family: inherit;
+	font-weight: 500;
+	line-height: 1.1;
+	color: inherit;
+}
+
 .el-dialog:not(.is-fullscreen){
 	margin-top: 6vh !important;
 }
@@ -120,6 +127,17 @@
 	width: inherit;
 }
 
+/** 表格更多操作下拉样式 */
+.el-table .el-dropdown-link {
+	cursor: pointer;
+	color: #1890ff;
+	margin-left: 5px;
+}
+
+.el-table .el-dropdown, .el-icon-arrow-down {
+	font-size: 12px;
+}
+
 .el-tree-node__content > .el-checkbox {
 	margin-right: 8px;
 }

+ 7 - 6
ruoyi-ui/src/components/Editor/index.vue

@@ -9,7 +9,7 @@
       :headers="headers"
       style="display: none"
       ref="upload"
-      v-if="this.uploadUrl"
+      v-if="this.type == 'url'"
     >
     </el-upload>
     <div class="editor" ref="editor" :style="styles"></div>
@@ -46,14 +46,15 @@ export default {
       type: Boolean,
       default: false,
     },
-    /* 上传地址 */
-    uploadUrl: {
+    /* 类型(base64格式、url格式) */
+    type: {
       type: String,
-      default: "",
+      default: "url",
     }
   },
   data() {
     return {
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken()
       },
@@ -119,7 +120,7 @@ export default {
       const editor = this.$refs.editor;
       this.Quill = new Quill(editor, this.options);
       // 如果设置了上传地址则自定义图片上传事件
-      if (this.uploadUrl) {
+      if (this.type == 'url') {
         let toolbar = this.Quill.getModule("toolbar");
         toolbar.addHandler("image", (value) => {
           this.uploadType = "image";
@@ -165,7 +166,7 @@ export default {
         // 获取光标所在位置
         let length = quill.getSelection().index;
         // 插入图片  res.url为服务器返回的图片地址
-        quill.insertEmbed(length, "image", res.url);
+        quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.data.fileName);
         // 调整光标到最后
         quill.setSelection(length + 1);
       } else {

+ 48 - 29
ruoyi-ui/src/components/FileUpload/index.vue

@@ -4,7 +4,7 @@
       :action="uploadFileUrl"
       :before-upload="handleBeforeUpload"
       :file-list="fileList"
-      :limit="1"
+      :limit="limit"
       :on-error="handleUploadError"
       :on-exceed="handleExceed"
       :on-success="handleUploadSuccess"
@@ -26,8 +26,8 @@
 
     <!-- 文件列表 -->
     <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
-      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in list">
-        <el-link :href="file.url" :underline="false" target="_blank">
+      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
+        <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
           <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
         </el-link>
         <div class="ele-upload-list__item-content-action">
@@ -42,9 +42,15 @@
 import { getToken } from "@/utils/auth";
 
 export default {
+  name: "FileUpload",
   props: {
     // 值
     value: [String, Object, Array],
+    // 数量限制
+    limit: {
+      type: Number,
+      default: 5,
+    },
     // 大小限制(MB)
     fileSize: {
       type: Number,
@@ -63,6 +69,7 @@ export default {
   },
   data() {
     return {
+      baseUrl: process.env.VUE_APP_BASE_API,
       uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken(),
@@ -70,30 +77,35 @@ export default {
       fileList: [],
     };
   },
+  watch: {
+    value: {
+      handler(val) {
+        if (val) {
+          let temp = 1;
+          // 首先将值转为数组
+          const list = Array.isArray(val) ? val : this.value.split(',');
+          // 然后将数组转为对象数组
+          this.fileList = list.map(item => {
+            if (typeof item === "string") {
+              item = { name: item, url: item };
+            }
+            item.uid = item.uid || new Date().getTime() + temp++;
+            return item;
+          });
+        } else {
+          this.fileList = [];
+          return [];
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
   computed: {
     // 是否显示提示
     showTip() {
       return this.isShowTip && (this.fileType || this.fileSize);
     },
-    // 列表
-    list() {
-      let temp = 1;
-      if (this.value) {
-        // 首先将值转为数组
-        const list = Array.isArray(this.value) ? this.value : [this.value];
-        // 然后将数组转为对象数组
-        return list.map((item) => {
-          if (typeof item === "string") {
-            item = { name: item, url: item };
-          }
-          item.uid = item.uid || new Date().getTime() + temp++;
-          return item;
-        });
-      } else {
-        this.fileList = [];
-        return [];
-      }
-    },
   },
   methods: {
     // 上传前校检格式和大小
@@ -126,7 +138,7 @@ export default {
     },
     // 文件个数超出
     handleExceed() {
-      this.$message.error(`只允许上传单个文件`);
+      this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
     },
     // 上传失败
     handleUploadError(err) {
@@ -135,12 +147,13 @@ export default {
     // 上传成功回调
     handleUploadSuccess(res, file) {
       this.$message.success("上传成功");
-      this.$emit("input", res.data.url);
+      this.fileList.push({ name: res.data.fileName, url: res.data.fileName });
+      this.$emit("input", this.listToString(this.fileList));
     },
     // 删除文件
     handleDelete(index) {
       this.fileList.splice(index, 1);
-      this.$emit("input", '');
+      this.$emit("input", this.listToString(this.fileList));
     },
     // 获取文件名称
     getFileName(name) {
@@ -149,11 +162,17 @@ export default {
       } else {
         return "";
       }
+    },
+    // 对象转成指定字符串分隔
+    listToString(list, separator) {
+      let strs = "";
+      separator = separator || ",";
+      for (let i in list) {
+        strs += list[i].url + separator;
+      }
+      return strs != '' ? strs.substr(0, strs.length - 1) : '';
     }
-  },
-  created() {
-    this.fileList = this.list;
-  },
+  }
 };
 </script>
 

+ 3 - 1
ruoyi-ui/src/components/HeaderSearch/index.vue

@@ -70,9 +70,11 @@ export default {
       this.show = false
     },
     change(val) {
+      const path = val.path;
       if(this.ishttp(val.path)) {
         // http(s):// 路径新窗口打开
-        window.open(val.path, "_blank");
+        const pindex = path.indexOf("http");
+        window.open(path.substr(pindex, path.length), "_blank");
       } else {
         this.$router.push(val.path)
       }

+ 154 - 44
ruoyi-ui/src/components/ImageUpload/index.vue

@@ -5,33 +5,38 @@
       list-type="picture-card"
       :on-success="handleUploadSuccess"
       :before-upload="handleBeforeUpload"
+      :limit="limit"
       :on-error="handleUploadError"
+      :on-exceed="handleExceed"
       name="file"
-      :show-file-list="false"
+      :on-remove="handleRemove"
+      :show-file-list="true"
       :headers="headers"
-      style="display: inline-block; vertical-align: top"
+      :file-list="fileList"
+      :on-preview="handlePictureCardPreview"
+      :class="{hide: this.fileList.length >= this.limit}"
     >
-      <el-image v-if="!value" :src="value">
-        <div slot="error" class="image-slot">
-          <i class="el-icon-plus" />
-        </div>
-      </el-image>
-      <div v-else class="image">
-        <el-image :src="value" :style="`width:150px;height:150px;`" fit="fill"/>
-        <div class="mask">
-          <div class="actions">
-            <span title="预览" @click.stop="dialogVisible = true">
-              <i class="el-icon-zoom-in" />
-            </span>
-            <span title="移除" @click.stop="removeImage">
-              <i class="el-icon-delete" />
-            </span>
-          </div>
-        </div>
-      </div>
+      <i class="el-icon-plus"></i>
     </el-upload>
-    <el-dialog :visible.sync="dialogVisible" title="预览" width="800" append-to-body>
-      <img :src="value" style="display: block; max-width: 100%; margin: 0 auto;">
+
+    <!-- 上传提示 -->
+    <div class="el-upload__tip" slot="tip" v-if="showTip">
+      请上传
+      <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
+      <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
+      的文件
+    </div>
+
+    <el-dialog
+      :visible.sync="dialogVisible"
+      title="预览"
+      width="800"
+      append-to-body
+    >
+      <img
+        :src="dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
     </el-dialog>
   </div>
 </template>
@@ -40,36 +45,128 @@
 import { getToken } from "@/utils/auth";
 
 export default {
+  props: {
+    value: [String, Object, Array],
+    // 图片数量限制
+    limit: {
+      type: Number,
+      default: 5,
+    },
+    // 大小限制(MB)
+    fileSize: {
+       type: Number,
+      default: 5,
+    },
+    // 文件类型, 例如['png', 'jpg', 'jpeg']
+    fileType: {
+      type: Array,
+      default: () => ["png", "jpg", "jpeg"],
+    },
+    // 是否显示提示
+    isShowTip: {
+      type: Boolean,
+      default: true
+    }
+  },
   data() {
     return {
+      dialogImageUrl: "",
       dialogVisible: false,
+      hideUpload: false,
+      baseUrl: process.env.VUE_APP_BASE_API,
       uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken(),
       },
+      fileList: []
     };
   },
-  props: {
+  watch: {
     value: {
-      type: String,
-      default: "",
+      handler(val) {
+        if (val) {
+          // 首先将值转为数组
+          const list = Array.isArray(val) ? val : this.value.split(',');
+          // 然后将数组转为对象数组
+          this.fileList = list.map(item => {
+            if (typeof item === "string") {
+              if (item.indexOf(this.baseUrl) === -1) {
+                  item = { name: this.baseUrl + item, url: this.baseUrl + item };
+              } else {
+                  item = { name: item, url: item };
+              }
+            }
+            return item;
+          });
+        } else {
+          this.fileList = [];
+          return [];
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  computed: {
+    // 是否显示提示
+    showTip() {
+      return this.isShowTip && (this.fileType || this.fileSize);
     },
   },
   methods: {
-    removeImage() {
-      this.$emit("input", "");
+    // 删除图片
+    handleRemove(file, fileList) {
+      const findex = this.fileList.indexOf(file.name);
+      this.fileList.splice(findex, 1);
+      this.$emit("input", this.listToString(this.fileList));
     },
+    // 上传成功回调
     handleUploadSuccess(res) {
-      this.$emit("input", res.data.url);
+      this.fileList.push({ name: res.data.fileName, url: res.data.fileName });
+      this.$emit("input", this.listToString(this.fileList));
       this.loading.close();
     },
-    handleBeforeUpload() {
+    // 上传前loading加载
+    handleBeforeUpload(file) {
+      let isImg = false;
+      if (this.fileType.length) {
+        let fileExtension = "";
+        if (file.name.lastIndexOf(".") > -1) {
+          fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
+        }
+        isImg = this.fileType.some(type => {
+          if (file.type.indexOf(type) > -1) return true;
+          if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+          return false;
+        });
+      } else {
+        isImg = file.type.indexOf("image") > -1;
+      }
+
+      if (!isImg) {
+        this.$message.error(
+          `文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`
+        );
+        return false;
+      }
+      if (this.fileSize) {
+        const isLt = file.size / 1024 / 1024 < this.fileSize;
+        if (!isLt) {
+          this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
+          return false;
+        }
+      }
       this.loading = this.$loading({
         lock: true,
         text: "上传中",
         background: "rgba(0, 0, 0, 0.7)",
       });
     },
+    // 文件个数超出
+    handleExceed() {
+      this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
+    },
+    // 上传失败
     handleUploadError() {
       this.$message({
         type: "error",
@@ -77,24 +174,37 @@ export default {
       });
       this.loading.close();
     },
-  },
-  watch: {},
+    // 预览
+    handlePictureCardPreview(file) {
+      this.dialogImageUrl = file.url;
+      this.dialogVisible = true;
+    },
+    // 对象转成指定字符串分隔
+    listToString(list, separator) {
+      let strs = "";
+      separator = separator || ",";
+      for (let i in list) {
+        strs += list[i].url + separator;
+      }
+      return strs != '' ? strs.substr(0, strs.length - 1) : '';
+    }
+  }
 };
 </script>
-
 <style scoped lang="scss">
-.image {
-  position: relative;
-  .mask {
+// .el-upload--picture-card 控制加号部分
+::v-deep.hide .el-upload--picture-card {
+    display: none;
+}
+// 去掉动画效果
+::v-deep .el-list-enter-active,
+::v-deep .el-list-leave-active {
+    transition: all 0s;
+}
+
+::v-deep .el-list-enter, .el-list-leave-active {
     opacity: 0;
-    position: absolute;
-    top: 0;
-    width: 100%;
-    background-color: rgba(0, 0, 0, 0.5);
-    transition: all 0.3s;
-  }
-  &:hover .mask {
-    opacity: 1;
-  }
+    transform: translateY(0);
 }
 </style>
+

+ 2 - 2
ruoyi-ui/src/components/TopNav/index.vue

@@ -73,9 +73,9 @@ export default {
             if(router.path === "/") {
               router.children[item].path = "/redirect/" + router.children[item].path;
             } else {
-			  if(!this.ishttp(router.children[item].path)) {
+              if(!this.ishttp(router.children[item].path)) {
                 router.children[item].path = router.path + "/" + router.children[item].path;
-			  }
+              }
             }
             router.children[item].parentPath = router.path;
           }

+ 64 - 0
ruoyi-ui/src/directive/dialog/drag.js

@@ -0,0 +1,64 @@
+/**
+* v-dialogDrag 弹窗拖拽
+* Copyright (c) 2019 ruoyi
+*/
+
+export default {
+  bind(el, binding, vnode, oldVnode) {
+    const value = binding.value
+    if (value == false) return
+    // 获取拖拽内容头部
+    const dialogHeaderEl = el.querySelector('.el-dialog__header');
+    const dragDom = el.querySelector('.el-dialog');
+    dialogHeaderEl.style.cursor = 'move';
+    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
+    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);
+    dragDom.style.position = 'absolute';
+    dragDom.style.marginTop = 0;
+    let width = dragDom.style.width;
+    if (width.includes('%')) {
+      width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100);
+    } else {
+      width = +width.replace(/\px/g, '');
+    }
+    dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`;
+    // 鼠标按下事件
+    dialogHeaderEl.onmousedown = (e) => {
+      // 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
+      const disX = e.clientX - dialogHeaderEl.offsetLeft;
+      const disY = e.clientY - dialogHeaderEl.offsetTop;
+
+      // 获取到的值带px 正则匹配替换
+      let styL, styT;
+
+      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
+      if (sty.left.includes('%')) {
+        styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
+        styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
+      } else {
+        styL = +sty.left.replace(/\px/g, '');
+        styT = +sty.top.replace(/\px/g, '');
+      };
+
+      // 鼠标拖拽事件
+      document.onmousemove = function (e) {
+        // 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
+        const l = e.clientX - disX;
+        const t = e.clientY - disY;
+
+        let finallyL = l + styL
+        let finallyT = t + styT
+
+        // 移动当前元素
+        dragDom.style.left = `${finallyL}px`;
+        dragDom.style.top = `${finallyT}px`;
+
+      };
+
+      document.onmouseup = function (e) {
+        document.onmousemove = null;
+        document.onmouseup = null;
+      };
+    }
+  }
+};

+ 5 - 2
ruoyi-ui/src/directive/permission/index.js → ruoyi-ui/src/directive/index.js

@@ -1,14 +1,17 @@
-import hasRole from './hasRole'
-import hasPermi from './hasPermi'
+import hasRole from './permission/hasRole'
+import hasPermi from './permission/hasPermi'
+import dialogDrag from './dialog/drag'
 
 const install = function(Vue) {
   Vue.directive('hasRole', hasRole)
   Vue.directive('hasPermi', hasPermi)
+  Vue.directive('dialogDrag', dialogDrag)
 }
 
 if (window.Vue) {
   window['hasRole'] = hasRole
   window['hasPermi'] = hasPermi
+  window['dialogDrag'] = dialogDrag
   Vue.use(install); // eslint-disable-line
 }
 

+ 2 - 2
ruoyi-ui/src/directive/permission/hasPermi.js

@@ -1,8 +1,8 @@
  /**
- * 操作权限处理
+ * v-hasPermi 操作权限处理
  * Copyright (c) 2019 ruoyi
  */
- 
+
 import store from '@/store'
 
 export default {

+ 2 - 2
ruoyi-ui/src/directive/permission/hasRole.js

@@ -1,8 +1,8 @@
  /**
- * 角色权限处理
+ * v-hasRole 角色权限处理
  * Copyright (c) 2019 ruoyi
  */
- 
+
 import store from '@/store'
 
 export default {

+ 1 - 1
ruoyi-ui/src/layout/components/AppMain.vue

@@ -51,7 +51,7 @@ export default {
 // fix css style bug in open el-dialog
 .el-popup-parent--hidden {
   .fixed-header {
-    padding-right: 15px;
+    padding-right: 17px;
   }
 }
 </style>

+ 27 - 0
ruoyi-ui/src/layout/components/InnerLink/index.vue

@@ -0,0 +1,27 @@
+<script>
+export default {
+  data() {
+    return {};
+  },
+  render() {
+    const { $route: { meta: { link } }, } = this;
+    if ({ link }.link === "") {
+      return "404";
+    }
+    let url = { link }.link;
+    const height = document.documentElement.clientHeight - 94.5 + "px";
+    const style = { height: height };
+
+    return (
+      <div style={style}>
+        <iframe
+          src={url}
+          frameborder="no"
+          style="width: 100%; height: 100%"
+          scrolling="auto"
+        ></iframe>
+      </div>
+    );
+  },
+};
+</script>

+ 11 - 2
ruoyi-ui/src/main.js

@@ -10,7 +10,7 @@ import '@/assets/styles/ruoyi.scss' // ruoyi css
 import App from './App'
 import store from './store'
 import router from './router'
-import permission from './directive/permission'
+import directive from './directive' //directive
 
 import './assets/icons' // icon
 import './permission' // permission control
@@ -20,6 +20,12 @@ import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels,
 import Pagination from "@/components/Pagination";
 // 自定义表格工具组件
 import RightToolbar from "@/components/RightToolbar"
+// 富文本组件
+import Editor from "@/components/Editor"
+// 文件上传组件
+import FileUpload from "@/components/FileUpload"
+// 图片上传组件
+import ImageUpload from "@/components/ImageUpload"
 // 字典标签组件
 import DictTag from '@/components/DictTag'
 // 头部标签组件
@@ -52,8 +58,11 @@ Vue.prototype.msgInfo = function (msg) {
 Vue.component('DictTag', DictTag)
 Vue.component('Pagination', Pagination)
 Vue.component('RightToolbar', RightToolbar)
+Vue.component('Editor', Editor)
+Vue.component('FileUpload', FileUpload)
+Vue.component('ImageUpload', ImageUpload)
 
-Vue.use(permission)
+Vue.use(directive)
 Vue.use(VueMeta)
 
 /**

+ 27 - 0
ruoyi-ui/src/router/index.js

@@ -6,6 +6,7 @@ Vue.use(Router)
 /* Layout */
 import Layout from '@/layout'
 import ParentView from '@/components/ParentView';
+import InnerLink from '@/layout/components/InnerLink'
 
 /**
  * Note: 路由配置项
@@ -80,6 +81,32 @@ export const constantRoutes = [
       }
     ]
   },
+  {
+    path: '/auth',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: 'role/:userId(\\d+)',
+        component: (resolve) => require(['@/views/system/user/authRole'], resolve),
+        name: 'AuthRole',
+        meta: { title: '分配角色'}
+      }
+    ]
+  },
+  {
+    path: '/auth',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: 'user/:roleId(\\d+)',
+        component: (resolve) => require(['@/views/system/role/authUser'], resolve),
+        name: 'AuthUser',
+        meta: { title: '分配用户'}
+      }
+    ]
+  },
   {
     path: '/dict',
     component: Layout,

+ 3 - 0
ruoyi-ui/src/store/modules/permission.js

@@ -2,6 +2,7 @@ import { constantRoutes } from '@/router'
 import { getRouters } from '@/api/menu'
 import Layout from '@/layout/index'
 import ParentView from '@/components/ParentView';
+import InnerLink from '@/layout/components/InnerLink'
 
 const permission = {
   state: {
@@ -65,6 +66,8 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
         route.component = Layout
       } else if (route.component === 'ParentView') {
         route.component = ParentView
+      } else if (route.component === 'InnerLink') {
+        route.component = InnerLink
       } else {
         route.component = loadView(route.component)
       }

+ 4 - 2
ruoyi-ui/src/views/demo/demo/index.vue

@@ -304,17 +304,19 @@ export default {
           this.buttonLoading = true;
           if (this.form.id != null) {
             updateDemo(this.form).then(response => {
-              this.buttonLoading = false;
               this.msgSuccess("修改成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           } else {
             addDemo(this.form).then(response => {
-              this.buttonLoading = false;
               this.msgSuccess("新增成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           }
         }

+ 4 - 2
ruoyi-ui/src/views/demo/tree/index.vue

@@ -255,17 +255,19 @@ export default {
           this.buttonLoading = true;
           if (this.form.id != null) {
             updateTree(this.form).then(response => {
-              this.buttonLoading = false;
               this.msgSuccess("修改成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           } else {
             addTree(this.form).then(response => {
-              this.buttonLoading = false;
               this.msgSuccess("新增成功");
               this.open = false;
               this.getList();
+            }).finally(() => {
+              this.buttonLoading = false;
             });
           }
         }

+ 26 - 0
ruoyi-ui/src/views/index.vue

@@ -91,6 +91,32 @@
             <span>更新日志</span>
           </div>
           <el-collapse accordion>
+            <el-collapse-item title="v2.5.0 - 2021-7-12">
+              <ol>
+                <li>update springboot 2.4.7 => 2.4.8</li>
+                <li>update knife4j 3.0.2 => 3.0.3</li>
+                <li>update hutool 5.7.2 => 5.7.4</li>
+                <li>update spring-boot-admin 2.4.1 => 2.4.3</li>
+                <li>update redisson 3.15.2 => 3.16.0</li>
+                <li>add 增加 docker 编排 与 shell 脚本</li>
+                <li>add 增加 feign 熔断 自定义结构体解析方法 与 demo 注释</li>
+                <li>add 用户管理新增分配角色功能</li>
+                <li>add 角色管理新增分配用户功能</li>
+                <li>add 增加spring-cache演示案例</li>
+                <li>update 独立 springboot-admin 监控到扩展模块项目</li>
+                <li>update springboot-admin 监控 增加用户登录权限管理</li>
+                <li>update 优化代码生成器 批量导入</li>
+                <li>update 优化 增加MP注入异常拦截</li>
+                <li>update 关闭默认二级缓存 推荐使用 spring-cache 注解手动缓存</li>
+                <li>update FileUpload ImageUpload组件 支持多图片上传</li>
+                <li>update 优化中英文语言配置</li>
+                <li>update 规范maven写法</li>
+                <li>fix redis获取map属性bug修复。</li>
+                <li>fix 修复 按钮loading 后端500卡死问题</li>
+                <li>fix 相对路径下载问题</li>
+                <li>fix 修复 hutool 工具返回结果不一致问题</li>
+              </ol>
+            </el-collapse-item>
             <el-collapse-item title="v2.4.0 - 2021-6-24">
               <ol>
                 <li>update springboot 2.3.11 => 2.4.7</li>

+ 5 - 15
ruoyi-ui/src/views/monitor/admin/index.vue

@@ -1,26 +1,16 @@
 <template>
-  <div v-loading="loading" :style="'height:'+ height">
-    <iframe :src="src" frameborder="no" style="width: 100%;height: 100%" scrolling="auto" />
-  </div>
+  <i-frame :src="url" />
 </template>
 <script>
+import iFrame from "@/components/iFrame/index";
 export default {
   name: "Admin",
+  components: { iFrame },
   data() {
+    console.log(process.env)
     return {
-      src: process.env.VUE_APP_BASE_API + "/admin",
-      height: document.documentElement.clientHeight - 94.5 + "px;",
-      loading: true
+      url: process.env.VUE_APP_MONITRO_ADMIN
     };
   },
-  mounted: function() {
-    setTimeout(() => {
-      this.loading = false;
-    }, 230);
-    const that = this;
-    window.onresize = function temp() {
-      that.height = document.documentElement.clientHeight - 94.5 + "px;";
-    };
-  }
 };
 </script>

+ 0 - 4
ruoyi-ui/src/views/system/notice/index.vue

@@ -177,13 +177,9 @@
 
 <script>
 import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice";
-import Editor from '@/components/Editor';
 
 export default {
   name: "Notice",
-  components: {
-    Editor
-  },
   data() {
     return {
       // 遮罩层

+ 213 - 0
ruoyi-ui/src/views/system/role/authUser.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="app-container">
+     <el-form :model="queryParams" ref="queryForm" v-show="showSearch" :inline="true">
+      <el-form-item label="用户名称" prop="userName">
+        <el-input
+          v-model="queryParams.userName"
+          placeholder="请输入用户名称"
+          clearable
+          size="small"
+          style="width: 240px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="手机号码" prop="phonenumber">
+        <el-input
+          v-model="queryParams.phonenumber"
+          placeholder="请输入手机号码"
+          clearable
+          size="small"
+          style="width: 240px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="openSelectUser"
+          v-hasPermi="['system:role:add']"
+        >添加用户</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-circle-close"
+          size="mini"
+          :disabled="multiple"
+          @click="cancelAuthUserAll"
+          v-hasPermi="['system:role:remove']"
+        >批量取消授权</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-close"
+          size="mini"
+          @click="handleClose"
+        >关闭</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
+      <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
+      <el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
+      <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-circle-close"
+            @click="cancelAuthUser(scope.row)"
+            v-hasPermi="['system:role:remove']"
+          >取消授权</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+    <select-user ref="select" :roleId="queryParams.roleId" @ok="handleQuery" />
+  </div>
+</template>
+
+<script>
+import { allocatedUserList, authUserCancel, authUserCancelAll } from "@/api/system/role";
+import selectUser from "./selectUser";
+
+export default {
+  name: "AuthUser",
+  components: { selectUser },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中用户组
+      userIds: [],
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 用户表格数据
+      userList: [],
+      // 状态数据字典
+      statusOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        roleId: undefined,
+        userName: undefined,
+        phonenumber: undefined
+      }
+    };
+  },
+  created() {
+    const roleId = this.$route.params && this.$route.params.roleId;
+    if (roleId) {
+      this.queryParams.roleId = roleId;
+      this.getList();
+      this.getDicts("sys_normal_disable").then(response => {
+        this.statusOptions = response.data;
+      });
+    }
+  },
+  methods: {
+    /** 查询授权用户列表 */
+    getList() {
+      this.loading = true;
+      allocatedUserList(this.queryParams).then(response => {
+          this.userList = response.rows;
+          this.total = response.total;
+          this.loading = false;
+        }
+      );
+    },
+    // 返回按钮
+    handleClose() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.push({ path: "/system/role" });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.userIds = selection.map(item => item.userId)
+      this.multiple = !selection.length
+    },
+    /** 打开授权用户表弹窗 */
+    openSelectUser() {
+      this.$refs.select.show();
+    },
+    /** 取消授权按钮操作 */
+    cancelAuthUser(row) {
+      const roleId = this.queryParams.roleId;
+      this.$confirm('确认要取消该用户"' + row.userName + '"角色吗?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return authUserCancel({ userId: row.userId, roleId: roleId });
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("取消授权成功");
+      }).catch(() => {});
+    },
+    /** 批量取消授权按钮操作 */
+    cancelAuthUserAll(row) {
+      const roleId = this.queryParams.roleId;
+      const userIds = this.userIds.join(",");
+      this.$confirm('是否取消选中用户授权数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+      }).then(() => {
+          return authUserCancelAll({ roleId: roleId, userIds: userIds });
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("取消授权成功");
+      }).catch(() => {});
+    }
+  }
+};
+</script>

+ 30 - 8
ruoyi-ui/src/views/system/role/index.vue

@@ -124,7 +124,7 @@
         </template>
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template slot-scope="scope">
+        <template slot-scope="scope" v-if="scope.row.roleId !== 1">
           <el-button
             size="mini"
             type="text"
@@ -132,13 +132,6 @@
             @click="handleUpdate(scope.row)"
             v-hasPermi="['system:role:edit']"
           >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-circle-check"
-            @click="handleDataScope(scope.row)"
-            v-hasPermi="['system:role:edit']"
-          >数据权限</el-button>
           <el-button
             size="mini"
             type="text"
@@ -146,6 +139,17 @@
             @click="handleDelete(scope.row)"
             v-hasPermi="['system:role:remove']"
           >删除</el-button>
+          <el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)">
+            <span class="el-dropdown-link">
+              <i class="el-icon-d-arrow-right el-icon--right"></i>更多
+            </span>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item command="handleDataScope" icon="el-icon-circle-check"
+                v-hasPermi="['system:role:edit']">数据权限</el-dropdown-item>
+              <el-dropdown-item command="handleAuthUser" icon="el-icon-user"
+                v-hasPermi="['system:role:edit']">分配用户</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
         </template>
       </el-table-column>
     </el-table>
@@ -469,6 +473,19 @@ export default {
       this.single = selection.length!=1
       this.multiple = !selection.length
     },
+    // 更多操作触发
+    handleCommand(command, row) {
+      switch (command) {
+        case "handleDataScope":
+          this.handleDataScope(row);
+          break;
+        case "handleAuthUser":
+          this.handleAuthUser(row);
+          break;
+        default:
+          break;
+      }
+    },
     // 树权限(展开/折叠)
     handleCheckedTreeExpand(value, type) {
       if (type == 'menu') {
@@ -548,6 +565,11 @@ export default {
         this.title = "分配数据权限";
       });
     },
+    /** 分配用户操作 */
+    handleAuthUser: function(row) {
+      const roleId = row.roleId;
+      this.$router.push("/auth/user/" + roleId);
+    },
     /** 提交按钮 */
     submitForm: function() {
       this.$refs["form"].validate(valid => {

+ 142 - 0
ruoyi-ui/src/views/system/role/selectUser.vue

@@ -0,0 +1,142 @@
+<template>
+  <!-- 授权用户 -->
+  <el-dialog title="选择用户" :visible.sync="visible" width="800px" top="5vh" append-to-body>
+    <el-form :model="queryParams" ref="queryForm" :inline="true">
+      <el-form-item label="用户名称" prop="userName">
+        <el-input
+          v-model="queryParams.userName"
+          placeholder="请输入用户名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="手机号码" prop="phonenumber">
+        <el-input
+          v-model="queryParams.phonenumber"
+          placeholder="请输入手机号码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+    <el-row>
+      <el-table @row-click="clickRow" ref="table" :data="userList" @selection-change="handleSelectionChange" height="260px">
+        <el-table-column type="selection" width="55"></el-table-column>
+        <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
+        <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
+        <el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
+        <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
+        <el-table-column label="状态" align="center" prop="status">
+          <template slot-scope="scope">
+            <dict-tag :options="statusOptions" :value="scope.row.status"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+          <template slot-scope="scope">
+            <span>{{ parseTime(scope.row.createTime) }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-show="total>0"
+        :total="total"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-row>
+    <div slot="footer" class="dialog-footer">
+      <el-button type="primary" @click="handleSelectUser">确 定</el-button>
+      <el-button @click="visible = false">取 消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { unallocatedUserList, authUserSelectAll } from "@/api/system/role";
+export default {
+  props: {
+    // 角色编号
+    roleId: {
+      type: Number
+    }
+  },
+  data() {
+    return {
+      // 遮罩层
+      visible: false,
+      // 选中数组值
+      userIds: [],
+      // 总条数
+      total: 0,
+      // 未授权用户数据
+      userList: [],
+      // 状态数据字典
+      statusOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        roleId: undefined,
+        userName: undefined,
+        phonenumber: undefined
+      }
+    };
+  },
+  created() {
+    this.getDicts("sys_normal_disable").then(response => {
+      this.statusOptions = response.data;
+    });
+  },
+  methods: {
+    // 显示弹框
+    show() {
+      this.queryParams.roleId = this.roleId;
+      this.getList();
+      this.visible = true;
+    },
+    clickRow(row) {
+      this.$refs.table.toggleRowSelection(row);
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.userIds = selection.map(item => item.userId);
+    },
+    // 查询表数据
+    getList() {
+      unallocatedUserList(this.queryParams).then(res => {
+        this.userList = res.rows;
+        this.total = res.total;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 选择授权用户操作 */
+    handleSelectUser() {
+      const roleId = this.queryParams.roleId;
+      const userIds = this.userIds.join(",");
+      authUserSelectAll({ roleId: roleId, userIds: userIds }).then(res => {
+        this.msgSuccess(res.msg);
+        if (res.code === 200) {
+          this.visible = false;
+          this.$emit("ok");
+        }
+      });
+    }
+  }
+};
+</script>

+ 117 - 0
ruoyi-ui/src/views/system/user/authRole.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="app-container">
+    <h4 class="form-header h4">基本信息</h4>
+    <el-form ref="form" :model="form" label-width="80px">
+      <el-row>
+        <el-col :span="8" :offset="2">
+          <el-form-item label="用户昵称" prop="nickName">
+            <el-input v-model="form.nickName" disabled />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8" :offset="2">
+          <el-form-item label="登录账号" prop="phonenumber">
+            <el-input  v-model="form.userName" disabled />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <h4 class="form-header h4">角色信息</h4>
+    <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="table" @selection-change="handleSelectionChange" :data="roles.slice((pageNum-1)*pageSize,pageNum*pageSize)">
+      <el-table-column label="序号" type="index" align="center">
+        <template slot-scope="scope">
+          <span>{{(pageNum - 1) * pageSize + scope.$index + 1}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column type="selection" :reserve-selection="true" width="55"></el-table-column>
+      <el-table-column label="角色编号" align="center" prop="roleId" />
+      <el-table-column label="角色名称" align="center" prop="roleName" />
+      <el-table-column label="权限字符" align="center" prop="roleKey" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="pageNum" :limit.sync="pageSize" />
+
+    <el-form label-width="100px">
+      <el-form-item style="text-align: center;margin-left:-120px;margin-top:30px;">
+        <el-button type="primary" @click="submitForm()">提交</el-button>
+        <el-button @click="close()">返回</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { getAuthRole, updateAuthRole } from "@/api/system/user";
+
+export default {
+  name: "AuthRole",
+  data() {
+    return {
+       // 遮罩层
+      loading: true,
+      // 分页信息
+      total: 0,
+      pageNum: 1,
+      pageSize: 10,
+      // 选中角色编号
+      roleIds:[],
+      // 角色信息
+      roles: [],
+      // 用户信息
+      form: {}
+    };
+  },
+  created() {
+    const userId = this.$route.params && this.$route.params.userId;
+    if (userId) {
+      this.loading = true;
+      getAuthRole(userId).then((response) => {
+        this.form = response.data.user;
+        this.roles = response.data.roles;
+        this.total = this.roles.length;
+        this.$nextTick(() => {
+          this.roles.forEach((row) => {
+            if (row.flag) {
+              this.$refs.table.toggleRowSelection(row);
+            }
+          });
+        });
+        this.loading = false;
+      });
+    }
+  },
+  methods: {
+    /** 单击选中行数据 */
+    clickRow(row) {
+      this.$refs.table.toggleRowSelection(row);
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.roleIds = selection.map((item) => item.roleId);
+    },
+    // 保存选中的数据编号
+    getRowKey(row) {
+      return row.roleId;
+    },
+    /** 提交按钮 */
+    submitForm() {
+      const userId = this.form.userId;
+      const roleIds = this.roleIds.join(",");
+      updateAuthRole({ userId: userId, roleIds: roleIds }).then((response) => {
+        this.msgSuccess("授权成功");
+        this.close();
+      });
+    },
+    /** 关闭按钮 */
+    close() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.push({ path: "/system/user" });
+    },
+  },
+};
+</script>

+ 30 - 9
ruoyi-ui/src/views/system/user/index.vue

@@ -167,7 +167,7 @@
             width="160"
             class-name="small-padding fixed-width"
           >
-            <template slot-scope="scope">
+            <template slot-scope="scope" v-if="scope.row.userId !== 1">
               <el-button
                 size="mini"
                 type="text"
@@ -176,20 +176,23 @@
                 v-hasPermi="['system:user:edit']"
               >修改</el-button>
               <el-button
-                v-if="scope.row.userId !== 1"
                 size="mini"
                 type="text"
                 icon="el-icon-delete"
                 @click="handleDelete(scope.row)"
                 v-hasPermi="['system:user:remove']"
               >删除</el-button>
-              <el-button
-                size="mini"
-                type="text"
-                icon="el-icon-key"
-                @click="handleResetPwd(scope.row)"
-                v-hasPermi="['system:user:resetPwd']"
-              >重置</el-button>
+              <el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)">
+                <span class="el-dropdown-link">
+                  <i class="el-icon-d-arrow-right el-icon--right"></i>更多
+                </span>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item command="handleResetPwd" icon="el-icon-key"
+                    v-hasPermi="['system:user:resetPwd']">重置密码</el-dropdown-item>
+                  <el-dropdown-item command="handleAuthRole" icon="el-icon-circle-check"
+                    v-hasPermi="['system:user:edit']">分配角色</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
             </template>
           </el-table-column>
         </el-table>
@@ -561,6 +564,19 @@ export default {
       this.single = selection.length != 1;
       this.multiple = !selection.length;
     },
+    // 更多操作触发
+    handleCommand(command, row) {
+      switch (command) {
+        case "handleResetPwd":
+          this.handleResetPwd(row);
+          break;
+        case "handleAuthRole":
+          this.handleAuthRole(row);
+          break;
+        default:
+          break;
+      }
+    },
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
@@ -603,6 +619,11 @@ export default {
           });
         }).catch(() => {});
     },
+    /** 分配角色操作 */
+    handleAuthRole: function(row) {
+      const userId = row.userId;
+      this.$router.push("/auth/role/" + userId);
+    },
     /** 提交按钮 */
     submitForm: function() {
       this.$refs["form"].validate(valid => {

+ 1 - 1
ruoyi-ui/vue.config.js

@@ -109,7 +109,7 @@ module.exports = {
           config.optimization.runtimeChunk('single'),
           {
              from: path.resolve(__dirname, './public/robots.txt'), //防爬虫文件
-             to: './', //到根目录下
+             to: './' //到根目录下
           }
         }
       )

+ 2 - 2
sql/ry_20210210.sql

@@ -159,7 +159,7 @@ create table sys_menu (
 insert into sys_menu values('1', '系统管理', '0', '1', 'system',           null,   1, 0, 'M', '0', '0', '', 'system',   'admin', sysdate(), '', null, '系统管理目录');
 insert into sys_menu values('2', '系统监控', '0', '2', 'monitor',          null,   1, 0, 'M', '0', '0', '', 'monitor',  'admin', sysdate(), '', null, '系统监控目录');
 insert into sys_menu values('3', '系统工具', '0', '3', 'tool',             null,   1, 0, 'M', '0', '0', '', 'tool',     'admin', sysdate(), '', null, '系统工具目录');
-insert into sys_menu values('4', '若依官网', '0', '4', 'http://ruoyi.vip', null ,  0, 0, 'M', '0', '0', '', 'guide',    'admin', sysdate(), '', null, '若依官网地址');
+insert into sys_menu values('4', 'PLUS官网', '0', '4', 'https://gitee.com/JavaLionLi/RuoYi-Vue-Plus', null ,  0, 0, 'M', '0', '0', '', 'guide',    'admin', sysdate(), '', null, 'RuoYi-Vue-Plus官网地址');
 -- 二级菜单
 insert into sys_menu values('100',  '用户管理', '1',   '1', 'user',       'system/user/index',        1, 0, 'C', '0', '0', 'system:user:list',        'user',          'admin', sysdate(), '', null, '用户管理菜单');
 insert into sys_menu values('101',  '角色管理', '1',   '2', 'role',       'system/role/index',        1, 0, 'C', '0', '0', 'system:role:list',        'peoples',       'admin', sysdate(), '', null, '角色管理菜单');
@@ -685,4 +685,4 @@ create table gen_table_column (
   update_by         varchar(64)     default ''                 comment '更新者',
   update_time       datetime                                   comment '更新时间',
   primary key (column_id)
-) engine=innodb auto_increment=1 comment = '代码生成业务表字段';
+) engine=innodb auto_increment=1 comment = '代码生成业务表字段';