完成主要在线 session 的功能

This commit is contained in:
YunaiV 2021-01-30 01:21:25 +08:00
parent ab94fe2d4b
commit 753c7678ee
34 changed files with 1349 additions and 1260 deletions

View File

@ -8,17 +8,15 @@ import com.ruoyi.common.core.controller.BaseController;
/** /**
* swagger 接口 * swagger 接口
* *
* @author ruoyi * @author ruoyi
*/ */
@Controller @Controller
@RequestMapping("/tool/swagger") @RequestMapping("/tool/swagger")
public class SwaggerController extends BaseController public class SwaggerController extends BaseController {
{
@PreAuthorize("@ss.hasPermi('tool:swagger:view')") @PreAuthorize("@ss.hasPermi('tool:swagger:view')")
@GetMapping() @GetMapping()
public String index() public String index() {
{
return redirect("/swagger-ui.html"); return redirect("/swagger-ui.html");
} }
} }

View File

@ -1,145 +1,145 @@
package com.ruoyi.framework.aspectj; package com.ruoyi.framework.aspectj;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature; import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataScope; import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.entity.SysRole; import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.web.service.TokenService; import com.ruoyi.framework.web.service.TokenService;
/** /**
* 数据过滤处理 * 数据过滤处理
* *
* @author ruoyi * @author ruoyi
*/ */
@Aspect @Aspect
@Component @Component
public class DataScopeAspect { public class DataScopeAspect {
/** /**
* 全部数据权限 * 全部数据权限
*/ */
public static final String DATA_SCOPE_ALL = "1"; public static final String DATA_SCOPE_ALL = "1";
/** /**
* 自定数据权限 * 自定数据权限
*/ */
public static final String DATA_SCOPE_CUSTOM = "2"; public static final String DATA_SCOPE_CUSTOM = "2";
/** /**
* 部门数据权限 * 部门数据权限
*/ */
public static final String DATA_SCOPE_DEPT = "3"; public static final String DATA_SCOPE_DEPT = "3";
/** /**
* 部门及以下数据权限 * 部门及以下数据权限
*/ */
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/** /**
* 仅本人数据权限 * 仅本人数据权限
*/ */
public static final String DATA_SCOPE_SELF = "5"; public static final String DATA_SCOPE_SELF = "5";
/** /**
* 数据权限过滤关键字 * 数据权限过滤关键字
*/ */
public static final String DATA_SCOPE = "dataScope"; public static final String DATA_SCOPE = "dataScope";
// 配置织入点 // 配置织入点
@Pointcut("@annotation(com.ruoyi.common.annotation.DataScope)") @Pointcut("@annotation(com.ruoyi.common.annotation.DataScope)")
public void dataScopePointCut() { public void dataScopePointCut() {
} }
@Before("dataScopePointCut()") @Before("dataScopePointCut()")
public void doBefore(JoinPoint point) throws Throwable { public void doBefore(JoinPoint point) throws Throwable {
handleDataScope(point); handleDataScope(point);
} }
protected void handleDataScope(final JoinPoint joinPoint) { protected void handleDataScope(final JoinPoint joinPoint) {
// 获得注解 // 获得注解
DataScope controllerDataScope = getAnnotationLog(joinPoint); DataScope controllerDataScope = getAnnotationLog(joinPoint);
if (controllerDataScope == null) { if (controllerDataScope == null) {
return; return;
} }
// 获取当前的用户 // 获取当前的用户
LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest()); LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNotNull(loginUser)) { if (StringUtils.isNotNull(loginUser)) {
SysUser currentUser = loginUser.getUser(); SysUser currentUser = loginUser.getUser();
// 如果是超级管理员则不过滤数据 // 如果是超级管理员则不过滤数据
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) { if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) {
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias()); controllerDataScope.userAlias());
} }
} }
} }
/** /**
* 数据范围过滤 * 数据范围过滤
* *
* @param joinPoint 切点 * @param joinPoint 切点
* @param user 用户 * @param user 用户
* @param userAlias 别名 * @param userAlias 别名
*/ */
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) { public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) {
StringBuilder sqlString = new StringBuilder(); StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles()) { for (SysRole role : user.getRoles()) {
String dataScope = role.getDataScope(); String dataScope = role.getDataScope();
if (DATA_SCOPE_ALL.equals(dataScope)) { if (DATA_SCOPE_ALL.equals(dataScope)) {
sqlString = new StringBuilder(); sqlString = new StringBuilder();
break; break;
} else if (DATA_SCOPE_CUSTOM.equals(dataScope)) { } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
sqlString.append(StringUtils.format( sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId())); role.getRoleId()));
} else if (DATA_SCOPE_DEPT.equals(dataScope)) { } else if (DATA_SCOPE_DEPT.equals(dataScope)) {
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
} else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) { } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
sqlString.append(StringUtils.format( sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId())); deptAlias, user.getDeptId(), user.getDeptId()));
} else if (DATA_SCOPE_SELF.equals(dataScope)) { } else if (DATA_SCOPE_SELF.equals(dataScope)) {
if (StringUtils.isNotBlank(userAlias)) { if (StringUtils.isNotBlank(userAlias)) {
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
} else { } else {
// 数据权限为仅本人且没有userAlias别名不查询任何数据 // 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(" OR 1=0 "); sqlString.append(" OR 1=0 ");
} }
} }
} }
if (StringUtils.isNotBlank(sqlString.toString())) { if (StringUtils.isNotBlank(sqlString.toString())) {
Object params = joinPoint.getArgs()[0]; Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params; BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
} }
} }
} }
/** /**
* 是否存在注解如果存在就获取 * 是否存在注解如果存在就获取
*/ */
private DataScope getAnnotationLog(JoinPoint joinPoint) { private DataScope getAnnotationLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature(); Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature; MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod(); Method method = methodSignature.getMethod();
if (method != null) { if (method != null) {
return method.getAnnotation(DataScope.class); return method.getAnnotation(DataScope.class);
} }
return null; return null;
} }
} }

View File

@ -1,64 +1,64 @@
package com.ruoyi.framework.aspectj; package com.ruoyi.framework.aspectj;
import java.util.Objects; import java.util.Objects;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataSource; import com.ruoyi.common.annotation.DataSource;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder; import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder;
/** /**
* 多数据源处理 * 多数据源处理
* *
* @author ruoyi * @author ruoyi
*/ */
@Aspect @Aspect
@Order(1) @Order(1)
@Component @Component
public class DataSourceAspect { public class DataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass()); protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)" @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
+ "|| @within(com.ruoyi.common.annotation.DataSource)") + "|| @within(com.ruoyi.common.annotation.DataSource)")
public void dsPointCut() { public void dsPointCut() {
} }
@Around("dsPointCut()") @Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point); DataSource dataSource = getDataSource(point);
if (StringUtils.isNotNull(dataSource)) { if (StringUtils.isNotNull(dataSource)) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
} }
try { try {
return point.proceed(); return point.proceed();
} finally { } finally {
// 销毁数据源 在执行方法之后 // 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDataSourceType(); DynamicDataSourceContextHolder.clearDataSourceType();
} }
} }
/** /**
* 获取需要切换的数据源 * 获取需要切换的数据源
*/ */
public DataSource getDataSource(ProceedingJoinPoint point) { public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature(); MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (Objects.nonNull(dataSource)) { if (Objects.nonNull(dataSource)) {
return dataSource; return dataSource;
} }
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
} }
} }

View File

@ -1,19 +1,19 @@
package com.ruoyi.common.enums; package com.ruoyi.common.enums;
/** /**
* 数据源 * 数据源
* *
* @author ruoyi * @author ruoyi
*/ */
public enum DataSourceType public enum DataSourceType
{ {
/** /**
* 主库 * 主库
*/ */
MASTER, MASTER,
/** /**
* 从库 * 从库
*/ */
SLAVE SLAVE
} }

View File

@ -1,24 +1,24 @@
package com.ruoyi.framework.datasource; package com.ruoyi.framework.datasource;
import java.util.Map; import java.util.Map;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/** /**
* 动态数据源 * 动态数据源
* *
* @author ruoyi * @author ruoyi
*/ */
public class DynamicDataSource extends AbstractRoutingDataSource { public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource); super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources); super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet(); super.afterPropertiesSet();
} }
@Override @Override
protected Object determineCurrentLookupKey() { protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType(); return DynamicDataSourceContextHolder.getDataSourceType();
} }
} }

View File

@ -1,41 +1,41 @@
package com.ruoyi.framework.datasource; package com.ruoyi.framework.datasource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* 数据源切换处理 * 数据源切换处理
* *
* @author ruoyi * @author ruoyi
*/ */
public class DynamicDataSourceContextHolder { public class DynamicDataSourceContextHolder {
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/** /**
* 使用ThreadLocal维护变量ThreadLocal为每个使用该变量的线程提供独立的变量副本 * 使用ThreadLocal维护变量ThreadLocal为每个使用该变量的线程提供独立的变量副本
* 所以每一个线程都可以独立地改变自己的副本而不会影响其它线程所对应的副本 * 所以每一个线程都可以独立地改变自己的副本而不会影响其它线程所对应的副本
*/ */
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/** /**
* 设置数据源的变量 * 设置数据源的变量
*/ */
public static void setDataSourceType(String dsType) { public static void setDataSourceType(String dsType) {
log.info("切换到{}数据源", dsType); log.info("切换到{}数据源", dsType);
CONTEXT_HOLDER.set(dsType); CONTEXT_HOLDER.set(dsType);
} }
/** /**
* 获得数据源的变量 * 获得数据源的变量
*/ */
public static String getDataSourceType() { public static String getDataSourceType() {
return CONTEXT_HOLDER.get(); return CONTEXT_HOLDER.get();
} }
/** /**
* 清空数据源变量 * 清空数据源变量
*/ */
public static void clearDataSourceType() { public static void clearDataSourceType() {
CONTEXT_HOLDER.remove(); CONTEXT_HOLDER.remove();
} }
} }

View File

@ -1,116 +1,116 @@
package com.ruoyi.framework.config; package com.ruoyi.framework.config;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.servlet.Filter; import javax.servlet.Filter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils; import com.alibaba.druid.util.Utils;
import com.ruoyi.common.enums.DataSourceType; import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.DruidProperties; import com.ruoyi.framework.config.properties.DruidProperties;
import com.ruoyi.framework.datasource.DynamicDataSource; import com.ruoyi.framework.datasource.DynamicDataSource;
/** /**
* druid 配置多数据源 * druid 配置多数据源
* *
* @author ruoyi * @author ruoyi
*/ */
@Configuration @Configuration
public class DruidConfig { public class DruidConfig {
@Bean @Bean
@ConfigurationProperties("spring.datasource.druid.master") @ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties) { public DataSource masterDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource); return druidProperties.dataSource(dataSource);
} }
@Bean @Bean
@ConfigurationProperties("spring.datasource.druid.slave") @ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties) { public DataSource slaveDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource); return druidProperties.dataSource(dataSource);
} }
@Bean(name = "dynamicDataSource") @Bean(name = "dynamicDataSource")
@Primary @Primary
public DynamicDataSource dataSource(DataSource masterDataSource) { public DynamicDataSource dataSource(DataSource masterDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(); Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources); return new DynamicDataSource(masterDataSource, targetDataSources);
} }
/** /**
* 设置数据源 * 设置数据源
* *
* @param targetDataSources 备选数据源集合 * @param targetDataSources 备选数据源集合
* @param sourceName 数据源名称 * @param sourceName 数据源名称
* @param beanName bean名称 * @param beanName bean名称
*/ */
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) { public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
try { try {
DataSource dataSource = SpringUtils.getBean(beanName); DataSource dataSource = SpringUtils.getBean(beanName);
targetDataSources.put(sourceName, dataSource); targetDataSources.put(sourceName, dataSource);
} catch (Exception e) { } catch (Exception e) {
} }
} }
/** /**
* 去除监控页面底部的广告 * 去除监控页面底部的广告
*/ */
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@Bean @Bean
@ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) { public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
// 获取web监控页面的参数 // 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径 // 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
final String filePath = "support/http/resources/js/common.js"; final String filePath = "support/http/resources/js/common.js";
// 创建filter进行过滤 // 创建filter进行过滤
Filter filter = new Filter() { Filter filter = new Filter() {
@Override @Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException { public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
} }
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
chain.doFilter(request, response); chain.doFilter(request, response);
// 重置缓冲区响应头不会被重置 // 重置缓冲区响应头不会被重置
response.resetBuffer(); response.resetBuffer();
// 获取common.js // 获取common.js
String text = Utils.readFromResource(filePath); String text = Utils.readFromResource(filePath);
// 正则替换banner, 除去底部的广告信息 // 正则替换banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", ""); text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", ""); text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text); response.getWriter().write(text);
} }
@Override @Override
public void destroy() { public void destroy() {
} }
}; };
FilterRegistrationBean registrationBean = new FilterRegistrationBean(); FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter); registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern); registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean; return registrationBean;
} }
} }

View File

@ -1,58 +1,58 @@
package com.ruoyi.framework.config; package com.ruoyi.framework.config;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import com.ruoyi.common.filter.RepeatableFilter; import com.ruoyi.common.filter.RepeatableFilter;
import com.ruoyi.common.filter.XssFilter; import com.ruoyi.common.filter.XssFilter;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
/** /**
* Filter配置 * Filter配置
* *
* @author ruoyi * @author ruoyi
*/ */
@Configuration @Configuration
public class FilterConfig { public class FilterConfig {
@Value("${xss.enabled}") @Value("${xss.enabled}")
private String enabled; private String enabled;
@Value("${xss.excludes}") @Value("${xss.excludes}")
private String excludes; private String excludes;
@Value("${xss.urlPatterns}") @Value("${xss.urlPatterns}")
private String urlPatterns; private String urlPatterns;
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@Bean @Bean
public FilterRegistrationBean xssFilterRegistration() { public FilterRegistrationBean xssFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean(); FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST); registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter()); registration.setFilter(new XssFilter());
registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
registration.setName("xssFilter"); registration.setName("xssFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
Map<String, String> initParameters = new HashMap<String, String>(); Map<String, String> initParameters = new HashMap<String, String>();
initParameters.put("excludes", excludes); initParameters.put("excludes", excludes);
initParameters.put("enabled", enabled); initParameters.put("enabled", enabled);
registration.setInitParameters(initParameters); registration.setInitParameters(initParameters);
return registration; return registration;
} }
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@Bean @Bean
public FilterRegistrationBean someFilterRegistration() { public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean(); FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RepeatableFilter()); registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*"); registration.addUrlPatterns("/*");
registration.setName("repeatableFilter"); registration.setName("repeatableFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration; return registration;
} }
} }

View File

@ -1,44 +1,44 @@
package com.ruoyi.framework.config; package com.ruoyi.framework.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.Constants;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
/** /**
* 通用配置 * 通用配置
* *
* @author ruoyi * @author ruoyi
*/ */
@Configuration @Configuration
public class ResourcesConfig implements WebMvcConfigurer { public class ResourcesConfig implements WebMvcConfigurer {
@Autowired @Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor; private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
/** 本地文件上传路径 */ /** 本地文件上传路径 */
registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
/** swagger配置 */ /** swagger配置 */
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
} }
/** /**
* 自定义拦截规则 * 自定义拦截规则
*/ */
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
} }
} }

View File

@ -1,30 +1,30 @@
package com.ruoyi.framework.config; package com.ruoyi.framework.config;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ServletUtils;
/** /**
* 服务相关配置 * 服务相关配置
* *
* @author ruoyi * @author ruoyi
*/ */
@Component @Component
public class ServerConfig { public class ServerConfig {
/** /**
* 获取完整的请求路径包括域名端口上下文访问路径 * 获取完整的请求路径包括域名端口上下文访问路径
* *
* @return 服务地址 * @return 服务地址
*/ */
public String getUrl() { public String getUrl() {
HttpServletRequest request = ServletUtils.getRequest(); HttpServletRequest request = ServletUtils.getRequest();
return getDomain(request); return getDomain(request);
} }
public static String getDomain(HttpServletRequest request) { public static String getDomain(HttpServletRequest request) {
StringBuffer url = request.getRequestURL(); StringBuffer url = request.getRequestURL();
String contextPath = request.getServletContext().getContextPath(); String contextPath = request.getServletContext().getContextPath();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
} }
} }

View File

@ -1,58 +1,58 @@
package com.ruoyi.framework.config; package com.ruoyi.framework.config;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import com.ruoyi.common.utils.Threads; import com.ruoyi.common.utils.Threads;
/** /**
* 线程池配置 * 线程池配置
* *
* @author ruoyi * @author ruoyi
**/ **/
@Configuration @Configuration
public class ThreadPoolConfig { public class ThreadPoolConfig {
// 核心线程池大小 // 核心线程池大小
private int corePoolSize = 50; private int corePoolSize = 50;
// 最大可创建的线程数 // 最大可创建的线程数
private int maxPoolSize = 200; private int maxPoolSize = 200;
// 队列最大长度 // 队列最大长度
private int queueCapacity = 1000; private int queueCapacity = 1000;
// 线程池维护线程所允许的空闲时间 // 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = 300; private int keepAliveSeconds = 300;
@Bean(name = "threadPoolTaskExecutor") @Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() { public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(maxPoolSize); executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize); executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity); executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds); executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略 // 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor; return executor;
} }
/** /**
* 执行周期性或定时任务 * 执行周期性或定时任务
*/ */
@Bean(name = "scheduledExecutorService") @Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() { protected ScheduledExecutorService scheduledExecutorService() {
return new ScheduledThreadPoolExecutor(corePoolSize, return new ScheduledThreadPoolExecutor(corePoolSize,
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build()) { new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build()) {
@Override @Override
protected void afterExecute(Runnable r, Throwable t) { protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t); super.afterExecute(r, t);
Threads.printException(r, t); Threads.printException(r, t);
} }
}; };
} }
} }

View File

@ -1,75 +1,75 @@
package com.ruoyi.framework.config.properties; package com.ruoyi.framework.config.properties;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSource;
/** /**
* druid 配置属性 * druid 配置属性
* *
* @author ruoyi * @author ruoyi
*/ */
@Configuration @Configuration
public class DruidProperties { public class DruidProperties {
@Value("${spring.datasource.druid.initialSize}") @Value("${spring.datasource.druid.initialSize}")
private int initialSize; private int initialSize;
@Value("${spring.datasource.druid.minIdle}") @Value("${spring.datasource.druid.minIdle}")
private int minIdle; private int minIdle;
@Value("${spring.datasource.druid.maxActive}") @Value("${spring.datasource.druid.maxActive}")
private int maxActive; private int maxActive;
@Value("${spring.datasource.druid.maxWait}") @Value("${spring.datasource.druid.maxWait}")
private int maxWait; private int maxWait;
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis; private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis; private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
private int maxEvictableIdleTimeMillis; private int maxEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validationQuery}") @Value("${spring.datasource.druid.validationQuery}")
private String validationQuery; private String validationQuery;
@Value("${spring.datasource.druid.testWhileIdle}") @Value("${spring.datasource.druid.testWhileIdle}")
private boolean testWhileIdle; private boolean testWhileIdle;
@Value("${spring.datasource.druid.testOnBorrow}") @Value("${spring.datasource.druid.testOnBorrow}")
private boolean testOnBorrow; private boolean testOnBorrow;
@Value("${spring.datasource.druid.testOnReturn}") @Value("${spring.datasource.druid.testOnReturn}")
private boolean testOnReturn; private boolean testOnReturn;
public DruidDataSource dataSource(DruidDataSource datasource) { public DruidDataSource dataSource(DruidDataSource datasource) {
/** 配置初始化大小、最小、最大 */ /** 配置初始化大小、最小、最大 */
datasource.setInitialSize(initialSize); datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive); datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle); datasource.setMinIdle(minIdle);
/** 配置获取连接等待超时的时间 */ /** 配置获取连接等待超时的时间 */
datasource.setMaxWait(maxWait); datasource.setMaxWait(maxWait);
/** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
/** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
/** /**
* 用来检测连接是否有效的sql要求是一个查询语句常用select 'x'如果validationQuery为nulltestOnBorrowtestOnReturntestWhileIdle都不会起作用 * 用来检测连接是否有效的sql要求是一个查询语句常用select 'x'如果validationQuery为nulltestOnBorrowtestOnReturntestWhileIdle都不会起作用
*/ */
datasource.setValidationQuery(validationQuery); datasource.setValidationQuery(validationQuery);
/** 建议配置为true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis执行validationQuery检测连接是否有效。 */ /** 建议配置为true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis执行validationQuery检测连接是否有效。 */
datasource.setTestWhileIdle(testWhileIdle); datasource.setTestWhileIdle(testWhileIdle);
/** 申请连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。 */ /** 申请连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。 */
datasource.setTestOnBorrow(testOnBorrow); datasource.setTestOnBorrow(testOnBorrow);
/** 归还连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。 */ /** 归还连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。 */
datasource.setTestOnReturn(testOnReturn); datasource.setTestOnReturn(testOnReturn);
return datasource; return datasource;
} }
} }

View File

@ -1,21 +1,21 @@
package com.ruoyi.common.annotation; package com.ruoyi.common.annotation;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited; import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/** /**
* 自定义注解防止表单重复提交 * 自定义注解防止表单重复提交
* *
* @author ruoyi * @author ruoyi
*/ */
@Inherited @Inherited
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
public @interface RepeatSubmit { public @interface RepeatSubmit {
} }

View File

@ -1,49 +1,49 @@
package com.ruoyi.framework.interceptor; package com.ruoyi.framework.interceptor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.annotation.RepeatSubmit; import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ServletUtils;
/** /**
* 防止重复提交拦截器 * 防止重复提交拦截器
* *
* @author ruoyi * @author ruoyi
*/ */
@Component @Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter { public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) { if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler; HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod(); Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null) { if (annotation != null) {
if (this.isRepeatSubmit(request)) { if (this.isRepeatSubmit(request)) {
AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试"); AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult)); ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
return false; return false;
} }
} }
return true; return true;
} else { } else {
return super.preHandle(request, response, handler); return super.preHandle(request, response, handler);
} }
} }
/** /**
* 验证是否重复提交由子类实现具体的防重复提交的规则 * 验证是否重复提交由子类实现具体的防重复提交的规则
* *
* @param request * @param request
* @return * @return
* @throws Exception * @throws Exception
*/ */
public abstract boolean isRepeatSubmit(HttpServletRequest request); public abstract boolean isRepeatSubmit(HttpServletRequest request);
} }

View File

@ -1,114 +1,114 @@
package com.ruoyi.framework.interceptor.impl; package com.ruoyi.framework.interceptor.impl;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.filter.RepeatedlyRequestWrapper; import com.ruoyi.common.filter.RepeatedlyRequestWrapper;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpHelper; import com.ruoyi.common.utils.http.HttpHelper;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
/** /**
* 判断请求url和数据是否和上一次相同 * 判断请求url和数据是否和上一次相同
* 如果和上次相同则是重复提交表单 有效时间为10秒内 * 如果和上次相同则是重复提交表单 有效时间为10秒内
* *
* @author ruoyi * @author ruoyi
*/ */
@Component @Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_PARAMS = "repeatParams";
public final String REPEAT_TIME = "repeatTime"; public final String REPEAT_TIME = "repeatTime";
// 令牌自定义标识 // 令牌自定义标识
@Value("${token.header}") @Value("${token.header}")
private String header; private String header;
@Autowired @Autowired
private RedisCache redisCache; private RedisCache redisCache;
/** /**
* 间隔时间单位: 默认10秒 * 间隔时间单位: 默认10秒
* <p> * <p>
* 两次相同参数的请求如果间隔时间大于该参数系统不会认定为重复提交的数据 * 两次相同参数的请求如果间隔时间大于该参数系统不会认定为重复提交的数据
*/ */
private int intervalTime = 10; private int intervalTime = 10;
public void setIntervalTime(int intervalTime) { public void setIntervalTime(int intervalTime) {
this.intervalTime = intervalTime; this.intervalTime = intervalTime;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public boolean isRepeatSubmit(HttpServletRequest request) { public boolean isRepeatSubmit(HttpServletRequest request) {
String nowParams = ""; String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper) { if (request instanceof RepeatedlyRequestWrapper) {
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest); nowParams = HttpHelper.getBodyString(repeatedlyRequest);
} }
// body参数为空获取Parameter的数据 // body参数为空获取Parameter的数据
if (StringUtils.isEmpty(nowParams)) { if (StringUtils.isEmpty(nowParams)) {
nowParams = JSONObject.toJSONString(request.getParameterMap()); nowParams = JSONObject.toJSONString(request.getParameterMap());
} }
Map<String, Object> nowDataMap = new HashMap<String, Object>(); Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址作为存放cache的key值 // 请求地址作为存放cache的key值
String url = request.getRequestURI(); String url = request.getRequestURI();
// 唯一值没有消息头则使用请求地址 // 唯一值没有消息头则使用请求地址
String submitKey = request.getHeader(header); String submitKey = request.getHeader(header);
if (StringUtils.isEmpty(submitKey)) { if (StringUtils.isEmpty(submitKey)) {
submitKey = url; submitKey = url;
} }
// 唯一标识指定key + 消息头 // 唯一标识指定key + 消息头
String cache_repeat_key = Constants.REPEAT_SUBMIT_KEY + submitKey; String cache_repeat_key = Constants.REPEAT_SUBMIT_KEY + submitKey;
Object sessionObj = redisCache.getCacheObject(cache_repeat_key); Object sessionObj = redisCache.getCacheObject(cache_repeat_key);
if (sessionObj != null) { if (sessionObj != null) {
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj; Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url)) { if (sessionMap.containsKey(url)) {
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url); Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) { if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) {
return true; return true;
} }
} }
} }
Map<String, Object> cacheMap = new HashMap<String, Object>(); Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap); cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cache_repeat_key, cacheMap, intervalTime, TimeUnit.SECONDS); redisCache.setCacheObject(cache_repeat_key, cacheMap, intervalTime, TimeUnit.SECONDS);
return false; return false;
} }
/** /**
* 判断参数是否相同 * 判断参数是否相同
*/ */
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) { private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
String nowParams = (String) nowMap.get(REPEAT_PARAMS); String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams); return nowParams.equals(preParams);
} }
/** /**
* 判断两次间隔时间 * 判断两次间隔时间
*/ */
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap) { private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap) {
long time1 = (Long) nowMap.get(REPEAT_TIME); long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < (this.intervalTime * 1000)) { if ((time1 - time2) < (this.intervalTime * 1000)) {
return true; return true;
} }
return false; return false;
} }
} }

View File

@ -1,57 +1,57 @@
# 数据源配置 # 数据源配置
spring: spring:
datasource: datasource:
type: com.alibaba.druid.pool.DruidDataSource type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver driverClassName: com.mysql.cj.jdbc.Driver
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root username: root
password: password password: password
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭
enabled: false enabled: false
url: url:
username: username:
password: password:
# 初始连接数 # 初始连接数
initialSize: 5 initialSize: 5
# 最小连接池数量 # 最小连接池数量
minIdle: 10 minIdle: 10
# 最大连接池数量 # 最大连接池数量
maxActive: 20 maxActive: 20
# 配置获取连接等待超时的时间 # 配置获取连接等待超时的时间
maxWait: 60000 maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000 timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒 # 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000 minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒 # 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000 maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效 # 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true testWhileIdle: true
testOnBorrow: false testOnBorrow: false
testOnReturn: false testOnReturn: false
webStatFilter: webStatFilter:
enabled: true enabled: true
statViewServlet: statViewServlet:
enabled: true enabled: true
# 设置白名单,不填则允许所有访问 # 设置白名单,不填则允许所有访问
allow: allow:
url-pattern: /druid/* url-pattern: /druid/*
# 控制台管理用户名和密码 # 控制台管理用户名和密码
login-username: login-username:
login-password: login-password:
filter: filter:
stat: stat:
enabled: true enabled: true
# 慢SQL记录 # 慢SQL记录
log-slow-sql: true log-slow-sql: true
slow-sql-millis: 1000 slow-sql-millis: 1000
merge-sql: true merge-sql: true
wall: wall:
config: config:
multi-statement-allow: true multi-statement-allow: true

View File

@ -1,44 +1,44 @@
# 项目相关配置 # 项目相关配置
ruoyi: ruoyi:
# 名称 # 名称
name: RuoYi name: RuoYi
# 版本 # 版本
version: 3.3.0 version: 3.3.0
# 版权年份 # 版权年份
copyrightYear: 2020 copyrightYear: 2020
# 实例演示开关 # 实例演示开关
demoEnabled: true demoEnabled: true
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath # 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: D:/ruoyi/uploadPath profile: D:/ruoyi/uploadPath
# 获取ip地址开关 # 获取ip地址开关
addressEnabled: false addressEnabled: false
# 开发环境配置 # 开发环境配置
server: server:
# 服务器的HTTP端口默认为8080 # 服务器的HTTP端口默认为8080
port: 8080 port: 8080
servlet: servlet:
# 应用的访问路径 # 应用的访问路径
context-path: / context-path: /
tomcat: tomcat:
# tomcat的URI编码 # tomcat的URI编码
uri-encoding: UTF-8 uri-encoding: UTF-8
# tomcat最大线程数默认为200 # tomcat最大线程数默认为200
max-threads: 800 max-threads: 800
# Tomcat启动初始化的线程数默认值25 # Tomcat启动初始化的线程数默认值25
min-spare-threads: 30 min-spare-threads: 30
# 日志配置 # 日志配置
logging: logging:
level: level:
com.ruoyi: debug com.ruoyi: debug
org.springframework: warn org.springframework: warn
# 防止XSS攻击 # 防止XSS攻击
xss: xss:
# 过滤开关 # 过滤开关
enabled: true enabled: true
# 排除链接(多个用逗号分隔) # 排除链接(多个用逗号分隔)
excludes: /system/notice/* excludes: /system/notice/*
# 匹配链接 # 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/* urlPatterns: /system/*,/monitor/*,/tool/*

View File

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

View File

@ -1,93 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<!-- 日志存放路径 --> <!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" /> <property name="log.path" value="/home/ruoyi/logs" />
<!-- 日志输出格式 --> <!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" /> <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 --> <!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>${log.pattern}</pattern> <pattern>${log.pattern}</pattern>
</encoder> </encoder>
</appender> </appender>
<!-- 系统日志输出 --> <!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file> <file>${log.path}/sys-info.log</file>
<!-- 循环政策:基于时间创建日志文件 --> <!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 --> <!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory> <maxHistory>60</maxHistory>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>
<pattern>${log.pattern}</pattern> <pattern>${log.pattern}</pattern>
</encoder> </encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"> <filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 --> <!-- 过滤的级别 -->
<level>INFO</level> <level>INFO</level>
<!-- 匹配时的操作:接收(记录) --> <!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch> <onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) --> <!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch> <onMismatch>DENY</onMismatch>
</filter> </filter>
</appender> </appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file> <file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 --> <!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 --> <!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory> <maxHistory>60</maxHistory>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>
<pattern>${log.pattern}</pattern> <pattern>${log.pattern}</pattern>
</encoder> </encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"> <filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 --> <!-- 过滤的级别 -->
<level>ERROR</level> <level>ERROR</level>
<!-- 匹配时的操作:接收(记录) --> <!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch> <onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) --> <!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch> <onMismatch>DENY</onMismatch>
</filter> </filter>
</appender> </appender>
<!-- 用户访问日志输出 --> <!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file> <file>${log.path}/sys-user.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily --> <!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory> <maxHistory>60</maxHistory>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>
<pattern>${log.pattern}</pattern> <pattern>${log.pattern}</pattern>
</encoder> </encoder>
</appender> </appender>
<!-- 系统模块日志级别控制 --> <!-- 系统模块日志级别控制 -->
<logger name="com.ruoyi" level="info" /> <logger name="com.ruoyi" level="info" />
<!-- Spring日志级别控制 --> <!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" /> <logger name="org.springframework" level="warn" />
<root level="info"> <root level="info">
<appender-ref ref="console" /> <appender-ref ref="console" />
</root> </root>
<!--系统操作日志--> <!--系统操作日志-->
<root level="info"> <root level="info">
<appender-ref ref="file_info" /> <appender-ref ref="file_info" />
<appender-ref ref="file_error" /> <appender-ref ref="file_error" />
</root> </root>
<!--系统用户操作日志--> <!--系统用户操作日志-->
<logger name="sys-user" level="info"> <logger name="sys-user" level="info">
<appender-ref ref="sys-user"/> <appender-ref ref="sys-user"/>
</logger> </logger>
</configuration> </configuration>

View File

@ -1,18 +1,18 @@
import request from '@/utils/request' import request from '@/utils/request'
// 查询在线用户列表 // 查询在线用户列表
export function list(query) { export function list(query) {
return request({ return request({
url: '/monitor/online/list', url: '/system/user-session/page',
method: 'get', method: 'get',
params: query params: query
}) })
} }
// 强退用户 // 强退用户
export function forceLogout(tokenId) { export function forceLogout(tokenId) {
return request({ return request({
url: '/monitor/online/' + tokenId, url: '/system/user-session/delete?id=' + tokenId,
method: 'delete' method: 'delete'
}) })
} }

View File

@ -1,128 +1,121 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px"> <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
<el-form-item label="登录地址" prop="ipaddr"> <el-form-item label="登录地址" prop="userIp">
<el-input <el-input
v-model="queryParams.ipaddr" v-model="queryParams.userIp"
placeholder="请输入登录地址" placeholder="请输入登录地址"
clearable clearable
size="small" size="small"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="用户名称" prop="userName"> <el-form-item label="用户名称" prop="username">
<el-input <el-input
v-model="queryParams.userName" v-model="queryParams.username"
placeholder="请输入用户名称" placeholder="请输入用户名称"
clearable clearable
size="small" size="small"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="list.slice((pageNum-1)*pageSize,pageNum*pageSize)" :data="list"
style="width: 100%;" style="width: 100%;"
> >
<el-table-column label="序号" type="index" align="center"> <el-table-column label="会话编号" align="center" prop="id" width="300" />
<template slot-scope="scope"> <el-table-column label="登录名称" align="center" prop="username" width="100" />
<span>{{(pageNum - 1) * pageSize + scope.$index + 1}}</span> <el-table-column label="部门名称" align="center" prop="deptName" width="100" />
</template> <el-table-column label="登陆地址" align="center" prop="userIp" width="100" />
</el-table-column> <el-table-column label="userAgent" align="center" prop="userAgent" :show-overflow-tooltip="true" />
<el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" /> <el-table-column label="登录时间" align="center" prop="createTime" width="180">
<el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" /> <template slot-scope="scope">
<el-table-column label="部门名称" align="center" prop="deptName" /> <span>{{ parseTime(scope.row.createTime) }}</span>
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" /> </template>
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" /> </el-table-column>
<el-table-column label="浏览器" align="center" prop="browser" /> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作系统" align="center" prop="os" /> <template slot-scope="scope">
<el-table-column label="登录时间" align="center" prop="loginTime" width="180"> <el-button
<template slot-scope="scope"> size="mini"
<span>{{ parseTime(scope.row.loginTime) }}</span> type="text"
</template> icon="el-icon-delete"
</el-table-column> @click="handleForceLogout(scope.row)"
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> v-hasPermi="['system:user-session:delete']"
<template slot-scope="scope"> >强退</el-button>
<el-button </template>
size="mini" </el-table-column>
type="text" </el-table>
icon="el-icon-delete"
@click="handleForceLogout(scope.row)" <pagination v-show="total>0" :total="total" :page.sync="pageNo" :limit.sync="pageSize" />
v-hasPermi="['monitor:online:forceLogout']" </div>
>强退</el-button> </template>
</template>
</el-table-column> <script>
</el-table> import { list, forceLogout } from "@/api/system/session";
<pagination v-show="total>0" :total="total" :page.sync="pageNum" :limit.sync="pageSize" /> export default {
</div> name: "Online",
</template> data() {
return {
<script> //
import { list, forceLogout } from "@/api/monitor/online"; loading: true,
//
export default { total: 0,
name: "Online", //
data() { list: [],
return { //
// queryParams: {
loading: true, pageNo: 1,
// pageSize: 10,
total: 0, userIp: undefined,
// username: undefined
list: [], }
pageNum: 1, };
pageSize: 10, },
// created() {
queryParams: { this.getList();
ipaddr: undefined, },
userName: undefined methods: {
} /** 查询登录日志列表 */
}; getList() {
}, this.loading = true;
created() { list(this.queryParams).then(response => {
this.getList(); this.list = response.data.list;
}, this.total = response.data.total;
methods: { this.loading = false;
/** 查询登录日志列表 */ });
getList() { },
this.loading = true; /** 搜索按钮操作 */
list(this.queryParams).then(response => { handleQuery() {
this.list = response.rows; this.pageNo = 1;
this.total = response.total; this.getList();
this.loading = false; },
}); /** 重置按钮操作 */
}, resetQuery() {
/** 搜索按钮操作 */ this.resetForm("queryForm");
handleQuery() { this.handleQuery();
this.pageNum = 1; },
this.getList(); /** 强退按钮操作 */
}, handleForceLogout(row) {
/** 重置按钮操作 */ this.$confirm('是否确认强退名称为"' + row.username + '"的数据项?', "警告", {
resetQuery() { confirmButtonText: "确定",
this.resetForm("queryForm"); cancelButtonText: "取消",
this.handleQuery(); type: "warning"
}, }).then(function() {
/** 强退按钮操作 */ return forceLogout(row.id);
handleForceLogout(row) { }).then(() => {
this.$confirm('是否确认强退名称为"' + row.userName + '"的数据项?', "警告", { this.getList();
confirmButtonText: "确定", this.msgSuccess("强退成功");
cancelButtonText: "取消", })
type: "warning" }
}).then(function() { }
return forceLogout(row.tokenId); };
}).then(() => { </script>
this.getList();
this.msgSuccess("强退成功");
})
}
}
};
</script>

View File

@ -4,8 +4,14 @@ import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult; import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageItemRespVO; import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageItemRespVO;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO; import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.convert.auth.SysUserSessionConvert;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dept.SysDeptDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService; import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import cn.iocoder.dashboard.modules.system.service.dept.SysDeptService;
import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
import cn.iocoder.dashboard.util.collection.MapUtils;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
@ -14,26 +20,50 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success; import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.util.collection.CollectionUtils.convertList;
@Api("用户 Session API") @Api("用户 Session API")
@RestController @RestController
@RequestMapping("/user-session") @RequestMapping("/system/user-session")
public class SysUserSessionController { public class SysUserSessionController {
@Resource @Resource
private SysUserSessionService userSessionService; private SysUserSessionService userSessionService;
@Resource
private SysUserService userService;
@Resource
private SysDeptService deptService;
@ApiOperation("获得 Session 分页列表") @ApiOperation("获得 Session 分页列表")
@PreAuthorize("@ss.hasPermission('system:user-session:page')") @PreAuthorize("@ss.hasPermission('system:user-session:page')")
@GetMapping("/page") @GetMapping("/page")
public CommonResult<PageResult<SysUserSessionPageItemRespVO>> getUserSessionPage(@Validated SysUserSessionPageReqVO reqVO) { public CommonResult<PageResult<SysUserSessionPageItemRespVO>> getUserSessionPage(@Validated SysUserSessionPageReqVO reqVO) {
// 获得 Session 分页 // 获得 Session 分页
PageResult<SysUserSessionDO> sessionPage = userSessionService.getUserSessionPage(reqVO); PageResult<SysUserSessionDO> pageResult = userSessionService.getUserSessionPage(reqVO);
// // 获得拼接需要的数据
return null; Map<Long, SysUserDO> userMap = userService.getUserMap(
convertList(pageResult.getList(), SysUserSessionDO::getUserId));
Map<Long, SysDeptDO> deptMap = deptService.getDeptMap(
convertList(userMap.values(), SysUserDO::getDeptId));
// 拼接结果返回
List<SysUserSessionPageItemRespVO> sessionList = new ArrayList<>(pageResult.getList().size());
pageResult.getList().forEach(session -> {
SysUserSessionPageItemRespVO respVO = SysUserSessionConvert.INSTANCE.convert(session);
sessionList.add(respVO);
// 设置用户账号
MapUtils.findAndThen(userMap, session.getUserId(), user -> {
respVO.setUsername(user.getUsername());
// 设置用户部门
MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> respVO.setDeptName(dept.getName()));
});
});
return success(new PageResult<>(sessionList, pageResult.getTotal()));
} }
@ApiOperation("删除 Session") @ApiOperation("删除 Session")

View File

@ -8,6 +8,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.util.Date;
@ApiModel(value = "用户在线 Session Response VO", description = "相比用户基本信息来说,会多部门、用户账号等信息") @ApiModel(value = "用户在线 Session Response VO", description = "相比用户基本信息来说,会多部门、用户账号等信息")
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@ -25,7 +27,7 @@ public class SysUserSessionPageItemRespVO extends PageParam {
private String userAgent; private String userAgent;
@ApiModelProperty(value = "登陆时间", required = true) @ApiModelProperty(value = "登陆时间", required = true)
private String createTime; private Date createTime;
@ApiModelProperty(value = "用户账号", required = true, example = "yudao") @ApiModelProperty(value = "用户账号", required = true, example = "yudao")
private String username; private String username;

View File

@ -14,7 +14,6 @@ import javax.validation.constraints.NotEmpty;
public class SysUserSessionPageReqVO extends PageParam { public class SysUserSessionPageReqVO extends PageParam {
@ApiModelProperty(value = "用户 IP", example = "127.0.0.1", notes = "模糊匹配") @ApiModelProperty(value = "用户 IP", example = "127.0.0.1", notes = "模糊匹配")
@NotEmpty(message = "用户 IP 不能为空")
private String userIp; private String userIp;
@ApiModelProperty(value = "用户账号", example = "yudao", notes = "模糊匹配") @ApiModelProperty(value = "用户账号", example = "yudao", notes = "模糊匹配")

View File

@ -0,0 +1,16 @@
package cn.iocoder.dashboard.modules.system.convert.auth;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageItemRespVO;
import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserPageItemRespVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface SysUserSessionConvert {
SysUserSessionConvert INSTANCE = Mappers.getMapper(SysUserSessionConvert.class);
SysUserSessionPageItemRespVO convert(SysUserSessionDO session);
}

View File

@ -1,9 +1,21 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dao.auth; package cn.iocoder.dashboard.modules.system.dal.mysql.dao.auth;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
@Mapper @Mapper
public interface SysUserSessionMapper extends BaseMapper<SysUserSessionDO> { public interface SysUserSessionMapper extends BaseMapperX<SysUserSessionDO> {
default PageResult<SysUserSessionDO> selectPage(SysUserSessionPageReqVO reqVO, Collection<Long> userIds) {
return selectPage(reqVO, new QueryWrapperX<SysUserSessionDO>()
.inIfPresent("user_id", userIds)
.likeIfPresent("user_ip", reqVO.getUserIp()));
}
} }

View File

@ -48,5 +48,9 @@ public interface SysUserMapper extends BaseMapperX<SysUserDO> {
return selectList(new QueryWrapperX<SysUserDO>().like("nickname", nickname)); return selectList(new QueryWrapperX<SysUserDO>().like("nickname", nickname));
} }
default List<SysUserDO> selectListByUsername(String username) {
return selectList(new QueryWrapperX<SysUserDO>().like("username", username));
}
} }

View File

@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.security.core.LoginUser; import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder; import lombok.Builder;
@ -27,7 +28,7 @@ public class SysUserSessionDO extends BaseDO {
/** /**
* 会话编号, sessionId * 会话编号, sessionId
*/ */
@TableId @TableId(type = IdType.INPUT)
private String id; private String id;
/** /**
* 用户编号 * 用户编号

View File

@ -30,8 +30,8 @@ public class SysLoginUserRedisDAO {
stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(loginUser), LOGIN_USER.getTimeout()); stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(loginUser), LOGIN_USER.getTimeout());
} }
public void delete(String accessToken) { public void delete(String sessionId) {
String redisKey = formatKey(accessToken); String redisKey = formatKey(sessionId);
stringRedisTemplate.delete(redisKey); stringRedisTemplate.delete(redisKey);
} }

View File

@ -0,0 +1 @@
package cn.iocoder.dashboard.modules.system.job;

View File

@ -1,19 +1,26 @@
package cn.iocoder.dashboard.modules.system.service.auth.impl; package cn.iocoder.dashboard.modules.system.service.auth.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.pojo.PageResult; import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties; import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.framework.security.core.LoginUser; import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO; import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.auth.SysUserSessionMapper; import cn.iocoder.dashboard.modules.system.dal.mysql.dao.auth.SysUserSessionMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.dal.redis.dao.auth.SysLoginUserRedisDAO; import cn.iocoder.dashboard.modules.system.dal.redis.dao.auth.SysLoginUserRedisDAO;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService; import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Collection;
import java.util.Date; import java.util.Date;
import static cn.iocoder.dashboard.util.collection.CollectionUtils.convertSet;
/** /**
* 在线用户 Session Service 实现类 * 在线用户 Session Service 实现类
* *
@ -27,10 +34,12 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
@Resource @Resource
private SysLoginUserRedisDAO loginUserRedisDAO; private SysLoginUserRedisDAO loginUserRedisDAO;
@Resource @Resource
private SysUserSessionMapper userSessionMapper; private SysUserSessionMapper userSessionMapper;
@Resource
private SysUserService userService;
@Override @Override
public String createUserSession(LoginUser loginUser, String userIp, String userAgent) { public String createUserSession(LoginUser loginUser, String userIp, String userAgent) {
// 生成 Session 编号 // 生成 Session 编号
@ -39,8 +48,8 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
loginUser.setUpdateTime(new Date()); loginUser.setUpdateTime(new Date());
loginUserRedisDAO.set(sessionId, loginUser); loginUserRedisDAO.set(sessionId, loginUser);
// 写入 DB // 写入 DB
SysUserSessionDO userSession = SysUserSessionDO.builder().userId(loginUser.getId()) SysUserSessionDO userSession = SysUserSessionDO.builder().id(sessionId)
.userIp(userIp).userAgent(userAgent).build(); .userId(loginUser.getId()).userIp(userIp).userAgent(userAgent).build();
userSessionMapper.insert(userSession); userSessionMapper.insert(userSession);
// 返回 Session 编号 // 返回 Session 编号
return sessionId; return sessionId;
@ -59,7 +68,10 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
@Override @Override
public void deleteUserSession(String sessionId) { public void deleteUserSession(String sessionId) {
// 删除 Redis 缓存
loginUserRedisDAO.delete(sessionId);
// 删除 DB 记录
userSessionMapper.deleteById(sessionId);
} }
@Override @Override
@ -74,7 +86,15 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
@Override @Override
public PageResult<SysUserSessionDO> getUserSessionPage(SysUserSessionPageReqVO reqVO) { public PageResult<SysUserSessionDO> getUserSessionPage(SysUserSessionPageReqVO reqVO) {
return null; // 处理基于用户昵称的查询
Collection<Long> userIds = null;
if (StrUtil.isNotEmpty(reqVO.getUsername())) {
userIds = convertSet(userService.listUsersByUsername(reqVO.getUsername()), SysUserDO::getId);
if (CollUtil.isEmpty(userIds)) {
return PageResult.empty();
}
}
return userSessionMapper.selectPage(reqVO, userIds);
} }
/** /**

View File

@ -79,6 +79,14 @@ public interface SysUserService {
*/ */
List<SysUserDO> listUsersByNickname(String nickname); List<SysUserDO> listUsersByNickname(String nickname);
/**
* 获得用户列表基于用户账号模糊匹配
*
* @param username 用户账号
* @return 用户列表
*/
List<SysUserDO> listUsersByUsername(String username);
/** /**
* 创建用户 * 创建用户
* *

View File

@ -92,6 +92,11 @@ public class SysUserServiceImpl implements SysUserService {
return userMapper.selectListByNickname(nickname); return userMapper.selectListByNickname(nickname);
} }
@Override
public List<SysUserDO> listUsersByUsername(String username) {
return userMapper.selectListByUsername(username);
}
/** /**
* 获得部门条件查询指定部门的子部门编号们包括自身 * 获得部门条件查询指定部门的子部门编号们包括自身
* *

View File

@ -26,35 +26,35 @@ public class CollectionUtils {
return from.stream().filter(predicate).collect(Collectors.toList()); return from.stream().filter(predicate).collect(Collectors.toList());
} }
public static <T, U> List<U> convertList(List<T> from, Function<T, U> func) { public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new ArrayList<>(); return new ArrayList<>();
} }
return from.stream().map(func).collect(Collectors.toList()); return from.stream().map(func).collect(Collectors.toList());
} }
public static <T, U> Set<U> convertSet(List<T> from, Function<T, U> func) { public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new HashSet<>(); return new HashSet<>();
} }
return from.stream().map(func).collect(Collectors.toSet()); return from.stream().map(func).collect(Collectors.toSet());
} }
public static <T, K> Map<K, T> convertMap(List<T> from, Function<T, K> keyFunc) { public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new HashMap<>(); return new HashMap<>();
} }
return from.stream().collect(Collectors.toMap(keyFunc, item -> item)); return from.stream().collect(Collectors.toMap(keyFunc, item -> item));
} }
public static <T, K, V> Map<K, V> convertMap(List<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new HashMap<>(); return new HashMap<>();
} }
return from.stream().collect(Collectors.toMap(keyFunc, valueFunc)); return from.stream().collect(Collectors.toMap(keyFunc, valueFunc));
} }
public static <T, K> Map<K, List<T>> convertMultiMap(List<T> from, Function<T, K> keyFunc) { public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new HashMap<>(); return new HashMap<>();
} }
@ -62,7 +62,7 @@ public class CollectionUtils {
Collectors.mapping(t -> t, Collectors.toList()))); Collectors.mapping(t -> t, Collectors.toList())));
} }
public static <T, K, V> Map<K, List<V>> convertMultiMap(List<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new HashMap<>(); return new HashMap<>();
} }
@ -71,7 +71,7 @@ public class CollectionUtils {
} }
// 暂时没想好名字先以 2 结尾噶 // 暂时没想好名字先以 2 结尾噶
public static <T, K, V> Map<K, Set<V>> convertMultiMap2(List<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new HashMap<>(); return new HashMap<>();
} }