Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into sms_temp_zzf_0127
Conflicts: pom.xml src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyDefine.java src/main/java/cn/iocoder/dashboard/framework/redis/core/RedisKeyRegistry.java src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java src/main/java/cn/iocoder/dashboard/modules/infra/controller/redis/RedisController.java src/main/java/cn/iocoder/dashboard/modules/system/dal/redis/RedisKeyConstants.java src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
package cn.iocoder.dashboard;
|
||||
|
||||
import de.codecentric.boot.admin.server.config.EnableAdminServer;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAdminServer
|
||||
public class DashboardApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
@@ -1,27 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通用状态枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum OldCommonStatusEnum {
|
||||
|
||||
ENABLE("0", "开启"),
|
||||
DISABLE("1", "关闭");
|
||||
|
||||
/**
|
||||
* 状态值
|
||||
*/
|
||||
private final String status;
|
||||
/**
|
||||
* 状态名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.dashboard.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 全局用户类型枚举
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum UserTypeEnum {
|
||||
|
||||
MEMBER(1, "会员"), // 面向 c 端,普通用户
|
||||
ADMIN(2, "管理员"); // 面向 b 端,管理后台
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer value;
|
||||
/**
|
||||
* 类型名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
}
|
@@ -23,11 +23,16 @@ public interface GlobalErrorCodeConstants {
|
||||
ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
|
||||
ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到");
|
||||
ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
|
||||
ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
|
||||
ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
|
||||
|
||||
// ========== 服务端错误段 ==========
|
||||
|
||||
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
|
||||
|
||||
// ========== 自定义错误段 ==========
|
||||
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
||||
|
||||
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
|
||||
|
||||
static boolean isMatch(Integer code) {
|
||||
|
@@ -2,6 +2,7 @@ package cn.iocoder.dashboard.common.exception.util;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.ErrorCode;
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -91,7 +92,8 @@ public class ServiceExceptionUtil {
|
||||
* @param params 参数
|
||||
* @return 格式化后的提示
|
||||
*/
|
||||
private static String doFormat(int code, String messagePattern, Object... params) {
|
||||
@VisibleForTesting
|
||||
public static String doFormat(int code, String messagePattern, Object... params) {
|
||||
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50);
|
||||
int i = 0;
|
||||
int j;
|
||||
|
@@ -13,14 +13,17 @@ import java.io.Serializable;
|
||||
@Data
|
||||
public class PageParam implements Serializable {
|
||||
|
||||
private static final Integer PAGE_NO = 1;
|
||||
private static final Integer PAGE_SIZE = 10;
|
||||
|
||||
@ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1")
|
||||
@NotNull(message = "页码不能为空")
|
||||
@Min(value = 1, message = "页码最小值为 1")
|
||||
private Integer pageNo;
|
||||
private Integer pageNo = PAGE_NO;
|
||||
|
||||
@ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10")
|
||||
@NotNull(message = "每页条数不能为空")
|
||||
@Range(min = 1, max = 100, message = "条数范围为 [1, 100]")
|
||||
private Integer pageSize;
|
||||
private Integer pageSize = PAGE_SIZE;
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.dashboard.framework.apollo.internals;
|
||||
|
||||
import cn.iocoder.dashboard.modules.infra.dal.mysql.dataobject.config.InfConfigDO;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
@@ -3,8 +3,8 @@ package cn.iocoder.dashboard.framework.apollo.internals;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.dashboard.framework.apollo.core.ConfigConsts;
|
||||
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.mysql.dao.config.InfConfigDAOImpl;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.mysql.dataobject.config.InfConfigDO;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.mysql.config.InfConfigDAOImpl;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
|
||||
import com.ctrip.framework.apollo.Apollo;
|
||||
import com.ctrip.framework.apollo.build.ApolloInjector;
|
||||
import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
|
||||
@@ -122,7 +122,7 @@ public class DBConfigRepository extends AbstractConfigRepository {
|
||||
|
||||
private Properties buildProperties(List<InfConfigDO> configs) {
|
||||
Properties properties = propertiesFactory.getPropertiesInstance();
|
||||
configs.stream().filter(config -> 0 == config.getDeleted()) // 过滤掉被删除的配置
|
||||
configs.stream().filter(BaseDO::getDeleted) // 过滤掉被删除的配置
|
||||
.forEach(config -> properties.put(config.getKey(), config.getValue()));
|
||||
return properties;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 配置中心客户端,基于 Apollo Client 进行简化
|
||||
*
|
||||
* 差别在于,我们使用 {@link cn.iocoder.dashboard.modules.infra.dal.mysql.dataobject.config.InfConfigDO} 表作为配置源。
|
||||
* 差别在于,我们使用 {@link cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO} 表作为配置源。
|
||||
* 当然,功能肯定也会相对少些,满足最小化诉求。
|
||||
*
|
||||
* 1. 项目初始化时,可以使用 SysConfigDO 表的配置
|
||||
|
@@ -1 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Async-Job/?dashboard>
|
||||
<http://www.iocoder.cn/Spring-Boot/Async-Job/?yudao>
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.dashboard.framework.codegen.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(CodegenProperties.class)
|
||||
public class CodegenConfiguration {
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.dashboard.framework.codegen.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Collection;
|
||||
|
||||
@ConfigurationProperties(prefix = "yudao.codegen")
|
||||
@Validated
|
||||
@Data
|
||||
public class CodegenProperties {
|
||||
|
||||
/**
|
||||
* 生成的 Java 代码的基础包
|
||||
*/
|
||||
@NotNull(message = "Java 代码的基础包不能为空")
|
||||
private String basePackage;
|
||||
|
||||
/**
|
||||
* 数据库名数组
|
||||
*/
|
||||
@NotEmpty(message = "数据库不能为空")
|
||||
private Collection<String> dbSchemas;
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 代码生成器
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.codegen;
|
@@ -1,4 +1,4 @@
|
||||
package cn.iocoder.dashboard.framework.datasource;
|
||||
package cn.iocoder.dashboard.framework.datasource.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/dynamic-datasource/?yudao>
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao>
|
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.dashboard.framework.dict.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dict.SysDictDataDO;
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package cn.iocoder.dashboard.framework.dict.core.util;
|
||||
|
||||
import cn.iocoder.dashboard.framework.dict.core.service.DictDataFrameworkService;
|
||||
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dict.SysDictDataDO;
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 字典数据模块
|
||||
* 字典数据模块,提供 {@link cn.iocoder.dashboard.framework.dict.core.util.DictUtils} 工具类
|
||||
*
|
||||
* 通过将字典缓存在内存中,保证性能
|
||||
*/
|
||||
|
@@ -4,7 +4,7 @@ package cn.iocoder.dashboard.framework.excel.core.convert;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.iocoder.dashboard.framework.dict.core.util.DictUtils;
|
||||
import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dict.SysDictDataDO;
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
|
||||
import cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum;
|
||||
import com.alibaba.excel.converters.Converter;
|
||||
import com.alibaba.excel.enums.CellDataTypeEnum;
|
||||
@@ -44,14 +44,19 @@ public class DictConvert implements Converter<Object> {
|
||||
|
||||
@Override
|
||||
public CellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 空时,返回空
|
||||
if (object == null) {
|
||||
return new CellData<>("");
|
||||
}
|
||||
|
||||
// 使用字典格式化
|
||||
SysDictTypeEnum type = getType(contentProperty);
|
||||
String value = String.valueOf(object);
|
||||
SysDictDataDO dictData = DictUtils.getDictDataFromCache(type.getValue(), value);
|
||||
if (dictData == null) {
|
||||
log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
|
||||
return null;
|
||||
return new CellData<>("");
|
||||
}
|
||||
// 生成 Excel 小表格
|
||||
return new CellData<>(dictData.getLabel());
|
||||
|
@@ -1 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.excel.core;
|
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.dashboard.framework.file.config;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.controller.common.SysFileController;
|
||||
import cn.iocoder.dashboard.modules.infra.controller.file.InfFileController;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -13,7 +13,7 @@ import javax.validation.constraints.NotNull;
|
||||
public class FileProperties {
|
||||
|
||||
/**
|
||||
* 对应 {@link SysFileController#}
|
||||
* 对应 {@link InfFileController#}
|
||||
*/
|
||||
@NotNull(message = "基础文件路径不能为空")
|
||||
private String basePath;
|
||||
|
@@ -0,0 +1,42 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfigureAfter(RedisAutoConfiguration.class)
|
||||
public class IdempotentConfiguration {
|
||||
|
||||
@Bean
|
||||
public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
|
||||
return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
|
||||
return new IdempotentRedisDAO(stringRedisTemplate);
|
||||
}
|
||||
|
||||
// ========== 各种 IdempotentKeyResolver Bean ==========
|
||||
|
||||
@Bean
|
||||
public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
|
||||
return new DefaultIdempotentKeyResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
|
||||
return new ExpressionIdempotentKeyResolver();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.annotation;
|
||||
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 幂等注解
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Idempotent {
|
||||
|
||||
/**
|
||||
* 幂等的超时时间,默认为 1 秒
|
||||
*
|
||||
* 注意,如果执行时间超过它,请求还是会进来
|
||||
*/
|
||||
int timeout() default 1;
|
||||
/**
|
||||
* 时间单位,默认为 SECONDS 秒
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 提示信息,正在执行中的提示
|
||||
*/
|
||||
String message() default "重复请求,请稍后重试";
|
||||
|
||||
/**
|
||||
* 使用的 Key 解析器
|
||||
*/
|
||||
Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
|
||||
/**
|
||||
* 使用的 Key 参数
|
||||
*/
|
||||
String keyArg() default "";
|
||||
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.aop;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO;
|
||||
import cn.iocoder.dashboard.util.collection.CollectionUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class IdempotentAspect {
|
||||
|
||||
/**
|
||||
* IdempotentKeyResolver 集合
|
||||
*/
|
||||
private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
|
||||
|
||||
private final IdempotentRedisDAO idempotentRedisDAO;
|
||||
|
||||
public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
|
||||
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
|
||||
this.idempotentRedisDAO = idempotentRedisDAO;
|
||||
}
|
||||
|
||||
@Before("@annotation(idempotent)")
|
||||
public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) {
|
||||
// 获得 IdempotentKeyResolver
|
||||
IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
|
||||
Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
|
||||
// 解析 Key
|
||||
String key = keyResolver.resolver(joinPoint, idempotent);
|
||||
|
||||
// 锁定 Key。
|
||||
boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
|
||||
// 锁定失败,抛出异常
|
||||
if (!success) {
|
||||
log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
|
||||
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.keyresolver;
|
||||
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
|
||||
/**
|
||||
* 幂等 Key 解析器接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IdempotentKeyResolver {
|
||||
|
||||
/**
|
||||
* 解析一个 Key
|
||||
*
|
||||
* @param idempotent 幂等注解
|
||||
* @param joinPoint AOP 切面
|
||||
* @return Key
|
||||
*/
|
||||
String resolver(JoinPoint joinPoint, Idempotent idempotent);
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
|
||||
/**
|
||||
* 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
|
||||
*
|
||||
* 为了避免 Key 过长,使用 MD5 进行“压缩”
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
|
||||
|
||||
@Override
|
||||
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
|
||||
String methodName = joinPoint.getSignature().toString();
|
||||
String argsStr = StrUtil.join(",", joinPoint.getArgs());
|
||||
return SecureUtil.md5(methodName + argsStr);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 基于 Spring EL 表达式,
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
|
||||
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
|
||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
|
||||
@Override
|
||||
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
|
||||
// 获得被拦截方法参数名列表
|
||||
Method method = getMethod(joinPoint);
|
||||
Object[] args = joinPoint.getArgs();
|
||||
String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
|
||||
// 准备 Spring EL 表达式解析的上下文
|
||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
|
||||
if (ArrayUtil.isNotEmpty(parameterNames)) {
|
||||
for (int i = 0; i < parameterNames.length; i++) {
|
||||
evaluationContext.setVariable(parameterNames[i], args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 解析参数
|
||||
Expression expression = expressionParser.parseExpression(idempotent.keyArg());
|
||||
return expression.getValue(evaluationContext, String.class);
|
||||
}
|
||||
|
||||
private static Method getMethod(JoinPoint point) {
|
||||
// 处理,声明在类上的情况
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
if (!method.getDeclaringClass().isInterface()) {
|
||||
return method;
|
||||
}
|
||||
|
||||
// 处理,声明在接口上的情况
|
||||
try {
|
||||
return point.getTarget().getClass().getDeclaredMethod(
|
||||
point.getSignature().getName(), method.getParameterTypes());
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.redis;
|
||||
|
||||
import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
|
||||
|
||||
/**
|
||||
* 幂等 Redis DAO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class IdempotentRedisDAO {
|
||||
|
||||
private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作",
|
||||
"idempotent:%s", // 参数为 uuid
|
||||
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
|
||||
String redisKey = formatKey(key);
|
||||
return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
|
||||
}
|
||||
|
||||
private static String formatKey(String key) {
|
||||
return String.format(IDEMPOTENT.getKeyTemplate(), key);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现
|
||||
* 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。
|
||||
*
|
||||
* 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。
|
||||
*
|
||||
* 和 it4alla/idempotent 组件的差异点,主要体现在两点:
|
||||
* 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力
|
||||
* 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。
|
||||
* 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.idempotent;
|
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.dashboard.framework.lock4j.config;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.iocoder.dashboard.framework.lock4j.core.DefaultLockFailureStrategy;
|
||||
import cn.iocoder.dashboard.framework.lock4j.core.Lock4jRedisKeyConstants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class Lock4jConfiguration {
|
||||
|
||||
static {
|
||||
// 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到
|
||||
// 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举
|
||||
ClassUtil.loadClass(Lock4jRedisKeyConstants.class.getName());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultLockFailureStrategy lockFailureStrategy() {
|
||||
return new DefaultLockFailureStrategy();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.dashboard.framework.lock4j.core;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.baomidou.lock.LockFailureStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 自定义获取锁失败策略,抛出 {@link cn.iocoder.dashboard.common.exception.ServiceException} 异常
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultLockFailureStrategy implements LockFailureStrategy {
|
||||
|
||||
@Override
|
||||
public void onLockFailure(String key, long acquireTimeout, int acquireCount) {
|
||||
log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout);
|
||||
throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package cn.iocoder.dashboard.framework.lock4j.core;
|
||||
|
||||
import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine;
|
||||
import org.redisson.api.RLock;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
|
||||
|
||||
/**
|
||||
* Lock4j Redis Key 枚举类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface Lock4jRedisKeyConstants {
|
||||
|
||||
RedisKeyDefine LOCK4J = new RedisKeyDefine("分布式锁",
|
||||
"lock4j:%s", // 参数来自 DefaultLockKeyBuilder 类
|
||||
HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.lock4j;
|
@@ -0,0 +1,34 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.web.config.WebProperties;
|
||||
import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
|
||||
@Configuration
|
||||
public class ApiLogConfiguration {
|
||||
|
||||
/**
|
||||
* 创建 ApiAccessLogFilter Bean,记录 API 请求日志
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
|
||||
@Value("${spring.application.name}") String applicationName,
|
||||
ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
|
||||
return createFilterBean(filter, FilterOrderEnum.API_ACCESS_LOG_FILTER);
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,112 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.filter;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
|
||||
import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils;
|
||||
import cn.iocoder.dashboard.framework.web.config.WebProperties;
|
||||
import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
|
||||
import cn.iocoder.dashboard.util.date.DateUtils;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* API 访问日志 Filter
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ApiAccessLogFilter extends OncePerRequestFilter {
|
||||
|
||||
private final WebProperties webProperties;
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只过滤 API 请求的地址
|
||||
return !request.getRequestURI().startsWith(webProperties.getApiPrefix());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 获得开始时间
|
||||
Date beginTim = new Date();
|
||||
// 提前获得参数,避免 XssFilter 过滤处理
|
||||
Map<String, String> queryString = ServletUtil.getParamMap(request);
|
||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null;
|
||||
|
||||
try {
|
||||
// 继续过滤器
|
||||
filterChain.doFilter(request, response);
|
||||
// 正常执行,记录日志
|
||||
createApiAccessLog(request, beginTim, queryString, requestBody, null);
|
||||
} catch (Exception ex) {
|
||||
// 异常执行,记录日志
|
||||
createApiAccessLog(request, beginTim, queryString, requestBody, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void createApiAccessLog(HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
ApiAccessLogCreateDTO accessLog = new ApiAccessLogCreateDTO();
|
||||
try {
|
||||
this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
|
||||
apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildApiAccessLogDTO(ApiAccessLogCreateDTO accessLog, HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
// 处理用户信息
|
||||
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
accessLog.setUserType(WebFrameworkUtils.getUesrType(request));
|
||||
// 设置访问结果
|
||||
CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
|
||||
if (result != null) {
|
||||
accessLog.setResultCode(result.getCode());
|
||||
accessLog.setResultMsg(result.getMsg());
|
||||
} else if (ex != null) {
|
||||
accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode());
|
||||
accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
|
||||
} else {
|
||||
accessLog.setResultCode(0);
|
||||
accessLog.setResultMsg("");
|
||||
}
|
||||
// 设置其它字段
|
||||
accessLog.setTraceId(TracerUtils.getTraceId());
|
||||
accessLog.setApplicationName(applicationName);
|
||||
accessLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build();
|
||||
accessLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
accessLog.setRequestMethod(request.getMethod());
|
||||
accessLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
accessLog.setUserIp(ServletUtil.getClientIP(request));
|
||||
// 持续时间
|
||||
accessLog.setBeginTime(beginTime);
|
||||
accessLog.setEndTime(new Date());
|
||||
accessLog.setDuration((int) DateUtils.diff(accessLog.getEndTime(), accessLog.getBeginTime()));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ApiAccessLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
* @return 是否创建成功
|
||||
*/
|
||||
Future<Boolean> createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ApiErrorLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
* @return 是否创建成功
|
||||
*/
|
||||
Future<Boolean> createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
|
||||
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 访问日志创建 DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class ApiAccessLogCreateDTO {
|
||||
|
||||
/**
|
||||
* 链路追踪编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 开始请求时间
|
||||
*/
|
||||
@NotNull(message = "开始请求时间不能为空")
|
||||
private Date beginTime;
|
||||
/**
|
||||
* 结束请求时间
|
||||
*/
|
||||
@NotNull(message = "结束请求时间不能为空")
|
||||
private Date endTime;
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
@NotNull(message = "执行时长不能为空")
|
||||
private Integer duration;
|
||||
/**
|
||||
* 结果码
|
||||
*/
|
||||
@NotNull(message = "错误码不能为空")
|
||||
private Integer resultCode;
|
||||
/**
|
||||
* 结果提示
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 错误日志创建 DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ApiErrorLogCreateDTO implements Serializable {
|
||||
|
||||
/**
|
||||
* 链路编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 账号编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 异常时间
|
||||
*/
|
||||
@NotNull(message = "异常时间不能为空")
|
||||
private Date exceptionTime;
|
||||
/**
|
||||
* 异常名
|
||||
*/
|
||||
@NotNull(message = "异常名不能为空")
|
||||
private String exceptionName;
|
||||
/**
|
||||
* 异常发生的类全名
|
||||
*/
|
||||
@NotNull(message = "异常发生的类全名不能为空")
|
||||
private String exceptionClassName;
|
||||
/**
|
||||
* 异常发生的类文件
|
||||
*/
|
||||
@NotNull(message = "异常发生的类文件不能为空")
|
||||
private String exceptionFileName;
|
||||
/**
|
||||
* 异常发生的方法名
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法名不能为空")
|
||||
private String exceptionMethodName;
|
||||
/**
|
||||
* 异常发生的方法所在行
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法所在行不能为空")
|
||||
private Integer exceptionLineNumber;
|
||||
/**
|
||||
* 异常的栈轨迹异常的栈轨迹
|
||||
*/
|
||||
@NotNull(message = "异常的栈轨迹不能为空")
|
||||
private String exceptionStackTrace;
|
||||
/**
|
||||
* 异常导致的根消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的根消息不能为空")
|
||||
private String exceptionRootCauseMessage;
|
||||
/**
|
||||
* 异常导致的消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的消息不能为空")
|
||||
private String exceptionMessage;
|
||||
|
||||
}
|
@@ -9,6 +9,11 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 操作日志注解
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface OperateLog {
|
||||
|
@@ -8,7 +8,7 @@ import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum;
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.service.OperateLogFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils;
|
||||
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
@@ -148,7 +148,7 @@ public class OperateLogAspect {
|
||||
}
|
||||
|
||||
private static void fillUserFields(SysOperateLogCreateReqVO operateLogVO) {
|
||||
operateLogVO.setUserId(SecurityUtils.getLoginUserId());
|
||||
operateLogVO.setUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
}
|
||||
|
||||
private static void fillModuleFields(SysOperateLogCreateReqVO operateLogVO,
|
||||
|
@@ -2,13 +2,16 @@ package cn.iocoder.dashboard.framework.logger.operatelog.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public interface OperateLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 要不记录操作日志
|
||||
* 异步记录操作日志
|
||||
*
|
||||
* @param reqVO 操作日志请求
|
||||
* @return true: 记录成功,false: 记录失败
|
||||
*/
|
||||
void createOperateLogAsync(SysOperateLogCreateReqVO reqVO);
|
||||
Future<Boolean> createOperateLogAsync(SysOperateLogCreateReqVO reqVO);
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.dashboard.framework.monitor.config;
|
||||
|
||||
import de.codecentric.boot.admin.server.config.EnableAdminServer;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableAdminServer
|
||||
public class AdminServerConfiguration {
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 使用 Spring Boot Admin 实现简单的监控平台
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.monitor;
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Admin/?yudao>
|
@@ -1,5 +1,7 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.mybatis.core.handler.DefaultDBFieldHandler;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@@ -13,7 +15,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan(value = "cn.iocoder.dashboard", annotationClass = Mapper.class)
|
||||
@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class,
|
||||
lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
|
||||
public class MybatisConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -23,4 +26,9 @@ public class MybatisConfiguration {
|
||||
return mybatisPlusInterceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MetaObjectHandler defaultMetaObjectHandler(){
|
||||
return new DefaultDBFieldHandler(); // 自动填充参数类
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.dataobject;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -15,26 +17,31 @@ public class BaseDO implements Serializable {
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
/**
|
||||
* 最后更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 创建者 TODO 芋艿:迁移成编号
|
||||
* 创建者,目前使用 SysUser 的 id 编号
|
||||
*
|
||||
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
|
||||
*/
|
||||
private String createBy;
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String creator;
|
||||
/**
|
||||
* 更新者 TODO 芋艿:迁移成编号
|
||||
* 更新者,目前使用 SysUser 的 id 编号
|
||||
*
|
||||
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
|
||||
*/
|
||||
private String updateBy;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updater;
|
||||
/**
|
||||
* 是否删除
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
// /** 备注 */ TODO 思考下,怎么解决
|
||||
// private String remark;
|
||||
private Boolean deleted;
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.handler;
|
||||
|
||||
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.dashboard.framework.security.core.LoginUser;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 通用参数填充实现类
|
||||
*
|
||||
* 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
|
||||
*
|
||||
* @author hexiaowu
|
||||
*/
|
||||
public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
|
||||
Date current = new Date();
|
||||
|
||||
// 创建时间为空,则以当前时间为插入时间
|
||||
if (Objects.isNull(baseDO.getCreateTime())) {
|
||||
baseDO.setCreateTime(current);
|
||||
}
|
||||
// 更新时间为空,则以当前时间为更新时间
|
||||
if (Objects.isNull(baseDO.getUpdateTime())) {
|
||||
baseDO.setUpdateTime(current);
|
||||
}
|
||||
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人
|
||||
if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) {
|
||||
baseDO.setCreator(loginUser.getId().toString());
|
||||
}
|
||||
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
|
||||
if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) {
|
||||
baseDO.setUpdater(loginUser.getId().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
Object modifyTime = getFieldValByName("updateTime", metaObject);
|
||||
Object modifier = getFieldValByName("updater", metaObject);
|
||||
// 获取登录用户信息
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
|
||||
// 更新时间为空,则以当前时间为更新时间
|
||||
if (Objects.isNull(modifyTime)) {
|
||||
setFieldValByName("updateTime", new Date(), metaObject);
|
||||
}
|
||||
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
|
||||
if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
|
||||
setFieldValByName("updater", loginUser.getId().toString(), metaObject);
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,6 +24,14 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
|
||||
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
|
||||
}
|
||||
|
||||
default T selectOne(String field, Object value) {
|
||||
return selectOne(new QueryWrapper<T>().eq(field, value));
|
||||
}
|
||||
|
||||
default Integer selectCount(String field, Object value) {
|
||||
return selectCount(new QueryWrapper<T>().eq(field, value));
|
||||
}
|
||||
|
||||
default List<T> selectList() {
|
||||
return selectList(new QueryWrapper<>());
|
||||
}
|
||||
|
@@ -44,6 +44,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> neIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.ne(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> gtIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.gt(column, val);
|
||||
@@ -51,6 +58,27 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> geIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.ge(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> ltIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.lt(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> leIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.le(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> betweenIfPresent(String column, Object val1, Object val2) {
|
||||
if (val1 != null && val2 != null) {
|
||||
return (QueryWrapperX<T>) super.between(column, val1, val2);
|
||||
@@ -90,4 +118,10 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryWrapperX<T> in(String column, Collection<?> coll) {
|
||||
super.in(column, coll);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.type;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
@@ -1 +1,4 @@
|
||||
/**
|
||||
* 使用 MyBatis Plus 提升使用 MyBatis 的开发效率
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.mybatis;
|
||||
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao>
|
@@ -1,9 +1,18 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.quartz.core.scheduler.SchedulerManager;
|
||||
import org.quartz.Scheduler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@Configuration
|
||||
@EnableScheduling // 开启 Spring 自带的定时任务
|
||||
public class QuartzConfig {
|
||||
|
||||
@Bean
|
||||
public SchedulerManager schedulerManager(Scheduler scheduler) {
|
||||
return new SchedulerManager(scheduler);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.enums;
|
||||
|
||||
/**
|
||||
* Quartz Job Data 的 key 枚举
|
||||
*/
|
||||
public enum JobDataKeyEnum {
|
||||
|
||||
JOB_ID,
|
||||
JOB_HANDLER_NAME,
|
||||
JOB_HANDLER_PARAM,
|
||||
JOB_RETRY_COUNT, // 最大重试次数
|
||||
JOB_RETRY_INTERVAL, // 每次重试间隔
|
||||
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.handler;
|
||||
|
||||
/**
|
||||
* 任务处理器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface JobHandler {
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*
|
||||
* @param param 参数
|
||||
* @return 结果
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
String execute(String param) throws Exception;
|
||||
|
||||
}
|
@@ -0,0 +1,113 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.handler;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum;
|
||||
import cn.iocoder.dashboard.framework.quartz.core.service.JobLogFrameworkService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.PersistJobDataAfterExecution;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.scheduling.quartz.QuartzJobBean;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
|
||||
import static cn.iocoder.dashboard.util.date.DateUtils.diff;
|
||||
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
|
||||
|
||||
/**
|
||||
* 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@DisallowConcurrentExecution
|
||||
@PersistJobDataAfterExecution
|
||||
@Slf4j
|
||||
public class JobHandlerInvoker extends QuartzJobBean {
|
||||
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Resource
|
||||
private JobLogFrameworkService jobLogFrameworkService;
|
||||
|
||||
@Override
|
||||
protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException {
|
||||
// 第一步,获得 Job 数据
|
||||
Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name());
|
||||
String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name());
|
||||
String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name());
|
||||
int refireCount = executionContext.getRefireCount();
|
||||
int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0);
|
||||
int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0);
|
||||
|
||||
// 第二步,执行任务
|
||||
Long jobLogId = null;
|
||||
Date startTime = new Date();
|
||||
String data = null;
|
||||
Throwable exception = null;
|
||||
try {
|
||||
// 记录 Job 日志(初始)
|
||||
jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1);
|
||||
// 执行任务
|
||||
data = this.executeInternal(jobHandlerName, jobHandlerParam);
|
||||
} catch (Throwable ex) {
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
// 第三步,记录执行日志
|
||||
this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext);
|
||||
|
||||
// 第四步,处理有异常的情况
|
||||
handleException(exception, refireCount, retryCount, retryInterval);
|
||||
}
|
||||
|
||||
private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception {
|
||||
// 获得 JobHandler 对象
|
||||
JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class);
|
||||
Assert.notNull(jobHandler, "JobHandler 不会为空");
|
||||
// 执行任务
|
||||
return jobHandler.execute(jobHandlerParam);
|
||||
}
|
||||
|
||||
private void updateJobLogResultAsync(Long jobLogId, Date startTime, String data, Throwable exception,
|
||||
JobExecutionContext executionContext) {
|
||||
Date endTime = new Date();
|
||||
// 处理是否成功
|
||||
boolean success = exception == null;
|
||||
if (!success) {
|
||||
data = getRootCauseMessage(exception);
|
||||
}
|
||||
// 更新日志
|
||||
try {
|
||||
jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) diff(endTime, startTime), success, data);
|
||||
} catch (Exception ex) {
|
||||
log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]",
|
||||
executionContext.getJobDetail().getKey(), jobLogId, success, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleException(Throwable exception,
|
||||
int refireCount, int retryCount, int retryInterval) throws JobExecutionException {
|
||||
// 如果有异常,则进行重试
|
||||
if (exception == null) {
|
||||
return;
|
||||
}
|
||||
// 情况一:如果到达重试上限,则直接抛出异常即可
|
||||
if (refireCount >= retryCount) {
|
||||
throw new JobExecutionException(exception);
|
||||
}
|
||||
|
||||
// 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试
|
||||
// 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。
|
||||
if (retryInterval > 0) {
|
||||
ThreadUtil.sleep(retryInterval);
|
||||
}
|
||||
// 第二个参数,refireImmediately = true,表示立即重试
|
||||
throw new JobExecutionException(exception, true);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,130 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.scheduler;
|
||||
|
||||
import cn.iocoder.dashboard.framework.quartz.core.enums.JobDataKeyEnum;
|
||||
import cn.iocoder.dashboard.framework.quartz.core.handler.JobHandlerInvoker;
|
||||
import org.quartz.*;
|
||||
|
||||
/**
|
||||
* {@link org.quartz.Scheduler} 的管理器,负责创建任务
|
||||
*
|
||||
* 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即:
|
||||
* 1. Job 的 {@link JobDetail#getKey()}
|
||||
* 2. Trigger 的 {@link Trigger#getKey()}
|
||||
*
|
||||
* 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class SchedulerManager {
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
public SchedulerManager(Scheduler scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 Job 到 Quartz 中
|
||||
*
|
||||
* @param jobId 任务编号
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @param jobHandlerParam 任务处理器的参数
|
||||
* @param cronExpression CRON 表达式
|
||||
* @param retryCount 重试次数
|
||||
* @param retryInterval 重试间隔
|
||||
* @throws SchedulerException 添加异常
|
||||
*/
|
||||
public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression,
|
||||
Integer retryCount, Integer retryInterval)
|
||||
throws SchedulerException {
|
||||
// 创建 JobDetail 对象
|
||||
JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class)
|
||||
.usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId)
|
||||
.usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName)
|
||||
.withIdentity(jobHandlerName).build();
|
||||
// 创建 Trigger 对象
|
||||
Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
|
||||
// 新增调度
|
||||
scheduler.scheduleJob(jobDetail, trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Job 到 Quartz
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @param jobHandlerParam 任务处理器的参数
|
||||
* @param cronExpression CRON 表达式
|
||||
* @param retryCount 重试次数
|
||||
* @param retryInterval 重试间隔
|
||||
* @throws SchedulerException 更新异常
|
||||
*/
|
||||
public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression,
|
||||
Integer retryCount, Integer retryInterval)
|
||||
throws SchedulerException {
|
||||
// 创建新 Trigger 对象
|
||||
Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
|
||||
// 修改调度
|
||||
scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Quartz 中的 Job
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @throws SchedulerException 删除异常
|
||||
*/
|
||||
public void deleteJob(String jobHandlerName) throws SchedulerException {
|
||||
scheduler.deleteJob(new JobKey(jobHandlerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停 Quartz 中的 Job
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @throws SchedulerException 暂停异常
|
||||
*/
|
||||
public void pauseJob(String jobHandlerName) throws SchedulerException {
|
||||
scheduler.pauseJob(new JobKey(jobHandlerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 Quartz 中的 Job
|
||||
*
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @throws SchedulerException 启动异常
|
||||
*/
|
||||
public void resumeJob(String jobHandlerName) throws SchedulerException {
|
||||
scheduler.resumeJob(new JobKey(jobHandlerName));
|
||||
scheduler.resumeTrigger(new TriggerKey(jobHandlerName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即触发一次 Quartz 中的 Job
|
||||
*
|
||||
* @param jobId 任务编号
|
||||
* @param jobHandlerName 任务处理器的名字
|
||||
* @param jobHandlerParam 任务处理器的参数
|
||||
* @throws SchedulerException 触发异常
|
||||
*/
|
||||
public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam)
|
||||
throws SchedulerException {
|
||||
JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval
|
||||
data.put(JobDataKeyEnum.JOB_ID.name(), jobId);
|
||||
data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName);
|
||||
data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam);
|
||||
// 触发任务
|
||||
scheduler.triggerJob(new JobKey(jobHandlerName), data);
|
||||
}
|
||||
|
||||
private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression,
|
||||
Integer retryCount, Integer retryInterval) {
|
||||
return TriggerBuilder.newTrigger()
|
||||
.withIdentity(jobHandlerName)
|
||||
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
|
||||
.usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam)
|
||||
.usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount)
|
||||
.usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.service;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Job 日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface JobLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 Job 日志
|
||||
*
|
||||
* @param jobId 任务编号
|
||||
* @param beginTime 开始时间
|
||||
* @param jobHandlerName Job 处理器的名字
|
||||
* @param jobHandlerParam Job 处理器的参数
|
||||
* @param executeIndex 第几次执行
|
||||
* @return Job 日志的编号
|
||||
*/
|
||||
Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId,
|
||||
@NotNull(message = "开始时间") Date beginTime,
|
||||
@NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName,
|
||||
String jobHandlerParam,
|
||||
@NotNull(message = "第几次执行不能为空") Integer executeIndex);
|
||||
|
||||
/**
|
||||
* 更新 Job 日志的执行结果
|
||||
*
|
||||
* @param logId 日志编号
|
||||
* @param endTime 结束时间。因为是异步,避免记录时间不准去
|
||||
* @param duration 运行时长,单位:毫秒
|
||||
* @param success 是否成功
|
||||
* @param result 成功数据
|
||||
*/
|
||||
void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId,
|
||||
@NotNull(message = "结束时间不能为空") Date endTime,
|
||||
@NotNull(message = "运行时长不能为空") Integer duration,
|
||||
boolean success, String result);
|
||||
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.dashboard.framework.quartz.core.util;
|
||||
|
||||
import org.quartz.CronExpression;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Quartz Cron 表达式的工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CronUtils {
|
||||
|
||||
/**
|
||||
* 校验 CRON 表达式是否有效
|
||||
*
|
||||
* @param cronExpression CRON 表达式
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValid(String cronExpression) {
|
||||
return CronExpression.isValidExpression(cronExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 CRON 表达式,获得下 n 个满足执行的时间
|
||||
*
|
||||
* @param cronExpression CRON 表达式
|
||||
* @param n 数量
|
||||
* @return 满足条件的执行时间
|
||||
*/
|
||||
public static List<Date> getNextTimes(String cronExpression, int n) {
|
||||
// 获得 CronExpression 对象
|
||||
CronExpression cron;
|
||||
try {
|
||||
cron = new CronExpression(cronExpression);
|
||||
} catch (ParseException e) {
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
// 从当前开始计算,n 个满足条件的
|
||||
Date now = new Date();
|
||||
List<Date> nextTimes = new ArrayList<>(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
Date nextTime = cron.getNextValidTimeAfter(now);
|
||||
nextTimes.add(nextTime);
|
||||
// 切换现在,为下一个触发时间;
|
||||
now = nextTime;
|
||||
}
|
||||
return nextTimes;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Job/?yudao>
|
@@ -27,7 +27,7 @@ public class RedisConfig {
|
||||
template.setConnectionFactory(factory);
|
||||
// 使用 String 序列化方式,序列化 KEY 。
|
||||
template.setKeySerializer(RedisSerializer.string());
|
||||
// 使用 JSON 序列化方式(库是 FastJSON ),序列化 VALUE 。
|
||||
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
|
||||
template.setValueSerializer(RedisSerializer.json());
|
||||
return template;
|
||||
}
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package cn.iocoder.dashboard.framework.redis.core;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@@ -12,27 +15,41 @@ import java.time.Duration;
|
||||
@Data
|
||||
public class RedisKeyDefine {
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum KeyTypeEnum {
|
||||
|
||||
STRING,
|
||||
LIST,
|
||||
HASH,
|
||||
SET,
|
||||
ZSET,
|
||||
STREAM,
|
||||
PUBSUB
|
||||
STRING("String"),
|
||||
LIST("List"),
|
||||
HASH("Hash"),
|
||||
SET("Set"),
|
||||
ZSET("Sorted Set"),
|
||||
STREAM("Stream"),
|
||||
PUBSUB("Pub/Sub");
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@JsonValue
|
||||
private final String type;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 过期时间 - 永不过期
|
||||
*/
|
||||
public static final Duration TIMEOUT_FOREVER = null;
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum TimeoutTypeEnum {
|
||||
|
||||
/**
|
||||
* 过期时间 - 动态,通过参数传入
|
||||
*/
|
||||
public static final Duration TIMEOUT_DYNAMIC = null;
|
||||
FOREVER(1), // 永不超时
|
||||
DYNAMIC(2), // 动态超时
|
||||
FIXED(3); // 固定超时
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@JsonValue
|
||||
private final Integer type;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Key 模板
|
||||
@@ -48,18 +65,37 @@ public class RedisKeyDefine {
|
||||
* 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型
|
||||
*/
|
||||
private final Class<?> valueType;
|
||||
/**
|
||||
* 超时类型
|
||||
*/
|
||||
private final TimeoutTypeEnum timeoutType;
|
||||
/**
|
||||
* 过期时间
|
||||
*
|
||||
* 为空时,表示永不过期 {@link #TIMEOUT_FOREVER}
|
||||
*/
|
||||
private final Duration timeout;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private final String memo;
|
||||
|
||||
public RedisKeyDefine(String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
|
||||
private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType,
|
||||
TimeoutTypeEnum timeoutType, Duration timeout) {
|
||||
this.memo = memo;
|
||||
this.keyTemplate = keyTemplate;
|
||||
this.keyType = keyType;
|
||||
this.valueType = valueType;
|
||||
this.timeout = timeout;
|
||||
this.timeoutType = timeoutType;
|
||||
// 添加注册表
|
||||
RedisKeyRegistry.add(this);
|
||||
}
|
||||
|
||||
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
|
||||
this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout);
|
||||
}
|
||||
|
||||
public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
|
||||
this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.dashboard.framework.redis.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link RedisKeyDefine} 注册表
|
||||
*/
|
||||
public class RedisKeyRegistry {
|
||||
|
||||
/**
|
||||
* Redis RedisKeyDefine 数组
|
||||
*/
|
||||
private static final List<RedisKeyDefine> defines = new ArrayList<>();
|
||||
|
||||
public static void add(RedisKeyDefine define) {
|
||||
defines.add(define);
|
||||
}
|
||||
|
||||
public static List<RedisKeyDefine> list() {
|
||||
return defines;
|
||||
}
|
||||
|
||||
public static int size() {
|
||||
return defines.size();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.redis;
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Redis/?yudao>
|
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 使用 Resilience4j 组件,实现服务保障,包括:
|
||||
* 1. 熔断器
|
||||
* 2. 限流器
|
||||
* 3. 舱壁隔离
|
||||
* 4. 重试
|
||||
* 5. 限时器
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.resilience4j;
|
@@ -0,0 +1 @@
|
||||
<https://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao>
|
@@ -4,6 +4,7 @@ import cn.iocoder.dashboard.framework.security.core.filter.JwtAuthenticationToke
|
||||
import cn.iocoder.dashboard.framework.security.core.handler.LogoutSuccessHandlerImpl;
|
||||
import cn.iocoder.dashboard.framework.web.config.WebProperties;
|
||||
import de.codecentric.boot.admin.server.config.AdminServerProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -68,8 +69,9 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
* 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
|
||||
* 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
|
||||
*/
|
||||
@Bean
|
||||
@Override
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(AuthenticationManager.class)
|
||||
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||
return super.authenticationManagerBean();
|
||||
}
|
||||
@@ -114,6 +116,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity httpSecurity) throws Exception {
|
||||
httpSecurity
|
||||
// 开启跨域
|
||||
.cors().and()
|
||||
// CSRF 禁用,因为不使用 Session
|
||||
.csrf().disable()
|
||||
// 基于 token 机制,所以不需要 Session
|
||||
@@ -121,36 +125,34 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
// 一堆自定义的 Spring Security 处理器
|
||||
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
|
||||
.accessDeniedHandler(accessDeniedHandler).and()
|
||||
// TODO 过滤请求
|
||||
// 设置每个请求的权限
|
||||
.authorizeRequests()
|
||||
// 登陆的接口,可匿名访问
|
||||
.antMatchers(webProperties.getApiPrefix() + "/login").anonymous()
|
||||
// 通用的接口,可匿名访问
|
||||
.antMatchers( webProperties.getApiPrefix() + "/system/captcha/**").anonymous()
|
||||
// TODO
|
||||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
||||
.antMatchers("/profile/**").anonymous()
|
||||
// 文件的获取接口,可匿名访问
|
||||
.antMatchers(webProperties.getApiPrefix() + "/system/file/get/**").anonymous()
|
||||
// TODO
|
||||
.antMatchers("/swagger-ui.html").anonymous()
|
||||
.antMatchers("/**").anonymous()
|
||||
.antMatchers("/swagger-resources/**").anonymous()
|
||||
.antMatchers("/webjars/**").anonymous()
|
||||
.antMatchers("/*/api-docs").anonymous()
|
||||
// Spring Boot Admin Server 的安全配置
|
||||
.antMatchers(adminServerProperties.getContextPath()).anonymous()
|
||||
.antMatchers(adminServerProperties.getContextPath() + "/**").anonymous()
|
||||
// Spring Boot Actuator 的安全配置
|
||||
.antMatchers("/actuator").anonymous()
|
||||
.antMatchers("/actuator/**").anonymous()
|
||||
// TODO
|
||||
.antMatchers("/druid/**").hasAnyAuthority("druid") // TODO 芋艿,未来需要在拓展下
|
||||
// 除上面外的所有请求全部需要鉴权认证
|
||||
.anyRequest().authenticated()
|
||||
// 登陆的接口,可匿名访问
|
||||
.antMatchers(webProperties.getApiPrefix() + "/login").anonymous()
|
||||
// 通用的接口,可匿名访问
|
||||
.antMatchers( webProperties.getApiPrefix() + "/system/captcha/**").anonymous()
|
||||
// 静态资源,可匿名访问
|
||||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
||||
// 文件的获取接口,可匿名访问
|
||||
.antMatchers(webProperties.getApiPrefix() + "/infra/file/get/**").anonymous()
|
||||
// Swagger 接口文档
|
||||
.antMatchers("/swagger-ui.html").anonymous()
|
||||
.antMatchers("/swagger-resources/**").anonymous()
|
||||
.antMatchers("/webjars/**").anonymous()
|
||||
.antMatchers("/*/api-docs").anonymous()
|
||||
// Spring Boot Admin Server 的安全配置
|
||||
.antMatchers(adminServerProperties.getContextPath()).anonymous()
|
||||
.antMatchers(adminServerProperties.getContextPath() + "/**").anonymous()
|
||||
// Spring Boot Actuator 的安全配置
|
||||
.antMatchers("/actuator").anonymous()
|
||||
.antMatchers("/actuator/**").anonymous()
|
||||
// Druid 监控
|
||||
.antMatchers("/druid/**").anonymous()
|
||||
// 除上面外的所有请求全部需要鉴权认证
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.headers().frameOptions().disable();
|
||||
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
|
||||
httpSecurity.logout().logoutUrl(webProperties.getApiPrefix() + "/logout").logoutSuccessHandler(logoutSuccessHandler);
|
||||
// 添加 JWT Filter
|
||||
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
}
|
||||
|
@@ -1,17 +1,13 @@
|
||||
package cn.iocoder.dashboard.framework.security.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
|
||||
import cn.iocoder.dashboard.framework.security.core.LoginUser;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.dashboard.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@@ -42,7 +38,7 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
|
||||
@SuppressWarnings("NullableProblems")
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
String token = SecurityUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
|
||||
String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
|
||||
if (StrUtil.isNotEmpty(token)) {
|
||||
try {
|
||||
// 验证 token 有效性
|
||||
@@ -53,7 +49,7 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
|
||||
}
|
||||
// 设置当前用户
|
||||
if (loginUser != null) {
|
||||
SecurityUtils.setLoginUser(loginUser, request);
|
||||
SecurityFrameworkUtils.setLoginUser(loginUser, request);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
|
||||
|
@@ -2,7 +2,7 @@ package cn.iocoder.dashboard.framework.security.core.handler;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
@@ -35,7 +35,7 @@ public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
|
||||
throws IOException, ServletException {
|
||||
// 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
|
||||
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
|
||||
SecurityUtils.getLoginUser().getId(), e);
|
||||
SecurityFrameworkUtils.getLoginUser().getId(), e);
|
||||
// 返回 403
|
||||
ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED));
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package cn.iocoder.dashboard.framework.security.core.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
|
||||
import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
@@ -31,12 +32,11 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
|
||||
@Override
|
||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
// 执行退出
|
||||
String token = SecurityUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
|
||||
String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
securityFrameworkService.logout(token);
|
||||
}
|
||||
// 返回成功
|
||||
ServletUtils.writeJSON(response, null);
|
||||
// ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.OK.value(), "退出成功")));
|
||||
ServletUtils.writeJSON(response, CommonResult.success(null));
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package cn.iocoder.dashboard.framework.security.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO;
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO;
|
||||
|
||||
/**
|
||||
* Security 框架 Permission Service 接口,定义 security 组件需要的功能
|
||||
|
@@ -1,7 +1,11 @@
|
||||
package cn.iocoder.dashboard.framework.security.core.util;
|
||||
|
||||
import cn.iocoder.dashboard.framework.security.core.LoginUser;
|
||||
import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -12,11 +16,11 @@ import java.util.Set;
|
||||
/**
|
||||
* 安全服务工具类
|
||||
*
|
||||
* @author ruoyi
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class SecurityUtils {
|
||||
public class SecurityFrameworkUtils {
|
||||
|
||||
private SecurityUtils() {}
|
||||
private SecurityFrameworkUtils() {}
|
||||
|
||||
/**
|
||||
* 从请求中,获得认证 Token
|
||||
@@ -39,22 +43,42 @@ public class SecurityUtils {
|
||||
|
||||
/**
|
||||
* 获取当前用户
|
||||
*
|
||||
* @return 当前用户
|
||||
*/
|
||||
@Nullable
|
||||
public static LoginUser getLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
SecurityContext context = SecurityContextHolder.getContext();
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
Authentication authentication = context.getAuthentication();
|
||||
if (authentication == null) {
|
||||
return null;
|
||||
}
|
||||
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的编号
|
||||
* 获得当前用户的编号,从上下文中
|
||||
*
|
||||
* @return 用户编号
|
||||
*/
|
||||
@Nullable
|
||||
public static Long getLoginUserId() {
|
||||
return getLoginUser().getId();
|
||||
LoginUser loginUser = getLoginUser();
|
||||
return loginUser != null ? loginUser.getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的角色编号数组
|
||||
*
|
||||
* @return 角色编号数组
|
||||
*/
|
||||
@Nullable
|
||||
public static Set<Long> getLoginUserRoleIds() {
|
||||
return getLoginUser().getRoleIds();
|
||||
LoginUser loginUser = getLoginUser();
|
||||
return loginUser != null ? loginUser.getRoleIds() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,6 +94,9 @@ public class SecurityUtils {
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
// 设置到上下文
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
// 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
|
||||
// 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
|
||||
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
|
||||
}
|
||||
|
||||
}
|
@@ -1,2 +1,2 @@
|
||||
* 芋道 Spring Security 入门:<http://www.iocoder.cn/Spring-Boot/Spring-Security/?dashboard>
|
||||
* Spring Security 基本概念:<http://www.iocoder.cn/Fight/Spring-Security-4-1-0-Basic-concept-description/?dashboard>
|
||||
* 芋道 Spring Security 入门:<http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao>
|
||||
* Spring Security 基本概念:<http://www.iocoder.cn/Fight/Spring-Security-4-1-0-Basic-concept-description/?yudao>
|
||||
|
@@ -10,15 +10,17 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.service.*;
|
||||
import springfox.documentation.service.ApiInfo;
|
||||
import springfox.documentation.service.ApiKey;
|
||||
import springfox.documentation.service.AuthorizationScope;
|
||||
import springfox.documentation.service.Contact;
|
||||
import springfox.documentation.service.SecurityReference;
|
||||
import springfox.documentation.service.SecurityScheme;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
import springfox.documentation.service.ApiKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Swagger/?dashboard>
|
||||
<http://www.iocoder.cn/Spring-Boot/Swagger/?yudao>
|
||||
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao>
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 使用 Hibernate Validator 实现参数校验
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.validator;
|
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Validation/?yudao>
|
@@ -1,9 +1,13 @@
|
||||
package cn.iocoder.dashboard.framework.web.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum;
|
||||
import cn.iocoder.dashboard.framework.web.core.filter.CacheRequestBodyFilter;
|
||||
import cn.iocoder.dashboard.framework.web.core.filter.XssFilter;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
@@ -12,12 +16,10 @@ import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.Filter;
|
||||
|
||||
/**
|
||||
* Web 配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(WebProperties.class)
|
||||
@EnableConfigurationProperties({WebProperties.class, XssProperties.class})
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
@@ -25,9 +27,10 @@ public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
// 设置 API 前缀,仅仅匹配 controller 包下的
|
||||
configurer.addPathPrefix(webProperties.getApiPrefix(), clazz ->
|
||||
clazz.isAnnotationPresent(RestController.class)
|
||||
&& clazz.getPackage().getName().startsWith(webProperties.getControllerPackage()));
|
||||
&& clazz.getPackage().getName().startsWith(webProperties.getControllerPackage())); // 仅仅匹配 controller 包
|
||||
}
|
||||
|
||||
// ========== Filter 相关 ==========
|
||||
@@ -36,8 +39,7 @@ public class WebConfiguration implements WebMvcConfigurer {
|
||||
* 创建 CorsFilter Bean,解决跨域问题
|
||||
*/
|
||||
@Bean
|
||||
@Order(Integer.MIN_VALUE)
|
||||
public CorsFilter corsFilter() {
|
||||
public FilterRegistrationBean<CorsFilter> corsFilterBean() {
|
||||
// 创建 CorsConfiguration 对象
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowCredentials(true);
|
||||
@@ -47,7 +49,29 @@ public class WebConfiguration implements WebMvcConfigurer {
|
||||
// 创建 UrlBasedCorsConfigurationSource 对象
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
|
||||
return new CorsFilter(source);
|
||||
return createFilterBean(new CorsFilter(source), FilterOrderEnum.CORS_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() {
|
||||
return createFilterBean(new CacheRequestBodyFilter(), FilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 XssFilter Bean,解决 Xss 安全问题
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
|
||||
return createFilterBean(new XssFilter(properties, pathMatcher), FilterOrderEnum.XSS_FILTER);
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.dashboard.framework.web.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Xss 配置属性
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "yudao.xss")
|
||||
@Validated
|
||||
@Data
|
||||
public class XssProperties {
|
||||
|
||||
/**
|
||||
* 是否开启,默认为 true
|
||||
*/
|
||||
private boolean enable = true;
|
||||
/**
|
||||
* 需要排除的 URL,默认为空
|
||||
*/
|
||||
private List<String> excludeUrls = Collections.emptyList();
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.enums;
|
||||
|
||||
/**
|
||||
* 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface FilterOrderEnum {
|
||||
|
||||
int CORS_FILTER = Integer.MIN_VALUE;
|
||||
|
||||
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
|
||||
|
||||
// OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
|
||||
|
||||
int API_ACCESS_LOG_FILTER = -104; // 需要保证在 RequestBodyCacheFilter 后面
|
||||
|
||||
int XSS_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
|
||||
|
||||
// Spring Security Filter 默认为 -100,可见 SecurityProperties 配置属性类
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.filter;
|
||||
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Request Body 缓存 Filter,实现它的可重复读取
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CacheRequestBodyFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new CacheRequestBodyWrapper(request), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只处理 json 请求内容
|
||||
return !ServletUtils.isJsonRequest(request);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Request Body 缓存 Wrapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 缓存的内容
|
||||
*/
|
||||
private final byte[] body;
|
||||
|
||||
public CacheRequestBodyWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
body = ServletUtil.getBodyBytes(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.filter;
|
||||
|
||||
import cn.iocoder.dashboard.framework.web.config.XssProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Xss 过滤器
|
||||
*
|
||||
* 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class XssFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* 属性
|
||||
*/
|
||||
private final XssProperties properties;
|
||||
/**
|
||||
* 路径匹配器
|
||||
*/
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new XssRequestWrapper(request), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 如果关闭,则不过滤
|
||||
if (!properties.isEnable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果匹配到无需过滤,则不过滤
|
||||
String uri = request.getRequestURI();
|
||||
return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,137 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HTMLFilter;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Xss 请求 Wrapper
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class XssRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 基于线程级别的 HTMLFilter 对象,因为它线程非安全
|
||||
*/
|
||||
private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
|
||||
HTMLFilter htmlFilter = new HTMLFilter();
|
||||
// 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符
|
||||
ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
|
||||
return htmlFilter;
|
||||
});
|
||||
|
||||
public XssRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
private static String filterXss(String content) {
|
||||
if (StrUtil.isEmpty(content)) {
|
||||
return content;
|
||||
}
|
||||
return HTML_FILTER.get().filter(content);
|
||||
}
|
||||
|
||||
// ========== IO 流相关 ==========
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
// 如果非 json 请求,不进行 Xss 处理
|
||||
if (!ServletUtils.isJsonRequest(this)) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
// 读取内容,并过滤
|
||||
String content = IoUtil.readUtf8(super.getInputStream());
|
||||
content = filterXss(content);
|
||||
final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return newInputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Param 相关 ==========
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
return filterXss(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (ArrayUtil.isEmpty(values)) {
|
||||
return values;
|
||||
}
|
||||
// 过滤处理
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> valueMap = super.getParameterMap();
|
||||
if (CollUtil.isEmpty(valueMap)) {
|
||||
return valueMap;
|
||||
}
|
||||
// 过滤处理
|
||||
for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
}
|
||||
|
||||
// ========== Header 相关 ==========
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
return filterXss(value);
|
||||
}
|
||||
|
||||
}
|
@@ -1,11 +1,24 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.handler;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.iocoder.dashboard.common.exception.GlobalException;
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils;
|
||||
import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
@@ -16,20 +29,31 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.ValidationException;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@Value("${spring.application.name}")
|
||||
private String applicationName;
|
||||
|
||||
@Resource
|
||||
private ApiErrorLogFrameworkService apiErrorLogFrameworkService;
|
||||
|
||||
/**
|
||||
* 处理所有异常,主要是提供给 Filter 使用
|
||||
* 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
|
||||
@@ -63,6 +87,9 @@ public class GlobalExceptionHandler {
|
||||
if (ex instanceof HttpRequestMethodNotSupportedException) {
|
||||
return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
|
||||
}
|
||||
if (ex instanceof RequestNotPermitted) {
|
||||
return requestNotPermittedExceptionHandler(request, (RequestNotPermitted) ex);
|
||||
}
|
||||
if (ex instanceof ServiceException) {
|
||||
return serviceExceptionHandler((ServiceException) ex);
|
||||
}
|
||||
@@ -163,6 +190,15 @@ public class GlobalExceptionHandler {
|
||||
return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Resilience4j 限流抛出的异常
|
||||
*/
|
||||
@ExceptionHandler(value = RequestNotPermitted.class)
|
||||
public CommonResult<?> requestNotPermittedExceptionHandler(HttpServletRequest req, RequestNotPermitted ex) {
|
||||
log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex);
|
||||
return CommonResult.error(TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Spring Security 权限不足的异常
|
||||
*
|
||||
@@ -170,7 +206,7 @@ public class GlobalExceptionHandler {
|
||||
*/
|
||||
@ExceptionHandler(value = AccessDeniedException.class)
|
||||
public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
|
||||
log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", SecurityUtils.getLoginUserId(),
|
||||
log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", SecurityFrameworkUtils.getLoginUserId(),
|
||||
req.getRequestURL(), ex);
|
||||
return CommonResult.error(FORBIDDEN);
|
||||
}
|
||||
@@ -217,57 +253,47 @@ public class GlobalExceptionHandler {
|
||||
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage());
|
||||
}
|
||||
|
||||
// TODO 芋艿:增加异常日志
|
||||
public void createExceptionLog(HttpServletRequest req, Throwable e) {
|
||||
// // 插入异常日志
|
||||
// SystemExceptionLogCreateDTO exceptionLog = new SystemExceptionLogCreateDTO();
|
||||
// try {
|
||||
// // 增加异常计数 metrics TODO 暂时去掉
|
||||
//// EXCEPTION_COUNTER.increment();
|
||||
// // 初始化 exceptionLog
|
||||
// initExceptionLog(exceptionLog, req, e);
|
||||
// // 执行插入 exceptionLog
|
||||
// createExceptionLog(exceptionLog);
|
||||
// } catch (Throwable th) {
|
||||
// log.error("[createExceptionLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
|
||||
// }
|
||||
private void createExceptionLog(HttpServletRequest req, Throwable e) {
|
||||
// 插入错误日志
|
||||
ApiErrorLogCreateDTO errorLog = new ApiErrorLogCreateDTO();
|
||||
try {
|
||||
// 初始化 errorLog
|
||||
initExceptionLog(errorLog, req, e);
|
||||
// 执行插入 errorLog
|
||||
apiErrorLogFrameworkService.createApiErrorLogAsync(errorLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
// // TODO 优化点:后续可以增加事件
|
||||
// @Async
|
||||
// public void createExceptionLog(SystemExceptionLogCreateDTO exceptionLog) {
|
||||
// try {
|
||||
// systemExceptionLogRpc.createSystemExceptionLog(exceptionLog);
|
||||
// } catch (Throwable th) {
|
||||
// log.error("[addAccessLog][插入异常日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void initExceptionLog(SystemExceptionLogCreateDTO exceptionLog, HttpServletRequest request, Throwable e) {
|
||||
// // 设置账号编号
|
||||
// exceptionLog.setUserId(CommonWebUtil.getUserId(request));
|
||||
// exceptionLog.setUserType(CommonWebUtil.getUserType(request));
|
||||
// // 设置异常字段
|
||||
// exceptionLog.setExceptionName(e.getClass().getName());
|
||||
// exceptionLog.setExceptionMessage(ExceptionUtil.getMessage(e));
|
||||
// exceptionLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
|
||||
// exceptionLog.setExceptionStackTrace(ExceptionUtil.getStackTrace(e));
|
||||
// StackTraceElement[] stackTraceElements = e.getStackTrace();
|
||||
// Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
|
||||
// StackTraceElement stackTraceElement = stackTraceElements[0];
|
||||
// exceptionLog.setExceptionClassName(stackTraceElement.getClassName());
|
||||
// exceptionLog.setExceptionFileName(stackTraceElement.getFileName());
|
||||
// exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName());
|
||||
// exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
|
||||
// // 设置其它字段
|
||||
// exceptionLog.setTraceId(MallUtils.getTraceId())
|
||||
// .setApplicationName(applicationName)
|
||||
// .setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
|
||||
// .setQueryString(HttpUtil.buildQueryString(request))
|
||||
// .setMethod(request.getMethod())
|
||||
// .setUserAgent(HttpUtil.getUserAgent(request))
|
||||
// .setIp(HttpUtil.getIp(request))
|
||||
// .setExceptionTime(new Date());
|
||||
// }
|
||||
private void initExceptionLog(ApiErrorLogCreateDTO errorLog, HttpServletRequest request, Throwable e) {
|
||||
// 处理用户信息
|
||||
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
errorLog.setUserType(WebFrameworkUtils.getUesrType(request));
|
||||
// 设置异常字段
|
||||
errorLog.setExceptionName(e.getClass().getName());
|
||||
errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
|
||||
errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
|
||||
errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e));
|
||||
StackTraceElement[] stackTraceElements = e.getStackTrace();
|
||||
Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
|
||||
StackTraceElement stackTraceElement = stackTraceElements[0];
|
||||
errorLog.setExceptionClassName(stackTraceElement.getClassName());
|
||||
errorLog.setExceptionFileName(stackTraceElement.getFileName());
|
||||
errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
|
||||
errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
|
||||
// 设置其它字段
|
||||
errorLog.setTraceId(TracerUtils.getTraceId());
|
||||
errorLog.setApplicationName(applicationName);
|
||||
errorLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
|
||||
.put("query", ServletUtil.getParamMap(request))
|
||||
.put("body", ServletUtil.getBody(request)).build();
|
||||
errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
errorLog.setRequestMethod(request.getMethod());
|
||||
errorLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
errorLog.setUserIp(ServletUtil.getClientIP(request));
|
||||
errorLog.setExceptionTime(new Date());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.handler;
|
||||
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
/**
|
||||
* 全局响应结果(ResponseBody)处理器
|
||||
*
|
||||
* 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult},
|
||||
* 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。
|
||||
* 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构
|
||||
*
|
||||
* 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果,
|
||||
* 方便 {@link cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter} 记录访问日志
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class GlobalResponseBodyHandler implements ResponseBodyAdvice {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
|
||||
public boolean supports(MethodParameter returnType, Class converterType) {
|
||||
if (returnType.getMethod() == null) {
|
||||
return false;
|
||||
}
|
||||
// 只拦截返回结果为 CommonResult 类型
|
||||
return returnType.getMethod().getReturnType() == CommonResult.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
|
||||
ServerHttpRequest request, ServerHttpResponse response) {
|
||||
// 记录 Controller 结果
|
||||
WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult<?>) body);
|
||||
return body;
|
||||
}
|
||||
|
||||
}
|
@@ -1 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.web.core;
|
@@ -0,0 +1,46 @@
|
||||
package cn.iocoder.dashboard.framework.web.core.util;
|
||||
|
||||
import cn.iocoder.dashboard.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 专属于 web 包的工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class WebFrameworkUtils {
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id";
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
|
||||
|
||||
public static void setLoginUserId(ServletRequest request, Long userId) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的编号,从请求中
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 用户编号
|
||||
*/
|
||||
public static Long getLoginUserId(HttpServletRequest request) {
|
||||
return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID);
|
||||
}
|
||||
|
||||
public static Integer getUesrType(HttpServletRequest request) {
|
||||
return UserTypeEnum.ADMIN.getValue(); // TODO 芋艿:等后续优化
|
||||
}
|
||||
|
||||
public static void setCommonResult(ServletRequest request, CommonResult<?> result) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result);
|
||||
}
|
||||
|
||||
public static CommonResult<?> getCommonResult(ServletRequest request) {
|
||||
return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
|
||||
}
|
||||
|
||||
}
|
@@ -1 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?dashboard>
|
||||
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?yudao>
|
||||
|
@@ -1,118 +1,105 @@
|
||||
package cn.iocoder.dashboard.modules.infra.controller.config;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.common.pojo.PageResult;
|
||||
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
|
||||
import cn.iocoder.dashboard.modules.infra.controller.config.vo.*;
|
||||
import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.mysql.dataobject.config.InfConfigDO;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
|
||||
import cn.iocoder.dashboard.modules.infra.service.config.InfConfigService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.Valid;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
|
||||
import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_SENSITIVE;
|
||||
|
||||
@Api(tags = "参数配置")
|
||||
@RestController
|
||||
@RequestMapping("/infra/config")
|
||||
@Validated
|
||||
public class InfConfigController {
|
||||
|
||||
@Value("${demo.test}")
|
||||
private String demo;
|
||||
|
||||
@GetMapping("/demo")
|
||||
public String demo() {
|
||||
return demo;
|
||||
}
|
||||
|
||||
@PostMapping("/demo")
|
||||
public void setDemo() {
|
||||
|
||||
}
|
||||
|
||||
@Resource
|
||||
private InfConfigService configService;
|
||||
|
||||
@ApiOperation("获取参数配置分页")
|
||||
@GetMapping("/page")
|
||||
// @PreAuthorize("@ss.hasPermi('infra:config:list')")
|
||||
public CommonResult<PageResult<InfConfigRespVO>> getConfigPage(@Validated InfConfigPageReqVO reqVO) {
|
||||
PageResult<InfConfigDO> page = configService.getConfigPage(reqVO);
|
||||
return success(InfConfigConvert.INSTANCE.convertPage(page));
|
||||
@PostMapping("/create")
|
||||
@ApiOperation("创建参数配置")
|
||||
@PreAuthorize("@ss.hasPermission('infra:config:create')")
|
||||
public CommonResult<Long> createConfig(@Valid @RequestBody InfConfigCreateReqVO reqVO) {
|
||||
return success(configService.createConfig(reqVO));
|
||||
}
|
||||
|
||||
@ApiOperation("导出参数配置")
|
||||
@GetMapping("/export")
|
||||
// @Log(title = "参数管理", businessType = BusinessType.EXPORT)
|
||||
// @PreAuthorize("@ss.hasPermi('infra:config:export')")
|
||||
public void exportSysConfig(HttpServletResponse response, @Validated InfConfigExportReqVO reqVO) throws IOException {
|
||||
List<InfConfigDO> list = configService.getConfigList(reqVO);
|
||||
// 拼接数据
|
||||
List<InfConfigExcelVO> excelDataList = InfConfigConvert.INSTANCE.convertList(list);
|
||||
// 输出
|
||||
ExcelUtils.write(response, "参数配置.xls", "配置列表",
|
||||
InfConfigExcelVO.class, excelDataList);
|
||||
@PutMapping("/update")
|
||||
@ApiOperation("修改参数配置")
|
||||
@PreAuthorize("@ss.hasPermission('infra:config:update')")
|
||||
public CommonResult<Boolean> updateConfig(@Valid @RequestBody InfConfigUpdateReqVO reqVO) {
|
||||
configService.updateConfig(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@ApiOperation("删除参数配置")
|
||||
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
|
||||
@PreAuthorize("@ss.hasPermission('infra:config:delete')")
|
||||
public CommonResult<Boolean> deleteConfig(@RequestParam("id") Long id) {
|
||||
configService.deleteConfig(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/get")
|
||||
@ApiOperation("获得参数配置")
|
||||
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
|
||||
@GetMapping(value = "/get")
|
||||
// @PreAuthorize("@ss.hasPermi('infra:config:query')")
|
||||
@PreAuthorize("@ss.hasPermission('infra:config:query')")
|
||||
public CommonResult<InfConfigRespVO> getConfig(@RequestParam("id") Long id) {
|
||||
return success(InfConfigConvert.INSTANCE.convert(configService.getConfig(id)));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/get-value-by-key")
|
||||
@ApiOperation(value = "根据参数键名查询参数值", notes = "敏感配置,不允许返回给前端")
|
||||
@ApiImplicitParam(name = "key", value = "参数键", required = true, example = "yunai.biz.username", dataTypeClass = String.class)
|
||||
@GetMapping(value = "/get-value-by-key")
|
||||
public CommonResult<String> getConfigKey(@RequestParam("key") String key) {
|
||||
InfConfigDO config = configService.getConfigByKey(key);
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
if (config.getSensitive()) {
|
||||
throw ServiceExceptionUtil.exception(CONFIG_GET_VALUE_ERROR_IF_SENSITIVE);
|
||||
throw exception(CONFIG_GET_VALUE_ERROR_IF_SENSITIVE);
|
||||
}
|
||||
return success(config.getValue());
|
||||
}
|
||||
|
||||
@ApiOperation("新增参数配置")
|
||||
@PostMapping("/create")
|
||||
// @PreAuthorize("@ss.hasPermi('infra:config:add')")
|
||||
// @Log(title = "参数管理", businessType = BusinessType.INSERT)
|
||||
// @RepeatSubmit
|
||||
public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) {
|
||||
return success(configService.createConfig(reqVO));
|
||||
@GetMapping("/page")
|
||||
@ApiOperation("获取参数配置分页")
|
||||
@PreAuthorize("@ss.hasPermission('infra:config:query')")
|
||||
public CommonResult<PageResult<InfConfigRespVO>> getConfigPage(@Valid InfConfigPageReqVO reqVO) {
|
||||
PageResult<InfConfigDO> page = configService.getConfigPage(reqVO);
|
||||
return success(InfConfigConvert.INSTANCE.convertPage(page));
|
||||
}
|
||||
|
||||
@ApiOperation("修改参数配置")
|
||||
@PutMapping("/update")
|
||||
// @PreAuthorize("@ss.hasPermi('infra:config:edit')")
|
||||
// @Log(title = "参数管理", businessType = BusinessType.UPDATE)
|
||||
public CommonResult<Boolean> edit(@Validated @RequestBody InfConfigUpdateReqVO reqVO) {
|
||||
configService.updateConfig(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@ApiOperation("删除参数配置")
|
||||
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
|
||||
@DeleteMapping("/delete")
|
||||
// @PreAuthorize("@ss.hasPermi('infra:config:remove')")
|
||||
// @Log(title = "参数管理", businessType = BusinessType.DELETE)
|
||||
public CommonResult<Boolean> deleteConfig(@RequestParam("id") Long id) {
|
||||
configService.deleteConfig(id);
|
||||
return success(true);
|
||||
@GetMapping("/export")
|
||||
@ApiOperation("导出参数配置")
|
||||
@PreAuthorize("@ss.hasPermission('infra:config:export')")
|
||||
@OperateLog(type = EXPORT)
|
||||
public void exportSysConfig(@Valid InfConfigExportReqVO reqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
List<InfConfigDO> list = configService.getConfigList(reqVO);
|
||||
// 拼接数据
|
||||
List<InfConfigExcelVO> datas = InfConfigConvert.INSTANCE.convertList(list);
|
||||
// 输出
|
||||
ExcelUtils.write(response, "参数配置.xls", "数据", InfConfigExcelVO.class, datas);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ public class InfConfigBaseVO {
|
||||
|
||||
@ApiModelProperty(value = "参数分组", required = true, example = "biz")
|
||||
@NotEmpty(message = "参数分组不能为空")
|
||||
@Size(max = 100, message = "参数名称不能超过50个字符")
|
||||
@Size(max = 50, message = "参数名称不能超过50个字符")
|
||||
private String group;
|
||||
|
||||
@ApiModelProperty(value = "参数名称", required = true, example = "数据库名")
|
||||
|
@@ -22,11 +22,11 @@ public class InfConfigExportReqVO {
|
||||
@ApiModelProperty(value = "参数类型", example = "1", notes = "参见 SysConfigTypeEnum 枚举")
|
||||
private Integer type;
|
||||
|
||||
@ApiModelProperty(value = "开始时间", example = "2020-10-24")
|
||||
@ApiModelProperty(value = "开始时间", example = "2020-10-24 00:00:00")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private Date beginTime;
|
||||
|
||||
@ApiModelProperty(value = "结束时间", example = "2020-10-24")
|
||||
@ApiModelProperty(value = "结束时间", example = "2020-10-24 23:59:59")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private Date endTime;
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -14,6 +15,7 @@ import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOU
|
||||
@ApiModel("参数配置分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class InfConfigPageReqVO extends PageParam {
|
||||
|
||||
@ApiModelProperty(value = "参数名称", example = "模糊匹配")
|
||||
@@ -25,11 +27,11 @@ public class InfConfigPageReqVO extends PageParam {
|
||||
@ApiModelProperty(value = "参数类型", example = "1", notes = "参见 SysConfigTypeEnum 枚举")
|
||||
private Integer type;
|
||||
|
||||
@ApiModelProperty(value = "开始时间", example = "2020-10-24")
|
||||
@ApiModelProperty(value = "开始时间", example = "2020-10-24 00:00:00")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private Date beginTime;
|
||||
|
||||
@ApiModelProperty(value = "结束时间", example = "2020-10-24")
|
||||
@ApiModelProperty(value = "结束时间", example = "2020-10-24 23:59:59")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private Date endTime;
|
||||
|
||||
|
@@ -4,12 +4,14 @@ import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@ApiModel("参数配置创建 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class InfConfigUpdateReqVO extends InfConfigBaseVO {
|
||||
|
||||
@ApiModelProperty(value = "参数配置序号", required = true, example = "1024")
|
||||
|
@@ -0,0 +1,150 @@
|
||||
package cn.iocoder.dashboard.modules.infra.controller.doc;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import cn.smallbun.screw.core.Configuration;
|
||||
import cn.smallbun.screw.core.engine.EngineConfig;
|
||||
import cn.smallbun.screw.core.engine.EngineFileType;
|
||||
import cn.smallbun.screw.core.engine.EngineTemplateType;
|
||||
import cn.smallbun.screw.core.execute.DocumentationExecute;
|
||||
import cn.smallbun.screw.core.process.ProcessConfig;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
@Api(tags = "数据库文档")
|
||||
@RestController
|
||||
@RequestMapping("/infra/db-doc")
|
||||
public class InfDbDocController {
|
||||
|
||||
@Resource
|
||||
private DataSourceProperties dataSourceProperties;
|
||||
|
||||
private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator
|
||||
+ "db-doc";
|
||||
private static final String DOC_FILE_NAME = "数据库文档";
|
||||
private static final String DOC_VERSION = "1.0.0";
|
||||
private static final String DOC_DESCRIPTION = "文档描述";
|
||||
|
||||
@GetMapping("/export-html")
|
||||
@ApiOperation("导出 html 格式的数据文档")
|
||||
@ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
|
||||
public void exportHtml(@RequestParam(defaultValue = "true") Boolean deleteFile,
|
||||
HttpServletResponse response) throws IOException {
|
||||
doExportFile(EngineFileType.HTML, deleteFile, response);
|
||||
}
|
||||
|
||||
@GetMapping("/export-word")
|
||||
@ApiOperation("导出 word 格式的数据文档")
|
||||
@ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
|
||||
public void exportWord(@RequestParam(defaultValue = "true") Boolean deleteFile,
|
||||
HttpServletResponse response) throws IOException {
|
||||
doExportFile(EngineFileType.WORD, deleteFile, response);
|
||||
}
|
||||
|
||||
@GetMapping("/export-markdown")
|
||||
@ApiOperation("导出 markdown 格式的数据文档")
|
||||
@ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
|
||||
public void exportMarkdown(@RequestParam(defaultValue = "true") Boolean deleteFile,
|
||||
HttpServletResponse response) throws IOException {
|
||||
doExportFile(EngineFileType.MD, deleteFile, response);
|
||||
}
|
||||
|
||||
private void doExportFile(EngineFileType fileOutputType, Boolean deleteFile,
|
||||
HttpServletResponse response) throws IOException {
|
||||
String docFileName = DOC_FILE_NAME + "_" + IdUtil.fastSimpleUUID();
|
||||
String filePath = doExportFile(fileOutputType, docFileName);
|
||||
String downloadFileName = DOC_FILE_NAME + fileOutputType.getFileSuffix(); //下载后的文件名
|
||||
try {
|
||||
// 读取,返回
|
||||
ServletUtils.writeAttachment(response, downloadFileName, FileUtil.readBytes(filePath));
|
||||
} finally {
|
||||
handleDeleteFile(deleteFile, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出文件,返回文件路径
|
||||
*
|
||||
* @param fileOutputType 文件类型
|
||||
* @param fileName 文件名, 无需 ".docx" 等文件后缀
|
||||
* @return 生成的文件所在路径
|
||||
*/
|
||||
private String doExportFile(EngineFileType fileOutputType, String fileName) {
|
||||
try (HikariDataSource dataSource = buildDataSource()) {
|
||||
// 创建 screw 的配置
|
||||
Configuration config = Configuration.builder()
|
||||
.version(DOC_VERSION) // 版本
|
||||
.description(DOC_DESCRIPTION) // 描述
|
||||
.dataSource(dataSource) // 数据源
|
||||
.engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置
|
||||
.produceConfig(buildProcessConfig()) // 处理配置
|
||||
.build();
|
||||
|
||||
// 执行 screw,生成数据库文档
|
||||
new DocumentationExecute(config).execute();
|
||||
|
||||
return FILE_OUTPUT_DIR + File.separator + fileName + fileOutputType.getFileSuffix();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeleteFile(Boolean deleteFile, String filePath) {
|
||||
if (!deleteFile) {
|
||||
return;
|
||||
}
|
||||
FileUtil.del(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据源
|
||||
*/
|
||||
// TODO 芋艿:screw 暂时不支持 druid,尴尬
|
||||
private HikariDataSource buildDataSource() {
|
||||
// 创建 HikariConfig 配置类
|
||||
HikariConfig hikariConfig = new HikariConfig();
|
||||
hikariConfig.setJdbcUrl(dataSourceProperties.getUrl());
|
||||
hikariConfig.setUsername(dataSourceProperties.getUsername());
|
||||
hikariConfig.setPassword(dataSourceProperties.getPassword());
|
||||
hikariConfig.addDataSourceProperty("useInformationSchema", "true"); // 设置可以获取 tables remarks 信息
|
||||
// 创建数据源
|
||||
return new HikariDataSource(hikariConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 screw 的引擎配置
|
||||
*/
|
||||
private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) {
|
||||
return EngineConfig.builder()
|
||||
.fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径
|
||||
.openOutputDir(false) // 打开目录
|
||||
.fileType(fileOutputType) // 文件类型
|
||||
.produceType(EngineTemplateType.freemarker) // 文件类型
|
||||
.fileName(docFileName) // 自定义文件名称
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 screw 的处理配置,一般可忽略
|
||||
* 指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置
|
||||
*/
|
||||
private static ProcessConfig buildProcessConfig() {
|
||||
return ProcessConfig.builder()
|
||||
.ignoreTablePrefix(Collections.singletonList("QRTZ_")) // 忽略表前缀
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
@@ -1,9 +1,13 @@
|
||||
package cn.iocoder.dashboard.modules.system.controller.common;
|
||||
package cn.iocoder.dashboard.modules.infra.controller.file;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.common.SysFileDO;
|
||||
import cn.iocoder.dashboard.modules.system.service.common.SysFileService;
|
||||
import cn.iocoder.dashboard.common.pojo.PageResult;
|
||||
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
|
||||
import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFileRespVO;
|
||||
import cn.iocoder.dashboard.modules.infra.convert.file.InfFileConvert;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
|
||||
import cn.iocoder.dashboard.modules.infra.service.file.InfFileService;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
@@ -11,40 +15,53 @@ import io.swagger.annotations.ApiImplicitParams;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.Valid;
|
||||
import java.io.IOException;
|
||||
|
||||
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
|
||||
|
||||
@Api(tags = "文件 API")
|
||||
@Api(tags = "文件存储")
|
||||
@RestController
|
||||
@RequestMapping("/system/file")
|
||||
@RequestMapping("/infra/file")
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class SysFileController {
|
||||
public class InfFileController {
|
||||
|
||||
@Resource
|
||||
private SysFileService fileService;
|
||||
private InfFileService fileService;
|
||||
|
||||
@PostMapping("/upload")
|
||||
@ApiOperation("上传文件")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
|
||||
@ApiImplicitParam(name = "path", value = "文件路径", required = true, example = "yudaoyuanma.png", dataTypeClass = Long.class)
|
||||
@ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
|
||||
@ApiImplicitParam(name = "path", value = "文件路径", required = false, example = "yudaoyuanma.png", dataTypeClass = String.class)
|
||||
})
|
||||
@PostMapping("/upload")
|
||||
public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("path") String path) throws IOException {
|
||||
return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@ApiOperation("删除文件")
|
||||
@ApiImplicitParam(name = "id", value = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('infra:file:delete')")
|
||||
public CommonResult<Boolean> deleteFile(@RequestParam("id") String id) {
|
||||
fileService.deleteFile(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get/{path}")
|
||||
@ApiOperation("下载文件")
|
||||
@ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class)
|
||||
@GetMapping("/get/{path}")
|
||||
public void getFile(HttpServletResponse response, @PathVariable("path") String path) throws IOException {
|
||||
SysFileDO file = fileService.getFile(path);
|
||||
InfFileDO file = fileService.getFile(path);
|
||||
if (file == null) {
|
||||
log.warn("[getFile][path({}) 文件不存在]", path);
|
||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||
@@ -53,4 +70,12 @@ public class SysFileController {
|
||||
ServletUtils.writeAttachment(response, path, file.getContent());
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@ApiOperation("获得文件分页")
|
||||
@PreAuthorize("@ss.hasPermission('infra:file:query')")
|
||||
public CommonResult<PageResult<InfFileRespVO>> getFilePage(@Valid InfFilePageReqVO pageVO) {
|
||||
PageResult<InfFileDO> pageResult = fileService.getFilePage(pageVO);
|
||||
return success(InfFileConvert.INSTANCE.convertPage(pageResult));
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user