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:
YunaiV
2021-03-14 18:42:52 +08:00
554 changed files with 21924 additions and 14563 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 表的配置

View File

@@ -1 +1 @@
<http://www.iocoder.cn/Spring-Boot/Async-Job/?dashboard>
<http://www.iocoder.cn/Spring-Boot/Async-Job/?yudao>

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 代码生成器
*/
package cn.iocoder.dashboard.framework.codegen;

View File

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

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/dynamic-datasource/?yudao>

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao>

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/**
* 字典数据模块
* 字典数据模块,提供 {@link cn.iocoder.dashboard.framework.dict.core.util.DictUtils} 工具类
*
* 通过将字典缓存在内存中,保证性能
*/

View File

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

View File

@@ -1 +0,0 @@
package cn.iocoder.dashboard.framework.excel.core;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 数据结构
}

View File

@@ -0,0 +1,4 @@
/**
* 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目
*/
package cn.iocoder.dashboard.framework.lock4j;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 使用 Spring Boot Admin 实现简单的监控平台
*/
package cn.iocoder.dashboard.framework.monitor;

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Admin/?yudao>

View File

@@ -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(); // 自动填充参数类
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1,4 @@
/**
* 使用 MyBatis Plus 提升使用 MyBatis 的开发效率
*/
package cn.iocoder.dashboard.framework.mybatis;

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao>

View File

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

View File

@@ -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, // 每次重试间隔
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Job/?yudao>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
/**
* 采用 Spring Data Redis 操作 Redis底层使用 Redisson 作为客户端
*/
package cn.iocoder.dashboard.framework.redis;

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Redis/?yudao>

View File

@@ -0,0 +1,9 @@
/**
* 使用 Resilience4j 组件,实现服务保障,包括:
* 1. 熔断器
* 2. 限流器
* 3. 舱壁隔离
* 4. 重试
* 5. 限时器
*/
package cn.iocoder.dashboard.framework.resilience4j;

View File

@@ -0,0 +1 @@
<https://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 组件需要的功能

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
<http://www.iocoder.cn/Spring-Boot/Swagger/?dashboard>
<http://www.iocoder.cn/Spring-Boot/Swagger/?yudao>

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao>

View File

@@ -0,0 +1,4 @@
/**
* 使用 Hibernate Validator 实现参数校验
*/
package cn.iocoder.dashboard.framework.validator;

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Validation/?yudao>

View File

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

View File

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

View File

@@ -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 配置属性类
}

View File

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

View File

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

View File

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

View File

@@ -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避免 " 被转移成 &quot; 字符
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);
}
}

View File

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

View File

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

View File

@@ -1 +0,0 @@
package cn.iocoder.dashboard.framework.web.core;

View File

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

View File

@@ -1 +1 @@
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?dashboard>
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?yudao>

View File

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

View File

@@ -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 = "数据库名")

View File

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

View File

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

View File

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

View File

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

View File

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