!67 同步dev分支

Merge pull request !67 from 疯狂的狮子Li/dev
This commit is contained in:
疯狂的狮子Li 2021-07-12 03:39:43 +00:00 committed by Gitee
commit 5632278688
90 changed files with 3513 additions and 1475 deletions

View File

@ -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
docker/deploy.sh Normal file
View File

@ -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
docker/docker-compose.yml Normal file
View File

@ -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
docker/nginx/nginx.conf Normal file
View File

@ -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;
}
}
}

21
pom.xml
View File

@ -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
ruoyi-admin/Dockerfile Normal file
View File

@ -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"]

View File

@ -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>
@ -83,6 +83,25 @@
<warName>${project.artifactId}</warName>
</configuration>
</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>

View File

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

View File

@ -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)://开头");
}

View File

@ -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;
@ -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));
}
}

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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=

View File

@ -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}]

View File

@ -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>

View File

@ -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";

View File

@ -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;

View File

@ -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

View File

@ -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) {

View File

@ -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);
}
/**

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -716,7 +716,7 @@ public class ExcelUtil<T>
for (String item : convertSource)
{
String[] itemArray = item.split("=");
if (StrUtil.containsAny(separator, propertyValue))
if (StrUtil.containsAny(propertyValue, separator))
{
for (String value : propertyValue.split(separator))
{
@ -753,7 +753,7 @@ public class ExcelUtil<T>
for (String item : convertSource)
{
String[] itemArray = item.split("=");
if (StrUtil.containsAny(separator, propertyValue))
if (StrUtil.containsAny(propertyValue, separator))
{
for (String value : propertyValue.split(separator))
{

View File

@ -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>

View File

@ -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);
}
}

View File

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

18
ruoyi-extend/pom.xml Normal file
View File

@ -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>

View File

@ -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"]

View File

@ -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>

View File

@ -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 监控启动成功" );
}
}

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
// }
// }
}

View File

@ -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);
}

View File

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

View File

@ -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;
}
}

View File

@ -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,6 +18,7 @@ public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
try {
//根据属性名字设置要填充的值
if (metaObject.hasGetter("createTime")) {
if (metaObject.getValue("createTime") == null) {
@ -26,10 +30,14 @@ public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
this.setFieldValByName("createBy", SecurityUtils.getUsername(), metaObject);
}
}
} catch (Exception e) {
throw new CustomException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
}
}
@Override
public void updateFill(MetaObject metaObject) {
try {
if (metaObject.hasGetter("updateBy")) {
if (metaObject.getValue("updateBy") == null) {
this.setFieldValByName("updateBy", SecurityUtils.getUsername(), metaObject);
@ -40,6 +48,9 @@ public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
} catch (Exception e) {
throw new CustomException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
}
}
}

View File

@ -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>

View File

@ -182,8 +182,8 @@ 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) {
@ -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)) {

View File

@ -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}> {
}

View File

@ -256,46 +256,10 @@
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() {
@ -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;
});
}
}

View File

@ -308,47 +308,9 @@
<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
@ -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;
});
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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);
/**
* 通过用户名查询用户
*

View File

@ -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);
}

View File

@ -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);
/**
* 修改用户状态
*

View File

@ -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,6 +345,8 @@ 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()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
component = UserConstants.INNER_LINK;
} else if (StrUtil.isEmpty(menu.getComponent()) && isParentView(menu)) {
component = UserConstants.PARENT_VIEW;
}
@ -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组件
*

View File

@ -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);
}
}

View File

@ -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);
}
/**
* 修改用户状态
*
@ -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删除用户
*

View File

@ -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}

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -73,3 +73,49 @@ export function exportRole(query) {
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
})
}

View File

@ -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
})
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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,19 +77,15 @@ export default {
fileList: [],
};
},
computed: {
//
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
//
list() {
watch: {
value: {
handler(val) {
if (val) {
let temp = 1;
if (this.value) {
//
const list = Array.isArray(this.value) ? this.value : [this.value];
const list = Array.isArray(val) ? val : this.value.split(',');
//
return list.map((item) => {
this.fileList = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
@ -94,6 +97,15 @@ export default {
return [];
}
},
deep: true,
immediate: true
}
},
computed: {
//
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
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>

View File

@ -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)
}

View File

@ -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();
},
//
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
watch: {},
//
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>

View File

@ -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;
};
}
}
};

View File

@ -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
}

View File

@ -1,5 +1,5 @@
/**
* 操作权限处理
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/

View File

@ -1,5 +1,5 @@
/**
* 角色权限处理
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/

View File

@ -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>

View File

@ -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>

View File

@ -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)
/**

View File

@ -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,

View File

@ -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)
}

View File

@ -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;
});
}
}

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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 {
//

View File

@ -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>

View File

@ -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 => {

View File

@ -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>

View File

@ -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>

View File

@ -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 => {

View File

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

View File

@ -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, '角色管理菜单');