diff --git a/derby.log b/derby.log index e0bcada..c94eab2 100644 --- a/derby.log +++ b/derby.log @@ -1,12 +1,12 @@ ---------------------------------------------------------------- -Sun Sep 01 21:38:15 CST 2024: -Booting Derby version The Apache Software Foundation - Apache Derby - 10.14.2.0 - (1828579): instance a816c00e-0191-adce-b9d3-000003b04a40 -on database directory memory:D:\Codes\remi\remi-cloud\4f82a0bb-9d60-4d6f-b6a3-e6480d5e589c with class loader jdk.internal.loader.ClassLoaders$AppClassLoader@60e53b93 -Loaded from file:/D:/Maven/sdt-repository/org/apache/derby/derby/10.14.2.0/derby-10.14.2.0.jar +Wed Sep 11 17:08:24 CST 2024: +Booting Derby version The Apache Software Foundation - Apache Derby - 10.14.2.0 - (1828579): instance a816c00e-0191-e057-45b8-000002512970 +on database directory memory:D:\Codes\remi-top\remi-cloud\1ec76c96-9207-4b71-b497-ffec1d63b532 with class loader jdk.internal.loader.ClassLoaders$AppClassLoader@5e481248 +Loaded from file:/D:/Maven/remi-top-repo/org/apache/derby/derby/10.14.2.0/derby-10.14.2.0.jar java.vendor=Amazon.com Inc. -java.runtime.version=17.0.11+9-LTS -user.dir=D:\Codes\remi\remi-cloud -os.name=Windows 11 +java.runtime.version=17.0.12+7-LTS +user.dir=D:\Codes\remi-top\remi-cloud +os.name=Windows 10 os.arch=amd64 os.version=10.0 derby.system.home=null diff --git a/docs/config/nacos/cloud-dev.yaml b/docs/config/nacos/remi-cloud-dev.yaml similarity index 100% rename from docs/config/nacos/cloud-dev.yaml rename to docs/config/nacos/remi-cloud-dev.yaml diff --git a/docs/config/nacos/sdt-iam-web-dev.yaml b/docs/config/nacos/remi-sys-web-dev.yaml similarity index 100% rename from docs/config/nacos/sdt-iam-web-dev.yaml rename to docs/config/nacos/remi-sys-web-dev.yaml diff --git a/pom.xml b/pom.xml index ae947dd..0f78ac5 100644 --- a/pom.xml +++ b/pom.xml @@ -4,9 +4,8 @@ 4.0.0 ai.remi.top.cloud remi-cloud - 3.1.0 + 3.1.1 pom - https://github.com/remi/remi-cloud remi-cloud Remi Cloud @@ -19,7 +18,7 @@ - 3.1.0 + 3.1.1 17 ${java.version} @@ -31,71 +30,59 @@ 2.7.18 2021.0.9 2021.0.6.0 - - 1.17.0 - - 3.2.2 - - 1.10.0 - + + 1.12.0 2.16.1 - - 3.14.0 - - 1.3.1 - - 3.10.0 - - 1.9.0 - + 3.17.0 + 1.17.1 + 1.3.4 + 3.2.2 + 3.11.1 1.5 - - 1.0.10 - - 2.1.3 - - 1.2.9 - - 3.1.1 - - 6.2.5.Final - - 1.21 - - 2.7.0 - + 5.8.4 - - 31.1-jre - + + 2.7.0 + 7.13.1 + 3.10.2 + 8.5.12 + 2.26.3 + 5.6.213 + + 4.0.2 + + 2.4.1 + + 1.0.10 + 1.21 + + 2.1.3 + + 1.9.0 + 8.0.1.Final + + 2.7.0 + 2.9.0 - 2.0.6 - 2.13.3 - - 0.12.6 - + + 33.3.0-jre 4.0.1 - 2.3.1 - 3.3.0 - + 4.5.0 - 2.6.0 - + 1.18.34 - 1.5.2.Final - 0.2.0 - - 3.5.2 - + + 1.2.9 2.2.2 - + 3.5.2 + 8.0.33 19.7.0.0 7.4.1.jre8 @@ -103,26 +90,20 @@ 42.6.0 2.2.224 4.10.2 - + 4.5.13 - - 8.5.12 - 4.12.0 1.7 - - 2.4.1 - + 8.13.0 - 1.7.32 - 1.3.14 + remi-comm remi-nacos remi-sys remi-demo @@ -160,82 +141,82 @@ + ai.remi.top.cloud remi-comm - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-core - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-minio - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-redis - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-jdbc - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-domain - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-exception - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-feign - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-job - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-log - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-swagger - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-thread - ai.remi.top.comm ${remi.version} + ai.remi.top.cloud remi-comm-util - ai.remi.top.comm ${remi.version} - ai.remi.top.comm + ai.remi.top.cloud remi-comm-auth ${remi.version} - ai.remi.top.comm + ai.remi.top.cloud remi-comm-safe ${remi.version} - ai.remi.top.comm + ai.remi.top.cloud remi-comm-web ${remi.version} @@ -396,12 +377,12 @@ commons-fileupload ${commons-fileupload.version} + commons-validator commons-validator ${commons-validator.version} - hibernate-validator org.hibernate.validator @@ -565,22 +546,6 @@ velocity ${velocity.version} - - - io.jsonwebtoken - jjwt-api - ${java-jwt.version} - - - io.jsonwebtoken - jjwt-impl - ${java-jwt.version} - - - io.jsonwebtoken - jjwt-jackson - ${java-jwt.version} - jaxb-api diff --git a/remi-comm/README.md b/remi-comm/README.md new file mode 100644 index 0000000..0e2f5c4 --- /dev/null +++ b/remi-comm/README.md @@ -0,0 +1,60 @@ +

Remi Comm

+ +## 平台简介 + +Remi Comm 即 Remi Dev Tools 启迪快速开发平台所依赖的公共依赖模块。 + +## 项目文档 + +https://remi.yuque.com/r/organizations/homepage + +## 体验地址 + +https://remi.run + +RemiAdmin/123456 + +## 项目源码 + +### Gitee + +https://gitee.com/remi-top + +### GitTok + +https://gittok.com/remi-top + +### Gitlab + +https://gitlab.com/remi-top + +### Github + +https://github.com/remi-top + +## 系统模块 + +~~~ +remi-comm +├── remi-comm-auth // 权限控制 +├── remi-comm-core // 核心模块 +├── remi-comm-domain // 领域模型 +├── remi-comm-jdbc // 数据配置 +├── remi-comm-exception // 异常模块 +├── remi-comm-feign // 通信模块 +├── remi-comm-job // 任务模块 +├── remi-comm-minio // MinIo 文件存储 +├── remi-comm-activemq // ActiveMQ +├── remi-comm-rabbitmq // RabbitMQ +├── remi-comm-rocketmq // RocketMQ +├── remi-comm-redis // 缓存服务 +├── remi-comm-util // 工具类 +├── remi-comm-thread // 线程池 +├── remi-comm-swagger // 系统接口 +├── remi-comm-netty // Netty +├── remi-comm-safe // 安全控制 +├── remi-comm-web // Web +├── remi-comm-webservice // WebService +├── remi-comm-websocket // WebSocket +├── pom.xml // 公共依赖 +~~~ diff --git a/remi-comm/pom.xml b/remi-comm/pom.xml new file mode 100644 index 0000000..0102c96 --- /dev/null +++ b/remi-comm/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + remi-comm + remi-comm + pom + 瑞米平台-公共依赖 + + + ai.remi.top.cloud + remi-cloud + 3.1.1 + + + + 17 + ${java.version} + ${java.version} + UTF-8 + + + + + remi-comm-core + remi-comm-util + remi-comm-domain + remi-comm-jdbc + remi-comm-thread + remi-comm-job + remi-comm-log + remi-comm-minio + remi-comm-redis + remi-comm-feign + remi-comm-email + remi-comm-exception + remi-comm-auth + remi-comm-safe + remi-comm-swagger + remi-comm-web + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + \ No newline at end of file diff --git a/remi-comm/remi-comm-auth/pom.xml b/remi-comm/remi-comm-auth/pom.xml new file mode 100644 index 0000000..6e859ba --- /dev/null +++ b/remi-comm/remi-comm-auth/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + remi-comm-auth + remi-comm-auth + remi-comm-auth + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + ai.remi.top.cloud + remi-comm-redis + + + ai.remi.top.cloud + remi-comm-exception + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + org.projectlombok + lombok + + + diff --git a/remi-comm/remi-comm-auth/src/main/java/ai/remi/comm/auth/annotation/RbacPermission.java b/remi-comm/remi-comm-auth/src/main/java/ai/remi/comm/auth/annotation/RbacPermission.java new file mode 100644 index 0000000..4ab178c --- /dev/null +++ b/remi-comm/remi-comm-auth/src/main/java/ai/remi/comm/auth/annotation/RbacPermission.java @@ -0,0 +1,34 @@ +package ai.remi.comm.auth.annotation; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + * @desc 权限校验 + */ +@Inherited +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface RbacPermission { + + /** + * 角色 + * + * @return + */ + String roles() default ""; + + /** + * 权限 + * + * @return + */ + String permissions() default ""; +} diff --git a/remi-comm/remi-comm-auth/src/main/java/ai/remi/comm/auth/aspect/RbacPermissionAspect.java b/remi-comm/remi-comm-auth/src/main/java/ai/remi/comm/auth/aspect/RbacPermissionAspect.java new file mode 100644 index 0000000..c6714eb --- /dev/null +++ b/remi-comm/remi-comm-auth/src/main/java/ai/remi/comm/auth/aspect/RbacPermissionAspect.java @@ -0,0 +1,224 @@ +package ai.remi.comm.auth.aspect; + + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import ai.remi.comm.auth.annotation.RbacPermission; +import ai.remi.comm.core.constant.Constants; +import ai.remi.comm.core.constant.HeaderConstant; +import ai.remi.comm.exception.custom.BusinessException; +import ai.remi.comm.redis.service.RedisService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.*; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + * @desc + */ +@Aspect +@Component +@Order(10) +@Slf4j +public class RbacPermissionAspect { + private static final Logger logger = LoggerFactory.getLogger(RbacPermissionAspect.class); + + private final ThreadLocal> threadLocal = new ThreadLocal>(); + + private final String ROLE_MENU = "tdcd-cloud-mps:role-menu:{}"; + + @Resource + private RedisService redisService; + + @Pointcut("@annotation(ai.remi.comm.auth.annotation.RbacPermission)") + public void rbacPermissionPointCut() { + } + + /** + * 环绕通知 + * 方式一:传入注解,获取注解中的参数 + * + * @Around("logRecordPointCut()&&@annotation(logRecord)") public void doAround(ProceedingJoinPoint joinPoint, LogRecord logRecord) {} + * 方式二:通过切点获取注解实体 + */ + @Around("rbacPermissionPointCut()") + //@Around("rbacPermissionPointCut()&&@annotation(RbacPermission)") + //public void doAround(ProceedingJoinPoint joinPoint, RbacPermission rbacPermission) { + public Object doAround(ProceedingJoinPoint joinPoint) { + // 接收到请求 + RequestAttributes ra = RequestContextHolder.getRequestAttributes(); + ServletRequestAttributes sra = (ServletRequestAttributes) ra; + HttpServletRequest request = sra.getRequest(); + // 记录请求内容,threadInfo存储所有内容 + Map threadInfo = new HashMap<>(); + //该方法用于获取客户端发出请求时的完整URL,包括协议、服务器名、端口号、资源路径等信息,但不包括后面的查询参数部分。 + // 注意,getRequestRUL()方法返回的时StringBuffer类型,而不是String类型。 + threadInfo.put("url", request.getRequestURL()); + //该方法用于获取请求行中资源名称部分,即位于URL的主机和端口之后、参数部分之前的部分 + threadInfo.put("uri", request.getRequestURI()); + //该方法用于获取请求的协议名,例如http、https或ftp + threadInfo.put("scheme", request.getScheme()); + //该方法用于获取HTTP请求消息中的请求方式(如GET、POST等) + threadInfo.put("method", request.getMethod()); + logger.info("请求方式:{}", threadInfo.get("method")); + //该方法用于获取请求客户端的IP地址,其格式类似于"192.168.0.1" + threadInfo.put("ip", request.getRemoteAddr()); + logger.info("请求ip:{}", threadInfo.get("ip")); + //该方法用于获取Content-Type头字段的值,结果为String类型 + threadInfo.put("contentType", request.getContentType()); + //包含了一个特征字符串,用来让网络协议的对端来识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。 + threadInfo.put("userAgent", request.getHeader("User-Agent")); + //threadInfo.put("businessNo", request.getHeader("businessNo")); + threadLocal.set(threadInfo); + //获取方法签名 + MethodSignature signature = getSignature(joinPoint); + //获取注解对象 + RbacPermission rbacPermission = getAnnotation(joinPoint, RbacPermission.class); + //获取注解方法 + Method method = getMethod(joinPoint); + // 获取连接点所在类名 + String classname = joinPoint.getTarget().getClass().getSimpleName(); + // 获取连接点所在方法名称 + String methodName = method.getName(); + String functionName = String.format("【%s.%s()】", classname, methodName); + threadInfo.put("functionName", functionName); + logger.info("请求类方法:{}", threadInfo.get("functionName")); + // 下面两个数组中,参数值和参数名的个数和位置是一一对应的。 + // 参数名 + String[] argNames = signature.getParameterNames(); + // 参数值 + Object[] args = joinPoint.getArgs(); + StringBuilder functionArgs = new StringBuilder(); + //不为空时便利组装 + if (Objects.nonNull(argNames) && argNames.length > 0 && args.length > 0) { + for (int i = 0; i < args.length; i++) { + functionArgs.append(argNames[i] + ":" + args[i] + ";"); + } + } + threadInfo.put("functionArgs", functionArgs.toString()); + logger.info("请求参数:{}", threadInfo.get("functionArgs")); + +// threadInfo.put("accessToken", request.getHeader("accessToken")); + threadInfo.put("accessToken", request.getHeader(HeaderConstant.X_ACCESS_TOKEN)); + logger.info("鉴权令牌:{}", threadInfo.get("accessToken")); + + // 权限校验 + if (method.isAnnotationPresent(RbacPermission.class)) { + String roles = rbacPermission.roles(); + String permissions = rbacPermission.permissions(); + + // 根据鉴权令牌去redis查询到用户信息、角色信息、权限信息 + //Map map = redisService.hmget(RedisDemoEnum.ACCESS_TOKEN.getKey(String.valueOf(threadInfo.get("accessToken")))); + Map map = redisService.hmget(String.valueOf(threadInfo.get("accessToken"))); + if (null == map || map.size() == 0) { + throw new BusinessException(Constants.LOGIN_NO_PERMISSION, "accessToken expire! please login again!"); + } + String roleCode = String.valueOf(map.get("roleCode")); + List roleCodes = Arrays.asList(roleCode.split(",")); + // 校验角色 + if (StringUtils.isNotBlank(roles)) { + // 校验用户角色 +// JSONObject jsonObject = JSONObject.parseObject(String.valueOf(map.get("user"))); +// if (!Arrays.asList(roles.split(",")).contains(jsonObject.get("roleCode"))) { +// throw new BusinessException(Constants.CODE_403, "当前角色权限不足"); +// } + for (String authRoleCode : Arrays.asList(roles.split(","))) { + if (!roleCodes.contains(authRoleCode)){ + throw new BusinessException(Constants.CODE_403, "the role no Permission!"); + }else { + break; + } + } +// if (!Arrays.asList(roles.split(",")).contains(roleCode)) { +// throw new BusinessException(Constants.CODE_403, "the role no Permission!"); +// } + } + // 校验权限 + if (StringUtils.isNotBlank(permissions)) { + // 组装用户权限 + StringBuilder userPermissions = new StringBuilder(); + +// String menuString = JSONObject.toJSONString(map.get("menus")); +// List menus = JSONObject.parseArray(menuString); + List menus = new ArrayList<>(); + for (String code : roleCodes) { + String data = null; + try { + data = redisService.getString(StrUtil.format(ROLE_MENU, code)); + } catch (Exception e) { + log.error("获取用户权限异常:{}", e.getMessage()); + throw new BusinessException(Constants.CODE_403, "get user Permission error!"); + } + List menuList = JSONObject.parseArray(data, JSONObject.class); + menus.addAll(menuList); + } + + for (int i = 0; i < menus.size(); i++) { + if ("2".equals(menus.get(i).get("menuType"))) { + userPermissions.append(menus.get(i).get("menuPermission")); + if (i != menus.size() - 1) { + userPermissions.append(","); + } + } + } + // 校验用户权限 + if (!Arrays.asList(userPermissions.toString().split(",")).containsAll(Arrays.asList(permissions.split(",")))) { + throw new BusinessException(Constants.CODE_403, "the user no Permission"); + } + } + } + + //执行到这里开始走进来的方法体(必须声明) + Object returnValue; + try { + returnValue = joinPoint.proceed(args); + } catch (Throwable throwable) { + throwable.printStackTrace(); + throw new BusinessException(((BusinessException) throwable).getCode(), (throwable).getMessage()); + } + return returnValue; + } + + + private MethodSignature getSignature(JoinPoint joinPoint) { + //通过连接点获取方法签名 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + return signature; + } + + private Method getMethod(JoinPoint joinPoint) { + //通过连接点获取方法签名 + MethodSignature signature = getSignature(joinPoint); + //获取注解服务的方法 + Method method = signature.getMethod(); + return method; + } + + private T getAnnotation(JoinPoint joinPoint, Class annotationClass) { + Method method = getMethod(joinPoint); + //获取注解类 + T annotation = method.getAnnotation(annotationClass); + return annotation; + } + +} diff --git a/remi-comm/remi-comm-core/README.md b/remi-comm/remi-comm-core/README.md new file mode 100644 index 0000000..41785b0 --- /dev/null +++ b/remi-comm/remi-comm-core/README.md @@ -0,0 +1,83 @@ +## 方式1: 实现ApplicationContextInitializer接口 +实现接口后,将此类注入到Spring容器中,有两种方式。当然也可以直接调用静态方法 + +第一种: 在此类上加`@Component`注解 + +第二种:在 `resources/META-INF/spring.factories`文件中添加以下配置: org.springframework.context.ApplicationContextInitializer=CustomerApplicationContextInitializer的路径 + +实现ApplicationContextInitializer的源码示例: +```java + +public class CustomApplicationContextInitializer implements ApplicationContextInitializer { + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + SpringBeanUtils.setApplicationContext(applicationContext); + } +} + +``` + + +## 方式2. 实现ApplicationListener接口 +实现接口后,将此类注入到Spring容器中,有两种方式。当然也可以直接调用静态方法 + +第一种: 在此类上加`@Component`注解 + +第二种:在 `resources/META-INF/spring.factories`文件中添加以下配置: org.springframework.context.ApplicationContextInitializer=CustomerApplicationListener的路径 + +实现ApplicationListener的源码示例: +```java + +public class CustomApplicationListener implements ApplicationListener { + @Override + public void onApplicationEvent(ApplicationContextEvent event) { + SpringBeanUtils.setApplicationContext(event.getApplicationContext()); + } +} + +``` + +方式3. 在启动类main方法中设置 +```java + +@SpringBootApplication +@EnableSwagger2 +public class Application { + public static void main(String[] args) { + ConfigurableApplicationContext applicationContext = SpringApplication.run(WangMikeSpringApplication.class, args); + SpringBeanUtils.setApplicationContext(applicationContext); + } +} + +``` + +方式4. 实现ApplicationContextAware接口 +```java + +@Component +public class SpringBeanUtils implements ApplicationContextAware { + private static ApplicationContext applicationContext; + public void setApplicationContext(ApplicationContext applicationContext){ + SpringBeanUtils.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext( + return applicationContext; + } +} + +``` +方式5. 直接通过@Resource注解注入 +```java + +@Component +@Order(value = 1) +@Slf4j +public class AppStartRunner implements org.springframework.boot.ApplicationRunner { + protected static Logger logger = LoggerFactory.getLogger(AppStartRunner.class); + + @Resource + ApplicationContext applicationContext; +} + +``` \ No newline at end of file diff --git a/remi-comm/remi-comm-core/pom.xml b/remi-comm/remi-comm-core/pom.xml new file mode 100644 index 0000000..aa4de50 --- /dev/null +++ b/remi-comm/remi-comm-core/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + remi-comm-core + remi-comm-core + remi-comm-core + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + org.springframework.boot + spring-boot-autoconfigure + + + + guava + com.google.guava + + + + + device-detector + io.github.mngsk + + + + + org.projectlombok + lombok + + + + + diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/config/AuthFilterConfig.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/config/AuthFilterConfig.java new file mode 100644 index 0000000..7bee73f --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/config/AuthFilterConfig.java @@ -0,0 +1,44 @@ +package ai.remi.comm.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc AuthFilterConfig + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "filter.config") +public class AuthFilterConfig { + + /** + * 公共忽略的url,网关、公共web的AuthFilter 都会忽略---- 配置在application-dev.yaml + */ + private List commonIgnoreUrl= new ArrayList<>(); + + /** + * 网关忽略的ur----配置在ebg-shop-gateway-dev.yaml + */ + private List gatewayIgnoreUrl=new ArrayList<>(); + + /** + *各个服务自定义忽略的url---配置在自己的服务 + */ + private List customIgnoreUrl= new ArrayList<>(); + + /** + * 是否验证权限----配置在idaas服务 + */ + private Boolean verifyPermission=true; + /** + * 只验证token不验证 权限的 + */ + private List onlyVerifyToken=new ArrayList<>(); + +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/ApprovalStatusConstants.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/ApprovalStatusConstants.java new file mode 100644 index 0000000..cef79b2 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/ApprovalStatusConstants.java @@ -0,0 +1,34 @@ +package ai.remi.comm.core.constant; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 流程审批常量信息 + */ +public class ApprovalStatusConstants { + + /** + * 待审批 + */ + public static final Integer DSP = 0; + + /** + * 审批中 + */ + public static final Integer SPZ = 1; + + /** + * 审批完成 + */ + public static final Integer SPWC = 2; + + /** + * 已撤销 + */ + public static final Integer YCX = 3; + + /** + * 已驳回 + */ + public static final Integer YBH = 4; +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/Constants.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/Constants.java new file mode 100644 index 0000000..ae21874 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/Constants.java @@ -0,0 +1,171 @@ +package ai.remi.comm.core.constant; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 通用常量信息 + */ +public class Constants { + + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 成功标记 + */ + public static final String SUCCESS = "000000"; + + /** + * 失败标记 + */ + public static final String ERROR = "111111"; + + /** + * 登录成功状态 + */ + public static final String LOGIN_SUCCESS_STATUS = "0"; + + /** + * 登录失败状态 + */ + public static final String LOGIN_FAIL_STATUS = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 验证码有效期(分钟) + */ + public static final long CAPTCHA_EXPIRATION = 2; + + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = {"ai.remi"}; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "ai.remi.comm.core.utils.file"}; + + /** + * 系统市公司编号 + */ + public static final String SYS_COMPANY_ID = "1667075454562709506"; + + /** + * 登录无权限 + */ + public static final String LOGIN_NO_PERMISSION = "401"; + + /** + * 无权限访问资源 + */ + public static final String CODE_401 = "401"; + + /** + * 当前角色权限不足 + */ + public static final String CODE_403 = "403"; + + /** + * SRS校验数据错误码 + */ + public static final String SRS_DATA_CHECK_ERROR = "A02001"; + + /** + * 功能检验错误码 + */ + public static final String COMM_CHECK_ERROR = "222222"; +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/FilterIgnoreConstant.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/FilterIgnoreConstant.java new file mode 100644 index 0000000..68b8f42 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/FilterIgnoreConstant.java @@ -0,0 +1,24 @@ +package ai.remi.comm.core.constant; + + +import com.google.common.collect.Sets; + +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 过滤器忽略常量 + */ +public class FilterIgnoreConstant { + /** + * 默认全部忽略的url 一般用于跟服务无关的 + */ + public static final Set FILTER_COMMON_IGNORE_URL = Sets.newHashSet("/**/swagger**/**", "/**/webjars/**", + "/**/v2/api-docs", "/**/v3/api-docs", "/**/error", "/**/doc.html", "/doc.html", "/**/favicon.ico"); + + /** + * 全局拦截器忽略的服务 + */ + public static final Set AUTH_FILTER_IGNORE_SERVICE_NAME = Sets.newHashSet("remi-ids-web","remi-ams-web","remi-mps-web","remi-sso-web","remi-dfe-web","remi-wfe-web","remi-bie-web","remi-wpe-web","remi-job-web","remi-lce-web"); +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/HeaderConstant.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/HeaderConstant.java new file mode 100644 index 0000000..58cee78 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/HeaderConstant.java @@ -0,0 +1,81 @@ +package ai.remi.comm.core.constant; + +import ai.remi.comm.core.enums.IdentityTypeEnum; +import ai.remi.comm.core.enums.PermissionScopeEnum; +import ai.remi.comm.core.enums.ServiceTypeEnum; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 全局请求头常量 + */ +public class HeaderConstant { + + /** + * 部门租户id + */ + public static final String X_DEPT_TENANT_ID = "X-DEPT-TENANT-ID"; + + /** + * 公司租户id + */ + public static final String X_COMPANY_TENANT_ID = "X-COMPANY-TENANT-ID"; + + /** + * 当前登陆人唯一id + */ + public static final String X_UNIQUE_ID = "X-UNIQUE-ID"; + + /** + * 用户系统语言 + */ + public static final String X_USER_LANGUAGE = "X-USER-LANGUAGE"; + + /** + * 用户设备唯一标识 + */ + public static final String X_DISTINCT_ID = "X-DISTINCT-ID"; + + /** + * 登录token + */ + public static final String X_ACCESS_TOKEN = "X-ACCESS-TOKEN"; + + /** + * 服务类型 + * + * @see ServiceTypeEnum + */ + public static final String X_SERVICE_TYPE = "X-SERVICE-TYPE"; + + /** + * 身份类型 公司用户 访客用户 瑞米用户 + * + * @see IdentityTypeEnum + */ + public static final String X_IDENTITY_TYPE = "X-IDENTITY-TYPE"; + + + /** + * 此请求头为用户请求过来的为集团还是公司权限 + * + * @see PermissionScopeEnum + */ + public static final String X_PERMISSION_SCOPE = "X-PERMISSION-SCOPE"; + + /** + * 当权限类型为集团类型时,此请求头包含的为这个用户管理的所有公司id {@link PermissionScopeEnum} + */ + public static final String X_COMPANY_IDS = "X-COMPANY-IDS"; + + /** + * 当权限类型为公司类型时,此请求头包含的为这个用户管理的所有部门id {@link PermissionScopeEnum} + */ + public static final String X_DEPT_IDS = "X-DEPT-IDS"; + + /** + * 请求/响应头报文类型 + */ + public static final String CONTENT_TYPE = "Content-Type"; + +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/HttpStatus.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/HttpStatus.java new file mode 100644 index 0000000..4845870 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/HttpStatus.java @@ -0,0 +1,89 @@ +package ai.remi.comm.core.constant; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc HTTP状态码 + */ +public class HttpStatus { + + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/OAuth2Constants.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/OAuth2Constants.java new file mode 100644 index 0000000..736542f --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/OAuth2Constants.java @@ -0,0 +1,36 @@ +package ai.remi.comm.core.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc OAuth2异常常量 + */ +public class OAuth2Constants { + + /** + * 认证异常 + */ + @Getter + @AllArgsConstructor + public enum Error { + USERNAME_NOT_FOUND("username_not_found", "用户名未找到", ""), + BAD_CREDENTIALS("bad_credentials", "错误凭证", "AbstractUserDetailsAuthenticationProvider.badCredentials : Bad credentials"), + USER_LOCKED("user_locked", "用户被锁", "AbstractUserDetailsAuthenticationProvider.locked : User account is locked"), + USER_DISABLE("user_disable", "用户禁用", "AbstractUserDetailsAuthenticationProvider.disabled : User is disabled"), + USER_EXPIRED("user_expired", "用户过期", "AbstractUserDetailsAuthenticationProvider.expired : User account has expired"), + CREDENTIALS_EXPIRED("credentials_expired", "证书过期", "AbstractUserDetailsAuthenticationProvider.credentialsExpired : User credentials have expired"), + INVALID_SCOPE("invalid_scope", "无效作用域", "AbstractAccessDecisionManager.accessDenied : invalid_scope"), + SCOPE_IS_EMPTY("scope_is_empty", "scope 为空异常", ""), + TOKEN_MISSING("token_missing", "令牌不存在", ""), + UN_KNOW_LOGIN_ERROR("un_know_login_error", "未知的登录异常", ""), + INVALID_BEARER_TOKEN("invalid_bearer_token", "不合法的Token", ""); + + private final String code; + private final String info; + private final String msg; + + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/TokenConstants.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/TokenConstants.java new file mode 100644 index 0000000..89a59ef --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/TokenConstants.java @@ -0,0 +1,35 @@ +package ai.remi.comm.core.constant; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc Token的Key通用常量 + */ +public class TokenConstants { + + /** + * 令牌自定义标识 + */ + public static final String AUTHENTICATION = "Authorization"; + + /** + * 补充令牌自定义标识 + */ + public static final String SUPPLY_AUTHORIZATION = "X-Access-Token"; + + /** + * 令牌前缀 + */ + public static final String PREFIX = "Remi"; + + /** + * 回调地址 + */ + public static final String REDIRECT_URL = "redirect_url"; + + /** + * 令牌秘钥 + */ + public final static String SECRET = "ce6e6cc15d0349b58f9d8c43e5f328ce7f329029434242c6aaaec5a06b930a4a4d73cf1a7e7b48eb80fad6a916ded78d614cc2e63779489d9a2aaaafd674c5515e708ba5869a4dcfa3e66799"; + +} \ No newline at end of file diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/UserConstants.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/UserConstants.java new file mode 100644 index 0000000..3fdd29a --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/constant/UserConstants.java @@ -0,0 +1,115 @@ +package ai.remi.comm.core.constant; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 用户常量信息 + */ +public class UserConstants { + + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** + * 正常状态 + */ + public static final String NORMAL = "0"; + + /** + * 异常状态 + */ + public static final String EXCEPTION = "1"; + + /** + * 用户封禁状态 + */ + public static final String USER_DISABLE = "1"; + + /** + * 角色封禁状态 + */ + public static final String ROLE_DISABLE = "1"; + + /** + * 部门正常状态 + */ + public static final String DEPT_NORMAL = "0"; + + /** + * 部门停用状态 + */ + public static final String DEPT_DISABLE = "1"; + + /** + * 字典正常状态 + */ + public static final String DICT_NORMAL = "0"; + + /** + * 是否为系统默认(是) + */ + public static final String YES = "Y"; + + /** + * 是否菜单外链(是) + */ + public static final String YES_FRAME = "0"; + + /** + * 是否菜单外链(否) + */ + public static final String NO_FRAME = "1"; + + /** + * 菜单类型(目录) + */ + public static final String TYPE_DIR = "M"; + + /** + * 菜单类型(菜单) + */ + public static final String TYPE_MENU = "C"; + + /** + * 菜单类型(按钮) + */ + public static final String TYPE_BUTTON = "F"; + + /** + * Layout组件标识 + */ + public final static String LAYOUT = "Layout"; + + /** + * ParentView组件标识 + */ + public final static String PARENT_VIEW = "ParentView"; + + /** + * InnerLink组件标识 + */ + public final static String INNER_LINK = "InnerLink"; + + /** + * 校验返回结果码 + */ + public final static String UNIQUE = "0"; + + public final static String NOT_UNIQUE = "1"; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/HttpMethod.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/HttpMethod.java new file mode 100644 index 0000000..b7958f7 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/HttpMethod.java @@ -0,0 +1,32 @@ +package ai.remi.comm.core.enums; + +import org.springframework.lang.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 请求方式 + */ +public enum HttpMethod { + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static { + for (HttpMethod httpMethod : values()) { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) { + return (this == resolve(method)); + } +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/IdentityTypeEnum.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/IdentityTypeEnum.java new file mode 100644 index 0000000..28abd2d --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/IdentityTypeEnum.java @@ -0,0 +1,23 @@ +package ai.remi.comm.core.enums; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 身份类型枚举定义 + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum IdentityTypeEnum { + + STARTDIS("remi", "瑞米官方账号"), + COMPANY("company", "集团公司账户"), + VISITOR("visitor", "游客体验账号"); + + private String code; + + private String desc; +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/PermissionScopeEnum.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/PermissionScopeEnum.java new file mode 100644 index 0000000..9c846b1 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/PermissionScopeEnum.java @@ -0,0 +1,37 @@ +package ai.remi.comm.core.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 权限范围枚举定义 + */ +@Getter +@AllArgsConstructor +public enum PermissionScopeEnum { + + GROUP("group","集团范围"), + COMPANY("company","公司范围"), + DEPT("dept","部门范围"); + + @JsonValue + private String code; + + private String desc; + + @JsonCreator + public static PermissionScopeEnum codeOf(String code) { + return Arrays.stream(PermissionScopeEnum.values()) + .filter(each -> each.getCode().equals(code)) + .findFirst() + .orElseThrow(() -> { + throw new IllegalArgumentException("非法参数"); + }); + } +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/ServiceTypeEnum.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/ServiceTypeEnum.java new file mode 100644 index 0000000..51ec6be --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/ServiceTypeEnum.java @@ -0,0 +1,43 @@ +package ai.remi.comm.core.enums; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 错误码设计描述 + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum ServiceTypeEnum { + + WEB_SERVICE("webService", "/web-api/**", "管理端服务"), + APP_SERVICE("appService", "/app-api/**", "移动端服务"); + + private String code; + + /** + * 服务对应的路径前缀 + */ + private String pathPrefix; + + private String desc; + + + public static ServiceTypeEnum codeOf(String code) { + return Arrays.stream(values()) + .filter((each) -> each.getCode().equals(code)).findFirst() + .orElseThrow(()->new RuntimeException("找不到服务")); + } + + public static ServiceTypeEnum pathPrefixOf(String pathPrefix){ + return Arrays.stream(values()) + .filter((each) -> each.getPathPrefix().equals(pathPrefix)).findFirst() + .orElseThrow(()->new RuntimeException("找不到服务")); + } + +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/TypeEnum.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/TypeEnum.java new file mode 100644 index 0000000..88534d4 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/TypeEnum.java @@ -0,0 +1,13 @@ +package ai.remi.comm.core.enums; + +public interface TypeEnum { + /** + * 类型编码 + */ + Integer getCode(); + + /** + * 类型描述 + */ + String getDesc(); +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/YesNoEnum.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/YesNoEnum.java new file mode 100644 index 0000000..5729ee9 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/enums/YesNoEnum.java @@ -0,0 +1,19 @@ +package ai.remi.comm.core.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum YesNoEnum implements TypeEnum{ + + NO(0, "否"), + + YES(1, "是"); + + @JsonValue + private Integer code; + + private String desc; +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/excel/ExcelDynamicSelect.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/excel/ExcelDynamicSelect.java new file mode 100644 index 0000000..07f402b --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/excel/ExcelDynamicSelect.java @@ -0,0 +1,10 @@ +package ai.remi.comm.core.excel; + +public interface ExcelDynamicSelect { + + /** + * 获取动态生成的下拉框可选数据 + * @return 动态生成的下拉框可选数据 + */ + String[] getSource(); +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/excel/ExcelSelected.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/excel/ExcelSelected.java new file mode 100644 index 0000000..6413eff --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/excel/ExcelSelected.java @@ -0,0 +1,29 @@ +package ai.remi.comm.core.excel; + +import java.lang.annotation.*; + +@Documented +@Target({ElementType.FIELD})//用此注解用在属性上。 +@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在; +public @interface ExcelSelected { + + /** + * 固定下拉内容 + */ + String[] source() default {}; + + /** + * 动态下拉内容 + */ + Class[] sourceClass() default {}; + + /** + * 设置下拉框的起始行,默认为第二行 + */ + int firstRow() default 1; + + /** + * 设置下拉框的结束行,默认为最后一行 + */ + int lastRow() default 0x10000; +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/result/PagerBean.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/result/PagerBean.java new file mode 100644 index 0000000..44e1e2e --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/result/PagerBean.java @@ -0,0 +1,52 @@ +package ai.remi.comm.core.result; + + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc PagerBean + */ +@Data +public class PagerBean implements Serializable { + + private static final long serialVersionUID = 4804053559968742915L; + + /** + * 总记录数 + */ + private Long total; + + /** + * 当前页 + */ + protected Long pageNum; + + /** + * 页大小 + */ + protected Long pageSize; + + /** + * 列表数据 + */ + private List records; + + /** + * 是否可导入 + */ + private boolean if_import; + public PagerBean() { + } + + public PagerBean(Long total, Long pageNum, Long pageSize, List records) { + this.total = total; + this.pageNum = pageNum; + this.pageSize = pageSize; + this.records = records; + } +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/result/ResultBean.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/result/ResultBean.java new file mode 100644 index 0000000..5d75ca2 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/result/ResultBean.java @@ -0,0 +1,90 @@ +package ai.remi.comm.core.result; + +import ai.remi.comm.core.constant.Constants; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc ResultBean + */ +@Data +@SuperBuilder +public class ResultBean implements Serializable { + + private static final long serialVersionUID = 8676131899637805509L; + + /** + * 成功 + */ + public static final String SUCCESS = Constants.SUCCESS; + + /** + * 失败 + */ + public static final String ERROR = Constants.ERROR; + + // 返回编码 + private String code; + // 返回信息 + private String msg; + // 返回数据封装 + @Builder.Default + private T data = (T) ""; + + public ResultBean() { + } + + private static ResultBean restResult(String code, String msg, T data) { + ResultBean apiResult = new ResultBean<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static ResultBean success() { + return restResult(SUCCESS, null, null); + } + + public static ResultBean success(String msg) { + return restResult(SUCCESS, msg, null); + } + + public static ResultBean success(T data) { + return restResult(SUCCESS, null, data); + } + + public static ResultBean success(String msg, T data) { + return restResult(SUCCESS, msg, data); + } + + public static ResultBean error() { + return restResult(ERROR, null, null); + } + + public static ResultBean error(String msg) { + return restResult(ERROR, msg, null); + } + + public static ResultBean error(T data) { + return restResult(ERROR, null, data); + } + + public static ResultBean error(String msg, T data) { + return restResult(ERROR, msg, data); + } + + public static ResultBean error(String code, String msg, T data) { + return restResult(code, msg, data); + } +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/spring/SpringBeans.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/spring/SpringBeans.java new file mode 100644 index 0000000..2770c58 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/spring/SpringBeans.java @@ -0,0 +1,70 @@ +package ai.remi.comm.core.spring; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.lang.annotation.Annotation; +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc SpringBeanUtils + */ +@Component +public class SpringBeans implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringBeans.applicationContext == null) { + SpringBeans.applicationContext = applicationContext; + } + } + + /** + * Created by dianjiu on 2022-06-23 ,contact lidianjiu@njydsz.com. 获取applicationContext. + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * Created by dianjiu on 2022-06-23 ,contact lidianjiu@njydsz.com. 通过name获取 Bean. + */ + public static Object getBean(String name) { + return getApplicationContext().getBean(name); + } + + /** + * Created by dianjiu on 2022-06-23 ,contact lidianjiu@njydsz.com. 通过class获取Bean. + */ + public static T getBean(Class clazz) { + return getApplicationContext().getBean(clazz); + } + + /** + * Created by dianjiu on 2022-06-23 ,contact lidianjiu@njydsz.com. 通过name,以及Clazz返回指定的Bean. + */ + public static T getBean(String name, Class clazz) { + return getApplicationContext().getBean(name, clazz); + } + + /** + * 根据clazz类型获取spring容器中的对象 + */ + public static Map getBeansOfType(Class clazz) { + return getApplicationContext().getBeansOfType(clazz); + } + + /** + * 根据注解类从容器中获取对象 + */ + public static Map getBeansOfAnnotation(Class clazz) { + return getApplicationContext().getBeansWithAnnotation(clazz); + } + +} diff --git a/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/spring/SpringProperties.java b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/spring/SpringProperties.java new file mode 100644 index 0000000..3ca98ed --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/java/ai/remi/comm/core/spring/SpringProperties.java @@ -0,0 +1,26 @@ +package ai.remi.comm.core.spring; + +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 通过"server.port"作为name格式调用getPropertiesValue()方法 + */ +@Component +public class SpringProperties implements EnvironmentAware { + + private static Environment env; + + @Override + public void setEnvironment(Environment environment) { + env = environment; + } + + public static String getString(String key) { + return env.getProperty(key); + } + +} diff --git a/remi-comm/remi-comm-core/src/main/resources/META-INF/spring.factories b/remi-comm/remi-comm-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..042e960 --- /dev/null +++ b/remi-comm/remi-comm-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + ai.remi.comm.core.config.AuthFilterConfig,\ + ai.remi.comm.core.spring.SpringProperties,\ + ai.remi.comm.core.spring.SpringBeans + + diff --git a/remi-comm/remi-comm-domain/pom.xml b/remi-comm/remi-comm-domain/pom.xml new file mode 100644 index 0000000..b36bb16 --- /dev/null +++ b/remi-comm/remi-comm-domain/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + remi-comm-domain + remi-comm-domain + remi-comm-domain + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + ai.remi.top.cloud + remi-comm-util + + + + com.baomidou + mybatis-plus-annotation + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + + commons-validator + commons-validator + + + org.hibernate.validator + hibernate-validator + + + org.hibernate + hibernate-validator + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-processor + + + + org.projectlombok + lombok + + + + diff --git a/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/entity/BaseEntity.java b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/entity/BaseEntity.java new file mode 100644 index 0000000..9345d77 --- /dev/null +++ b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/entity/BaseEntity.java @@ -0,0 +1,84 @@ +package ai.remi.comm.domain.entity; + + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author dianjiu【】 + * @email dianjiuxyz@gmail.com + * @desc BaseEntity + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PACKAGE) +public class BaseEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 乐观锁 + */ + @Version + @TableField("revision") + private Integer revision; + + /** + * 逻辑删除 (0正常 1删除) + * value = "" 默认的原值,默认为 0; + * delval = "" 删除后的值,默认为1; + */ +// @TableLogic(value="0",delval="1") + @TableField("deleted") + private Integer deleted; + + /** + * 部门租户ID + */ + @TableField("dept_tenant_id") + private String deptTenantId; + + /** + * 公司租户ID + */ + @TableField("company_tenant_id") + private String companyTenantId; + + /** + * 创建人 + */ + @TableField("created_by") + private String createdBy; + + /** + * 创建时间 + */ + @TableField(value = "created_at", fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新人 + */ + @TableField("updated_by") + private String updatedBy; + + /** + * 更新时间 + */ + @TableField(value = "updated_at", fill = FieldFill.UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; + +} diff --git a/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/entity/RootEntity.java b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/entity/RootEntity.java new file mode 100644 index 0000000..0a06a2c --- /dev/null +++ b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/entity/RootEntity.java @@ -0,0 +1,50 @@ +package ai.remi.comm.domain.entity; + + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc RootEntity + */ +public interface RootEntity extends Serializable { + + public String getId(); + + public RootEntity setId(String id); + + public Integer getRevision(); + + public RootEntity setRevision(Integer revision); + + public Integer getDeleted(); + + public RootEntity setDeleted(Integer deleted); + + public String getDeptTenantId(); + + public RootEntity setDeptTenantId(String deptTenantId); + + public String getCompanyTenantId(); + + public RootEntity setCompanyTenantId(String companyTenantId); + + public String getCreatedBy(); + + public RootEntity setCreatedBy(String createdBy); + + public LocalDateTime getCreatedAt(); + + public RootEntity setCreatedAt(LocalDateTime createdAt); + + public String getUpdatedBy(); + + public RootEntity setUpdatedBy(String updatedBy); + + public LocalDateTime getUpdatedAt(); + + public RootEntity setUpdatedAt(LocalDateTime updatedAt); + +} diff --git a/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/query/PageQuery.java b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/query/PageQuery.java new file mode 100644 index 0000000..af84d17 --- /dev/null +++ b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/query/PageQuery.java @@ -0,0 +1,39 @@ +package ai.remi.comm.domain.query; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc PageQuery + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder(toBuilder = true) +public class PageQuery implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 当前页 + */ + @NotBlank(message = "pageNum当前页不能为空") + private Integer pageNum; + + /** + * 页大小 + */ + @NotBlank(message = "pageSize页大小不能为空") + private Integer pageSize; + + /** + * 指定排序字段 + */ + private String orderBy; +} diff --git a/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/tree/TreeBuild.java b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/tree/TreeBuild.java new file mode 100644 index 0000000..9ee0c8d --- /dev/null +++ b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/tree/TreeBuild.java @@ -0,0 +1,90 @@ +package ai.remi.comm.domain.tree; + +import ai.remi.comm.util.collection.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +public class TreeBuild { + /** + * rootId + */ + public String rootId; + /** + * 保存参与构建树形的所有数据(通常数据库查询结果) + */ + public List nodeList = new ArrayList<>(); + + /** + * 构造方法 + * + * @param nodeList 将数据集合赋值给nodeList,即所有数据作为所有节点。 + */ + public TreeBuild(String rootId, List nodeList) { + this.rootId = rootId; + this.nodeList = nodeList; + } + + /** + * 获取需构建的所有根节点(顶级节点) "0" + * + * @return 所有根节点List集合 + */ + public List getRootNode() { + // 保存所有根节点(所有根节点的数据) + List rootNodeList = new ArrayList<>(); + // treeNode:查询出的每一条数据(节点) + for (T treeNode : nodeList) { + // 判断当前节点是否为根节点,此处注意:若parentId类型是String,则要采用equals()方法判断。 + if (this.rootId.equals(treeNode.getParentId())) { + // 是,添加 + rootNodeList.add(treeNode); + } + } + return rootNodeList; + } + + /** + * 根据每一个顶级节点(根节点)进行构建树形结构 + * + * @return 构建整棵树 + */ + public List buildTree() { + // treeNodes:保存一个顶级节点所构建出来的完整树形 + List treeNodes = new ArrayList(); + // getRootNode():获取所有的根节点 + for (T treeRootNode : getRootNode()) { + // 将顶级节点进行构建子树 + treeRootNode = buildChildTree(treeRootNode); + // 完成一个顶级节点所构建的树形,增加进来 + treeNodes.add(treeRootNode); + } + // 这里对父节点排序 + CollectionUtils.sort(treeNodes, "sort"); + // for循环结束,即获取所有的根节点,树形构建结束,设置树结果 + return treeNodes; + } + + /** + * 递归-----构建子树形结构 + * + * @param pNode 根节点(顶级节点) + * @return 整棵树 + */ + public T buildChildTree(T pNode) { + List childTree = new ArrayList(); + // nodeList:所有节点集合(所有数据) + for (T treeNode : nodeList) { + // 判断当前节点的父节点ID是否等于根节点的ID,即当前节点为其下的子节点 + if (treeNode.getParentId().equals(pNode.getId())) { + // 再递归进行判断当前节点的情况,调用自身方法 + childTree.add(buildChildTree(treeNode)); + } + } + // 这里对子节点排序 + CollectionUtils.sort(childTree, "sort"); + // for循环结束,即节点下没有任何节点,树形构建结束,设置树结果 + pNode.setChildren(childTree); + return pNode; + } +} diff --git a/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/tree/TreeNode.java b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/tree/TreeNode.java new file mode 100644 index 0000000..651fb26 --- /dev/null +++ b/remi-comm/remi-comm-domain/src/main/java/ai/remi/comm/domain/tree/TreeNode.java @@ -0,0 +1,39 @@ +package ai.remi.comm.domain.tree; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc ResultBean + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PACKAGE) +public class TreeNode implements Serializable { + + private static final long serialVersionUID = 8676131899637805509L; + + /** + * 当前节点ID + */ + private String id; + + /** + * 父节点ID 顶级节点为0 + */ + private String parentId; + + /** + * 子节点 + */ + private List children; +} diff --git a/remi-comm/remi-comm-email/pom.xml b/remi-comm/remi-comm-email/pom.xml new file mode 100644 index 0000000..b16d910 --- /dev/null +++ b/remi-comm/remi-comm-email/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + remi-comm-email + remi-comm-email + remi-comm-email + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + org.springframework.boot + spring-boot-starter-mail + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + org.slf4j + slf4j-api + + + + diff --git a/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/config/EmailConfig.java b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/config/EmailConfig.java new file mode 100644 index 0000000..df1409d --- /dev/null +++ b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/config/EmailConfig.java @@ -0,0 +1,42 @@ +package ai.remi.comm.email.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import javax.annotation.Resource; +import java.util.Properties; + +/** + * @author dianjiu【公众号 点九开源】 + * @email startdis@njydsz.com + * @desc MinioConfig + */ +@Configuration +@EnableConfigurationProperties(EmailProperties.class) +public class EmailConfig { + + @Resource + private EmailProperties emailProperties; + + + /** + * 初始化 JavaMailSender + */ + @Bean + public JavaMailSenderImpl javaMailSenderImpl() { + JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); + javaMailSender.setProtocol(emailProperties.getProtocol()); + javaMailSender.setHost(emailProperties.getSmtpHost()); + javaMailSender.setPort(Integer.parseInt(emailProperties.getSmtpPort())); + javaMailSender.setDefaultEncoding(emailProperties.getEncoding()); + javaMailSender.setUsername(emailProperties.getFromMail()); + javaMailSender.setPassword(emailProperties.getPassword()); + // TODO 设置邮件参数 + Properties props = new Properties(); + // props.setProperty("mail.smtp.auth", emailProperties.get()); + javaMailSender.setJavaMailProperties(props); + return javaMailSender; + } +} diff --git a/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/config/EmailProperties.java b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/config/EmailProperties.java new file mode 100644 index 0000000..856a1a1 --- /dev/null +++ b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/config/EmailProperties.java @@ -0,0 +1,56 @@ +package ai.remi.comm.email.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +/** + * @author dianjiu【公众号 点九开源】 + * @email startdis@njydsz.com + * @desc Email配置类 + */ +@Data +@Component +@ConfigurationProperties(prefix = "remi-comm.email") +public class EmailProperties { + /** + * 发送协议 + */ + private String protocol; + /** + * 服务器地址 + */ + private String smtpHost; + /** + * 服务器端口 + */ + private String smtpPort; + /** + * 发件人名称 + */ + private String fromName; + /** + * 发件人邮箱 + */ + private String fromMail; + /** + * 发件人密码 + */ + private String password; + /** + * 邮件编码 + */ + private String encoding; + + /** + * SSL配置 + */ + @Data + @Configuration + @ConfigurationProperties("remi-comm.email.ssl") + class sslConf { + private String enabled; + private String sslPort; + } +} diff --git a/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/domain/Email.java b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/domain/Email.java new file mode 100644 index 0000000..0f97c60 --- /dev/null +++ b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/domain/Email.java @@ -0,0 +1,57 @@ +package ai.remi.comm.email.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.util.Map; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class Email { + + /** + * 收件人,多个收件人用“,”隔开 + */ + private String to; + /** + * 抄送人,多个抄送人用“,”隔开 + */ + private String cc; + /** + * 密送人,多个密送人用“,”隔开 + */ + private String bcc; + /** + * 邮件主题 + */ + private String subject; + /** + * 邮件内容 + */ + private String content; + /** + * 图片资源路径 + */ + private String rscPath; + /** + * 图片资源ID(CID) + */ + private String rscId; + /** + * 附件路径 + */ + private String[] filePaths; + /** + * 邮件模板地址 + */ + private String template; + /** + * 邮件模板变量 + */ + private Map variable; + +} diff --git a/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/service/EmailService.java b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/service/EmailService.java new file mode 100644 index 0000000..393cea3 --- /dev/null +++ b/remi-comm/remi-comm-email/src/main/java/ai/remi/comm/email/service/EmailService.java @@ -0,0 +1,227 @@ +package ai.remi.comm.email.service; + +import ai.remi.comm.email.config.EmailProperties; +import ai.remi.comm.email.domain.Email; +import freemarker.template.Configuration; +import freemarker.template.Template; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.FileSystemResource; +import org.springframework.mail.MailException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import javax.annotation.Resource; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.File; + +@Slf4j +@Component +public class EmailService { + + /** + * 邮件配置 + */ + @Resource + private EmailProperties emailProperties; + + /** + * 邮件执行 + */ + @Resource + private JavaMailSender mailSender; + + /** + * thymeleaf + */ + @Resource + private TemplateEngine templateEngine; + + /** + * freemarker + */ + @Resource + public Configuration configuration; + + + /** + * 发送文本邮件 + */ + public void sendTextMail(Email email) { + SimpleMailMessage message = new SimpleMailMessage(); + try { + message.setFrom(emailProperties.getFromName()); + message.setTo(email.getTo()); + message.setCc(email.getCc()); + message.setBcc(email.getBcc()); + message.setSubject(email.getSubject()); + message.setText(email.getContent()); + mailSender.send(message); + log.info("发送基础邮件成功!"); + } catch (MailException e) { + log.error("发送基础邮件异常:" + e); + } + } + + /** + * 发送html邮件 + */ + public void sendHtmlMail(Email email) { + MimeMessage message = mailSender.createMimeMessage(); + try { + //true表示需要创建一个multipart message + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(emailProperties.getFromName()); + helper.setTo(email.getTo()); + helper.setSubject(email.getSubject()); + helper.setText(email.getContent(), true); + // 检查附件数组是否为空 + String[] filePaths = email.getFilePaths(); + if (checkFilePaths(filePaths)) { + for (String filePath : filePaths) { + FileSystemResource file = new FileSystemResource(new File(filePath)); + String fileName = filePath.substring(filePath.lastIndexOf(File.separator)); + helper.addAttachment(fileName, file); + } + } + mailSender.send(message); + log.info("发送html邮件成功"); + } catch (MessagingException e) { + log.error("发送html邮件异常:" + e); + } + } + + /** + * 发送正文是静态资源(图片)的邮件 + */ + public void sendInlineMail(Email email) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(emailProperties.getFromName()); + helper.setTo(email.getTo()); + helper.setSubject(email.getSubject()); + helper.setText(email.getContent(), true); + // 嵌入静态资源 + FileSystemResource res = new FileSystemResource(new File(email.getRscPath())); + helper.addInline(email.getRscId(), res); + // 检查附件数组是否为空 + String[] filePaths = email.getFilePaths(); + if (checkFilePaths(filePaths)) { + for (String filePath : filePaths) { + FileSystemResource file = new FileSystemResource(new File(filePath)); + String fileName = filePath.substring(filePath.lastIndexOf(File.separator)); + helper.addAttachment(fileName, file); + } + } + mailSender.send(message); + log.info("发送嵌入图片的邮件成功!"); + } catch (MessagingException e) { + log.error("发送嵌入图片的邮件异常:" + e); + } + } + + /** + * 发送带附件的邮件 + */ + public void sendAttachmentsMail(Email email) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(emailProperties.getFromName()); + helper.setTo(email.getTo()); + helper.setSubject(email.getSubject()); + helper.setText(email.getContent(), true); + // 检查附件数组是否为空 + String[] filePaths = email.getFilePaths(); + if (checkFilePaths(filePaths)) { + for (String filePath : filePaths) { + FileSystemResource file = new FileSystemResource(new File(filePath)); + String fileName = filePath.substring(filePath.lastIndexOf(File.separator)); + helper.addAttachment(fileName, file); + } + } + mailSender.send(message); + log.info("发送带附件的邮件成功!"); + } catch (MessagingException e) { + log.error("发送带附件的邮件异常:" + e); + } + } + + public void sendThymeleafMail(Email email) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(emailProperties.getFromName()); + helper.setTo(email.getTo()); + helper.setSubject(email.getSubject()); + helper.setText(email.getContent(), true); + // 设置邮件正文 + Context context = new Context(); + context.setVariables(email.getVariable()); + String htmlContent = templateEngine.process(email.getTemplate(), context); + // 检查附件数组是否为空 + String[] filePaths = email.getFilePaths(); + if (checkFilePaths(filePaths)) { + for (String filePath : filePaths) { + FileSystemResource file = new FileSystemResource(new File(filePath)); + String fileName = filePath.substring(filePath.lastIndexOf(File.separator)); + helper.addAttachment("瑞米平台:" + fileName, file); + } + } + // 第二个参数设置为true表示HTML格式 + helper.setText(htmlContent, true); + mailSender.send(message); + log.info("发送Thymeleaf模板邮件成功!"); + } catch (MessagingException e) { + log.error("发送Thymeleaf模板邮件异常:" + e); + } + } + + public void sendFreemarkerMail(Email email) { + MimeMessage message = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + helper.setFrom(emailProperties.getFromName()); + helper.setTo(email.getTo()); + helper.setSubject(email.getSubject()); + helper.setText(email.getContent(), true); + // 设置邮件正文 + Template template = configuration.getTemplate(email.getTemplate() + ".flt"); + String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, email.getVariable()); + // 检查附件数组是否为空 + String[] filePaths = email.getFilePaths(); + if (checkFilePaths(filePaths)) { + for (String filePath : filePaths) { + FileSystemResource file = new FileSystemResource(new File(filePath)); + String fileName = filePath.substring(filePath.lastIndexOf(File.separator)); + helper.addAttachment("瑞米平台:" + fileName, file); + } + } + // 第二个参数设置为true表示HTML格式 + helper.setText(htmlContent, true); + mailSender.send(message); + log.info("发送Freemarker模板邮件成功!"); + } catch (Exception e) { + log.error("发送Freemarker模板邮件异常:" + e); + } + } + + public static boolean checkFilePaths(String[] array) { + if (array == null) { + log.info("附件数组为null"); + return false; + } else if (array.length == 0) { + log.info("附件数组为空"); + return false; + } else { + log.info("附件数组不为空且不为null"); + return true; + } + } +} diff --git a/remi-comm/remi-comm-email/src/main/resources/META-INF/spring.factories b/remi-comm/remi-comm-email/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ce395ea --- /dev/null +++ b/remi-comm/remi-comm-email/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + ai.remi.comm.email.service.EmailService \ No newline at end of file diff --git a/remi-comm/remi-comm-email/src/main/resources/templates/freemarker.ftl b/remi-comm/remi-comm-email/src/main/resources/templates/freemarker.ftl new file mode 100644 index 0000000..deb73b2 --- /dev/null +++ b/remi-comm/remi-comm-email/src/main/resources/templates/freemarker.ftl @@ -0,0 +1,11 @@ + + + + + ${title} + + +

${greeting}

+

${message}

+ + \ No newline at end of file diff --git a/remi-comm/remi-comm-email/src/main/resources/templates/thymeleaf.html b/remi-comm/remi-comm-email/src/main/resources/templates/thymeleaf.html new file mode 100644 index 0000000..1a6eb12 --- /dev/null +++ b/remi-comm/remi-comm-email/src/main/resources/templates/thymeleaf.html @@ -0,0 +1,11 @@ + + + + + Welcome + + +

Hello,

+

Welcome to our site!

+ + diff --git a/remi-comm/remi-comm-exception/README.md b/remi-comm/remi-comm-exception/README.md new file mode 100644 index 0000000..4ffd5ad --- /dev/null +++ b/remi-comm/remi-comm-exception/README.md @@ -0,0 +1,6 @@ +```yaml +i18n: + messages: + basename: i18n/messages + encoding: utf-8 +``` \ No newline at end of file diff --git a/remi-comm/remi-comm-exception/pom.xml b/remi-comm/remi-comm-exception/pom.xml new file mode 100644 index 0000000..fdc23b1 --- /dev/null +++ b/remi-comm/remi-comm-exception/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + remi-comm-exception + remi-comm-exception + remi-comm-exception + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-domain + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-nop + + + + diff --git a/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/config/I18nConfig.java b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/config/I18nConfig.java new file mode 100644 index 0000000..9088c21 --- /dev/null +++ b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/config/I18nConfig.java @@ -0,0 +1,40 @@ +package ai.remi.comm.exception.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class I18nConfig implements WebMvcConfigurer { + + //@Value("${i18n.messages.basename:}") + //private String messagesBasename; + + //@Value("${i18n.messages.encoding:}") + //private String messagesEncoding; + + @Bean(name = "messageSource") + public ReloadableResourceBundleMessageSource messageSource() { + ReloadableResourceBundleMessageSource rbms = new ReloadableResourceBundleMessageSource(); + //if (!StringUtils.isEmpty(messagesBasename)) { + // //设置path + // rbms.addBasenames(messagesBasename.split(",")); + //} + //rbms.setDefaultEncoding(messagesEncoding); + rbms.setBasenames("classpath:i18n/messages"); + rbms.setDefaultEncoding("UTF-8"); + rbms.setUseCodeAsDefaultMessage(true); + rbms.setFallbackToSystemLocale(false); + return rbms; + } + + @Bean + public LocalValidatorFactoryBean getValidator() { + LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); + bean.setValidationMessageSource(messageSource()); + return bean; + } +} + diff --git a/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/custom/BusinessException.java b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/custom/BusinessException.java new file mode 100644 index 0000000..440127c --- /dev/null +++ b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/custom/BusinessException.java @@ -0,0 +1,89 @@ +package ai.remi.comm.exception.custom; + +import ai.remi.comm.core.constant.Constants; +import ai.remi.comm.exception.util.MessageUtils; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc BusinessException + */ +@Data +@NoArgsConstructor +public class BusinessException extends RuntimeException { + + @Getter + @Setter + private String code; + @Getter + @Setter + private String key; + @Getter + @Setter + private Object[] params; + @Getter + @Setter + private String message; + + public BusinessException(String key) { + super(MessageUtils.getMessage(key)); + this.code = Constants.ERROR; + this.message = MessageUtils.getMessage(key); + } + + public BusinessException(String key, Object[] params) { + super(MessageUtils.getMessage(key, params)); + this.code = Constants.ERROR; + this.message = MessageUtils.getMessage(key, params); + } + + public BusinessException(String code, String key) { + super(MessageUtils.getMessage(key)); + this.code = code; + this.message = MessageUtils.getMessage(key); + } + + public BusinessException(String code, String key, Object[] params) { + super(MessageUtils.getMessage(key, params)); + this.code = code; + this.message = MessageUtils.getMessage(key, params); + } + + public BusinessException(Throwable cause) { + super(cause); + } + + public BusinessException(String code, Throwable cause) { + super(cause); + this.code = code; + } + + public BusinessException(String code, String key, Throwable cause) { + super(MessageUtils.getMessage(key), cause); + this.code = code; + this.message = MessageUtils.getMessage(key); + } + + public BusinessException(String code, String key, Object[] params, Throwable cause) { + super(MessageUtils.getMessage(key, params), cause); + this.code = code; + this.message = MessageUtils.getMessage(key, params); + } + + public BusinessException(String code, String key, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(MessageUtils.getMessage(key), cause, enableSuppression, writableStackTrace); + this.code = code; + this.message = MessageUtils.getMessage(key); + } + + public BusinessException(String code, String key, Object[] params, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(MessageUtils.getMessage(key, params), cause, enableSuppression, writableStackTrace); + this.code = code; + this.message = MessageUtils.getMessage(key, params); + } +} + diff --git a/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/enums/CommExceptionCode.java b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/enums/CommExceptionCode.java new file mode 100644 index 0000000..952d93e --- /dev/null +++ b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/enums/CommExceptionCode.java @@ -0,0 +1,52 @@ +package ai.remi.comm.exception.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 错误码设计描述 + *

+ * 面向日志的错误码定义共十三位(十位有意义,三位连接符),并且应该具有如下分类: 应用标识,表示错误属于哪个应用,三位数字。 功能域标识,表示错误属于应用中的哪个功能模块,三位数字。 错误类型,表示错误属于那种类型,一位字母。 + * 错误编码,错误类型下的具体错误,三位数字。 + *

+ * 面向外部传递的错误码共六位,并且有如下分类: 错误类型,表示错误来源,1位字母。 应用标识,表示错误属于哪个应用,2位数字。 错误编码,错误类型下的具体错误,3位数字。 + *

+ * 错误类型: A-表示错误来源于用户 B-表示错误来源于系统 C-表示错误来源于第三方接口 + */ +@Getter +@AllArgsConstructor +public enum CommExceptionCode implements ExceptionCode { + + /** + * 面向外部传递的错误码共六位,并且有如下分类: + * 错误类型,表示错误来源,1位字母。 + * 应用标识,表示错误属于哪个应用,2位数字。 + * 错误编码,错误类型下的具体错误,3位数字。 + *

+ * 错误类型: A-表示错误来源于用户 B-表示错误来源于系统 C-表示错误来源于第三方接口 + */ + /****************************客户端异常****************************/ + BUSINESS_ERROR("A01001", "business.error"), + ILLEGAL_ARGUMENT("A01002", "illegal.argument"), + NOT_LOGGED_IN("A01003", "not.logged.in"), + /****************************系统异常****************************/ + SYSTEM_ERROR("B01001", "system.error"), + ASSERT_ERROR("B01002", "assert.error"), + /****************************三方异常****************************/ + OTHER_SYSTEM_ERROR("C01001", "other.system.error"), + CALL_FEIGN_ERROR("C01002", "call.feign.error"), + CALL_SQL_ERROR("C01003", "call.sql.error"), + CALL_REDIS_ERROR("C01004", "call.redis.error"), + CALL_MINIO_ERROR("C01005", "call.minio.error"), + CALL_NACOS_ERROR("C01006", "call.nacos.error"), + CALL_JOB_ERROR("C01007", "call.job.error"), + CALL_MQ_ERROR("C01008", "call.mq.error"), + CALL_ES_ERROR("C01009", "call.es.error"); + + private String code; + + private String key; + +} diff --git a/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/enums/ExceptionCode.java b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/enums/ExceptionCode.java new file mode 100644 index 0000000..539b9c6 --- /dev/null +++ b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/enums/ExceptionCode.java @@ -0,0 +1,17 @@ +package ai.remi.comm.exception.enums; + +/** + * 异常code接口 + */ +public interface ExceptionCode { + /** + * 异常code + * @return + */ + String getCode(); + /** + * 异常消息 + * @return + */ + String getKey(); +} diff --git a/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/util/ExceptionUtils.java b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/util/ExceptionUtils.java new file mode 100644 index 0000000..3c68f5d --- /dev/null +++ b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/util/ExceptionUtils.java @@ -0,0 +1,24 @@ +package ai.remi.comm.exception.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc ExceptionUtils + */ +public class ExceptionUtils { + + + /** + * 获取exception的详细错误信息。 + */ + public static String getTrace(Throwable e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + +} diff --git a/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/util/MessageUtils.java b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/util/MessageUtils.java new file mode 100644 index 0000000..f32cce6 --- /dev/null +++ b/remi-comm/remi-comm-exception/src/main/java/ai/remi/comm/exception/util/MessageUtils.java @@ -0,0 +1,136 @@ +package ai.remi.comm.exception.util; + +import ai.remi.comm.core.spring.SpringBeans; +import ai.remi.comm.util.auth.AuthInfoUtils; +import ai.remi.comm.util.string.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc MessageUtils + */ +@Component +public class MessageUtils { + private static final Logger logger = LoggerFactory.getLogger(MessageUtils.class); + + /** + * 获得多语言内容,默认根据当前用户的Local获取 + * + * @param key 多语言key + * @return 翻译后的值,如获取不到则返回key + */ + public static String getMessage(String key) { + return getMessage(key, new Object[]{}); + } + + + /** + * 获得多语言内容--带参数,默认根据当前用户的Local获取 + * + * @param key 多语言key + * @param params 参数 + * @return 翻译后的值,如获取不到则返回key + */ + public static String getMessage(String key, Object[] params) { + Locale locale = getLocale(); + if (params == null) { + params = new Object[]{}; + } + return getMessage(locale, key, params); + + } + + /** + * 获取多语言内容--带默认值,默认根据当前用户的Local获取 + * + * @param key 多语言key + * @param defaultMsg 默认返回值 + * @return 翻译后的值,获取不到返回默认值 + */ + public static String getMessage(String key, String defaultMsg) { + return getMessage(key, new Object[]{}, defaultMsg); + } + + /** + * 获得多语言内容--带参数--带默认值,默认根据当前用户的Local获取 + * + * @param key 多语言key + * @param defaultMsg 默认返回值 + * @param params 参数 + * @return 翻译后的值,获取不到返回默认值 + */ + public static String getMessage(String key, Object[] params, String defaultMsg) { + Locale locale = getLocale(); + return getMessage(locale, key, params, defaultMsg); + } + + /** + * 不带参数 + * + * @param locale 语言信息 + * @param key 多语言key + * @return 翻译后的值 + */ + public static String getMessage(Locale locale, String key) { + return getMessage(locale, key, new Object[]{}); + } + + + /** + * 获取多语言数据 + * + * @param locale 语言信息 + * @param key 多语言key + * @param params 参数 + * @return 翻译后的值 + */ + public static String getMessage(Locale locale, String key, Object[] params) { + try { + MessageSource messageSource = (MessageSource) SpringBeans.getBean("messageSource"); + return messageSource.getMessage(key, params, locale); + } catch (Exception e) { + logger.error("get locale msg error", e); + } + return key; + } + + /** + * 获取多语言数据 + * + * @param key 多语言key + * @param params 参数 + * @param defaultMsg 默认返回值 + * @param locale 语言信息 + * @return 翻译后的值 + */ + public static String getMessage(Locale locale, String key, Object[] params, String defaultMsg) { + MessageSource messageSource = (MessageSource) SpringBeans.getBean("messageSource"); + return messageSource.getMessage(key, params, defaultMsg, locale); + } + + /** + * 获取当前语言 + * + * @return + */ + private static Locale getLocale() { + String userLanguage = AuthInfoUtils.getUserLanguage(); + if (StringUtils.isBlank(userLanguage)) { + return Locale.CHINA; + } + switch (userLanguage.toLowerCase()) { + case "zh_cn": + case "zh-CN": + case "zh": + return Locale.CHINA; + default: + return Locale.US; + } + } +} diff --git a/remi-comm/remi-comm-exception/src/main/resources/META-INF/spring.factories b/remi-comm/remi-comm-exception/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a18f148 --- /dev/null +++ b/remi-comm/remi-comm-exception/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +ai.remi.comm.exception.config.I18nConfig \ No newline at end of file diff --git a/remi-comm/remi-comm-feign/pom.xml b/remi-comm/remi-comm-feign/pom.xml new file mode 100644 index 0000000..d3a0e00 --- /dev/null +++ b/remi-comm/remi-comm-feign/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + remi-comm-feign + remi-comm-feign + remi-comm-feign + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + ai.remi.top.cloud + remi-comm-util + + + + javax.servlet + javax.servlet-api + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + org.slf4j + slf4j-nop + + + diff --git a/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/annotation/EnableRemiFeignClients.java b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/annotation/EnableRemiFeignClients.java new file mode 100644 index 0000000..91a93bf --- /dev/null +++ b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/annotation/EnableRemiFeignClients.java @@ -0,0 +1,27 @@ +package ai.remi.comm.feign.annotation; + +import org.springframework.cloud.openfeign.EnableFeignClients; + +import java.lang.annotation.*; + + +/** + * @author Remi + * @email remi@dianjiu.cc + * @desc 自定义feign注解,添加basePackages路径 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnableFeignClients +public @interface EnableRemiFeignClients { + String[] value() default {}; + + String[] basePackages() default {"ai.remi"}; + + Class[] basePackageClasses() default {}; + + Class[] defaultConfiguration() default {}; + + Class[] clients() default {}; +} diff --git a/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/FeignAutoConfiguration.java b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/FeignAutoConfiguration.java new file mode 100644 index 0000000..c044df8 --- /dev/null +++ b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/FeignAutoConfiguration.java @@ -0,0 +1,52 @@ +package ai.remi.comm.feign.aspect; + +import feign.Logger; +import feign.RequestInterceptor; +import feign.codec.ErrorDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +/** + * @author Remi + * @email remi@dianjiu.cc + * @desc Feign 配置注册 + */ +@Configuration +public class FeignAutoConfiguration { + + /** + * 自定义请求拦截器 + * 传递用户信息请求头,防止丢失 + * @return + */ + @Bean + public RequestInterceptor requestInterceptor() { + return new FeignRequestInterceptor(); + } + + /** + * 设置Feign客户端日志级别 + *

+ * 有四种日志记录级别可供选择: + *

+ * - NONE – 不记录日志,这是默认设置 + * - BASIC – 仅记录请求方法、URL 和响应状态 + * - HEADERS – 将基本信息与请求和响应标头一起记录 + * - FULL – 记录请求和响应的主体、标头和元数据 + */ + @Bean + Logger.Level level() { + return Logger.Level.FULL; + } + + /** + * Feign 的默认错误处理程序ErrorDecoder.default总是抛出FeignException。 + * 种行为并不总是最有用的。因此,要自定义抛出的异常 + * @return + */ + @Bean + public ErrorDecoder errorDecoder() { + return new RemiFeignErrorDecoder(); + } +} diff --git a/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/FeignRequestInterceptor.java b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/FeignRequestInterceptor.java new file mode 100644 index 0000000..9e605b8 --- /dev/null +++ b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/FeignRequestInterceptor.java @@ -0,0 +1,72 @@ +package ai.remi.comm.feign.aspect; + +import ai.remi.comm.core.constant.HeaderConstant; +import ai.remi.comm.core.enums.IdentityTypeEnum; +import ai.remi.comm.core.enums.PermissionScopeEnum; +import ai.remi.comm.util.auth.AuthInfoUtils; +import ai.remi.comm.util.http.ServletUtils; +import ai.remi.comm.util.ip.IpAddrUtils; +import ai.remi.comm.util.string.StringUtils; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Optional; + +/** + * @author Remi + * @email remi@dianjiu.cc + * @desc Feign 请求拦截器 + */ +@Component +public class FeignRequestInterceptor implements RequestInterceptor { + private Logger log = LoggerFactory.getLogger(FeignRequestInterceptor.class); + + @Override + public void apply(RequestTemplate requestTemplate) { + HttpServletRequest httpServletRequest = ServletUtils.getRequest(); + Map headers = ServletUtils.getHeaders(httpServletRequest); + // 传递用户信息请求头,防止丢失 + //String userId = headers.get(HeaderConstant.X_UNIQUE_ID); + if (StringUtils.isNotNull(httpServletRequest)) { + // 传递服务类型请求头,防止丢失 + log.info("FeignHeaderRequestInterceptor X_SERVICE_TYPE:{}", AuthInfoUtils.getServiceTypeCode()); + requestTemplate.header(HeaderConstant.X_SERVICE_TYPE, AuthInfoUtils.getServiceTypeCode()); + // 传递部门租户请求头,防止丢失 + requestTemplate.header(HeaderConstant.X_DEPT_TENANT_ID, AuthInfoUtils.getDeptTenantId()); + // 传递公司租户请求头,防止丢失 + requestTemplate.header(HeaderConstant.X_COMPANY_TENANT_ID, AuthInfoUtils.getCompanyTenantId()); + // 传递用户信息请求头,防止丢失 + requestTemplate.header(HeaderConstant.X_UNIQUE_ID, AuthInfoUtils.getUniqueId()); + // 传递系统语言请求头,防止丢失 + requestTemplate.header(HeaderConstant.X_USER_LANGUAGE, AuthInfoUtils.getUserLanguage()); + // 传递用户设备请求头,防止丢失 + requestTemplate.header(HeaderConstant.X_DISTINCT_ID, headers.get(HeaderConstant.X_DISTINCT_ID)); + // 传递鉴权信息请求头,防止丢失 + requestTemplate.header(HeaderConstant.X_ACCESS_TOKEN, headers.get(HeaderConstant.X_ACCESS_TOKEN)); + + // 判断用户类型是否为集团公司,是的话进一步判断是否为集团权限,是集团权限需要传递集团下属公司IDs + IdentityTypeEnum identityTypeEnum = AuthInfoUtils.getIdentityTypeEnum(); + //集团公司的用户 + if (identityTypeEnum == IdentityTypeEnum.COMPANY) { + //权限范围 + String permissionScope = Optional.ofNullable(AuthInfoUtils.getPermissionScopeEnum()) + .map(PermissionScopeEnum::getCode).orElse(""); + requestTemplate.header(HeaderConstant.X_PERMISSION_SCOPE, permissionScope); + //当权限范围为集团时,为这个用户管理公司的id + requestTemplate.header(HeaderConstant.X_COMPANY_IDS, AuthInfoUtils.getHasPermissionCompanyIds()); + //当权限范围为公司时,为这个用户管理部门的id + requestTemplate.header(HeaderConstant.X_COMPANY_IDS, AuthInfoUtils.getHasPermissionCompanyIds()); + } else if (identityTypeEnum == IdentityTypeEnum.VISITOR) { + //访客体验的用户 do anything + } + + // 传递请求来源请求头,防止丢失 + requestTemplate.header("X-Forwarded-For", IpAddrUtils.getIpAddr(ServletUtils.getRequest())); + } + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/RemiFeignErrorDecoder.java b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/RemiFeignErrorDecoder.java new file mode 100644 index 0000000..3d22ea2 --- /dev/null +++ b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/aspect/RemiFeignErrorDecoder.java @@ -0,0 +1,19 @@ +package ai.remi.comm.feign.aspect; + +import ai.remi.comm.feign.exception.BadRequestException; +import ai.remi.comm.feign.exception.NotFoundException; +import ai.remi.comm.feign.exception.OpenFeignException; +import feign.Response; +import feign.codec.ErrorDecoder; + +public class RemiFeignErrorDecoder implements ErrorDecoder { + @Override + public Exception decode(String methodKey, Response response) { + + return switch (response.status()) { + case 400 -> new BadRequestException(); + case 404 -> new NotFoundException("Remi Feign "); + default -> new OpenFeignException("Remi Feign error"); + }; + } +} diff --git a/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/BadRequestException.java b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/BadRequestException.java new file mode 100644 index 0000000..160eb37 --- /dev/null +++ b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/BadRequestException.java @@ -0,0 +1,17 @@ +package ai.remi.comm.feign.exception; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Remi + * @email remi@dianjiu.cc + * @desc BadRequestException + */ +@Data +@NoArgsConstructor +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/NotFoundException.java b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/NotFoundException.java new file mode 100644 index 0000000..339b6fd --- /dev/null +++ b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/NotFoundException.java @@ -0,0 +1,33 @@ +package ai.remi.comm.feign.exception; + +import ai.remi.comm.core.constant.Constants; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @author Remi + * @email remi@dianjiu.cc + * @desc NotFoundException + */ +@Data +@NoArgsConstructor +public class NotFoundException extends Exception { + private static final long serialVersionUID = 1L; + @Getter + @Setter + private String code; + @Getter + @Setter + private String message; + + public NotFoundException(String msg) { + super(msg); + this.code = Constants.ERROR; + } + + public NotFoundException(String msg, Exception e) { + super(msg + " not found,because of" + e.getMessage()); + } +} diff --git a/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/OpenFeignException.java b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/OpenFeignException.java new file mode 100644 index 0000000..679fb29 --- /dev/null +++ b/remi-comm/remi-comm-feign/src/main/java/ai/remi/comm/feign/exception/OpenFeignException.java @@ -0,0 +1,63 @@ +package ai.remi.comm.feign.exception; + +import ai.remi.comm.core.constant.Constants; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * @author Remi + * @email remi@dianjiu.cc + * @desc OpenFeignException + */ +@Data +@NoArgsConstructor +public class OpenFeignException extends RuntimeException { + + @Getter + @Setter + private String code; + @Getter + @Setter + private String message; + + @Getter + @Setter + private Object data; + + public OpenFeignException(Throwable cause) { + super(cause); + } + + public OpenFeignException(String message) { + super(message); + this.code = Constants.ERROR; + this.message = message; + } + + public OpenFeignException(String code, String message) { + super(message); + this.code = code; + this.message = message; + } + + public OpenFeignException(String code, Throwable cause) { + super(cause); + this.code = code; + this.message = cause.getMessage(); + } + + public OpenFeignException(String code, String message, Throwable cause) { + super(message, cause); + this.code = code; + this.message = message; + } + + public OpenFeignException(String code, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + this.code = code; + this.message = message; + } +} + diff --git a/remi-comm/remi-comm-jdbc/pom.xml b/remi-comm/remi-comm-jdbc/pom.xml new file mode 100644 index 0000000..62bb34e --- /dev/null +++ b/remi-comm/remi-comm-jdbc/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + remi-comm-jdbc + remi-comm-jdbc + remi-comm-jdbc + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + ai.remi.top.cloud + remi-comm-util + + + + mybatis-plus-boot-starter + com.baomidou + + + mybatis-plus-annotation + com.baomidou + + + com.baomidou + mybatis-plus-extension + + + + mybatis-spring-boot-starter + org.mybatis.spring.boot + + + + druid-spring-boot-starter + com.alibaba + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + mysql-connector-java + mysql + runtime + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + org.slf4j + slf4j-nop + + + + + diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/AutoConfig.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/AutoConfig.java new file mode 100644 index 0000000..54685ce --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/AutoConfig.java @@ -0,0 +1,19 @@ +package ai.remi.comm.jdbc.config; + +import ai.remi.comm.jdbc.handler.CompanyTenantHandler; +import ai.remi.comm.jdbc.handler.DeptTenantHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 自动配置类 + */ +@Configuration +@Import({CompanyTenantHandler.class, DeptTenantHandler.class,TenantConfig.class}) +public class AutoConfig { + + + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/DruidConfig.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/DruidConfig.java new file mode 100644 index 0000000..2f3f6b1 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/DruidConfig.java @@ -0,0 +1,30 @@ +package ai.remi.comm.jdbc.config; + +import com.alibaba.druid.pool.DruidDataSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc DruidConfig 配置类 + */ +@Configuration +public class DruidConfig { + + /* + 将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建 + 绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效 + @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中 + 前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中 + */ + @ConfigurationProperties(prefix = "spring.datasource") + @Bean + public DataSource druidDataSource() { + return new DruidDataSource(); + } + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/FieldFillConfig.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/FieldFillConfig.java new file mode 100644 index 0000000..a022e9a --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/FieldFillConfig.java @@ -0,0 +1,27 @@ +package ai.remi.comm.jdbc.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 字段填充配置类 + */ +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "sql-intercept.field-fill") +public class FieldFillConfig { + + private InterceptConfig createdByIntercept=new InterceptConfig(); + + private InterceptConfig updateByIntercept=new InterceptConfig(); + + private InterceptConfig createAtIntercept=new InterceptConfig(); + + private InterceptConfig updateAtIntercept=new InterceptConfig(); + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/InterceptConfig.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/InterceptConfig.java new file mode 100644 index 0000000..0546889 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/InterceptConfig.java @@ -0,0 +1,35 @@ +package ai.remi.comm.jdbc.config; + +import ai.remi.comm.jdbc.enums.InterceptTableStrategy; +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 拦截器配置类 + */ +@Data +public class InterceptConfig { + /** + * 拦截表策略,默认拦截不包含这些表 + */ + private InterceptTableStrategy interceptTableStrategy = InterceptTableStrategy.EXCLUDE; + + /** + * 是否打开拦截配置 + */ + private Boolean enable = false; + + /** + * 不拦截或拦截的表 + */ + private Set tables = new HashSet<>(); + + /** + * 处理的字段 + */ + private String column = ""; +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/MybatisPlusConfig.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/MybatisPlusConfig.java new file mode 100644 index 0000000..58c0c88 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/MybatisPlusConfig.java @@ -0,0 +1,134 @@ +package ai.remi.comm.jdbc.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import ai.remi.comm.jdbc.handler.*; +import ai.remi.comm.jdbc.interceptor.FieldFillInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; + + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc MybatisPlusConfig 配置类 + */ +@Slf4j +@Configuration +@MapperScan(value = {"ai.remi.*.infra.mapper"}) +public class MybatisPlusConfig { + + @Resource + private TenantConfig tenantConfig; + + @Resource + private FieldFillConfig fieldFillConfig; + + /** + * 分页插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 多租户(部门)拦截器 + Boolean deptTenantEnable = tenantConfig.getDeptTenant().getEnable(); + if (deptTenantEnable){ + TenantLineInnerInterceptor tenantSqlParser = new TenantLineInnerInterceptor(); + tenantSqlParser.setTenantLineHandler(getDeptTenantInterceptor()); + interceptor.addInnerInterceptor(tenantSqlParser); + log.info("----------------部门租户拦截器开启-----------------------"); + } + + // 多租户(公司)拦截器 + Boolean companyTenantEnable = tenantConfig.getCompanyTenant().getEnable(); + if (companyTenantEnable){ + TenantLineInnerInterceptor dealerSqlParser = new TenantLineInnerInterceptor(); + dealerSqlParser.setTenantLineHandler(getCompanyTenantInterceptor()); + interceptor.addInnerInterceptor(dealerSqlParser); + log.info("----------------公司租户拦截器开启-----------------------"); + } + + //创建人填充 + Boolean createByEnable = fieldFillConfig.getCreatedByIntercept().getEnable(); + if (createByEnable){ + FieldFillInterceptor createByInterceptor = new FieldFillInterceptor(); + createByInterceptor.setFieldFillHandler(getCreatedByHandler()); + interceptor.addInnerInterceptor(createByInterceptor); + log.info("----------------创建人填充器开启-----------------------"); + } + + //更新人填充 + Boolean updateByEnable = fieldFillConfig.getUpdateByIntercept().getEnable(); + if (updateByEnable){ + FieldFillInterceptor updateByInterceptor = new FieldFillInterceptor(); + updateByInterceptor.setFieldFillHandler(getUpdatedByHandler()); + interceptor.addInnerInterceptor(updateByInterceptor); + log.info("----------------更新人填充器开启-----------------------"); + } + + //创建时间 + Boolean createAtEnable = fieldFillConfig.getCreateAtIntercept().getEnable(); + if (createAtEnable){ + FieldFillInterceptor createAtInterceptor = new FieldFillInterceptor(); + createAtInterceptor.setFieldFillHandler(getCreatedAtHandler()); + interceptor.addInnerInterceptor(createAtInterceptor); + log.info("----------------创建时间填充器开启-----------------------"); + } + //更新时间 + Boolean updateAtEnable = fieldFillConfig.getUpdateAtIntercept().getEnable(); + if (updateAtEnable){ + FieldFillInterceptor updateAtInterceptor = new FieldFillInterceptor(); + updateAtInterceptor.setFieldFillHandler(getUpdatedAtHandler()); + interceptor.addInnerInterceptor(updateAtInterceptor); + log.info("----------------更新时间填充器开启-----------------------"); + } + + //自带分页拦截器 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL)); + + //自带乐观锁插件 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + + return interceptor; + } + + @Bean + public UpdatedByHandler getUpdatedByHandler(){ + return new UpdatedByHandler(fieldFillConfig); + } + @Bean + public UpdatedAtHandler getUpdatedAtHandler(){ + return new UpdatedAtHandler(fieldFillConfig); + } + + + @Bean + public CreatedByHandler getCreatedByHandler(){ + return new CreatedByHandler(fieldFillConfig); + } + + + @Bean + public CreatedAtHandler getCreatedAtHandler(){ + return new CreatedAtHandler(fieldFillConfig); + } + + @Bean + public DeptTenantHandler getDeptTenantInterceptor() { + return new DeptTenantHandler(tenantConfig); + } + + @Bean + public CompanyTenantHandler getCompanyTenantInterceptor() { + return new CompanyTenantHandler(tenantConfig); + } + + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/TenantConfig.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/TenantConfig.java new file mode 100644 index 0000000..145434c --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/config/TenantConfig.java @@ -0,0 +1,27 @@ +package ai.remi.comm.jdbc.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 租户配置类 + */ +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "sql-intercept.tenant") +public class TenantConfig { + + //不拦截的请求 + private String anonUrlSet=""; + + private InterceptConfig deptTenant=new InterceptConfig(); + + private InterceptConfig companyTenant=new InterceptConfig(); + + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/DataSourceType.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/DataSourceType.java new file mode 100644 index 0000000..46c1730 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/DataSourceType.java @@ -0,0 +1,20 @@ +package ai.remi.comm.jdbc.enums; + +/** + * 数据源 + * + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + */ +public enum DataSourceType { + /** + * 主库 + */ + MASTER, + + /** + * 从库 + */ + SLAVE +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/FieldFillStrategyEnum.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/FieldFillStrategyEnum.java new file mode 100644 index 0000000..02355c9 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/FieldFillStrategyEnum.java @@ -0,0 +1,22 @@ +package ai.remi.comm.jdbc.enums; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 字段填充策略枚举 + */ +public enum FieldFillStrategyEnum { + + /** + * 插入时填充字段 + */ + INSERT, + /** + * 更新时填充字段 + */ + UPDATE, + /** + * 插入和更新时填充字段 + */ + INSERT_UPDATE +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/InterceptTableStrategy.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/InterceptTableStrategy.java new file mode 100644 index 0000000..dba54db --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/enums/InterceptTableStrategy.java @@ -0,0 +1,11 @@ +package ai.remi.comm.jdbc.enums; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 拦截表策略 + */ +public enum InterceptTableStrategy { + INCLUDE, + EXCLUDE; +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractFieldFillHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractFieldFillHandler.java new file mode 100644 index 0000000..0ea3935 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractFieldFillHandler.java @@ -0,0 +1,66 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.InterceptConfig; +import ai.remi.comm.jdbc.enums.FieldFillStrategyEnum; +import net.sf.jsqlparser.expression.Expression; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 字段填充拦截器抽象类 + */ +@SuppressWarnings("checkstyle:Indentation") +public abstract class AbstractFieldFillHandler extends AbstractSqlHandler implements FieldFillHandler { + + + private FieldFillStrategyEnum fieldFillStrategyEnum; + + public AbstractFieldFillHandler(FieldFillStrategyEnum fieldFillStrategyEnum, InterceptConfig interceptConfig) { + super(interceptConfig); + this.fieldFillStrategyEnum = fieldFillStrategyEnum; + } + + + protected abstract Expression doGetFieldFillValue(); + + @Override + public Expression getFieldFillValue() { + return doGetFieldFillValue(); + } + + /** + * 优先级配置填充字段--》子类实现getDefaultFieldFillColumn + * 如果都为指定则抛出异常 + */ + @Override + public String getFieldFillColumn() { + return handleColumn(interceptConfig.getColumn()); + } + + + + + + + @Override + public boolean createIgnore(String tableName) { + if (FieldFillStrategyEnum.UPDATE==fieldFillStrategyEnum){ + return true; + } + return defaultIgnoreStrategy(tableName,interceptConfig); + } + + + @Override + protected boolean customIgnore(){ + return false; + } + + @Override + public boolean updateIgnore(String tableName) { + if (FieldFillStrategyEnum.INSERT==fieldFillStrategyEnum){ + return true; + } + return defaultIgnoreStrategy(tableName,interceptConfig); + } +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractSqlHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractSqlHandler.java new file mode 100644 index 0000000..8d2b903 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractSqlHandler.java @@ -0,0 +1,70 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.InterceptConfig; +import ai.remi.comm.jdbc.enums.InterceptTableStrategy; + +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc SQL拦截器抽象类 + */ +public abstract class AbstractSqlHandler { + + protected InterceptConfig interceptConfig; + + public AbstractSqlHandler(InterceptConfig interceptConfig) { + this.interceptConfig = interceptConfig; + } + + protected boolean defaultIgnoreStrategy(String tableName, InterceptConfig interceptConfig) { + if (customIgnore()) { + return true; + } + InterceptTableStrategy interceptTableStrategy = interceptConfig.getInterceptTableStrategy(); + Set tables = interceptConfig.getTables(); + if (InterceptTableStrategy.EXCLUDE.equals(interceptTableStrategy)) { + return tables.contains(tableName); + } else if (InterceptTableStrategy.INCLUDE.equals(interceptTableStrategy)) { + return !tables.contains(tableName); + }else { + throw new RuntimeException("未指定表拦截策略"); + } + } + + /** + * 如果传入的字段名为空串,则获取默认的字段 + * @param column + * @return + */ + protected String handleColumn(String column) { + if (isNotBlank(column)){ + return column; + } + column = getDefaultColumn(); + if (isNotBlank(column)){ + return column; + } + throw new RuntimeException(this.getClass()+"未指定填充字段,请检查"); + } + + public static boolean isNotBlank(String str) { + return str!=null&&str.trim().length() > 0; + } + + /** + * 子类实现指定 字段名 + * @return + */ + protected String getDefaultColumn(){ + return ""; + } + + /** + * 子类需要实现自定义忽略策略 + * @return + */ + protected abstract boolean customIgnore(); + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractTenantHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractTenantHandler.java new file mode 100644 index 0000000..270985a --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/AbstractTenantHandler.java @@ -0,0 +1,65 @@ +package ai.remi.comm.jdbc.handler; + + +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import ai.remi.comm.jdbc.config.InterceptConfig; +import ai.remi.comm.util.auth.TenantHolder; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import org.springframework.beans.factory.annotation.Value; + +import java.util.List; +import java.util.Optional; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 租户拦截器抽象类 + */ +@Slf4j +public abstract class AbstractTenantHandler extends AbstractSqlHandler implements TenantLineHandler { + + + @Value("${spring.application.name}") + private String applicationContextName; + + private final static List IGNORE_APP_NAME = List.of("remi-ids-web","remi-ams-web","remi-mps-web","remi-sso-web","remi-dfe-web","remi-wfe-web","remi-bie-web","remi-wpe-web","remi-job-web","remi-lce-web"); + + + public AbstractTenantHandler(InterceptConfig interceptConfig) { + super(interceptConfig); + } + + + protected abstract String doGetTenantId(); + + @Override + public Expression getTenantId() { + return new StringValue(doGetTenantId()); + } + + @Override + public String getTenantIdColumn() { + return handleColumn(interceptConfig.getColumn()); + } + + @Override + public boolean ignoreTable(String tableName) { + return defaultIgnoreStrategy(tableName, interceptConfig); + } + + @Override + protected boolean customIgnore(){ + // 忽略的服务 + if (IGNORE_APP_NAME.contains(applicationContextName)){ + return true; + } + Boolean isAdmin=Optional.ofNullable(TenantHolder.isAdmin()).orElse(false); + // 是否是不拦截的请求 + Boolean ignoredRequest = Optional.ofNullable(TenantHolder.getIgnoredRequest()).orElse(false); + // 拦截未打开、忽略的请求、ebg账号---》这些情况都忽略掉 + return ignoredRequest || isAdmin; + } + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CompanyTenantHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CompanyTenantHandler.java new file mode 100644 index 0000000..983498f --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CompanyTenantHandler.java @@ -0,0 +1,32 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.TenantConfig; +import ai.remi.comm.util.auth.AuthInfoUtils; +import lombok.extern.slf4j.Slf4j; + +import java.util.Optional; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 部门租户ID字段填充拦截器 + */ +@Slf4j +public class CompanyTenantHandler extends AbstractTenantHandler { + + + public CompanyTenantHandler(TenantConfig tenantConfig) { + super(tenantConfig.getCompanyTenant()); + } + + + @Override + protected String doGetTenantId() { + return Optional.ofNullable(AuthInfoUtils.getCompanyTenantId()).orElse(""); + } + + @Override + protected String getDefaultColumn() { + return "company_tenant_id"; + } +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CreatedAtHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CreatedAtHandler.java new file mode 100644 index 0000000..fb8a076 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CreatedAtHandler.java @@ -0,0 +1,35 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.FieldFillConfig; +import ai.remi.comm.jdbc.enums.FieldFillStrategyEnum; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.TimestampValue; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 创建时间字段填充拦截器 + */ +public class CreatedAtHandler extends AbstractFieldFillHandler { + + public CreatedAtHandler(FieldFillConfig fieldFillConfig) { + super(FieldFillStrategyEnum.INSERT, fieldFillConfig.getUpdateAtIntercept()); + } + + + @Override + protected Expression doGetFieldFillValue() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String date = format.format(new Date()); + return new TimestampValue(date); + } + + @Override + protected String getDefaultColumn() { + return "created_at"; + } + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CreatedByHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CreatedByHandler.java new file mode 100644 index 0000000..856abcf --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/CreatedByHandler.java @@ -0,0 +1,38 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.FieldFillConfig; +import ai.remi.comm.jdbc.enums.FieldFillStrategyEnum; +import ai.remi.comm.util.auth.AuthInfo; +import ai.remi.comm.util.auth.AuthInfoUtils; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; + +import java.util.Optional; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 创建人字段填充拦截器 + */ +public class CreatedByHandler extends AbstractFieldFillHandler { + + + public CreatedByHandler(FieldFillConfig fieldFillConfig) { + super(FieldFillStrategyEnum.INSERT, fieldFillConfig.getCreatedByIntercept()); + } + + + @Override + protected Expression doGetFieldFillValue() { + AuthInfo authInfo = AuthInfoUtils.getAuthInfo(); + String uniqueId = Optional.ofNullable(authInfo) + .map(AuthInfo::getUniqueId).orElse("系统创建"); + return new StringValue(uniqueId); + } + + @Override + protected String getDefaultColumn() { + return "created_by"; + } + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/DeptTenantHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/DeptTenantHandler.java new file mode 100644 index 0000000..5f16198 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/DeptTenantHandler.java @@ -0,0 +1,31 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.TenantConfig; +import ai.remi.comm.util.auth.AuthInfoUtils; +import lombok.extern.slf4j.Slf4j; + +import java.util.Optional; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 部门租户ID字段填充拦截器 + */ +@Slf4j +public class DeptTenantHandler extends AbstractTenantHandler { + + public DeptTenantHandler(TenantConfig tenantConfig) { + super(tenantConfig.getDeptTenant()); + } + + @Override + protected String doGetTenantId() { + return Optional.ofNullable(AuthInfoUtils.getDeptTenantId()).orElse(""); + } + + + @Override + protected String getDefaultColumn() { + return "dept_tenant_id"; + } +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/FieldFillHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/FieldFillHandler.java new file mode 100644 index 0000000..5a81f0a --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/FieldFillHandler.java @@ -0,0 +1,43 @@ +package ai.remi.comm.jdbc.handler; + +import net.sf.jsqlparser.expression.Expression; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 字段填充拦截器 + */ +public interface FieldFillHandler { + + /** + * 获取自动填充值表达式 + */ + Expression getFieldFillValue(); + + /** + * 获取自动填充的字段名 + */ + String getFieldFillColumn(); + + /** + * 在创建的时候根据表名判断是否忽略拼接字段 + * + * @param tableName 表名 + * @return 是否忽略, true:表示忽略,false:需要解析并拼接 + */ + default boolean createIgnore(String tableName){ + return false; + } + + /** + * 在更新的时候根据表名判断是否忽略拼接字段 + * + * @param tableName 表名 + * @return 是否忽略, true:表示忽略,false:需要解析并拼接 + */ + default boolean updateIgnore(String tableName) { + return false; + } + + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/JsonTypeHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/JsonTypeHandler.java new file mode 100644 index 0000000..afb7f86 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/JsonTypeHandler.java @@ -0,0 +1,70 @@ +package ai.remi.comm.jdbc.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc Json类型转换器 + */ +public class JsonTypeHandler extends BaseTypeHandler { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + private Class type; + + public JsonTypeHandler(Class type) { + if (type == null) { + throw new NullPointerException("Type argument cannot be null"); + } + this.type = type; + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, toJsonString(parameter)); + } + + @Override + public T getNullableResult(ResultSet rs, String columnName) throws SQLException { + return parse(rs.getString(columnName)); + } + + + @Override + public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return parse(rs.getString(columnIndex)); + } + + @Override + public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return parse(cs.getString(columnIndex)); + } + + + private String toJsonString(Object parameter) { + try { + return objectMapper.writeValueAsString(parameter); + } catch (JsonProcessingException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + private T parse(String json) { + try { + return objectMapper.readValue(json, type); + } catch (JsonProcessingException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/ListTypeHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/ListTypeHandler.java new file mode 100644 index 0000000..e29ef61 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/ListTypeHandler.java @@ -0,0 +1,14 @@ +package ai.remi.comm.jdbc.handler; + +import java.util.List; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc List类型转换器 + */ +public class ListTypeHandler extends JsonTypeHandler { + public ListTypeHandler() { + super(List.class); + } +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/MapTypeHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/MapTypeHandler.java new file mode 100644 index 0000000..1cece29 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/MapTypeHandler.java @@ -0,0 +1,14 @@ +package ai.remi.comm.jdbc.handler; + +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc Map类型转换器 + */ +public class MapTypeHandler extends JsonTypeHandler { + public MapTypeHandler() { + super(Map.class); + } +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/MyMetaObjectHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/MyMetaObjectHandler.java new file mode 100644 index 0000000..d4d5263 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/MyMetaObjectHandler.java @@ -0,0 +1,32 @@ +package ai.remi.comm.jdbc.handler; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc MyMetaObjectHandler 配置类 + */ +@Slf4j +@Component +public class MyMetaObjectHandler implements MetaObjectHandler { + @Override + public void insertFill(MetaObject metaObject) { + log.info("start insert fill..."); + this.setFieldValByName("createdAt", LocalDateTime.now(), metaObject); + //this.setFieldValByName("createdBy", AuthInfoUtils.getUniqueId(), metaObject); + this.setFieldValByName("updatedAt", LocalDateTime.now(), metaObject); + //this.setFieldValByName("updatedBy", AuthInfoUtils.getUniqueId(), metaObject); + } + + @Override + public void updateFill(MetaObject metaObject) { + log.info("start update fill..."); + this.setFieldValByName("updatedAt", LocalDateTime.now(), metaObject); + } +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/UpdatedAtHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/UpdatedAtHandler.java new file mode 100644 index 0000000..c61ff21 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/UpdatedAtHandler.java @@ -0,0 +1,35 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.FieldFillConfig; +import ai.remi.comm.jdbc.enums.FieldFillStrategyEnum; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.TimestampValue; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 更新时间字段填充拦截器 + */ +public class UpdatedAtHandler extends AbstractFieldFillHandler { + + public UpdatedAtHandler(FieldFillConfig fieldFillConfig) { + super(FieldFillStrategyEnum.INSERT_UPDATE, fieldFillConfig.getUpdateAtIntercept()); + } + + + @Override + protected Expression doGetFieldFillValue() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String date = format.format(new Date()); + return new TimestampValue(date); + } + + @Override + protected String getDefaultColumn() { + return "updated_at"; + } + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/UpdatedByHandler.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/UpdatedByHandler.java new file mode 100644 index 0000000..b60c015 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/handler/UpdatedByHandler.java @@ -0,0 +1,37 @@ +package ai.remi.comm.jdbc.handler; + +import ai.remi.comm.jdbc.config.FieldFillConfig; +import ai.remi.comm.jdbc.enums.FieldFillStrategyEnum; +import ai.remi.comm.util.auth.AuthInfo; +import ai.remi.comm.util.auth.AuthInfoUtils; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; + +import java.util.Optional; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 更新人字段填充拦截器 + */ +public class UpdatedByHandler extends AbstractFieldFillHandler { + + public UpdatedByHandler(FieldFillConfig fieldFillConfig) { + super(FieldFillStrategyEnum.INSERT_UPDATE, fieldFillConfig.getUpdateByIntercept()); + } + + + @Override + protected Expression doGetFieldFillValue() { + AuthInfo authInfo = AuthInfoUtils.getAuthInfo(); + String uniqueId = Optional.ofNullable(authInfo) + .map(AuthInfo::getUniqueId).orElse("系统创建"); + return new StringValue(uniqueId); + } + + @Override + protected String getDefaultColumn() { + return "updated_by"; + } + +} diff --git a/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/interceptor/FieldFillInterceptor.java b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/interceptor/FieldFillInterceptor.java new file mode 100644 index 0000000..632f811 --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/java/ai/remi/comm/jdbc/interceptor/FieldFillInterceptor.java @@ -0,0 +1,430 @@ +package ai.remi.comm.jdbc.interceptor; + + +import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper; +import com.baomidou.mybatisplus.core.toolkit.*; +import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import com.baomidou.mybatisplus.extension.toolkit.PropertyMapper; +import ai.remi.comm.jdbc.handler.FieldFillHandler; +import lombok.*; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.*; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; + +import java.sql.Connection; +import java.util.List; +import java.util.Properties; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 字段填充拦截器 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class FieldFillInterceptor extends JsqlParserSupport implements InnerInterceptor { + + private FieldFillHandler fieldFillHandler; + + /** + * 拦截器处理前 + * + * @param sh + * @param connection + * @param transactionTimeout + */ + @Override + public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { + PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); + MappedStatement ms = mpSh.mappedStatement(); + SqlCommandType sct = ms.getSqlCommandType(); + if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { + if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) { + return; + } + PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); + mpBs.sql(parserMulti(mpBs.sql(), null)); + } + } + + /** + * 查询进程拦截器 + * + * @param select + * @param index + * @param sql + * @param obj + */ + @Override + protected void processSelect(Select select, int index, String sql, Object obj) { + + } + + /** + * 删除进程拦截器 + * + * @param delete + * @param index + * @param sql + * @param obj + */ + @Override + protected void processDelete(Delete delete, int index, String sql, Object obj) { + + } + + /** + * 查询属性进程拦截器 + * + * @param selectBody + */ + protected void processSelectBody(SelectBody selectBody) { + if (selectBody == null) { + return; + } + if (selectBody instanceof PlainSelect) { + processPlainSelect((PlainSelect) selectBody); + } else if (selectBody instanceof WithItem) { + WithItem withItem = (WithItem) selectBody; + processSelectBody(withItem.getSubSelect().getSelectBody()); + } else { + SetOperationList operationList = (SetOperationList) selectBody; + List selectBodys = operationList.getSelects(); + if (CollectionUtils.isNotEmpty(selectBodys)) { + selectBodys.forEach(this::processSelectBody); + } + } + } + + /** + * 新增进程拦截器 + * + * @param insert + * @param index + * @param sql + * @param obj + */ + @Override + protected void processInsert(Insert insert, int index, String sql, Object obj) { + if (fieldFillHandler.createIgnore(insert.getTable().getName())) { + // 过滤退出执行 + return; + } + //获取所有字段 + List columns = insert.getColumns(); + if (CollectionUtils.isEmpty(columns)) { + // 针对没有任何列的insert 不处理 + return; + } + //获取操作人字段 + String FieldFillColumn = fieldFillHandler.getFieldFillColumn(); + if (columns.stream().map(Column::getColumnName).anyMatch(i -> i.equals(FieldFillColumn))) { + // 针对已给出操作人列的insert 不处理 防止重复字段 + return; + } + columns.add(new Column(fieldFillHandler.getFieldFillColumn())); + + //获取重复表达式 列表 + List duplicateUpdateColumns = insert.getDuplicateUpdateExpressionList(); + if (CollectionUtils.isNotEmpty(duplicateUpdateColumns)) { + EqualsTo equalsTo = new EqualsTo(); + equalsTo.setLeftExpression(new StringValue(fieldFillHandler.getFieldFillColumn())); + equalsTo.setRightExpression(fieldFillHandler.getFieldFillValue()); + duplicateUpdateColumns.add(equalsTo); + } + + Select select = insert.getSelect(); + if (select != null) { + this.processInsertSelect(select.getSelectBody()); + } else if (insert.getItemsList() != null) { + ItemsList itemsList = insert.getItemsList(); + if (itemsList instanceof MultiExpressionList) { + // 支持批量操作 + ((MultiExpressionList) itemsList).getExprList() + .forEach(el -> el.getExpressions().add(fieldFillHandler.getFieldFillValue())); + } else { + ((ExpressionList) itemsList).getExpressions().add(fieldFillHandler.getFieldFillValue()); + } + } else { + throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId"); + } + } + + /** + * update 语句处理 + */ + @Override + protected void processUpdate(Update update, int index, String sql, Object obj) { + final Table table = update.getTable(); + if (fieldFillHandler.updateIgnore(table.getName())) { + // 过滤退出执行 + return; + } + List columns = update.getColumns(); + if (CollectionUtils.isEmpty(columns)) { + // 针对没有任何列的 update 不处理 + return; + } + String FieldFillColumn = fieldFillHandler.getFieldFillColumn(); + + if (columns.stream().map(Column::getColumnName).anyMatch(i -> i.equals(FieldFillColumn))) { + // 针对已给出操作人列的 update 不处理 防止重复字段 + return; + } + //这里出问题了,已修复,使用addUpdateSet()实现动态添加更新人和更新时间 + //columns.add(new Column(fieldFillHandler.getFieldFillColumn())); + //update.getExpressions().add(fieldFillHandler.getFieldFillValue()); + update.addUpdateSet(new Column(fieldFillHandler.getFieldFillColumn()), fieldFillHandler.getFieldFillValue()); + + } + + + /** + * 处理 insert into select + *

+ * 进入这里表示需要 insert 的表启用了操作人插件,则 select 的表都启动了 + * + * @param selectBody SelectBody + */ + protected void processInsertSelect(SelectBody selectBody) { + PlainSelect plainSelect = (PlainSelect) selectBody; + FromItem fromItem = plainSelect.getFromItem(); + if (fromItem instanceof Table) { + processPlainSelect(plainSelect); + appendSelectItem(plainSelect.getSelectItems()); + } else if (fromItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) fromItem; + appendSelectItem(plainSelect.getSelectItems()); + processInsertSelect(subSelect.getSelectBody()); + } + } + + /** + * 追加 SelectItem + * + * @param selectItems SelectItem + */ + protected void appendSelectItem(List selectItems) { + if (CollectionUtils.isEmpty(selectItems)) { + return; + } + if (selectItems.size() == 1) { + SelectItem item = selectItems.get(0); + if (item instanceof AllColumns || item instanceof AllTableColumns) { + return; + } + } + selectItems.add(new SelectExpressionItem(new Column(fieldFillHandler.getFieldFillColumn()))); + } + + /** + * 处理 PlainSelect + */ + protected void processPlainSelect(PlainSelect plainSelect) { + FromItem fromItem = plainSelect.getFromItem(); + Expression where = plainSelect.getWhere(); + processWhereSubSelect(where); + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + if (!fieldFillHandler.createIgnore(fromTable.getName())) { + plainSelect.setWhere(builderExpression(where, fromTable)); + } + } else { + processFromItem(fromItem); + } + + List selectItems = plainSelect.getSelectItems(); + if (CollectionUtils.isNotEmpty(selectItems)) { + selectItems.forEach(this::processSelectItem); + } + List joins = plainSelect.getJoins(); + if (CollectionUtils.isNotEmpty(joins)) { + joins.forEach(j -> { + processJoin(j); + processFromItem(j.getRightItem()); + }); + } + } + + /** + * 处理where条件内的子查询 + *

+ * 支持如下: + * 1. in + * 2. = + * 3. > + * 4. < + * 5. >= + * 6. <= + * 7. <> + * 8. EXISTS + * 9. NOT EXISTS + *

+ * 前提条件: + * 1. 子查询必须放在小括号中 + * 2. 子查询一般放在比较操作符的右边 + * + * @param where where 条件 + */ + protected void processWhereSubSelect(Expression where) { + if (where == null) { + return; + } + if (where instanceof FromItem) { + processFromItem((FromItem) where); + return; + } + if (where.toString().indexOf("SELECT") > 0) { + // 有子查询 + if (where instanceof BinaryExpression) { + // 比较符号 , and , or , 等等 + BinaryExpression expression = (BinaryExpression) where; + processWhereSubSelect(expression.getLeftExpression()); + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof InExpression) { + // in + InExpression expression = (InExpression) where; + ItemsList itemsList = expression.getRightItemsList(); + if (itemsList instanceof SubSelect) { + processSelectBody(((SubSelect) itemsList).getSelectBody()); + } + } else if (where instanceof ExistsExpression) { + // exists + ExistsExpression expression = (ExistsExpression) where; + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof NotExpression) { + // not exists + NotExpression expression = (NotExpression) where; + processWhereSubSelect(expression.getExpression()); + } else if (where instanceof Parenthesis) { + Parenthesis expression = (Parenthesis) where; + processWhereSubSelect(expression.getExpression()); + } + } + } + + protected void processSelectItem(SelectItem selectItem) { + if (selectItem instanceof SelectExpressionItem) { + SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; + if (selectExpressionItem.getExpression() instanceof SubSelect) { + processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); + } else if (selectExpressionItem.getExpression() instanceof Function) { + processFunction((Function) selectExpressionItem.getExpression()); + } + } + } + + /** + * 处理函数 + *

支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)

+ * + * @param function + */ + protected void processFunction(Function function) { + ExpressionList parameters = function.getParameters(); + if (parameters != null) { + parameters.getExpressions().forEach(expression -> { + if (expression instanceof SubSelect) { + processSelectBody(((SubSelect) expression).getSelectBody()); + } else if (expression instanceof Function) { + processFunction((Function) expression); + } + }); + } + } + + /** + * 处理子查询等 + */ + protected void processFromItem(FromItem fromItem) { + if (fromItem instanceof SubJoin) { + SubJoin subJoin = (SubJoin) fromItem; + if (subJoin.getJoinList() != null) { + subJoin.getJoinList().forEach(this::processJoin); + } + if (subJoin.getLeft() != null) { + processFromItem(subJoin.getLeft()); + } + } else if (fromItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) fromItem; + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } else if (fromItem instanceof ValuesList) { + logger.debug("Perform a subquery, if you do not give us feedback"); + } else if (fromItem instanceof LateralSubSelect) { + LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; + if (lateralSubSelect.getSubSelect() != null) { + SubSelect subSelect = lateralSubSelect.getSubSelect(); + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } + } + } + + /** + * 处理联接语句 + */ + protected void processJoin(Join join) { + if (join.getRightItem() instanceof Table) { + Table fromTable = (Table) join.getRightItem(); + if (fieldFillHandler.createIgnore(fromTable.getName())) { + // 过滤退出执行 + return; + } + join.setOnExpression(builderExpression(join.getOnExpression(), fromTable)); + } + } + + /** + * 处理条件 + */ + protected Expression builderExpression(Expression currentExpression, Table table) { + EqualsTo equalsTo = new EqualsTo(); + equalsTo.setLeftExpression(this.getAliasColumn(table)); + equalsTo.setRightExpression(fieldFillHandler.getFieldFillValue()); + if (currentExpression == null) { + return equalsTo; + } + if (currentExpression instanceof OrExpression) { + return new AndExpression(new Parenthesis(currentExpression), equalsTo); + } else { + return new AndExpression(currentExpression, equalsTo); + } + } + + /** + * 字段别名设置 + * + * @param table 表对象 + * @return 字段 + */ + protected Column getAliasColumn(Table table) { + StringBuilder column = new StringBuilder(); + if (table.getAlias() != null) { + column.append(table.getAlias().getName()).append(StringPool.DOT); + } + column.append(fieldFillHandler.getFieldFillColumn()); + return new Column(column.toString()); + } + + @Override + public void setProperties(Properties properties) { + PropertyMapper.newInstance(properties).whenNotBlank("fieldFillHandler", ClassUtils::newInstance, this::setFieldFillHandler); + } +} diff --git a/remi-comm/remi-comm-jdbc/src/main/resources/META-INF/spring.factories b/remi-comm/remi-comm-jdbc/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..5532e7d --- /dev/null +++ b/remi-comm/remi-comm-jdbc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,9 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + ai.remi.comm.jdbc.config.AutoConfig,\ + ai.remi.comm.jdbc.config.DruidConfig,\ + ai.remi.comm.jdbc.config.FieldFillConfig,\ + ai.remi.comm.jdbc.config.InterceptConfig,\ + ai.remi.comm.jdbc.config.MybatisPlusConfig,\ + ai.remi.comm.jdbc.config.TenantConfig + + diff --git a/remi-comm/remi-comm-job/pom.xml b/remi-comm/remi-comm-job/pom.xml new file mode 100644 index 0000000..a663bc9 --- /dev/null +++ b/remi-comm/remi-comm-job/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + remi-comm-job + remi-comm-job + remi-comm-job + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + com.xuxueli + xxl-job-core + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.slf4j + slf4j-api + + + + diff --git a/remi-comm/remi-comm-job/src/main/java/ai/remi/comm/job/config/XxlJobConfiguration.java b/remi-comm/remi-comm-job/src/main/java/ai/remi/comm/job/config/XxlJobConfiguration.java new file mode 100644 index 0000000..cf13d6b --- /dev/null +++ b/remi-comm/remi-comm-job/src/main/java/ai/remi/comm/job/config/XxlJobConfiguration.java @@ -0,0 +1,60 @@ +package ai.remi.comm.job.config; + +import ai.remi.comm.job.properties.XxlJobProperties; +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; + +/** + * 定时任务配置 + * + * @author dianjiu + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(value = XxlJobProperties.class) +@ConditionalOnProperty(value = "xxl-job.enabled", havingValue = "true") +public class XxlJobConfiguration { + + @Resource + private XxlJobProperties xxlJobProperties; + + /** + * 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP; + *

+ * 1、引入依赖: + * + * org.springframework.cloud + * spring-cloud-commons + * ${version} + * + *

+ * 2、配置文件,或者容器启动变量 + * spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.' + *

+ * 3、获取IP + * String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress(); + */ + @Bean + public XxlJobSpringExecutor xxlJobExecutor() { + log.info(">>>>>>>>>>> xxl-job config init start."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdminAddresses()); + xxlJobSpringExecutor.setAppname(xxlJobProperties.getExecutor().getAppname()); + xxlJobSpringExecutor.setAddress(xxlJobProperties.getExecutor().getAddress()); + xxlJobSpringExecutor.setIp(xxlJobProperties.getExecutor().getIp()); + xxlJobSpringExecutor.setPort(xxlJobProperties.getExecutor().getPort()); + xxlJobSpringExecutor.setAccessToken(xxlJobProperties.getAccessToken()); + xxlJobSpringExecutor.setLogPath(xxlJobProperties.getExecutor().getLogPath()); + xxlJobSpringExecutor.setLogRetentionDays(xxlJobProperties.getExecutor().getLogRetentionDays()); + log.info(">>>>>>>>>>> xxl-job config init end."); + return xxlJobSpringExecutor; + } + + +} diff --git a/remi-comm/remi-comm-job/src/main/java/ai/remi/comm/job/properties/XxlJobProperties.java b/remi-comm/remi-comm-job/src/main/java/ai/remi/comm/job/properties/XxlJobProperties.java new file mode 100644 index 0000000..b184297 --- /dev/null +++ b/remi-comm/remi-comm-job/src/main/java/ai/remi/comm/job/properties/XxlJobProperties.java @@ -0,0 +1,63 @@ +package ai.remi.comm.job.properties; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * xxl-job的配置属性 + * + * @author dianjiu + * 2021-12-28 09:40:38 + */ +@Data +@ConfigurationProperties(prefix = "xxl-job") +public class XxlJobProperties { + /** + * 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; + * eg:http://127.0.0.1:8080/xxl-job-admin + */ + private String adminAddresses; + /** + * 执行器通讯TOKEN [选填]:非空时启用; + */ + private String accessToken; + + private Boolean enabled; + + private Executor executor; + + @Getter + @Setter + public static class Executor { + /** + * 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 + * eg:remi-boot-job + */ + private String appname; + /** + * 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。 + */ + private String address; + /** + * 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; + */ + private String ip; + /** + * 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; + */ + private int port; + /** + * 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; + */ + private String logPath; + /** + * 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能; + */ + private int logRetentionDays; + + } + + +} diff --git a/remi-comm/remi-comm-job/src/main/resources/META-INF/spring.factories b/remi-comm/remi-comm-job/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..7a6e245 --- /dev/null +++ b/remi-comm/remi-comm-job/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +ai.remi.comm.job.config.XxlJobConfiguration \ No newline at end of file diff --git a/remi-comm/remi-comm-log/README.md b/remi-comm/remi-comm-log/README.md new file mode 100644 index 0000000..de5d40c --- /dev/null +++ b/remi-comm/remi-comm-log/README.md @@ -0,0 +1,69 @@ +# 工程简介 +瑞米平台公共的日志模块 + +## 操作日志 +主要面向用户的,要求简单易懂,反映出用户所做的动作。 + +### 核心功能 +- 要实现以下功能: + + - 自定义spring boot starter + - 定义日志注解 + - AOP拦截日志注解方法 + - 定义日志动态内容模板 + +- 模板中又需要实现: + - 动态模板表达式解析:用强大的SpEL来解析表达式 + - 自定义函数:支持目标方法前置/后置的自定义函数 + +## + +### SQL脚本 +```sql +DROP TABLE IF EXISTS t_system_log; +CREATE TABLE t_system_log +( + id VARCHAR(32) NOT NULL COMMENT '日志编号', + user_id VARCHAR(32) COMMENT '用户ID', + user_name VARCHAR(255) COMMENT '用户名称', + app_code VARCHAR(255) COMMENT '应用编码', + app_name VARCHAR(255) COMMENT '应用名称', + business_no VARCHAR(255) COMMENT '业务流水号', + business_type VARCHAR(255) COMMENT '业务类型(新增、修改、删除、查询、导入、导出、其他)', + log_content LONGTEXT COMMENT '日志模板内容', + log_extras LONGTEXT COMMENT '额外的日志内容', + request_url VARCHAR(255) COMMENT '请求地址', + request_ip VARCHAR(255) COMMENT '请求IP', + request_type VARCHAR(255) COMMENT '请求类型(POST、PUT、OPTIONS、DELETE)', + content_type VARCHAR(255) COMMENT '请求报文类型(map、json)', + request_body LONGTEXT COMMENT '请求报文', + response_body LONGTEXT COMMENT '响应报文', + request_time DATETIME COMMENT '请求时间', + response_time DATETIME COMMENT '响应时间', + execute_time BIGINT COMMENT '请求耗时', + trace_id VARCHAR(255) COMMENT '链路追踪ID', + error_info LONGTEXT COMMENT '异常信息', + user_agent VARCHAR(255) COMMENT '浏览器、操作系统、UI引擎标识', + revision INTEGER(11) COMMENT '乐观锁', + is_deleted INTEGER(1) COMMENT '逻辑删除;0-未删除 1-已删除', + group_tenant_id VARCHAR(32) COMMENT '部门租户ID', + company_tenant_id VARCHAR(32) COMMENT '公司租户ID', + created_by VARCHAR(32) COMMENT '创建人', + created_at DATETIME COMMENT '创建时间', + updated_by VARCHAR(32) COMMENT '更新人', + updated_at DATETIME COMMENT '更新时间', + PRIMARY KEY (id) +) COMMENT = '系统日志表'; +``` +## 系统日志 +主要用于开发者调试排查系统问题的,不要求固定格式和可读性。 + +### + +## 延伸阅读 +https://tech.meituan.com/2021/09/16/operational-logbook.html + +https://juejin.cn/post/7075184311397253128 + +http://mysql.taobao.org/monthly/2017/09/08/ + diff --git a/remi-comm/remi-comm-log/pom.xml b/remi-comm/remi-comm-log/pom.xml new file mode 100644 index 0000000..e2bfae0 --- /dev/null +++ b/remi-comm/remi-comm-log/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + remi-comm-log + remi-comm-log + remi-comm-log + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + ai.remi.top.cloud + remi-comm-core + + + ai.remi.top.cloud + remi-comm-util + + + ai.remi.top.cloud + remi-comm-redis + + + + ai.remi.top.cloud + remi-sys-api + + + + org.projectlombok + lombok + + + diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/annotation/EnableLogRecord.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/annotation/EnableLogRecord.java new file mode 100644 index 0000000..66e8eb2 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/annotation/EnableLogRecord.java @@ -0,0 +1,23 @@ +package ai.remi.comm.log.annotation; + +import ai.remi.comm.log.config.LogRecordAutoConfigure; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + * @desc 日志记录开启注解 + */ +@Documented +@Inherited +@Order(Integer.MIN_VALUE) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(LogRecordAutoConfigure.class) +public @interface EnableLogRecord { + +} \ No newline at end of file diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/annotation/LogRecord.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/annotation/LogRecord.java new file mode 100644 index 0000000..e867461 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/annotation/LogRecord.java @@ -0,0 +1,74 @@ +package ai.remi.comm.log.annotation; + + +import ai.remi.comm.log.enums.BusinessType; +import ai.remi.comm.log.enums.OperatorType; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + * @desc 操作日志记录 + */ +@Inherited +@Documented +@Order(Integer.MIN_VALUE) +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface LogRecord { + + /** + * 接口名称 + */ + String content() default ""; + + /** + * 功能操作类型 + */ + BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + boolean isSaveResponseData() default true; + + /** + * 是否保存异常的信息 + */ + boolean isSaveErrorInfo() default true; + + /** + * 在方法成功执行后打点,记录方法的执行时间发送到指标系统,默认关闭 + * + * @return + */ + boolean recordSuccessMetrics() default false; + + /** + * 在方法成功失败后打点,记录方法的执行时间发送到指标系统,默认关闭 + */ + boolean recordFailMetrics() default false; + + /** + * 出现异常后忽略异常返回默认值,默认关闭 + */ + boolean ignoreException() default false; + + /** + * 排除指定的请求参数 + */ + public String[] excludeParamNames() default {}; +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/aspect/LogRecordAspect.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/aspect/LogRecordAspect.java new file mode 100644 index 0000000..a2d6e53 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/aspect/LogRecordAspect.java @@ -0,0 +1,399 @@ +package ai.remi.comm.log.aspect; + +import ai.remi.comm.core.spring.SpringProperties; +import ai.remi.comm.log.annotation.LogRecord; +import ai.remi.comm.log.domain.OperateLog; +import ai.remi.comm.log.enums.BusinessStatus; +import ai.remi.comm.log.filter.PropertyPreExcludeFilter; +import ai.remi.comm.log.service.AsyncLogService; +import ai.remi.comm.redis.service.RedisService; +import ai.remi.comm.util.ip.IpAddrUtils; +import ai.remi.comm.util.ip.IpInfoUtils; +import ai.remi.comm.util.string.StringUtils; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import ai.remi.comm.log.enums.RedisKeyEnum; +import eu.bitwalker.useragentutils.UserAgent; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.MapUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.NamedThreadLocal; +import org.springframework.http.HttpMethod; +import org.springframework.validation.BindingResult; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 操作日志记录处理 + */ +@Slf4j +@Aspect +public class LogRecordAspect { + /** + * 排除敏感属性字段 + */ + public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"}; + + private final ThreadLocal> INFO_THREADLOCAL = new ThreadLocal>(); + + /** + * 计算操作消耗时间 + */ + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal<>("Cost Time"); + + @Resource + private AsyncLogService asyncLogService; + + @Resource + private RedisService redisService; + + /** + * 处理请求前执行 + */ + @Before(value = "@annotation(logRecord)") + public void boBefore(JoinPoint joinPoint, LogRecord logRecord) { + TIME_THREADLOCAL.set(System.currentTimeMillis()); + //登出接口需要前置储存用户信息 + handleLog(joinPoint, logRecord); + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(logRecord)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, LogRecord logRecord, Object jsonResult) { + handleLog(joinPoint, logRecord, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(logRecord)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, LogRecord logRecord, Exception e) { + handleLog(joinPoint, logRecord, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, LogRecord logRecord) { + // 传递线程信息 + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + RequestContextHolder.setRequestAttributes(requestAttributes, true); + HttpServletRequest request = requestAttributes.getRequest(); + // 记录请求内容,threadInfo存储所有内容 + Map threadInfo = new HashMap<>(); + //该方法用于获取客户端发出请求时的完整URL,包括协议、服务器名、端口号、资源路径等信息,但不包括后面的查询参数部分。 + // 注意,getRequestRUL()方法返回的时StringBuffer类型,而不是String类型。 + threadInfo.put("url", request.getRequestURL()); + //该方法用于获取请求行中资源名称部分,即位于URL的主机和端口之后、参数部分之前的部分 + threadInfo.put("uri", request.getRequestURI()); + //该方法用于获取HTTP请求消息中的请求方式(如GET、POST等) + threadInfo.put("httpMethod", request.getMethod()); + //该方法用于获取请求的协议名,例如http、https或ftp + threadInfo.put("scheme", request.getScheme()); + //该方法用于获取请求客户端的IP地址,其格式类似于"192.168.0.1" + //threadInfo.put("ip", request.getRemoteAddr()); + threadInfo.put("ip", IpAddrUtils.getIpAddr(request)); + //检查是否为内网IP + String ipAddress = IpAddrUtils.internalIp(IpAddrUtils.getIpAddr(request)) ? "内网IP" : IpInfoUtils.getInfo(IpAddrUtils.getIpAddr(request)).getProvince() + "-" + IpInfoUtils.getInfo(IpAddrUtils.getIpAddr(request)).getCity(); + threadInfo.put("ipAddress", ipAddress); + //该方法用于获取Content-Type头字段的值,结果为String类型 + threadInfo.put("contentType", request.getContentType()); + //包含了一个特征字符串,用来让网络协议的对端来识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。 + //UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); + threadInfo.put("platform", userAgent.getOperatingSystem().getName()); + threadInfo.put("browser", userAgent.getBrowser().getName()); + //从请求头获取操作人 + threadInfo.put("operator", request.getHeader("X-Unique-Id")); + //从请求头获取部门租户 + threadInfo.put("deptTenantId", request.getHeader("X-Dept-Tenant-Id")); + //从请求头获取公司租户 + threadInfo.put("companyTenantId", request.getHeader("X-Company-Tenant-Id")); + //从请求头获取操作人 + String accessToken = request.getHeader("X-Access-Token"); + threadInfo.put("token", accessToken); + threadInfo.put("userId", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "id")); + threadInfo.put("userCode", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userCode")); + threadInfo.put("userName", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userName")); + threadInfo.put("userNameEn", redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userNameEn")); + //从请求头获取系统语言 + threadInfo.put("language", request.getHeader("X-USER-LANGUAGE")); + //从请求头获取业务流水号 + threadInfo.put("businessNo", request.getHeader("X-Business-No")); + INFO_THREADLOCAL.set(threadInfo); + } + + protected void handleLog(final JoinPoint joinPoint, LogRecord logRecord, final Exception e, Object jsonResult) { + try { + // *========解析请求信息=========*// + OperateLog operateLog = new OperateLog(); + + // *========获取注解信息=========*// + //获取方法签名 + //MethodSignature signature = getSignature(joinPoint); + //获取注解方法 + Method method = getMethod(joinPoint); + //获取注解对象 + //LogRecord logRecord = getAnnotation(joinPoint, LogRecord.class); + // 获取连接点所在类名 + String classname = joinPoint.getTarget().getClass().getSimpleName(); + // 获取连接点所在方法名称 + String methodName = method.getName(); + String name = String.format("%s.%s()", classname, methodName); + // 保存请求方法 + operateLog.setRequestMethod(name); + + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, logRecord, operateLog, jsonResult); + + // 是否需要保存request,参数和值 + if (logRecord.isSaveRequestData()) { + // 获取参数的信息,传入到数据库中 + setRequestValue(joinPoint, operateLog, logRecord.excludeParamNames()); + } + + // 是否需要保存response,参数和值 + if (logRecord.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) { + String result = JSON.toJSONString(jsonResult); + // 对登录日志的特殊处理 + if (operateLog.getBusinessType()== 9) { + Object data = JSON.parseObject(result).get("data"); + String accessToken = String.valueOf(JSON.parseObject(JSON.toJSONString(data)).get("accessToken")); + setOperateUser(accessToken, operateLog); + } + operateLog.setStatus(String.valueOf(JSON.parseObject(result).get("code"))); + operateLog.setResponseBody(result); + } + + //请求的成功或失败 + if (e != null) { + operateLog.setStatus(BusinessStatus.FAIL.getCode()); + // 是否需要保存errorMsg,参数和值 + if (logRecord.isSaveErrorInfo()) { + operateLog.setStatus("111111"); + operateLog.setErrorInfo(StrUtil.sub(e.getMessage(), 0, 5000)); + } + } + + // 保存数据库 + //asyncLogService.saveOperateLog(operateLog); + } catch (Exception exp) { + // 记录本地异常日志 + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } finally { + TIME_THREADLOCAL.remove(); + INFO_THREADLOCAL.remove(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param logRecord 日志切面 + * @param operateLog 操作日志 + */ + public void getControllerMethodDescription(JoinPoint joinPoint, LogRecord logRecord, OperateLog operateLog, Object jsonResult) throws Exception { + Map objectMap = INFO_THREADLOCAL.get(); + // 业务模块 + operateLog.setAppId(SpringProperties.getString("log.record.appId")); + operateLog.setAppCode(SpringProperties.getString("log.record.appCode")); + // 操作方法名称或描述 + operateLog.setContent(logRecord.content()); + // 业务操作类型 + operateLog.setBusinessType(logRecord.businessType().getCode()); + // 操作人类别 + operateLog.setOperatorType(logRecord.operatorType().getCode()); + // 操作人IP + operateLog.setUserIp(String.valueOf(objectMap.get("ip"))); + operateLog.setIpAddress(String.valueOf(objectMap.get("ipAddress"))); + // 操作人Id + //operateLog.setUserId(Objects.nonNull(objectMap.get("operator")) ? String.valueOf(objectMap.get("operator")) : null); + // 操作人信息 + operateLog.setUserId(Objects.nonNull(objectMap.get("userId")) ? String.valueOf(objectMap.get("userId")) : null); + operateLog.setUserCode(Objects.nonNull(objectMap.get("userCode")) ? String.valueOf(objectMap.get("userCode")) : null); + operateLog.setUserName(Objects.nonNull(objectMap.get("userName")) ? String.valueOf(objectMap.get("userName")) : null); + operateLog.setUserNameEn(Objects.nonNull(objectMap.get("userNameEn")) ? String.valueOf(objectMap.get("userNameEn")) : null); + // 用户系统语言 + operateLog.setUserLanguage(Objects.nonNull(objectMap.get("language")) ? String.valueOf(objectMap.get("language")) : null); + // 请求地址 + operateLog.setRequestUrl(String.valueOf(objectMap.get("url"))); + // 请求类型 + operateLog.setRequestType(String.valueOf(objectMap.get("httpMethod"))); + // 请求报文类型 + operateLog.setContentType(Objects.nonNull(objectMap.get("contentType")) ? String.valueOf(objectMap.get("contentType")) : null); + // 业务流水号 + operateLog.setBusinessNo(Objects.nonNull(objectMap.get("businessNo")) ? String.valueOf(objectMap.get("businessNo")) : null); + // 链路追踪ID + //logRecordDTO.setTraceId(); + // 浏览器、操作系统、UI引擎标识 + operateLog.setPlatform(String.valueOf(objectMap.get("platform"))); + operateLog.setBrowser(String.valueOf(objectMap.get("browser"))); + // 部门租户 + operateLog.setDeptTenantId(String.valueOf(objectMap.get("deptTenantId"))); + // 公司租户 + operateLog.setCompanyTenantId(String.valueOf(objectMap.get("companyTenantId"))); + // 设置消耗时间 + operateLog.setRequestTime(LocalDateTime.ofEpochSecond(TIME_THREADLOCAL.get() / 1000, 0, ZoneOffset.ofHours(8))); + operateLog.setResponseTime(LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, ZoneOffset.ofHours(8))); + operateLog.setExecuteTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); + } + + /** + * 保存操作人信息 + * + * @param accessToken + * @param operateLog + */ + private void setOperateUser(String accessToken, OperateLog operateLog) { + if (StringUtils.isNotBlank(accessToken)) { + String id = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "id")); + operateLog.setUserId(id); + String userCode = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userCode")); + operateLog.setUserCode(userCode); + String userName = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userName")); + operateLog.setUserName(userName); + String userNameEn = String.valueOf(redisService.hget(RedisKeyEnum.USER_INFO.getKey(accessToken), "userNameEn")); + operateLog.setUserNameEn(userNameEn); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operateLog 操作日志 + * @param excludeParamNames 排除指定的请求参数 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, OperateLog operateLog, String[] excludeParamNames) throws Exception { + // 获取RequestAttributes + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + // 从获取RequestAttributes中获取HttpServletRequest的信息 + HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); + // 获取请求类型 + // String requestType = operateLog.getRequestType(); + String requestType = request.getMethod(); + // 获取请求参数 + Map paramsMap = converMap(request.getParameterMap()); + // 组装请求参数 + if (MapUtils.isEmpty(paramsMap) && (HttpMethod.PUT.name().equals(requestType) || HttpMethod.POST.name().equals(requestType))) { + String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); + operateLog.setRequestBody(params); + } else { + operateLog.setRequestBody(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames))); + } + } + + /** + * 请求参数 转换 + * + * @param paramMap request获取的参数数组 + */ + public Map converMap(Map paramMap) { + Map returnMap = new HashMap<>(); + for (String key : paramMap.keySet()) { + returnMap.put(key, paramMap.get(key)[0]); + } + return returnMap; + } + + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) { + StringBuilder params = new StringBuilder(); + if (ArrayUtil.isNotEmpty(paramsArray)) { + for (Object o : paramsArray) { + if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) { + try { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); + params.append(jsonObj).append(StrUtil.SPACE); + } catch (Exception ignored) { + } + } + } + } + return params.toString().trim(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) { + return new PropertyPreExcludeFilter().addExcludes(ArrayUtil.addAll(EXCLUDE_PROPERTIES, excludeParamNames)); + } + + /** + * 判断是否需要过滤的对象 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) { + Class clazz = o.getClass(); + if (clazz.isArray()) { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection collection = (Collection) o; + for (Object value : collection) { + return value instanceof MultipartFile; + } + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = (Map) o; + for (Object value : map.entrySet()) { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } + + private MethodSignature getSignature(JoinPoint joinPoint) { + //通过连接点获取方法签名 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + return signature; + } + + private Method getMethod(JoinPoint joinPoint) { + //通过连接点获取方法签名 + MethodSignature signature = getSignature(joinPoint); + //获取注解服务的方法 + Method method = signature.getMethod(); + return method; + } + + private T getAnnotation(JoinPoint joinPoint, Class annotationClass) { + Method method = getMethod(joinPoint); + //获取注解类 + T annotation = method.getAnnotation(annotationClass); + return annotation; + } +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/config/LogRecordAutoConfigure.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/config/LogRecordAutoConfigure.java new file mode 100644 index 0000000..4ed55f0 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/config/LogRecordAutoConfigure.java @@ -0,0 +1,56 @@ +package ai.remi.comm.log.config; + +import ai.remi.comm.log.aspect.LogRecordAspect; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + * @desc + */ +@Slf4j +@RefreshScope +@Configuration +public class LogRecordAutoConfigure { + /** + * 是否开启监控配置参数 + */ + @Value("${remi-log.record.enabled}") + private Boolean enabled; + + /** + * 日志监控的应用名称 + */ + @Value("${remi-log.record.appName}") + private String appName; + + /** + * 在方法成功执行后打点,记录方法的执行时间发送到指标系统,默认关闭 + * 在方法成功失败后打点,记录方法的执行时间发送到指标系统,默认关闭 + */ + @Value("${remi-log.record.pushUrl}") + private String pushUrl; + + + /** + * 根据运行环境决定是否开启日志记录监控 + */ + @PostConstruct + protected LogRecordAspect init() { + if(!enabled){ + return null; + } + if (enabled && StringUtils.isBlank(appName)) { + log.error("Please config the remi-log.record property in application.yml file to enabled log record, but appName is null."); + throw new RuntimeException("Please config the remi-log.record property in application.yml file to config log record appName."); + } + return new LogRecordAspect(); + } +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/domain/OperateLog.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/domain/OperateLog.java new file mode 100644 index 0000000..5432bf5 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/domain/OperateLog.java @@ -0,0 +1,176 @@ +package ai.remi.comm.log.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 系统日志表 + *

+ * + * @author 点九 + * @since 2022-07-20 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString(callSuper = true) +public class OperateLog implements Serializable { + + private static final long serialVersionUID = -85509022761738038L; + + /** + * 业务模块,区分不同业务模块的应用ID + */ + private String appId; + + /** + * 业务模块,区分不同业务模块的应用编码 + */ + private String appCode; + + /** + * 操作方法名称或描述 + */ + private String content; + + /** + * 请求状态 成功 or 失败 + */ + private String status; + + /** + * 操作人类别 + */ + private Integer operatorType; + + /** + * 业务操作类型 + */ + private Integer businessType; + + /** + * 操作人Id + */ + private String userId; + + /** + * 操作人编码 + */ + private String userCode; + + /** + * 操作人员名称 + */ + private String userName; + + /** + * 操作人员英文名称 + */ + private String userNameEn; + + /** + * 请求地址 + */ + private String requestUrl; + + /** + * 请求方法 + */ + private String requestMethod; + + /** + * 请求类型(POST、PUT、OPTIONS、DELETE) + */ + private String requestType; + + /** + * 请求报文类型(map、json) + */ + private String contentType; + + /** + * 请求报文 + */ + private String requestBody; + + /** + * 响应报文 + */ + private String responseBody; + + /** + * 异常信息 + */ + private String errorInfo; + + /** + * 请求时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime requestTime; + + /** + * 响应时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime responseTime; + + /** + * 操作花费的时间 单位:ms + */ + private Long executeTime; + + /** + * 用户系统语言 + */ + private String userLanguage; + + /** + * 业务流水号 + */ + private String businessNo; + + /** + * 链路追踪ID + */ + private String traceId; + + /** + * 操作系统 + */ + private String platform; + + /** + * 浏览器 + */ + private String browser; + + /** + * 操作人IP + */ + private String userIp; + + /** + * IP归属地址 + */ + private String ipAddress; + + /** + * 公司租户ID + */ + private String companyTenantId; + + /** + * 部门租户ID + */ + private String deptTenantId; + +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/BusinessStatus.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/BusinessStatus.java new file mode 100644 index 0000000..d24fbbd --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/BusinessStatus.java @@ -0,0 +1,28 @@ +package ai.remi.comm.log.enums; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作状态 + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum BusinessStatus { + /** + * 成功 + */ + SUCCESS("000000", "成功"), + + /** + * 失败 + */ + FAIL("111111", "失败"); + + + private String code; + + private String desc; + +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/BusinessType.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/BusinessType.java new file mode 100644 index 0000000..9faf3a0 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/BusinessType.java @@ -0,0 +1,98 @@ +package ai.remi.comm.log.enums; + +import ai.remi.comm.core.enums.TypeEnum; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 业务操作类型 + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum BusinessType implements TypeEnum { + + /** + * 新增 + */ + INSERT(1, "新增"), + + /** + * 修改 + */ + UPDATE(2, "修改"), + + /** + * 删除 + */ + DELETE(3, "删除"), + + /** + * 导入 + */ + IMPORT(4, "导入"), + + /** + * 导出 + */ + EXPORT(5, "导出"), + + /** + * 下载 + */ + DOWNLOAD(6, "下载"), + + /** + * 上传 + */ + UPLOAD(7, "上传"), + + /** + * 授权 + */ + GRANT(8, "授权"), + + /** + * 登录 + */ + LOGIN(9, "登录"), + + /** + * 登出 + */ + LOGOUT(10, "登出"), + + /** + * 强退 + */ + FORCE(11, "强退"), + + /** + * 修改状态 + */ + STATUS(12, "修改状态"), + + /** + * 生成代码 + */ + GENCODE(13, "生成代码"), + + /** + * 清空数据 + */ + CLEAN(14, "清空数据"), + + /** + * 更新缓存 + */ + REFRESH(15, "更新缓存"), + + /** + * 其它 + */ + OTHER(99, "其它"); + + private Integer code; + + private String desc; +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/OperatorType.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/OperatorType.java new file mode 100644 index 0000000..fa87501 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/OperatorType.java @@ -0,0 +1,34 @@ +package ai.remi.comm.log.enums; + +import ai.remi.comm.core.enums.TypeEnum; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作人类别 + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum OperatorType implements TypeEnum { + + /** + * 后台用户 + */ + MANAGE(1, "管理端用户"), + + /** + * 手机端用户 + */ + MOBILE(2, "移动端用户"), + + /** + * 其它 + */ + OTHER(99, "其它"); + + private Integer code; + + private String desc; + +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/RedisKeyEnum.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/RedisKeyEnum.java new file mode 100644 index 0000000..73492b0 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/enums/RedisKeyEnum.java @@ -0,0 +1,39 @@ +package ai.remi.comm.log.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Redis key枚举类 + * + * @author dianjiu + * @date 2021-12-7 16:10:35 + */ +@Getter +@AllArgsConstructor +public enum RedisKeyEnum { + /** + * 客户登录token,24小时失效 1:{} = customerId 2:{} = token 使用方式 RedisKeysEnum.CUSTOMER_LOGIN_TOKEN.getKey(customerId,token) + */ + APP_CODE("sdt-saas-iam:app-code:{}:{}"), + USER_INFO("sdt-saas-iam:user-info:{}"), + TICKET("sdt-saas-iam:ticket:{}"), + AUTH_CODE("sdt-saas-iam:auth-code:{}"), + ACCESS_TOKEN("sdt-saas-iam:access-token:{}"), + REFRESH_TOKEN("sdt-saas-iam:refresh-token:{}"); + + + /** + * redis key + */ + private String key; + + /** + * key失效时间,单位秒 + */ + //private Long expireAt; + public String getKey(String... key) { + return StrUtil.format(this.key, key); + } +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/filter/PropertyPreExcludeFilter.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/filter/PropertyPreExcludeFilter.java new file mode 100644 index 0000000..a0241a0 --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/filter/PropertyPreExcludeFilter.java @@ -0,0 +1,18 @@ +package ai.remi.comm.log.filter; + +import com.alibaba.fastjson2.filter.SimplePropertyPreFilter; +import lombok.NoArgsConstructor; + +/** + * 排除JSON敏感属性 + */ +@NoArgsConstructor +public class PropertyPreExcludeFilter extends SimplePropertyPreFilter { + + public PropertyPreExcludeFilter addExcludes(String... filters) { + for (String filter : filters) { + this.getExcludes().add(filter); + } + return this; + } +} diff --git a/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/service/AsyncLogService.java b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/service/AsyncLogService.java new file mode 100644 index 0000000..2c04f4b --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/java/ai/remi/comm/log/service/AsyncLogService.java @@ -0,0 +1,29 @@ +package ai.remi.comm.log.service; + +import org.springframework.stereotype.Service; + +/** + * 异步调用日志服务 + */ +@Service +public class AsyncLogService { + + /** + * 通过OpenFeign客户端注入 + */ + //@Resource + //private SysClientApi sysClientApi; + + /** + * 保存系统日志记录 + */ + //@Async + //public void saveOperateLog(OperateLog operateLog) { + //CompanyAuthInfo companyAuthInfo = new CompanyAuthInfo(); + //companyAuthInfo.setDeptTenantId(operateLog.getDeptTenantId()); + //companyAuthInfo.setCompanyTenantId(operateLog.getCompanyTenantId()); + //RequestHolder.add(companyAuthInfo); + //LogRecordPostDTO logRecordPostDTO = BeanCopyUtils.copyPropertiesThird(operateLog, LogRecordPostDTO.class); + //sysClientApi.addLogRecord(logRecordPostDTO); + //} +} diff --git a/remi-comm/remi-comm-log/src/main/resources/META-INF/spring.factories b/remi-comm/remi-comm-log/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ab226da --- /dev/null +++ b/remi-comm/remi-comm-log/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + ai.remi.comm.log.config.LogRecordAutoConfigure,\ + ai.remi.comm.log.aspect.LogRecordAspect,\ + ai.remi.comm.log.service.AsyncLogService diff --git a/remi-comm/remi-comm-minio/README.md b/remi-comm/remi-comm-minio/README.md new file mode 100644 index 0000000..6935fde --- /dev/null +++ b/remi-comm/remi-comm-minio/README.md @@ -0,0 +1,3 @@ +```yaml + +``` \ No newline at end of file diff --git a/remi-comm/remi-comm-minio/pom.xml b/remi-comm/remi-comm-minio/pom.xml new file mode 100644 index 0000000..f8baa08 --- /dev/null +++ b/remi-comm/remi-comm-minio/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + remi-comm-minio + remi-comm-minio + remi-comm-minio + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + ai.remi.top.cloud + remi-comm-core + + + + io.minio + minio + + + okhttp + com.squareup.okhttp3 + + + logging-interceptor + com.squareup.okhttp3 + + + + + + okhttp + com.squareup.okhttp3 + + + logging-interceptor + com.squareup.okhttp3 + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + org.slf4j + slf4j-nop + + + + diff --git a/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/config/MinioConfig.java b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/config/MinioConfig.java new file mode 100644 index 0000000..7873744 --- /dev/null +++ b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/config/MinioConfig.java @@ -0,0 +1,38 @@ +package ai.remi.comm.minio.config; + +import io.minio.MinioClient; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; + +/** + * @author dianjiu【公众号 点九开源】 + * @email startdis@njydsz.com + * @desc MinioConfig + */ +@Configuration +@EnableConfigurationProperties(MinioProperties.class) +public class MinioConfig { + + @Resource + private MinioProperties minioProperties; + + + /** + * 初始化 MinIO 客户端 + */ + @Bean + public MinioClient minioClient() { + try { + return MinioClient.builder() + .endpoint(minioProperties.getEndpoint()) + .credentials(minioProperties.getUsername(), minioProperties.getPassword()) + .build(); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize Minio client", e); + } + } + +} diff --git a/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/config/MinioProperties.java b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/config/MinioProperties.java new file mode 100644 index 0000000..0d67d8d --- /dev/null +++ b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/config/MinioProperties.java @@ -0,0 +1,34 @@ +package ai.remi.comm.minio.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author dianjiu【公众号 点九开源】 + * @email startdis@njydsz.com + * @desc Minio配置类 + */ +@Data +@Component +@ConfigurationProperties(prefix = "minio") +public class MinioProperties { + + /** + * 端点 + */ + private String endpoint; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + + /** + * 桶名称 + */ + private String bucketName; +} diff --git a/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/domian/BucketS3.java b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/domian/BucketS3.java new file mode 100644 index 0000000..0201966 --- /dev/null +++ b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/domian/BucketS3.java @@ -0,0 +1,99 @@ +package ai.remi.comm.minio.domian; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 存储桶管理 BucketVO对象 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString(callSuper = true) +public class BucketS3 implements Serializable { + private static final long serialVersionUID = -20900046798199937L; + /** + * ID + */ + private String id; + /** + * 存储桶编码 + */ + private String bucketCode; + /** + * 存储桶名称 + */ + private String bucketName; + /** + * 存储桶标签 + */ + private String bucketTags; + /** + * 存储桶所属区域 + */ + private String bucketRegion; + /** + * 访问权限(1Private 2Public 3Custom) + */ + private Integer accessPolicy; + /** + * 使用空间 + */ + private String useSpace; + /** + * 配额空间 + */ + private String hardQuota; + /** + * 是否加密(0停用 1启用) + */ + private Integer encryption; + /** + * 是否启用(0停用 1启用) + */ + private Integer status; + /** + * 是否删除(0正常 1删除) + */ + private Integer deleted; + /** + * 乐观锁 + */ + private Integer revision; + /** + * 部门租户ID + */ + private String deptTenantId; + /** + * 公司租户ID + */ + private String companyTenantId; + /** + * 创建人 + */ + private String createdBy; + /** + * 创建时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createdAt; + /** + * 更新人 + */ + private String updatedBy; + /** + * 更新时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updatedAt; + +} diff --git a/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/domian/FileS3.java b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/domian/FileS3.java new file mode 100644 index 0000000..b2851a9 --- /dev/null +++ b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/domian/FileS3.java @@ -0,0 +1,103 @@ +package ai.remi.comm.minio.domian; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 文件管理 FileVO对象 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ToString(callSuper = true) +public class FileS3 implements Serializable { + private static final long serialVersionUID = -71908971735852628L; + /** + * ID + */ + private String id; + /** + * 存储桶编码 + */ + private String bucketCode; + /** + * 存储桶名称 + */ + private String bucketName; + /** + * 文件编号 + */ + private String fileCode; + /** + * 文件类型 + */ + private String fileType; + /** + * 文件大小(KiB) + */ + private String fileSize; + /** + * 文件标签 + */ + private String fileTags; + /** + * 文件名称 + */ + private String fileName; + /** + * 文件目录 + */ + private String filePath; + /** + * 原文件名 + */ + private String original; + /** + * 是否启用(0停用 1启用) + */ + private Integer status; + /** + * 是否删除(0正常 1删除) + */ + private Integer deleted; + /** + * 乐观锁 + */ + private Integer revision; + /** + * 部门租户ID + */ + private String deptTenantId; + /** + * 公司租户ID + */ + private String companyTenantId; + /** + * 创建人 + */ + private String createdBy; + /** + * 创建时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createdAt; + /** + * 更新人 + */ + private String updatedBy; + /** + * 更新时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updatedAt; + +} diff --git a/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/service/MinIOService.java b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/service/MinIOService.java new file mode 100644 index 0000000..bd3085d --- /dev/null +++ b/remi-comm/remi-comm-minio/src/main/java/ai/remi/comm/minio/service/MinIOService.java @@ -0,0 +1,491 @@ +package ai.remi.comm.minio.service; + +import ai.remi.comm.minio.domian.BucketS3; +import ai.remi.comm.minio.domian.FileS3; +import io.minio.*; +import io.minio.http.Method; +import io.minio.messages.Bucket; +import io.minio.messages.DeleteObject; +import io.minio.messages.Item; +import io.minio.messages.SseConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * @author dianjiu【公众号 点九开源】 + * @email startdis@njydsz.com + * @desc MinIOService + */ +@Slf4j +@Component +public class MinIOService { + + @Resource + private MinioClient minioClient; + + public static void main(String[] args) throws Exception { + //MinioClient minioClient = MinioClient.builder().endpoint("http://192.168.31.88:9000").credentials("admin", "Sdt@1020~").build(); + MinioClient minioClient = MinioClient.builder().endpoint("http://192.168.31.200:30900").credentials("minioadmin", "minioadmin").build(); + /*MinioClient minioClient = + MinioClient.builder() + .endpoint("https://play.min.io") + .credentials("Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG") + .build();*/ + boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket("remi").build()); + if (found) { + System.out.println("dianjiu bucket name exists"); + } else { + System.out.println("dianjiu bucket name does not exist"); + } + } + + /****************************** Operate Bucket Start ******************************/ + + /** + * 启动SpringBoot容器的时候初始化Bucket 如果没有Bucket则创建 + * + * @throws Exception + */ + public void createBucket(BucketS3 bucket) throws Exception { + // 判断桶是否存在 + if (!bucketExists(bucket.getBucketCode())) { + // 创建存储桶 + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket.getBucketCode()).build()); + // 设置存储桶访问权限 + String policy = ""; + switch (bucket.getAccessPolicy()) { + case 1: + policy = "Private"; + break; + case 2: + policy = "Public"; + break; + case 3: + policy = "Custom"; + break; + default: + policy = "Private"; + } + minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().config(policy).bucket(bucket.getBucketCode()).build()); + } + } + + /** + * 启动SpringBoot容器的时候初始化Bucket 如果没有Bucket则创建 + * + * @throws Exception + */ + public void createRegionBucket(BucketS3 bucket) throws Exception { + // 判断桶是否存在 + if (!bucketExists(bucket.getBucketCode(), bucket.getBucketRegion())) { + // 创建存储桶 + minioClient.makeBucket(MakeBucketArgs.builder().region(bucket.getBucketRegion()).bucket(bucket.getBucketCode()).build()); + // 设置存储桶访问权限 + String policy = ""; + switch (bucket.getAccessPolicy()) { + case 1: + policy = "Private"; + break; + case 2: + policy = "Public"; + break; + case 3: + policy = "Custom"; + break; + default: + policy = "Private"; + } + minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().config(policy).bucket(bucket.getBucketCode()).build()); + } + } + + /** + * 判断Bucket是否存在,true:存在,false:不存在 + * + * @return + * @throws Exception + */ + public boolean bucketExists(String bucketCode) throws Exception { + return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketCode).build()); + } + + /** + * 判断Bucket是否存在,true:存在,false:不存在 + * + * @return + * @throws Exception + */ + public boolean bucketExists(String bucketCode, String bucketRegion) throws Exception { + return minioClient.bucketExists(BucketExistsArgs.builder().region(bucketRegion).bucket(bucketCode).build()); + } + + + /** + * 获得Bucket的访问策略 + * + * @param bucketName + * @return + * @throws Exception + */ + public String getBucketPolicy(String bucketName) throws Exception { + return minioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build()); + } + + /** + * 获得Bucket的加密策略 + * + * @param bucketName + * @return + * @throws Exception + */ + public String getBucketEncryption(String bucketName) throws Exception { + SseConfiguration bucketEncryption = minioClient.getBucketEncryption(GetBucketEncryptionArgs.builder().bucket(bucketName).build()); + return bucketEncryption.rule().sseAlgorithm().name(); + } + + + /** + * 获得所有Bucket列表 + * + * @return + * @throws Exception + */ + public List listBuckets() throws Exception { + return minioClient.listBuckets(); + } + + /** + * 根据bucketName获取其相关信息 + * + * @param bucketCode + * @return + * @throws Exception + */ + public Optional getBucket(String bucketCode) throws Exception { + return listBuckets().stream().filter(b -> b.name().equals(bucketCode)).findFirst(); + } + + /** + * 根据bucketName删除Bucket,true:删除成功; false:删除失败,文件或已不存在 + * + * @param bucketName + * @throws Exception + */ + public void deleteBucket(String bucketName) throws Exception { + minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); + } + + /****************************** Operate Bucket End ******************************/ + + /****************************** Operate Files Start ******************************/ + + /** + * 创建文件夹或目录 + * + * @param bucketName 存储桶 + * @param objectName 目录路径 + */ + public ObjectWriteResponse createDir(String bucketName, String objectName) throws Exception { + return minioClient.putObject( + PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(new ByteArrayInputStream(new byte[]{}), 0, -1).build()); + } + + /** + * 判断文件是否存在 + * + * @param bucketName 存储桶 + * @param objectName 文件名 + * @return + */ + public boolean isObjectExist(String bucketName, String objectName) { + boolean exist = true; + try { + minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()); + } catch (Exception e) { + log.error("[Minio工具类]>>>> 判断文件是否存在, 异常:", e); + exist = false; + } + return exist; + } + + /** + * 判断文件夹是否存在 + * + * @param bucketName 存储桶 + * @param objectName 文件夹名称 + * @return + */ + public boolean isFolderExist(String bucketName, String objectName) { + boolean exist = false; + try { + Iterable> results = + minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build()); + for (Result result : results) { + Item item = result.get(); + if (item.isDir() && objectName.equals(item.objectName())) { + exist = true; + } + } + } catch (Exception e) { + log.error("[Minio工具类]>>>> 判断文件夹是否存在,异常:", e); + exist = false; + } + return exist; + } + + /** + * 获取minio中所有的文件 + * + * @Param: [] + * @return: java.util.List + * @Author: dianjiu + * @Date: 2021/11/15 + */ + public List listAllFile() throws Exception { + List list = this.listBuckets(); + List fileInfos = new ArrayList<>(); + for (Bucket bucket : list) { + fileInfos.addAll(this.listObjects(bucket.name())); + } + return fileInfos; + } + + + /** + * 根据文件前置查询文件 + * + * @param bucketName 存储桶 + * @param prefix 前缀 + * @param recursive 是否使用递归查询 + * @return MinioItem 列表 + * @throws Exception + */ + public List getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) throws Exception { + List list = new ArrayList<>(); + Iterable> objectsIterator = + minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build()); + if (objectsIterator != null) { + for (Result o : objectsIterator) { + Item item = o.get(); + list.add(item); + } + } + return list; + } + + /** + * 获取路径下文件列表 + * + * @param bucketName 存储桶 + * @param prefix 文件名称 + * @param recursive 是否递归查找,false:模拟文件夹结构查找 + * @return 二进制流 + */ + public List listObjects(String bucketName, String prefix, boolean recursive) { + List infos = new ArrayList<>(); + Iterable> results = + minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build()); + results.forEach(r -> { + FileS3 info = new FileS3(); + try { + Item item = r.get(); + info.setFileName(item.objectName()); + infos.add(info); + } catch (Exception e) { + e.printStackTrace(); + } + }); + return infos; + } + + /** + * 递归查找指定桶中的所有文件和目录 + * + * @param bucketName 存储桶 + */ + public List listObjects(String bucketName) { + List infos = new ArrayList<>(); + Iterable> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).recursive(true).build()); + results.forEach(r -> { + FileS3 info = new FileS3(); + try { + Item item = r.get(); + info.setFileName(item.objectName()); + infos.add(info); + } catch (Exception e) { + e.printStackTrace(); + } + }); + return infos; + } + + /** + * 使用MultipartFile进行文件上传 + * + * @param bucketName 存储桶 + * @param file 文件名 + * @param objectName 对象名 + * @param contentType 类型 + * @return + * @throws Exception + */ + public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) throws Exception { + InputStream inputStream = file.getInputStream(); + return minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).contentType(contentType) + .stream(inputStream, inputStream.available(), -1).build()); + } + + /** + * 上传本地文件 + * + * @param bucketName 存储桶 + * @param objectName 对象名称 + * @param fileName 本地文件路径 + */ + public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) throws Exception { + return minioClient.uploadObject(UploadObjectArgs.builder().bucket(bucketName).object(objectName).filename(fileName).build()); + } + + /** + * 通过流上传文件 + * + * @param bucketName 存储桶 + * @param objectName 文件对象 + * @param inputStream 文件流 + */ + public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) throws Exception { + return minioClient.putObject( + PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build()); + } + + /** + * 获取文件流 + * + * @param bucketName 存储桶 + * @param objectName 文件名 + * @return 二进制流 + */ + public InputStream download(String bucketName, String objectName) throws Exception { + return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build()); + } + + /** + * 断点下载 + * + * @param bucketName 存储桶 + * @param objectName 文件名称 + * @param offset 起始字节的位置 + * @param length 要读取的长度 + * @return 二进制流 + */ + public InputStream download(String bucketName, String objectName, long offset, long length) throws Exception { + return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length).build()); + } + + /** + * 获取文件信息, 如果抛出异常则说明文件不存在 + * + * @param bucketName 存储桶 + * @param objectName 文件名称 + */ + public String getObjectInfo(String bucketName, String objectName) throws Exception { + return minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()).toString(); + } + + /** + * 拷贝文件 + * + * @param bucketName 存储桶 + * @param objectName 文件名 + * @param srcBucketName 目标存储桶 + * @param srcObjectName 目标文件名 + */ + public ObjectWriteResponse copyObject(String bucketName, String objectName, String srcBucketName, String srcObjectName) throws Exception { + return minioClient.copyObject( + CopyObjectArgs.builder().source(CopySource.builder().bucket(bucketName).object(objectName).build()).bucket(srcBucketName) + .object(srcObjectName).build()); + } + + /** + * 删除文件 + * + * @param bucketName 存储桶 + * @param objectName 文件名称 + */ + public void deleteObject(String bucketName, String objectName) throws Exception { + minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build()); + } + + /** + * 批量删除文件 + * + * @param bucketName 存储桶 + * @param keys 需要删除的文件列表 + * @return + */ + public void deleteObjects(String bucketName, List keys) { + List objects = new LinkedList<>(); + keys.forEach(s -> { + objects.add(new DeleteObject(s)); + try { + deleteObject(bucketName, s); + } catch (Exception e) { + log.error("[Minio工具类]>>>> 批量删除文件,异常:", e); + } + }); + } + + /** + * 获取文件外链 + * + * @param bucketName 存储桶 + * @param objectName 文件名 + * @param expires 过期时间 <=7 天 (外链有效时间(单位:天)) + * @return url + * @throws Exception + */ + public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception { + GetPresignedObjectUrlArgs args = + GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectName).expiry(expires, TimeUnit.DAYS).build(); + return minioClient.getPresignedObjectUrl(args); + } + + /** + * 获得文件外链 + * + * @param bucketName + * @param objectName + * @return url + * @throws Exception + */ + public String getPresignedObjectUrl(String bucketName, String objectName) throws Exception { + GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectName).expiry(1, TimeUnit.DAYS).build(); + return minioClient.getPresignedObjectUrl(args); + } + + /** + * 将URLDecoder编码转成UTF8 + * + * @param str + * @return + * @throws UnsupportedEncodingException + */ + public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException { + String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25"); + return URLDecoder.decode(url, "UTF-8"); + } + + /****************************** Operate Files End ******************************/ +} diff --git a/remi-comm/remi-comm-minio/src/main/resources/resources/META-INF/spring.factories b/remi-comm/remi-comm-minio/src/main/resources/resources/META-INF/spring.factories new file mode 100644 index 0000000..847fddf --- /dev/null +++ b/remi-comm/remi-comm-minio/src/main/resources/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + ai.remi.comm.minio.service.MinIOService \ No newline at end of file diff --git a/remi-comm/remi-comm-redis/pom.xml b/remi-comm/remi-comm-redis/pom.xml new file mode 100644 index 0000000..1449c56 --- /dev/null +++ b/remi-comm/remi-comm-redis/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + remi-comm-redis + remi-comm-redis + remi-comm-redis + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + + commons-collections + commons-collections + + + + cn.hutool + hutool-core + + + + com.alibaba + fastjson + + + + org.slf4j + slf4j-api + + + + diff --git a/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/config/RedisConfig.java b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/config/RedisConfig.java new file mode 100644 index 0000000..119a0ed --- /dev/null +++ b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/config/RedisConfig.java @@ -0,0 +1,131 @@ +package ai.remi.comm.redis.config; + +import ai.remi.comm.redis.serializer.FastJson2JsonRedisSerializer; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import javax.annotation.Resource; +import java.time.Duration; + + +@Configuration +public class RedisConfig implements EnvironmentAware { + + @Resource + private RedisProperties redisProperties; + + private Environment environment; + + private final String REDIS_USER = "spring.redis.user"; + + @Bean + @ConditionalOnProperty(prefix = "spring.redis", value = "user") + public LettuceConnectionFactory redisConnectionFactory() { + String username = this.environment.getProperty(REDIS_USER); + + RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes()); + + redisClusterConfiguration.setMaxRedirects(redisProperties.getCluster().getMaxRedirects()); + redisClusterConfiguration.setPassword(redisProperties.getPassword()); + redisClusterConfiguration.setUsername(username); + + //支持自适应集群拓扑刷新和静态刷新源 + ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder() + .enablePeriodicRefresh() + .enableAllAdaptiveRefreshTriggers() + .refreshPeriod(Duration.ofSeconds(2000)) + .build(); + +// ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder() +// .topologyRefreshOptions(clusterTopologyRefreshOptions).build(); + + LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder() + //.readFrom(ReadFrom.REPLICA_PREFERRED) + //.clientOptions(clusterClientOptions) + .useSsl() + //.startTls() + .build(); + + return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(stringRedisSerializer); + template.setValueSerializer(stringRedisSerializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(RedisSerializer.string()); + template.setHashValueSerializer(RedisSerializer.java()); + + template.afterPropertiesSet(); + return template; + } + + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisJacksonTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisFastJsonTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } +} diff --git a/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/enums/RedisDemoEnum.java b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/enums/RedisDemoEnum.java new file mode 100644 index 0000000..33ba968 --- /dev/null +++ b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/enums/RedisDemoEnum.java @@ -0,0 +1,34 @@ +package ai.remi.comm.redis.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Redis key枚举类 + * + * @author dianjiu + * @date 2021-12-7 16:10:35 + */ +@Getter +@AllArgsConstructor +public enum RedisDemoEnum { + /** + * 客户登录token,24小时失效 1:{} = customerId 2:{} = token 使用方式 RedisKeysEnum.CUSTOMER_LOGIN_TOKEN.getKey(customerId,token) + */ + DEMO("remi-comm-redis:demo:{}"); + + + /** + * redis key + */ + private String key; + + /** + * key失效时间,单位秒 + */ + //private Long expireAt; + public String getKey(String... key) { + return StrUtil.format(this.key, key); + } +} diff --git a/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/serializer/FastJson2JsonRedisSerializer.java b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/serializer/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..5038c96 --- /dev/null +++ b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/serializer/FastJson2JsonRedisSerializer.java @@ -0,0 +1,50 @@ +package ai.remi.comm.redis.serializer; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.nio.charset.Charset; + +/** + * Redis使用FastJson序列化 + * + * @author startdis + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private Class clazz; + + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType); + } +} diff --git a/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/serializer/SerializeUtils.java b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/serializer/SerializeUtils.java new file mode 100644 index 0000000..1c86e49 --- /dev/null +++ b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/serializer/SerializeUtils.java @@ -0,0 +1,71 @@ +package ai.remi.comm.redis.serializer; + +import java.io.*; + +/** + * 序列化工具类 + */ +public class SerializeUtils { + /** + * 将对象序列化 + * @Edit_Description: + * @Create_Version:shebao-framelib 1.0 + */ + public static byte[] serialize(Object source) { + byte[] bs = null; + ByteArrayOutputStream bos = null; + ObjectOutputStream oos = null; + try { + if (source != null) { + bos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(bos); + oos.writeObject(source); + bs = bos.toByteArray(); + } + } catch (IOException exception) { + exception.printStackTrace(); + } finally { + try { + if (bos != null) { + bos.close(); + } + if (oos != null) { + oos.flush(); + oos.close(); + } + } catch (IOException exception) { + exception.printStackTrace(); + } + } + return bs; + } + + + /** + * 将字节数组反序列化 + * @Edit_Description: + * @Create_Version:shebao-framelib 1.0 + */ + public static Object deserialize(byte[] source) { + Object obj = null; + ObjectInputStream inputStream = null; + try { + if (source != null) { + ByteArrayInputStream bis = new ByteArrayInputStream(source); + inputStream = new ObjectInputStream(bis); + obj = inputStream.readObject(); + } + } catch (Exception exception) { + exception.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException exception) { + exception.printStackTrace(); + } + } + } + return obj; + } +} diff --git a/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/service/RedisService.java b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/service/RedisService.java new file mode 100644 index 0000000..3dd0378 --- /dev/null +++ b/remi-comm/remi-comm-redis/src/main/java/ai/remi/comm/redis/service/RedisService.java @@ -0,0 +1,676 @@ +package ai.remi.comm.redis.service; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * @author DianJiu 【公众号 点九开源】 + * @date 2022/1/26 + * @desc RedisService + */ +@Slf4j +@Component +public class RedisService { + private static Logger logger = LoggerFactory.getLogger(RedisService.class); + + @Resource + private RedisTemplate redisTemplate; + + /** + * @param key redis key, 唯一键 + * @param value redis value, 这里是时间戳 + * @return + * @desc 加锁 true已锁 false未锁 + */ + public boolean setLock(String key, String value) { + // 对应setnx命令 + if (redisTemplate.opsForValue().setIfAbsent(key, value)) { + // 可以成功设置,也就是key不存在 + return true; + } + // 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁 + String currentValue = (String) redisTemplate.opsForValue().get(key); + // 如果锁过期 + // currentValue 不为空且小于当前时间 + if (!ObjectUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) { + // 获取上一个锁的时间value + // 对应getset,如果key存在返回当前key的值,并重新设置新的值 + // redis是单线程处理,即使并发存在,这里的getAndSet也是单个执行 + // 所以,加上下面的 !StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) + // 就能轻松解决并发问题 + String oldValue = (String) redisTemplate.opsForValue().getAndSet(key, value); + if (!ObjectUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) { + return true; + } + } + return false; + } + + /** + * @param key redis key, 唯一键 + * @param value redis value, 这里是时间戳 + * @return + * @desc 释放锁 true已释放 false未释放 + */ + public void releaseLock(String key, String value) { + try { + String currentValue = (String) redisTemplate.opsForValue().get(key); + if (!ObjectUtils.isEmpty(currentValue) && currentValue.equals(value)) { + // 删除key + redisTemplate.opsForValue().getOperations().delete(key); + } + } catch (Exception e) { + log.error("解锁出现异常了,{}", e); + } + } + + /** + * @param userDto + * @desc 锁机制保存对象数据 + * 此锁适合确定超时时间的场景 + */ + // public void lockOfUserProcess(UserDto userDto) { + // String key = "myLock_" + userDto.getUserName(); + // int timeout = 300 * 1000;//超时时间 5分钟 + // long value = System.currentTimeMillis() + timeout; + // + // try { + // // 加锁 + // if (!redisDistributedLock.setLock(key, String.valueOf(value))) { + // throw new Exception("对不起,redis被挤爆了,请休息片刻再重试。"); + // } + // // 做一些业务相关操作,这里只是demo,随便保存个对象信息 + // redisTemplate.opsForValue().set(userDto.getUserName(), userDto, 60, + // TimeUnit.SECONDS); + // log.info("原来值内容:" + JsonWare.beanToJson(userDto)); + // // 修改值,重新保存到redis中 + // UserDto dto = new UserDto(); + // BeanUtils.copyProperties(userDto, dto); + // dto.setUserAge(30); + // redisTemplate.opsForValue().set(dto.getUserName(), dto, 60, + // TimeUnit.SECONDS); + // log.info("修改值内容:" + JsonWare.beanToJson(dto)); + // } catch (Exception e) { + // log.error("异常发生,信息如下:", e.getMessage()); + // } finally { + // // 释放锁 + // redisDistributedLock.releaseLock(key, String.valueOf(value)); + // } + // } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) + * @return + */ + public boolean expire(String key, long time) { + try { + if (time > 0) { + redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + return true; + } catch (Exception e) { + log.error("exception when expire key {}. ", key, e); + return false; + } + } + + /** + * 根据key获取过期时间 + * + * @param key 键 不能为null + * @return 时间(秒) 返回0代表为永久有效 + */ + public long getExpire(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + /** + * 判断key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public boolean hasKey(String key) { + try { + return redisTemplate.hasKey(key); + } catch (Exception e) { + log.error("exception when check key {}. ", key, e); + return false; + } + } + + /** + * 删除缓存 + * + * @param key 可以传一个值 或多个 + */ + @SuppressWarnings("unchecked") + public void del(String... key) { + if (key != null && key.length > 0) { + if (key.length == 1) { + redisTemplate.delete(key[0]); + } else { + redisTemplate.delete((Collection) CollectionUtils.arrayToList(key)); + } + } + } + + /** + * 普通缓存获取 + * + * @param key 键 + * @return 值 + */ + public Object get(String key) { + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + /** + * 普通缓存放入 + * + * @param key 键 + * @param value 值 + * @return true成功 false失败 + */ + public boolean set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + return true; + } catch (Exception e) { + log.error("exception when set key {}. ", key, e); + return false; + } + + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error("exception when set key {}. ", key, e); + return false; + } + } + + /** + * 递增 + * + * @param key 键 + * @param delta 要增加几(大于0) + * @return + */ + public long incr(String key, long delta) { + if (delta <= 0) { + throw new RuntimeException("递增因子必须大于0"); + } + return redisTemplate.opsForValue().increment(key, delta); + } + + /** + * 递减 + * + * @param key 键 + * @param delta 要减少几(小于0) + * @return + */ + public long decr(String key, long delta) { + if (delta <= 0) { + throw new RuntimeException("递减因子必须大于0"); + } + return redisTemplate.opsForValue().increment(key, -delta); + } + + /** + * HashGet + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return 值 + */ + public Object hget(String key, String item) { + return redisTemplate.opsForHash().get(key, item); + } + + /** + * 获取hashKey对应的所有键值 + * + * @param key 键 + * @return 对应的多个键值 + */ + public Map hmget(String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * HashSet + * + * @param key 键 + * @param map 对应多个键值 + * @return true 成功 false 失败 + */ + public boolean hmset(String key, Map map) { + try { + redisTemplate.opsForHash().putAll(key, map); + return true; + } catch (Exception e) { + log.error("exception when hash set key {}. ", key, e); + return false; + } + } + + /** + * HashSet 并设置时间 + * + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) + * @return true成功 false失败 + */ + public boolean hmset(String key, Map map, long time) { + try { + redisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error("exception when hash set key {}. ", key, e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value) { + try { + redisTemplate.opsForHash().put(key, item, value); + return true; + } catch (Exception e) { + log.error("exception when hash set key {}, item {} ", key, item, e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value, long time) { + try { + redisTemplate.opsForHash().put(key, item, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error("exception when hash set key {}, item {} ", key, item, e); + return false; + } + } + + /** + * 删除hash表中的值 + * + * @param key 键 不能为null + * @param item 项 可以使多个 不能为null + */ + public void hdel(String key, Object... item) { + redisTemplate.opsForHash().delete(key, item); + } + + /** + * 判断hash表中是否有该项的值 + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return true 存在 false不存在 + */ + public boolean hHasKey(String key, String item) { + return redisTemplate.opsForHash().hasKey(key, item); + } + + /** + * hash递增 如果不存在,就会创建一个 并把新增后的值返回 + * + * @param key 键 + * @param item 项 + * @param by 要增加几(大于0) + * @return + */ + public double hincr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, by); + } + + /** + * hash递减 + * + * @param key 键 + * @param item 项 + * @param by 要减少记(小于0) + * @return + */ + public double hdecr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, -by); + } + + /** + * 根据key获取Set中的所有值 + * + * @param key 键 + * @return + */ + public Set sGet(String key) { + try { + return redisTemplate.opsForSet().members(key); + } catch (Exception e) { + return null; + } + } + + /** + * 根据value从一个set中查询,是否存在 + * + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + */ + public boolean sHasKey(String key, Object value) { + try { + return redisTemplate.opsForSet().isMember(key, value); + } catch (Exception e) { + return false; + } + } + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSet(String key, Object... values) { + try { + return redisTemplate.opsForSet().add(key, values); + } catch (Exception e) { + return 0; + } + } + + /** + * 将set数据放入缓存 + * + * @param key 键 + * @param time 时间(秒) + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSetAndTime(String key, long time, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + if (time > 0) { + expire(key, time); + } + return count; + } catch (Exception e) { + return 0; + } + } + + /** + * 获取set缓存的长度 + * + * @param key 键 + * @return + */ + public long sGetSetSize(String key) { + try { + return redisTemplate.opsForSet().size(key); + } catch (Exception e) { + return 0; + } + } + + /** + * 移除值为value的 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + */ + public long setRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().remove(key, values); + return count; + } catch (Exception e) { + return 0; + } + } + + /** + * 获取list缓存的内容 + * + * @param key 键 + * @param start 开始 + * @param end 结束 0 到 -1代表所有值 + * @return + */ + public List lGet(String key, long start, long end) { + try { + return redisTemplate.opsForList().range(key, start, end); + } catch (Exception e) { + return null; + } + } + + /** + * 获取list缓存的长度 + * + * @param key 键 + * @return + */ + public long lGetListSize(String key) { + try { + return redisTemplate.opsForList().size(key); + } catch (Exception e) { + return 0; + } + } + + /** + * 通过索引 获取list中的值 + * + * @param key 键 + * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 + * @return + */ + public Object lGetIndex(String key, long index) { + try { + return redisTemplate.opsForList().index(key, index); + } catch (Exception e) { + return null; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public boolean lSet(String key, Object value) { + try { + redisTemplate.opsForList().rightPush(key, value); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return + */ + public boolean lSet(String key, Object value, long time) { + try { + redisTemplate.opsForList().rightPush(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public boolean lSet(String key, List value) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return + */ + public boolean lSet(String key, List value, long time) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 根据索引修改list中的某条数据 + * + * @param key 键 + * @param index 索引 + * @param value 值 + * @return + */ + public boolean lUpdateIndex(String key, long index, Object value) { + try { + redisTemplate.opsForList().set(key, index, value); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * 移除N个值为value + * + * @param key 键 + * @param count 移除多少个 + * @param value 值 + * @return 移除的个数 + */ + public long lRemove(String key, long count, Object value) { + try { + Long remove = redisTemplate.opsForList().remove(key, count, value); + return remove; + } catch (Exception e) { + return 0; + } + } + + /** + * 对存储在指定key的数值执行原子的加1操作 + * + * @param key key + * @return + */ + public Long incrKey(String key) { + return redisTemplate.execute((RedisCallback) connection -> + connection.incr(redisTemplate.getStringSerializer().serialize(key)) + ); + } + + /** + * 以天为单位设置key的超时时间 + * + * @param key key + * @param expireTime 超时时间 + * @return boolean + */ + public boolean expireByDays(String key, Long expireTime) { + return redisTemplate.expire(key, expireTime, TimeUnit.DAYS); + } + + public String getString(final String key) throws Exception { + try { + return redisTemplate.execute((RedisCallback) connection -> { + byte[] bytes = connection.get(redisTemplate.getStringSerializer().serialize(key)); + return null != bytes ? redisTemplate.getStringSerializer().deserialize(bytes) : null; + }); + } catch (Exception e) { + log.error("redis getString,Excption{}", e); + } + return null; + } +} diff --git a/remi-comm/remi-comm-redis/src/main/resources/META-INF/spring.factories b/remi-comm/remi-comm-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..e82d526 --- /dev/null +++ b/remi-comm/remi-comm-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + ai.remi.comm.redis.service.RedisService \ No newline at end of file diff --git a/remi-comm/remi-comm-redis/src/main/resources/rebel.xml b/remi-comm/remi-comm-redis/src/main/resources/rebel.xml new file mode 100644 index 0000000..823bd20 --- /dev/null +++ b/remi-comm/remi-comm-redis/src/main/resources/rebel.xml @@ -0,0 +1,16 @@ + + + + + + remi-comm-redis + + + + + + + diff --git a/remi-comm/remi-comm-safe/pom.xml b/remi-comm/remi-comm-safe/pom.xml new file mode 100644 index 0000000..243dfb3 --- /dev/null +++ b/remi-comm/remi-comm-safe/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + remi-comm-safe + remi-comm-safe + remi-comm-safe + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + ai.remi.top.cloud + remi-comm-redis + + + ai.remi.top.cloud + remi-comm-util + + + ai.remi.top.cloud + remi-comm-exception + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + org.projectlombok + lombok + + + diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/RedisLimit.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/RedisLimit.java new file mode 100644 index 0000000..1a9962e --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/RedisLimit.java @@ -0,0 +1,47 @@ +package ai.remi.comm.safe.annotation; + +import ai.remi.comm.safe.constant.SafeConstants; +import ai.remi.comm.safe.enums.LimitType; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * 基于Redis的防刷限流注解 + * + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + */ +@Inherited +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface RedisLimit { + /** + * key + */ + String key() default SafeConstants.REDIS_LIMIT_KEY; + + /** + * Key的前缀 + */ + String prefix() default ""; + + /** + * 一定时间内最多访问次数 + */ + int count(); + + /** + * 给定的时间范围 单位(秒) + */ + int period(); + + /** + * 限流的类型(用户自定义key或者请求ip) + */ + LimitType limitType() default LimitType.CUSTOMER; +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/RepeatSubmit.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/RepeatSubmit.java new file mode 100644 index 0000000..e151ff7 --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/RepeatSubmit.java @@ -0,0 +1,26 @@ +package ai.remi.comm.safe.annotation; + +import java.lang.annotation.*; + +/** + * 自定义注解防止表单重复提交 + * + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit { + /** + * 间隔时间(ms),小于此时间视为重复提交 + */ + public int interval() default 5000; + + /** + * 提示消息 + */ + public String message() default "不允许重复提交,请稍候再试"; +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/SentinelLimit.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/SentinelLimit.java new file mode 100644 index 0000000..041cd41 --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/SentinelLimit.java @@ -0,0 +1,26 @@ +package ai.remi.comm.safe.annotation; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.*; + +/** + * 基于Sentinel的防刷限流注解 + * + * @author DianJiu 【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-19 + */ +@Inherited +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(value = RetentionPolicy.RUNTIME) +@Order(Ordered.HIGHEST_PRECEDENCE) +public @interface SentinelLimit { + + String resourceName(); + + int limitCount() default 5; + +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/Xss.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/Xss.java new file mode 100644 index 0000000..c2c4463 --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/annotation/Xss.java @@ -0,0 +1,21 @@ +package ai.remi.comm.safe.annotation; + +import ai.remi.comm.safe.aspect.XssValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) +@Constraint(validatedBy = {XssValidator.class}) +public @interface Xss { + String message() default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/RedisLimitAspect.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/RedisLimitAspect.java new file mode 100644 index 0000000..b389644 --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/RedisLimitAspect.java @@ -0,0 +1,146 @@ +package ai.remi.comm.safe.aspect; + + +import com.google.common.collect.ImmutableList; +import ai.remi.comm.exception.custom.BusinessException; +import ai.remi.comm.safe.annotation.RedisLimit; +import ai.remi.comm.safe.enums.LimitType; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.lang.reflect.Method; + +@Aspect +@Component +@Order(10) +public class RedisLimitAspect { + + private static final Logger logger = LoggerFactory.getLogger(RedisLimitAspect.class); + private final RedisTemplate limitRedisTemplate; + + @Resource + private DefaultRedisScript redisLuaScript; + + @Autowired + public RedisLimitAspect(RedisTemplate limitRedisTemplate) { + this.limitRedisTemplate = limitRedisTemplate; + } + + + @Pointcut(value = "@annotation(ai.remi.comm.safe.annotation.RedisLimit)") + public void redisLimit() { + + } + + @Around("redisLimit()") + public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + // 获取连接点所在类名 + String classname = joinPoint.getTarget().getClass().getSimpleName(); + // 获取连接点所在方法名称 + String methodName = method.getName(); + String functionName = String.format("【%s.%s()】", classname, methodName); + + RedisLimit redisLimit = method.getAnnotation(RedisLimit.class); + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + + LimitType limitType = redisLimit.limitType(); + String key = redisLimit.key(); + if (StringUtils.isEmpty(key)) { + if (limitType == LimitType.IP) { + key = getIpAddr(request); + } else { + key = redisLimit.key(); + } + } + + ImmutableList keys = ImmutableList.of(StringUtils.join(redisLimit.prefix(), key)); + + //执行到这里开始走进来的方法体(必须声明) + Object returnValue; + try { + String luaScript = buildLuaScript(); + redisLuaScript = new DefaultRedisScript<>(luaScript, Number.class); + //调用lua脚本,获取返回结果,这里即为请求的次数 + Number number = limitRedisTemplate.execute( + redisLuaScript, + keys, + redisLimit.count(), + redisLimit.period() + ); + if (number == null || number.intValue() == 0) { + throw new BusinessException("111111", "访问频率过快,被限流了"); + } + logger.info("第{}次访问 [{}] 接口!", number, functionName); + returnValue = joinPoint.proceed(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + throw new BusinessException(((BusinessException) throwable).getCode(), (throwable).getMessage()); + } + return returnValue; + } + + /** + * 限流脚本 + */ + private String buildLuaScript() { + return "local limit = tonumber(ARGV[1])" + + "local current = tonumber(redis.call('get', key) or '0')" + + "if current + 1 > limit then" + + " return 0" + + "else" + + " current = redis.call('incr',KEYS[1])" + + " redis.call('expire',KEYS[1],ARGV[2])" + + " return current" + + "end;"; + } + + /** + * 获取请求的IP方法 + * + * @param request + * @return + */ + private static String getIpAddr(HttpServletRequest request) { + String ipAddress = null; + try { + ipAddress = request.getHeader("x-forwarded-for"); + if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getHeader("WL-Proxy-Client-IP"); + } + if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { + ipAddress = request.getRemoteAddr(); + } + // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 + if (ipAddress != null && ipAddress.length() > 15) { + if (ipAddress.indexOf(",") > 0) { + ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); + } + } + } catch (Exception e) { + ipAddress = ""; + } + return ipAddress; + } + +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/SentinelLimitAspect.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/SentinelLimitAspect.java new file mode 100644 index 0000000..d9750ee --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/SentinelLimitAspect.java @@ -0,0 +1,98 @@ +package ai.remi.comm.safe.aspect; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import ai.remi.comm.safe.annotation.SentinelLimit; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Aspect +@Component +@Order(10) +public class SentinelLimitAspect { + private static void initFlowRule(String resourceName,int limitCount) { + List rules = new ArrayList<>(); + FlowRule rule = new FlowRule(); + //设置受保护的资源 + rule.setResource(resourceName); + //设置流控规则 QPS + rule.setGrade(RuleConstant.FLOW_GRADE_QPS); + //设置受保护的资源阈值 + rule.setCount(limitCount); + rules.add(rule); + //加载配置好的规则 + FlowRuleManager.loadRules(rules); + } + + @Pointcut(value = "@annotation(ai.remi.comm.safe.annotation.SentinelLimit)") + public void rateLimit() { + + } + + @Around("rateLimit()") + public Object around(ProceedingJoinPoint joinPoint) { + //1、获取当前的调用方法 + Method currentMethod = getCurrentMethod(joinPoint); + if (Objects.isNull(currentMethod)) { + return null; + } + //2、从方法注解定义上获取限流的类型 + String resourceName = currentMethod.getAnnotation(SentinelLimit.class).resourceName(); + if(StringUtils.isEmpty(resourceName)){ + throw new RuntimeException("资源名称为空"); + } + int limitCount = currentMethod.getAnnotation(SentinelLimit.class).limitCount(); + initFlowRule(resourceName,limitCount); + + Entry entry = null; + Object result = null; + try { + entry = SphU.entry(resourceName); + try { + result = joinPoint.proceed(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } catch (BlockException ex) { + // 资源访问阻止,被限流或被降级 + // 在此处进行相应的处理操作 + System.out.println("blocked"); + return "被限流了"; + } catch (Exception e) { + Tracer.traceEntry(e, entry); + } finally { + if (entry != null) { + entry.exit(); + } + } + return result; + } + + private Method getCurrentMethod(JoinPoint joinPoint) { + Method[] methods = joinPoint.getTarget().getClass().getMethods(); + Method target = null; + for (Method method : methods) { + if (method.getName().equals(joinPoint.getSignature().getName())) { + target = method; + break; + } + } + return target; + } +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/XssValidator.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/XssValidator.java new file mode 100644 index 0000000..8080aff --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/aspect/XssValidator.java @@ -0,0 +1,27 @@ +package ai.remi.comm.safe.aspect; + +import ai.remi.comm.safe.annotation.Xss; +import org.apache.commons.lang3.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class XssValidator implements ConstraintValidator { + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { + if (StringUtils.isBlank(value)) { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) { + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + return matcher.matches(); + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/config/RedisLimiterHelper.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/config/RedisLimiterHelper.java new file mode 100644 index 0000000..49cde3a --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/config/RedisLimiterHelper.java @@ -0,0 +1,24 @@ +package ai.remi.comm.safe.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.io.Serializable; + +@Configuration +public class RedisLimiterHelper { + + @Bean + public RedisTemplate limitRedisTemplate(LettuceConnectionFactory factory) { + RedisTemplate template = new RedisTemplate(); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setConnectionFactory(factory); + return template; + } + +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/constant/SafeConstants.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/constant/SafeConstants.java new file mode 100644 index 0000000..655c323 --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/constant/SafeConstants.java @@ -0,0 +1,18 @@ +package ai.remi.comm.safe.constant; + +/** + * 缓存的key 常量 + */ +public class SafeConstants { + + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** + * 限流 redis key + */ + public static final String REDIS_LIMIT_KEY = "redis_limit:"; +} diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/enums/LimitType.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/enums/LimitType.java new file mode 100644 index 0000000..3676e93 --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/enums/LimitType.java @@ -0,0 +1,18 @@ +package ai.remi.comm.safe.enums; + +/** + * 限制的类型 + */ +public enum LimitType { + + /** + * 自定义key + */ + CUSTOMER, + /** + * 根据请求者IP + */ + IP; + +} + diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/filter/XssFilter.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/filter/XssFilter.java new file mode 100644 index 0000000..e80e06c --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/filter/XssFilter.java @@ -0,0 +1,61 @@ +package ai.remi.comm.safe.filter; + +import ai.remi.comm.core.enums.HttpMethod; +import ai.remi.comm.util.string.StringUtils; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 防止XSS攻击的过滤器 + * + */ +public class XssFilter implements Filter { + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String tempExcludes = filterConfig.getInitParameter("excludes"); + if (StringUtils.isNotEmpty(tempExcludes)) { + String[] url = tempExcludes.split(","); + for (int i = 0; url != null && i < url.length; i++) { + excludes.add(url[i]); + } + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) { + String url = request.getServletPath(); + String method = request.getMethod(); + // GET DELETE 不过滤 + if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) { + return true; + } + return StringUtils.matches(url, excludes); + } + + @Override + public void destroy() { + + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/filter/XssHttpServletRequestWrapper.java b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..3381529 --- /dev/null +++ b/remi-comm/remi-comm-safe/src/main/java/ai/remi/comm/safe/filter/XssHttpServletRequestWrapper.java @@ -0,0 +1,95 @@ +package ai.remi.comm.safe.filter; + +import ai.remi.comm.util.html.EscapeUtils; +import ai.remi.comm.util.string.StringUtils; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +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.ByteArrayInputStream; +import java.io.IOException; + +/** + * XSS过滤处理 + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (values != null) { + int length = values.length; + String[] escapesValues = new String[length]; + for (int i = 0; i < length; i++) { + // 防xss攻击和过滤前后空格 + escapesValues[i] = EscapeUtils.clean(values[i]).trim(); + } + return escapesValues; + } + return super.getParameterValues(name); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + // 非json类型,直接返回 + if (!isJsonRequest()) { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isEmpty(json)) { + return super.getInputStream(); + } + + // xss过滤 + json = EscapeUtils.clean(json).trim(); + byte[] jsonBytes = json.getBytes("utf-8"); + final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public int available() throws IOException { + return jsonBytes.length; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public int read() throws IOException { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + * + * @param request + */ + public boolean isJsonRequest() { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-swagger/pom.xml b/remi-comm/remi-comm-swagger/pom.xml new file mode 100644 index 0000000..1f24fde --- /dev/null +++ b/remi-comm/remi-comm-swagger/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + remi-comm-swagger + remi-comm-swagger + remi-comm-swagger + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-aop + + + + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + + + com.github.xiaoymin + knife4j-gateway-spring-boot-starter + + + + org.slf4j + slf4j-api + + + diff --git a/remi-comm/remi-comm-swagger/src/main/java/ai/remi/comm/swagger/config/OpenApiConfig.java b/remi-comm/remi-comm-swagger/src/main/java/ai/remi/comm/swagger/config/OpenApiConfig.java new file mode 100644 index 0000000..625c69d --- /dev/null +++ b/remi-comm/remi-comm-swagger/src/main/java/ai/remi/comm/swagger/config/OpenApiConfig.java @@ -0,0 +1,95 @@ +package ai.remi.comm.swagger.config; + +import ai.remi.comm.core.constant.HeaderConstant; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Configuration +@EnableAutoConfiguration +@ConditionalOnProperty(name = "knife4j.enable", matchIfMissing = true) +public class OpenApiConfig { + @Bean + public OpenAPI springShopOpenAPI() { + return new OpenAPI() + // 接口文档标题 + .info(new Info().title("Remi API 3") + // 接口文档描述 + .description("
Remi OpenApi 3
") + // 接口文档版本 + .version("v2.0.8") + // 开发者联系方式 + .contact(new Contact().name("点九开源").email("dianjiuxyz@gmail.com").url("https://dianjiu.xyz"))) + .components(new Components() + .headers(setHeaderParam())) + .externalDocs(new ExternalDocumentation() + // 额外补充说明 + .description("瑞米平台官网") + // 额外补充链接 + .url("https://startdis.com/")); + } + + /** + * JWT token + */ + private Map setHeaderParam() { + Map headers = new LinkedHashMap(); + + Header serviceType = new Header(); + serviceType.setDescription("公共-服务类型"); + serviceType.setRequired(false); + headers.put(HeaderConstant.X_SERVICE_TYPE, serviceType); + + Header deptTenantId = new Header(); + deptTenantId.setDescription("公共-部门租户Id"); + deptTenantId.setRequired(false); + headers.put(HeaderConstant.X_DEPT_TENANT_ID, deptTenantId); + + Header companyTenantId = new Header(); + companyTenantId.setDescription("公共-公司租户Id"); + companyTenantId.setRequired(false); + headers.put(HeaderConstant.X_COMPANY_TENANT_ID, companyTenantId); + + Header uniqueId = new Header(); + uniqueId.setDescription("公共-用户唯一ID"); + uniqueId.setRequired(false); + headers.put(HeaderConstant.X_UNIQUE_ID, uniqueId); + + Header accessToken = new Header(); + accessToken.setDescription("公共-用户鉴权Token"); + accessToken.setRequired(false); + headers.put(HeaderConstant.X_ACCESS_TOKEN, accessToken); + + Header distinctId = new Header(); + distinctId.setDescription("公共-设备唯一标识"); + distinctId.setRequired(false); + headers.put(HeaderConstant.X_DISTINCT_ID, distinctId); + + Header permissionScope = new Header(); + permissionScope.setDescription("公共-权限类型"); + permissionScope.setRequired(false); + headers.put(HeaderConstant.X_PERMISSION_SCOPE, permissionScope); + + Header companyIds = new Header(); + companyIds.setDescription("公共-公司ID集合"); + companyIds.setRequired(false); + headers.put(HeaderConstant.X_COMPANY_IDS, companyIds); + + Header deptIds = new Header(); + deptIds.setDescription("公共-部门ID集合"); + deptIds.setRequired(false); + headers.put(HeaderConstant.X_DEPT_IDS, deptIds); + + return headers; + } +} diff --git a/remi-comm/remi-comm-thread/pom.xml b/remi-comm/remi-comm-thread/pom.xml new file mode 100644 index 0000000..33a686e --- /dev/null +++ b/remi-comm/remi-comm-thread/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + remi-comm-thread + remi-comm-thread + remi-comm-thread + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + + + + org.projectlombok + lombok + + + + diff --git a/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/config/CustomThreadPollTaskExecutor.java b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/config/CustomThreadPollTaskExecutor.java new file mode 100644 index 0000000..d26a823 --- /dev/null +++ b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/config/CustomThreadPollTaskExecutor.java @@ -0,0 +1,75 @@ +package ai.remi.comm.thread.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.concurrent.ListenableFuture; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email lidianjiu@njydsz.com + * @date 2022-07-14 + * @desc + */ +@Configuration +public class CustomThreadPollTaskExecutor extends ThreadPoolTaskExecutor { + + private Logger log = LoggerFactory.getLogger(CustomThreadPollTaskExecutor.class); + private void showThreadPoolInfo(String prefix) { + ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor(); + + if (null == threadPoolExecutor) { + return; + } + + log.info("{}, {},taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]", + this.getThreadNamePrefix(), + prefix, + threadPoolExecutor.getTaskCount(), + threadPoolExecutor.getCompletedTaskCount(), + threadPoolExecutor.getActiveCount(), + threadPoolExecutor.getQueue().size()); + } + + @Override + public void execute(Runnable task) { + showThreadPoolInfo("1. do execute"); + super.execute(task); + } + + @Override + public void execute(Runnable task, long startTimeout) { + showThreadPoolInfo("2. do execute"); + super.execute(task, startTimeout); + } + + @Override + public Future submit(Runnable task) { + showThreadPoolInfo("1. do submit"); + return super.submit(task); + } + + @Override + public Future submit(Callable task) { + showThreadPoolInfo("2. do submit"); + return super.submit(task); + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + showThreadPoolInfo("1. do submitListenable"); + return super.submitListenable(task); + } + + @Override + public ListenableFuture submitListenable(Callable task) { + showThreadPoolInfo("2. do submitListenable"); + return super.submitListenable(task); + } + +} \ No newline at end of file diff --git a/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/config/ThreadPoolConfiguration.java b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/config/ThreadPoolConfiguration.java new file mode 100644 index 0000000..fa515d5 --- /dev/null +++ b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/config/ThreadPoolConfiguration.java @@ -0,0 +1,34 @@ +package ai.remi.comm.thread.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email lidianjiu@njydsz.com + * @date 2022-07-20 + * @desc + */ +@Data +@Component +@ConfigurationProperties(prefix = "async.executor.thread", ignoreInvalidFields = true) +public class ThreadPoolConfiguration { + /** + * 线程池总数 + */ + private int corePoolSize; + /** + * 最大线程池数 + */ + private int maxPoolSize; + /** + * 队列大小 + */ + private int queueCapacity; + /** + * 缓冲队列中线程的空闲时间 + */ + private int keepAliveSeconds; + +} diff --git a/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/BusinessPlanThreadPoolTask.java b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/BusinessPlanThreadPoolTask.java new file mode 100644 index 0000000..23cb874 --- /dev/null +++ b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/BusinessPlanThreadPoolTask.java @@ -0,0 +1,76 @@ +package ai.remi.comm.thread.pool; + +import ai.remi.comm.thread.config.ThreadPoolConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import javax.annotation.Resource; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 基于ThreadPoolTaskExecutor线程池配置 + * 分析下继承关系: + * + * 1、ThreadPoolTaskExecutor extends (2)ExecutorConfigurationSupport + * implements (3)AsyncListenableTaskExecutor, (4)systemTaskExecutor + * 2、 ExecutorConfigurationSupport extends CustomizableThreadFactory implements BeanNameAware, InitializingBean, DisposableBean + * + * 3、public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor + * 4、public interface systemTaskExecutor extends AsyncTaskExecutor + * + * 从上继承关系可知: + * ThreadPoolExecutor是一个java类不提供spring生命周期和参数装配。 + * ThreadPoolTaskExecutor实现了InitializingBean, DisposableBean ,xxaware等,具有spring特性 + * + * AsyncListenableTaskExecutor提供了监听任务方法(相当于添加一个任务监听,提交任务完成都会回调该方法) + * + * 简单理解: + * 1、ThreadPoolTaskExecutor使用ThreadPoolExecutor并增强,扩展了更多特性 + * 2、ThreadPoolTaskExecutor只关注自己增强的部分,任务执行还是ThreadPoolExecutor处理。 + * 3、前者spring自己用着爽,后者离开spring我们用ThreadPoolExecutor爽。 + * 注意:ThreadPoolTaskExecutor 不会自动创建ThreadPoolExecutor需要手动调initialize才会创建 + * 如果@Bean 就不需手动,会自动InitializingBean的afterPropertiesSet来调initialize + * @author DianJiu 【公众号 点九开源】 + */ +@Configuration +public class BusinessPlanThreadPoolTask { + private Logger log = LoggerFactory.getLogger(BusinessPlanThreadPoolTask.class); + + @Resource + private ThreadPoolConfiguration threadPoolConfiguration; + + /** + * 使用方式 + * @Resource(name = "socketServerTaskPool") + * private ThreadPoolTaskExecutor serverExecutor + * 然后调用excute方法或者submit方法 + */ + @Bean(name = "businessPlanExcelRead") + public ThreadPoolTaskExecutor businessPlanThreadPool() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //获取到服务器的cpu内核 + int i = Runtime.getRuntime().availableProcessors(); + //核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //存活时间 300s + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //阻塞队列长度 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //线程名前缀 + executor.setThreadNamePrefix("business-plan-"); + //拒绝策略 + //AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException + //CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。 + //DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。 + //DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } +} diff --git a/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/OverrideDefaultThreadPool.java b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/OverrideDefaultThreadPool.java new file mode 100644 index 0000000..7ec3f98 --- /dev/null +++ b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/OverrideDefaultThreadPool.java @@ -0,0 +1,279 @@ +package ai.remi.comm.thread.pool; + +import ai.remi.comm.thread.config.CustomThreadPollTaskExecutor; +import ai.remi.comm.thread.config.ThreadPoolConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import javax.annotation.Resource; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @author DianJiu 【公众号 点九开源】 + * @author DianJiu 【公众号 点九开源】 + * @email lidianjiu@njydsz.com + * @date 2022-07-20 + * @desc 重写默认线程池配置 + * 基于ThreadPoolTaskExecutor线程池配置 + * 分析下继承关系: + *

+ * 1、ThreadPoolTaskExecutor extends (2)ExecutorConfigurationSupport + * implements (3)AsyncListenableTaskExecutor, (4)systemTaskExecutor + * 2、 ExecutorConfigurationSupport extends CustomizableThreadFactory implements BeanNameAware, InitializingBean, DisposableBean + *

+ * 3、public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor + * 4、public interface systemTaskExecutor extends AsyncTaskExecutor + *

+ * 从上继承关系可知: + * ThreadPoolExecutor是一个java类不提供spring生命周期和参数装配。 + * ThreadPoolTaskExecutor实现了InitializingBean, DisposableBean ,xxaware等,具有spring特性 + *

+ * AsyncListenableTaskExecutor提供了监听任务方法(相当于添加一个任务监听,提交任务完成都会回调该方法) + *

+ * 简单理解: + * 1、ThreadPoolTaskExecutor使用ThreadPoolExecutor并增强,扩展了更多特性 + * 2、ThreadPoolTaskExecutor只关注自己增强的部分,任务执行还是ThreadPoolExecutor处理。 + * 3、前者spring自己用着爽,后者离开spring我们用ThreadPoolExecutor爽。 + * 注意:ThreadPoolTaskExecutor 不会自动创建ThreadPoolExecutor需要手动调initialize才会创建 + * 如果@Bean 就不需手动,会自动InitializingBean的afterPropertiesSet来调initialize + */ +@Configuration +@EnableAsync +public class OverrideDefaultThreadPool implements AsyncConfigurer { + + private Logger log = LoggerFactory.getLogger(OverrideDefaultThreadPool.class); + + @Resource + private ThreadPoolConfiguration threadPoolConfiguration; + + /** + * 使用方式:在某个service方法上使用该注解 + * + * @Async("customizeThreadPool") 注意: + * 如下方式会使@Async失效 + * 一、异步方法使用static修饰 + * 二、异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类 + * 三、异步方法不能与异步方法在同一个类中 + * 四、类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象 + * 五、如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解 + * 六、在Async 方法上标注@Transactional是没用的。 在Async 方法调用的方法上标注@Transactional 有效。 + * 七、调用被@Async标记的方法的调用者不能和被调用的方法在同一类中不然不会起作用!!!!!!! + * 八、使用@Async时要求是不能有返回值的不然会报错的 因为异步要求是不关心结果的 + */ + @Bean(name = "customizeThreadPool") + public Executor customizeThreadExecutor() { + log.info("start customizeThreadExecutor"); + ThreadPoolTaskExecutor executor = new CustomThreadPollTaskExecutor(); + //配置核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //配置最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //配置队列大小 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //活跃时间 + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //配置线程池中的线程的名称前缀 + executor.setThreadNamePrefix("customize-thread-"); + //设置是否允许核心线程超时。若允许,核心线程超时后,会被销毁。默认为不允许(fasle)。 + executor.setAllowCoreThreadTimeOut(false); + //设置shutdown时是否等到所有任务完成再真正关闭 + executor.setWaitForTasksToCompleteOnShutdown(true); + //当 setWaitForTasksToCompleteOnShutdown(true)时,setAwaitTerminationSeconds 设置在 shutdown 之后最多等待多长时间后再真正关闭线程池 + executor.setAwaitTerminationSeconds(60); + // rejection-policy:当pool已经达到max size的时候,如何处理新任务 + // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } + + + /** + * 保存主表日志线程池 + * + * @return Executor + */ + @Bean(name = "saveMasterLogThreadPool") + public Executor saveMasterLogThreadExecutor() { + log.info("start saveMasterLogThreadExecutor"); + ThreadPoolTaskExecutor executor = new CustomThreadPollTaskExecutor(); + //配置核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //配置最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //配置队列大小 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //活跃时间 + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //配置线程池中的线程的名称前缀 + executor.setThreadNamePrefix("saveMasterLog-thread-"); + //设置是否允许核心线程超时。若允许,核心线程超时后,会被销毁。默认为不允许(fasle)。 + executor.setAllowCoreThreadTimeOut(false); + //设置shutdown时是否等到所有任务完成再真正关闭 + executor.setWaitForTasksToCompleteOnShutdown(true); + //当 setWaitForTasksToCompleteOnShutdown(true)时,setAwaitTerminationSeconds 设置在 shutdown 之后最多等待多长时间后再真正关闭线程池 + executor.setAwaitTerminationSeconds(60); + // rejection-policy:当pool已经达到max size的时候,如何处理新任务 + // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } + + /** + * 保存子表日志线程池 + * + * @return Executor + */ + @Bean(name = "saveDayLogThreadPool") + public Executor saveDayLogThreadExecutor() { + log.info("start saveDayLogThreadExecutor"); + ThreadPoolTaskExecutor executor = new CustomThreadPollTaskExecutor(); + //配置核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //配置最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //配置队列大小 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //活跃时间 + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //配置线程池中的线程的名称前缀 + executor.setThreadNamePrefix("saveDayLog-thread-"); + //设置是否允许核心线程超时。若允许,核心线程超时后,会被销毁。默认为不允许(fasle)。 + executor.setAllowCoreThreadTimeOut(false); + //设置shutdown时是否等到所有任务完成再真正关闭 + executor.setWaitForTasksToCompleteOnShutdown(true); + //当 setWaitForTasksToCompleteOnShutdown(true)时,setAwaitTerminationSeconds 设置在 shutdown 之后最多等待多长时间后再真正关闭线程池 + executor.setAwaitTerminationSeconds(60); + // rejection-policy:当pool已经达到max size的时候,如何处理新任务 + // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } + + + /** + * 保存子表线程池 + * + * @return Executor + */ + @Bean(name = "saveDayThreadPool") + public Executor saveDayThreadExecutor() { + log.info("start saveDayThreadExecutor"); + ThreadPoolTaskExecutor executor = new CustomThreadPollTaskExecutor(); + //配置核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //配置最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //配置队列大小 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //活跃时间 + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //配置线程池中的线程的名称前缀 + executor.setThreadNamePrefix("saveDay-thread-"); + //设置是否允许核心线程超时。若允许,核心线程超时后,会被销毁。默认为不允许(fasle)。 + executor.setAllowCoreThreadTimeOut(false); + //设置shutdown时是否等到所有任务完成再真正关闭 + executor.setWaitForTasksToCompleteOnShutdown(true); + //当 setWaitForTasksToCompleteOnShutdown(true)时,setAwaitTerminationSeconds 设置在 shutdown 之后最多等待多长时间后再真正关闭线程池 + executor.setAwaitTerminationSeconds(60); + // rejection-policy:当pool已经达到max size的时候,如何处理新任务 + // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } + + + /** + * 保存redis线程池 + * + * @return Executor + */ + @Bean(name = "saveRedisThreadPool") + public Executor saveRedisThreadExecutor() { + log.info("start saveRedisThreadExecutor"); + ThreadPoolTaskExecutor executor = new CustomThreadPollTaskExecutor(); + //配置核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //配置最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //配置队列大小 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //活跃时间 + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //配置线程池中的线程的名称前缀 + executor.setThreadNamePrefix("saveRedis-thread-"); + //设置是否允许核心线程超时。若允许,核心线程超时后,会被销毁。默认为不允许(fasle)。 + executor.setAllowCoreThreadTimeOut(false); + //设置shutdown时是否等到所有任务完成再真正关闭 + executor.setWaitForTasksToCompleteOnShutdown(true); + //当 setWaitForTasksToCompleteOnShutdown(true)时,setAwaitTerminationSeconds 设置在 shutdown 之后最多等待多长时间后再真正关闭线程池 + executor.setAwaitTerminationSeconds(60); + // rejection-policy:当pool已经达到max size的时候,如何处理新任务 + // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } + + + /** + * 使用方式:在某个service方法上使用该注解 + * + * @return + * @Async + */ + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //核心线程池大小 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //队列容量 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //活跃时间 + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //线程名字前缀 + executor.setThreadNamePrefix("default-thread-"); + //设置是否允许核心线程超时。若允许,核心线程超时后,会被销毁。默认为不允许(fasle)。 + executor.setAllowCoreThreadTimeOut(false); + //设置shutdown时是否等到所有任务完成再真正关闭 + executor.setWaitForTasksToCompleteOnShutdown(true); + //当 setWaitForTasksToCompleteOnShutdown(true)时,setAwaitTerminationSeconds 设置在 shutdown 之后最多等待多长时间后再真正关闭线程池 + executor.setAwaitTerminationSeconds(60); + // rejection-policy:当pool已经达到max size的时候,如何处理新任务 + // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } + + /** + * 异步任务中异常处理 + * + * @return + */ + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return (ex, method, params) -> { + log.error("==========================" + ex.getMessage() + "=======================", ex); + log.error("default-thread-pool exception method:" + method.getName()); + }; + } +} diff --git a/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/WebSocketThreadPoolTask.java b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/WebSocketThreadPoolTask.java new file mode 100644 index 0000000..4fdb89e --- /dev/null +++ b/remi-comm/remi-comm-thread/src/main/java/ai/remi/comm/thread/pool/WebSocketThreadPoolTask.java @@ -0,0 +1,100 @@ +package ai.remi.comm.thread.pool; + +import ai.remi.comm.thread.config.ThreadPoolConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import javax.annotation.Resource; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 基于ThreadPoolTaskExecutor线程池配置 + * 分析下继承关系: + * + * 1、ThreadPoolTaskExecutor extends (2)ExecutorConfigurationSupport + * implements (3)AsyncListenableTaskExecutor, (4)systemTaskExecutor + * 2、 ExecutorConfigurationSupport extends CustomizableThreadFactory implements BeanNameAware, InitializingBean, DisposableBean + * + * 3、public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor + * 4、public interface systemTaskExecutor extends AsyncTaskExecutor + * + * 从上继承关系可知: + * ThreadPoolExecutor是一个java类不提供spring生命周期和参数装配。 + * ThreadPoolTaskExecutor实现了InitializingBean, DisposableBean ,xxaware等,具有spring特性 + * + * AsyncListenableTaskExecutor提供了监听任务方法(相当于添加一个任务监听,提交任务完成都会回调该方法) + * + * 简单理解: + * 1、ThreadPoolTaskExecutor使用ThreadPoolExecutor并增强,扩展了更多特性 + * 2、ThreadPoolTaskExecutor只关注自己增强的部分,任务执行还是ThreadPoolExecutor处理。 + * 3、前者spring自己用着爽,后者离开spring我们用ThreadPoolExecutor爽。 + * 注意:ThreadPoolTaskExecutor 不会自动创建ThreadPoolExecutor需要手动调initialize才会创建 + * 如果@Bean 就不需手动,会自动InitializingBean的afterPropertiesSet来调initialize + * @author DianJiu 【公众号 点九开源】 + */ +@Configuration +public class WebSocketThreadPoolTask { + private Logger log = LoggerFactory.getLogger(WebSocketThreadPoolTask.class); + + @Resource + private ThreadPoolConfiguration threadPoolConfiguration; + + /** + * 使用方式 + * @Resource(name = "socketServerTaskPool") + * private ThreadPoolTaskExecutor serverExecutor + * 然后调用excute方法或者submit方法 + */ + @Bean(name = "socketServerTaskPool") + public ThreadPoolTaskExecutor serverTaskPool() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //获取到服务器的cpu内核 + int i = Runtime.getRuntime().availableProcessors(); + //核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //存活时间 300s + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //阻塞队列长度 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //线程名前缀 + executor.setThreadNamePrefix("socket-server-"); + //拒绝策略 + //AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException + //CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。 + //DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。 + //DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } + + @Bean(name = "socketMessageTaskPool") + public ThreadPoolTaskExecutor serverMessageTaskPool() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //核心线程数 + executor.setCorePoolSize(threadPoolConfiguration.getCorePoolSize()); + //最大线程数 + executor.setMaxPoolSize(threadPoolConfiguration.getMaxPoolSize()); + //存活时间 300s + executor.setKeepAliveSeconds(threadPoolConfiguration.getKeepAliveSeconds()); + //阻塞队列长度 + executor.setQueueCapacity(threadPoolConfiguration.getQueueCapacity()); + //线程名前缀 + executor.setThreadNamePrefix("socket-message-"); + //拒绝策略 + //AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException + //CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。 + //DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。 + //DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + //执行初始化 + executor.initialize(); + return executor; + } +} diff --git a/remi-comm/remi-comm-thread/src/main/resources/rebel.xml b/remi-comm/remi-comm-thread/src/main/resources/rebel.xml new file mode 100644 index 0000000..4a6451c --- /dev/null +++ b/remi-comm/remi-comm-thread/src/main/resources/rebel.xml @@ -0,0 +1,16 @@ + + + + + + remi-comm-thread + + +

+ + + + diff --git a/remi-comm/remi-comm-util/pom.xml b/remi-comm/remi-comm-util/pom.xml new file mode 100644 index 0000000..2492454 --- /dev/null +++ b/remi-comm/remi-comm-util/pom.xml @@ -0,0 +1,185 @@ + + + 4.0.0 + remi-comm-util + remi-comm-util + remi-comm-util + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + jaxb-api + javax.xml.bind + + + + fastjson + com.alibaba + + + + gson + com.google.code.gson + + + + jackson-core + com.fasterxml.jackson.core + + + jackson-annotations + com.fasterxml.jackson.core + + + jackson-databind + com.fasterxml.jackson.core + + + + eu.bitwalker + UserAgentUtils + + + + org.lionsoul + ip2region + + + + org.apache.skywalking + apm-toolkit-trace + + + + hutool-core + cn.hutool + + + hutool-extra + cn.hutool + + + hutool-all + cn.hutool + + + + com.squareup.okhttp3 + okhttp + + + + httpclient + org.apache.httpcomponents + + + + javax.servlet-api + javax.servlet + + + + dom4j + org.dom4j + + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + + + commons-codec + commons-codec + + + commons-net + commons-net + + + commons-validator + commons-validator + + + commons-collections + commons-collections + + + commons-fileupload + commons-fileupload + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + org.slf4j + slf4j-nop + + + + org.projectlombok + lombok + + + + easyexcel + com.alibaba + + + + hibernate-validator + org.hibernate.validator + + + hibernate-validator + org.hibernate + + + + reactor-core + io.projectreactor + + + + org.springframework + spring-web + + + + + diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/array/ArrayUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/array/ArrayUtils.java new file mode 100644 index 0000000..681df12 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/array/ArrayUtils.java @@ -0,0 +1,705 @@ +package ai.remi.comm.util.array; + +import cn.hutool.core.collection.CollectionUtil; + +import java.lang.reflect.Array; +import java.util.*; + +public class ArrayUtils { + /** + * 判断数组是否为空 + * + * @param array 数组 + * @return 布尔值 + */ + public static boolean isNotEmpty(Object[] array) { + return !isEmpty(array); + } + + /** + * 判断数组是否非空 + * + * @param array 数组 + * @return 返回布尔值 + */ + public static boolean isEmpty(Object[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(boolean[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(byte[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(char[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(double[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(float[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(int[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(long[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 若数组为null, 或长度为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(short[] array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(boolean[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(byte[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(char[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(double[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(float[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(int[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(long[] array) { + return !isEmpty(array); + } + + /** + * 数组是否为不空 + * + * @param array 数组 + * @return 若数组不为null, 而且长度不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(short[] array) { + return !isEmpty(array); + } + + /** + * 判断参数是否是一个数组 + * + * @param obj 对象 + * @return 布尔值 + */ + public static boolean isArray(Object obj) { + return obj != null && obj.getClass().isArray(); + } + + /** + * 将参数组装成数组 + * + * @param obj 参数对象 + * @return 返回对象数组 + */ + public static E[] asArray(E... obj) { + return obj; + } + + /** + * 将参数集合转换为数组表示 + * + * @param collection 集合参数 + * @return 布尔值 + */ + public static E[] asArray(Collection collection) { + if (collection == null || collection.size() == 0) { + return null; + } + int length = collection.size(); + E e = collection.iterator().next(); + @SuppressWarnings("unchecked") + E[] array = (E[]) Array.newInstance(e.getClass(), length); + return collection.toArray(array); + } + + /** + * 获取数组元素的实际数据类型 + * + * @param array 数组 + * @return 布尔值 + */ + public static Class getElementType(E[] array) { + Class clas = array.getClass(); + Class type = (Class) clas.getComponentType(); + return type; + } + + /** + * 获取数组元素的实际数据类型 + * + * @param array 数组 + * @return 布尔值 + */ + public static Class getElementType(Object array) { + if (!isArray(array)) { + throw new IllegalArgumentException("argument must be an array"); + } + return array.getClass().getComponentType(); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(E[] array) { + return Arrays.asList(array); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(boolean[] array) { + int length = Array.getLength(array); + return asList(new Boolean[length], array, length); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(byte[] array) { + int length = Array.getLength(array); + return asList(new Byte[length], array, length); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(char[] array) { + int length = Array.getLength(array); + return asList(new Character[length], array, length); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(double[] array) { + int length = Array.getLength(array); + return asList(new Double[length], array, length); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(float[] array) { + int length = Array.getLength(array); + return asList(new Float[length], array, length); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(int[] array) { + int length = Array.getLength(array); + return asList(new Integer[length], array, length); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(long[] array) { + int length = Array.getLength(array); + return asList(new Long[length], array, length); + } + + /** + * 数组转化为List + * + * @param array 数组 + * @return 布尔值 + */ + public static List toList(short[] array) { + int length = Array.getLength(array); + return asList(new Short[length], array, length); + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return + */ + public static boolean contains(E[] array, E arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 布尔值 + */ + public static boolean contains(byte[] array, byte arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 布尔值 + */ + public static boolean contains(char[] array, char arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 布尔值 + */ + public static boolean contains(short[] array, short arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 返回布尔值 + */ + public static boolean contains(int[] array, int arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 返回布尔值 + */ + public static boolean contains(long[] array, long arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 返回布尔值 + */ + public static boolean contains(float[] array, float arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 返回布尔值 + */ + public static boolean contains(double[] array, double arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 数组元素中是否包含参数对象 + * + * @param array 数组 + * @param arg 元素参数 + * @return 返回布尔值 + */ + public static boolean contains(boolean[] array, boolean arg) { + return indexOf(array, arg) >= 0; + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(E[] array, E element) { + int size = array.length; + if (element == null) { + for (int i = 0; i < size; i++) { + if (array[i] == null) { + return i; + } + } + } else { + for (int i = 0; i < size; i++) { + if (element.equals(array[i])) { + return i; + } + } + } + return -1; + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(boolean[] array, boolean element) { + return indexFrom(array, element); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(byte[] array, byte element) { + return indexFrom(array, element); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(char[] array, char element) { + return indexFrom(array, element); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(double[] array, double element) { + return indexFrom(array, element); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(float[] array, float element) { + return indexFrom(array, element); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(int[] array, int element) { + return indexFrom(array, element); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(long[] array, long element) { + return indexFrom(array, element); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + public static int indexOf(short[] array, short element) { + return indexFrom(array, element); + } + + /** + * 列举 + * + * @param array 数组 + * @return + */ + public static Enumeration enumeration(E[] array) { + return new ArrayEnumeration(array); + } + + /** + * 元素所在数组的索引 + * + * @param array 数组 + * @param element 元素 + * @return 若数组中不存在该元素, 则返回-1 + */ + private static int indexFrom(Object array, Object element) { + int length = Array.getLength(array); + for (int i = 0; i < length; i++) { + Object e = Array.get(array, i); + if (element.equals(e)) { + return i; + } + } + return -1; + } + + /** + * 数组转化为List + * + * @param target 目标数组 + * @param origin 源数组对象 + * @param length 数组长度 + * @return + */ + private static List asList(E[] target, Object origin, int length) { + // java.lang.ArrayStoreException + // System.arraycopy(origin, 0, target, 0, length); + for (int i = 0; i < length; i++) { + target[i] = (E) Array.get(origin, i); + } + return Arrays.asList(target); + } + + /** + * 获取int的list中的最大值 + * + * @param list + * @return 返回int最大值 + */ + private static int getListMaxValue(List list) { + return Collections.max(list); + } + + /** + * 获取int的list中的最大值的下标 + * + * @param list + * @return 返回int最大值 + */ + private static int getListMaxValueIndex(List list) { + int max = list.get(0); + // 最大值下标 + int max_a = 0; + for (int i = 0; i < list.size(); i++) { + if (list.get(i) > list.get(max_a)) { + max = list.get(i); + max_a = i; + } + } + return max_a; + } + + /** + * 获取int的list中的最小值 + * + * @param list + * @return 返回int最小值 + */ + private static int getListMinValue(List list) { + return Collections.min(list); + } + + /** + * 获取int的list中的最大值的下标 + * + * @param list list + * @return 返回int最下值的下标 + */ + private static int getListMinValueIndex(List list) { + int min = list.get(0); + // 最大值下标 + int min_a = 0; + for (int i = 0; i < list.size(); i++) { + if (list.get(i) < list.get(min_a)) { + min_a = i; + } + } + return min_a; + } + + /** + * 数组列举 + */ + private static class ArrayEnumeration implements Enumeration { + + private E[] es; + private int index; + private int length; + + private ArrayEnumeration(E[] es) { + this.es = es; + this.length = es.length; + } + + @Override + public boolean hasMoreElements() { + return index < length; + } + + @Override + public E nextElement() { + return es[index++]; + } + } + + /** + * 链表中随机选择一定数量的元素 + * + * @param list 对象链表 + * @param num 选择链表长度 + * @return 字符串链表 + */ + public static List getRandomList(List list, int num) { + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + if (list.size() <= num) { + return list; + } + Collections.shuffle(list); + return list.subList(0, num); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/array/SortUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/array/SortUtils.java new file mode 100644 index 0000000..c562858 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/array/SortUtils.java @@ -0,0 +1,157 @@ +package ai.remi.comm.util.array; + +import java.util.Arrays; + +public class SortUtils { + + /** + * 如果找到了直接进行返回,如果没有找到就继续去找 + * + * @param arr 数组 + * @param key 关键字 + * @return 位置 + */ + public static int binarySearch(int[] arr, int key) { + int i = 0; + int j = arr.length - 1; + while (i < j) { + int middle = i + (j - i) / 2; //防止middle的和超过Integer.MAX_VALUE的值 + if (key > arr[middle]) { + i = middle + 1; + } else if (key < arr[middle]) { + j = middle - 1; + } else { + return middle; + } + } + return -1; + } + + /** + * 选择排序 + * + * @param arr 数组 + * @return int[] 数组 + */ + public static int[] selectSort(int[] arr) { + if(arr == null || arr.length < 2) { + return arr; + } + int len = arr.length; + for (int i = 0; i < len - 1; i++) { + for (int j = i + 1; j < len; j++) { + if (arr[i] > arr[j]) { + swap(arr, i, j); + } + } + } + return arr; + } + + /** + * 快速排序 + * + * @param arr 数组 + */ + public static void quickSort(int[] arr) { + int low = 0; + int high = arr.length - 1; + quickSortHelper(arr, low, high); + } + + /** + * 快速排序,递归算法 + * + * @param arr 数组 + * @param low 低位 + * @param high 高位 + */ + public static void quickSortHelper(int[] arr, int low, int high) { + int start = low; + int end = high; + int key = arr[start]; + while (start < end) { + // 要从尾到前面进行比较 + while (start < end && arr[end] >= key) { + end--; + } + if (arr[end] <= key) { + swap(arr, start, end); + } + // 要从前到后进行比较 + while (start < end && arr[start] <= key) { + start++; + } + if (arr[start] >= key) { + swap(arr, start, end); + } + } + if (start > low) { + quickSortHelper(arr, low, start - 1); + } + if (end < high) { + quickSortHelper(arr, end + 1, high); + } + } + + + /** + * 快速排序,递归算法 + * + * @param arr 输入数组 + * @return 新数组 + */ + public static int[] quickSort2(int[] arr) { + if (arr == null || arr.length < 2) { + return arr; + } + int pivot = arr[arr.length / 2]; + int[] left = Arrays.stream(arr).boxed().filter(e -> e < pivot).mapToInt(e -> e).toArray(); + int[] middle = Arrays.stream(arr).boxed().filter(e -> e == pivot).mapToInt(e -> e).toArray(); + int[] right = Arrays.stream(arr).boxed().filter(e -> e > pivot).mapToInt(e -> e).toArray(); + return concat(quickSort2(left), middle, quickSort2(right)); + } + + /** + * 冒泡排序 + * + * @param arr 数组 + */ + public static void bubbleSort(int[] arr) { + if(arr == null || arr.length < 2) { + return; + } + int len = arr.length; + for (int i = 0; i < len - 1; i++) { + for (int j = 0; j < len - 1 - i; j++) { + if (arr[j] > arr[j + 1]) { + swap(arr, j, j + 1); + } + } + } + } + + /** + * 交换他们数字的值 + * + * @param arr 数组 + * @param i 位置i + * @param j 位置j + */ + private static void swap(int[] arr, int i, int j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + + public static int[] concat(int[] left, int[] middle, int[] right) { + int totalLength = left.length + middle.length + right.length; + int[] result = new int[totalLength]; + + System.arraycopy(left, 0, result, 0, left.length); + System.arraycopy(middle, 0, result, left.length, middle.length); + System.arraycopy(right, 0, result, left.length + middle.length, right.length); + + return result; + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/asserts/AssertUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/asserts/AssertUtils.java new file mode 100644 index 0000000..4090bd9 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/asserts/AssertUtils.java @@ -0,0 +1,220 @@ +package ai.remi.comm.util.asserts; + +import ai.remi.comm.util.collection.CollectionUtils; +import ai.remi.comm.util.object.ObjectUtils; +import ai.remi.comm.util.string.StringUtils; +import lombok.SneakyThrows; + +import java.util.Collection; +import java.util.Map; + +/** + * 断言工具类 + */ +public class AssertUtils { + /** + * 如果不是true,则抛异常 + * @param expression + * @param msg + */ + @SneakyThrows + public static void isTrue(boolean expression, String msg) { + if (!expression) { + throw new RuntimeException(msg); + } + } + + /** + * 如果不是false,则抛异常 + * @param expression + * @param msg + */ + @SneakyThrows + public static void isFalse(boolean expression, String msg) { + if (expression) { + throw new RuntimeException(msg); + } + } + + /** + * 如果不是空或者空字符串,则抛异常 + * @param str + * @param msg + */ + @SneakyThrows + public static void isEmpty(String str, String msg) { + if (StringUtils.hasText(str)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果是空或者空字符串,则抛异常 + * @param str + * @param msg + */ + @SneakyThrows + public static void isNotEmpty(String str, String msg) { + if (!StringUtils.hasText(str)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果数组为不空或者长度大于等于1,则抛异常 + * @param str + * @param msg + */ + @SneakyThrows + public static void isEmpty(Object[] str, String msg) { + if (!ObjectUtils.isEmpty(str)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果数组为空或者长度小于1,则抛异常 + * @param str + * @param msg + */ + @SneakyThrows + public static void isNotEmpty(Object[] str, String msg) { + if (ObjectUtils.isEmpty(str)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果集合不为空或者长度大于等于1,则抛异常 + * @param collection + * @param msg + */ + @SneakyThrows + public static void isEmpty(Collection collection, String msg) { + if (CollectionUtils.isNotEmpty(collection)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果集合为空或者长度小于1,则抛异常 + * @param collection + * @param msg + */ + @SneakyThrows + public static void isNotEmpty(Collection collection, String msg) { + if (CollectionUtils.isEmpty(collection)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果集合不为空或者长度大于等于1,则抛异常 + * @param map + * @param msg + */ + @SneakyThrows + public static void isEmpty(Map map, String msg) { + if (CollectionUtils.isNotEmpty(map)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果集合为空或者长度小于1,则抛异常 + * @param map + * @param msg + */ + @SneakyThrows + public static void isNotEmpty(Map map, String msg) { + if (CollectionUtils.isEmpty(map)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果对象不为空,则抛异常 + * @param object + * @param msg + */ + @SneakyThrows + public static void isNull(Object object, String msg){ + if (object != null) { + throw new RuntimeException(msg); + } + } + + /** + * 如果对象空,则抛异常 + * @param object + * @param msg + */ + @SneakyThrows + public static void isNotNull(Object object, String msg){ + if (object == null) { + throw new RuntimeException(msg); + } + } + + /** + * 如果对象不为空,则抛异常 + * @param objects + * @param msg + */ + @SneakyThrows + public static void isNull(Object[] objects, String msg) { + if (!ObjectUtils.isEmpty(objects)) { + throw new RuntimeException(msg); + } + } + + /** + * 如果对象空,则抛异常 + * @param objects + * @param msg + */ + @SneakyThrows + public static void isNotNull(Object[] objects, String msg) { + if (ObjectUtils.isEmpty(objects)) { + throw new RuntimeException(msg); + } + } + + /** + * 比较对象 + * @param obj1 + * @param obj2 + * @param msg + */ + @SneakyThrows + public static void equals(Object obj1, Object obj2, String msg) { + if (!equals(obj1, obj2)) { + throw new RuntimeException(msg); + } + } + + /** + * 比较对象的相等 + * @param cs1 + * @param cs2 + * @return + */ + public static boolean equals(final Object cs1, final Object cs2) { + if (cs1 == cs2) { + return true; + } + if (null == cs1 && null != cs2) { + return false; + } + if (null != cs1 && null == cs2) { + return false; + } + if (cs1 == null || null == cs2) { + return true; + } + if (cs1 instanceof String && cs2 instanceof String) { + return cs1.equals(cs2); + } + return cs1.equals(cs2); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/AuthInfo.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/AuthInfo.java new file mode 100644 index 0000000..e0eac55 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/AuthInfo.java @@ -0,0 +1,48 @@ +package ai.remi.comm.util.auth; + + +import ai.remi.comm.core.enums.IdentityTypeEnum; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc AuthInfo + */ +public interface AuthInfo { + + /** + * 用户系统语言 + */ + String getUserLanguage(); + + /** + * 用户唯一标识 + */ + String getUniqueId(); + + /** + * 部门租户号 + */ + String getDeptTenantId(); + + /** + * 公司租户号 + */ + String getCompanyTenantId(); + + /** + * 身份类型 + */ + IdentityTypeEnum getIdentityTypeEnum(); + + /** + * 服务类型 + */ + String getServiceTypeCode(); + + /** + * 鉴权Token + * @return + */ + String getAccessToken(); +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/AuthInfoUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/AuthInfoUtils.java new file mode 100644 index 0000000..7cd38de --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/AuthInfoUtils.java @@ -0,0 +1,120 @@ +package ai.remi.comm.util.auth; + + +import ai.remi.comm.core.enums.IdentityTypeEnum; +import ai.remi.comm.core.enums.PermissionScopeEnum; + +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc AuthInfoUtils + */ +public class AuthInfoUtils { + + /** + * 获取认证信息 + */ + public static AuthInfo getAuthInfo() { + return RequestHolder.getAuthInfo(); + } + + /** + * 获取用户语言 + */ + public static String getUserLanguage() { + return RequestHolder.getAuthInfo().getUserLanguage(); + } + + /** + * 鉴权Token + * @return + */ + public static String getAccessToken() { + return RequestHolder.getCompanyAuthInfo().getAccessToken(); + } + + /** + * 获取用户唯一id 对应 {@link IdentityTypeEnum} + */ + public static String getUniqueId() { + return RequestHolder.getAuthInfo().getUniqueId(); + } + + /** + * 获取部门租户号 + */ + public static String getDeptTenantId() { + return RequestHolder.getAuthInfo().getDeptTenantId(); + } + + /** + * 获取公司租户号 + */ + public static String getCompanyTenantId() { + return RequestHolder.getAuthInfo().getCompanyTenantId(); + } + + + /** + * 获取用户类型 瑞米官方账户、集团公司账户、访客体验用户 + */ + public static IdentityTypeEnum getIdentityTypeEnum() { + return RequestHolder.getAuthInfo().getIdentityTypeEnum(); + } + + /** + * 获取当前服务类型,主要是给feign调用内部服务时使用 + */ + public static String getServiceTypeCode() { + return RequestHolder.getAuthInfo().getServiceTypeCode(); + } + + /** + * 获取权限范围 + */ + public static PermissionScopeEnum getPermissionScopeEnum() { + return RequestHolder.getCompanyAuthInfo().getPermissionScope(); + } + + /** + * 当权限范围为 {@link PermissionScopeEnum } 时, + * 为当前请求可操作、查询的公司id集合 + */ + public static Set getHasPermissionCompanyIds() { + return RequestHolder.getCompanyAuthInfo().getHasPermissionCompanyIds(); + } + + + /** + * 获取瑞米信息 + * + * @return 瑞米信息 + * @createTime 2022/1/6 19:35 + */ + //public static CustomerAuthInfo getCustomerAuthInfo() { + // return RequestHolder.getCustomerAuthInfo(); + //} + + /** + * 获取公司信息 + * + * @return 客户信息 + * @createTime 2022/1/6 19:35 + */ + public static CompanyAuthInfo getCompanyAuthInfo() { + return RequestHolder.getCompanyAuthInfo(); + } + + /** + * 获取信息 + * + * @return 访客信息 + * @createTime 2022/1/6 19:35 + */ + //public static CompanyAuthInfo getCompanyAuthInfo() { + // return RequestHolder.getCompanyAuthInfo(); + //} + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/CompanyAuthInfo.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/CompanyAuthInfo.java new file mode 100644 index 0000000..c2ce97e --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/CompanyAuthInfo.java @@ -0,0 +1,68 @@ +package ai.remi.comm.util.auth; + +import ai.remi.comm.core.enums.IdentityTypeEnum; +import ai.remi.comm.core.enums.PermissionScopeEnum; +import ai.remi.comm.core.enums.ServiceTypeEnum; +import lombok.Data; + +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc CompanyAuthInfo + */ +@Data +public class CompanyAuthInfo implements AuthInfo { + + /** + * 用户系统语言 + */ + private String userLanguage; + + /** + * 根据 {@link IdentityTypeEnum} 不同代表不同的身份 + * 用户id、不同端代表对应的id + */ + private String uniqueId; + + /** + * 部门租户号 + */ + private String deptTenantId; + + /** + * 公司租户号 + */ + private String companyTenantId; + + /** + * 鉴权Token + */ + private String accessToken; + + /** + * 权限范围 + */ + private PermissionScopeEnum permissionScope; + + /** + * 如果具有集团范围权限时,这个值为拥有这个权限的公司id集合 + */ + private Set hasPermissionCompanyIds; + + /** + * 如果具有公司范围权限时,这个值为拥有这个权限的部门id集合 + */ + private Set hasPermissionDeptIds; + + @Override + public IdentityTypeEnum getIdentityTypeEnum() { + return IdentityTypeEnum.COMPANY; + } + + @Override + public String getServiceTypeCode() { + return ServiceTypeEnum.WEB_SERVICE.getCode(); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/RequestHolder.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/RequestHolder.java new file mode 100644 index 0000000..df3b620 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/RequestHolder.java @@ -0,0 +1,79 @@ +package ai.remi.comm.util.auth; + + +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc RequestHolder + */ +@Slf4j +public class RequestHolder { + + private static final ThreadLocal authInfoHolder = new ThreadLocal(); + + private static final ThreadLocal requestHolder = new ThreadLocal(); + + + /** + * 使用实例: + * CompanyAuthInfo companyAuthInfo = new CompanyAuthInfo(); + * companyAuthInfo.setStoreId(pageRenderRealTimeDTO.getStoreId()); + * companyAuthInfo.setCompanyId(pageRenderRealTimeDTO.getCompanyId()); + * companyAuthInfo.setPermissionScope(PermissionScopeEnum.COMPANY); + * RequestHolder.add(companyAuthInfo); + * @param authInfo + */ + public static void add(AuthInfo authInfo) { + authInfoHolder.set(authInfo); + } + + public static void add(HttpServletRequest request) { + requestHolder.set(request); + } + + public static AuthInfo getAuthInfo() { + AuthInfo authInfo = authInfoHolder.get(); + if (authInfo == null) { + log.warn("当前线程AuthInfo为空,请检查"); + } + return authInfo; + } + + /*public static RemiAuthInfo getRemiAuthInfo() { + AuthInfo authInfo = getAuthInfo(); + if (authInfo instanceof RemiAuthInfo){ + return (RemiAuthInfo) authInfo; + } + throw new RuntimeException("类型转换错误"); + }*/ + + public static CompanyAuthInfo getCompanyAuthInfo() { + AuthInfo authInfo = getAuthInfo(); + if (authInfo instanceof CompanyAuthInfo) { + return (CompanyAuthInfo) authInfo; + } + throw new RuntimeException("类型转换错误"); + } + + /*public static VisitorAuthInfo getVisitorAuthInfo() { + AuthInfo authInfo = getAuthInfo(); + if (authInfo instanceof VisitorAuthInfo){ + return (VisitorAuthInfo) authInfo; + } + throw new RuntimeException("类型转换错误"); + }*/ + + public static HttpServletRequest getCurrentRequest() { + return requestHolder.get(); + } + + public static void remove() { + authInfoHolder.remove(); + requestHolder.remove(); + } + +} \ No newline at end of file diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/TenantHolder.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/TenantHolder.java new file mode 100644 index 0000000..7bb9f91 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/auth/TenantHolder.java @@ -0,0 +1,42 @@ +package ai.remi.comm.util.auth; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc TenantHolder + */ +public class TenantHolder { + + /** + * 是否是管理员 + */ + private static final ThreadLocal isAdminThreadLocal = new ThreadLocal<>(); + + /** + * 是否是不拦截的请求 + */ + private static final ThreadLocal urlIgnoreThreadLocal = new ThreadLocal<>(); + + + public static Boolean isAdmin() { + return isAdminThreadLocal.get(); + } + + public static void admin(boolean isAdmin) { + isAdminThreadLocal.set(isAdmin); + } + + public static Boolean getIgnoredRequest() { + return urlIgnoreThreadLocal.get(); + } + + public static void setIgnoredRequest(boolean ignoredRequest) { + urlIgnoreThreadLocal.set(ignoredRequest); + } + + + public static void remove() { + isAdminThreadLocal.remove(); + urlIgnoreThreadLocal.remove(); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/bean/BeanCopyUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/bean/BeanCopyUtils.java new file mode 100644 index 0000000..091ee69 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/bean/BeanCopyUtils.java @@ -0,0 +1,126 @@ +package ai.remi.comm.util.bean; + +import com.alibaba.fastjson.JSONObject; +import org.springframework.beans.BeanUtils; + +import java.lang.reflect.Field; +import java.util.*; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc BeanCopyUtils + */ +public class BeanCopyUtils { + + /** + * 浅拷贝list + * + * @param source 数据源 + * @param clazz 对象 + * @return LIST + */ + public static List copyListProperties(List source, Class clazz) { + List target = new ArrayList(); + source.forEach(o -> { + try { + target.add(JSONObject.parseObject(JSONObject.toJSONString(o), clazz)); + } catch (Exception e) { + e.printStackTrace(); + } + }); + return target; + } + + /** + * 源对象和目标对象浅拷贝 PS:目标对象和源对象中的属性名称需一至,否则无法copy + * + * @param sourceObj 源对象 + * @param targetObj 目标对象 + */ + public static void copyPropertiesThird(Object sourceObj, Object targetObj) { + BeanUtils.copyProperties(sourceObj, targetObj); + } + + public static T copyPropertiesThird(Object sourceObj, Class calzz) { + if (Objects.isNull(sourceObj) || Objects.isNull(calzz)) { + return null; + } + T c = null; + try { + // c = calzz.newInstance(); + c = calzz.getDeclaredConstructor().newInstance(); + BeanCopyUtils.copyPropertiesThird(sourceObj, c); + } catch (Exception e) { + System.out.println("BeanCopyUtils -> copyPropertiesThird error:" + e); + } + return c; + } + + /** + * 转换list泛型 + * + * @param source 数据源 + * @param clazz 对象 + * @return LIST + */ + public static List coverList(List source, Class clazz) { + List res = new ArrayList<>(); + source.forEach(o -> res.add(BeanCopyUtils.copyPropertiesThird(o, clazz))); + return res; + } + + + public static Map entityToMap(Object object) { + Map reMap = new HashMap(); + if (object == null) { + return null; + } else { + List fields = new ArrayList(); + List fieldsName = new ArrayList(); + + for(Class tempClass = object.getClass(); tempClass != null; tempClass = tempClass.getSuperclass()) { + fields.addAll(Arrays.asList(tempClass.getDeclaredFields())); + } + + List childFields = Arrays.asList(object.getClass().getDeclaredFields()); + Iterator var6 = childFields.iterator(); + + Field field; + while(var6.hasNext()) { + field = (Field)var6.next(); + fieldsName.add(field.getName()); + } + + try { + var6 = fields.iterator(); + + while(var6.hasNext()) { + field = (Field)var6.next(); + + try { + Field f; + Object o; + if (fieldsName.contains(field.getName())) { + f = object.getClass().getDeclaredField(field.getName()); + f.setAccessible(true); + o = f.get(object); + reMap.put(field.getName(), o); + } else { + f = object.getClass().getSuperclass().getDeclaredField(field.getName()); + f.setAccessible(true); + o = f.get(object); + reMap.put(field.getName(), o); + } + } catch (Exception var10) { + var10.printStackTrace(); + } + } + } catch (SecurityException var11) { + var11.printStackTrace(); + } + + return reMap; + } + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/collection/CollectionUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/collection/CollectionUtils.java new file mode 100644 index 0000000..3b3b621 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/collection/CollectionUtils.java @@ -0,0 +1,465 @@ +package ai.remi.comm.util.collection; + +import ai.remi.comm.util.array.ArrayUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; + +public class CollectionUtils { + public static final String DESC = "desc"; + public static final String ASC = "asc"; + + /** + * 集合是否为空 + * + * @param collection 集合 + * @return 若集合为null, 或集合大小为0, 则返回true, 否则返回false + */ + public static boolean isEmpty(Collection collection) { + return collection == null || collection.size() == 0; + } + + /** + * Map是否为空 + * + * @param map + * @return + */ + public static boolean isEmpty(Map map) { + return map == null || map.isEmpty(); + } + + /** + * 集合是否不为空 + * + * @param collection 集合 + * @return 若集合不为null, 而且集合大小不为0, 则返回true, 否则返回false + */ + public static boolean isNotEmpty(Collection collection) { + return !isEmpty(collection); + } + + /** + * Map是否不为空 + * + * @param map + * @return + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * 参数转换为List集合 + * + * @param objs 参数对象 + * @return 返回泛型 + */ + public static List asList(E... objs) { + return Arrays.asList(objs); + } + + /** + * 参数集合转换为List集合 + * + * @param collection 集合参数 + * @return 返回list泛型 + */ + public static List asList(Collection collection) { + if (collection == null) { + return null; + } + if (collection instanceof List) { + return (List) collection; + } + return new ArrayList(collection); + } + + /** + * 参数转换为Set集合 + * + * @param objs 参数对象 + * @return 返回set泛型 + */ + public static Set asSet(E... objs) { + return new HashSet(Arrays.asList(objs)); + } + + /** + * 参数转换为Set集合 + * + * @param collection 集合参数 + * @return 返回set泛型 + */ + public static Set asSet(Collection collection) { + if (collection == null) { + return null; + } + if (collection instanceof Set) { + return (Set) collection; + } + return new HashSet(collection); + } + + /** + * 将集合转换为数组 + * + * @param collection 集合 + * @returns 返回数组 + */ + public static E[] toArray(Collection collection) { + return ArrayUtils.asArray(collection); + } + + /** + * 创建ArrayList实例 + * + * @return 返回list + */ + public static List newList() { + return new ArrayList(); + } + + /** + * 创建一个指定容量的ArrayList实例 + * + * @param size 大小 + * @return 返回list + */ + public static List newList(int size) { + return new ArrayList(size); + } + + /** + * 创建一个HashSet实例 + * + * @return 返回set泛型 + */ + public static Set newSet() { + return new HashSet(16, .75f); + } + + /** + * 创建一个指定容量大小的HashSet实例 + * + * @param initialCapacity 初始容量 + * @param loadFactor 加载因子 + * @return 返回set + */ + public static Set newSet(int initialCapacity, double loadFactor) { + return new HashSet(initialCapacity, (float) loadFactor); + } + + /** + * 获取map的最小key的value + * + * @param map 传入map的值 + * @return String + */ + public static String getMinKeysValue(Map map) { + if (map == null || map.size() < 1) { + return new String(); + } + int key = map.keySet().stream().min(Comparator.comparing(Function.identity())).get(); + return map.get(key); + } + + /** + * 对list中的元素按升序排列. + * + * @param list 排序集合 + * @param field 排序字段 + * @return + */ + public static List sort(List list, final String field) { + return sort(list, field, null); + } + + /** + * 对list中的元素进行排序. + * + * @param list 排序集合 + * @param field 排序字段 + * @param sort 排序方式: SortList.DESC(降序) SortList.ASC(升序). + */ + @SuppressWarnings("unchecked") + public static List sort(List list, final String field, + final String sort) { + Collections.sort(list, new Comparator() { + public int compare(Object a, Object b) { + int ret = 0; + try { + Field f = a.getClass().getDeclaredField(field); + f.setAccessible(true); + Class type = f.getType(); + + if (type == int.class) { + ret = Integer.compare(f.getInt(a), f + .getInt(b)); + } else if (type == double.class) { + ret = Double.compare(f.getDouble(a), f + .getDouble(b)); + } else if (type == long.class) { + ret = Long.compare(f.getLong(a), f + .getLong(b)); + } else if (type == float.class) { + ret = Float.compare(f.getFloat(a), f + .getFloat(b)); + } else if (type == Date.class) { + ret = ((Date) f.get(a)).compareTo((Date) f.get(b)); + } else if (isImplementsOf(type, Comparable.class)) { + ret = ((Comparable) f.get(a)).compareTo(f + .get(b)); + } else { + ret = String.valueOf(f.get(a)).compareTo( + String.valueOf(f.get(b))); + } + + } catch (SecurityException | NoSuchFieldException | IllegalAccessException | + IllegalArgumentException e) { + e.printStackTrace(); + } + if (sort != null && sort.equalsIgnoreCase(DESC)) { + return -ret; + } else { + return ret; + } + + } + }); + return list; + } + + /** + * 对list中的元素按fields和sorts进行排序, + * fields[i]指定排序字段,sorts[i]指定排序方式.如果sorts[i]为空则默认按升序排列. + * + * @param list + * @param fields + * @param sorts + */ + @SuppressWarnings("unchecked") + public static List sort(List list, String[] fields, String[] sorts) { + if (fields != null && fields.length > 0) { + for (int i = fields.length - 1; i >= 0; i--) { + final String field = fields[i]; + String tmpSort = ASC; + if (sorts != null && sorts.length > i && sorts[i] != null) { + tmpSort = sorts[i]; + } + final String sort = tmpSort; + Collections.sort(list, new Comparator() { + public int compare(Object a, Object b) { + int ret = 0; + try { + Field f = a.getClass().getDeclaredField(field); + f.setAccessible(true); + Class type = f.getType(); + if (type == int.class) { + ret = ((Integer) f.getInt(a)) + .compareTo(f.getInt(b)); + } else if (type == double.class) { + ret = ((Double) f.getDouble(a)) + .compareTo(f.getDouble(b)); + } else if (type == long.class) { + ret = ((Long) f.getLong(a)).compareTo(f + .getLong(b)); + } else if (type == float.class) { + ret = ((Float) f.getFloat(a)) + .compareTo(f.getFloat(b)); + } else if (type == Date.class) { + ret = ((Date) f.get(a)).compareTo((Date) f + .get(b)); + } else if (isImplementsOf(type, Comparable.class)) { + ret = ((Comparable) f.get(a)) + .compareTo(f.get(b)); + } else { + ret = String.valueOf(f.get(a)).compareTo( + String.valueOf(f.get(b))); + } + + } catch (SecurityException | NoSuchFieldException | IllegalArgumentException | + IllegalAccessException e) { + e.printStackTrace(); + } + + if (sort != null && sort.equalsIgnoreCase(DESC)) { + return -ret; + } else { + return ret; + } + } + }); + } + } + return list; + } + + /** + * 默认按正序排列 + * + * @param list + * @param method + */ + public static List sortByMethod(List list, final String method) { + return sortByMethod(list, method, null); + } + + @SuppressWarnings("unchecked") + public static List sortByMethod(List list, final String method, + final String sort) { + Collections.sort(list, new Comparator() { + public int compare(Object a, Object b) { + int ret = 0; + try { + Method m = a.getClass().getMethod(method, null); + m.setAccessible(true); + Class type = m.getReturnType(); + if (type == int.class) { + ret = ((Integer) m.invoke(a, null)) + .compareTo((Integer) m.invoke(b, null)); + } else if (type == double.class) { + ret = ((Double) m.invoke(a, null)).compareTo((Double) m + .invoke(b, null)); + } else if (type == long.class) { + ret = ((Long) m.invoke(a, null)).compareTo((Long) m + .invoke(b, null)); + } else if (type == float.class) { + ret = ((Float) m.invoke(a, null)).compareTo((Float) m + .invoke(b, null)); + } else if (type == Date.class) { + ret = ((Date) m.invoke(a, null)).compareTo((Date) m + .invoke(b, null)); + } else if (isImplementsOf(type, Comparable.class)) { + ret = ((Comparable) m.invoke(a, null)) + .compareTo(m.invoke(b, null)); + } else { + ret = String.valueOf(m.invoke(a, null)).compareTo( + String.valueOf(m.invoke(b, null))); + } + + if (isImplementsOf(type, Comparable.class)) { + ret = ((Comparable) m.invoke(a, null)) + .compareTo(m.invoke(b, null)); + } else { + ret = String.valueOf(m.invoke(a, null)).compareTo( + String.valueOf(m.invoke(b, null))); + } + + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ne) { + ne.printStackTrace(); + } + + if (sort != null && sort.toLowerCase().equals(DESC)) { + return -ret; + } else { + return ret; + } + } + }); + return list; + } + + @SuppressWarnings("unchecked") + public static List sortByMethod(List list, final String methods[], + final String sorts[]) { + if (methods != null && methods.length > 0) { + for (int i = methods.length - 1; i >= 0; i--) { + final String method = methods[i]; + String tmpSort = ASC; + if (sorts != null && sorts.length > i && sorts[i] != null) { + tmpSort = sorts[i]; + } + final String sort = tmpSort; + Collections.sort(list, new Comparator() { + public int compare(Object a, Object b) { + int ret = 0; + try { + Method m = a.getClass().getMethod(method, null); + m.setAccessible(true); + Class type = m.getReturnType(); + if (type == int.class) { + ret = ((Integer) m.invoke(a, null)) + .compareTo((Integer) m.invoke(b, null)); + } else if (type == double.class) { + ret = ((Double) m.invoke(a, null)) + .compareTo((Double) m.invoke(b, null)); + } else if (type == long.class) { + ret = ((Long) m.invoke(a, null)) + .compareTo((Long) m.invoke(b, null)); + } else if (type == float.class) { + ret = ((Float) m.invoke(a, null)) + .compareTo((Float) m.invoke(b, null)); + } else if (type == Date.class) { + ret = ((Date) m.invoke(a, null)) + .compareTo((Date) m.invoke(b, null)); + } else if (isImplementsOf(type, Comparable.class)) { + ret = ((Comparable) m.invoke(a, null)) + .compareTo(m.invoke(b, + null)); + } else { + ret = String.valueOf(m.invoke(a, null)) + .compareTo( + String.valueOf(m + .invoke(b, null))); + } + + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ne) { + ne.printStackTrace(); + } + + if (sort != null && sort.toLowerCase().equals(DESC)) { + return -ret; + } else { + return ret; + } + } + }); + } + } + return list; + } + + /** + * 判断对象实现的所有接口中是否包含szInterface + * + * @param clazz + * @param szInterface + */ + public static boolean isImplementsOf(Class clazz, Class szInterface) { + boolean flag = false; + + Class[] face = clazz.getInterfaces(); + for (Class c : face) { + if (c == szInterface) { + flag = true; + } else { + flag = isImplementsOf(c, szInterface); + } + } + + if (!flag && null != clazz.getSuperclass()) { + return isImplementsOf(clazz.getSuperclass(), szInterface); + } + return flag; + } + + /** + * 根据集合的某个指定字段去重 + * 使用方式: + * list.stream().filter(CollectionUtils.distinctByKey(RoleVO::getId)).collect(Collectors.toList()); + * + * @param keyExtractor 需要去重的字段 + * @param + * @return java.util.function.Predicate + */ + public static Predicate distinctByKey(Function keyExtractor) { + Map seen = new ConcurrentHashMap<>(); + return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/cron/CronUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/cron/CronUtils.java new file mode 100644 index 0000000..2fe2f4d --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/cron/CronUtils.java @@ -0,0 +1,26 @@ +package ai.remi.comm.util.cron; + +import org.springframework.scheduling.support.CronExpression; + +/** + * Quartz Cron 表达式的工具类 + */ +public class CronUtils { + /** + * 校验 CRON 表达式是否有效 + * + * @param cronExpression CRON 表达式 + * @return 是否有效 + */ + public static boolean isValid(String cronExpression) { + return CronExpression.isValidExpression(cronExpression); + } + + public static void main(String[] args) { + String validCron = "0 0 12 * * ?"; + String invalidCron = "invalid"; + + System.out.println("Valid Cron: " + isValid(validCron)); // 输出 true + System.out.println("Invalid Cron: " + isValid(invalidCron)); // 输出 false + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/date/LocalDateTimeUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/date/LocalDateTimeUtils.java new file mode 100644 index 0000000..a5e3d4c --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/date/LocalDateTimeUtils.java @@ -0,0 +1,727 @@ +package ai.remi.comm.util.date; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.TemporalUnit; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + +/** + * 时间处理工具类 + * + * @author ws + * @mail 1720696548@qq.com + * @date 2020/4/24 0024 9:54 + * @return + */ +public class LocalDateTimeUtils { + + /** + * 时间格式 + */ + private static final String YYYY_MM_DD_HH_MM_SS_SSS = "yyyy-MM-dd HH:mm:ss.SSS"; + private static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + private static final String YYYY_MM_DD_HH_MM = "yyyy-MM-dd HH:mm"; + private static final String YYYY_MM_DD_HH = "yyyy-MM-dd HH"; + private static final String YYYY_MM_DD = "yyyy-MM-dd"; + private static final String YYYY_MM = "yyyy-MM"; + private static final String YYYY = "yyyy"; + + /** + * 获取时间类型 + * 1-包含开始和结束时间(默认) + * 2-包含结束-不包含开始时间 // 开始时间+1天 + * 3-包含开始-不包含结束时间 // 结束时间-1天 + * 4-不包含开始和结束时间 // 开始时间+1天 or 结束时间-1天 + */ + private static final int BETWEEN_TYPE_ONE = 1; + private static final int BETWEEN_TYPE_TWO = 2; + private static final int BETWEEN_TYPE_THREE = 3; + private static final int BETWEEN_TYPE_FOUR = 4; + + private static Random random; + + static { + try { + random = SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + /** + * 判断时间 小于 + *

t1 < t2 = true (如:2019-10-13 11:11:00 < 2020-11-13 13:13:00 = true)

+ * + * @author wangsong + */ + public static boolean isBefore(LocalDateTime t1, LocalDateTime t2) { + return t1.isBefore(t2); + } + + /** + * 判断时间 大于 + *

t1 > t2 = true

+ * + * @author wangsong + */ + public static boolean isAfter(LocalDateTime t1, LocalDateTime t2) { + return t1.isAfter(t2); + } + + + /** + * 自构建 LocalDateTime ==> 年,月,日,时,分 + * + * @author wangsong + */ + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute) { + return LocalDateTime.of(year, month, dayOfMonth, hour, minute); + } + + /** + * 自构建 LocalDateTime ==> 年,月,日,时,分,秒,毫秒(精确到9位数) + * + * @author wangsong + */ + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond) { + return LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond); + } + + + //======================================================================================================== + //======================================================================================================== + //======================================================================================================== + //============================================== 时间获取 ================================================= + //======================================================================================================== + //======================================================================================================== + + /** + * 获取指定某一天的开始时间 00:00:00 + * + * @param time + * @return java.time.LocalDateTime + * @author wangsong + * @date 2020/12/24 0024 15:10 + * @version 1.0.1 + */ + public static LocalDateTime getDayStart(LocalDateTime time) { + return time.withHour(0) + .withMinute(0) + .withSecond(0) + .withNano(0); + } + + + /** + * 获取指定某一天的结束时间 23:59:59.999999 + * + * @author wangsong + */ + public static LocalDateTime getDayEnd(LocalDateTime time) { + // 年 月 天 时 分 秒 毫秒(这里精确到6位数) + return time.withHour(23) + .withMinute(59) + .withSecond(59) + .withNano(999999); + } + + /** + * 获取指定时间是周几 1到7 + * + * @author wangsong + */ + public static int week(LocalDateTime time) { + return time.getDayOfWeek().getValue(); + } + + + /** + * 获取指定时间之后的日期 + *

根据field不同加不同值 , field为ChronoUnit.* + * 秒 ChronoUnit.SECONDS + * 分 ChronoUnit.MINUTES + * 时 ChronoUnit.HOURS + * 半天 ChronoUnit.HALF_DAYS + * 天 ChronoUnit.DAYS + * 月 ChronoUnit.MONTHS + * 年 ChronoUnit.YEARS + *

+ * + * @author wangsong + */ + public static LocalDateTime plus(LocalDateTime time, long number, TemporalUnit field) { + return time.plus(number, field); + } + + + /** + * 获取两个日期的时间差 + * + * @param startTime 开始时间 + * @param endTime 计算时间 + * @param field 根据field不同减不同值 , field 为 ChronoUnit.* + * @return startTime小 endTime大 返回正数,则反之 + * @author wangsong + *

+ * 秒 ChronoUnit.SECONDS + * 分 ChronoUnit.MINUTES + * 时 ChronoUnit.HOURS + * 半天 ChronoUnit.HALF_DAYS + * 天 ChronoUnit.DAYS + * 月 ChronoUnit.MONTHS + * 年 ChronoUnit.YEARS + *

+ */ + public static long betweenTwoTime(LocalDateTime startTime, LocalDateTime endTime, ChronoUnit field) { + Period period = Period.between(LocalDate.from(startTime), LocalDate.from(endTime)); + if (field == ChronoUnit.YEARS) { + return period.getYears(); + } + if (field == ChronoUnit.MONTHS) { + return period.getYears() * 12L + period.getMonths(); + } + return field.between(startTime, endTime); + } + + + /** + * 获取指定时间之前的日期 + * + * @author wangsong + *

根据field不同减不同值, field 为 ChronoUnit.* + * 秒 ChronoUnit.SECONDS + * 分 ChronoUnit.MINUTES + * 时 ChronoUnit.HOURS + * 半天 ChronoUnit.HALF_DAYS + * 天 ChronoUnit.DAYS + * 月 ChronoUnit.MONTHS + * 年 ChronoUnit.YEARS + *

+ * @version 1.0.1 + */ + public static LocalDateTime subtract(LocalDateTime time, long number, TemporalUnit field) { + return time.minus(number, field); + } + + + /** + * 获取指定时间 加或减N周的第一天 00:00:00 + * + * @author wangsong + */ + public static LocalDateTime weekFirstDay(LocalDateTime time, int num) { + int week = week(LocalDateTime.now()); + LocalDateTime newTime = subtract(LocalDateTime.now(), week - 1L, ChronoUnit.DAYS); + newTime = plus(newTime, num * 7L, ChronoUnit.DAYS); + return getDayStart(newTime); + } + + + /** + * 获取指定时间 加或减N周的最后一天 23:59:59:999999 + * + * @author wangsong + */ + public static LocalDateTime weekLastDay(LocalDateTime time, int num) { + int week = week(LocalDateTime.now()); + LocalDateTime newTime = plus(LocalDateTime.now(), 7L - week, ChronoUnit.DAYS); + newTime = plus(newTime, num * 7L, ChronoUnit.DAYS); + return getDayEnd(newTime); + } + + + /** + * 获取指定月 加或减N月的第一天 00:00:00 + * + * @author wangsong + */ + public static LocalDateTime monthFirstDay(LocalDateTime time, int num) { + LocalDateTime newTime = plus(time, num, ChronoUnit.MONTHS); + newTime = newTime.with(TemporalAdjusters.firstDayOfMonth()); + return getDayStart(newTime); + } + + /** + * 获取指定月 加或减N月的最后一天 23:59:59:999999 + * + * @author wangsong + */ + public static LocalDateTime monthLastDay(LocalDateTime time, int num) { + LocalDateTime newTime = plus(time, num, ChronoUnit.MONTHS); + newTime = newTime.with(TemporalAdjusters.lastDayOfMonth()); + return getDayEnd(newTime); + } + + + /** + * 获取指定年 加或减N年的第一天 00:00:00 + * + * @author wangsong + */ + public static LocalDateTime yearFirstDay(LocalDateTime time, int num) { + LocalDateTime newTime = plus(time, num, ChronoUnit.YEARS); + int year = newTime.getYear(); + // 年 月 天 时 分 秒 毫秒(这里精确到9位数) + return LocalDateTime.of(year, 1, 1, 0, 0, 0); + } + + /** + * 获取指定年 加或减N年最后一天 23:59:59:999999 + * + * @author wangsong + */ + public static LocalDateTime yearLastDay(LocalDateTime time, int num) { + LocalDateTime newTime = subtract(time, num, ChronoUnit.YEARS); + int year = newTime.getYear(); + // 年 月 天 时 分 秒 毫秒(这里精确到6位数) + return LocalDateTime.of(year, 12, 31, 23, 59, 59, 999999); + } + + + /** + * 获取17位时间戳字符串+3位随机数 + *

这里增加了线程锁和延时一毫秒,单体项目100%不会重复,可用于生成订单号

+ * 20200101125959999 2020-01-01 12:59:59:999 + * + * @return + * @author wangsong + */ + public static synchronized String getNo() { + String timeStamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); + timeStamp += (random.nextInt(10) + "") + (random.nextInt(10) + "") + (random.nextInt(10) + ""); + return timeStamp; + } + + + /** + * 获取整点-- 把指定时间的 分+秒设置为0 + * + * @param time time + * @return java.time.LocalDateTime + * @author wangsong + * @date 2020/12/24 0024 15:10 + * @version 1.0.1 + */ + public static LocalDateTime getTheHour(LocalDateTime time) { + // 分 // 秒 // 毫秒(这里精确到9位数) + return time.withMinute(0) + .withSecond(0) + .withNano(0); + } + + + /** + * 获取整分-- 把指定时间的 秒设置为0 + *

+ * // * 如: + * // * 2020-01-01 12:10 ===> 等于 2020-01-01 12:20 + * // * 2020-01-01 12:11 ===> 等于 2020-01-01 12:20 + * // * 2020-01-01 12:19 ===> 等于 2020-01-01 12:20 + *

+ * + * @param time + * @return java.time.LocalDateTime + * @author wangsong + * @date 2020/12/24 0024 15:21 + * @version 1.0.1 + */ + public static LocalDateTime getTheMinute(LocalDateTime time) { + // 秒 // 毫秒(这里精确到9位数) + return time.withSecond(0).withNano(0); + } + + + //======================================================================================================== + //======================================================================================================== + //======================================================================================================== + //============================================== 转换相关 ================================================= + //======================================================================================================== + //======================================================================================================== + + /** + * LocalDateTime 转为 天 的字符串,如 1号返回 01 + * + * @author wangsong + */ + public static Integer parseDayInt(LocalDateTime time) { + return Integer.parseInt(parse(time, "dd")); + } + + + /** + * Date 转 LocalDateTime + * + * @author wangsong + */ + public static LocalDateTime parseLdt(Date date) { + return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + } + + /** + * LocalDateTime 转 Date + * + * @author wangsong + */ + public static Date parseDate(LocalDateTime time) { + return Date.from(time.atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * LocalDateTime 转 毫秒 + * + * @author wangsong + */ + public static Long parseMillisecond(LocalDateTime time) { + return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + /** + * LocalDateTime 转 秒 + * + * @author wangsong + */ + public static Long parseSecond(LocalDateTime time) { + return time.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond(); + } + + + /** + * 将时间戳转 为 LocalDateTime + * + * @param timestamp + * @return java.lang.String + * @author wangsong + * @date 2021/5/12 0012 17:13 + * @version 1.0.1 + */ + public static LocalDateTime parseTimestamp(Long timestamp) { + return LocalDateTime.ofEpochSecond(timestamp / 1000, 0, ZoneOffset.ofHours(8)); + } + + /** + * 将LocalDateTime 转 为时间戳 + * + * @return java.lang.String + * @author wangsong + * @date 2021/5/12 0012 17:13 + * @version 1.0.1 + */ + public static Long parseTimestamp(LocalDateTime time) { + return time.toEpochSecond(ZoneOffset.ofHours(8)); + } + + + /** + * String 类型转成 LocalDateTime ,必须为完整时间,如:2020-01-20 00:00:00 + * + * @param timeStr 时间字符串 + * @return java.time.LocalDateTime + */ + public static LocalDateTime parse(String timeStr) { + return parse(timeStr, YYYY_MM_DD_HH_MM_SS); + } + + + /** + * String (2020-01-20 00:00:00)类型转成 LocalDateTime + * + * @param timeStr timeStr 时间字符串 + * @param pattern pattern 格式 + * @return java.time.LocalDateTime + */ + public static LocalDateTime parse(String timeStr, String pattern) { + if (pattern.equals(YYYY)) { + timeStr += "-01-01 00:00:00"; + } else if (pattern.equals(YYYY_MM)) { + timeStr += "-01 00:00:00"; + } else if (pattern.equals(YYYY_MM_DD)) { + timeStr += " 00:00:00"; + } else if (pattern.equals(YYYY_MM_DD_HH)) { + timeStr += ":00:00"; + } else if (pattern.equals(YYYY_MM_DD_HH_MM)) { + timeStr += ":00"; + } + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS); + return LocalDateTime.parse(timeStr, dtf); + } + + + /** + * LocalDateTime 转完整 String 类型的时间 如:2020-01-20 00:00:00 + * + * @param time time + * @return java.lang.String + */ + public static String parse(LocalDateTime time) { + return parse(time, YYYY_MM_DD_HH_MM_SS); + } + + /** + * LocalDateTime 转指定类型的字符串 + * + * @param time time 时间 + * @param pattern pattern 格式 + * @return java.lang.String + * @author wangsong + */ + public static String parse(LocalDateTime time, String pattern) { + if (time == null) { + return null; + } + DateTimeFormatter df = DateTimeFormatter.ofPattern(pattern); + return df.format(time); + } + + + /** + * Date 转指定格式的字符串 + * + * @param time + * @author wangsong + */ + public static String parse(Date time, String pattern) { + if (time == null) { + return null; + } + SimpleDateFormat format = new SimpleDateFormat(pattern); + return format.format(time); + } + + + /** + * 获取指定天的24小时(0-23) | yyyy-MM-dd HH 格式 + * + * @param t 开始月 + * @return + */ + public static List getDay24Hour(LocalDateTime t) { + if (t == null) { + return new ArrayList<>(); + } + List times = new ArrayList<>(); + String time = parse(t, YYYY_MM_DD); + int hourNum = 24; + for (int i = 0; i < hourNum; i++) { + if (i < 10) { + times.add(time + " 0" + i); + } else { + times.add(time + " " + i); + } + } + return times; + } + + + /** + * 获取每一天的时间 (指定时间 前n月前的第一天 到 n月后的最后一天的所有时间) + *

一天一条数据 List

+ * + * @param startNum 前n月,当前月开始为 0 + * @param endNum 后n月,当前月就是为 0 + * @return java.util.List + * @author wangsong + */ + public static List getBetweenDaysUpListByMonth(LocalDateTime time, Integer startNum, Integer endNum) { + // 本月第一天 00:00:00 + LocalDateTime startTime = monthFirstDay(time, startNum); + // n月后的最后一天 23:59:59.999 + LocalDateTime endTime = monthLastDay(time, endNum); + return getBetweenDaysUpList(startTime, endTime, BETWEEN_TYPE_ONE); + } + + + /** + * 获取每一天的时间 (指定开始时间和结束时间) + *

+ * 一天一条数据 List + * 返回数据包括 开始时间 和 结束时间 的当天数据 + *

+ * + * @param startTime 开始时间 (时分秒已开始时间位为准) + * @param endTime 结束时间 + * @param type 1-包含开始和结束时间 2-包含结束-不包含开始时间 3-包含开始-不包含结束时间 4-不包含开始和结束时间 + * @return java.util.List + * @author wangsong + */ + public static List getBetweenDaysUpList(LocalDateTime startTime, LocalDateTime endTime, Integer type) { + List dateDaysList = new ArrayList<>(); + List betweenList = getBetweenDaysList(startTime, endTime, type); + for (LocalDateTime localDateTime : betweenList) { + dateDaysList.add(new DateDays(localDateTime, week(localDateTime))); + } + return dateDaysList; + } + + + /** + * 获取指定开始时间到指定结束时间的每一天, 包括开始时间,不包括结束时间,如:2020-5-16到2020-5-18 获得时间为:[2020-5-16,2020-5-17] + * + * @param startTime + * @param endTime + * @param type 1-包含开始和结束时间 2-包含结束-不包含开始时间 3-包含开始-不包含结束时间 4-不包含开始和结束时间 + * @return java.util.List + * @author wangsong + * @date 2020/12/24 0024 15:16 + * @version 1.0.1 + */ + public static List getBetweenDaysList(LocalDateTime startTime, LocalDateTime endTime, Integer type) { + // 指定开始时间 00:00:00 // 指定结束时间 00:00:00 + LocalDateTime oldStartTime = getDayStart(startTime); + LocalDateTime oldEndTime = getDayStart(endTime); + // 1-包含开始和结束时间(默认) BetweenType + // 2-包含结束-不包含开始时间 // 开始时间+1天 + // 3-包含开始-不包含结束时间 // 结束时间-1天 + // 4-不包含开始和结束时间 // 开始时间+1天 or 结束时间-1天 + if (type == BETWEEN_TYPE_TWO) { + oldStartTime = plus(oldStartTime, 1, ChronoUnit.DAYS); + } else if (type == BETWEEN_TYPE_THREE) { + oldEndTime = subtract(endTime, 1, ChronoUnit.DAYS); + } else if (type == BETWEEN_TYPE_FOUR) { + oldStartTime = plus(oldStartTime, 1, ChronoUnit.DAYS); + oldEndTime = subtract(endTime, 1, ChronoUnit.DAYS); + } + // 返回数据 + List everyDays = new ArrayList<>(); + // 第一天数据 + everyDays.add(oldStartTime); + while (true) { + // 获取之后的每一天时间 + LocalDateTime nextDay = plus(everyDays.get(everyDays.size() - 1), 1, ChronoUnit.DAYS); + // 大于最后一天-跳出循环 + if (isAfter(nextDay, oldEndTime)) { + break; + } + everyDays.add(nextDay); + } + return everyDays; + } + + + /** + * 获取月 (返回每一个月的字串, yyyy-MM 格式) + *

包含结束月,不包含开始月 + * + * @param startTime 开始月 + * @param endTime 结束月 + * @return + */ + public static List getBetweenMonthsList(LocalDateTime startTime, LocalDateTime endTime) { + List times = new ArrayList<>(); + if (startTime != null && endTime != null) { + // 获取开始月的第一天 + endTime = monthFirstDay(endTime, 0); + times.add(parse(startTime, YYYY_MM)); + while (isBefore(startTime, endTime)) { + startTime = plus(startTime, 1, ChronoUnit.MONTHS); + times.add(parse(startTime, YYYY_MM)); + } + } + return times; + } + + + /** + * 获取日期端的数据保存对象 + * + * @author ws + * @mail 1720696548@qq.com + * @date 2020/5/7 0007 9:41 + */ + public static class DateDays { + // 当天时间- 年月日/00:00:00 + private LocalDateTime dayTime; + // 当天是周几 + private int week; + + public DateDays(LocalDateTime dayTime, int week) { + this.dayTime = dayTime; + this.week = week; + } + + public LocalDateTime getDayTime() { + return dayTime; + } + + public void setDayTime(LocalDateTime dayTime) { + this.dayTime = dayTime; + } + + public int getWeek() { + return week; + } + + public void setWeek(int week) { + this.week = week; + } + } + + /** + * 计算两个日期之间的天数 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 从开始日期到结束日期(包括这两个日期)之间的总天数 + */ + public static long betweenDays(LocalDate startDate, LocalDate endDate) { + List dates = new ArrayList<>(); + + while (!startDate.isEqual(endDate)) { + dates.add(startDate); + startDate = startDate.plusDays(1); + } + + dates.add(endDate); + + long totalDays = dates.size(); + + return totalDays; + } + + /** + * 计算两个日期之间的工作日数,移除了星期六和星期日 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 两个日期之间的工作日数 + */ + public static long removeWeekends(LocalDate startDate, LocalDate endDate) { + List dates = new ArrayList<>(); + + while (!startDate.isEqual(endDate)) { + dates.add(startDate); + startDate = startDate.plusDays(1); + } + + dates.add(endDate); + + long totalDays = dates.size(); + long weekends = 0; + + for (LocalDate date : dates) { + if (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { + weekends++; + } + } + + return totalDays - weekends; + } + + public static void main(String[] args) { + LocalDate startDate = LocalDate.of(2024, 8, 22); + LocalDate endDate = LocalDate.of(2024, 8, 26); + + long daysBetween = betweenDays(startDate, endDate); + System.out.println("Total days between: " + daysBetween); + + long weekendsRemoved = removeWeekends(startDate, endDate); + System.out.println("Days without weekends: " + weekendsRemoved); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/FileTypeUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/FileTypeUtils.java new file mode 100644 index 0000000..d143560 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/FileTypeUtils.java @@ -0,0 +1,82 @@ +package ai.remi.comm.util.file; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.util.Objects; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc FileTypeUtils + */ +public class FileTypeUtils { + /** + * 获取文件类型 + *

+ * 例如: startdis.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) { + if (null == file) { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: startdis.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) { + strFileExtendName = "GIF"; + } else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) { + strFileExtendName = "JPG"; + } else if ((photoByte[0] == 66) && (photoByte[1] == 77)) { + strFileExtendName = "BMP"; + } else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) { + extension = MediaType.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } + +} \ No newline at end of file diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/FileUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/FileUtils.java new file mode 100644 index 0000000..d23e642 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/FileUtils.java @@ -0,0 +1,293 @@ +package ai.remi.comm.util.file; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc FileUtils + */ +public class FileUtils { + /** + * 字符常量:斜杠 {@code '/'} + */ + public static final char SLASH = '/'; + + /** + * 字符常量:反斜杠 {@code '\\'} + */ + public static final char BACKSLASH = '\\'; + + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException { + FileInputStream fis = null; + try { + File file = new File(filePath); + if (!file.exists()) { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) { + os.write(b, 0, length); + } + } catch (IOException e) { + throw e; + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + if (fis != null) { + try { + fis.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + } + + //路径校验不存在就创建对应的目录 + public static void filePath(String path) { + File file = new File(path); + // 判断路径是否存在,不存在创建 + if (!file.exists()) { + file.mkdirs(); + } + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) { + file.delete(); + flag = true; + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtils.contains(MediaType.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } else if (agent.contains("Firefox")) { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } else if (agent.contains("Chrome")) { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } else { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 返回文件名 + * + * @param filePath 文件 + * @return 文件名 + */ + public static String getName(String filePath) { + if (null == filePath) { + return null; + } + int len = filePath.length(); + if (0 == len) { + return filePath; + } + if (isFileSeparator(filePath.charAt(len - 1))) { + // 以分隔符结尾的去掉结尾分隔符 + len--; + } + + int begin = 0; + char c; + for (int i = len - 1; i > -1; i--) { + c = filePath.charAt(i); + if (isFileSeparator(c)) { + // 查找最后一个路径分隔符(/或者\) + begin = i + 1; + break; + } + } + + return filePath.substring(begin, len); + } + + /** + * 是否为Windows或者Linux(Unix)文件分隔符
+ * Windows平台下分隔符为\,Linux(Unix)为/ + * + * @param c 字符 + * @return 是否为Windows或者Linux(Unix)文件分隔符 + */ + public static boolean isFileSeparator(char c) { + return SLASH == c || BACKSLASH == c; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + * @return + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + public static String formatSize(long fileS) { + DecimalFormat df = new DecimalFormat("#.00"); + String fileSizeString = ""; + String wrongSize = "0B"; + if (fileS == 0) { + return wrongSize; + } + if (fileS < 1024) { + fileSizeString = df.format((double) fileS) + "B"; + } else if (fileS < 1048576) { + fileSizeString = df.format((double) fileS / 1024) + "KB"; + } else if (fileS < 1073741824) { + fileSizeString = df.format((double) fileS / 1048576) + "MB"; + } else { + fileSizeString = df.format((double) fileS / 1073741824) + "GB"; + } + return fileSizeString; + } + + public static BigDecimal formatKB(Long fileSize) { + DecimalFormat df = new DecimalFormat("#.000"); + if (fileSize == 0) { + return new BigDecimal(0.000); + } + return new BigDecimal(fileSize).divide(new BigDecimal(1024)).setScale(3, RoundingMode.HALF_UP); + } + + + /** + * @param url 指定文件的位置 + * @param content 异常输出的内容 + * @throws Exception + */ + + public static void writeFile(String url, String content) throws Exception { + File file = new File(url); + FileWriter fw = null; + + //文件不存在创建新文件 + if (!file.exists()) { + file.createNewFile(); + } else { + deleteFile(url); + file.createNewFile(); + } + String writeDate = content; + try { + fw = new FileWriter(file, true); + fw.write(writeDate + "\r\n"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (fw != null) { + fw.close(); + } + } + + } + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/ImageUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/ImageUtils.java new file mode 100644 index 0000000..a11a91f --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/ImageUtils.java @@ -0,0 +1,67 @@ +package ai.remi.comm.util.file; + +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc ImageUtils + */ +public class ImageUtils { + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) { + InputStream is = getFile(imagePath); + try { + return IOUtils.toByteArray(is); + } catch (Exception e) { + log.error("图片加载异常 {}", e); + return null; + } finally { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) { + try { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } catch (Exception e) { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) { + InputStream in = null; + try { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + return IOUtils.toByteArray(in); + } catch (Exception e) { + log.error("访问文件异常 {}", e); + return null; + } finally { + IOUtils.closeQuietly(in); + } + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/MediaType.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/MediaType.java new file mode 100644 index 0000000..61db4a0 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/file/MediaType.java @@ -0,0 +1,56 @@ +package ai.remi.comm.util.file; + +/** + * 媒体类型工具类 + * + * @author startdis + */ +public class MediaType { + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"}; + + public static final String[] FLASH_EXTENSION = {"swf", "flv"}; + + public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb"}; + + public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"}; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf"}; + + public static String getExtension(String prefix) { + switch (prefix) { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ftp/FtpUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ftp/FtpUtils.java new file mode 100644 index 0000000..9827cc6 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ftp/FtpUtils.java @@ -0,0 +1,148 @@ +package ai.remi.comm.util.ftp; + +import org.apache.commons.net.ftp.FTP; +import org.apache.commons.net.ftp.FTPClient; +import org.apache.commons.net.ftp.FTPFile; +import org.apache.commons.net.ftp.FTPReply; + +import java.io.*; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc FtpUtils + */ +public class FtpUtils { + + /** + * Description: 向FTP服务器上传文件 + * + * @param host FTP服务器hostname + * @param port FTP服务器端口 + * @param username FTP登录账号 + * @param password FTP登录密码 + * @param basePath FTP服务器基础目录 + * @param filePath FTP服务器文件存放路径。例如分日期存放:/2015/01/01。文件的路径为basePath+filePath + * @param filename 上传到FTP服务器上的文件名 + * @param input 输入流 + * @return 成功返回true,否则返回false + */ + public static boolean uploadFile(String host, int port, String username, String password, String basePath, + String filePath, String filename, InputStream input) { + boolean result = false; + FTPClient ftp = new FTPClient(); + try { + int reply; + ftp.connect(host, port);// 连接FTP服务器 + // 如果采用默认端口,可以使用ftp.connect(host)的方式直接连接FTP服务器 + ftp.login(username, password);// 登录 + reply = ftp.getReplyCode(); + if (!FTPReply.isPositiveCompletion(reply)) { + ftp.disconnect(); + return result; + } + // 切换到上传目录 + if (!ftp.changeWorkingDirectory(basePath + filePath)) { + // 如果目录不存在创建目录 + String[] dirs = filePath.split("/"); + String tempPath = basePath; + for (String dir : dirs) { + if (null == dir || "".equals(dir)) { + continue; + } + tempPath += "/" + dir; + if (!ftp.changeWorkingDirectory(tempPath)) { + if (!ftp.makeDirectory(tempPath)) { + return result; + } else { + ftp.changeWorkingDirectory(tempPath); + } + } + } + } + // 设置上传文件的类型为二进制类型 + ftp.setFileType(FTP.BINARY_FILE_TYPE); + // 上传文件 + if (!ftp.storeFile(filename, input)) { + return result; + } + input.close(); + ftp.logout(); + result = true; + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (ftp.isConnected()) { + try { + ftp.disconnect(); + } catch (IOException ioe) { + } + } + } + return result; + } + + /** + * Description: 从FTP服务器下载文件 + * + * @param host FTP服务器hostname + * @param port FTP服务器端口 + * @param username FTP登录账号 + * @param password FTP登录密码 + * @param remotePath FTP服务器上的相对路径 + * @param fileName 要下载的文件名 + * @param localPath 下载后保存到本地的路径 + * @return + */ + public static boolean downloadFile(String host, int port, String username, String password, String remotePath, + String fileName, String localPath) { + boolean result = false; + FTPClient ftp = new FTPClient(); + try { + int reply; + ftp.connect(host, port); + // 如果采用默认端口,可以使用ftp.connect(host)的方式直接连接FTP服务器 + ftp.login(username, password);// 登录 + reply = ftp.getReplyCode(); + if (!FTPReply.isPositiveCompletion(reply)) { + ftp.disconnect(); + return result; + } + ftp.changeWorkingDirectory(remotePath);// 转移到FTP服务器目录 + FTPFile[] fs = ftp.listFiles(); + for (FTPFile ff : fs) { + if (ff.getName().equals(fileName)) { + File localFile = new File(localPath + "/" + ff.getName()); + + OutputStream is = new FileOutputStream(localFile); + ftp.retrieveFile(ff.getName(), is); + is.close(); + } + } + + ftp.logout(); + result = true; + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (ftp.isConnected()) { + try { + ftp.disconnect(); + } catch (IOException ioe) { + } + } + } + return result; + } + + public static void main(String[] args) { + try { + FileInputStream in = new FileInputStream(new File("D:\\temp\\image\\gaigeming.jpg")); + boolean flag = uploadFile("192.168.25.133", 21, "ftpuser", "ftpuser", "/app/startdis/excal", "/2022/07/11", + "gaigeming.jpg", in); + System.out.println(flag); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/hash/HashUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/hash/HashUtils.java new file mode 100644 index 0000000..fdb3f61 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/hash/HashUtils.java @@ -0,0 +1,33 @@ +package ai.remi.comm.util.hash; + +import cn.hutool.core.lang.hash.MurmurHash; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc HashUtils + */ +public class HashUtils { + private static char[] CHARS = new char[]{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' + }; + private static int SIZE = CHARS.length; + + private static String convertDecToBase62(long num) { + StringBuilder sb = new StringBuilder(); + while (num > 0) { + int i = (int) (num % SIZE); + sb.append(CHARS[i]); + num /= SIZE; + } + return sb.reverse().toString(); + } + + public static String hashToBase62(String str) { + int i = MurmurHash.hash32(str); + long num = i < 0 ? Integer.MAX_VALUE - (long) i : i; + return convertDecToBase62(num); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/html/EscapeUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/html/EscapeUtils.java new file mode 100644 index 0000000..49fc885 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/html/EscapeUtils.java @@ -0,0 +1,138 @@ +package ai.remi.comm.util.html; + +import ai.remi.comm.util.string.StringUtils; + +/** + * 转义和反转义工具类 + */ +public class EscapeUtils { + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static { + for (int i = 0; i < 64; i++) { + TEXT[i] = new char[]{(char) i}; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) { + if (StringUtils.isEmpty(text)) { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) { + c = text.charAt(i); + if (c < 256) { + tmp.append("%"); + if (c < 16) { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } else { + tmp.append("%u"); + if (c <= 0xfff) { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) { + if (StringUtils.isEmpty(content)) { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) { + if (content.charAt(pos + 1) == 'u') { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } else { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } else { + if (pos == -1) { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } else { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) { + String html = ""; + String escape = EscapeUtils.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtils.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtils.unescape(escape)); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/html/HTMLFilter.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/html/HTMLFilter.java new file mode 100644 index 0000000..fb34663 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/html/HTMLFilter.java @@ -0,0 +1,495 @@ +package ai.remi.comm.util.html; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + */ +public final class HTMLFilter { + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[]{"img"}; + vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; + vDisallowed = new String[]{}; + vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp. + vProtocolAtts = new String[]{"src", "href"}; + vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; + vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() { + return alwaysMakeTags; + } + + public boolean isStripComments() { + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (!inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) { + if (encodeQuotes) { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } else { + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/CookieUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/CookieUtils.java new file mode 100644 index 0000000..02e646f --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/CookieUtils.java @@ -0,0 +1,98 @@ +package ai.remi.comm.util.http; + +import org.springframework.util.StringUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * cookie操作工具 + */ +public class CookieUtils { + /** + * 按名称获取cookie + * + * @param request + * @param name + * @return + */ + public static Cookie getCookie(String name, HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null || !StringUtils.hasLength(name)) { + return null; + } + + for (Cookie cookie : cookies) { + if (name.equals(cookie.getName())) { + return cookie; + } + } + + return null; + } + + /** + * 按名称获取cookie中的值 + * + * @param request + * @param name + * @return + */ + public static String getCookieValue(String name, HttpServletRequest request) { + Cookie cookie = getCookie(name, request); + return cookie == null ? null : cookie.getValue(); + } + + /** + * 添加cookie + * + * @param name + * @param value + * @param path + * @param request + * @param response + */ + public static void addCookie(String name, String value, String path, HttpServletRequest request, + HttpServletResponse response) { + Cookie cookie = new Cookie(name, value); + if (path != null) { + cookie.setPath(path); + } + if ("https".equals(request.getScheme())) { + cookie.setSecure(true); + } + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + + /** + * 更新cookie中的值 + * + * @param name + * @param value + * @param request + */ + public static void updateCookie(String name, String value, HttpServletRequest request) { + Cookie cookie = CookieUtils.getCookie(name, request); + cookie.setValue(value); + } + + /** + * 清除cookie + * + * @param name + * @param path + * @param response + */ + public static void removeCookie(String name, String path, HttpServletResponse response) { + + Cookie cookie = new Cookie(name, null); + + if (path != null) { + cookie.setPath(path); + } + cookie.setMaxAge(-1000); + response.addCookie(cookie); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/HttpClientUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/HttpClientUtils.java new file mode 100644 index 0000000..b29d704 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/HttpClientUtils.java @@ -0,0 +1,232 @@ +package ai.remi.comm.util.http; + +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc HttpClientUtils + */ +public class HttpClientUtils { + + public static String doGet(String url, Map param) { + + // 创建Httpclient对象 + CloseableHttpClient httpclient = HttpClients.createDefault(); + + String resultString = ""; + CloseableHttpResponse response = null; + try { + // 创建uri + URIBuilder builder = new URIBuilder(url); + if (param != null) { + for (String key : param.keySet()) { + builder.addParameter(key, param.get(key)); + } + } + URI uri = builder.build(); + + // 创建http GET请求 + HttpGet httpGet = new HttpGet(uri); + + // 执行请求 + response = httpclient.execute(httpGet); + // 判断返回状态是否为200 + if (response.getStatusLine().getStatusCode() == 200) { + resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (response != null) { + response.close(); + } + httpclient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return resultString; + } + + public static String doGet(String url) { + return doGet(url, null); + } + + public static String doPost(String url, Map param) { + // 创建Httpclient对象 + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = null; + String resultString = ""; + try { + // 创建Http Post请求 + HttpPost httpPost = new HttpPost(url); + // 创建参数列表 + if (param != null) { + List paramList = new ArrayList<>(); + for (String key : param.keySet()) { + paramList.add(new BasicNameValuePair(key, param.get(key))); + } + // 模拟表单 + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList,"utf-8"); + httpPost.setEntity(entity); + } + // 执行http请求 + response = httpClient.execute(httpPost); + resultString = EntityUtils.toString(response.getEntity(), "utf-8"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + response.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + return resultString; + } + + /** + * Post请求 + * @param url + * @param param + * @return + */ + public static String doPost(String url, Map param, Map headData) { + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = null; + String resultString = ""; + + try { + HttpPost httpPost = new HttpPost(url); + if (param != null) { + List formParams = new ArrayList(); + for (String key : param.keySet()) { + formParams.add(new BasicNameValuePair(key, String.valueOf(param.get(key)))); + } + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams,"UTF-8"); + httpPost.setEntity(entity); + } + //携带head数据 + if (headData != null && headData.size() > 0) { + for (String key : headData.keySet()) { + httpPost.setHeader(key, (String) headData.get(key)); + } + } + + //执行post请求 + response = httpClient.execute(httpPost); + + if (response!=null && response.getStatusLine().getStatusCode() == 200) { + resultString = EntityUtils.toString(response.getEntity(),"UTF-8"); + } + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + }finally { + try { + if (response != null) { + response.close(); + } + httpClient.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + return resultString; + } + + public static String doPost(String url) { + return doPost(url, null); + } + + public static String doPostJson(String url, String json) { + // 创建Httpclient对象 + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = null; + String resultString = ""; + try { + // 创建Http Post请求 + HttpPost httpPost = new HttpPost(url); + // 创建请求内容 + StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON); + httpPost.setEntity(entity); + // 执行http请求 + response = httpClient.execute(httpPost); + resultString = EntityUtils.toString(response.getEntity(), "utf-8"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + response.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + return resultString; + } + + public static String doPostJson(String url, String json, Map headData) { + // 创建Httpclient对象 + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = null; + String resultString = ""; + try { + // 创建Http Post请求 + HttpPost httpPost = new HttpPost(url); + // 创建请求内容 + StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON); + httpPost.setEntity(entity); + //携带head数据 + if (headData != null && headData.size() > 0) { + for (String key : headData.keySet()) { + httpPost.setHeader(key, (String) headData.get(key)); + } + } + // 执行http请求 + response = httpClient.execute(httpPost); + resultString = EntityUtils.toString(response.getEntity(), "utf-8"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + response.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + return resultString; + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/ServletUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/ServletUtils.java new file mode 100644 index 0000000..036e04e --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/ServletUtils.java @@ -0,0 +1,394 @@ +package ai.remi.comm.util.http; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import ai.remi.comm.core.constant.Constants; +import ai.remi.comm.core.constant.TokenConstants; +import ai.remi.comm.core.result.ResultBean; +import ai.remi.comm.util.string.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 客户端工具类 + */ +@Slf4j +public class ServletUtils { + /** + * 获取请求token + */ + public static String getToken(ServerHttpRequest request) { + String token = request.getHeaders().getFirst(TokenConstants.SUPPLY_AUTHORIZATION); + if (StrUtil.isBlank(token)) { + token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION); + } + return getToken(token); + } + + /** + * 获取请求token + */ + public static String getToken(String token) { + if (StrUtil.isNotEmpty(token) && StrUtil.startWith(token, TokenConstants.PREFIX)) { + token = StringUtils.replaceFirst(token, TokenConstants.PREFIX, StrUtil.EMPTY); + } + return token; + } + + /** + * 鉴权失败处理 + */ + public static Mono unauthorizedResponse(ServerWebExchange exchange, String msg) { + log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath()); + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, 401); + } + + /** + * 增加请求头 + */ + public static void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) { + if (value == null) { + return; + } + String valueStr = value.toString(); + String valueEncode = ServletUtils.urlEncode(valueStr); + mutate.header(name, valueEncode); + } + + /** + * 移除请求头 + */ + public static void removeHeader(ServerHttpRequest.Builder mutate, String name) { + mutate.headers(httpHeaders -> httpHeaders.remove(name)).build(); + } + + /** + * 获取参数Map + */ + public static MultiValueMap getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap parameters = new LinkedMultiValueMap<>(parameterMap.size()); + parameterMap.forEach((key, values) -> { + for (String value : values) { + parameters.add(key, value); + } + }); + return parameters; + } + + /** + * 获取String参数 + */ + public static String getParameter(String name) { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) { + params.put(entry.getKey(), ArrayUtil.join(entry.getValue(), StrUtil.COMMA)); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() { + try { + return getRequestAttributes().getRequest(); + } catch (Exception e) { + return null; + } + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() { + try { + return getRequestAttributes().getResponse(); + } catch (Exception e) { + return null; + } + } + + /** + * 获取session + */ + public static HttpSession getSession() { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() { + try { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } catch (Exception e) { + return null; + } + } + + public static String getHeader(HttpServletRequest request, String name) { + String value = request.getHeader(name); + if (StringUtils.isEmpty(value)) { + return StringUtils.EMPTY; + } + return urlDecode(value); + } + + public static Map getHeaders(HttpServletRequest request) { + Map map = new LinkedCaseInsensitiveMap<>(); + Enumeration enumeration = request.getHeaderNames(); + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + String key = enumeration.nextElement(); + String value = request.getHeader(key); + map.put(key, value); + } + } + return map; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) { + try { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) { + try { + return URLEncoder.encode(str, Constants.UTF8); + } catch (UnsupportedEncodingException e) { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) { + try { + return URLDecoder.decode(str, Constants.UTF8); + } catch (UnsupportedEncodingException e) { + return StringUtils.EMPTY; + } + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value) { + return webFluxResponseWriter(response, HttpStatus.OK, value, 500); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, Object value, int code) { + return webFluxResponseWriter(response, HttpStatus.OK, value, code); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param status http状态码 + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code) { + return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code); + } + + /** + * 设置webflux模型响应 + * + * @param response ServerHttpResponse + * @param contentType content-type + * @param status http状态码 + * @param code 响应状态码 + * @param value 响应内容 + * @return Mono + */ + public static Mono webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code) { + response.setStatusCode(status); + response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType); + ResultBean result = ResultBean.error(String.valueOf(code), value.toString()); + DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes()); + return response.writeWith(Mono.just(dataBuffer)); + } + + /** + * 设置servlet响应 + * + * @param response HttpServletResponse + * @param value 响应内容 + */ + public static void webResponseWriter(HttpServletResponse response, Object value) { + webResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, value); + } + + /** + * 设置servlet响应 + * + * @param response HttpServletResponse + * @param contentType content-type + * @param value 响应内容 + */ + public static void webResponseWriter(HttpServletResponse response, String contentType, Object value) { + response.setContentType(contentType); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + Writer writer = null; + try { + writer = response.getWriter(); + writer.write(JSON.toJSONString(value)); + writer.flush(); + } catch (IOException var8) { + throw new UtilException(var8); + } finally { + IoUtil.close(writer); + } + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/UrlUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/UrlUtils.java new file mode 100644 index 0000000..97217cc --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/http/UrlUtils.java @@ -0,0 +1,27 @@ +package ai.remi.comm.util.http; + +/** + * url工具类 + */ +public class UrlUtils { + + /** + * @return {@code true} if the url is an http: url. + */ + public static boolean isHttpUrl(String url) { + return (null != url) + && (url.length() > 6) + && url.substring(0, 7).equalsIgnoreCase("http://"); + } + + /** + * @return {@code true} if the url is an https: url. + */ + public static boolean isHttpsUrl(String url) { + return (null != url) + && (url.length() > 7) + && url.substring(0, 8).equalsIgnoreCase("https://"); + } + + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/RandomUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/RandomUtils.java new file mode 100644 index 0000000..597a4ad --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/RandomUtils.java @@ -0,0 +1,240 @@ +package ai.remi.comm.util.id; + + +import java.util.Random; +import java.math.BigDecimal; + +/** + * 随机方法工具类 + * + * @author dianjiu + */ +public class RandomUtils { + static Random random = new Random(); + static String GREATER_THAN = "x must be greater than or equal 0"; + static String Y_GREATER_THAN_X = "y must be greater than x"; + private static final int DISPLAYABLE_MIN_INDEX = 33; + private static final int DISPLAYABLE_MAX_INDEX = 127 - 1; + private static final String ALL_CHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String LETTER_CHAR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String NUMBER_CHAR = "0123456789"; + private static final ThreadLocal local = new ThreadLocal() { + @Override + protected Random initialValue() { + return new Random(); + } + }; + + + /** + * 获取定长的随机数,包含大小写、数字 + * + * @param length 随机数长度 + * @return 返回字符串 + */ + public static String generateString(int length) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(ALL_CHAR.charAt(random.nextInt(ALL_CHAR.length()))); + } + return sb.toString(); + } + + /** + * 获取定长的随机数,包含大小写字母 + * + * @param length 随机数长度 + * @return 返回字符串 + */ + public static String generateMixString(int length) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(LETTER_CHAR.charAt(random.nextInt(LETTER_CHAR.length()))); + } + return sb.toString(); + } + + /** + * 获取定长的随机数,只包含小写字母 + * + * @param length 随机数长度 + * @return 返回字符串 + */ + public static String generateLowerString(int length) { + return generateMixString(length).toLowerCase(); + } + + /** + * 获取定长的随机数,只包含大写字母 + * + * @param length 随机数长度 + * @return 返回字符串 + */ + public static String generateUpperString(int length) { + return generateMixString(length).toUpperCase(); + } + + /** + * 获取定长的随机数,只包含数字 + * + * @param length 随机数长度 + * @return 返回字符串 + */ + public static String generateNumberString(int length) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(NUMBER_CHAR.charAt(random.nextInt(NUMBER_CHAR.length()))); + } + return sb.toString(); + } + + /** + * 获取Random实例 + * + * @return 返回随机值 + */ + public static Random getRandom() { + return local.get(); + } + + /** + * 产生[0, x]区间的随机数 + * + * @param x > 0 + * @return + */ + public static int intSeed(int x) { + return intSeed(0, x); + } + + /** + * 产生[x, y]区间的随机数 + * + * @param x >= 0 + * @param y > x + * @return 返回int数 + */ + public static int intSeed(int x, int y) { + if (x < 0) { // ensure x >= 0 + throw new IllegalArgumentException( + Y_GREATER_THAN_X); + } + if (x >= y) { // ensure y > x + throw new IllegalArgumentException(""); + } + return x + getRandom().nextInt(y - x + 1); + } + + /** + * 产生[0, x]区间的随机数 + * + * @param x> 0 + * @return 返回长整型 + */ + public static long longSeed(long x) { + return longSeed(0, x); + } + + /** + * 产生[x, y]区间的随机数 + * + * @param x >= 0 + * @param y > x + * @return 返回长整型 + */ + public static long longSeed(long x, long y) { + if (x < 0) { // ensure x >= 0 + throw new IllegalArgumentException( + GREATER_THAN); + } + if (x >= y) { // ensure y > x + throw new IllegalArgumentException(Y_GREATER_THAN_X); + } + return Math.abs(getRandom().nextLong() % (y - x + 1)) + x; + } + + /** + * 产生[0, x]区间的随机小数 + * + * @param x > 0 + * @return 返回浮点数 + */ + public static float floatSeed(float x) { + return (float) doubleSeed(0., x); + } + + /** + * 产生[x, y]区间的随机小数 + * + * @param x >= 0 + * @param y > x + * @return 返回浮点数 + */ + public static float floatSeed(float x, float y) { + return (float) doubleSeed(x, y); + } + + /** + * 产生[0, x]区间的随机小数 + * + * @param x > 0 + * @return 返回double类型 + */ + public static double doubleSeed(double x) { + return doubleSeed(0., x); + } + + /** + * 产生[x, y]区间的随机小数 + * + * @param x >= 0 + * @param y > x + * @return 返回double类型 + */ + public static double doubleSeed(double x, double y) { + if (x < 0) { // ensure x >= 0 + throw new IllegalArgumentException( + "GREATER_THAN"); + } + if (x >= y) { // ensure y > x + throw new IllegalArgumentException("Y_GREATER_THAN_X"); + } + BigDecimal dx = new BigDecimal(String.valueOf(x)); + BigDecimal dy = new BigDecimal(String.valueOf(y)); + double diff = dy.subtract(dx).doubleValue(); + dx = dx.add(new BigDecimal(String.valueOf(getRandom().nextDouble() + * diff))); + return dx.doubleValue(); + } + + /** + * 随机产生true或false + * + * @return 返回布尔类型 + */ + public static boolean boolSeed() { + return intSeed(0, 1) == 0 ? false : true; + } + + /** + * 随机产生有效的可显示的字符 + * + * @param x >= 33 + * @param y > x & <= 126 + * @return 返回char + */ + public static char charSeed(char x, char y) { + if (x < DISPLAYABLE_MIN_INDEX) { // ensure x >= 33 ('!') + throw new IllegalArgumentException( + "x must be greater than or equal '!'"); + } + if (x >= y) { // ensure y > x + throw new IllegalArgumentException("Y_GREATER_THAN_X"); + } + if (y > DISPLAYABLE_MAX_INDEX) { // ensure y <= 126 ('~') + throw new IllegalArgumentException( + "y must be less than or equal '~'"); + } + return (char) (getRandom().nextInt(y - x + 1) + x); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/SequenceUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/SequenceUtils.java new file mode 100644 index 0000000..84be2f0 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/SequenceUtils.java @@ -0,0 +1,121 @@ +package ai.remi.comm.util.id; + +import ai.remi.comm.util.ip.MacAddressUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +public class SequenceUtils { + + private static final Logger log = LoggerFactory.getLogger(SequenceUtils.class); + + + public static String OLD_DATETIME = ""; + /** + * 静态属性 + */ + public static int STATIC_SEQUENCE = 0; + + /** + * 默认节点 + */ + public static String DEFAULT_NODE_NUMBER = "01"; + + public static String STATIC_NODE_NUMBER = "--"; + + /** + * 生成20位的流水号 + * + * @return 流水号 + */ + public static synchronized String next() { + String currentDateTime = getCurrentSystemDateTime(); + + if (null == currentDateTime) { + log.error("获取系统日期失败"); + return null; + } + + StringBuffer sequenceNumber = new StringBuffer(); + + sequenceNumber.append(currentDateTime.substring(0, 8)); + sequenceNumber.append(getNodeNumber()); + sequenceNumber.append(currentDateTime.substring(8)); + sequenceNumber.append(nextSequence()); + return sequenceNumber.toString(); + } + + public static final String initNodeNumber(String nodeNumbers) { + if (STATIC_NODE_NUMBER.equals("--")) { + if (null != nodeNumbers && !nodeNumbers.equals("")) { + + String ipAddressConfigValue = nodeNumbers; + log.info("ARE config.node.number : " + ipAddressConfigValue); + if (ipAddressConfigValue.indexOf(",") > -1) { + + String hostIpAddress = MacAddressUtils.getAllHostMacAddress();//获得本机IP + + log.info("hostIpAddress : " + hostIpAddress); + + String[] ipAddressValues = ipAddressConfigValue.split(","); + for (String ipvalue : ipAddressValues) { + String[] ipNode = ipvalue.split("="); + if (ipNode != null && ipNode.length > 0 && hostIpAddress.indexOf(ipNode[0]) > -1) { + STATIC_NODE_NUMBER = ipNode[1]; + } + } + + if (STATIC_NODE_NUMBER.equals("--")) { + log.error("GET MAC BIND NODE ERROR . "); + STATIC_NODE_NUMBER = DEFAULT_NODE_NUMBER; + } + + } else { + STATIC_NODE_NUMBER = nodeNumbers; + } + log.info("STATIC_NODE_SEQUENCE_NUMBER : " + STATIC_NODE_NUMBER); + if (STATIC_NODE_NUMBER.length() != 2) { + log.error("系统节点号必须2位"); + } + } else { + STATIC_NODE_NUMBER = DEFAULT_NODE_NUMBER; + } + } + return STATIC_NODE_NUMBER; + } + + public static final String getNodeNumber() { + return STATIC_NODE_NUMBER; + } + + /** + * 同一时刻只有一个访问 + * + * @return + */ + private static final synchronized String nextSequence() { + STATIC_SEQUENCE = (STATIC_SEQUENCE + 1) % 10000; + return String.format("%04d", STATIC_SEQUENCE); + } + + /** + * 获取系统当前日期,格式为yyyyMMddHHmmSS + * + * @return 当前系统日期 + */ + private static synchronized String getCurrentSystemDateTime() { + String currentdatetime = null; + synchronized (OLD_DATETIME) { + currentdatetime = (new java.text.SimpleDateFormat("yyyyMMddHHmmss")).format(new Date()); + /** + * 判断是否是新的时间,如果是新时间则STATIC_SEQUENCE从0开始计数 + */ + if (!currentdatetime.equals(OLD_DATETIME)) { + STATIC_SEQUENCE = 0; + OLD_DATETIME = currentdatetime; + } + } + return currentdatetime; + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/SnowflakeUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/SnowflakeUtils.java new file mode 100644 index 0000000..ecee483 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/SnowflakeUtils.java @@ -0,0 +1,194 @@ +package ai.remi.comm.util.id; + +import ai.remi.comm.util.hash.HashUtils; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc SnowflakeIDUtils + */ +public class SnowflakeUtils { + private static final SnowflakeUtils intance = new SnowflakeUtils(1, 1, 1); + + //因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。 + //下面两个每个5位,加起来就是10位的工作机器id + /** + * 机器ID 2进制5位 32位减掉1位 31个 + */ + private long workerId; + /** + * //机房ID 2进制5位 32位减掉1位 31个 + */ + private long datacenterId; + /** + * 12位的序列号,代表一毫秒内生成的多个id的最新序号 4096 -1 = 4095 个 + */ + private long sequence; + /** + * 设置一个时间初始值 2^41 - 1 差不多可以用69年 + */ + private long twepoch = 1888888888L; + /** + * 5位的机器id + */ + private long workerIdBits = 5L; + /** + * 5位的机房id + */ + private long datacenterIdBits = 5L; + /** + * 每毫秒内产生的id数 2 的 12次方 + */ + private long sequenceBits = 12L; + /** + * 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内 + */ + private long maxWorkerId = -1L ^ (-1L << workerIdBits); + /** + * 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内 + */ + private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + + private long workerIdShift = sequenceBits; + private long datacenterIdShift = sequenceBits + workerIdBits; + private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + private long sequenceMask = -1L ^ (-1L << sequenceBits); + /** + * 记录产生时间毫秒数,判断是否是同1毫秒 + */ + private long lastTimestamp = -1L; + + public long getWorkerId() { + return workerId; + } + + public long getDatacenterId() { + return datacenterId; + } + + public long getTimestamp() { + return System.currentTimeMillis(); + } + + + public static SnowflakeUtils getInstance() { + return intance; + } + + public static SnowflakeUtils getInstance(long workerId, long datacenterId) { + return new SnowflakeUtils(workerId, datacenterId, 1); + } + + /** + * 构造方法 + * + * @param workerId + * @param datacenterId + * @param sequence + */ + public SnowflakeUtils(long workerId, long datacenterId, long sequence) { + + // 检查机房id和机器id是否超过31 不能小于0 + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException( + String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + + if (datacenterId > maxDatacenterId || datacenterId < 0) { + + throw new IllegalArgumentException( + String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + this.sequence = sequence; + } + + public synchronized String nextShortId() { + return HashUtils.hashToBase62(this.nextIdStr()); + } + + public synchronized String nextIdStr() { + return Long.toString(this.nextId()); + } + + /** + * 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id + * + * @return + */ + public synchronized long nextId() { + // 这儿就是获取当前时间戳,单位是毫秒 + long timestamp = timeGen(); + if (timestamp < lastTimestamp) { + + System.err.printf( + "clock is moving backwards. Rejecting requests until %d.", lastTimestamp); + throw new RuntimeException( + String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", + lastTimestamp - timestamp)); + } + + // 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id + // 这个时候就得把seqence序号给递增1,最多就是4096 + if (lastTimestamp == timestamp) { + + // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来, + //这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围 + sequence = (sequence + 1) & sequenceMask; + //当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); + } + + } else { + sequence = 0; + } + // 这儿记录一下最近一次生成id的时间戳,单位是毫秒 + lastTimestamp = timestamp; + // 这儿就是最核心的二进制位运算操作,生成一个64bit的id + // 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit + // 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型 + return ((timestamp - twepoch) << timestampLeftShift) | + (datacenterId << datacenterIdShift) | + (workerId << workerIdShift) | sequence; + } + + /** + * 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID + * + * @param lastTimestamp + * @return + */ + private long tilNextMillis(long lastTimestamp) { + + long timestamp = timeGen(); + + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + //获取当前时间戳 + private long timeGen() { + return System.currentTimeMillis(); + } + + /** + * main 测试类 + * + * @param args + */ + public static void main(String[] args) { + //SnowflakeIDUtils snowflakeIDUtils = new SnowflakeIDUtils(1, 1, 1); + //SnowflakeIDUtils instance = getInstance(1, 1); + System.out.println(getInstance().nextId()); + System.out.println(getInstance(2,1).nextId()); + //for (int i = 0; i < 9; i++) { + // System.out.println(getInstance().nextId()); + // //System.out.println(instance.nextShortId()); + // //System.out.println(snowflakeIDUtils.nextId()); + //} + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/TracerUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/TracerUtils.java new file mode 100644 index 0000000..69e49cc --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/TracerUtils.java @@ -0,0 +1,26 @@ +package ai.remi.comm.util.id; + +import org.apache.skywalking.apm.toolkit.trace.TraceContext; + +/** + * 链路追踪工具类 + */ +public class TracerUtils { + + /** + * 私有化构造方法 + */ + private TracerUtils() { + } + + /** + * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 + * 如果不存在的话为空字符串!!! + * + * @return 链路追踪编号 + */ + public static String getTraceId() { + return TraceContext.traceId(); + } + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/UUIDUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/UUIDUtils.java new file mode 100644 index 0000000..d2c8cce --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/id/UUIDUtils.java @@ -0,0 +1,72 @@ +package ai.remi.comm.util.id; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc UUIDUtils + */ +public class UUIDUtils { + + /** + * All possible chars for representing a number as a String + * copy from mica:https://github.com/lets-mica/mica/blob/master/mica-core/src/main/java/net/dreamlu/mica/core/utils/NumberUtil.java#L113 + */ + private final static byte[] DIGITS = { + '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z' + }; + + /** + * 生成不带有-的UUID字符串 + * + * @return 不带有-的UUID字符串 + */ + public static String simpleUuid() { + return uuid().replaceAll("-", ""); + } + + /** + * 生成带有-的UUID字符串 + * + * @return 带有-的UUID字符串 + */ + public static String uuid() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + long lsb = random.nextLong(); + long msb = random.nextLong(); + byte[] buf = new byte[32]; + formatUnsignedLong(lsb, buf, 20, 12); + formatUnsignedLong(lsb >>> 48, buf, 16, 4); + formatUnsignedLong(msb, buf, 12, 4); + formatUnsignedLong(msb >>> 16, buf, 8, 4); + formatUnsignedLong(msb >>> 32, buf, 0, 8); + return new String(buf, StandardCharsets.UTF_8); + } + + /** + * copy from mica:https://github.com/lets-mica/mica/blob/master/mica-core/src/main/java/net/dreamlu/mica/core/utils/StringUtil.java#L348 + */ + private static void formatUnsignedLong(long val, byte[] buf, int offset, int len) { + int charPos = offset + len; + int radix = 1 << 4; + int mask = radix - 1; + do { + buf[--charPos] = DIGITS[((int) val) & mask]; + val >>>= 4; + } while (charPos > offset); + } + +} + diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/IpAddrUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/IpAddrUtils.java new file mode 100644 index 0000000..7fcfd7b --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/IpAddrUtils.java @@ -0,0 +1,240 @@ +package ai.remi.comm.util.ip; + +import ai.remi.comm.util.string.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc IpUtils IP地址工具类 + */ +public class IpAddrUtils { + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + if (request == null) { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) { + if (StringUtils.isNull(addr) || addr.length < 2) { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) { + return true; + } + case SECTION_5: + switch (b1) { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) { + if (text.length() == 0) { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try { + long l; + int i; + switch (elements.length) { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } catch (NumberFormatException e) { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) { + if (false == isUnknown(subIp)) { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 验证IP字符串是否合规 + * + * @param ip ip字符串 + * @return true标识合规 + */ + public static boolean validIp(String ip) { + String pattern = "((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}"; + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(ip); + boolean matches = m.matches(); + return matches; + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/IpInfoUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/IpInfoUtils.java new file mode 100644 index 0000000..49b6879 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/IpInfoUtils.java @@ -0,0 +1,127 @@ +package ai.remi.comm.util.ip; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc IpUtils IP归属地工具类 + */ +@Slf4j +public class IpInfoUtils { + + private static Searcher searcher; + + static { + try (InputStream inputStream = IpInfoUtils.class.getClassLoader().getResourceAsStream("ip2region.xdb")) { + if (inputStream == null) { + throw new IOException("ip2region.xdb loading failed."); + } + byte[] buffer = new byte[8192]; + int bytesRead; + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + while ((bytesRead = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + } + searcher = Searcher.newWithBuffer(output.toByteArray()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 获取 ip 对应的信息 + * @param ip + * @return 格式: 中国|0|浙江|杭州|电信 + */ + public static IpInfo getInfo(String ip) { + + IpInfo ipInfo = new IpInfo(ip); + + if (!IpAddrUtils.validIp(ip)) { + log.warn("========非法 IP 格式========"); + return ipInfo; + } + try { + convert(searcher.search(ip),ipInfo); + } catch (Exception e) { + throw new RuntimeException(e); + } + return ipInfo; + } + + private static IpInfo convert(String regionStr,IpInfo ipInfo) { + if (regionStr == null || regionStr.length() == 0) { + return null; + } + String[] regionSplit = regionStr.split("\\|"); + if (regionSplit.length != 5) { + return null; + } + ipInfo.setCountry("0".equals(regionSplit[0]) ? null : regionSplit[0]); + ipInfo.setRegion("0".equals(regionSplit[1]) ? null : regionSplit[1]); + ipInfo.setProvince("0".equals(regionSplit[2]) ? null : regionSplit[2]); + ipInfo.setCity("0".equals(regionSplit[3]) ? null : regionSplit[3]); + ipInfo.setIsp("0".equals(regionSplit[4]) ? null : regionSplit[4]); + return ipInfo; + } + + @Setter + @Getter + @Accessors(chain = true) + @ToString + public static class IpInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 国家 + */ + private String country = "未知"; + + /** + * 区域 + */ + private String region = "未知"; + + /** + * 省份 + */ + private String province = "未知"; + + /** + * 城市 + */ + private String city = "未知"; + + /** + * 运营商 + */ + private String isp = "未知"; + + /** + * IP地址 + */ + private String ip; + + public IpInfo(String ip) { + this.ip = ip; + } + } + + public static void main(String[] args) { + IpInfo info = getInfo("14.29.139.251"); + System.out.println(info.toString()); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/MacAddressUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/MacAddressUtils.java new file mode 100644 index 0000000..d26209b --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/ip/MacAddressUtils.java @@ -0,0 +1,73 @@ +package ai.remi.comm.util.ip; + +import ai.remi.comm.util.id.SequenceUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; +import java.util.Properties; + +public class MacAddressUtils { + + private static final Logger log = LoggerFactory.getLogger(SequenceUtils.class); + + public static String os; + + static { + Properties prop = System.getProperties(); + os = prop.getProperty("os.name"); + log.info("OS : " + os); + } + + public static String getAllHostMacAddress() { + String hostIpAddress = ""; + try { + Enumeration netInterfaces = NetworkInterface.getNetworkInterfaces(); + InetAddress inetAddress = null; + while (netInterfaces.hasMoreElements()) { + NetworkInterface ni = (NetworkInterface) netInterfaces.nextElement(); + if (ni.getInetAddresses().hasMoreElements()) { + inetAddress = (InetAddress) ni.getInetAddresses().nextElement(); + if (!inetAddress.isLoopbackAddress()) { + hostIpAddress += getMac(inetAddress) + ","; + log.info("host MAC : " + getMac(inetAddress)); + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + return hostIpAddress; + } + + + public static String getMac(InetAddress ia) throws SocketException { + //获取网卡,获取地址 + byte[] mac = NetworkInterface.getByInetAddress(ia).getHardwareAddress(); + //LoggerFactory.getLogger(MacAddress.class).info("mac数组长度:"+mac.length); + + + StringBuffer sb = new StringBuffer(""); + for (int i = 0; i < mac.length; i++) { + if (i != 0) { + if (os.startsWith("win") || os.startsWith("Win")) { + sb.append("-");//win + } else { + sb.append(":");//linux + } + } + //字节转换为整数 + int temp = mac[i] & 0xff; + String str = Integer.toHexString(temp); + if (str.length() == 1) { + sb.append("0" + str); + } else { + sb.append(str); + } + } + return sb.toString().toUpperCase(); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/merge/MergeUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/merge/MergeUtils.java new file mode 100644 index 0000000..419834b --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/merge/MergeUtils.java @@ -0,0 +1,23 @@ +package ai.remi.comm.util.merge; + +import java.util.List; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc MergeUtils + */ +public class MergeUtils { + /** + * + * @param listA 需要合并的A对象集合 + * @param listB 需要合并的B对象集合 + * @param compare 对比A和B对象,如果true 就执行 merge方法 + * @param merge 合并A和B对象,需要用户自己实现 + * @param 返回值的对象 + * @param 被合并的对象 + */ + public static void merge(List listA, List listB, Merged.Compare compare, Merged merge){ + merge.apply(listA,listB,compare); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/merge/Merged.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/merge/Merged.java new file mode 100644 index 0000000..a2bcfad --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/merge/Merged.java @@ -0,0 +1,27 @@ +package ai.remi.comm.util.merge; + + +import java.util.List; + +/** + * + * @param 返回值的对象类型 + * @param 需要被合并的类型 + */ +@FunctionalInterface +public interface Merged { + + void merge(R r, T t); + + default void apply(List listA, List listB,Compare compare){ + listA.forEach(a-> listB.forEach(b -> { + if(compare.compare(a,b)){ + merge(a,b); + } + })); + } + @FunctionalInterface + interface Compare{ + boolean compare(R r,T t); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/money/MoneyUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/money/MoneyUtils.java new file mode 100644 index 0000000..ec5fce3 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/money/MoneyUtils.java @@ -0,0 +1,266 @@ +package ai.remi.comm.util.money; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金钱处理工具类 + * + * @author dianjiu + */ +public class MoneyUtils { + /** + * 汉语中数字大写 + */ + private static final String[] CN_UPPER_NUMBER = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}; + + /** + * 汉语中货币单位大写 + */ + private static final String[] CN_UPPER_MONETRAY_UNIT = {"分", "角", "元", "拾", "佰", "仟", "万", "拾", + "佰", "仟", "亿", "拾", "佰", "仟", "兆", "拾", + "佰", "仟"}; + /** + * 特殊字符:整 + */ + private static final String CN_FULL = ""; + + /** + * 特殊字符:负 + */ + private static final String CN_NEGATIVE = "负"; + /** + * 零元整 + */ + private static final String CN_ZEOR_FULL = "零元整"; + + /** + * 金额的精度,默认值为2 + */ + private static final int MONEY_PRECISION = 2; + + /** + * 人民币转换为大写,格式为:x万x千x百x十x元x角x分 + * + * @param numberOfMoney 传入的金额 + * @return 字符串 + */ + public static String number2CNMontray(String numberOfMoney) { + return number2CNMontray(new BigDecimal(numberOfMoney)); + } + + + /** + * 人民币转换为大写,格式为:x万x千x百x十x元x角x分 + * + * @param numberOfMoney 传入的金额 + * @return 字符串 + */ + public static String number2CNMontray(BigDecimal numberOfMoney) { + StringBuilder sb = new StringBuilder(); + int signum = numberOfMoney.signum(); + if (signum == 0) { + return CN_ZEOR_FULL; + } + long number = numberOfMoney.movePointRight(MONEY_PRECISION).setScale(0, 4).abs().longValue(); + long scale = number % 100; + int numUnit = 0; + int numIndex = 0; + boolean getZero = false; + // 判断最后两位数,一共有四中情况:00 = 0, 01 = 1, 10, 11 + if (!(scale > 0)) { + numIndex = 2; + number = number / 100; + getZero = true; + } + if ((scale > 0) && (!(scale % 10 > 0))) { + numIndex = 1; + number = number / 10; + getZero = true; + } + int zeroSize = 0; + while (true) { + if (number <= 0) { + break; + } + // 每次获取到最后一个数 + numUnit = (int) (number % 10); + if (numUnit > 0) { + if ((numIndex == 9) && (zeroSize >= 3)) { + sb.insert(0, CN_UPPER_MONETRAY_UNIT[6]); + } + if ((numIndex == 13) && (zeroSize >= 3)) { + sb.insert(0, CN_UPPER_MONETRAY_UNIT[10]); + } + sb.insert(0, CN_UPPER_MONETRAY_UNIT[numIndex]); + sb.insert(0, CN_UPPER_NUMBER[numUnit]); + getZero = false; + zeroSize = 0; + } else { + ++zeroSize; + if (!(getZero)) { + sb.insert(0, CN_UPPER_NUMBER[numUnit]); + } + if (numIndex == 2) { + if (number > 0) { + sb.insert(0, CN_UPPER_MONETRAY_UNIT[numIndex]); + } + } else if (((numIndex - 2) % 4 == 0) && (number % 1000 > 0)) { + sb.insert(0, CN_UPPER_MONETRAY_UNIT[numIndex]); + } + getZero = true; + } + // 让number每次都去掉最后一个数 + number = number / 10; + ++numIndex; + } + // 如果signum == -1,则说明输入的数字为负数,就在最前面追加特殊字符:负 + if (signum == -1) { + sb.insert(0, CN_NEGATIVE); + } + // 输入的数字小数点后两位为"00"的情况,则要在最后追加特殊字符:整 + if (!(scale > 0)) { + sb.append(CN_FULL); + } + return sb.toString(); + } + + /** + * 将人民币转换为会计格式金额(xxxx,xxxx,xxxx.xx),保留两位小数 + * + * @param money 待转换的金额 + * @return 字符串 + */ + public static String accountantMoney(BigDecimal money) { + return accountantMoney(money, 2, 1); + } + + /** + * 格式化金额,显示为xxx万元,xxx百万,xxx亿 + * + * @param money 待处理的金额 + * @param scale 小数点后保留的位数 + * @param divisor 格式化值(10:十元、100:百元,1000千元,10000万元......) + * @return 字符串 + */ + public static String getFormatMoney(BigDecimal money, int scale, double divisor) { + return formatMoney(money, scale, divisor) + getCellFormat(divisor); + } + + /** + * 获取会计格式的人民币(格式为:xxxx,xxxx,xxxx.xx) + * + * @param money 待处理的金额 + * @param scale 小数点后保留的位数 + * @param divisor 格式化值(10:十元、100:百元,1000千元,10000万元......) + * @return 字符串 + */ + public static String getAccountantMoney(BigDecimal money, int scale, double divisor) { + return accountantMoney(money, scale, divisor) + getCellFormat(divisor); + } + + /** + * 将人民币转换为会计格式金额(xxxx,xxxx,xxxx.xx) + * + * @param money 待处理的金额 + * @param scale 小数点后保留的位数 + * @param divisor 格式化值 + * @return 字符串 + */ + private static String accountantMoney(BigDecimal money, int scale, double divisor) { + String disposeMoneyStr = formatMoney(money, scale, divisor); + int dotPosition = disposeMoneyStr.indexOf('.'); + String exceptDotMoeny = null;//小数点之前的字符串 + String dotMeony = null;//小数点之后的字符串 + if (dotPosition > 0) { + exceptDotMoeny = disposeMoneyStr.substring(0, dotPosition); + dotMeony = disposeMoneyStr.substring(dotPosition); + } else { + exceptDotMoeny = disposeMoneyStr; + } + //负数处理 + int negativePosition = exceptDotMoeny.indexOf('-'); + if (negativePosition == 0) { + exceptDotMoeny = exceptDotMoeny.substring(1); + } + StringBuilder reverseExceptDotMoney = new StringBuilder(exceptDotMoeny); + reverseExceptDotMoney.reverse();//字符串倒转 + char[] moneyChar = reverseExceptDotMoney.toString().toCharArray(); + StringBuilder returnMeony = new StringBuilder();//返回值 + for (int i = 0; i < moneyChar.length; i++) { + if (i != 0 && i % 3 == 0) { + returnMeony.append(",");//每隔3位加',' + } + returnMeony.append(moneyChar[i]); + } + returnMeony.reverse();//字符串倒转 + if (dotPosition > 0) { + returnMeony.append(dotMeony); + } + if (negativePosition == 0) { + return "-" + returnMeony.toString(); + } else { + return returnMeony.toString(); + } + } + + /** + * 格式化金额,显示为xxx万元,xxx百万,xxx亿 + * + * @param money 待处理的金额 + * @param scale 小数点后保留的位数 + * @param divisor 格式化值 + * @return 字符串 + */ + private static String formatMoney(BigDecimal money, int scale, double divisor) { + if (divisor == 0) { + return "0.00"; + } + if (scale < 0) { + return "0.00"; + } + BigDecimal divisorBD = BigDecimal.valueOf(divisor); + return money.divide(divisorBD, scale, RoundingMode.HALF_UP).toString(); + } + + private static String getCellFormat(double divisor) { + String str = String.valueOf(divisor); + int len = str.substring(0, str.indexOf('.')).length(); + String cell = ""; + switch (len) { + case 1: + cell = "元"; + break; + case 2: + cell = "十元"; + break; + case 3: + cell = "百元"; + break; + case 4: + cell = "千元"; + break; + case 5: + cell = "万元"; + break; + case 6: + cell = "十万元"; + break; + case 7: + cell = "百万元"; + break; + case 8: + cell = "千万元"; + break; + case 9: + cell = "亿元"; + break; + case 10: + cell = "十亿元"; + break; + default: + cell = "-1"; + } + return cell; + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/number/BigDecimalUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/number/BigDecimalUtils.java new file mode 100644 index 0000000..17da20d --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/number/BigDecimalUtils.java @@ -0,0 +1,103 @@ +package ai.remi.comm.util.number; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc BigDecimalUtils + */ +public class BigDecimalUtils { + + /** + * 中位数 + */ + public static BigDecimal median(List list) { + + if (null == list) { + return null; + } + Collections.sort(list); + int size = list.size(); + if (size % 2 == 1) { + + return list.get((size - 1) / 2); + } else { + + return (list.get(size / 2 - 1).add(list.get(size / 2))).divide(new BigDecimal(2), 3); + } + } + + /** + * 排序 + */ + + public static List sort(List list) { + + if (null == list) { + return null; + } + Collections.sort(list); + return list; + + } + + /** + * 最大值 + */ + public static BigDecimal max(List list) { + + if (null == list) { + return null; + } + // BigDecimal max = Collections.max(list); + + BigDecimal sum = list.get(0); + for (int i = 1; i < list.size(); i++) { + + sum = sum.max(list.get(i)); + } + return sum; + } + + /** + * 最小值 + */ + public static BigDecimal min(List list) { + + if (null == list) { + return null; + } + // BigDecimal min = Collections.min(list); + BigDecimal sum = list.get(0); + for (int i = 1; i < list.size(); i++) { + + sum = sum.min(list.get(i)); + } + return sum; + } + + /** + * 平均值 + * + * @return + */ + public static BigDecimal avg(List list) { + + if (null == list) { + return null; + } + BigDecimal sum = list.get(0); + for (int i = 1; i < list.size(); i++) { + + sum = sum.add(list.get(i)); + } + BigDecimal sum1 = new BigDecimal(list.size()); + BigDecimal avg = sum.divide(sum1, 2); + System.out.println(avg); + return avg; + } + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/object/ObjectUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/object/ObjectUtils.java new file mode 100644 index 0000000..a206e43 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/object/ObjectUtils.java @@ -0,0 +1,301 @@ +package ai.remi.comm.util.object; + +import ai.remi.comm.util.string.StringUtils; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.*; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc ObjectUtils 对象工具箱 + */ +public class ObjectUtils { + + /** + * 判断字符串是否为true + * + * @param str + * @return + */ + public static boolean isStrTrue(String str) { + return StringUtils.isNotEmpty(str) && "true".equals(str); + } + + /** + * 判断对象是否不为空 + * + * @param object + * @return: boolean + */ + public static boolean isNotEmpty(Object object) { + return !isEmpty(object); + } + + /** + * 判断对象是否为空 + * + * @param object + * @return: boolean + */ + public static boolean isEmpty(Object object) { + boolean aNull = isNull(object); + if (!aNull) { + //普通的类对象 + Field[] fields = object.getClass().getDeclaredFields(); + //先假设全部属性都是空的,所以只要出现一个属性不为空的就不需要在循环判断 + try { + for (Field field : fields) { + field.setAccessible(true); + if (!"serialVersionUID".equals(field.getName()) && !ObjectUtils.isNull(field.get(object))) { + return false; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return true; + } + + /** + * 判断对象是否不为空 + * + * @param object + * @return: boolean + */ + public static boolean isNotNull(Object object) { + return !isNull(object); + } + + /** + * 判断对象是否为空 + * + * @param object + * @return: boolean + */ + public static boolean isNull(Object object) { + //判断对象是否为null + if (null == object) { + return true; + } + //判断对象是否为容器对象类型 + if (object instanceof Optional) { + return !((Optional) object).isPresent(); + } + //判断对象是否为字符集合类型 + if (object instanceof CharSequence) { + return ((CharSequence) object).length() == 0; + } + //判断对象是否为数组类型 + if (object.getClass().isArray()) { + return Array.getLength(object) == 0; + } + //判断对象是否为集合类型 + if (object instanceof Collection) { + return ((Collection) object).isEmpty(); + } + //判断对象是否为Map + return object instanceof Map ? ((Map) object).isEmpty() : false; + } + + /** + * 判断obejct对象中除了names里面的字段,其他字段都为null(已知对象类型) + * + * @param object + * @param names + * @return + */ + public static boolean isEmpty(Object object, String... names) { + Field[] fields = object.getClass().getDeclaredFields(); + //用于判断所有属性是否为空,如果参数为空则不查询 + boolean flag = true; + for (Field field : fields) { + //不检查 直接取值 + field.setAccessible(true); + try { + String fieldName = field.getName(); + List nameList = new ArrayList<>(); + if (null != names && names.length != 0) { + nameList = Arrays.asList(names); + } + if (!nameList.contains(fieldName) && !Objects.isNull(field.get(object))) { + //不为空 + flag = false; + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return flag; + } + + /** + * 判断object对象中除了names里面的字段,其他字段都为null或者是""(已知对象类型) + * + * @param object + * @param names + * @return + */ + public static boolean isBlank(Object object, String... names) { + Field[] fields = object.getClass().getDeclaredFields(); + //用于判断所有属性是否为空,如果参数为空则不查询 + boolean flag = true; + for (Field field : fields) { + //不检查 直接取值 + field.setAccessible(true); + try { + String fieldName = field.getName(); + List nameList = new ArrayList<>(); + if (null != names && names.length != 0) { + nameList = Arrays.asList(names); + } + Object value = field.get(object); + if (!nameList.contains(fieldName) && !isEmpty(value)) { + //不为空 + flag = false; + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return flag; + } + + /** + * 利用反射 将Java实体转化为 map + * + * @param object object + * @return map + * @version 1.0 + */ + public static Map objectToMap(Object object) { + Map reMap = new HashMap<>(); + if (object == null) { + return null; + } + List fields = new ArrayList<>(); + List childFields; + List fieldsName = new ArrayList<>(); + Class tempClass = object.getClass(); + //当父类为null的时候说明到达了最上层的父类(Object类). + while (tempClass != null) { + fields.addAll(Arrays.asList(tempClass.getDeclaredFields())); + //得到父类,然后赋给自己 + tempClass = tempClass.getSuperclass(); + } + childFields = Arrays.asList(object.getClass().getDeclaredFields()); + for (Field field : childFields) { + fieldsName.add(field.getName()); + } + try { + for (Field field : fields) { + try { + if (fieldsName.contains(field.getName())) { + Field f = object.getClass().getDeclaredField(field.getName()); + f.setAccessible(true); + Object o = f.get(object); + reMap.put(field.getName(), o); + } else { + Field f = object.getClass().getSuperclass().getDeclaredField(field.getName()); + f.setAccessible(true); + Object o = f.get(object); + reMap.put(field.getName(), o); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } catch (SecurityException e) { + e.printStackTrace(); + } + return reMap; + } + + /** + * 利用反射将map集合封装成bean对象 + * + * @param map + * @param clazz + * @return + */ + public static T mapToObject(Map map, Class clazz) throws Exception { + Object obj = clazz.newInstance(); + if (map != null && !map.isEmpty() && map.size() > 0) { + for (Map.Entry entry : map.entrySet()) { + String propertyName = String.valueOf(entry.getKey()); // 属性名 + Object value = entry.getValue(); // 属性值 + String setMethodName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + Field field = getClassField(clazz, propertyName); //获取和map的key匹配的属性名称 + if (field == null) { + continue; + } + Class fieldTypeClass = field.getType(); + value = convertValType(value, fieldTypeClass); + try { + clazz.getMethod(setMethodName, field.getType()).invoke(obj, value); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + } + } + return (T) obj; + } + + /** + * 根据给定对象类匹配对象中的特定字段 + * + * @param clazz + * @param fieldName + * @return + */ + private static Field getClassField(Class clazz, String fieldName) { + if (Object.class.getName().equals(clazz.getName())) { + return null; + } + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + if (field.getName().equals(fieldName)) { + return field; + } + } + Class superClass = clazz.getSuperclass(); //如果该类还有父类,将父类对象中的字段也取出 + if (superClass != null) { //递归获取 + return getClassField(superClass, fieldName); + } + return null; + } + + /** + * 将map的value值转为实体类中字段类型匹配的方法 + * + * @param value + * @param fieldTypeClass + * @return + */ + private static Object convertValType(Object value, Class fieldTypeClass) { + Object retVal = null; + + if (Long.class.getName().equals(fieldTypeClass.getName()) + || long.class.getName().equals(fieldTypeClass.getName())) { + retVal = Long.parseLong(value.toString()); + } else if (Integer.class.getName().equals(fieldTypeClass.getName()) + || int.class.getName().equals(fieldTypeClass.getName())) { + retVal = Integer.parseInt(value.toString()); + } else if (Float.class.getName().equals(fieldTypeClass.getName()) + || float.class.getName().equals(fieldTypeClass.getName())) { + retVal = Float.parseFloat(value.toString()); + } else if (Double.class.getName().equals(fieldTypeClass.getName()) + || double.class.getName().equals(fieldTypeClass.getName())) { + retVal = Double.parseDouble(value.toString()); + } else { + retVal = value; + } + return retVal; + } + +} + diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/object/SerializeUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/object/SerializeUtils.java new file mode 100644 index 0000000..79c621a --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/object/SerializeUtils.java @@ -0,0 +1,5 @@ +package ai.remi.comm.util.object; + +public class SerializeUtils { + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/page/PageUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/page/PageUtils.java new file mode 100644 index 0000000..8411ee0 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/page/PageUtils.java @@ -0,0 +1,54 @@ +package ai.remi.comm.util.page; + + +import java.util.ArrayList; +import java.util.List; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc PageUtils 分页工具箱 + */ +public class PageUtils { + /** + * 开始分页 + * @param list + * @param pageNum 页码 + * @param pageSize 每页多少数据 + * @param + * @return + */ + public static List startPage(List list, int pageNum, int pageSize){ + if(null == list){ + return new ArrayList(); + } + if(list.size() == 0){ + return list; + } + if(pageNum == 0){ + pageNum = 1; + } + int count = list.size(); //记录总数 + int pageCount = 0; //页数 + if(count % pageSize == 0){ + pageCount = count / pageSize; + }else{ + pageCount = count / pageSize + 1; + } + + int fromIndex = 0; //开始索引 + int toIndex = 0; //结束索引 + if(pageNum > pageCount){ + return new ArrayList(); + } + + if(pageNum != pageCount){ + fromIndex = (pageNum -1) * pageSize; + toIndex = fromIndex + pageSize; + }else { + fromIndex = (pageNum - 1) * pageSize; + toIndex = count; + } + return list.subList(fromIndex,toIndex); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/regex/RegexUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/regex/RegexUtils.java new file mode 100644 index 0000000..6071b6f --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/regex/RegexUtils.java @@ -0,0 +1,405 @@ +package ai.remi.comm.util.regex; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 正则表达式工具类 + */ +public class RegexUtils { + + /******************** 正则相关常量 ********************/ + /** + * 正则:手机号(简单) + */ + public static final String REGEX_MOBILE_SIMPLE = "^[1]\\d{10}$"; + /** + * 正则:手机号(精确) + *

移动:134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、178、182、183、184、187、188

+ *

联通:130、131、132、145、155、156、175、176、185、186

+ *

电信:133、153、173、177、180、181、189

+ *

全球星:1349

+ *

虚拟运营商:170

+ */ + public static final String REGEX_MOBILE_EXACT = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|(147))\\d{8}$"; + /** + * 正则:电话号码 + */ + public static final String REGEX_TEL = "^0\\d{2,3}[- ]?\\d{7,8}"; + /** + * 正则:身份证号码15位 + */ + public static final String REGEX_ID_CARD15 = "^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$"; + /** + * 正则:身份证号码18位 + */ + public static final String REGEX_ID_CARD18 = "^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9Xx])$"; + /** + * 正则:邮箱 + */ + public static final String REGEX_EMAIL = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"; + /** + * 正则:URL + */ + public static final String REGEX_URL = "[a-zA-z]+://[^\\s]*"; + /** + * 正则:汉字 + */ + public static final String REGEX_ZH = "^[\\u4e00-\\u9fa5]+$"; + /** + * 正则:用户名,取值范围为a-z,A-Z,0-9,"_",汉字,不能以"_"结尾,用户名必须是6-20位 + */ + public static final String REGEX_USERNAME = "^[\\w\\u4e00-\\u9fa5]{6,20}(?{@code false}: 不匹配 + */ + public static boolean isMobileSimple(CharSequence input) { + return isMatch(REGEX_MOBILE_SIMPLE, input); + } + + /** + * 验证手机号(精确) + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isMobileExact(CharSequence input) { + return isMatch(REGEX_MOBILE_EXACT, input); + } + + /** + * 验证电话号码 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isTel(CharSequence input) { + return isMatch(REGEX_TEL, input); + } + + /** + * 验证身份证号码15位 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isIDCard15(CharSequence input) { + return isMatch(REGEX_ID_CARD15, input); + } + + /** + * 验证身份证号码18位 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isIDCard18(CharSequence input) { + return isMatch(REGEX_ID_CARD18, input); + } + + /** + * 验证邮箱 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isEmail(CharSequence input) { + return isMatch(REGEX_EMAIL, input); + } + + /** + * 验证URL + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isURL(CharSequence input) { + return isMatch(REGEX_URL, input); + } + + /** + * 验证汉字 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isZh(CharSequence input) { + return isMatch(REGEX_ZH, input); + } + + /** + * 验证用户名 + *

取值范围为a-z,A-Z,0-9,"_",汉字,不能以"_"结尾,用户名必须是6-20位

+ * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isUsername(CharSequence input) { + return isMatch(REGEX_USERNAME, input); + } + + /** + * 验证yyyy-MM-dd格式的日期校验,已考虑平闰年 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isDate(CharSequence input) { + return isMatch(REGEX_DATE, input); + } + + /** + * 验证IP地址 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isIP(CharSequence input) { + return isMatch(REGEX_IP, input); + } + + /** + * 验证是否为双字节字符(包括汉字在内) + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isDoubleByteChar(CharSequence input) { + return isMatch(REGEX_DOUBLE_BYTE_CHAR, input); + } + + /** + * 验证是否空白行 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isBlankLine(CharSequence input) { + return isMatch(REGEX_BLANK_LINE, input); + } + + /** + * 验证是否中国邮政编码 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isZipCode(CharSequence input) { + return isMatch(REGEX_ZIP_CODE, input); + } + + /** + * 验证是否正整数 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isPositiveInteger(CharSequence input) { + return isMatch(REGEX_POSITIVE_INTEGER, input); + } + + /** + * 验证是否负整数 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isNegativeInteger(CharSequence input) { + return isMatch(REGEX_NEGATIVE_INTEGER, input); + } + + /** + * 验证是否为整数 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isRegexInteger(CharSequence input) { + return isMatch(REGEX_INTEGER, input); + } + + /** + * 验证是否为非负整数 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isNotNegativeInteger(CharSequence input) { + return isMatch(REGEX_NOT_NEGATIVE_INTEGER, input); + } + + /** + * 验证是否为非正整数 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isNotPositiveInteger(CharSequence input) { + return isMatch(REGEX_NOT_POSITIVE_INTEGER, input); + } + + /** + * 验证是否正浮点数 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isPositiveFloat(CharSequence input) { + return isMatch(REGEX_POSITIVE_FLOAT, input); + } + + /** + * 验证是否负浮点数 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean NEGATIVE_FLOAT(CharSequence input) { + return isMatch(REGEX_NEGATIVE_FLOAT, input); + } + + /** + * 验证是否空白行 + * + * @param input 待验证文本 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isIntegerOrFloat(CharSequence input) { + return isMatch(REGEX_INTEGER_OR_FLOAT, input); + } + + /** + * 判断是否匹配正则 + * + * @param regex 正则表达式 + * @param input 要匹配的字符串 + * @return {@code true}: 匹配
{@code false}: 不匹配 + */ + public static boolean isMatch(String regex, CharSequence input) { + return input != null && input.length() > 0 && Pattern.matches(regex, input); + } + + /** + * 获取正则匹配的部分 + * + * @param regex 正则表达式 + * @param input 要匹配的字符串 + * @return 正则匹配的部分 + */ + public static List getMatches(String regex, CharSequence input) { + if (input == null) { + return null; + } + List matches = new ArrayList<>(); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(input); + while (matcher.find()) { + matches.add(matcher.group()); + } + return matches; + } + + /** + * 获取正则匹配分组 + * + * @param input 要分组的字符串 + * @param regex 正则表达式 + * @return 正则匹配分组 + */ + public static String[] getSplits(String input, String regex) { + if (input == null) { + return null; + } + return input.split(regex); + } + + /** + * 替换正则匹配的第一部分 + * + * @param input 要替换的字符串 + * @param regex 正则表达式 + * @param replacement 代替者 + * @return 替换正则匹配的第一部分 + */ + public static String getReplaceFirst(String input, String regex, String replacement) { + if (input == null) { + return null; + } + return Pattern.compile(regex).matcher(input).replaceFirst(replacement); + } + + /** + * 替换所有正则匹配的部分 + * + * @param input 要替换的字符串 + * @param regex 正则表达式 + * @param replacement 代替者 + * @return 替换所有正则匹配的部分 + */ + public static String getReplaceAll(String input, String regex, String replacement) { + if (input == null) { + return null; + } + return Pattern.compile(regex).matcher(input).replaceAll(replacement); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/saml/SAMLUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/saml/SAMLUtils.java new file mode 100644 index 0000000..8c95f4e --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/saml/SAMLUtils.java @@ -0,0 +1,29 @@ +package ai.remi.comm.util.saml; + +import cn.hutool.core.codec.Base64; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +public class SAMLUtils { + + /** + * 解析SAMLRequest SAMLResponse + */ + public static String decodeAndInflateXML(String samlStr) throws Exception { + // base64解码 + byte[] decodedBytes = Base64.decode(java.net.URLDecoder.decode(samlStr, "utf-8")); + // 获取输入流 + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decodedBytes); + InflaterInputStream inflaterInputStream = new InflaterInputStream(byteArrayInputStream, new Inflater(true)); + byte[] buffer = new byte[decodedBytes.length]; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + // 信息写到输出流 + for (int i = 0; i != -1; i = inflaterInputStream.read(buffer)) { + byteArrayOutputStream.write(buffer, 0, i); + } + return new String(byteArrayOutputStream.toByteArray(), "UTF-8"); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/AesUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/AesUtils.java new file mode 100644 index 0000000..96a2132 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/AesUtils.java @@ -0,0 +1,264 @@ +package ai.remi.comm.util.security; + +import cn.hutool.core.exceptions.UtilException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-26 + * @desc AesUtils 加/解密工具类 + * 使用密钥时请使用 initKey() 方法来生成随机密钥 + * initKey 方法内部使用 java.crypto.KeyGenerator 密钥生成器来生成特定于 AES 算法参数集的随机密钥 + * 使用密钥生成器生成的密钥能保证更强的随机性 + * 生成的二进制密钥建议使用 Hex 进行编码 + */ +public class AesUtils { + + private AesUtils() { + } + + /** + * 密钥算法类型 + */ + public static final String KEY_ALGORITHM = "AES"; + + /** + * 密钥的默认位长度 + */ + public static final int DEFAULT_KEY_SIZE = 128; + + /** + * 加解密算法/工作模式/填充方式 + */ + private static final String ECB_PKCS_5_PADDING = "AES/ECB/PKCS5Padding"; + public static final String ECB_NO_PADDING = "AES/ECB/NoPadding"; + + public static String base64Encode(byte[] bytes) { + return Base64.encodeBase64String(bytes); + } + + public static byte[] base64Decode(String base64Code) { + return Base64.decodeBase64(base64Code); + } + + public static byte[] aesEncryptToBytes(String content, String hexAesKey) throws Exception { + return encrypt(content.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(hexAesKey.toCharArray())); + } + + public static String aesEncrypt(String content, String hexAesKey) throws Exception { + return base64Encode(aesEncryptToBytes(content, hexAesKey)); + } + + public static String aesDecryptByBytes(byte[] encryptBytes, String hexAesKey) throws Exception { + byte[] decrypt = decrypt(encryptBytes, Hex.decodeHex(hexAesKey.toCharArray())); + return new String(decrypt, StandardCharsets.UTF_8); + } + + public static String aesDecrypt(String encryptStr, String hexAesKey) throws Exception { + return aesDecryptByBytes(base64Decode(encryptStr), hexAesKey); + } + + + /** + * 生成 Hex 格式默认长度的随机密钥 + * 32,解二进制后为 16 个字节 + * + * @return String Hex 格式的随机密钥 + */ + public static String initHexKey() { + return Hex.encodeHexString(initKey()); + } + + /** + * 生成默认长度的随机密钥 + * 默认长度为 128 + * + * @return byte[] 二进制密钥 + */ + public static byte[] initKey() { + return initKey(DEFAULT_KEY_SIZE); + } + + /** + * 生成密钥 + * 128、192、256 可选 + * + * @param keySize 密钥长度 + * @return byte[] 二进制密钥 + */ + public static byte[] initKey(int keySize) { + // AES 要求密钥长度为 128 位、192 位或 256 位 + if (keySize != 128 && keySize != 192 && keySize != 256) { + throw new RuntimeException("error keySize: " + keySize); + } + // 实例化 + KeyGenerator keyGenerator; + try { + keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + throw new UtilException("no such algorithm exception: " + KEY_ALGORITHM, e); + } + keyGenerator.init(keySize); + // 生成秘密密钥 + SecretKey secretKey = keyGenerator.generateKey(); + // 获得密钥的二进制编码形式 + return secretKey.getEncoded(); + } + + /** + * 转换密钥 + * + * @param key 二进制密钥 + * @return Key 密钥 + */ + private static Key toKey(byte[] key) { + // 实例化 DES 密钥材料 + return new SecretKeySpec(key, KEY_ALGORITHM); + } + + /** + * 加密 + * + * @param data 待加密数据 + * @param key 密钥 + * @return byte[] 加密的数据 + */ + public static byte[] encrypt(byte[] data, byte[] key) { + return encrypt(data, key, ECB_PKCS_5_PADDING); + } + + /** + * 加密 + * + * @param data 待加密数据 + * @param key 密钥 + * @param cipherAlgorithm 算法/工作模式/填充模式 + * @return byte[] 加密的数据 + */ + public static byte[] encrypt(byte[] data, byte[] key, final String cipherAlgorithm) { + // 还原密钥 + Key k = toKey(key); + try { + Cipher cipher = Cipher.getInstance(cipherAlgorithm); + // 初始化,设置为加密模式 + cipher.init(Cipher.ENCRYPT_MODE, k); + + // 发现使用 NoPadding 时,使用 ZeroPadding 填充 + if (ECB_NO_PADDING.equals(cipherAlgorithm)) { + return cipher.doFinal(formatWithZeroPadding(data, cipher.getBlockSize())); + } + + // 执行操作 + return cipher.doFinal(data); + } catch (Exception e) { + throw new RuntimeException("AES encrypt error", e); + } + } + + /** + * 解密 + * + * @param data 待解密数据 + * @param key 密钥 + * @return byte[] 解密的数据 + */ + public static byte[] decrypt(byte[] data, byte[] key) { + return decrypt(data, key, ECB_PKCS_5_PADDING); + } + + /** + * 解密 + * + * @param data 待解密数据 + * @param key 密钥 + * @param cipherAlgorithm 算法/工作模式/填充模式 + * @return byte[] 解密的数据 + */ + public static byte[] decrypt(byte[] data, byte[] key, final String cipherAlgorithm) { + // 还原密钥 + Key k = toKey(key); + try { + Cipher cipher = Cipher.getInstance(cipherAlgorithm); + // 初始化,设置为解密模式 + cipher.init(Cipher.DECRYPT_MODE, k); + + // 发现使用 NoPadding 时,使用 ZeroPadding 填充 + if (ECB_NO_PADDING.equals(cipherAlgorithm)) { + return removeZeroPadding(cipher.doFinal(data), cipher.getBlockSize()); + } + + // 执行操作 + return cipher.doFinal(data); + } catch (Exception e) { + throw new RuntimeException("AES decrypt error", e); + } + } + + private static byte[] formatWithZeroPadding(byte[] data, final int blockSize) { + final int length = data.length; + final int remainLength = length % blockSize; + + if (remainLength > 0) { + byte[] inputData = new byte[length + blockSize - remainLength]; + System.arraycopy(data, 0, inputData, 0, length); + return inputData; + } + return data; + } + + private static byte[] removeZeroPadding(byte[] data, final int blockSize) { + final int length = data.length; + final int remainLength = length % blockSize; + if (remainLength == 0) { + // 解码后的数据正好是块大小的整数倍,说明可能存在补 0 的情况,去掉末尾所有的 0 + int i = length - 1; + while (i >= 0 && 0 == data[i]) { + i--; + } + byte[] outputData = new byte[i + 1]; + System.arraycopy(data, 0, outputData, 0, outputData.length); + return outputData; + } + return data; + } + + public static void main(String[] args) throws Exception { + //long current = Calendar.getInstance().getTimeInMillis(); + //long current = 1616031425922L; + //System.out.println("\n当前时间戳为:" + current); + + + // 使用密钥生成器 KeyGenerator 生成的 16 字节随机密钥的 hex 字符串,使用时解 hex 得到二进制密钥 + //byte[] bytes = AesUtils.initKey(); + //String aesKey = Hex.encodeHexString(bytes); + String aesKey = "c39de8d1308e0ad7afbe1b4749c5d3a9"; + System.out.println("\n当前密钥为" + aesKey); + + + //String content = "{\"isPhone\":\"0\",\"userid\":\"zhangsan\",\"passwd\":\"123456\",\"timestamp\":1610610307826}"; + String content = "tya_40hyJF"; + System.out.println("\nAES加密前:" + content); + + String encrypt = aesEncrypt(content, aesKey); + System.out.println("\nAES加密后:" + encrypt); + + String decrypt = aesDecrypt(encrypt, aesKey); + System.out.println("\nAES解密后:" + decrypt); + + //String bb = "{\"isPhone\":\"0\",\"userid\":\"zhangsan\",\"passwd\":\"123456\",\"timestamp\":1610610307826}"; + //String md5 = DigestUtils.md5DigestAsHex(bb.getBytes("UTF-8")); + + //System.out.println("\nmd5加密后:" + md5); + } + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Base64Utils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Base64Utils.java new file mode 100644 index 0000000..e37b637 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Base64Utils.java @@ -0,0 +1,132 @@ +package ai.remi.comm.util.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.util.Base64; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc Base64Kits + */ +public class Base64Utils { + public static final String CHARSET = "utf-8"; + + private static final Logger logger = LoggerFactory.getLogger(Base64Utils.class); + + public static void main(String[] args) throws UnsupportedEncodingException { + //测试FileBase64 + //long start = System.currentTimeMillis(); + //String imgBase64Str = Base64Utils.convertFileToBase64("C:\\demos\\a3fa6ce0-b9a9-4313-93f0-501d1149596c.jpg"); + //System.out.println("本地图片转换Base64:" + imgBase64Str); + //System.out.println("Base64字符串length=" + imgBase64Str.length()); + //Base64Utils.convertBase64ToFile(imgBase64Str, "C:\\demos\\", "test.jpg"); + //System.out.println("duration:" + (System.currentTimeMillis() - start)); + //测试StringBase64 + long start = System.currentTimeMillis(); + //ThisIsMyCustomSecretKey012345678 + //String base64Str = Base64Utils.encode("RemiAdminNacosCustomSecretKey2024"); + String base64Str = Base64Utils.encode("RemiAdminNacosCustomSecretKey2024"); + System.out.println("字符串加密后的Base64为:" + base64Str); + System.out.println("Base64字符串length=" + base64Str.length()); + String decode = Base64Utils.decode(base64Str); + //String decode = Base64Utils.decode("VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg="); + System.out.println("Base64解密后的字符串为:"+decode); + System.out.println("duration:" + (System.currentTimeMillis() - start)); + + } + /** + * 字符串解密 + */ + public static String decode(String data) { + try { + if (null == data) { + return null; + } + + return new String(Base64.getDecoder().decode(data.getBytes(CHARSET)), CHARSET); + } catch (UnsupportedEncodingException e) { + logger.error(String.format("字符串:%s,解密异常", data), e); + } + + return null; + } + + /** + * 字符串加密 + */ + public static String encode(String data) { + try { + if (null == data) { + return null; + } + return new String(Base64.getEncoder().encode(data.getBytes(CHARSET)), CHARSET); + } catch (UnsupportedEncodingException e) { + logger.error(String.format("字符串:%s,加密异常", data), e); + } + + return null; + } + + /** + * 本地文件(图片、excel等)转换成Base64字符 + */ + public static String convertFileToBase64(String filePath) throws UnsupportedEncodingException { + byte[] data = null; + // 读取图片字节数组 + try { + InputStream in = new FileInputStream(filePath); + System.out.println("文件大小(字节)=" + in.available()); + data = new byte[in.available()]; + in.read(data); + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + // 对字节数组进行Base64编码,得到Base64编码的字符串 + return new String(Base64.getEncoder().encode(data), CHARSET); + } + + /** + * 将base64字符串,生成文件 + */ + public static File convertBase64ToFile(String fileBase64String, String filePath, String fileName) { + + BufferedOutputStream bos = null; + FileOutputStream fos = null; + File file = null; + try { + File dir = new File(filePath); + //判断文件目录是否存在 + if (!dir.exists() && dir.isDirectory()) { + dir.mkdirs(); + } + byte[] bfile = Base64.getDecoder().decode(fileBase64String); + file = new File(filePath + File.separator + fileName); + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos); + bos.write(bfile); + return file; + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (bos != null) { + try { + bos.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/DesUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/DesUtils.java new file mode 100644 index 0000000..6917419 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/DesUtils.java @@ -0,0 +1,82 @@ +package ai.remi.comm.util.security; + + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.SecureRandom; + +/** + * DES加密介绍 DES是一种对称加密算法,所谓对称加密算法即:加密和解密使用相同密钥的算法。DES加密算法出自IBM的研究, + * 后来被美国政府正式采用,之后开始广泛流传,但是近些年使用越来越少,因为DES使用56位密钥,以现代计算能力, + * 24小时内即可被破解。虽然如此,在某些简单应用中,我们还是可以使用DES加密算法,本文简单讲解DES的JAVA实现 。 + * 注意:DES加密和解密过程中,密钥长度都必须是8的倍数 + */ + +public class DesUtils { + public DesUtils() { + } + + // 测试 + public static void main(String args[]) { + // 待加密内容 + String str = "cryptology"; + // 密码,长度要是8的倍数 + String password = "95880288"; + + byte[] result; + try { + result = DesUtils.encrypt(str.getBytes(), password); + System.out.println("加密后: " + result); + byte[] decryResult = DesUtils.decrypt(result, password); + System.out.println("解密后: " + new String(decryResult)); + } catch (UnsupportedEncodingException e2) { + // TODO Auto-generated catch block + e2.printStackTrace(); + } catch (Exception e1) { + e1.printStackTrace(); + } + } + + public static byte[] encrypt(byte[] datasource, String password) { + try { + SecureRandom random = new SecureRandom(); + DESKeySpec desKey = new DESKeySpec(password.getBytes()); + // 创建一个密匙工厂,然后用它把DESKeySpec转换成 + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey securekey = keyFactory.generateSecret(desKey); + // Cipher对象实际完成加密操作 + Cipher cipher = Cipher.getInstance("DES"); + // 用密匙初始化Cipher对象,ENCRYPT_MODE用于将 Cipher 初始化为加密模式的常量 + cipher.init(Cipher.ENCRYPT_MODE, securekey, random); + // 现在,获取数据并加密 + // 正式执行加密操作 + return cipher.doFinal(datasource); // 按单部分操作加密或解密数据,或者结束一个多部分操作 + } catch (Throwable e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] decrypt(byte[] src, String password) throws Exception { + // DES算法要求有一个可信任的随机数源 + SecureRandom random = new SecureRandom(); + // 创建一个DESKeySpec对象 + DESKeySpec desKey = new DESKeySpec(password.getBytes()); + // 创建一个密匙工厂 + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");// 返回实现指定转换的 + // Cipher + // 对象 + // 将DESKeySpec对象转换成SecretKey对象 + SecretKey securekey = keyFactory.generateSecret(desKey); + // Cipher对象实际完成解密操作 + Cipher cipher = Cipher.getInstance("DES"); + // 用密匙初始化Cipher对象 + cipher.init(Cipher.DECRYPT_MODE, securekey, random); + // 真正开始解密操作 + return cipher.doFinal(src); + } +} + diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/DigestUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/DigestUtils.java new file mode 100644 index 0000000..826d153 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/DigestUtils.java @@ -0,0 +1,78 @@ +package ai.remi.comm.util.security; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; + +import lombok.SneakyThrows; +import org.apache.commons.lang3.Validate; + +/** + * 不可逆加密工具类 + */ +public class DigestUtils { + + private static SecureRandom random = new SecureRandom(); + + /** + * 生成随机的Byte[]作为salt密钥. + * + * @param numBytes byte数组的大小 + */ + public static byte[] genSalt(int numBytes) { + Validate.isTrue(numBytes > 0, "numBytes argument must be a positive integer (1 or larger)", numBytes); + byte[] bytes = new byte[numBytes]; + random.nextBytes(bytes); + return bytes; + } + + /** + * 对字符串进行散列, 支持md5与sha1算法. + * + * @param input 需要散列的字符串 + * @param algorithm 散列算法("SHA-1"、"MD5") + * @param salt + * @param iterations 迭代次数 + * @return + */ + @SneakyThrows + public static byte[] digest(byte[] input, String algorithm, byte[] salt, int iterations) { + MessageDigest digest = MessageDigest.getInstance(algorithm); + + if (salt != null) { + digest.update(salt); + } + + byte[] result = digest.digest(input); + + for (int i = 1; i < iterations; i++) { + digest.reset(); + result = digest.digest(result); + } + return result; + } + + /** + * 对文件进行sha1散列. + * + * @param input 需要散列的流 + * @param algorithm 散列算法("SHA-1"、"MD5") + */ + @SneakyThrows + public static byte[] digest(InputStream input, String algorithm) throws IOException { + MessageDigest messageDigest = MessageDigest.getInstance(algorithm); + int bufferLength = 8 * 1024; + byte[] buffer = new byte[bufferLength]; + int read = input.read(buffer, 0, bufferLength); + + while (read > -1) { + messageDigest.update(buffer, 0, read); + read = input.read(buffer, 0, bufferLength); + } + + return messageDigest.digest(); + } + +} \ No newline at end of file diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Md5Utils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Md5Utils.java new file mode 100644 index 0000000..8765c4c --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Md5Utils.java @@ -0,0 +1,48 @@ +package ai.remi.comm.util.security; + +import java.security.MessageDigest; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc Md5Utils + */ +public class Md5Utils { + /** + * 将给定的字符串MD5加密 + * + * @param string 给定的字符串 + * @return MD5加密后生成的字符串 + */ + public static String encode(String string) { + String result = null; + try { + char[] charArray = string.toCharArray(); + byte[] byteArray = new byte[charArray.length]; + for (int i = 0; i < charArray.length; i++) { + byteArray[i] = (byte) charArray[i]; + } + + StringBuffer hexValue = new StringBuffer(); + byte[] md5Bytes = MessageDigest.getInstance("MD5") + .digest(byteArray); + for (int i = 0; i < md5Bytes.length; i++) { + int val = ((int) md5Bytes[i]) & 0xff; + if (val < 16) { + hexValue.append("0"); + } + hexValue.append(Integer.toHexString(val)); + } + + result = hexValue.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } + + public static void main(String[] args) { + String encode = encode("1122"); + System.out.println(encode); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/PwdUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/PwdUtils.java new file mode 100644 index 0000000..39467a7 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/PwdUtils.java @@ -0,0 +1,32 @@ +package ai.remi.comm.util.security; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc PwdUtils 用户密码工具类 + */ +public class PwdUtils { + + //private final static String DEFAULT_PASS="Remi2022"; + + private final static String DEFAULT_PASS="Remi@123"; + + + public static boolean isValidPassword(String password,String encodePassword) { + return encode(password).equals(encodePassword); + } + + + public static String encode(String s){ + return Md5Utils.encode(s); + } + + public static String getDefaultPassEncryption(){ + return encode(DEFAULT_PASS); + } + + public static void main(String[] args) { + System.out.println(getDefaultPassEncryption()); + System.out.println(Md5Utils.encode(DEFAULT_PASS)); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Rsa2Demo.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Rsa2Demo.java new file mode 100644 index 0000000..af312b2 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Rsa2Demo.java @@ -0,0 +1,75 @@ +package ai.remi.comm.util.security; + +/** + * @author DianJiu 【公众号 点九开源】 + * @email lidianjiu@njydsz.com + * @date 2022-07-26 + * @desc 公钥加密,私钥解密;私钥加签,公钥验签。 + */ +public class Rsa2Demo { + + public static void main(String[] args) { + mookHttp(); + } + + public static void mookHttp(){ + Rsa2Utils.initRSAKey(2048); + // 初始化A公司的公钥、私钥 + String privateKeyA= "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC1YbT3O4Rd2F5d7R/+61clUBvGtpnYYzDsUCmVLCYnE1zCNQawi5ga+cpdgyafOLFJcVGfcHEushXgJ3XFp0mriRCKnBiA0OwoNa0sVZyW46eLsYhxx6aNmcsfk04xUNR52FYRD3gvLpfIXHlW4Zib9FtYJThjXo6kSLrZbh2f9gaYNlJzJn8tWljI3sPgzgvvVCtPbwFOW5D7Pt7n/nsL3AUM5ZqItRBvlkejitbdjYlg2WXJA6pDYtAxO9+iwiDsftQqF8QzXiZDTBEr+jqKPk3ZpYpuE8nXSWpx/1c4ujhI6R+/CVvj4PEccfDs9x2dusVLgVdYgXxFY4jyyFhrAgMBAAECggEBAJdxCAaw89jjMg9cUC2o0289DqQAjD1GvG9ikwWRAh34JaelXHuFYi+LxjIDsfiY/jVi3xKvcGDbojYw3D12aqVFYMFKiY7sYXFFN9zfcknttFOHnHPbja2G7jjj5hJ4+Z7uSRYfg2ppjaNlyo4G+EOmZ6oWEZmuNozmiNPIBAYsmNXmxK7jmFKsMr8dN8oTraP2j5rjpWwvcoqoJKacGLY8dV8/miGlaTyJzrJT/JRbvPl1pCe9W5MMv2XhwaIrpM2kPuO1HQjBLgsFXQyYxAP0BvqID7QX+MYK104VcpuuAARLqZZUwEZP2quJUMxT973KUlLkBUdgfrLoElvNXOECgYEA/1x4botRAudqjaWH4UTAqaCy0/WrWluZqGxaZn/aS3BdFYya/zMH8kY6s16yKZ5qErxNamQWrGElpHHR8dz3fqkPp5cjqYRcxztPhtCxwkNO5lPRZPIngD7c4CNN1nKV9rO1+a4ALDZJzOKYZSybwOYWkKfEC9+ZNQz2mk98TNECgYEAtdXcba2ayxb6BypunV8xXgX7e57b7VmwSARJGjGAwJJ+Y9drFTFrn/Ei+Tl6r+lnjB7fDejlzxCKQDtP+ZwEMWKaE+LWiHDMucAC5x77wlaWzLEMSpfBsNp2Sw9PmAoQrteHFKJ6V03jMJR2SB3ihH1uj99+4i6U9svHa+Y0cHsCgYAqSMGYbHFJrdAzDEnnv163t+hkuLVbDPfwr6B4gItBGqYPP20a/PQ/5ekqcm/F/HlMCOo4x8IJYTy+cip1hkVlL9FrytcY5tYGF4gfpscIh+S0twRhVse7sK0aYD4vWGIEq9ViNrHIoGSwDIV3m19fHbp20ZDg9nGEG1VqT+chQQKBgQCukOr2rJUP5jYRDQ52rL3TO0EMlB0lR1ZyPRMFP4MQ3VLTfSrDhNeDDFf8Fc+P9C7BmZ2gV/fQC1v+gDKuOrbXxoaPEtXM1SXmKc1fnyIpl0Fcr5hSQFDl/UVVmBRcrUUI5cc76yQyjobOy6imYXJmqZisLO8+K2Q9brM8BhxP4wKBgQDd0r1LAXXDmfEdtZAz0kfhssWVb2E/w+TSyvSqFsvJh8bCrZ18WgUeiAya47mjXCUqB8dOUTPjtO8HqfUfqI+Qr0agdErt1BvoBD10ntVowbOJrj0RJLfFr8YLrHhGkLPTZzt17upv+l5x+pyMaDPxePXO8jy1Nk3rL2okpHqW6A=="; + String publicKeyA= "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtWG09zuEXdheXe0f/utXJVAbxraZ2GMw7FAplSwmJxNcwjUGsIuYGvnKXYMmnzixSXFRn3BxLrIV4Cd1xadJq4kQipwYgNDsKDWtLFWcluOni7GIccemjZnLH5NOMVDUedhWEQ94Ly6XyFx5VuGYm/RbWCU4Y16OpEi62W4dn/YGmDZScyZ/LVpYyN7D4M4L71QrT28BTluQ+z7e5/57C9wFDOWaiLUQb5ZHo4rW3Y2JYNllyQOqQ2LQMTvfosIg7H7UKhfEM14mQ0wRK/o6ij5N2aWKbhPJ10lqcf9XOLo4SOkfvwlb4+DxHHHw7PcdnbrFS4FXWIF8RWOI8shYawIDAQAB"; + // 初始化B公司的公钥、私钥 + String privateKeyB="MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCD9IqMkrTxcsS/ldiY4ryZzTPY2mPiqjh6bTWCfR1+TJfh7cH9H7TH3cPWfizwa/nb9JaAqpZFEmkn1aprNKkjnZM6F1bW9F5N02mARxxUUL4J+BCnYjNwj/f25b8VQp6XrA37P3mi2Z97m9xBdGSooLCPhsFKFzq99wCJHzBivGCmlvr9IgFbsiYkjdOuMn4EFFhGqubfazECrbpVYQPzq0eeEOOal11ACmMysTinHg9DEG4X73Q9lT5Cwb38sm7mmq6e8PkWUDzKbG+CQxG/IBEio35/OB7zNcJ9G87eUIK0s5OVApxdBGFBfG6gHOjx+iTwJyEpZziZljLZf+07AgMBAAECggEAcHYHax1R54IoGxyr2ElN8ksIiCZBARqQPg87jrXxYAYJYD2RmpjeK4Ve9jzdMh7keUkwyAIbRk0VoCSjNPFRwg/3PYhMs7DberrW7MQuadHS1Y49K1svehzJefx+6bC4l43E8MA9e8S7/s5sXsDDVVB2iiyQKgQ4RVTxeOxDAbJOOSj5QZxqwR4x2JalDGFawhKsX9d2l1fQAV/JdDKKK1xqgRzvWqub9MhZzH9Cf1o+gqQ20paFwAiuHVfJpc1QjgdHI9ExDn45jqAhxe6C+YqeUVHrl0cFaCMnrSkQAcvTUT6ancZY3QW/yJlil3boQojmy+P2RkFESqnEz3EWqQKBgQDL1y7AAfUMsdjYXE+hnADuQznAv01IwcoNYmInXGhGTiAwfpQPNYsi/+u4ia0l1Myu1SVt1QcDdemLEo5YKAQepHHJ6Zi/CZNvfKSfKhbnSzDJzxBX3garSdV65rkLMCPxOEFDZtoF//kgDQg3Pl0fsHORw5isvbHqfFb689sP7wKBgQCluGz0MID2Fh/29Q001toQ1wRHjpuo5FdKn+NkWfDCdaZX3T0XmyF9vNpQM02HsC7Dpipakk/ex/ZJr277DRCWP5WuhjMHgIT6KUannMT4ARj0hmEqdwiAYlZGcK9PKhpERWDrlG4tXO+MsLAiBRA/R0SPZyeuybtoVxZ+XBOrdQKBgBa9T5LN5Rdgc69XRO7jZ1stFrFA5g86xx6NHiQD9AH7zy15wWyc7YOfBx3UBgPVZGwQjRdej2S8b6w7CrQc6rC8aZE7xPVyzQa/4eF7oYh14+BvgAQ9WYMcPntyZ6wu16OeQfGR9Lp8nr8LlSIMi/jwB0R4jHygwMtGybE/KO3zAoGAByqMe5rFWN7oDvPV2F0aHPRPWRDPpmQJak6ztJk3EhGoWdBVXfn1XsyV3DX7ukxiGU/MMRJ2pbmb6t/NOsn716N0Q5HGDyL9nRu/1GuxENBrVdehyAZRtcuK04z358J4VE9zu3w1r3x/V2QkmbQCWdopGtJpIuPSWNa7QEeXCikCgYAri2pTViyR3o98uGpCxVLFp8zLF6YHh8ufjGyhp/l/wk928rmXJM5nSlJYuFJ7MgvOjeJK6u0ebGDX58lvuSN6o6EeFWw3H/WFxEqHAJETpkOl7ya7hbJABCFzL2xG5oCDHLzpykENUrIs2ZpmvND5yiFwqfjqThmKzXcH6oBS7w=="; + String publicKeyB="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg/SKjJK08XLEv5XYmOK8mc0z2Npj4qo4em01gn0dfkyX4e3B/R+0x93D1n4s8Gv52/SWgKqWRRJpJ9WqazSpI52TOhdW1vReTdNpgEccVFC+CfgQp2IzcI/39uW/FUKel6wN+z95otmfe5vcQXRkqKCwj4bBShc6vfcAiR8wYrxgppb6/SIBW7ImJI3TrjJ+BBRYRqrm32sxAq26VWED86tHnhDjmpddQApjMrE4px4PQxBuF+90PZU+QsG9/LJu5pqunvD5FlA8ymxvgkMRvyARIqN+fzge8zXCfRvO3lCCtLOTlQKcXQRhQXxuoBzo8fok8CchKWc4mZYy2X/tOwIDAQAB"; + // 初始化请求、响应数据 + String req ="1111111111111"; + String resp ="22222222222222"; + + /** + * (公钥加密,私钥解密;私钥签名,公钥验签。) + * 模拟A公司向B公司发送请求 + * B公司验签后解密请求报文 + * B公司处理完自己的业务后 + * 并对响应报文加密和加签 + * A公司收到B公司响应后 + * A公司验签后解密响应 + */ + //请求 + System.out.println("A向B 发送请求开始=========="); + System.out.println("加密前的请求数据为===>"+req); + //用B公司的公钥加密请求 + String encryptByPublicKeyB = Rsa2Utils.buildRSAEncryptByPublicKey(req, publicKeyB); + System.out.println("用B公司的公钥加密请求===>"+encryptByPublicKeyB); + //用A公司的私钥生成签名 + String reqSignature = Rsa2Utils.buildRSASignByPrivateKey(encryptByPublicKeyB, privateKeyA); + System.out.println("用A公司的私钥生成签名===>"+reqSignature); + + System.out.println("B收到A的请求开始=========="); + //用A公司的公钥验证签名 + boolean SignatureBoolByPublicKeyA = Rsa2Utils.buildRSAverifyByPublicKey(encryptByPublicKeyB, publicKeyA, reqSignature); + System.out.println("用A公司的公钥验证签名===>"+SignatureBoolByPublicKeyA); + //用B公司的私钥解密请求 + String decryptByPrivateKeyB = Rsa2Utils.buildRSADecryptByPrivateKey(encryptByPublicKeyB, privateKeyB); + System.out.println("用B公司的私钥解密请求===>"+decryptByPrivateKeyB); + + + //响应 + System.out.println("B发送A的响应开始=========="); + System.out.println("加密前的响应数据为===>"+resp); + //用A公司的公钥加密响应 + String encryptByPublicKeyA = Rsa2Utils.buildRSAEncryptByPublicKey(resp, publicKeyA); + System.out.println("用A公司的公钥加密请求===>"+encryptByPublicKeyA); + //用B公司的私钥生成签名 + String respSignature = Rsa2Utils.buildRSASignByPrivateKey(encryptByPublicKeyA, privateKeyB); + System.out.println("用B公司的私钥生成签名===>"+respSignature); + + System.out.println("A收到B的响应开始=========="); + //用B公司的公钥验证签名 + boolean SignatureBoolByPublicKeyB = Rsa2Utils.buildRSAverifyByPublicKey(encryptByPublicKeyA, publicKeyB, respSignature); + System.out.println("用B公司的公钥验证签名===>"+SignatureBoolByPublicKeyB); + //用A公司的私钥解密响应 + String decryptByPrivateKeyA = Rsa2Utils.buildRSADecryptByPrivateKey(encryptByPublicKeyA, privateKeyA); + System.out.println("用A公司的私钥解密请求===>"+decryptByPrivateKeyA); + } + +} + diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Rsa2Utils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Rsa2Utils.java new file mode 100644 index 0000000..5c0eabf --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/security/Rsa2Utils.java @@ -0,0 +1,253 @@ +package ai.remi.comm.util.security; + +import org.apache.commons.io.IOUtils; + +import javax.crypto.Cipher; +import java.io.ByteArrayOutputStream; +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc Rsa2Utils + */ +public class Rsa2Utils { + public static final String CHARSET = "UTF-8"; + //密钥算法 + public static final String ALGORITHM_RSA = "RSA"; + //RSA 签名算法 + public static final String ALGORITHM_RSA_SIGN = "SHA256WithRSA"; + public static final int ALGORITHM_RSA_PRIVATE_KEY_LENGTH = 2048; + //用于封装随机产生的公钥与私钥 + private static Map keyPairMap = new HashMap(); + + private Rsa2Utils() { + } + + public static void main(String[] args) { + //生成公钥私钥 + initRSAKey(2048); + //私钥 + String privateKey = keyPairMap.get("privateKey"); + System.out.println("生成私钥:"+privateKey); + //String privateKey="MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDH4u7Shk55VvrOt7pEcyV4eLETNVgec1hGwtRc/YicYLNS8SSQlozESidoo7rh03QYHrEKswhltPfo63fOydkFWMAbX1M9VbJQin9OR2Xxu/OGBAUrXPArdfzlLpMtG+xsG2iapVBXOHx77E1DX0AehDw/jDGAawlOyqSavdWlVDDLQP2312FLvR9TjfBly9P3oQbbum9JrSwgkJM+rjwvElUC4Rs48w6l0RI+hJIe4RIoTyXefBQI4jNUrcVsJZpJEtp4khlmDJKx632fumg9zi4tctS/3MZTGoJaA1zei/UuzXD33nHsL+6ZAwCSayidLeXjKCn5CCAQO6PGKg2pAgMBAAECggEAA/+fJWj4YucBh2cXe80N39YSPL8hbyKBjpDRLLcJp48rpyoZwCay1C5jkW1NDL1tIhpjEkK4lUhEeU1UjTdudvPqi+GhSaXQNlo+57L38BkBaxnjfJWUXFo03+TJnupjFVqLAiTycXw4yUXV2dYq8zFT/zVvvBDoE8Jr/ohzoKYAb4ziahDPg7D15oaX8S6dAa/qBoSapLSO+rN6eELEqAaH8r4QVCtVpVzDLXcIpF9kgaJmAkJfrb9Cmi6dA7MBbwCjEK/mtyGJTZaw7ZTJR6+CumPmGZCjWppNawX/8ISYbZeAmX9mGSEp6iI17ciYZt79YfQGDOSvRgjBA67vrQKBgQDu4X5AVaxQwLKLFfuhoim9ZmB4/MAQntvCD5Oe7RQM/eepSX9LIuDcoj7tDjqCvPemEosLoR05WHxe8eLNFwcUzkiQFdhB2lNn02rBn8rCHTwZvf1WHwhkNTi0/szRgZexzRIqrUJrNlAiezsbcYxeoOsLqnKzQExNM4mTvPkcDwKBgQDWNgyPHTGXLpXjCPJ8HF92xveUOwXb+H8SVs5AVHwg+kJNvtjqAwASPnaBs9xMCEaXChF/2O04AUB29comjPZ3By0VmWiH3ngOucsIGih/9RDs0JTWaNaMDYvhA9huXKeIRpo8hyTLcLJ8Y0XVHgi8a68tnP+qfA5kANC17kDixwKBgQC9ayLQYt7rfZFWBo1soojUfZyb81ddFoqNfAqDZ9yRXLhYnUYJz1NcwG6DTNgKmUsKbgPBqfThZw/fvZApLkxYLCaP0jKULUobtyIc/NcrS0E/7E44/wyJYUZhBvZkXo1p2IHCQ3a6SuwKG4tH/Kbg1lrq+jJPfvnB8c7yzlL98wKBgBHKbWKZdxvqR5KSlKkQJjGUNs291S++xLb+NKUNHz4R6t3QIu3Uj/Xz8B23lW1PJqW2tVtb5d/H4tW+diqbfIDIPMtVcWQPawM2FRG4MnLmJKC2dhYUoPUCzwJCMoB/+34K9xZd1wV6d6hzCkK3+tvDScpNBsLfCMbvdmYVwpfTAoGBAMv12bQYwCK/EmezCl484AvNJ9/oIjqz1QyK141HXb5QXIXN4DL//TcngslDvqpqvoVXM4TRoc8Ne6aQeRaNgIeOuYeyixWi48x6csN5FC41lnUdEkPYIxqZCrkxglS8J/oLKRiu385gQPxd2EqVE21snajRoY3CiKXQkchxyycB"; + //公钥 + String publicKey = keyPairMap.get("publicKey"); + System.out.println("生成公钥:"+publicKey); + //String publicKey="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx+Lu0oZOeVb6zre6RHMleHixEzVYHnNYRsLUXP2InGCzUvEkkJaMxEonaKO64dN0GB6xCrMIZbT36Ot3zsnZBVjAG19TPVWyUIp/Tkdl8bvzhgQFK1zwK3X85S6TLRvsbBtomqVQVzh8e+xNQ19AHoQ8P4wxgGsJTsqkmr3VpVQwy0D9t9dhS70fU43wZcvT96EG27pvSa0sIJCTPq48LxJVAuEbOPMOpdESPoSSHuESKE8l3nwUCOIzVK3FbCWaSRLaeJIZZgySset9n7poPc4uLXLUv9zGUxqCWgNc3ov1Ls1w995x7C/umQMAkmsonS3l4ygp+QggEDujxioNqQIDAQAB"; + //私钥加密解密 + String encryptByPrivateKey = buildRSAEncryptByPrivateKey("ABCabc123测试", privateKey); + System.out.println("私钥加密:"+encryptByPrivateKey); + String decryptByPublicKey = buildRSADecryptByPublicKey(encryptByPrivateKey, publicKey); + System.out.println("公钥解密:"+decryptByPublicKey); + //公钥加密解密 + //String encryptByPublicKey = buildRSAEncryptByPublicKey("ABCabc123测试", publicKey); + //System.out.println("公钥加密:"+encryptByPublicKey); + //String decryptByPrivateKey = buildRSADecryptByPrivateKey(encryptByPublicKey, privateKey); + //System.out.println("私钥解密:"+decryptByPrivateKey); + //使用私钥生成数字签名 + String privateValue = buildRSASignByPrivateKey("ABCabc123测试", privateKey); + System.out.println("私钥加签:"+privateValue); + //使用公钥校验数字签名 + boolean publicValue = buildRSAverifyByPublicKey("ABCabc123测试", publicKey, privateValue); + System.out.println("公钥验签:"+publicValue); + + } + /** + * 初始化RSA算法密钥对 + * + * @param keysize RSA1024已经不安全了,建议2048 + * @return 经过Base64编码后的公私钥Map, 键名分别为publicKey和privateKey + */ + public static void initRSAKey(int keysize) { + if (keysize != ALGORITHM_RSA_PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException("RSA1024已经不安全了,请使用" + ALGORITHM_RSA_PRIVATE_KEY_LENGTH + "初始化RSA密钥对"); + } + //为RSA算法创建一个KeyPairGenerator对象 + KeyPairGenerator kpg; + try { + kpg = KeyPairGenerator.getInstance(ALGORITHM_RSA); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("No such algorithm-->[" + ALGORITHM_RSA + "]"); + } + //初始化KeyPairGenerator对象,不要被initialize()源码表面上欺骗,其实这里声明的size是生效的 + kpg.initialize(ALGORITHM_RSA_PRIVATE_KEY_LENGTH); + //生成密匙对 + KeyPair keyPair = kpg.generateKeyPair(); + //得到公钥 + Key publicKey = keyPair.getPublic(); + String publicKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded()); + //得到私钥 + Key privateKey = keyPair.getPrivate(); + String privateKeyStr = Base64.getEncoder().encodeToString(privateKey.getEncoded()); + keyPairMap.put("publicKey", publicKeyStr); + keyPairMap.put("privateKey", privateKeyStr); + } + + /** + * RSA算法公钥加密数据 + * + * @param data 待加密的明文字符串 + * @param key RSA公钥字符串 + * @return RSA公钥加密后的经过Base64编码的密文字符串 + */ + public static String buildRSAEncryptByPublicKey(String data, String key) { + try { + //通过X509编码的Key指令获得公钥对象 + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(key)); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); + Key publicKey = keyFactory.generatePublic(x509KeySpec); + Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return Base64.getEncoder().encodeToString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET))); + } catch (Exception e) { + throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e); + } + } + /** + * RSA算法公钥解密数据 + * + * @param data 待解密的经过Base64编码的密文字符串 + * @param key RSA公钥字符串 + * @return RSA公钥解密后的明文字符串 + */ + public static String buildRSADecryptByPublicKey(String data, String key) { + try { + //通过X509编码的Key指令获得公钥对象 + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(key)); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); + Key publicKey = keyFactory.generatePublic(x509KeySpec); + Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + cipher.init(Cipher.DECRYPT_MODE, publicKey); + return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.getDecoder().decode(data)), CHARSET); + } catch (Exception e) { + throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e); + } + } + /** + * RSA算法私钥加密数据 + * + * @param data 待加密的明文字符串 + * @param key RSA私钥字符串 + * @return RSA私钥加密后的经过Base64编码的密文字符串 + */ + public static String buildRSAEncryptByPrivateKey(String data, String key) { + try { + //通过PKCS#8编码的Key指令获得私钥对象 + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key)); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); + Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec); + Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + return Base64.getEncoder().encodeToString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET))); + } catch (Exception e) { + throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e); + } + } + /** + * RSA算法私钥解密数据 + * @param data 待解密的经过Base64编码的密文字符串 + * @param key RSA私钥字符串 + * @return RSA私钥解密后的明文字符串 + */ + public static String buildRSADecryptByPrivateKey(String data, String key) { + try { + //通过PKCS#8编码的Key指令获得私钥对象 + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key)); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); + Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec); + Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.getDecoder().decode(data)), CHARSET); + } catch (Exception e) { + throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e); + } + } + /** + * RSA算法使用私钥对数据生成数字签名 + * + * @param data 待签名的明文字符串 + * @param key RSA私钥字符串 + * @return RSA私钥签名后的经过Base64编码的字符串 + */ + public static String buildRSASignByPrivateKey(String data, String key) { + try { + //通过PKCS#8编码的Key指令获得私钥对象 + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key)); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec); + Signature signature = Signature.getInstance(ALGORITHM_RSA_SIGN); + signature.initSign(privateKey); + signature.update(data.getBytes(CHARSET)); + return Base64.getEncoder().encodeToString(signature.sign()); + } catch (Exception e) { + throw new RuntimeException("签名字符串[" + data + "]时遇到异常", e); + } + } + + /** + * RSA算法使用公钥校验数字签名 + * + * @param data 参与签名的明文字符串 + * @param key RSA公钥字符串 + * @param sign RSA签名得到的经过Base64编码的字符串 + * @return true--验签通过,false--验签未通过 + */ + public static boolean buildRSAverifyByPublicKey(String data, String key, String sign) { + try { + //通过X509编码的Key指令获得公钥对象 + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(key)); + KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_RSA); + PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); + Signature signature = Signature.getInstance(ALGORITHM_RSA_SIGN); + signature.initVerify(publicKey); + signature.update(data.getBytes(CHARSET)); + return signature.verify(Base64.getDecoder().decode(sign)); + } catch (Exception e) { + throw new RuntimeException("验签字符串[" + data + "]时遇到异常", e); + } + } + + /** + * RSA算法分段加解密数据 + * + * @param cipher 初始化了加解密工作模式后的javax.crypto.Cipher对象 + * @param opmode 加解密模式,值为javax.crypto.Cipher.ENCRYPT_MODE/DECRYPT_MODE + * @return 加密或解密后得到的数据的字节数组 + */ + private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas) { + int maxBlock = 0; + if (opmode == Cipher.DECRYPT_MODE) { + maxBlock = ALGORITHM_RSA_PRIVATE_KEY_LENGTH / 8; + } else { + maxBlock = ALGORITHM_RSA_PRIVATE_KEY_LENGTH / 8 - 11; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] buff; + int i = 0; + try { + while (datas.length > offSet) { + if (datas.length - offSet > maxBlock) { + buff = cipher.doFinal(datas, offSet, maxBlock); + } else { + buff = cipher.doFinal(datas, offSet, datas.length - offSet); + } + out.write(buff, 0, buff.length); + i++; + offSet = i * maxBlock; + } + } catch (Exception e) { + throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e); + } + byte[] resultDatas = out.toByteArray(); + IOUtils.closeQuietly(out); + return resultDatas; + } + +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/CharsetUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/CharsetUtils.java new file mode 100644 index 0000000..14beaca --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/CharsetUtils.java @@ -0,0 +1,89 @@ +package ai.remi.comm.util.string; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * 字符集工具类 + * + * @author startdis + */ +public class CharsetUtils { + /** + * ISO-8859-1 + */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** + * UTF-8 + */ + public static final String UTF_8 = "UTF-8"; + /** + * GBK + */ + public static final String GBK = "GBK"; + + /** + * ISO-8859-1 + */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** + * UTF-8 + */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** + * GBK + */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) { + if (null == srcCharset) { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() { + return Charset.defaultCharset().name(); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringConvertUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringConvertUtils.java new file mode 100644 index 0000000..630e298 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringConvertUtils.java @@ -0,0 +1,868 @@ +package ai.remi.comm.util.string; + +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * 类型转换器 + * + * @author startdis + */ +public class StringConvertUtils { + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) { + if (null == value) { + return defaultValue; + } + if (value instanceof String) { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) { + if (null == value) { + return defaultValue; + } + if (value instanceof Character) { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Byte) { + return (Byte) value; + } + if (value instanceof Number) { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Byte.parseByte(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Short) { + return (Short) value; + } + if (value instanceof Number) { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Short.parseShort(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Number) { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return NumberFormat.getInstance().parse(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Integer.parseInt(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) { + if (StringUtils.isEmpty(str)) { + return new Integer[]{}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) { + if (StringUtils.isEmpty(str)) { + return new Long[]{}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) { + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) { + return str.split(split); + } + + /** + * 字符串转数组 + * + * @param split 分隔符 + * @param str 字符串 + * @return + */ + public static List strToList(String split, String str) { + return StringUtils.isEmpty(str) ? null : Arrays.asList(str.split(split)); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Double) { + return (Double) value; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Float) { + return (Float) value; + } + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Float.parseFloat(valueStr.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof Boolean) { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) { + case "true": + case "yes": + case "ok": + case "1": + return true; + case "false": + case "no": + case "0": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) { + if (value == null) { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return Enum.valueOf(clazz, valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof BigInteger) { + return (BigInteger) value; + } + if (value instanceof Long) { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return new BigInteger(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) { + if (value == null) { + return defaultValue; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof Long) { + return new BigDecimal((Long) value); + } + if (value instanceof Double) { + return new BigDecimal((Double) value); + } + if (value instanceof Integer) { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) { + return defaultValue; + } + try { + return new BigDecimal(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) { + return str(obj, CharsetUtils.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) { + if (null == obj) { + return null; + } + + if (obj instanceof String) { + return (String) obj; + } else if (obj instanceof byte[] || obj instanceof Byte[]) { + if (obj instanceof byte[]) { + return str((byte[]) obj, charset); + } else { + Byte[] bytes = (Byte[]) obj; + int length = bytes.length; + byte[] dest = new byte[length]; + for (int i = 0; i < length; i++) { + dest[i] = bytes[i]; + } + return str(dest, charset); + } + } else if (obj instanceof ByteBuffer) { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) { + if (data == null) { + return null; + } + + if (null == charset) { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) { + if (data == null) { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) { + if (null == charset) { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') { + c[i] = '\u3000'; + } else if (c[i] < '\177') { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') { + c[i] = ' '; + } else if (c[i] > '\uFF00' && c[i] < '\uFF5F') { + c[i] = (char) (c[i] - 65248); + } + } + String returnString = new String(c); + + return returnString; + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) { + String[] fraction = {"角", "分"}; + String[] digit = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}; + String[][] unit = {{"元", "万", "亿"}, {"", "拾", "佰", "仟"}}; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) { + s += (digit[(int) (Math.floor(n * 10 * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringFormatterUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringFormatterUtils.java new file mode 100644 index 0000000..e311985 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringFormatterUtils.java @@ -0,0 +1,77 @@ +package ai.remi.comm.util.string; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * 字符串格式化 + * + * @author startdis + */ +public class StringFormatterUtils { + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) { + if (StringUtils.isEmpty(strPattern) || ObjectUtils.isEmpty(argArray)) { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) { + if (handledPosition == 0) { + return strPattern; + } else { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } else { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(StringConvertUtils.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } else { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } else { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(StringConvertUtils.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringUtils.java new file mode 100644 index 0000000..b50b6bc --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/string/StringUtils.java @@ -0,0 +1,521 @@ +package ai.remi.comm.util.string; + +import org.springframework.util.AntPathMatcher; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 字符串工具类 + * + * @author startdis + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils { + /** + * 空字符串 + */ + private static final String NULLSTR = ""; + + /** + * 下划线 + */ + private static final char SEPARATOR = '_'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + * * @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) { + return !isNull(object); + } + + /** + * 如果给定字符串{@code str}中不包含{@code appendStr},则在{@code str}后追加{@code appendStr}; + * 如果已包含{@code appendStr},则在{@code str}后追加{@code otherwise} + * + * @param str 给定的字符串 + * @param appendStr 需要追加的内容 + * @param otherwise 当{@code appendStr}不满足时追加到{@code str}后的内容 + * @return 追加后的字符串 + */ + public static String appendIfNotContain(String str, String appendStr, String otherwise) { + if (isEmpty(str) || isEmpty(appendStr)) { + return str; + } + if (str.contains(appendStr)) { + return str.concat(otherwise); + } + return str.concat(appendStr); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) { + return (str == null ? "" : str.trim()); + } + + /** + * 自定义分割函数,返回第一个 + * + * @param str 待分割的字符串 + * @param delim 分隔符 + * @return 分割后的第一个字符串 + */ + public static String splitFirst(final String str, final String delim) { + if (null == str || org.apache.commons.lang3.StringUtils.isEmpty(delim)) { + return str; + } + + int index = str.indexOf(delim); + if (index < 0) { + return str; + } + if (index == 0) { + // 一开始就是分隔符,返回空串 + return ""; + } + return str.substring(0, index); + } + + /** + * 自定义分割函数,返回全部 + * + * @param str 待分割的字符串 + * @param delim 分隔符 + * @return 分割后的返回结果 + */ + public static List splitStr(String str, final String delim) { + if (null == str) { + return new ArrayList<>(0); + } + + if (StringUtils.isEmpty(delim)) { + List result = new ArrayList<>(1); + result.add(str); + + return result; + } + + final List stringList = new ArrayList<>(); + while (true) { + int index = str.indexOf(delim); + if (index < 0) { + stringList.add(str); + break; + } + stringList.add(str.substring(0, index)); + str = str.substring(index + delim.length()); + } + return stringList; + } + + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) { + if (str == null) { + return NULLSTR; + } + + if (start < 0) { + start = str.length() + start; + } + + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) { + if (str == null) { + return NULLSTR; + } + + if (end < 0) { + end = str.length() + end; + } + if (start < 0) { + start = str.length() + start; + } + + if (end > str.length()) { + end = str.length(); + } + + if (start > end) { + return NULLSTR; + } + + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) { + if (isEmpty(params) || isEmpty(template)) { + return template; + } + return StringFormatterUtils.format(template, params); + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) { + if (str == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i > 0) { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } else { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) { + sb.append(SEPARATOR); + } else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) { + if (str != null && strs != null) { + for (String s : strs) { + if (str.equalsIgnoreCase(trim(s))) { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) { + // 没必要转换 + return ""; + } else if (!name.contains("_")) { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + List camels = splitStr(name, "_"); + for (String camel : camels) { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 例如:user_name->userName + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) { + if (isEmpty(str) || isEmpty(strs)) { + return false; + } + for (String pattern : strs) { + if (isMatch(pattern, str)) { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) { + final StringBuilder sb = new StringBuilder(size); + if (s != null) { + final int len = s.length(); + if (s.length() <= size) { + for (int i = size - len; i > 0; i--) { + sb.append(c); + } + sb.append(s); + } else { + return s.substring(len - size, len); + } + } else { + for (int i = size; i > 0; i--) { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/url/UrlIgnoreUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/url/UrlIgnoreUtils.java new file mode 100644 index 0000000..8f41a3f --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/url/UrlIgnoreUtils.java @@ -0,0 +1,31 @@ +package ai.remi.comm.util.url; + +import org.springframework.util.AntPathMatcher; + +import java.util.List; +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 请求路径忽略工具类 + */ +public class UrlIgnoreUtils { + + private static final AntPathMatcher matcher = new AntPathMatcher(); + + public static boolean isIgnoreUrl(Set ignoreUrl, String requestPath) { + return ignoreUrl.stream().anyMatch((url) -> { + //判断请求的url是否在忽略拦截的url中 + return matcher.match(url, requestPath); + }); + } + + + public static boolean isIgnoreUrl(List ignoreUrl, String requestPath) { + return ignoreUrl.stream().anyMatch((url) -> { + //判断请求的url是否在忽略拦截的url中 + return matcher.match(url, requestPath); + }); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/url/UrlMatchUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/url/UrlMatchUtils.java new file mode 100644 index 0000000..6ccbbb0 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/url/UrlMatchUtils.java @@ -0,0 +1,31 @@ +package ai.remi.comm.util.url; + +import cn.hutool.core.collection.CollectionUtil; +import org.springframework.util.AntPathMatcher; + +import java.util.Collection; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 请求路径匹配工具类 + */ +public class UrlMatchUtils { + private static final AntPathMatcher matcher = new AntPathMatcher(); + + /** + * 集合内所有正则是否包含 请求路径 + * @param patterns 所有正则 + * @param requestPath 请求路径 + * @return 返回是否在包含在内 + */ + public static boolean contain(Collection patterns, String requestPath){ + if (CollectionUtil.isEmpty(patterns)){ + return false; + } + return patterns.stream().anyMatch((pattern) -> { + //判断请求的url是否在忽略拦截的url中 + return matcher.match(pattern, requestPath); + }); + } +} diff --git a/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/xml/DOMUtils.java b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/xml/DOMUtils.java new file mode 100644 index 0000000..aaaea2e --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/java/ai/remi/comm/util/xml/DOMUtils.java @@ -0,0 +1,550 @@ +package ai.remi.comm.util.xml; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.SAXReader; +import org.dom4j.io.XMLWriter; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @date 2022-07-26 + * @desc DOMUtils + */ +public class DOMUtils { + + /** + * 添加根节点元素 + * + * @param document 根文档 + * @param rootName 根节点名称 + * @return 新增节点 + */ + public static Element addRoot(Document document, String rootName) { + // 添加子节点元素 + return document.addElement(rootName); + } + + /** + * 添加根节点元素 + * + * @param document 根文档 + * @param rootName 根节点名称 + * @param rootValue 根节点值 + * @return 新增节点 + */ + public static Element addRoot(Document document, String rootName, String rootValue) { + // 添加子节点元素 + Element root = document.addElement(rootName); + // 为子节点元素设值 + root.setText(rootValue == null ? "" : rootValue); + return root; + } + + /** + * 添加根节点元素 + * + * @param document 根文档 + * @param rootName 根节点名称 + * @param attributes 根节点属性 + * @return 新增节点 + */ + public static Element addRoot(Document document, String rootName, Map attributes) { + // 添加子节点元素 + Element root = document.addElement(rootName); + // 为子节点元素设属性 + if (MapUtils.isEmpty(attributes)) { + return root; + } + attributes.forEach((key, value) -> { + if (key.contains(":")) { + //处理冒号属性问题 + root.addNamespace(key.substring(key.indexOf(":")+1), value); + } + root.addAttribute(key, value); + }); + return root; + } + + /** + * 添加根节点元素 + * + * @param document 根文档 + * @param rootName 根节点名称 + * @param rootValue 根节点值 + * @param attributes 根节点属性 + * @return 新增节点 + */ + public static Element addRoot(Document document, String rootName, String rootValue, Map attributes) { + // 添加子节点元素 + Element root = document.addElement(rootName); + // 为子节点元素设值 + root.setText(rootValue == null ? "" : rootValue); + // 为子节点元素设属性 + if (MapUtils.isEmpty(attributes)) { + return root; + } + attributes.forEach((key, value) -> { + if (key.contains(":")) { + //处理冒号属性问题 + root.addNamespace(key.substring(key.indexOf(":")+1), value); + } + root.addAttribute(key, value); + }); + return root; + } + + /** + * 添加孩子节点元素 + * + * @param parent 父节点 + * @param childName 孩子节点名称 + * @return 新增节点 + */ + public static Element addChild(Element parent, String childName) { + // 添加子节点元素 + return parent.addElement(childName); + } + + /** + * 添加孩子节点元素 + * + * @param parent 父节点 + * @param childName 孩子节点名称 + * @param childValue 孩子节点值 + * @return 新增节点 + */ + public static Element addChild(Element parent, String childName, String childValue) { + // 添加子节点元素 + Element child = parent.addElement(childName); + // 为子节点元素设值 + child.setText(childValue == null ? "" : childValue); + return child; + } + + /** + * 添加孩子节点元素 + * + * @param parent 父节点 + * @param childName 孩子节点名称 + * @param attributes 孩子节点属性 + * @return 新增节点 + */ + public static Element addChild(Element parent, String childName, Map attributes) { + // 添加子节点元素 + Element child = parent.addElement(childName); + // 为子节点元素设属性 + if (MapUtils.isEmpty(attributes)) { + return child; + } + attributes.forEach((key, value) -> { + if (key.contains(":")) { + //处理冒号属性问题 + //child.addNamespace(key.substring(key.indexOf(":")+1), value); + } + child.addAttribute(key, value); + }); + return child; + } + + /** + * 添加孩子节点元素 + * + * @param parent 父节点 + * @param childName 孩子节点名称 + * @param childValue 孩子节点值 + * @param attributes 孩子节点属性 + * @return 新增节点 + */ + public static Element addChild(Element parent, String childName, String childValue, Map attributes) { + // 添加子节点元素 + Element child = parent.addElement(childName); + // 为子节点元素设值 + child.setText(childValue == null ? "" : childValue); + // 为子节点元素设属性 + if (MapUtils.isEmpty(attributes)) { + return child; + } + attributes.forEach((key, value) -> { + if (key.contains(":")) { + //处理冒号属性问题 + //child.addNamespace(key.substring(key.indexOf(":")+1), value); + } + child.addAttribute(key, value); + }); + return child; + } + + /** + * 将文档对象写入对应的文件中 + * + * @param xmlstr 字符串 + * @param path 写入文档的路径 + * @throws IOException + */ + public final static void writeXmlStrToFile(String xmlstr, String path) throws Exception { + if (StringUtils.isBlank(xmlstr)) { + return; + } + Document document = DocumentHelper.parseText(xmlstr); + writeXMLToFile(document, path); + } + + /** + * 将文档对象写入对应的文件中 + * + * @param document 文档对象 + * @param path 写入文档的路径 + * @throws IOException + */ + public final static void writeXMLToFile(Document document, String path) throws IOException { + if (document == null || path == null) { + return; + } + XMLWriter writer = new XMLWriter(new FileWriter(path)); + writer.write(document); + writer.close(); + } + + /** + * 将文档对象写入对应的文件中 + * + * @param document 文档对象 + * @param path 写入文档的路径 + * @throws IOException + */ + public final static void writeXMLToFile(Document document, String path, String encoding) throws IOException { + if (document == null || path == null) { + return; + } + // 设置生成xml的格式 + OutputFormat format = OutputFormat.createPrettyPrint(); + encoding = StringUtils.isBlank(encoding) ? "UTF-8" : encoding; + // 设置编码格式 + format.setEncoding(encoding); + XMLWriter writer = new XMLWriter(new FileWriter(path), format); + writer.write(document); + writer.close(); + } + + /** + * 通过文件的路径获取xml的document对象 + * + * @param path 文件的路径 + * @return 返回文档对象 + */ + public static Document getXMLByFilePath(String path) { + if (null == path) { + return null; + } + Document document = null; + try { + SAXReader reader = new SAXReader(); + document = reader.read(new File(path)); + } catch (Exception e) { + e.printStackTrace(); + } + return document; + } + + /** + * 通过xml字符获取document文档 + * + * @param xmlstr 要序列化的xml字符 + * @return 返回文档对象 + * @throws DocumentException + */ + public static Document getXMLByString(String xmlstr) throws DocumentException { + if (xmlstr == "" || xmlstr == null) { + return null; + } + Document document = DocumentHelper.parseText(xmlstr); + return document; + } + + /** + * 获取某个元素的所有的子节点 + * + * @param node 制定节点 + * @return 返回所有的子节点 + */ + public static List getChildElements(Element node) { + if (null == node) { + return null; + } + @SuppressWarnings("unchecked") + List lists = node.elements(); + return lists; + } + + /** + * 获取指定节点的子节点 + * + * @param node 父节点 + * @param childnode 指定名称的子节点 + * @return 返回指定的子节点 + */ + public static Element getChildElement(Element node, String childnode) { + if (null == node || null == childnode || "".equals(childnode)) { + return null; + } + return node.element(childnode); + } + + /** + * 获取所有的属性值 + * + * @param node + * @param arg + * @return + */ + public static Map getAttributes(Element node, String... arg) { + if (node == null || arg.length == 0) { + return null; + } + Map attrMap = new HashMap(); + for (String attr : arg) { + String attrValue = node.attributeValue(attr); + attrMap.put(attr, attrValue); + } + return attrMap; + } + + /** + * 获取element的单个属性 + * + * @param node 需要获取属性的节点对象 + * @param attr 需要获取的属性值 + * @return 返回属性的值 + */ + public static String getAttribute(Element node, String attr) { + if (null == node || attr == null || "".equals(attr)) { + return ""; + } + return node.attributeValue(attr); + } + + /** + * DOM4j的Document对象转为XML报文串 + * + * @param document + * @param charset + * @return 经过解析后的xml字符串 + */ + public static String documentToString(Document document, String charset) { + StringWriter stringWriter = new StringWriter(); + OutputFormat format = OutputFormat.createPrettyPrint();// 获得格式化输出流 + format.setEncoding(charset);// 设置字符集,默认为UTF-8 + XMLWriter xmlWriter = new XMLWriter(stringWriter, format);// 写文件流 + try { + xmlWriter.write(document); + xmlWriter.flush(); + xmlWriter.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return stringWriter.toString(); + } + + /** + * 去掉声明头的(即去掉) + * + * @param document + * @param charset + * @return + */ + public static String documentToStringNoDeclaredHeader(Document document, String charset) { + String xml = documentToString(document, charset); + return xml.replaceFirst("\\s*<[^<>]+>\\s*", ""); + } + + /** + * 解析XML为Document对象 + * + * @param xml 被解析的XMl + * @return Document + */ + public final static Element parseXml(String xml) { + StringReader sr = new StringReader(xml); + SAXReader saxReader = new SAXReader(); + Document document = null; + try { + document = saxReader.read(sr); + } catch (DocumentException e) { + e.printStackTrace(); + } + Element rootElement = document != null ? document.getRootElement() : null; + return rootElement; + } + + /** + * 获取element对象的text的值 + * + * @param e 节点的对象 + * @param tac 节点的tac + * @return + */ + public final static String getText(Element e, String tac) { + Element _e = e.element(tac); + if (_e != null) { + return _e.getText(); + } else { + return null; + } + } + + /** + * 获取去除空格的字符串 + * + * @param e + * @param tac + * @return + */ + public final static String getTextTrim(Element e, String tac) { + Element _e = e.element(tac); + if (_e != null) { + return _e.getTextTrim(); + } else { + return null; + } + } + + /** + * 获取节点值.节点必须不能为空,否则抛错 + * + * @param parent 父节点 + * @param tac 想要获取的子节点 + * @return 返回子节点 + */ + public final static String getTextTrimNotNull(Element parent, String tac) { + Element e = parent.element(tac); + if (e == null) { + throw new NullPointerException("节点为空"); + } else { + return e.getTextTrim(); + } + } + + /** + * 节点必须不能为空,否则抛错 + * + * @param parent 父节点 + * @param tac 想要获取的子节点 + * @return 子节点 + */ + public final static Element elementNotNull(Element parent, String tac) { + Element e = parent.element(tac); + if (e == null) { + throw new NullPointerException("节点为空"); + } else { + return e; + } + } + + /** + * 将对象直接转换成String类型的 XML输出 + * + * @param obj + * @return + */ + public static String convertToXml(Object obj) { + // 创建输出流 + StringWriter sw = new StringWriter(); + try { + // 利用jdk中自带的转换类实现 + JAXBContext context = JAXBContext.newInstance(obj.getClass()); + + Marshaller marshaller = context.createMarshaller(); + // 格式化xml输出的格式 + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, + Boolean.TRUE); + // 将对象转换成输出流形式的xml + marshaller.marshal(obj, sw); + } catch (JAXBException e) { + e.printStackTrace(); + } + return sw.toString(); + } + + /** + * 将对象根据路径转换成xml文件 + * + * @param obj + * @param path + * @return + */ + public static void convertToXml(Object obj, String path) { + try { + // 利用jdk中自带的转换类实现 + JAXBContext context = JAXBContext.newInstance(obj.getClass()); + + Marshaller marshaller = context.createMarshaller(); + // 格式化xml输出的格式 + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, + Boolean.TRUE); + // 将对象转换成输出流形式的xml + // 创建输出流 + FileWriter fw = null; + try { + fw = new FileWriter(path); + } catch (IOException e) { + e.printStackTrace(); + } + marshaller.marshal(obj, fw); + } catch (JAXBException e) { + e.printStackTrace(); + } + } + + /** + * 将String类型的xml转换成对象 + */ + public static Object convertXmlStrToObject(Class clazz, String xmlStr) { + Object xmlObject = null; + try { + JAXBContext context = JAXBContext.newInstance(clazz); + // 进行将Xml转成对象的核心接口 + Unmarshaller unmarshaller = context.createUnmarshaller(); + StringReader sr = new StringReader(xmlStr); + xmlObject = unmarshaller.unmarshal(sr); + } catch (JAXBException e) { + e.printStackTrace(); + } + return xmlObject; + } + + /** + * 将file类型的xml转换成对象 + */ + public static Object convertXmlFileToObject(Class clazz, String xmlPath) { + Object xmlObject = null; + try { + JAXBContext context = JAXBContext.newInstance(clazz); + Unmarshaller unmarshaller = context.createUnmarshaller(); + FileReader fr = null; + try { + fr = new FileReader(xmlPath); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + xmlObject = unmarshaller.unmarshal(fr); + } catch (JAXBException e) { + e.printStackTrace(); + } + return xmlObject; + } +} diff --git a/remi-comm/remi-comm-util/src/main/resources/ip2region.xdb b/remi-comm/remi-comm-util/src/main/resources/ip2region.xdb new file mode 100644 index 0000000..9f6502b Binary files /dev/null and b/remi-comm/remi-comm-util/src/main/resources/ip2region.xdb differ diff --git a/remi-comm/remi-comm-util/src/main/resources/rebel.xml b/remi-comm/remi-comm-util/src/main/resources/rebel.xml new file mode 100644 index 0000000..151b157 --- /dev/null +++ b/remi-comm/remi-comm-util/src/main/resources/rebel.xml @@ -0,0 +1,16 @@ + + + + + + remi-comm-util + + + + + + + diff --git a/remi-comm/remi-comm-web/pom.xml b/remi-comm/remi-comm-web/pom.xml new file mode 100644 index 0000000..e56e81f --- /dev/null +++ b/remi-comm/remi-comm-web/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + remi-comm-web + remi-comm-web + remi-comm-web + + + ai.remi.top.cloud + remi-comm + 3.1.1 + + + + + + maven-releases + release + https://repository.njydsz.com/repository/maven-releases/ + + + maven-snapshots + snapshot + https://repository.njydsz.com/repository/maven-snapshots/ + + + + + + + ai.remi.top.cloud + remi-comm-core + + + + ai.remi.top.cloud + remi-comm-jdbc + + + + ai.remi.top.cloud + remi-comm-util + + + + ai.remi.top.cloud + remi-comm-exception + + + + ai.remi.top.cloud + remi-comm-swagger + + + + org.springframework.boot + spring-boot-starter-web + + + + org.apache.skywalking + apm-toolkit-trace + + + + org.projectlombok + lombok + + + + org.slf4j + slf4j-simple + + + org.slf4j + slf4j-nop + + + + diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/AuthHandler.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/AuthHandler.java new file mode 100644 index 0000000..5fc611c --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/AuthHandler.java @@ -0,0 +1,15 @@ +package ai.remi.comm.web.auth; + +import ai.remi.comm.util.auth.AuthInfo; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc AuthHandler + */ +public interface AuthHandler { + AuthInfo getAuthInfo(HttpServletRequest request, HttpServletResponse response); +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/AuthHandlerFactory.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/AuthHandlerFactory.java new file mode 100644 index 0000000..6e59218 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/AuthHandlerFactory.java @@ -0,0 +1,29 @@ +package ai.remi.comm.web.auth; + +import ai.remi.comm.core.enums.ServiceTypeEnum; +import ai.remi.comm.web.excel.SpringContextUtil; + +import java.util.Map; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc AuthHandlerFactory + */ +public class AuthHandlerFactory { + + public static AuthHandler getAuthHandler(ServiceTypeEnum serviceTypeEnum) { + //Map authHandlerMap = SpringUtil.getBeansOfType(AuthHandler.class); + Map authHandlerMap = SpringContextUtil.getApplicationContext().getBeansOfType(AuthHandler.class); + //AssertUtils.check(CollectionUtil.isNotEmpty(authHandlerMap), "AuthHandler 服务未注册"); + switch (serviceTypeEnum) { + case WEB_SERVICE: + return authHandlerMap.get("webAuthHandler"); + case APP_SERVICE: + return authHandlerMap.get("appAuthHandler"); + default: + return authHandlerMap.get("defaultAuthHandler"); + } + } + +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/WebAuthHandler.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/WebAuthHandler.java new file mode 100644 index 0000000..5b1d600 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/auth/WebAuthHandler.java @@ -0,0 +1,47 @@ +package ai.remi.comm.web.auth; + + +import com.google.common.collect.Sets; +import ai.remi.comm.core.constant.HeaderConstant; +import ai.remi.comm.core.enums.PermissionScopeEnum; +import ai.remi.comm.util.auth.AuthInfo; +import ai.remi.comm.util.auth.CompanyAuthInfo; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc B端集团公司验证类 + */ +@Component +public class WebAuthHandler implements AuthHandler { + + @Override + public AuthInfo getAuthInfo(HttpServletRequest request, HttpServletResponse response) { + String companyTenantId = request.getHeader(HeaderConstant.X_COMPANY_TENANT_ID); + String deptTenantId = request.getHeader(HeaderConstant.X_DEPT_TENANT_ID); + String authToken = request.getHeader(HeaderConstant.X_ACCESS_TOKEN); + String userId = request.getHeader(HeaderConstant.X_UNIQUE_ID); + String language = request.getHeader(HeaderConstant.X_USER_LANGUAGE); + String permissionScopeCode = request.getHeader(HeaderConstant.X_PERMISSION_SCOPE); + PermissionScopeEnum permissionScopeEnum = StringUtils.isBlank(permissionScopeCode)?null: + PermissionScopeEnum.codeOf(permissionScopeCode); + Set companyIds = Sets.newHashSet(request.getHeaders(HeaderConstant.X_COMPANY_IDS).asIterator()); + Set deptIds = Sets.newHashSet(request.getHeaders(HeaderConstant.X_DEPT_IDS).asIterator()); + CompanyAuthInfo companyAuthInfo = new CompanyAuthInfo(); + companyAuthInfo.setCompanyTenantId(companyTenantId); + companyAuthInfo.setDeptTenantId(deptTenantId); + companyAuthInfo.setAccessToken(authToken); + companyAuthInfo.setUniqueId(userId); + companyAuthInfo.setUserLanguage(language); + companyAuthInfo.setPermissionScope(permissionScopeEnum); + companyAuthInfo.setHasPermissionCompanyIds(companyIds); + companyAuthInfo.setHasPermissionDeptIds(deptIds); + return companyAuthInfo; + } +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/SpringConfig.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/SpringConfig.java new file mode 100644 index 0000000..91dac27 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/SpringConfig.java @@ -0,0 +1,27 @@ +package ai.remi.comm.web.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.sql.DataSource; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 设置项目默认时区 + */ +@Configuration +//@ComponentScan("ai.remi") +//@PropertySource("classpath:jdbc.properties") +//spring中开启事务保证事务的完整性 +@EnableTransactionManagement +public class SpringConfig { + + @Bean(name = "transactionManager") + public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource){ + return new DataSourceTransactionManager(dataSource); + } + +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/TimezoneConfig.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/TimezoneConfig.java new file mode 100644 index 0000000..2badd5c --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/TimezoneConfig.java @@ -0,0 +1,24 @@ +package ai.remi.comm.web.config; + +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import java.util.TimeZone; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 设置项目默认时区 + */ +@Configuration +public class TimezoneConfig { + + @PostConstruct + public void defaultTimeZone() { + // 设置用户时区为 UTC + //TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + // 设置用户时区为 Asia/Shanghai + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); + } + +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/UserAgentConfig.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/UserAgentConfig.java new file mode 100644 index 0000000..c27c36f --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/UserAgentConfig.java @@ -0,0 +1,33 @@ +package ai.remi.comm.web.config; + +import io.github.mngsk.devicedetector.Detection; +import io.github.mngsk.devicedetector.DeviceDetector; +import io.github.mngsk.devicedetector.device.Device; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc user-agent解析器 + */ +@Configuration +public class UserAgentConfig { + + /** + * 初始化user-agent解析器 + */ + @Bean + public DeviceDetector deviceDetector() { + return new DeviceDetector.DeviceDetectorBuilder().build(); + } + + public static void main(String[] args) { + DeviceDetector dd = new DeviceDetector.DeviceDetectorBuilder().build(); + String str = "Mozilla/5.0 (Linux; U; Android 8.0.0; zh-CN; BAC-AL00 Build/HUAWEIBAC-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/11.9.4.974 UWS/2.13.1.48 Mobile Safari/537.36 AliApp(DingTalk/4.5.11) com.alibaba.android.rimet/10487439 Channel/227200 language/zh-CN"; + Detection detect = dd.detect(str); + Device device = detect.getDevice().get(); + System.out.println(device.getModel().get()); + } +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/WebMvcConfig.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/WebMvcConfig.java new file mode 100644 index 0000000..ee7b36b --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/config/WebMvcConfig.java @@ -0,0 +1,209 @@ +package ai.remi.comm.web.config; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.DateSerializer; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import ai.remi.comm.core.constant.Constants; +import ai.remi.comm.exception.custom.BusinessException; +import ai.remi.comm.web.filter.AuthFilter; +import ai.remi.comm.web.interceptor.HttpInterceptor; +import ai.remi.comm.web.interceptor.TenantInterceptor; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; +import java.io.IOException; +import java.lang.reflect.Field; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc WebMvcConfig + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + /** + * 防抖过滤器 + */ + //@Autowired + + //private ResubmitFilter resubmitFilter; + + /** + * 租户拦截器 + */ + @Resource + private TenantInterceptor tenantInterceptor; + + /** + * Http拦截器 + */ + @Resource + private HttpInterceptor httpInterceptor; + + /** + * 防抖拦截器 + */ + //@Autowired + //private ResubmitInterceptor resubmitInterceptor; + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(tenantInterceptor).addPathPatterns("/**").order(Integer.MIN_VALUE); + registry.addInterceptor(httpInterceptor).addPathPatterns("/**").order(Integer.MAX_VALUE); + //registry.addInterceptor(resubmitInterceptor).addPathPatterns("/**").order(Integer.MAX_VALUE); + } + + /** + * 解决跨域请求 方式一: + */ + //@Override + //public void addCorsMappings(CorsRegistry registry) { + // //项目中的所有接口都支持跨域 + // registry.addMapping("/**") + // //是否支持跨域Cookie + // .allowCredentials(true) + // //配置访问的地址,*代表所有的都可以访问 + // //.allowedOrigins("http://localhost:8080") + // .allowedOriginPatterns(CorsConfiguration.ALL) + // //允许的请求头参数 + // .allowedHeaders(CorsConfiguration.ALL) + // //允许的请求方式 "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" + // .allowedMethods(CorsConfiguration.ALL) + // // 跨域允许时间 + // .maxAge(3600); + // + // WebMvcConfigurer.super.addCorsMappings(registry); + //} + + /** + * 解决跨域请求 方式二:可指定拦截器执行顺序 + */ + @Bean + public FilterRegistrationBean corsFilter() { + CorsConfiguration corsConfig = new CorsConfiguration(); + //是否支持跨域Cookie + corsConfig.setAllowCredentials(true); + //配置访问的地址,*代表所有的都可以访问 + //corsConfig.addAllowedOrigin("http://localhost:8080"); + corsConfig.addAllowedOriginPattern(CorsConfiguration.ALL); + //允许的请求头参数 + corsConfig.addAllowedHeader(CorsConfiguration.ALL); + //允许的请求头参数 + corsConfig.addAllowedMethod(CorsConfiguration.ALL); + //默认可不设置这个暴露的头。这个为了安全问题,不能使用*。设置成*,后面会报错:throw new IllegalArgumentException("'*' is not a valid exposed header value"); + //corsConfig.addExposedHeader(""); + corsConfig.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); + configSource.registerCorsConfiguration("/**", corsConfig); + + FilterRegistrationBean corsBean = new FilterRegistrationBean(new CorsFilter(configSource)); + corsBean.setName("crossOriginFilter"); + //这个顺序也有可能会有影响,尽量设置在拦截器前面 + corsBean.setOrder(0); + return corsBean; + } + + /** + * 自定义全局序列化方式 + */ + @Bean + public HttpMessageConverters httpMessageConverters() { + MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = httpMessageConverter.getObjectMapper(); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + SimpleModule simpleModule = new SimpleModule(); + //序列化日期格式 + simpleModule.addSerializer(Date.class, new DateSerializer(false, simpleDateFormat)); + //Long序列化为String + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); + //字符串转String + simpleModule.addDeserializer(Date.class, new JsonDeserializer() { + @Override + public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + String date = jsonParser.getText(); + try { + return simpleDateFormat.parse(date); + } catch (ParseException e) { + throw new BusinessException(Constants.ERROR, e.getMessage()); + } + } + }); + //反序列化日期格式 + objectMapper.registerModule(simpleModule); + //序列化多余字段不报错 + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + //序列化地区 + objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + //如果值为null时字段key还是否输出:ALWAYS(总是输出),NON_NULL(为null的不输出) + objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer() { + @Override + public void serialize(Object o, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException { + String fieldName = gen.getOutputContext().getCurrentName(); + try { + //反射获取字段类型 + Field field = gen.getCurrentValue().getClass().getDeclaredField(fieldName); + if (Objects.equals(field.getType(), String.class)) { + //字符串型空值"" + gen.writeString(""); + return; + } else if (Objects.equals(field.getType(), List.class)) { + //列表型空值返回[] + gen.writeStartArray(); + gen.writeEndArray(); + return; + } else if (Objects.equals(field.getType(), Map.class)) { + //map型空值返回{} + gen.writeStartObject(); + gen.writeEndObject(); + return; + } + } catch (NoSuchFieldException e) { + } + //默认返回"" + gen.writeString(""); + } + }); + return new HttpMessageConverters(httpMessageConverter); + } + + /*@Bean + public FilterRegistrationBean filterRegistrationBean() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean(resubmitFilter); + //过滤所有路径 + registrationBean.addUrlPatterns("/*"); + registrationBean.setName("resubmitFilter"); + registrationBean.setOrder(1); + return registrationBean; + }*/ + + @Bean + public FilterRegistrationBean authFilter() { + AuthFilter authFilter = new AuthFilter(); + FilterRegistrationBean authFilterBean = new FilterRegistrationBean<>(authFilter); + //过滤所有路径 + authFilterBean.addUrlPatterns("/*"); + authFilterBean.setName("authFilter"); + authFilterBean.setOrder(3); + return authFilterBean; + } +} \ No newline at end of file diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/EasyExcelUtil.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/EasyExcelUtil.java new file mode 100644 index 0000000..7ab8c1d --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/EasyExcelUtil.java @@ -0,0 +1,67 @@ +package ai.remi.comm.web.excel; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.write.metadata.WriteSheet; +import ai.remi.comm.core.excel.ExcelSelected; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +public class EasyExcelUtil { + /** + * 创建即将导出的sheet页(sheet页中含有带下拉框的列) + * @param head 导出的表头信息和配置 + * @param sheetNo sheet索引 + * @param sheetName sheet名称 + * @param 泛型 + * @return sheet页 + */ + public static WriteSheet writeSelectedSheet(Class head, Integer sheetNo, String sheetName) { + Map selectedMap = resolveSelectedAnnotation(head); + + return EasyExcel.writerSheet(sheetNo, sheetName) + .head(head) + .registerWriteHandler(new SelectedSheetWriteHandler(selectedMap)) + .build(); + } + + /** + * 解析表头类中的下拉注解 + * @param head 表头类 + * @param 泛型 + * @return Map<下拉框列索引, 下拉框内容> map + */ + private static Map resolveSelectedAnnotation(Class head) { + Map selectedMap = new HashMap<>(); + + // getDeclaredFields(): 返回全部声明的属性;getFields(): 返回public类型的属性 + Field[] fields = head.getDeclaredFields(); + for (int i = 0; i < fields.length; i++){ + Field field = fields[i]; + // 解析注解信息 + ExcelSelected selected = field.getAnnotation(ExcelSelected.class); + ExcelProperty property = field.getAnnotation(ExcelProperty.class); + if (selected != null) { + ExcelSelectedResolve excelSelectedResolve = new ExcelSelectedResolve(); + String[] source = excelSelectedResolve.resolveSelectedSource(selected); + if (source != null && source.length > 0){ + excelSelectedResolve.setSource(source); + excelSelectedResolve.setFirstRow(selected.firstRow()); + excelSelectedResolve.setLastRow(selected.lastRow()); + if (property != null && property.index() >= 0){ + selectedMap.put(property.index(), excelSelectedResolve); + } else { + selectedMap.put(i, excelSelectedResolve); + } + } + } + } + + return selectedMap; + } + +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/ExcelSelectedResolve.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/ExcelSelectedResolve.java new file mode 100644 index 0000000..e6212da --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/ExcelSelectedResolve.java @@ -0,0 +1,55 @@ +package ai.remi.comm.web.excel; + +import ai.remi.comm.core.excel.ExcelDynamicSelect; +import ai.remi.comm.core.excel.ExcelSelected; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + + +//自定义注解解析 +@Data +@Slf4j +public class ExcelSelectedResolve { + /** + * 下拉内容 + */ + private String[] source; + + /** + * 设置下拉框的起始行,默认为第二行 + */ + private int firstRow; + + /** + * 设置下拉框的结束行,默认为最后一行 + */ + private int lastRow; + + public String[] resolveSelectedSource(ExcelSelected excelSelected) { + if (excelSelected == null) { + return null; + } + + // 获取固定下拉框的内容 + String[] source = excelSelected.source(); + if (source.length > 0) { + return source; + } + + // 获取动态下拉框的内容 + Class[] classes = excelSelected.sourceClass(); + if (classes.length > 0) { + try { + ExcelDynamicSelect excelDynamicSelect = classes[0].newInstance(); + String[] dynamicSelectSource = excelDynamicSelect.getSource(); + if (dynamicSelectSource != null && dynamicSelectSource.length > 0) { + return dynamicSelectSource; + } + } catch (InstantiationException | IllegalAccessException e) { + log.error("解析动态下拉框数据异常", e); + } + } + return null; + } + +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/SelectedSheetWriteHandler.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/SelectedSheetWriteHandler.java new file mode 100644 index 0000000..f578562 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/SelectedSheetWriteHandler.java @@ -0,0 +1,53 @@ +package ai.remi.comm.web.excel; + + +import com.alibaba.excel.write.handler.SheetWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddressList; + +import java.util.Map; + +@Data +@AllArgsConstructor +public class SelectedSheetWriteHandler implements SheetWriteHandler { + private final Map selectedMap; + + /** + * Called before create the sheet + */ + @Override + public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + + } + + /** + * Called after the sheet is created + */ + @Override + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + // 这里可以对cell进行任何操作 + Sheet sheet = writeSheetHolder.getSheet(); + DataValidationHelper helper = sheet.getDataValidationHelper(); + selectedMap.forEach((k, v) -> { + // 设置下拉列表的行: 首行,末行,首列,末列 + CellRangeAddressList rangeList = new CellRangeAddressList(v.getFirstRow(), v.getLastRow(), k, k); + // 设置下拉列表的值 + DataValidationConstraint constraint = helper.createExplicitListConstraint(v.getSource()); + // 设置约束 + DataValidation validation = helper.createValidation(constraint, rangeList); + // 阻止输入非下拉选项的值 + validation.setErrorStyle(DataValidation.ErrorStyle.STOP); + validation.setShowErrorBox(true); + validation.setSuppressDropDownArrow(true); + validation.createErrorBox("提示", "请输入下拉选项中的内容"); + sheet.addValidationData(validation); + }); + } +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/SpringContextUtil.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/SpringContextUtil.java new file mode 100644 index 0000000..571c650 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/excel/SpringContextUtil.java @@ -0,0 +1,33 @@ +package ai.remi.comm.web.excel; + + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class SpringContextUtil implements ApplicationContextAware { + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringContextUtil.applicationContext = applicationContext; + } + + // 获取ApplicationContext + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + // 通过class获取Bean + public static T getBean(Class clazz) { + return applicationContext.getBean(clazz); + } + + // 通过name以及class获取Bean + public static T getBean(String name, Class clazz) { + return applicationContext.getBean(name, clazz); + } + +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/exception/GlobalExceptionHandler.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..85d4f29 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,168 @@ +package ai.remi.comm.web.exception; + +import ai.remi.comm.core.result.ResultBean; +import ai.remi.comm.exception.custom.BusinessException; +import ai.remi.comm.exception.enums.CommExceptionCode; +import ai.remi.comm.exception.util.MessageUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MaxUploadSizeExceededException; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.stream.Collectors; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc GlobalExceptionHandler + */ +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + /** + * 全局异常 + * + * @return R + */ + @ResponseBody + @ExceptionHandler({Exception.class, Throwable.class}) + public ResultBean exception(Exception e) { + log.error("全局异常信息 ==> {}", e); + return ResultBean.error(MessageUtils.getMessage("system.error")); + } + + /** + * 系统异常 + * + * @return R + */ + @ResponseBody + @ExceptionHandler(RuntimeException.class) + public ResultBean runtimeException(Exception e) { + log.error("全局系统异常信息 ==> {}", e); + return ResultBean.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ResponseBody + @ExceptionHandler(BusinessException.class) + public ResultBean businessException(BusinessException e) { + log.error("全局业务异常信息 ==>{}:{}", e.getCode(), e.getMessage()); + return ResultBean.error(e.getCode(), e.getMessage()); + } + + /** + * 上传异常 + */ + @ResponseBody + @ExceptionHandler(MaxUploadSizeExceededException.class) + //@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResultBean handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) { + log.error("文件上传太大异常 ==> {}", e.getMessage(), e); + return ResultBean.error("Upload file too large"); + } + + /** + * 非法参数异常 + */ + @ResponseBody + @ExceptionHandler(IllegalArgumentException.class) + //@ResponseStatus(HttpStatus.BAD_REQUEST) + public ResultBean illegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常 ==> {}", e.getMessage(), e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } + + /** + * 处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常,详情继续往下看代码 + */ + @ResponseBody + @ExceptionHandler(BindException.class) + //@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + public ResultBean bindExceptionHandler(BindException e) { + String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(", ")); + log.error("绑定异常 ==> {}", message, e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } + + /** + * 处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException + */ + @ResponseBody + @ExceptionHandler(ConstraintViolationException.class) + //@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + public ResultBean constraintViolationExceptionHandler(ConstraintViolationException e) { + String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(", ")); + log.error("请求参数格式错误 ==> {}", message, e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } + + /** + * 处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。 + */ + @ResponseBody + @ExceptionHandler(MethodArgumentNotValidException.class) + //@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + public ResultBean methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(", ")); + log.error("请求参数格式错误 ==> {}", message, e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } + + /** + * 处理请求数据格式异常,比如JSON格式错误。 + */ + @ResponseBody + @ExceptionHandler(HttpMessageNotReadableException.class) + //@ResponseStatus(HttpStatus.BAD_REQUEST) + public ResultBean httpMessageNotReadableExceptionHandler(HttpMessageNotReadableException e) { + String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage(); + log.error("请求数据格式错误 ==> {}", message, e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } + + /** + * 缺少请求头参数 + */ + @ResponseBody + @ExceptionHandler(MissingRequestHeaderException.class) + //@ResponseStatus(HttpStatus.BAD_REQUEST) + public ResultBean missingRequestHeaderException(MissingRequestHeaderException e) { + String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage(); + log.error("缺少请求头参数 ==> {}", message, e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } + + + @ResponseBody + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + //@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public ResultBean httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage(); + log.error("请求方法类型错误 ==> {}", message, e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } + + @ResponseBody + @ExceptionHandler(MissingServletRequestParameterException.class) + //@ResponseStatus(HttpStatus.BAD_REQUEST) + public ResultBean missingServletRequestParameterException(MissingServletRequestParameterException e) { + String message = String.format("参数 %s 不存在", e.getParameterName()); + log.error("请求参数不存在 ==> {}", message, e); + return ResultBean.error(CommExceptionCode.ILLEGAL_ARGUMENT.getCode(), MessageUtils.getMessage(CommExceptionCode.ILLEGAL_ARGUMENT.getKey()), null); + } +} + + diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/filter/AuthFilter.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/filter/AuthFilter.java new file mode 100644 index 0000000..1a46ea4 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/filter/AuthFilter.java @@ -0,0 +1,80 @@ +package ai.remi.comm.web.filter; + + +import com.alibaba.fastjson.JSON; +import ai.remi.comm.core.config.AuthFilterConfig; +import ai.remi.comm.core.constant.FilterIgnoreConstant; +import ai.remi.comm.core.constant.HeaderConstant; +import ai.remi.comm.core.enums.ServiceTypeEnum; +import ai.remi.comm.util.auth.AuthInfo; +import ai.remi.comm.util.auth.RequestHolder; +import ai.remi.comm.util.url.UrlIgnoreUtils; +import ai.remi.comm.web.auth.AuthHandler; +import ai.remi.comm.web.auth.AuthHandlerFactory; +import ai.remi.comm.web.excel.SpringContextUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 全局验证过滤器 + */ +@Slf4j +public class AuthFilter implements Filter { + + + /** + * 获取验证信息 并存储到 ThreadLocal 中,可通过 {@link RequestHolder#getAuthInfo()} } 获取验证信息 + * + * @param servletRequest 请求数据 + * @param servletResponse 响应数据 + * @param filterChain 过滤器链 + */ + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + String servletPath = request.getServletPath(); + +// String applicationName = SpringUtil.getApplicationName(); + String applicationName = SpringContextUtil.getApplicationContext().getApplicationName(); + boolean ignoreService = FilterIgnoreConstant.AUTH_FILTER_IGNORE_SERVICE_NAME.contains(applicationName); + if (ignoreService) { + filterChain.doFilter(request, servletResponse); + return; + } + + AuthFilterConfig authFilterConfig = SpringContextUtil.getBean(AuthFilterConfig.class); +// AuthFilterConfig authFilterConfig = SpringUtil.getBean(AuthFilterConfig.class); + Set ignoreUrl = FilterIgnoreConstant.FILTER_COMMON_IGNORE_URL; + List commonIgnoreUrl = authFilterConfig.getCommonIgnoreUrl(); + List customIgnoreUrl = authFilterConfig.getCustomIgnoreUrl(); + ignoreUrl.addAll(commonIgnoreUrl); + ignoreUrl.addAll(customIgnoreUrl); + boolean ignoredUrl = UrlIgnoreUtils.isIgnoreUrl(ignoreUrl, servletPath); + if (ignoredUrl) { + filterChain.doFilter(request, servletResponse); + return; + } + HttpServletResponse response = (HttpServletResponse) servletResponse; + String serviceType = request.getHeader(HeaderConstant.X_SERVICE_TYPE); + log.info("X_SERVICE_TYPE:{}", serviceType); + if (StringUtils.isEmpty(serviceType)){ + serviceType = ServiceTypeEnum.WEB_SERVICE.getCode(); + } + AuthHandler authHandler = AuthHandlerFactory.getAuthHandler(ServiceTypeEnum.codeOf(serviceType)); + AuthInfo authInfo = authHandler.getAuthInfo(request, response); + log.debug("当前请求路径:{},当前请求认证信息:{}", servletPath, JSON.toJSONString(authInfo)); + RequestHolder.add(authInfo); + RequestHolder.add(request); + filterChain.doFilter(request, response); + } +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/interceptor/HttpInterceptor.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/interceptor/HttpInterceptor.java new file mode 100644 index 0000000..90e5f53 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/interceptor/HttpInterceptor.java @@ -0,0 +1,31 @@ +package ai.remi.comm.web.interceptor; + +import ai.remi.comm.util.auth.RequestHolder; +import ai.remi.comm.util.auth.TenantHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc 主要用于请求结束 清除当前线程相关数据 清除所有 ThreadLocal + */ +@Component +public class HttpInterceptor implements HandlerInterceptor { + /** + * This implementation is empty. + * + * @param request + * @param response + * @param handler + * @param ex + */ + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + RequestHolder.remove(); + TenantHolder.remove(); + } +} diff --git a/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/interceptor/TenantInterceptor.java b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/interceptor/TenantInterceptor.java new file mode 100644 index 0000000..5e62236 --- /dev/null +++ b/remi-comm/remi-comm-web/src/main/java/ai/remi/comm/web/interceptor/TenantInterceptor.java @@ -0,0 +1,61 @@ +package ai.remi.comm.web.interceptor; + +import com.google.common.base.Splitter; +import com.google.common.collect.Sets; +import ai.remi.comm.core.constant.FilterIgnoreConstant; +import ai.remi.comm.util.auth.TenantHolder; +import ai.remi.comm.util.url.UrlIgnoreUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Set; + +/** + * @author dianjiu【公众号 点九开源】 + * @email dianjiuxyz@gmail.com + * @desc TenantInterceptor + */ +@Slf4j +@Component +public class TenantInterceptor implements HandlerInterceptor { + + + /** + * 不拦截的url + */ + private Set ignoreUrlSet = Sets.newConcurrentHashSet(); + + + + @Value("${sql-intercept.tenant.anonUrlSet:''}") + private String anonUrlSet; + + @PostConstruct + public void init() { + ignoreUrlSet.addAll(FilterIgnoreConstant.FILTER_COMMON_IGNORE_URL); + List exclusionUrlList = + Splitter.on(",") + .trimResults() + .omitEmptyStrings() + .splitToList(anonUrlSet); + ignoreUrlSet.addAll(exclusionUrlList); + log.info("多租户拦截器忽略得请求路径有:{}",ignoreUrlSet); + } + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String servletPath = request.getServletPath(); + boolean isIgnoreRequest = UrlIgnoreUtils.isIgnoreUrl(ignoreUrlSet, servletPath); + log.info("本次请求多租户拦截器是否拦截:{},请求路径:{}",!isIgnoreRequest,servletPath); + TenantHolder.setIgnoredRequest(isIgnoreRequest); + return true; + } + +} diff --git a/remi-demo/pom.xml b/remi-demo/pom.xml index 25d84f9..a090021 100644 --- a/remi-demo/pom.xml +++ b/remi-demo/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-cloud - 3.1.0 + 3.1.1 diff --git a/remi-demo/remi-demo-api/pom.xml b/remi-demo/remi-demo-api/pom.xml index 925eabc..ea234d5 100644 --- a/remi-demo/remi-demo-api/pom.xml +++ b/remi-demo/remi-demo-api/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-demo - 3.1.0 + 3.1.1 @@ -29,7 +29,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-feign diff --git a/remi-demo/remi-demo-app/pom.xml b/remi-demo/remi-demo-app/pom.xml index f38f779..41a007b 100644 --- a/remi-demo/remi-demo-app/pom.xml +++ b/remi-demo/remi-demo-app/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-demo - 3.1.0 + 3.1.1 @@ -71,7 +71,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-feign diff --git a/remi-demo/remi-demo-app/src/main/resources/docker/Dockerfile b/remi-demo/remi-demo-app/src/main/resources/docker/Dockerfile index df227bd..b506c1b 100644 --- a/remi-demo/remi-demo-app/src/main/resources/docker/Dockerfile +++ b/remi-demo/remi-demo-app/src/main/resources/docker/Dockerfile @@ -10,7 +10,7 @@ VOLUME /temp #ADD 将app.jar文件将拷贝到容器中,默认拷贝到根路径下 #也可以直接写成ADD app.jar /app.jar 或者 ADD app.jar / -ADD remi-demo-app-3.1.0.jar remi-demo-app.jar +ADD remi-demo-app-3.1.1.jar remi-demo-app.jar #修改app.jar这个文件的访问时间和修改时间为当前时间,而不会修改文件的内容。 RUN bash -c "touch /remi-demo-app.jar" diff --git a/remi-demo/remi-demo-domain/pom.xml b/remi-demo/remi-demo-domain/pom.xml index b932cc7..e899545 100644 --- a/remi-demo/remi-demo-domain/pom.xml +++ b/remi-demo/remi-demo-domain/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-demo - 3.1.0 + 3.1.1 @@ -29,12 +29,12 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-domain - ai.remi.top.comm + ai.remi.top.cloud remi-comm-swagger diff --git a/remi-demo/remi-demo-infra/pom.xml b/remi-demo/remi-demo-infra/pom.xml index 6afe40b..35be3a1 100644 --- a/remi-demo/remi-demo-infra/pom.xml +++ b/remi-demo/remi-demo-infra/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-demo - 3.1.0 + 3.1.1 @@ -29,7 +29,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-jdbc diff --git a/remi-demo/remi-demo-job/pom.xml b/remi-demo/remi-demo-job/pom.xml index 508cc41..0d59ebe 100644 --- a/remi-demo/remi-demo-job/pom.xml +++ b/remi-demo/remi-demo-job/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-demo - 3.1.0 + 3.1.1 @@ -29,7 +29,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-job diff --git a/remi-demo/remi-demo-server/pom.xml b/remi-demo/remi-demo-server/pom.xml index 9d6d70e..dfef1fb 100644 --- a/remi-demo/remi-demo-server/pom.xml +++ b/remi-demo/remi-demo-server/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-demo - 3.1.0 + 3.1.1 @@ -34,23 +34,23 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-exception - ai.remi.top.comm + ai.remi.top.cloud remi-comm-minio - ai.remi.top.comm + ai.remi.top.cloud remi-comm-redis - ai.remi.top.comm + ai.remi.top.cloud remi-comm-thread - ai.remi.top.comm + ai.remi.top.cloud remi-comm-util diff --git a/remi-demo/remi-demo-web/pom.xml b/remi-demo/remi-demo-web/pom.xml index d6e2f1b..96476df 100644 --- a/remi-demo/remi-demo-web/pom.xml +++ b/remi-demo/remi-demo-web/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-demo - 3.1.0 + 3.1.1 @@ -84,13 +84,13 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-web - ai.remi.top.comm + ai.remi.top.cloud remi-comm-log diff --git a/remi-demo/remi-demo-web/src/main/resources/bootstrap.yml b/remi-demo/remi-demo-web/src/main/resources/bootstrap.yml index d5a33b7..7bb5970 100644 --- a/remi-demo/remi-demo-web/src/main/resources/bootstrap.yml +++ b/remi-demo/remi-demo-web/src/main/resources/bootstrap.yml @@ -15,7 +15,7 @@ spring: password: nacos discovery: # 配置中心的地址 - server-addr: ${NACOS_HOST:192.168.31.18}:${NACOS_PORT:8848} + server-addr: ${NACOS_HOST:192.168.31.204}:${NACOS_PORT:8848} #命名空间 namespace: ${SERVICE_NAME_SPACE_NAME:dev} # 配置文件的环境 diff --git a/remi-demo/remi-demo-web/src/main/resources/docker/Dockerfile b/remi-demo/remi-demo-web/src/main/resources/docker/Dockerfile index aef6e1c..6919f6c 100644 --- a/remi-demo/remi-demo-web/src/main/resources/docker/Dockerfile +++ b/remi-demo/remi-demo-web/src/main/resources/docker/Dockerfile @@ -10,7 +10,7 @@ VOLUME /temp #ADD 将app.jar文件将拷贝到容器中,默认拷贝到根路径下 #也可以直接写成ADD app.jar /app.jar 或者 ADD app.jar / -ADD remi-demo-web-3.1.0.jar remi-demo-web.jar +ADD remi-demo-web-3.1.1.jar remi-demo-web.jar #修改app.jar这个文件的访问时间和修改时间为当前时间,而不会修改文件的内容。 RUN bash -c "touch /remi-demo-web.jar" diff --git a/remi-nacos/pom.xml b/remi-nacos/pom.xml index 2fa10f7..e8bc4ef 100644 --- a/remi-nacos/pom.xml +++ b/remi-nacos/pom.xml @@ -11,7 +11,7 @@ ai.remi.top.cloud remi-cloud - 3.1.0 + 3.1.1 diff --git a/remi-nacos/src/main/resources/docker/Dockerfile b/remi-nacos/src/main/resources/docker/Dockerfile index fa0dce8..f5ec545 100644 --- a/remi-nacos/src/main/resources/docker/Dockerfile +++ b/remi-nacos/src/main/resources/docker/Dockerfile @@ -10,7 +10,7 @@ VOLUME /temp #ADD 将app.jar文件将拷贝到容器中,默认拷贝到根路径下 #也可以直接写成ADD app.jar /app.jar 或者 ADD app.jar / -ADD remi-nacos-3.1.0.jar remi-nacos.jar +ADD remi-nacos-3.1.1.jar remi-nacos.jar #修改app.jar这个文件的访问时间和修改时间为当前时间,而不会修改文件的内容。 RUN bash -c "touch /remi-nacos.jar" diff --git a/remi-nacos/src/main/resources/docker/docker_deploy.sh b/remi-nacos/src/main/resources/docker/docker_deploy.sh index c1a08b7..06a90a9 100644 --- a/remi-nacos/src/main/resources/docker/docker_deploy.sh +++ b/remi-nacos/src/main/resources/docker/docker_deploy.sh @@ -6,9 +6,9 @@ docker build -t sit-remi-nacos-web . docker images | grep sit-remi-nacos-web #把新镜像推送到私服 docker login -uadmin -pYdsz1020 harbor.njydsz.com -docker tag sit-remi-nacos-web harbor.njydsz.com/remi/sit-remi-nacos-web:3.1.0 +docker tag sit-remi-nacos-web harbor.njydsz.com/remi/sit-remi-nacos-web:3.1.1 docker tag sit-remi-nacos-web harbor.njydsz.com/remi/sit-remi-nacos-web -docker push harbor.njydsz.com/remi/sit-remi-nacos-web:3.1.0 +docker push harbor.njydsz.com/remi/sit-remi-nacos-web:3.1.1 docker push harbor.njydsz.com/remi/sit-remi-nacos-web #启动最新的镜像服务 docker run --restart=always -t -dit -p 8848:8848 --name sit-remi-nacos-web sit-remi-nacos-web diff --git a/remi-nacos/src/main/resources/static/js/main.js b/remi-nacos/src/main/resources/static/js/main.js index 77ef0bb..5973572 100644 --- a/remi-nacos/src/main/resources/static/js/main.js +++ b/remi-nacos/src/main/resources/static/js/main.js @@ -335,6 +335,6 @@ var S=P(693),o=P(694),s=P(695);function n(){return c.TYPED_ARRAY_SUPPORT?2147483 * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var a=60103,r=60106,o=60107,i=60108,s=60114,l=60109,u=60110,d=60112,c=60113,f=60120,p=60115,h=60116,m=60121,g=60122,y=60117,v=60129,_=60131;function b(e){if("object"==typeof e&&null!==e){var t=e.$$typeof;switch(t){case a:switch(e=e.type){case o:case s:case i:case c:case f:return e;default:switch(e=e&&e.$$typeof){case u:case d:case h:case p:case l:return e;default:return t}}case r:return t}}}"function"==typeof Symbol&&Symbol.for&&(a=(w=Symbol.for)("react.element"),r=w("react.portal"),o=w("react.fragment"),i=w("react.strict_mode"),s=w("react.profiler"),l=w("react.provider"),u=w("react.context"),d=w("react.forward_ref"),c=w("react.suspense"),f=w("react.suspense_list"),p=w("react.memo"),h=w("react.lazy"),m=w("react.block"),g=w("react.server.block"),y=w("react.fundamental"),v=w("react.debug_trace_mode"),_=w("react.legacy_hidden"));var w=l,M=a,k=d,S=o,E=h,x=p,C=r,T=s,L=i,O=c;t.ContextConsumer=u,t.ContextProvider=w,t.Element=M,t.ForwardRef=k,t.Fragment=S,t.Lazy=E,t.Memo=x,t.Portal=C,t.Profiler=T,t.StrictMode=L,t.Suspense=O,t.isAsyncMode=function(){return!1},t.isConcurrentMode=function(){return!1},t.isContextConsumer=function(e){return b(e)===u},t.isContextProvider=function(e){return b(e)===l},t.isElement=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===a},t.isForwardRef=function(e){return b(e)===d},t.isFragment=function(e){return b(e)===o},t.isLazy=function(e){return b(e)===h},t.isMemo=function(e){return b(e)===p},t.isPortal=function(e){return b(e)===r},t.isProfiler=function(e){return b(e)===s},t.isStrictMode=function(e){return b(e)===i},t.isSuspense=function(e){return b(e)===c},t.isValidElementType=function(e){return"string"==typeof e||"function"==typeof e||e===o||e===s||e===v||e===i||e===c||e===f||e===_||"object"==typeof e&&null!==e&&(e.$$typeof===h||e.$$typeof===p||e.$$typeof===l||e.$$typeof===u||e.$$typeof===d||e.$$typeof===y||e.$$typeof===m||e[0]===g)},t.typeOf=b},function(e,t,n){"use strict";var a,r=n(1);window.edasprefix="acm",window.globalConfig={isParentEdas:function(){return window.parent&&-1!==window.parent.location.host.indexOf("edas")}},r.c.middleWare(function(){var e=0=e.length?{value:void 0,done:!0}:(e=a(e,t),this._i+=e.length,{value:e,done:!1})})},function(e,t,n){var o=n(150),i=n(149);e.exports=function(r){return function(e,t){var n,e=String(i(e)),t=o(t),a=e.length;return t<0||a<=t?r?"":void 0:(n=e.charCodeAt(t))<55296||56319=e.length?(this._t=void 0,r(1)):r(0,"keys"==t?n:"values"==t?e[n]:[n,e[n]])},"values"),o.Arguments=o.Array,a("keys"),a("values"),a("entries")},function(e,t){e.exports=function(){}},function(e,t){e.exports=function(e,t){return{value:t,done:!!e}}},function(e,t,n){e.exports={default:n(497),__esModule:!0}},function(e,t,n){n(498),n(503),n(504),n(505),e.exports=n(80).Symbol},function(I,A,e){"use strict";function a(e){var t=T[e]=_(M[E]);return t._k=e,t}function n(e,t){m(e);for(var n,a=B(t=g(t)),r=0,o=a.length;rr;)l(T,t=n[r++])||t==x||t==H||a.push(t);return a}function i(e){for(var t,n=e===O,a=X(n?L:g(e)),r=[],o=0;a.length>o;)!l(T,t=a[o++])||n&&!l(O,t)||r.push(T[t]);return r}var s=e(79),l=e(89),u=e(81),d=e(95),R=e(209),H=e(499).KEY,c=e(112),f=e(152),p=e(158),F=e(128),h=e(99),z=e(159),W=e(160),B=e(500),U=e(501),m=e(111),V=e(97),K=e(155),g=e(98),y=e(148),v=e(125),_=e(157),q=e(502),G=e(211),b=e(154),$=e(88),J=e(126),Q=G.f,w=$.f,X=q.f,M=s.Symbol,k=s.JSON,S=k&&k.stringify,E="prototype",x=h("_hidden"),Z=h("toPrimitive"),ee={}.propertyIsEnumerable,C=f("symbol-registry"),T=f("symbols"),L=f("op-symbols"),O=Object[E],f="function"==typeof M&&!!b.f,D=s.QObject,N=!D||!D[E]||!D[E].findChild,P=u&&c(function(){return 7!=_(w({},"a",{get:function(){return w(this,"a",{value:7}).a}})).a})?function(e,t,n){var a=Q(O,t);a&&delete O[t],w(e,t,n),a&&e!==O&&w(O,t,a)}:w,j=f&&"symbol"==typeof M.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof M},Y=function(e,t,n){return e===O&&Y(L,t,n),m(e),t=y(t,!0),m(n),(l(T,t)?(n.enumerable?(l(e,x)&&e[x][t]&&(e[x][t]=!1),n=_(n,{enumerable:v(0,!1)})):(l(e,x)||w(e,x,v(1,{})),e[x][t]=!0),P):w)(e,t,n)};f||(R((M=function(){if(this instanceof M)throw TypeError("Symbol is not a constructor!");var t=F(0ne;)h(te[ne++]);for(var ae=J(h.store),re=0;ae.length>re;)W(ae[re++]);d(d.S+d.F*!f,"Symbol",{for:function(e){return l(C,e+="")?C[e]:C[e]=M(e)},keyFor:function(e){if(!j(e))throw TypeError(e+" is not a symbol!");for(var t in C)if(C[t]===e)return t},useSetter:function(){N=!0},useSimple:function(){N=!1}}),d(d.S+d.F*!f,"Object",{create:function(e,t){return void 0===t?_(e):n(_(e),t)},defineProperty:Y,defineProperties:n,getOwnPropertyDescriptor:r,getOwnPropertyNames:o,getOwnPropertySymbols:i});D=c(function(){b.f(1)});d(d.S+d.F*D,"Object",{getOwnPropertySymbols:function(e){return b.f(K(e))}}),k&&d(d.S+d.F*(!f||c(function(){var e=M();return"[null]"!=S([e])||"{}"!=S({a:e})||"{}"!=S(Object(e))})),"JSON",{stringify:function(e){for(var t,n,a=[e],r=1;ri;)o.call(e,a=r[i++])&&t.push(a);return t}},function(e,t,n){var a=n(207);e.exports=Array.isArray||function(e){return"Array"==a(e)}},function(e,t,n){var a=n(98),r=n(210).f,o={}.toString,i="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){if(!i||"[object Window]"!=o.call(e))return r(a(e));try{return r(e)}catch(e){return i.slice()}}},function(e,t){},function(e,t,n){n(160)("asyncIterator")},function(e,t,n){n(160)("observable")},function(e,t,n){e.exports={default:n(507),__esModule:!0}},function(e,t,n){n(508),e.exports=n(80).Object.setPrototypeOf},function(e,t,n){var a=n(95);a(a.S,"Object",{setPrototypeOf:n(509).set})},function(e,t,r){function o(e,t){if(a(e),!n(t)&&null!==t)throw TypeError(t+": can't set as prototype!")}var n=r(97),a=r(111);e.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(e,n,a){try{(a=r(202)(Function.call,r(211).f(Object.prototype,"__proto__").set,2))(e,[]),n=!(e instanceof Array)}catch(e){n=!0}return function(e,t){return o(e,t),n?e.__proto__=t:a(e,t),e}}({},!1):void 0),check:o}},function(e,t,n){e.exports={default:n(511),__esModule:!0}},function(e,t,n){n(512);var a=n(80).Object;e.exports=function(e,t){return a.create(e,t)}},function(e,t,n){var a=n(95);a(a.S,"Object",{create:n(157)})},function(e,t,n){"use strict";var i=n(514);function a(){}function r(){}r.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,r,o){if(o!==i)throw(o=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types")).name="Invariant Violation",o}function t(){return e}var n={array:e.isRequired=e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:r,resetWarningCache:a};return n.PropTypes=n}},function(e,t,n){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},function(e,t,n){"use strict";function l(e,t,n,a){e.removeEventListener&&e.removeEventListener(t,n,a||!1)}function a(e,t,n,a){return e.addEventListener&&e.addEventListener(t,n,a||!1),{off:function(){return l(e,t,n,a)}}}t.__esModule=!0,t.on=a,t.once=function(r,o,i,s){return a(r,o,function e(){for(var t=arguments.length,n=Array(t),a=0;a68?1900:2e3)},r=function(t){return function(e){this[t]=+e}},o=[/[+-]\d\d:?(\d\d)?|Z/,function(e){(this.zone||(this.zone={})).offset=function(e){if(!e)return 0;if("Z"===e)return 0;var t=e.match(/([+-]|\d\d)/g),n=60*t[1]+(+t[2]||0);return 0===n?0:"+"===t[0]?-n:n}(e)}],i=function(e){var t=h[e];return t&&(t.indexOf?t:t.s.concat(t.f))},s=function(e,t){var n,a=h.meridiem;if(a){for(var r=1;r<=24;r+=1)if(e.indexOf(a(r,0,t))>-1){n=r>12;break}}else n=e===(t?"pm":"PM");return n},f={A:[n,function(e){this.afternoon=s(e,!1)}],a:[n,function(e){this.afternoon=s(e,!0)}],S:[/\d/,function(e){this.milliseconds=100*+e}],SS:[e,function(e){this.milliseconds=10*+e}],SSS:[/\d{3}/,function(e){this.milliseconds=+e}],s:[t,r("seconds")],ss:[t,r("seconds")],m:[t,r("minutes")],mm:[t,r("minutes")],H:[t,r("hours")],h:[t,r("hours")],HH:[t,r("hours")],hh:[t,r("hours")],D:[t,r("day")],DD:[e,r("day")],Do:[n,function(e){var t=h.ordinal,n=e.match(/\d+/);if(this.day=n[0],t)for(var a=1;a<=31;a+=1)t(a).replace(/\[|\]/g,"")===e&&(this.day=a)}],M:[t,r("month")],MM:[e,r("month")],MMM:[n,function(e){var t=i("months"),n=(i("monthsShort")||t.map(function(e){return e.slice(0,3)})).indexOf(e)+1;if(n<1)throw new Error;this.month=n%12||n}],MMMM:[n,function(e){var t=i("months").indexOf(e)+1;if(t<1)throw new Error;this.month=t%12||t}],Y:[/[+-]?\d+/,r("year")],YY:[e,function(e){this.year=a(e)}],YYYY:[/\d{4}/,r("year")],Z:o,ZZ:o};function b(e){var t,r;t=e,r=h&&h.formats;for(var u=(e=t.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,function(e,t,n){var a=n&&n.toUpperCase();return t||r[n]||l[n]||r[a].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,function(e,t,n){return t||n.slice(1)})})).match(c),d=u.length,n=0;n-1)return new Date(("X"===t?1e3:1)*e);var a=b(t)(e),r=a.year,o=a.month,i=a.day,s=a.hours,l=a.minutes,u=a.seconds,d=a.milliseconds,c=a.zone,f=new Date,p=i||(r||o?1:f.getDate()),h=r||f.getFullYear(),m=0;r&&!o||(m=o>0?o-1:f.getMonth());var g=s||0,y=l||0,v=u||0,_=d||0;return c?new Date(Date.UTC(h,m,p,g,y,v,_+60*c.offset*1e3)):n?new Date(Date.UTC(h,m,p,g,y,v,_)):new Date(h,m,p,g,y,v,_)}catch(e){return new Date("")}}(t,r,n),this.init(),l&&!0!==l&&(this.$L=this.locale(l).$L),s&&t!=this.format(r)&&(this.$d=new Date("")),h={}}else if(r instanceof Array)for(var u=r.length,d=1;d<=u;d+=1){a[1]=r[d-1];var c=f.apply(this,a);if(c.isValid()){this.$d=c.$d,this.$L=c.$L,this.init();break}d===u&&(this.$d=new Date(""))}else p.call(this,e)}}}()},function(e,t,n){e.exports=function(){"use strict";return function(e,t,a){a.updateLocale=function(e,t){var n=a.Ls[e];if(n)return(t?Object.keys(t):[]).forEach(function(e){n[e]=t[e]}),n}}}()},function(e,t,n){e.exports=function(e,t,n){function a(e,t,n,a,r){var o,e=e.name?e:e.$locale(),t=s(e[t]),n=s(e[n]),i=t||n.map(function(e){return e.slice(0,a)});return r?(o=e.weekStart,i.map(function(e,t){return i[(t+(o||0))%7]})):i}function r(){return n.Ls[n.locale()]}function o(e,t){return e.formats[t]||e.formats[t.toUpperCase()].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,function(e,t,n){return t||n.slice(1)})}var t=t.prototype,s=function(e){return e&&(e.indexOf?e:e.s)};t.localeData=function(){return function(){var t=this;return{months:function(e){return e?e.format("MMMM"):a(t,"months")},monthsShort:function(e){return e?e.format("MMM"):a(t,"monthsShort","months",3)},firstDayOfWeek:function(){return t.$locale().weekStart||0},weekdays:function(e){return e?e.format("dddd"):a(t,"weekdays")},weekdaysMin:function(e){return e?e.format("dd"):a(t,"weekdaysMin","weekdays",2)},weekdaysShort:function(e){return e?e.format("ddd"):a(t,"weekdaysShort","weekdays",3)},longDateFormat:function(e){return o(t.$locale(),e)},meridiem:this.$locale().meridiem,ordinal:this.$locale().ordinal}}.bind(this)()},n.localeData=function(){var t=r();return{firstDayOfWeek:function(){return t.weekStart||0},weekdays:function(){return n.weekdays()},weekdaysShort:function(){return n.weekdaysShort()},weekdaysMin:function(){return n.weekdaysMin()},months:function(){return n.months()},monthsShort:function(){return n.monthsShort()},longDateFormat:function(e){return o(t,e)},meridiem:t.meridiem,ordinal:t.ordinal}},n.months=function(){return a(r(),"months")},n.monthsShort=function(){return a(r(),"monthsShort","months",3)},n.weekdays=function(e){return a(r(),"weekdays",null,null,e)},n.weekdaysShort=function(e){return a(r(),"weekdaysShort","weekdays",3,e)},n.weekdaysMin=function(e){return a(r(),"weekdaysMin","weekdays",2,e)}}},function(e,t,n){e.exports=function(){"use strict";var i="month",s="quarter";return function(e,t){var n=t.prototype;n.quarter=function(e){return this.$utils().u(e)?Math.ceil((this.month()+1)/3):this.month(this.month()%3+3*(e-1))};var a=n.add;n.add=function(e,t){return e=Number(e),this.$utils().p(t)===s?this.add(3*e,i):a.bind(this)(e,t)};var o=n.startOf;n.startOf=function(e,t){var n=this.$utils(),a=!!n.u(t)||t;if(n.p(e)===s){var r=this.quarter()-1;return a?this.month(3*r).startOf(i).startOf("day"):this.month(3*r+2).endOf(i).endOf("day")}return o.bind(this)(e,t)}}}()},function(e,t,n){e.exports=function(){"use strict";return function(e,t){var n=t.prototype,o=n.format;n.format=function(e){var t=this,n=this.$locale();if(!this.isValid())return o.bind(this)(e);var a=this.$utils(),r=(e||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,function(e){switch(e){case"Q":return Math.ceil((t.$M+1)/3);case"Do":return n.ordinal(t.$D);case"gggg":return t.weekYear();case"GGGG":return t.isoWeekYear();case"wo":return n.ordinal(t.week(),"W");case"w":case"ww":return a.s(t.week(),"w"===e?1:2,"0");case"W":case"WW":return a.s(t.isoWeek(),"W"===e?1:2,"0");case"k":case"kk":return a.s(String(0===t.$H?24:t.$H),"k"===e?1:2,"0");case"X":return Math.floor(t.$d.getTime()/1e3);case"x":return t.$d.getTime();case"z":return"["+t.offsetName()+"]";case"zzz":return"["+t.offsetName("long")+"]";default:return e}});return o.bind(this)(r)}}}()},function(e,t,n){e.exports=function(){"use strict";var s="week",l="year";return function(e,t,i){var n=t.prototype;n.week=function(e){if(void 0===e&&(e=null),null!==e)return this.add(7*(e-this.week()),"day");var t=this.$locale().yearStart||1;if(11===this.month()&&this.date()>25){var n=i(this).startOf(l).add(1,l).date(t),a=i(this).endOf(s);if(n.isBefore(a))return 1}var r=i(this).startOf(l).date(t).startOf(s).subtract(1,"millisecond"),o=this.diff(r,s,!0);return o<0?i(this).startOf("week").week():Math.ceil(o)},n.weeks=function(e){return void 0===e&&(e=null),this.week(e)}}}()},function(e,t,n){e.exports=function(e){"use strict";function t(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var n=t(e),a={name:"zh-cn",weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),ordinal:function(e,t){return"W"===t?e+"周":e+"日"},weekStart:1,yearStart:4,formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah点mm分",LLLL:"YYYY年M月D日ddddAh点mm分",l:"YYYY/M/D",ll:"YYYY年M月D日",lll:"YYYY年M月D日 HH:mm",llll:"YYYY年M月D日dddd HH:mm"},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1 分钟",mm:"%d 分钟",h:"1 小时",hh:"%d 小时",d:"1 天",dd:"%d 天",M:"1 个月",MM:"%d 个月",y:"1 年",yy:"%d 年"},meridiem:function(e,t){var n=100*e+t;return n<600?"凌晨":n<900?"早上":n<1100?"上午":n<1300?"中午":n<1800?"下午":"晚上"}};return n.default.locale(a,null,!0),a}(n(217))},function(e,t,n){"use strict";t.__esModule=!0,t.flex=t.transition=t.animation=void 0;var r=n(213),o=n(100);function a(e){var n,a;return!!r.hasDOM&&(n=document.createElement("div"),(a=!1,o.each)(e,function(e,t){if(void 0!==n.style[t])return!(a={end:e})}),a)}var i,s;t.animation=a({WebkitAnimation:"webkitAnimationEnd",OAnimation:"oAnimationEnd",animation:"animationend"}),t.transition=a({WebkitTransition:"webkitTransitionEnd",OTransition:"oTransitionEnd",transition:"transitionend"}),t.flex=(n={display:["flex","-webkit-flex","-moz-flex","-ms-flexbox"]},!!r.hasDOM&&(i=document.createElement("div"),(s=!1,o.each)(n,function(e,t){return(0,o.each)(e,function(e){try{i.style[t]=e,s=s||i.style[t]===e}catch(e){}return!s}),!s}),s))},function(e,t,n){"use strict";t.__esModule=!0,t.getFocusNodeList=i,t.saveLastFocusNode=function(){s=document.activeElement},t.clearLastFocusNode=function(){s=null},t.backLastFocusNode=function(){if(s)try{s.focus()}catch(e){}},t.limitTabRange=function(e,t){{var n,a;t.keyCode===r.default.TAB&&(e=i(e),n=e.length-1,-1<(a=e.indexOf(document.activeElement)))&&(a=a+(t.shiftKey?-1:1),e[a=n<(a=a<0?n:a)?0:a].focus(),t.preventDefault())}};var t=n(218),r=(t=t)&&t.__esModule?t:{default:t},a=n(100);function o(e){var t=e.nodeName.toLowerCase(),n=parseInt(e.getAttribute("tabindex"),10),n=!isNaN(n)&&-1a.height)&&(r[1]=-t.top-("t"===e?t.height:0)),r},this._getParentScrollOffset=function(e){var t=0,n=0;return e&&e.offsetParent&&e.offsetParent!==document.body&&(isNaN(e.offsetParent.scrollTop)||(t+=e.offsetParent.scrollTop),isNaN(e.offsetParent.scrollLeft)||(n+=e.offsetParent.scrollLeft)),{top:t,left:n}}};var p=a;function h(e){(0,o.default)(this,h),r.call(this),this.pinElement=e.pinElement,this.baseElement=e.baseElement,this.pinFollowBaseElementWhenFixed=e.pinFollowBaseElementWhenFixed,this.container=function(e){var t=e.container,e=e.baseElement;if("undefined"==typeof document)return t;for(var n=(n=(0,i.default)(t,e))||document.body;"static"===y.dom.getStyle(n,"position");){if(!n||n===document.body)return document.body;n=n.parentNode}return n}(e),this.autoFit=e.autoFit||!1,this.align=e.align||"tl tl",this.offset=e.offset||[0,0],this.needAdjust=e.needAdjust||!1,this.isRtl=e.isRtl||!1}t.default=p,e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var w=a(n(2)),M=a(n(12)),k=n(0),S=a(k),E=a(n(17)),x=a(n(194)),C=a(n(82)),T=n(11);function a(e){return e&&e.__esModule?e:{default:e}}t.default=function(e){var t,n,a,r,o,i,s,l,u,d,c,f,p,h,m,g,y,v,_,b;return k.useState&&k.useRef&&k.useEffect?(t=void 0===(t=e.prefix)?"next-":t,r=e.animation,n=void 0===r?{in:"expandInDown",out:"expandOutUp"}:r,a=e.visible,r=e.hasMask,o=e.align,o=void 0===(s=e.points)?o?o.split(" "):void 0:s,i=e.onPosition,s=e.children,b=e.className,l=e.style,u=e.wrapperClassName,d=e.beforeOpen,c=e.onOpen,f=e.afterOpen,p=e.beforeClose,h=e.onClose,m=e.afterClose,e=(0,M.default)(e,["prefix","animation","visible","hasMask","align","points","onPosition","children","className","style","wrapperClassName","beforeOpen","onOpen","afterOpen","beforeClose","onClose","afterClose"]),g=(_=(0,k.useState)(!0))[0],y=_[1],v=(0,k.useRef)(null),_=S.default.createElement(C.default.OverlayAnimate,{visible:a,animation:n,onEnter:function(){y(!1),"function"==typeof d&&d(v.current)},onEntering:function(){"function"==typeof c&&c(v.current)},onEntered:function(){"function"==typeof f&&f(v.current)},onExit:function(){"function"==typeof p&&p(v.current)},onExiting:function(){"function"==typeof h&&h(v.current)},onExited:function(){y(!0),"function"==typeof m&&m(v.current)},timeout:300,style:l},s?(0,k.cloneElement)(s,{className:(0,E.default)([t+"overlay-inner",b,s&&s.props&&s.props.className])}):S.default.createElement("span",null)),b=(0,E.default)(((l={})[t+"overlay-wrapper v2"]=!0,l[u]=u,l.opened=a,l)),S.default.createElement(x.default,(0,w.default)({},e,{visible:a,isAnimationEnd:g,hasMask:r,wrapperClassName:b,maskClassName:t+"overlay-backdrop",maskRender:function(e){return S.default.createElement(C.default.OverlayAnimate,{visible:a,animation:!!n&&{in:"fadeIn",out:"fadeOut"},timeout:300,unmountOnExit:!0},e)},points:o,onPosition:function(e){(0,w.default)(e,{align:e.config.points}),"function"==typeof i&&i(e)},ref:v}),_)):(T.log.warning("need react version > 16.8.0"),null)},e.exports=t.default},function(n,e){function a(e,t){return n.exports=a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},n.exports.__esModule=!0,n.exports.default=n.exports,a(e,t)}n.exports=a,n.exports.__esModule=!0,n.exports.default=n.exports},function(e,t,n){"use strict";t.__esModule=!0;var a,d=g(n(12)),c=g(n(2)),r=g(n(4)),o=g(n(6)),i=g(n(7)),l=n(0),f=g(l),p=n(24),s=n(30),u=g(n(5)),h=n(11),m=g(n(360));function g(e){return e&&e.__esModule?e:{default:e}}var y,n=h.func.noop,v=h.func.makeChain,_=h.func.bindCtx,u=(y=l.Component,(0,i.default)(b,y),b.getDerivedStateFromProps=function(e,t){return"visible"in e?(0,c.default)({},t,{visible:e.visible}):null},b.prototype.componentWillUnmount=function(){var t=this;["_timer","_hideTimer","_showTimer"].forEach(function(e){t[e]&&clearTimeout(t[e])})},b.prototype.handleVisibleChange=function(e,t,n){"visible"in this.props||this.setState({visible:e}),this.props.onVisibleChange(e,t,n)},b.prototype.handleTriggerClick=function(e){this.state.visible&&!this.props.canCloseByTrigger||this.handleVisibleChange(!this.state.visible,"fromTrigger",e)},b.prototype.handleTriggerKeyDown=function(e){var t=this.props.triggerClickKeycode;(Array.isArray(t)?t:[t]).includes(e.keyCode)&&(e.preventDefault(),this.handleTriggerClick(e))},b.prototype.handleTriggerMouseEnter=function(e){var t=this;this._mouseNotFirstOnMask=!1,this._hideTimer&&(clearTimeout(this._hideTimer),this._hideTimer=null),this._showTimer&&(clearTimeout(this._showTimer),this._showTimer=null),this.state.visible||(this._showTimer=setTimeout(function(){t.handleVisibleChange(!0,"fromTrigger",e)},this.props.delay))},b.prototype.handleTriggerMouseLeave=function(e,t){var n=this;this._showTimer&&(clearTimeout(this._showTimer),this._showTimer=null),this.state.visible&&(this._hideTimer=setTimeout(function(){n.handleVisibleChange(!1,t||"fromTrigger",e)},this.props.delay))},b.prototype.handleTriggerFocus=function(e){this.handleVisibleChange(!0,"fromTrigger",e)},b.prototype.handleTriggerBlur=function(e){this._isForwardContent||this.handleVisibleChange(!1,"fromTrigger",e),this._isForwardContent=!1},b.prototype.handleContentMouseDown=function(){this._isForwardContent=!0},b.prototype.handleContentMouseEnter=function(){clearTimeout(this._hideTimer)},b.prototype.handleContentMouseLeave=function(e){this.handleTriggerMouseLeave(e,"fromContent")},b.prototype.handleMaskMouseEnter=function(){this._mouseNotFirstOnMask||(clearTimeout(this._hideTimer),this._hideTimer=null,this._mouseNotFirstOnMask=!1)},b.prototype.handleMaskMouseLeave=function(){this._mouseNotFirstOnMask=!0},b.prototype.handleRequestClose=function(e,t){this.handleVisibleChange(!1,e,t)},b.prototype.renderTrigger=function(){var e,t,n,a,r,o,i,s=this,l=this.props,u=l.trigger,l=l.disabled,d={key:"trigger","aria-haspopup":!0,"aria-expanded":this.state.visible};return this.state.visible||(d["aria-describedby"]=void 0),l||(l=this.props.triggerType,l=Array.isArray(l)?l:[l],e=u&&u.props||{},t=e.onClick,n=e.onKeyDown,a=e.onMouseEnter,r=e.onMouseLeave,o=e.onFocus,i=e.onBlur,l.forEach(function(e){switch(e){case"click":d.onClick=v(s.handleTriggerClick,t),d.onKeyDown=v(s.handleTriggerKeyDown,n);break;case"hover":d.onMouseEnter=v(s.handleTriggerMouseEnter,a),d.onMouseLeave=v(s.handleTriggerMouseLeave,r);break;case"focus":d.onFocus=v(s.handleTriggerFocus,o),d.onBlur=v(s.handleTriggerBlur,i)}})),u&&f.default.cloneElement(u,d)},b.prototype.renderContent=function(){var t=this,e=this.props,n=e.children,e=e.triggerType,e=Array.isArray(e)?e:[e],n=l.Children.only(n),a=n.props,r=a.onMouseDown,o=a.onMouseEnter,i=a.onMouseLeave,s={key:"portal"};return e.forEach(function(e){switch(e){case"focus":s.onMouseDown=v(t.handleContentMouseDown,r);break;case"hover":s.onMouseEnter=v(t.handleContentMouseEnter,o),s.onMouseLeave=v(t.handleContentMouseLeave,i)}}),f.default.cloneElement(n,s)},b.prototype.renderPortal=function(){function e(){return(0,p.findDOMNode)(t)}var t=this,n=this.props,a=n.target,r=n.safeNode,o=n.followTrigger,i=n.triggerType,s=n.hasMask,l=n.wrapperStyle,n=(0,d.default)(n,["target","safeNode","followTrigger","triggerType","hasMask","wrapperStyle"]),u=this.props.container,r=Array.isArray(r)?[].concat(r):[r],l=(r.unshift(e),l||{});return o&&(u=function(e){return e&&e.parentNode||e},l.position="relative"),"hover"===i&&s&&(n.onMaskMouseEnter=this.handleMaskMouseEnter,n.onMaskMouseLeave=this.handleMaskMouseLeave),f.default.createElement(m.default,(0,c.default)({},n,{key:"overlay",ref:function(e){return t.overlay=e},visible:this.state.visible,target:a||e,container:u,safeNode:r,wrapperStyle:l,triggerType:i,hasMask:s,onRequestClose:this.handleRequestClose}),this.props.children&&this.renderContent())},b.prototype.render=function(){return[this.renderTrigger(),this.renderPortal()]},a=i=b,i.propTypes={children:u.default.node,trigger:u.default.element,triggerType:u.default.oneOfType([u.default.string,u.default.array]),triggerClickKeycode:u.default.oneOfType([u.default.number,u.default.array]),visible:u.default.bool,defaultVisible:u.default.bool,onVisibleChange:u.default.func,disabled:u.default.bool,autoFit:u.default.bool,delay:u.default.number,canCloseByTrigger:u.default.bool,target:u.default.any,safeNode:u.default.any,followTrigger:u.default.bool,container:u.default.any,hasMask:u.default.bool,wrapperStyle:u.default.object,rtl:u.default.bool,v2:u.default.bool,placement:u.default.string,placementOffset:u.default.number,autoAdjust:u.default.bool},i.defaultProps={triggerType:"hover",triggerClickKeycode:[h.KEYCODE.SPACE,h.KEYCODE.ENTER],defaultVisible:!1,onVisibleChange:n,disabled:!1,autoFit:!1,delay:200,canCloseByTrigger:!0,followTrigger:!1,container:function(){return document.body},rtl:!1},a);function b(e){(0,r.default)(this,b);var t=(0,o.default)(this,y.call(this,e));return t.state={visible:void 0===e.visible?e.defaultVisible:e.visible},_(t,["handleTriggerClick","handleTriggerKeyDown","handleTriggerMouseEnter","handleTriggerMouseLeave","handleTriggerFocus","handleTriggerBlur","handleContentMouseEnter","handleContentMouseLeave","handleContentMouseDown","handleRequestClose","handleMaskMouseEnter","handleMaskMouseLeave"]),t}u.displayName="Popup",t.default=(0,s.polyfill)(u),e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var L=a(n(2)),O=a(n(12)),D=n(0),N=a(D),P=a(n(17)),j=a(n(194)),Y=a(n(82)),I=n(11);function a(e){return e&&e.__esModule?e:{default:e}}t.default=function(r){var e,t,o,n,a,i,s,l,u,d,c,f,p,h,m,g,y,v,_,b,w,M,k,S,E,x,C,T;return D.useState&&D.useRef&&D.useEffect?(e=void 0===(e=r.prefix)?"next-":e,E=r.animation,t=void 0===E?{in:"expandInDown",out:"expandOutUp"}:E,E=r.defaultVisible,x=r.onVisibleChange,o=void 0===x?function(){}:x,x=r.trigger,n=void 0===(n=r.triggerType)?"hover":n,C=r.overlay,a=r.onPosition,T=r.children,i=r.className,s=r.style,l=r.wrapperClassName,u=r.triggerClickKeycode,d=r.align,c=r.beforeOpen,f=r.onOpen,p=r.afterOpen,h=r.beforeClose,m=r.onClose,g=r.afterClose,y=(0,O.default)(r,["prefix","animation","defaultVisible","onVisibleChange","trigger","triggerType","overlay","onPosition","children","className","style","wrapperClassName","triggerClickKeycode","align","beforeOpen","onOpen","afterOpen","beforeClose","onClose","afterClose"]),E=(0,D.useState)(E),v=E[0],_=E[1],E=(0,D.useState)(t),b=E[0],w=E[1],M=(E=(0,D.useState)(!0))[0],k=E[1],S=(0,D.useRef)(null),(0,D.useEffect)(function(){"visible"in r&&_(r.visible)},[r.visible]),(0,D.useEffect)(function(){"animation"in r&&b!==t&&w(t)},[t]),E=C?T:x,x=N.default.createElement(Y.default.OverlayAnimate,{visible:v,animation:b,timeout:200,onEnter:function(){k(!1),"function"==typeof c&&c(S.current)},onEntering:function(){"function"==typeof f&&f(S.current)},onEntered:function(){"function"==typeof p&&p(S.current)},onExit:function(){"function"==typeof h&&h(S.current)},onExiting:function(){"function"==typeof m&&m(S.current)},onExited:function(){k(!0),"function"==typeof g&&g(S.current)},style:s},(x=C||T)?(0,D.cloneElement)(x,{className:(0,P.default)([e+"overlay-inner",i,x&&x.props&&x.props.className])}):N.default.createElement("span",null)),C=(0,P.default)(((s={})[e+"overlay-wrapper v2"]=!0,s[l]=l,s.opened=v,s)),T={},d&&(T.points=d.split(" ")),N.default.createElement(j.default.Popup,(0,L.default)({},y,T,{wrapperClassName:C,overlay:x,visible:v,isAnimationEnd:M,triggerType:n,onVisibleChange:function(e){for(var t=arguments.length,n=Array(1 16.8.0"),null)},e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var l=a(n(2)),u=a(n(12)),o=n(0),d=a(o),i=a(n(24)),s=a(n(8)),c=a(n(82)),f=a(n(162)),p=n(11);function a(e){return e&&e.__esModule?e:{default:e}}var h={top:8,maxCount:0,duration:3e3},m=s.default.config(function(e){var t=e.prefix,s=void 0===t?"next-":t,t=e.dataSource,a=void 0===t?[]:t,r=(0,o.useState)()[1];return a.forEach(function(n){n.timer||(n.timer=setTimeout(function(){var e,t=a.indexOf(n);-1a&&y.shift(),i.default.render(d.default.createElement(s.default,s.default.getContext(),d.default.createElement(m,{dataSource:y})),g),{key:n,close:function(){r.timer&&clearTimeout(r.timer);var e=y.indexOf(r);-1 16.8.0")}},e.exports=t.default},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";n(562)},function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var p=l(n(2)),r=l(n(4)),o=l(n(6)),a=l(n(7)),h=l(n(42)),m=l(n(0)),i=l(n(5)),g=l(n(17)),y=n(11),s=l(n(28)),v=l(n(366));function l(e){return e&&e.__esModule?e:{default:e}}function _(e,r){var o=r.size,i=r.device,s=r.labelAlign,l=r.labelTextAlign,u=r.labelCol,d=r.wrapperCol,c=r.responsive,f=r.colon;return m.default.Children.map(e,function(e){var t,n,a;return y.obj.isReactFragment(e)?_(e.props.children,r):e&&-1<["function","object"].indexOf((0,h.default)(e.type))&&"form_item"===e.type._typeMark?(t={labelCol:e.props.labelCol||u,wrapperCol:e.props.wrapperCol||d,labelAlign:e.props.labelAlign||("phone"===i?"top":s),labelTextAlign:e.props.labelTextAlign||l,colon:"colon"in e.props?e.props.colon:f,size:e.props.size||o,responsive:c},m.default.cloneElement(e,(n=t,a={},Object.keys(n).forEach(function(e){void 0!==n[e]&&(a[e]=n[e])}),a))):e})}u=m.default.Component,(0,a.default)(b,u),b.prototype.getChildContext=function(){return{_formField:this.props.field||this._formField,_formSize:this.props.size,_formDisabled:this.props.disabled,_formPreview:this.props.isPreview,_formFullWidth:this.props.fullWidth,_formLabelForErrorMessage:this.props.useLabelForErrorMessage}},b.prototype.componentDidUpdate=function(e){var t=this.props;this._formField&&("value"in t&&t.value!==e.value&&this._formField.setValues(t.value),"error"in t)&&t.error!==e.error&&this._formField.setValues(t.error)},b.prototype.render=function(){var e=this.props,t=e.className,n=e.inline,a=e.size,r=(e.device,e.labelAlign,e.labelTextAlign,e.onSubmit),o=e.children,i=(e.labelCol,e.wrapperCol,e.style),s=e.prefix,l=e.rtl,u=e.isPreview,d=e.component,c=e.responsive,f=e.gap,n=(e.colon,(0,g.default)(((e={})[s+"form"]=!0,e[s+"inline"]=n,e[""+s+a]=a,e[s+"form-responsive-grid"]=c,e[s+"form-preview"]=u,e[t]=!!t,e))),a=_(o,this.props);return m.default.createElement(d,(0,p.default)({role:"grid"},y.obj.pickOthers(b.propTypes,this.props),{className:n,style:i,dir:l?"rtl":void 0,onSubmit:r}),c?m.default.createElement(v.default,{gap:f},a):a)},a=n=b,n.propTypes={prefix:i.default.string,inline:i.default.bool,size:i.default.oneOf(["large","medium","small"]),fullWidth:i.default.bool,labelAlign:i.default.oneOf(["top","left","inset"]),labelTextAlign:i.default.oneOf(["left","right"]),field:i.default.any,saveField:i.default.func,labelCol:i.default.object,wrapperCol:i.default.object,onSubmit:i.default.func,children:i.default.any,className:i.default.string,style:i.default.object,value:i.default.object,onChange:i.default.func,component:i.default.oneOfType([i.default.string,i.default.func]),fieldOptions:i.default.object,rtl:i.default.bool,device:i.default.oneOf(["phone","tablet","desktop"]),responsive:i.default.bool,isPreview:i.default.bool,useLabelForErrorMessage:i.default.bool,colon:i.default.bool,disabled:i.default.bool,gap:i.default.oneOfType([i.default.arrayOf(i.default.number),i.default.number])},n.defaultProps={prefix:"next-",onSubmit:function(e){e.preventDefault()},size:"medium",labelAlign:"left",onChange:y.func.noop,component:"form",saveField:y.func.noop,device:"desktop",colon:!1,disabled:!1},n.childContextTypes={_formField:i.default.object,_formSize:i.default.string,_formDisabled:i.default.bool,_formPreview:i.default.bool,_formFullWidth:i.default.bool,_formLabelForErrorMessage:i.default.bool};var u,n=a;function b(e){(0,r.default)(this,b);var t,n,a=(0,o.default)(this,u.call(this,e));return a.onChange=function(e,t){a.props.onChange(a._formField.getValues(),{name:e,value:t,field:a._formField})},a._formField=null,!1!==e.field&&(t=(0,p.default)({},e.fieldOptions,{onChange:a.onChange}),e.field?(a._formField=e.field,n=a._formField.options.onChange,t.onChange=y.func.makeChain(n,a.onChange),a._formField.setOptions&&a._formField.setOptions(t)):("value"in e&&(t.values=e.value),a._formField=new s.default(a,t)),e.locale&&e.locale.Validate&&a._formField.setOptions({messages:e.locale.Validate}),e.saveField(a._formField)),a}n.displayName="Form",t.default=n,e.exports=t.default},function(e,t,n){"use strict";var a=n(90),m=(Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0,a(n(166))),i=a(n(567)),r=a(n(167)),o=a(n(115)),b=a(n(168)),w=a(n(76)),s=a(n(364)),l=a(n(365)),g=a(n(574)),M=n(583),u={state:"",valueName:"value",trigger:"onChange",inputValues:[]},a=function(){function a(e){var t=this,n=1e.length)&&(t=e.length);for(var n=0,a=new Array(t);n=a.length?n:(o=a[r],n=e(t&&t[o],n,a,r+1),t?Array.isArray(t)?((a=[].concat(t))[o]=n,a):(0,l.default)({},t,(0,i.default)({},o,n)):((r=isNaN(o)?{}:[])[o]=n,r))};t=function(){};void 0!==e&&e.env,n.warning=t}.call(this,r(102))},function(e,t,n){"use strict";t.__esModule=!0,t.cloneAndAddKey=function(e){{var t;if(e&&(0,a.isValidElement)(e))return t=e.key||"error",(0,a.cloneElement)(e,{key:t})}return e},t.scrollToFirstError=function(e){var t=e.errorsGroup,n=e.options,a=e.instance;if(t&&n.scrollToFirstError){var r,o=void 0,i=void 0;for(r in t)if(t.hasOwnProperty(r)){var s=u.default.findDOMNode(a[r]);if(!s)return;var l=s.offsetTop;(void 0===i||l), use instead of.'),S.default.cloneElement(e,{className:t,size:c||C(r)})):(0,k.isValidElement)(e)?e:S.default.createElement("span",{className:a+"btn-helper"},e)}),t=d,_=(0,b.default)({},x.obj.pickOthers(Object.keys(T.propTypes),e),{type:o,disabled:p,onClick:h,className:(0,E.default)(n)});return"button"!==t&&(delete _.type,_.disabled)&&(delete _.onClick,_.href)&&delete _.href,S.default.createElement(t,(0,b.default)({},_,{dir:g?"rtl":void 0,onMouseUp:this.onMouseUp,ref:this.buttonRefHandler}),s,u)},a=n=T,n.propTypes=(0,b.default)({},s.default.propTypes,{prefix:r.default.string,rtl:r.default.bool,type:r.default.oneOf(["primary","secondary","normal"]),size:r.default.oneOf(["small","medium","large"]),icons:r.default.shape({loading:r.default.node}),iconSize:r.default.oneOfType([r.default.oneOf(["xxs","xs","small","medium","large","xl","xxl","xxxl","inherit"]),r.default.number]),htmlType:r.default.oneOf(["submit","reset","button"]),component:r.default.oneOf(["button","a","div","span"]),loading:r.default.bool,ghost:r.default.oneOf([!0,!1,"light","dark"]),text:r.default.bool,warning:r.default.bool,disabled:r.default.bool,onClick:r.default.func,className:r.default.string,onMouseUp:r.default.func,children:r.default.node}),n.defaultProps={prefix:"next-",type:"normal",size:"medium",icons:{},htmlType:"button",component:"button",loading:!1,ghost:!1,text:!1,warning:!1,disabled:!1,onClick:function(){}};var u,s=a;function T(){var e,t;(0,o.default)(this,T);for(var n=arguments.length,a=Array(n),r=0;ra[r])return!0;if(n[r] 0, or `null`');if(U(i,"numericSeparator")&&"boolean"!=typeof i.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var t=i.numericSeparator;if(void 0===n)return"undefined";if(null===n)return"null";if("boolean"==typeof n)return n?"true":"false";if("string"==typeof n)return function e(t,n){if(t.length>n.maxStringLength)return a=t.length-n.maxStringLength,a="... "+a+" more character"+(1"}if(z(n))return 0===n.length?"[]":(l=$(n,m),h&&!function(e){for(var t=0;t "+m(e,n))}),ne("Map",_.call(n),u,h)):function(e){if(w&&e&&"object"==typeof e)try{w.call(e);try{_.call(e)}catch(e){return 1}return e instanceof Set}catch(e){}return}(n)?(d=[],M&&M.call(n,function(e){d.push(m(e,n))}),ne("Set",w.call(n),d,h)):function(e){if(k&&e&&"object"==typeof e)try{k.call(e,k);try{S.call(e,S)}catch(e){return 1}return e instanceof WeakMap}catch(e){}return}(n)?q("WeakMap"):function(e){if(S&&e&&"object"==typeof e)try{S.call(e,S);try{k.call(e,k)}catch(e){return 1}return e instanceof WeakSet}catch(e){}return}(n)?q("WeakSet"):function(e){if(E&&e&&"object"==typeof e)try{return E.call(e),1}catch(e){}return}(n)?q("WeakRef"):"[object Number]"!==V(c=n)||j&&"object"==typeof c&&j in c?function(e){if(e&&"object"==typeof e&&D)try{return D.call(e),1}catch(e){}return}(n)?K(m(D.call(n))):"[object Boolean]"!==V(t=n)||j&&"object"==typeof t&&j in t?"[object String]"!==V(e=n)||j&&"object"==typeof e&&j in e?("[object Date]"!==V(t=n)||j&&"object"==typeof t&&j in t)&&!W(n)?(e=$(n,m),t=I?I(n)===Object.prototype:n instanceof Object||n.constructor===Object,f=n instanceof Object?"":"null prototype",p=!t&&j&&Object(n)===n&&j in n?x.call(V(n),8,-1):f?"Object":"",t=(!t&&"function"==typeof n.constructor&&n.constructor.name?n.constructor.name+" ":"")+(p||f?"["+O.call(L.call([],p||[],f||[]),": ")+"] ":""),0===e.length?t+"{}":h?t+"{"+G(e,h)+"}":t+"{ "+O.call(e,", ")+" }"):String(n):K(m(String(n))):K(J.call(n)):K(m(Number(n)))};var l=Object.prototype.hasOwnProperty||function(e){return e in this};function U(e,t){return l.call(e,t)}function V(e){return i.call(e)}function ee(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0,a=e.length;n, as child."),a.push(t),e.props.children)&&(t.children=n(e.props.children))}),a}(e.children):t},N.prototype.fetchInfoFromBinaryChildren=function(e){function r(e,t){return t=t||0,e.forEach(function(e){e.children?t=r(e.children,t):t+=1}),t}var a=!1,o=[],i=[],e=(function t(){var e=0r.tRight&&(e=r.tRight,r.changedPageX=r.tRight-r.startLeft),e-r.cellLefto.clientHeight,o.scrollWidth,o.clientWidth,o={},e||(o[r]=0,o[a]=0),+i&&(o.marginBottom=-i,o.paddingBottom=i,e)&&(o[a]=i),h.dom.setStyle(this.headerNode,o)),n&&!this.props.lockType&&this.headerNode&&(r=this.headerNode.querySelector("."+t+"table-header-fixer"),e=h.dom.getStyle(this.headerNode,"height"),a=h.dom.getStyle(this.headerNode,"paddingBottom"),h.dom.setStyle(r,{width:i,height:e-a}))},o.prototype.render=function(){var e=this.props,t=e.components,n=e.className,a=e.prefix,r=e.fixedHeader,o=e.lockType,i=e.dataSource,e=(e.maxBodyHeight,(0,u.default)(e,["components","className","prefix","fixedHeader","lockType","dataSource","maxBodyHeight"]));return r&&((t=(0,l.default)({},t)).Header||(t.Header=m.default),t.Body||(t.Body=g.default),t.Wrapper||(t.Wrapper=y.default),n=(0,p.default)(((r={})[a+"table-fixed"]=!0,r[a+"table-wrap-empty"]=!i.length,r[n]=n,r))),c.default.createElement(s,(0,l.default)({},e,{dataSource:i,lockType:o,components:t,className:n,prefix:a}))},o}(c.default.Component),n.FixedHeader=m.default,n.FixedBody=g.default,n.FixedWrapper=y.default,n.propTypes=(0,l.default)({hasHeader:r.default.bool,fixedHeader:r.default.bool,maxBodyHeight:r.default.oneOfType([r.default.number,r.default.string])},s.propTypes),n.defaultProps=(0,l.default)({},s.defaultProps,{hasHeader:!0,fixedHeader:!1,maxBodyHeight:200,components:{},refs:{},prefix:"next-"}),n.childContextTypes={fixedHeader:r.default.bool,getNode:r.default.func,onFixedScrollSync:r.default.func,getTableInstanceForFixed:r.default.func,maxBodyHeight:r.default.oneOfType([r.default.number,r.default.string])};var t,n=t;return n.displayName="FixedTable",(0,o.statics)(n,s),n};var c=s(n(0)),r=s(n(5)),f=n(24),p=s(n(17)),h=n(11),m=s(n(134)),g=s(n(402)),y=s(n(135)),o=n(70);function s(e){return e&&e.__esModule?e:{default:e}}e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var i=o(n(12)),f=o(n(2)),r=o(n(4)),s=o(n(6)),l=o(n(7)),u=(t.default=function(o){e=t=function(n){function a(e,t){(0,r.default)(this,a);var c=(0,s.default)(this,n.call(this,e,t));return c.addSelection=function(e){var t=c.props,n=t.prefix,a=t.rowSelection,t=t.size,a=a.columnProps&&a.columnProps()||{};e.find(function(e){return"selection"===e.key})||e.unshift((0,f.default)({key:"selection",title:c.renderSelectionHeader.bind(c),cell:c.renderSelectionBody.bind(c),width:"small"===t?34:50,className:n+"table-selection "+n+"table-prerow",__normalized:!0},a))},c.renderSelectionHeader=function(){var e=c.selectAllRow,t={},n=c.props,a=n.rowSelection,r=n.primaryKey,o=n.dataSource,i=n.entireDataSource,n=n.locale,s=c.state.selectedRowKeys,l=a.mode||"multiple",u=!!s.length,d=!1,i=(c.flatDataSource(i||o).filter(function(e,t){return!a.getProps||!(a.getProps(e,t)||{}).disabled}).map(function(e){return e[r]}).forEach(function(e){-1===s.indexOf(e)?u=!1:d=!0}),t.onClick=b(function(e){e.stopPropagation()},t.onClick),a.titleProps&&a.titleProps()||{});return u&&(d=!1),["multiple"===l?p.default.createElement(h.default,(0,f.default)({key:"_total",indeterminate:d,"aria-label":n.selectAll,checked:u,onChange:e},t,i)):null,a.titleAddons&&a.titleAddons()]},c.renderSelectionBody=function(e,t,n){var a=c.props,r=a.rowSelection,a=a.primaryKey,o=c.state.selectedRowKeys,i=r.mode||"multiple",o=-1l.length&&(u=o),w(u.filter(function(e){return-1=Math.max(a-y,0)&&oc.clientHeight;this.isLock()?(e=this.bodyLeftNode,t=this.bodyRightNode,n=this.getWrapperNode("right"),a=f?d:0,c=c.offsetHeight-d,f||(r[l]=0,r[u]=0),+d?(r.marginBottom=-d,r.paddingBottom=d):(r.marginBottom=-20,r.paddingBottom=20),c={"max-height":c},o||+d||(c[u]=0),+d&&(c[u]=-d),e&&g.dom.setStyle(e,c),t&&g.dom.setStyle(t,c),n&&+d&&g.dom.setStyle(n,i?"left":"right",a+"px")):(r.marginBottom=-d,r.paddingBottom=d,r[u]=0,f||(r[l]=0)),s&&g.dom.setStyle(s,r)},a.prototype.adjustHeaderSize=function(){var o=this;this.isLock()&&this.tableInc.groupChildren.forEach(function(e,t){var n=o.tableInc.groupChildren[t].length-1,n=o.getHeaderCellNode(t,n),a=o.getHeaderCellNode(t,0),r=o.getHeaderCellNode(t,0,"right"),t=o.getHeaderCellNode(t,0,"left");n&&r&&(n=n.offsetHeight,g.dom.setStyle(r,"height",n),setTimeout(function(){var e=o.tableRightInc.affixRef;return e&&e.getInstance()&&e.getInstance().updatePosition()})),a&&t&&(r=a.offsetHeight,g.dom.setStyle(t,"height",r),setTimeout(function(){var e=o.tableLeftInc.affixRef;return e&&e.getInstance()&&e.getInstance().updatePosition()}))})},a.prototype.adjustRowHeight=function(){var n=this;this.isLock()&&this.tableInc.props.dataSource.forEach(function(e,t){t=""+("object"===(void 0===e?"undefined":(0,r.default)(e))&&"__rowIndex"in e?e.__rowIndex:t)+(e.__expanded?"_expanded":"");n.setRowHeight(t,"left"),n.setRowHeight(t,"right")})},a.prototype.setRowHeight=function(e,t){var t=this.getRowNode(e,t),e=this.getRowNode(e),e=(M?e&&e.offsetHeight:e&&parseFloat(getComputedStyle(e).height))||"auto",n=(M?t&&t.offsetHeight:t&&parseFloat(getComputedStyle(t).height))||"auto";t&&e!==n&&g.dom.setStyle(t,"height",e)},a.prototype.getWrapperNode=function(e){e=e?e.charAt(0).toUpperCase()+e.substr(1):"";try{return(0,u.findDOMNode)(this["lock"+e+"El"])}catch(e){return null}},a.prototype.getRowNode=function(e,t){t=this["table"+(t=t?t.charAt(0).toUpperCase()+t.substr(1):"")+"Inc"];try{return(0,u.findDOMNode)(t.getRowRef(e))}catch(e){return null}},a.prototype.getHeaderCellNode=function(e,t,n){n=this["table"+(n=n?n.charAt(0).toUpperCase()+n.substr(1):"")+"Inc"];try{return(0,u.findDOMNode)(n.getHeaderCellRef(e,t))}catch(e){return null}},a.prototype.getCellNode=function(e,t,n){n=this["table"+(n=n?n.charAt(0).toUpperCase()+n.substr(1):"")+"Inc"];try{return(0,u.findDOMNode)(n.getCellRef(e,t))}catch(e){return null}},a.prototype.render=function(){var e,t=this.props,n=(t.children,t.columns,t.prefix),a=t.components,r=t.className,o=t.dataSource,i=t.tableWidth,t=(0,f.default)(t,["children","columns","prefix","components","className","dataSource","tableWidth"]),s=this.normalizeChildrenState(this.props),l=s.lockLeftChildren,u=s.lockRightChildren,s=s.children,d={left:this.getFlatenChildrenLength(l),right:this.getFlatenChildrenLength(u),origin:this.getFlatenChildrenLength(s)};return this._notNeedAdjustLockLeft&&(l=[]),this._notNeedAdjustLockRight&&(u=[]),this.lockLeftChildren=l,this.lockRightChildren=u,this.isOriginLock()?((a=(0,p.default)({},a)).Body=a.Body||v.default,a.Header=a.Header||_.default,a.Wrapper=a.Wrapper||b.default,a.Row=a.Row||y.default,r=(0,m.default)(((e={})[n+"table-lock"]=!0,e[n+"table-wrap-empty"]=!o.length,e[r]=r,e)),e=[h.default.createElement(c,(0,p.default)({},t,{dataSource:o,key:"lock-left",columns:l,className:n+"table-lock-left",lengths:d,prefix:n,lockType:"left",components:a,ref:this.saveLockLeftRef,loading:!1,"aria-hidden":!0})),h.default.createElement(c,(0,p.default)({},t,{dataSource:o,key:"lock-right",columns:u,className:n+"table-lock-right",lengths:d,prefix:n,lockType:"right",components:a,ref:this.saveLockRightRef,loading:!1,"aria-hidden":!0}))],h.default.createElement(c,(0,p.default)({},t,{tableWidth:i,dataSource:o,columns:s,prefix:n,lengths:d,wrapperContent:e,components:a,className:r}))):h.default.createElement(c,this.props)},a}(h.default.Component),t.LockRow=y.default,t.LockBody=v.default,t.LockHeader=_.default,t.propTypes=(0,p.default)({scrollToCol:a.default.number,scrollToRow:a.default.number},c.propTypes),t.defaultProps=(0,p.default)({},c.defaultProps),t.childContextTypes={getTableInstance:a.default.func,getLockNode:a.default.func,onLockBodyScroll:a.default.func,onRowMouseEnter:a.default.func,onRowMouseLeave:a.default.func};var e,t=e;return t.displayName="LockTable",(0,w.statics)(t,c),t},n(0)),h=c(l),u=n(24),a=c(n(5)),m=c(n(17)),d=c(n(177)),g=n(11),y=c(n(179)),v=c(n(403)),_=c(n(404)),b=c(n(135)),w=n(70);function c(e){return e&&e.__esModule?e:{default:e}}var M=g.env.ieVersion;function k(e){return function n(e){return e.map(function(e){var t=(0,p.default)({},e);return e.children&&(e.children=n(e.children)),t})}(e)}e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var l=s(n(12)),u=s(n(2)),r=s(n(4)),o=s(n(6)),i=s(n(7)),d=(t.default=function(s){e=t=function(n){function a(e,t){(0,r.default)(this,a);var d=(0,o.default)(this,n.call(this,e));return d.state={},d.updateOffsetArr=function(){var e=d.splitChildren||{},t=e.lockLeftChildren,n=e.lockRightChildren,e=e.originChildren,a=d.getFlatenChildren(t).length,r=d.getFlatenChildren(n).length,e=a+r+d.getFlatenChildren(e).length,a=0r.top-e.offset?(t?(l.position="absolute",l.top=a-(r.top-e.offset),u="relative"):(l.position="fixed",l.top=e.offset+n.top),d._setAffixStyle(l,!0),d._setContainerStyle(s)):e.bottom&&a{e=new Date(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}},function(e,t,n){"use strict";const a=n(182);class r extends Date{constructor(e){super(e+"Z"),this.isFloating=!0}toISOString(){return`${this.getUTCFullYear()}-${a(2,this.getUTCMonth()+1)}-`+a(2,this.getUTCDate())+"T"+(`${a(2,this.getUTCHours())}:${a(2,this.getUTCMinutes())}:${a(2,this.getUTCSeconds())}.`+a(3,this.getUTCMilliseconds()))}}e.exports=e=>{e=new r(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}},function(a,e,r){"use strict";!function(e){const t=r(182);class n extends e.Date{constructor(e){super(e),this.isDate=!0}toISOString(){return`${this.getUTCFullYear()}-${t(2,this.getUTCMonth()+1)}-`+t(2,this.getUTCDate())}}a.exports=e=>{e=new n(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}}.call(this,r(65))},function(e,t,n){"use strict";const a=n(182);class r extends Date{constructor(e){super(`0000-01-01T${e}Z`),this.isTime=!0}toISOString(){return`${a(2,this.getUTCHours())}:${a(2,this.getUTCMinutes())}:${a(2,this.getUTCSeconds())}.`+a(3,this.getUTCMilliseconds())}}e.exports=e=>{e=new r(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}},function(e,t,n){"use strict";!function(s){e.exports=function(r,e){e=e||{};const n=e.blocksize||40960,o=new t;return new Promise((e,t)=>{s(i,0,n,e,t)});function i(e,t,n,a){if(e>=r.length)try{return n(o.finish())}catch(e){return a(l(e,r))}try{o.parse(r.slice(e,e+t)),s(i,e+t,t,n,a)}catch(e){a(l(e,r))}}};const t=n(181),l=n(183)}.call(this,n(408).setImmediate)},function(e,t,n){!function(e,p){!function(n,o){"use strict";var a,i,s,r,l,u,t,e;function d(e){delete i[e]}function c(e){if(s)setTimeout(c,0,e);else{var t=i[e];if(t){s=!0;try{var n=t,a=n.callback,r=n.args;switch(r.length){case 0:a();break;case 1:a(r[0]);break;case 2:a(r[0],r[1]);break;case 3:a(r[0],r[1],r[2]);break;default:a.apply(o,r)}}finally{d(e),s=!1}}}}function f(){function e(e){e.source===n&&"string"==typeof e.data&&0===e.data.indexOf(t)&&c(+e.data.slice(t.length))}var t="setImmediate$"+Math.random()+"$";n.addEventListener?n.addEventListener("message",e,!1):n.attachEvent("onmessage",e),l=function(e){n.postMessage(t+e,"*")}}n.setImmediate||(a=1,s=!(i={}),r=n.document,e=(e=Object.getPrototypeOf&&Object.getPrototypeOf(n))&&e.setTimeout?e:n,"[object process]"==={}.toString.call(n.process)?l=function(e){p.nextTick(function(){c(e)})}:!function(){var e,t;if(n.postMessage&&!n.importScripts)return e=!0,t=n.onmessage,n.onmessage=function(){e=!1},n.postMessage("","*"),n.onmessage=t,e}()?l=n.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):r&&"onreadystatechange"in r.createElement("script")?(u=r.documentElement,function(e){var t=r.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,u.removeChild(t),t=null},u.appendChild(t)}):function(e){setTimeout(c,0,e)}:f(),e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n{let n,a=!1,r=!1;function o(){if(a=!0,!n)try{e(l.finish())}catch(e){t(e)}}function i(e){r=!0,t(e)}s.once("end",o),s.once("error",i),function e(){n=!0;let t;for(;null!==(t=s.read());)try{l.parse(t)}catch(e){return i(e)}n=!1;if(a)return o();if(r)return;s.once("readable",e)}()})}(e):function(){const a=new o;return new r.Transform({objectMode:!0,transform(e,t,n){try{a.parse(e.toString(t))}catch(e){this.emit("error",e)}n()},flush(e){try{this.push(a.finish())}catch(e){this.emit("error",e)}e()}})}()};const r=n(691),o=n(181)},function(e,t,n){e.exports=a;var d=n(184).EventEmitter;function a(){d.call(this)}n(104)(a,d),a.Readable=n(185),a.Writable=n(701),a.Duplex=n(702),a.Transform=n(703),a.PassThrough=n(704),(a.Stream=a).prototype.pipe=function(t,e){var n=this;function a(e){t.writable&&!1===t.write(e)&&n.pause&&n.pause()}function r(){n.readable&&n.resume&&n.resume()}n.on("data",a),t.on("drain",r),t._isStdio||e&&!1===e.end||(n.on("end",i),n.on("close",s));var o=!1;function i(){o||(o=!0,t.end())}function s(){o||(o=!0,"function"==typeof t.destroy&&t.destroy())}function l(e){if(u(),0===d.listenerCount(this,"error"))throw e}function u(){n.removeListener("data",a),t.removeListener("drain",r),n.removeListener("end",i),n.removeListener("close",s),n.removeListener("error",l),t.removeListener("error",l),n.removeListener("end",u),n.removeListener("close",u),t.removeListener("close",u)}return n.on("error",l),t.on("error",l),n.on("end",u),n.on("close",u),t.on("close",u),t.emit("pipe",n),t}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){"use strict";t.byteLength=function(e){var e=d(e),t=e[0],e=e[1];return 3*(t+e)/4-e},t.toByteArray=function(e){var t,n,a=d(e),r=a[0],a=a[1],o=new u(function(e,t){return 3*(e+t)/4-t}(r,a)),i=0,s=0>16&255,o[i++]=t>>8&255,o[i++]=255&t;2===a&&(t=l[e.charCodeAt(n)]<<2|l[e.charCodeAt(n+1)]>>4,o[i++]=255&t);1===a&&(t=l[e.charCodeAt(n)]<<10|l[e.charCodeAt(n+1)]<<4|l[e.charCodeAt(n+2)]>>2,o[i++]=t>>8&255,o[i++]=255&t);return o},t.fromByteArray=function(e){for(var t,n=e.length,a=n%3,r=[],o=0,i=n-a;o>18&63]+s[e>>12&63]+s[e>>6&63]+s[63&e]}(a));return r.join("")}(e,o,i>2]+s[t<<4&63]+"==")):2==a&&(t=(e[n-2]<<8)+e[n-1],r.push(s[t>>10]+s[t>>4&63]+s[t<<2&63]+"="));return r.join("")};for(var s=[],l=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=0,o=a.length;r=e.length?{value:void 0,done:!0}:(e=a(e,t),this._i+=e.length,{value:e,done:!1})})},function(e,t,n){var o=n(150),i=n(149);e.exports=function(r){return function(e,t){var n,e=String(i(e)),t=o(t),a=e.length;return t<0||a<=t?r?"":void 0:(n=e.charCodeAt(t))<55296||56319=e.length?(this._t=void 0,r(1)):r(0,"keys"==t?n:"values"==t?e[n]:[n,e[n]])},"values"),o.Arguments=o.Array,a("keys"),a("values"),a("entries")},function(e,t){e.exports=function(){}},function(e,t){e.exports=function(e,t){return{value:t,done:!!e}}},function(e,t,n){e.exports={default:n(497),__esModule:!0}},function(e,t,n){n(498),n(503),n(504),n(505),e.exports=n(80).Symbol},function(I,A,e){"use strict";function a(e){var t=T[e]=_(M[E]);return t._k=e,t}function n(e,t){m(e);for(var n,a=B(t=g(t)),r=0,o=a.length;rr;)l(T,t=n[r++])||t==x||t==H||a.push(t);return a}function i(e){for(var t,n=e===O,a=X(n?L:g(e)),r=[],o=0;a.length>o;)!l(T,t=a[o++])||n&&!l(O,t)||r.push(T[t]);return r}var s=e(79),l=e(89),u=e(81),d=e(95),R=e(209),H=e(499).KEY,c=e(112),f=e(152),p=e(158),F=e(128),h=e(99),z=e(159),W=e(160),B=e(500),U=e(501),m=e(111),V=e(97),K=e(155),g=e(98),y=e(148),v=e(125),_=e(157),q=e(502),G=e(211),b=e(154),$=e(88),J=e(126),Q=G.f,w=$.f,X=q.f,M=s.Symbol,k=s.JSON,S=k&&k.stringify,E="prototype",x=h("_hidden"),Z=h("toPrimitive"),ee={}.propertyIsEnumerable,C=f("symbol-registry"),T=f("symbols"),L=f("op-symbols"),O=Object[E],f="function"==typeof M&&!!b.f,D=s.QObject,N=!D||!D[E]||!D[E].findChild,P=u&&c(function(){return 7!=_(w({},"a",{get:function(){return w(this,"a",{value:7}).a}})).a})?function(e,t,n){var a=Q(O,t);a&&delete O[t],w(e,t,n),a&&e!==O&&w(O,t,a)}:w,j=f&&"symbol"==typeof M.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof M},Y=function(e,t,n){return e===O&&Y(L,t,n),m(e),t=y(t,!0),m(n),(l(T,t)?(n.enumerable?(l(e,x)&&e[x][t]&&(e[x][t]=!1),n=_(n,{enumerable:v(0,!1)})):(l(e,x)||w(e,x,v(1,{})),e[x][t]=!0),P):w)(e,t,n)};f||(R((M=function(){if(this instanceof M)throw TypeError("Symbol is not a constructor!");var t=F(0ne;)h(te[ne++]);for(var ae=J(h.store),re=0;ae.length>re;)W(ae[re++]);d(d.S+d.F*!f,"Symbol",{for:function(e){return l(C,e+="")?C[e]:C[e]=M(e)},keyFor:function(e){if(!j(e))throw TypeError(e+" is not a symbol!");for(var t in C)if(C[t]===e)return t},useSetter:function(){N=!0},useSimple:function(){N=!1}}),d(d.S+d.F*!f,"Object",{create:function(e,t){return void 0===t?_(e):n(_(e),t)},defineProperty:Y,defineProperties:n,getOwnPropertyDescriptor:r,getOwnPropertyNames:o,getOwnPropertySymbols:i});D=c(function(){b.f(1)});d(d.S+d.F*D,"Object",{getOwnPropertySymbols:function(e){return b.f(K(e))}}),k&&d(d.S+d.F*(!f||c(function(){var e=M();return"[null]"!=S([e])||"{}"!=S({a:e})||"{}"!=S(Object(e))})),"JSON",{stringify:function(e){for(var t,n,a=[e],r=1;ri;)o.call(e,a=r[i++])&&t.push(a);return t}},function(e,t,n){var a=n(207);e.exports=Array.isArray||function(e){return"Array"==a(e)}},function(e,t,n){var a=n(98),r=n(210).f,o={}.toString,i="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){if(!i||"[object Window]"!=o.call(e))return r(a(e));try{return r(e)}catch(e){return i.slice()}}},function(e,t){},function(e,t,n){n(160)("asyncIterator")},function(e,t,n){n(160)("observable")},function(e,t,n){e.exports={default:n(507),__esModule:!0}},function(e,t,n){n(508),e.exports=n(80).Object.setPrototypeOf},function(e,t,n){var a=n(95);a(a.S,"Object",{setPrototypeOf:n(509).set})},function(e,t,r){function o(e,t){if(a(e),!n(t)&&null!==t)throw TypeError(t+": can't set as prototype!")}var n=r(97),a=r(111);e.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(e,n,a){try{(a=r(202)(Function.call,r(211).f(Object.prototype,"__proto__").set,2))(e,[]),n=!(e instanceof Array)}catch(e){n=!0}return function(e,t){return o(e,t),n?e.__proto__=t:a(e,t),e}}({},!1):void 0),check:o}},function(e,t,n){e.exports={default:n(511),__esModule:!0}},function(e,t,n){n(512);var a=n(80).Object;e.exports=function(e,t){return a.create(e,t)}},function(e,t,n){var a=n(95);a(a.S,"Object",{create:n(157)})},function(e,t,n){"use strict";var i=n(514);function a(){}function r(){}r.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,r,o){if(o!==i)throw(o=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types")).name="Invariant Violation",o}function t(){return e}var n={array:e.isRequired=e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:r,resetWarningCache:a};return n.PropTypes=n}},function(e,t,n){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},function(e,t,n){"use strict";function l(e,t,n,a){e.removeEventListener&&e.removeEventListener(t,n,a||!1)}function a(e,t,n,a){return e.addEventListener&&e.addEventListener(t,n,a||!1),{off:function(){return l(e,t,n,a)}}}t.__esModule=!0,t.on=a,t.once=function(r,o,i,s){return a(r,o,function e(){for(var t=arguments.length,n=Array(t),a=0;a68?1900:2e3)},r=function(t){return function(e){this[t]=+e}},o=[/[+-]\d\d:?(\d\d)?|Z/,function(e){(this.zone||(this.zone={})).offset=function(e){if(!e)return 0;if("Z"===e)return 0;var t=e.match(/([+-]|\d\d)/g),n=60*t[1]+(+t[2]||0);return 0===n?0:"+"===t[0]?-n:n}(e)}],i=function(e){var t=h[e];return t&&(t.indexOf?t:t.s.concat(t.f))},s=function(e,t){var n,a=h.meridiem;if(a){for(var r=1;r<=24;r+=1)if(e.indexOf(a(r,0,t))>-1){n=r>12;break}}else n=e===(t?"pm":"PM");return n},f={A:[n,function(e){this.afternoon=s(e,!1)}],a:[n,function(e){this.afternoon=s(e,!0)}],S:[/\d/,function(e){this.milliseconds=100*+e}],SS:[e,function(e){this.milliseconds=10*+e}],SSS:[/\d{3}/,function(e){this.milliseconds=+e}],s:[t,r("seconds")],ss:[t,r("seconds")],m:[t,r("minutes")],mm:[t,r("minutes")],H:[t,r("hours")],h:[t,r("hours")],HH:[t,r("hours")],hh:[t,r("hours")],D:[t,r("day")],DD:[e,r("day")],Do:[n,function(e){var t=h.ordinal,n=e.match(/\d+/);if(this.day=n[0],t)for(var a=1;a<=31;a+=1)t(a).replace(/\[|\]/g,"")===e&&(this.day=a)}],M:[t,r("month")],MM:[e,r("month")],MMM:[n,function(e){var t=i("months"),n=(i("monthsShort")||t.map(function(e){return e.slice(0,3)})).indexOf(e)+1;if(n<1)throw new Error;this.month=n%12||n}],MMMM:[n,function(e){var t=i("months").indexOf(e)+1;if(t<1)throw new Error;this.month=t%12||t}],Y:[/[+-]?\d+/,r("year")],YY:[e,function(e){this.year=a(e)}],YYYY:[/\d{4}/,r("year")],Z:o,ZZ:o};function b(e){var t,r;t=e,r=h&&h.formats;for(var u=(e=t.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,function(e,t,n){var a=n&&n.toUpperCase();return t||r[n]||l[n]||r[a].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,function(e,t,n){return t||n.slice(1)})})).match(c),d=u.length,n=0;n-1)return new Date(("X"===t?1e3:1)*e);var a=b(t)(e),r=a.year,o=a.month,i=a.day,s=a.hours,l=a.minutes,u=a.seconds,d=a.milliseconds,c=a.zone,f=new Date,p=i||(r||o?1:f.getDate()),h=r||f.getFullYear(),m=0;r&&!o||(m=o>0?o-1:f.getMonth());var g=s||0,y=l||0,v=u||0,_=d||0;return c?new Date(Date.UTC(h,m,p,g,y,v,_+60*c.offset*1e3)):n?new Date(Date.UTC(h,m,p,g,y,v,_)):new Date(h,m,p,g,y,v,_)}catch(e){return new Date("")}}(t,r,n),this.init(),l&&!0!==l&&(this.$L=this.locale(l).$L),s&&t!=this.format(r)&&(this.$d=new Date("")),h={}}else if(r instanceof Array)for(var u=r.length,d=1;d<=u;d+=1){a[1]=r[d-1];var c=f.apply(this,a);if(c.isValid()){this.$d=c.$d,this.$L=c.$L,this.init();break}d===u&&(this.$d=new Date(""))}else p.call(this,e)}}}()},function(e,t,n){e.exports=function(){"use strict";return function(e,t,a){a.updateLocale=function(e,t){var n=a.Ls[e];if(n)return(t?Object.keys(t):[]).forEach(function(e){n[e]=t[e]}),n}}}()},function(e,t,n){e.exports=function(e,t,n){function a(e,t,n,a,r){var o,e=e.name?e:e.$locale(),t=s(e[t]),n=s(e[n]),i=t||n.map(function(e){return e.slice(0,a)});return r?(o=e.weekStart,i.map(function(e,t){return i[(t+(o||0))%7]})):i}function r(){return n.Ls[n.locale()]}function o(e,t){return e.formats[t]||e.formats[t.toUpperCase()].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,function(e,t,n){return t||n.slice(1)})}var t=t.prototype,s=function(e){return e&&(e.indexOf?e:e.s)};t.localeData=function(){return function(){var t=this;return{months:function(e){return e?e.format("MMMM"):a(t,"months")},monthsShort:function(e){return e?e.format("MMM"):a(t,"monthsShort","months",3)},firstDayOfWeek:function(){return t.$locale().weekStart||0},weekdays:function(e){return e?e.format("dddd"):a(t,"weekdays")},weekdaysMin:function(e){return e?e.format("dd"):a(t,"weekdaysMin","weekdays",2)},weekdaysShort:function(e){return e?e.format("ddd"):a(t,"weekdaysShort","weekdays",3)},longDateFormat:function(e){return o(t.$locale(),e)},meridiem:this.$locale().meridiem,ordinal:this.$locale().ordinal}}.bind(this)()},n.localeData=function(){var t=r();return{firstDayOfWeek:function(){return t.weekStart||0},weekdays:function(){return n.weekdays()},weekdaysShort:function(){return n.weekdaysShort()},weekdaysMin:function(){return n.weekdaysMin()},months:function(){return n.months()},monthsShort:function(){return n.monthsShort()},longDateFormat:function(e){return o(t,e)},meridiem:t.meridiem,ordinal:t.ordinal}},n.months=function(){return a(r(),"months")},n.monthsShort=function(){return a(r(),"monthsShort","months",3)},n.weekdays=function(e){return a(r(),"weekdays",null,null,e)},n.weekdaysShort=function(e){return a(r(),"weekdaysShort","weekdays",3,e)},n.weekdaysMin=function(e){return a(r(),"weekdaysMin","weekdays",2,e)}}},function(e,t,n){e.exports=function(){"use strict";var i="month",s="quarter";return function(e,t){var n=t.prototype;n.quarter=function(e){return this.$utils().u(e)?Math.ceil((this.month()+1)/3):this.month(this.month()%3+3*(e-1))};var a=n.add;n.add=function(e,t){return e=Number(e),this.$utils().p(t)===s?this.add(3*e,i):a.bind(this)(e,t)};var o=n.startOf;n.startOf=function(e,t){var n=this.$utils(),a=!!n.u(t)||t;if(n.p(e)===s){var r=this.quarter()-1;return a?this.month(3*r).startOf(i).startOf("day"):this.month(3*r+2).endOf(i).endOf("day")}return o.bind(this)(e,t)}}}()},function(e,t,n){e.exports=function(){"use strict";return function(e,t){var n=t.prototype,o=n.format;n.format=function(e){var t=this,n=this.$locale();if(!this.isValid())return o.bind(this)(e);var a=this.$utils(),r=(e||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,function(e){switch(e){case"Q":return Math.ceil((t.$M+1)/3);case"Do":return n.ordinal(t.$D);case"gggg":return t.weekYear();case"GGGG":return t.isoWeekYear();case"wo":return n.ordinal(t.week(),"W");case"w":case"ww":return a.s(t.week(),"w"===e?1:2,"0");case"W":case"WW":return a.s(t.isoWeek(),"W"===e?1:2,"0");case"k":case"kk":return a.s(String(0===t.$H?24:t.$H),"k"===e?1:2,"0");case"X":return Math.floor(t.$d.getTime()/1e3);case"x":return t.$d.getTime();case"z":return"["+t.offsetName()+"]";case"zzz":return"["+t.offsetName("long")+"]";default:return e}});return o.bind(this)(r)}}}()},function(e,t,n){e.exports=function(){"use strict";var s="week",l="year";return function(e,t,i){var n=t.prototype;n.week=function(e){if(void 0===e&&(e=null),null!==e)return this.add(7*(e-this.week()),"day");var t=this.$locale().yearStart||1;if(11===this.month()&&this.date()>25){var n=i(this).startOf(l).add(1,l).date(t),a=i(this).endOf(s);if(n.isBefore(a))return 1}var r=i(this).startOf(l).date(t).startOf(s).subtract(1,"millisecond"),o=this.diff(r,s,!0);return o<0?i(this).startOf("week").week():Math.ceil(o)},n.weeks=function(e){return void 0===e&&(e=null),this.week(e)}}}()},function(e,t,n){e.exports=function(e){"use strict";function t(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var n=t(e),a={name:"zh-cn",weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),ordinal:function(e,t){return"W"===t?e+"周":e+"日"},weekStart:1,yearStart:4,formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah点mm分",LLLL:"YYYY年M月D日ddddAh点mm分",l:"YYYY/M/D",ll:"YYYY年M月D日",lll:"YYYY年M月D日 HH:mm",llll:"YYYY年M月D日dddd HH:mm"},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1 分钟",mm:"%d 分钟",h:"1 小时",hh:"%d 小时",d:"1 天",dd:"%d 天",M:"1 个月",MM:"%d 个月",y:"1 年",yy:"%d 年"},meridiem:function(e,t){var n=100*e+t;return n<600?"凌晨":n<900?"早上":n<1100?"上午":n<1300?"中午":n<1800?"下午":"晚上"}};return n.default.locale(a,null,!0),a}(n(217))},function(e,t,n){"use strict";t.__esModule=!0,t.flex=t.transition=t.animation=void 0;var r=n(213),o=n(100);function a(e){var n,a;return!!r.hasDOM&&(n=document.createElement("div"),(a=!1,o.each)(e,function(e,t){if(void 0!==n.style[t])return!(a={end:e})}),a)}var i,s;t.animation=a({WebkitAnimation:"webkitAnimationEnd",OAnimation:"oAnimationEnd",animation:"animationend"}),t.transition=a({WebkitTransition:"webkitTransitionEnd",OTransition:"oTransitionEnd",transition:"transitionend"}),t.flex=(n={display:["flex","-webkit-flex","-moz-flex","-ms-flexbox"]},!!r.hasDOM&&(i=document.createElement("div"),(s=!1,o.each)(n,function(e,t){return(0,o.each)(e,function(e){try{i.style[t]=e,s=s||i.style[t]===e}catch(e){}return!s}),!s}),s))},function(e,t,n){"use strict";t.__esModule=!0,t.getFocusNodeList=i,t.saveLastFocusNode=function(){s=document.activeElement},t.clearLastFocusNode=function(){s=null},t.backLastFocusNode=function(){if(s)try{s.focus()}catch(e){}},t.limitTabRange=function(e,t){{var n,a;t.keyCode===r.default.TAB&&(e=i(e),n=e.length-1,-1<(a=e.indexOf(document.activeElement)))&&(a=a+(t.shiftKey?-1:1),e[a=n<(a=a<0?n:a)?0:a].focus(),t.preventDefault())}};var t=n(218),r=(t=t)&&t.__esModule?t:{default:t},a=n(100);function o(e){var t=e.nodeName.toLowerCase(),n=parseInt(e.getAttribute("tabindex"),10),n=!isNaN(n)&&-1a.height)&&(r[1]=-t.top-("t"===e?t.height:0)),r},this._getParentScrollOffset=function(e){var t=0,n=0;return e&&e.offsetParent&&e.offsetParent!==document.body&&(isNaN(e.offsetParent.scrollTop)||(t+=e.offsetParent.scrollTop),isNaN(e.offsetParent.scrollLeft)||(n+=e.offsetParent.scrollLeft)),{top:t,left:n}}};var p=a;function h(e){(0,o.default)(this,h),r.call(this),this.pinElement=e.pinElement,this.baseElement=e.baseElement,this.pinFollowBaseElementWhenFixed=e.pinFollowBaseElementWhenFixed,this.container=function(e){var t=e.container,e=e.baseElement;if("undefined"==typeof document)return t;for(var n=(n=(0,i.default)(t,e))||document.body;"static"===y.dom.getStyle(n,"position");){if(!n||n===document.body)return document.body;n=n.parentNode}return n}(e),this.autoFit=e.autoFit||!1,this.align=e.align||"tl tl",this.offset=e.offset||[0,0],this.needAdjust=e.needAdjust||!1,this.isRtl=e.isRtl||!1}t.default=p,e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var w=a(n(2)),M=a(n(12)),k=n(0),S=a(k),E=a(n(17)),x=a(n(194)),C=a(n(82)),T=n(11);function a(e){return e&&e.__esModule?e:{default:e}}t.default=function(e){var t,n,a,r,o,i,s,l,u,d,c,f,p,h,m,g,y,v,_,b;return k.useState&&k.useRef&&k.useEffect?(t=void 0===(t=e.prefix)?"next-":t,r=e.animation,n=void 0===r?{in:"expandInDown",out:"expandOutUp"}:r,a=e.visible,r=e.hasMask,o=e.align,o=void 0===(s=e.points)?o?o.split(" "):void 0:s,i=e.onPosition,s=e.children,b=e.className,l=e.style,u=e.wrapperClassName,d=e.beforeOpen,c=e.onOpen,f=e.afterOpen,p=e.beforeClose,h=e.onClose,m=e.afterClose,e=(0,M.default)(e,["prefix","animation","visible","hasMask","align","points","onPosition","children","className","style","wrapperClassName","beforeOpen","onOpen","afterOpen","beforeClose","onClose","afterClose"]),g=(_=(0,k.useState)(!0))[0],y=_[1],v=(0,k.useRef)(null),_=S.default.createElement(C.default.OverlayAnimate,{visible:a,animation:n,onEnter:function(){y(!1),"function"==typeof d&&d(v.current)},onEntering:function(){"function"==typeof c&&c(v.current)},onEntered:function(){"function"==typeof f&&f(v.current)},onExit:function(){"function"==typeof p&&p(v.current)},onExiting:function(){"function"==typeof h&&h(v.current)},onExited:function(){y(!0),"function"==typeof m&&m(v.current)},timeout:300,style:l},s?(0,k.cloneElement)(s,{className:(0,E.default)([t+"overlay-inner",b,s&&s.props&&s.props.className])}):S.default.createElement("span",null)),b=(0,E.default)(((l={})[t+"overlay-wrapper v2"]=!0,l[u]=u,l.opened=a,l)),S.default.createElement(x.default,(0,w.default)({},e,{visible:a,isAnimationEnd:g,hasMask:r,wrapperClassName:b,maskClassName:t+"overlay-backdrop",maskRender:function(e){return S.default.createElement(C.default.OverlayAnimate,{visible:a,animation:!!n&&{in:"fadeIn",out:"fadeOut"},timeout:300,unmountOnExit:!0},e)},points:o,onPosition:function(e){(0,w.default)(e,{align:e.config.points}),"function"==typeof i&&i(e)},ref:v}),_)):(T.log.warning("need react version > 16.8.0"),null)},e.exports=t.default},function(n,e){function a(e,t){return n.exports=a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},n.exports.__esModule=!0,n.exports.default=n.exports,a(e,t)}n.exports=a,n.exports.__esModule=!0,n.exports.default=n.exports},function(e,t,n){"use strict";t.__esModule=!0;var a,d=g(n(12)),c=g(n(2)),r=g(n(4)),o=g(n(6)),i=g(n(7)),l=n(0),f=g(l),p=n(24),s=n(30),u=g(n(5)),h=n(11),m=g(n(360));function g(e){return e&&e.__esModule?e:{default:e}}var y,n=h.func.noop,v=h.func.makeChain,_=h.func.bindCtx,u=(y=l.Component,(0,i.default)(b,y),b.getDerivedStateFromProps=function(e,t){return"visible"in e?(0,c.default)({},t,{visible:e.visible}):null},b.prototype.componentWillUnmount=function(){var t=this;["_timer","_hideTimer","_showTimer"].forEach(function(e){t[e]&&clearTimeout(t[e])})},b.prototype.handleVisibleChange=function(e,t,n){"visible"in this.props||this.setState({visible:e}),this.props.onVisibleChange(e,t,n)},b.prototype.handleTriggerClick=function(e){this.state.visible&&!this.props.canCloseByTrigger||this.handleVisibleChange(!this.state.visible,"fromTrigger",e)},b.prototype.handleTriggerKeyDown=function(e){var t=this.props.triggerClickKeycode;(Array.isArray(t)?t:[t]).includes(e.keyCode)&&(e.preventDefault(),this.handleTriggerClick(e))},b.prototype.handleTriggerMouseEnter=function(e){var t=this;this._mouseNotFirstOnMask=!1,this._hideTimer&&(clearTimeout(this._hideTimer),this._hideTimer=null),this._showTimer&&(clearTimeout(this._showTimer),this._showTimer=null),this.state.visible||(this._showTimer=setTimeout(function(){t.handleVisibleChange(!0,"fromTrigger",e)},this.props.delay))},b.prototype.handleTriggerMouseLeave=function(e,t){var n=this;this._showTimer&&(clearTimeout(this._showTimer),this._showTimer=null),this.state.visible&&(this._hideTimer=setTimeout(function(){n.handleVisibleChange(!1,t||"fromTrigger",e)},this.props.delay))},b.prototype.handleTriggerFocus=function(e){this.handleVisibleChange(!0,"fromTrigger",e)},b.prototype.handleTriggerBlur=function(e){this._isForwardContent||this.handleVisibleChange(!1,"fromTrigger",e),this._isForwardContent=!1},b.prototype.handleContentMouseDown=function(){this._isForwardContent=!0},b.prototype.handleContentMouseEnter=function(){clearTimeout(this._hideTimer)},b.prototype.handleContentMouseLeave=function(e){this.handleTriggerMouseLeave(e,"fromContent")},b.prototype.handleMaskMouseEnter=function(){this._mouseNotFirstOnMask||(clearTimeout(this._hideTimer),this._hideTimer=null,this._mouseNotFirstOnMask=!1)},b.prototype.handleMaskMouseLeave=function(){this._mouseNotFirstOnMask=!0},b.prototype.handleRequestClose=function(e,t){this.handleVisibleChange(!1,e,t)},b.prototype.renderTrigger=function(){var e,t,n,a,r,o,i,s=this,l=this.props,u=l.trigger,l=l.disabled,d={key:"trigger","aria-haspopup":!0,"aria-expanded":this.state.visible};return this.state.visible||(d["aria-describedby"]=void 0),l||(l=this.props.triggerType,l=Array.isArray(l)?l:[l],e=u&&u.props||{},t=e.onClick,n=e.onKeyDown,a=e.onMouseEnter,r=e.onMouseLeave,o=e.onFocus,i=e.onBlur,l.forEach(function(e){switch(e){case"click":d.onClick=v(s.handleTriggerClick,t),d.onKeyDown=v(s.handleTriggerKeyDown,n);break;case"hover":d.onMouseEnter=v(s.handleTriggerMouseEnter,a),d.onMouseLeave=v(s.handleTriggerMouseLeave,r);break;case"focus":d.onFocus=v(s.handleTriggerFocus,o),d.onBlur=v(s.handleTriggerBlur,i)}})),u&&f.default.cloneElement(u,d)},b.prototype.renderContent=function(){var t=this,e=this.props,n=e.children,e=e.triggerType,e=Array.isArray(e)?e:[e],n=l.Children.only(n),a=n.props,r=a.onMouseDown,o=a.onMouseEnter,i=a.onMouseLeave,s={key:"portal"};return e.forEach(function(e){switch(e){case"focus":s.onMouseDown=v(t.handleContentMouseDown,r);break;case"hover":s.onMouseEnter=v(t.handleContentMouseEnter,o),s.onMouseLeave=v(t.handleContentMouseLeave,i)}}),f.default.cloneElement(n,s)},b.prototype.renderPortal=function(){function e(){return(0,p.findDOMNode)(t)}var t=this,n=this.props,a=n.target,r=n.safeNode,o=n.followTrigger,i=n.triggerType,s=n.hasMask,l=n.wrapperStyle,n=(0,d.default)(n,["target","safeNode","followTrigger","triggerType","hasMask","wrapperStyle"]),u=this.props.container,r=Array.isArray(r)?[].concat(r):[r],l=(r.unshift(e),l||{});return o&&(u=function(e){return e&&e.parentNode||e},l.position="relative"),"hover"===i&&s&&(n.onMaskMouseEnter=this.handleMaskMouseEnter,n.onMaskMouseLeave=this.handleMaskMouseLeave),f.default.createElement(m.default,(0,c.default)({},n,{key:"overlay",ref:function(e){return t.overlay=e},visible:this.state.visible,target:a||e,container:u,safeNode:r,wrapperStyle:l,triggerType:i,hasMask:s,onRequestClose:this.handleRequestClose}),this.props.children&&this.renderContent())},b.prototype.render=function(){return[this.renderTrigger(),this.renderPortal()]},a=i=b,i.propTypes={children:u.default.node,trigger:u.default.element,triggerType:u.default.oneOfType([u.default.string,u.default.array]),triggerClickKeycode:u.default.oneOfType([u.default.number,u.default.array]),visible:u.default.bool,defaultVisible:u.default.bool,onVisibleChange:u.default.func,disabled:u.default.bool,autoFit:u.default.bool,delay:u.default.number,canCloseByTrigger:u.default.bool,target:u.default.any,safeNode:u.default.any,followTrigger:u.default.bool,container:u.default.any,hasMask:u.default.bool,wrapperStyle:u.default.object,rtl:u.default.bool,v2:u.default.bool,placement:u.default.string,placementOffset:u.default.number,autoAdjust:u.default.bool},i.defaultProps={triggerType:"hover",triggerClickKeycode:[h.KEYCODE.SPACE,h.KEYCODE.ENTER],defaultVisible:!1,onVisibleChange:n,disabled:!1,autoFit:!1,delay:200,canCloseByTrigger:!0,followTrigger:!1,container:function(){return document.body},rtl:!1},a);function b(e){(0,r.default)(this,b);var t=(0,o.default)(this,y.call(this,e));return t.state={visible:void 0===e.visible?e.defaultVisible:e.visible},_(t,["handleTriggerClick","handleTriggerKeyDown","handleTriggerMouseEnter","handleTriggerMouseLeave","handleTriggerFocus","handleTriggerBlur","handleContentMouseEnter","handleContentMouseLeave","handleContentMouseDown","handleRequestClose","handleMaskMouseEnter","handleMaskMouseLeave"]),t}u.displayName="Popup",t.default=(0,s.polyfill)(u),e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var L=a(n(2)),O=a(n(12)),D=n(0),N=a(D),P=a(n(17)),j=a(n(194)),Y=a(n(82)),I=n(11);function a(e){return e&&e.__esModule?e:{default:e}}t.default=function(r){var e,t,o,n,a,i,s,l,u,d,c,f,p,h,m,g,y,v,_,b,w,M,k,S,E,x,C,T;return D.useState&&D.useRef&&D.useEffect?(e=void 0===(e=r.prefix)?"next-":e,E=r.animation,t=void 0===E?{in:"expandInDown",out:"expandOutUp"}:E,E=r.defaultVisible,x=r.onVisibleChange,o=void 0===x?function(){}:x,x=r.trigger,n=void 0===(n=r.triggerType)?"hover":n,C=r.overlay,a=r.onPosition,T=r.children,i=r.className,s=r.style,l=r.wrapperClassName,u=r.triggerClickKeycode,d=r.align,c=r.beforeOpen,f=r.onOpen,p=r.afterOpen,h=r.beforeClose,m=r.onClose,g=r.afterClose,y=(0,O.default)(r,["prefix","animation","defaultVisible","onVisibleChange","trigger","triggerType","overlay","onPosition","children","className","style","wrapperClassName","triggerClickKeycode","align","beforeOpen","onOpen","afterOpen","beforeClose","onClose","afterClose"]),E=(0,D.useState)(E),v=E[0],_=E[1],E=(0,D.useState)(t),b=E[0],w=E[1],M=(E=(0,D.useState)(!0))[0],k=E[1],S=(0,D.useRef)(null),(0,D.useEffect)(function(){"visible"in r&&_(r.visible)},[r.visible]),(0,D.useEffect)(function(){"animation"in r&&b!==t&&w(t)},[t]),E=C?T:x,x=N.default.createElement(Y.default.OverlayAnimate,{visible:v,animation:b,timeout:200,onEnter:function(){k(!1),"function"==typeof c&&c(S.current)},onEntering:function(){"function"==typeof f&&f(S.current)},onEntered:function(){"function"==typeof p&&p(S.current)},onExit:function(){"function"==typeof h&&h(S.current)},onExiting:function(){"function"==typeof m&&m(S.current)},onExited:function(){k(!0),"function"==typeof g&&g(S.current)},style:s},(x=C||T)?(0,D.cloneElement)(x,{className:(0,P.default)([e+"overlay-inner",i,x&&x.props&&x.props.className])}):N.default.createElement("span",null)),C=(0,P.default)(((s={})[e+"overlay-wrapper v2"]=!0,s[l]=l,s.opened=v,s)),T={},d&&(T.points=d.split(" ")),N.default.createElement(j.default.Popup,(0,L.default)({},y,T,{wrapperClassName:C,overlay:x,visible:v,isAnimationEnd:M,triggerType:n,onVisibleChange:function(e){for(var t=arguments.length,n=Array(1 16.8.0"),null)},e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var l=a(n(2)),u=a(n(12)),o=n(0),d=a(o),i=a(n(24)),s=a(n(8)),c=a(n(82)),f=a(n(162)),p=n(11);function a(e){return e&&e.__esModule?e:{default:e}}var h={top:8,maxCount:0,duration:3e3},m=s.default.config(function(e){var t=e.prefix,s=void 0===t?"next-":t,t=e.dataSource,a=void 0===t?[]:t,r=(0,o.useState)()[1];return a.forEach(function(n){n.timer||(n.timer=setTimeout(function(){var e,t=a.indexOf(n);-1a&&y.shift(),i.default.render(d.default.createElement(s.default,s.default.getContext(),d.default.createElement(m,{dataSource:y})),g),{key:n,close:function(){r.timer&&clearTimeout(r.timer);var e=y.indexOf(r);-1 16.8.0")}},e.exports=t.default},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";n(562)},function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var p=l(n(2)),r=l(n(4)),o=l(n(6)),a=l(n(7)),h=l(n(42)),m=l(n(0)),i=l(n(5)),g=l(n(17)),y=n(11),s=l(n(28)),v=l(n(366));function l(e){return e&&e.__esModule?e:{default:e}}function _(e,r){var o=r.size,i=r.device,s=r.labelAlign,l=r.labelTextAlign,u=r.labelCol,d=r.wrapperCol,c=r.responsive,f=r.colon;return m.default.Children.map(e,function(e){var t,n,a;return y.obj.isReactFragment(e)?_(e.props.children,r):e&&-1<["function","object"].indexOf((0,h.default)(e.type))&&"form_item"===e.type._typeMark?(t={labelCol:e.props.labelCol||u,wrapperCol:e.props.wrapperCol||d,labelAlign:e.props.labelAlign||("phone"===i?"top":s),labelTextAlign:e.props.labelTextAlign||l,colon:"colon"in e.props?e.props.colon:f,size:e.props.size||o,responsive:c},m.default.cloneElement(e,(n=t,a={},Object.keys(n).forEach(function(e){void 0!==n[e]&&(a[e]=n[e])}),a))):e})}u=m.default.Component,(0,a.default)(b,u),b.prototype.getChildContext=function(){return{_formField:this.props.field||this._formField,_formSize:this.props.size,_formDisabled:this.props.disabled,_formPreview:this.props.isPreview,_formFullWidth:this.props.fullWidth,_formLabelForErrorMessage:this.props.useLabelForErrorMessage}},b.prototype.componentDidUpdate=function(e){var t=this.props;this._formField&&("value"in t&&t.value!==e.value&&this._formField.setValues(t.value),"error"in t)&&t.error!==e.error&&this._formField.setValues(t.error)},b.prototype.render=function(){var e=this.props,t=e.className,n=e.inline,a=e.size,r=(e.device,e.labelAlign,e.labelTextAlign,e.onSubmit),o=e.children,i=(e.labelCol,e.wrapperCol,e.style),s=e.prefix,l=e.rtl,u=e.isPreview,d=e.component,c=e.responsive,f=e.gap,n=(e.colon,(0,g.default)(((e={})[s+"form"]=!0,e[s+"inline"]=n,e[""+s+a]=a,e[s+"form-responsive-grid"]=c,e[s+"form-preview"]=u,e[t]=!!t,e))),a=_(o,this.props);return m.default.createElement(d,(0,p.default)({role:"grid"},y.obj.pickOthers(b.propTypes,this.props),{className:n,style:i,dir:l?"rtl":void 0,onSubmit:r}),c?m.default.createElement(v.default,{gap:f},a):a)},a=n=b,n.propTypes={prefix:i.default.string,inline:i.default.bool,size:i.default.oneOf(["large","medium","small"]),fullWidth:i.default.bool,labelAlign:i.default.oneOf(["top","left","inset"]),labelTextAlign:i.default.oneOf(["left","right"]),field:i.default.any,saveField:i.default.func,labelCol:i.default.object,wrapperCol:i.default.object,onSubmit:i.default.func,children:i.default.any,className:i.default.string,style:i.default.object,value:i.default.object,onChange:i.default.func,component:i.default.oneOfType([i.default.string,i.default.func]),fieldOptions:i.default.object,rtl:i.default.bool,device:i.default.oneOf(["phone","tablet","desktop"]),responsive:i.default.bool,isPreview:i.default.bool,useLabelForErrorMessage:i.default.bool,colon:i.default.bool,disabled:i.default.bool,gap:i.default.oneOfType([i.default.arrayOf(i.default.number),i.default.number])},n.defaultProps={prefix:"next-",onSubmit:function(e){e.preventDefault()},size:"medium",labelAlign:"left",onChange:y.func.noop,component:"form",saveField:y.func.noop,device:"desktop",colon:!1,disabled:!1},n.childContextTypes={_formField:i.default.object,_formSize:i.default.string,_formDisabled:i.default.bool,_formPreview:i.default.bool,_formFullWidth:i.default.bool,_formLabelForErrorMessage:i.default.bool};var u,n=a;function b(e){(0,r.default)(this,b);var t,n,a=(0,o.default)(this,u.call(this,e));return a.onChange=function(e,t){a.props.onChange(a._formField.getValues(),{name:e,value:t,field:a._formField})},a._formField=null,!1!==e.field&&(t=(0,p.default)({},e.fieldOptions,{onChange:a.onChange}),e.field?(a._formField=e.field,n=a._formField.options.onChange,t.onChange=y.func.makeChain(n,a.onChange),a._formField.setOptions&&a._formField.setOptions(t)):("value"in e&&(t.values=e.value),a._formField=new s.default(a,t)),e.locale&&e.locale.Validate&&a._formField.setOptions({messages:e.locale.Validate}),e.saveField(a._formField)),a}n.displayName="Form",t.default=n,e.exports=t.default},function(e,t,n){"use strict";var a=n(90),m=(Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0,a(n(166))),i=a(n(567)),r=a(n(167)),o=a(n(115)),b=a(n(168)),w=a(n(76)),s=a(n(364)),l=a(n(365)),g=a(n(574)),M=n(583),u={state:"",valueName:"value",trigger:"onChange",inputValues:[]},a=function(){function a(e){var t=this,n=1e.length)&&(t=e.length);for(var n=0,a=new Array(t);n=a.length?n:(o=a[r],n=e(t&&t[o],n,a,r+1),t?Array.isArray(t)?((a=[].concat(t))[o]=n,a):(0,l.default)({},t,(0,i.default)({},o,n)):((r=isNaN(o)?{}:[])[o]=n,r))};t=function(){};void 0!==e&&e.env,n.warning=t}.call(this,r(102))},function(e,t,n){"use strict";t.__esModule=!0,t.cloneAndAddKey=function(e){{var t;if(e&&(0,a.isValidElement)(e))return t=e.key||"error",(0,a.cloneElement)(e,{key:t})}return e},t.scrollToFirstError=function(e){var t=e.errorsGroup,n=e.options,a=e.instance;if(t&&n.scrollToFirstError){var r,o=void 0,i=void 0;for(r in t)if(t.hasOwnProperty(r)){var s=u.default.findDOMNode(a[r]);if(!s)return;var l=s.offsetTop;(void 0===i||l), use instead of.'),S.default.cloneElement(e,{className:t,size:c||C(r)})):(0,k.isValidElement)(e)?e:S.default.createElement("span",{className:a+"btn-helper"},e)}),t=d,_=(0,b.default)({},x.obj.pickOthers(Object.keys(T.propTypes),e),{type:o,disabled:p,onClick:h,className:(0,E.default)(n)});return"button"!==t&&(delete _.type,_.disabled)&&(delete _.onClick,_.href)&&delete _.href,S.default.createElement(t,(0,b.default)({},_,{dir:g?"rtl":void 0,onMouseUp:this.onMouseUp,ref:this.buttonRefHandler}),s,u)},a=n=T,n.propTypes=(0,b.default)({},s.default.propTypes,{prefix:r.default.string,rtl:r.default.bool,type:r.default.oneOf(["primary","secondary","normal"]),size:r.default.oneOf(["small","medium","large"]),icons:r.default.shape({loading:r.default.node}),iconSize:r.default.oneOfType([r.default.oneOf(["xxs","xs","small","medium","large","xl","xxl","xxxl","inherit"]),r.default.number]),htmlType:r.default.oneOf(["submit","reset","button"]),component:r.default.oneOf(["button","a","div","span"]),loading:r.default.bool,ghost:r.default.oneOf([!0,!1,"light","dark"]),text:r.default.bool,warning:r.default.bool,disabled:r.default.bool,onClick:r.default.func,className:r.default.string,onMouseUp:r.default.func,children:r.default.node}),n.defaultProps={prefix:"next-",type:"normal",size:"medium",icons:{},htmlType:"button",component:"button",loading:!1,ghost:!1,text:!1,warning:!1,disabled:!1,onClick:function(){}};var u,s=a;function T(){var e,t;(0,o.default)(this,T);for(var n=arguments.length,a=Array(n),r=0;ra[r])return!0;if(n[r] 0, or `null`');if(U(i,"numericSeparator")&&"boolean"!=typeof i.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var t=i.numericSeparator;if(void 0===n)return"undefined";if(null===n)return"null";if("boolean"==typeof n)return n?"true":"false";if("string"==typeof n)return function e(t,n){if(t.length>n.maxStringLength)return a=t.length-n.maxStringLength,a="... "+a+" more character"+(1"}if(z(n))return 0===n.length?"[]":(l=$(n,m),h&&!function(e){for(var t=0;t "+m(e,n))}),ne("Map",_.call(n),u,h)):function(e){if(w&&e&&"object"==typeof e)try{w.call(e);try{_.call(e)}catch(e){return 1}return e instanceof Set}catch(e){}return}(n)?(d=[],M&&M.call(n,function(e){d.push(m(e,n))}),ne("Set",w.call(n),d,h)):function(e){if(k&&e&&"object"==typeof e)try{k.call(e,k);try{S.call(e,S)}catch(e){return 1}return e instanceof WeakMap}catch(e){}return}(n)?q("WeakMap"):function(e){if(S&&e&&"object"==typeof e)try{S.call(e,S);try{k.call(e,k)}catch(e){return 1}return e instanceof WeakSet}catch(e){}return}(n)?q("WeakSet"):function(e){if(E&&e&&"object"==typeof e)try{return E.call(e),1}catch(e){}return}(n)?q("WeakRef"):"[object Number]"!==V(c=n)||j&&"object"==typeof c&&j in c?function(e){if(e&&"object"==typeof e&&D)try{return D.call(e),1}catch(e){}return}(n)?K(m(D.call(n))):"[object Boolean]"!==V(t=n)||j&&"object"==typeof t&&j in t?"[object String]"!==V(e=n)||j&&"object"==typeof e&&j in e?("[object Date]"!==V(t=n)||j&&"object"==typeof t&&j in t)&&!W(n)?(e=$(n,m),t=I?I(n)===Object.prototype:n instanceof Object||n.constructor===Object,f=n instanceof Object?"":"null prototype",p=!t&&j&&Object(n)===n&&j in n?x.call(V(n),8,-1):f?"Object":"",t=(!t&&"function"==typeof n.constructor&&n.constructor.name?n.constructor.name+" ":"")+(p||f?"["+O.call(L.call([],p||[],f||[]),": ")+"] ":""),0===e.length?t+"{}":h?t+"{"+G(e,h)+"}":t+"{ "+O.call(e,", ")+" }"):String(n):K(m(String(n))):K(J.call(n)):K(m(Number(n)))};var l=Object.prototype.hasOwnProperty||function(e){return e in this};function U(e,t){return l.call(e,t)}function V(e){return i.call(e)}function ee(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0,a=e.length;n, as child."),a.push(t),e.props.children)&&(t.children=n(e.props.children))}),a}(e.children):t},N.prototype.fetchInfoFromBinaryChildren=function(e){function r(e,t){return t=t||0,e.forEach(function(e){e.children?t=r(e.children,t):t+=1}),t}var a=!1,o=[],i=[],e=(function t(){var e=0r.tRight&&(e=r.tRight,r.changedPageX=r.tRight-r.startLeft),e-r.cellLefto.clientHeight,o.scrollWidth,o.clientWidth,o={},e||(o[r]=0,o[a]=0),+i&&(o.marginBottom=-i,o.paddingBottom=i,e)&&(o[a]=i),h.dom.setStyle(this.headerNode,o)),n&&!this.props.lockType&&this.headerNode&&(r=this.headerNode.querySelector("."+t+"table-header-fixer"),e=h.dom.getStyle(this.headerNode,"height"),a=h.dom.getStyle(this.headerNode,"paddingBottom"),h.dom.setStyle(r,{width:i,height:e-a}))},o.prototype.render=function(){var e=this.props,t=e.components,n=e.className,a=e.prefix,r=e.fixedHeader,o=e.lockType,i=e.dataSource,e=(e.maxBodyHeight,(0,u.default)(e,["components","className","prefix","fixedHeader","lockType","dataSource","maxBodyHeight"]));return r&&((t=(0,l.default)({},t)).Header||(t.Header=m.default),t.Body||(t.Body=g.default),t.Wrapper||(t.Wrapper=y.default),n=(0,p.default)(((r={})[a+"table-fixed"]=!0,r[a+"table-wrap-empty"]=!i.length,r[n]=n,r))),c.default.createElement(s,(0,l.default)({},e,{dataSource:i,lockType:o,components:t,className:n,prefix:a}))},o}(c.default.Component),n.FixedHeader=m.default,n.FixedBody=g.default,n.FixedWrapper=y.default,n.propTypes=(0,l.default)({hasHeader:r.default.bool,fixedHeader:r.default.bool,maxBodyHeight:r.default.oneOfType([r.default.number,r.default.string])},s.propTypes),n.defaultProps=(0,l.default)({},s.defaultProps,{hasHeader:!0,fixedHeader:!1,maxBodyHeight:200,components:{},refs:{},prefix:"next-"}),n.childContextTypes={fixedHeader:r.default.bool,getNode:r.default.func,onFixedScrollSync:r.default.func,getTableInstanceForFixed:r.default.func,maxBodyHeight:r.default.oneOfType([r.default.number,r.default.string])};var t,n=t;return n.displayName="FixedTable",(0,o.statics)(n,s),n};var c=s(n(0)),r=s(n(5)),f=n(24),p=s(n(17)),h=n(11),m=s(n(134)),g=s(n(402)),y=s(n(135)),o=n(70);function s(e){return e&&e.__esModule?e:{default:e}}e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var i=o(n(12)),f=o(n(2)),r=o(n(4)),s=o(n(6)),l=o(n(7)),u=(t.default=function(o){e=t=function(n){function a(e,t){(0,r.default)(this,a);var c=(0,s.default)(this,n.call(this,e,t));return c.addSelection=function(e){var t=c.props,n=t.prefix,a=t.rowSelection,t=t.size,a=a.columnProps&&a.columnProps()||{};e.find(function(e){return"selection"===e.key})||e.unshift((0,f.default)({key:"selection",title:c.renderSelectionHeader.bind(c),cell:c.renderSelectionBody.bind(c),width:"small"===t?34:50,className:n+"table-selection "+n+"table-prerow",__normalized:!0},a))},c.renderSelectionHeader=function(){var e=c.selectAllRow,t={},n=c.props,a=n.rowSelection,r=n.primaryKey,o=n.dataSource,i=n.entireDataSource,n=n.locale,s=c.state.selectedRowKeys,l=a.mode||"multiple",u=!!s.length,d=!1,i=(c.flatDataSource(i||o).filter(function(e,t){return!a.getProps||!(a.getProps(e,t)||{}).disabled}).map(function(e){return e[r]}).forEach(function(e){-1===s.indexOf(e)?u=!1:d=!0}),t.onClick=b(function(e){e.stopPropagation()},t.onClick),a.titleProps&&a.titleProps()||{});return u&&(d=!1),["multiple"===l?p.default.createElement(h.default,(0,f.default)({key:"_total",indeterminate:d,"aria-label":n.selectAll,checked:u,onChange:e},t,i)):null,a.titleAddons&&a.titleAddons()]},c.renderSelectionBody=function(e,t,n){var a=c.props,r=a.rowSelection,a=a.primaryKey,o=c.state.selectedRowKeys,i=r.mode||"multiple",o=-1l.length&&(u=o),w(u.filter(function(e){return-1=Math.max(a-y,0)&&oc.clientHeight;this.isLock()?(e=this.bodyLeftNode,t=this.bodyRightNode,n=this.getWrapperNode("right"),a=f?d:0,c=c.offsetHeight-d,f||(r[l]=0,r[u]=0),+d?(r.marginBottom=-d,r.paddingBottom=d):(r.marginBottom=-20,r.paddingBottom=20),c={"max-height":c},o||+d||(c[u]=0),+d&&(c[u]=-d),e&&g.dom.setStyle(e,c),t&&g.dom.setStyle(t,c),n&&+d&&g.dom.setStyle(n,i?"left":"right",a+"px")):(r.marginBottom=-d,r.paddingBottom=d,r[u]=0,f||(r[l]=0)),s&&g.dom.setStyle(s,r)},a.prototype.adjustHeaderSize=function(){var o=this;this.isLock()&&this.tableInc.groupChildren.forEach(function(e,t){var n=o.tableInc.groupChildren[t].length-1,n=o.getHeaderCellNode(t,n),a=o.getHeaderCellNode(t,0),r=o.getHeaderCellNode(t,0,"right"),t=o.getHeaderCellNode(t,0,"left");n&&r&&(n=n.offsetHeight,g.dom.setStyle(r,"height",n),setTimeout(function(){var e=o.tableRightInc.affixRef;return e&&e.getInstance()&&e.getInstance().updatePosition()})),a&&t&&(r=a.offsetHeight,g.dom.setStyle(t,"height",r),setTimeout(function(){var e=o.tableLeftInc.affixRef;return e&&e.getInstance()&&e.getInstance().updatePosition()}))})},a.prototype.adjustRowHeight=function(){var n=this;this.isLock()&&this.tableInc.props.dataSource.forEach(function(e,t){t=""+("object"===(void 0===e?"undefined":(0,r.default)(e))&&"__rowIndex"in e?e.__rowIndex:t)+(e.__expanded?"_expanded":"");n.setRowHeight(t,"left"),n.setRowHeight(t,"right")})},a.prototype.setRowHeight=function(e,t){var t=this.getRowNode(e,t),e=this.getRowNode(e),e=(M?e&&e.offsetHeight:e&&parseFloat(getComputedStyle(e).height))||"auto",n=(M?t&&t.offsetHeight:t&&parseFloat(getComputedStyle(t).height))||"auto";t&&e!==n&&g.dom.setStyle(t,"height",e)},a.prototype.getWrapperNode=function(e){e=e?e.charAt(0).toUpperCase()+e.substr(1):"";try{return(0,u.findDOMNode)(this["lock"+e+"El"])}catch(e){return null}},a.prototype.getRowNode=function(e,t){t=this["table"+(t=t?t.charAt(0).toUpperCase()+t.substr(1):"")+"Inc"];try{return(0,u.findDOMNode)(t.getRowRef(e))}catch(e){return null}},a.prototype.getHeaderCellNode=function(e,t,n){n=this["table"+(n=n?n.charAt(0).toUpperCase()+n.substr(1):"")+"Inc"];try{return(0,u.findDOMNode)(n.getHeaderCellRef(e,t))}catch(e){return null}},a.prototype.getCellNode=function(e,t,n){n=this["table"+(n=n?n.charAt(0).toUpperCase()+n.substr(1):"")+"Inc"];try{return(0,u.findDOMNode)(n.getCellRef(e,t))}catch(e){return null}},a.prototype.render=function(){var e,t=this.props,n=(t.children,t.columns,t.prefix),a=t.components,r=t.className,o=t.dataSource,i=t.tableWidth,t=(0,f.default)(t,["children","columns","prefix","components","className","dataSource","tableWidth"]),s=this.normalizeChildrenState(this.props),l=s.lockLeftChildren,u=s.lockRightChildren,s=s.children,d={left:this.getFlatenChildrenLength(l),right:this.getFlatenChildrenLength(u),origin:this.getFlatenChildrenLength(s)};return this._notNeedAdjustLockLeft&&(l=[]),this._notNeedAdjustLockRight&&(u=[]),this.lockLeftChildren=l,this.lockRightChildren=u,this.isOriginLock()?((a=(0,p.default)({},a)).Body=a.Body||v.default,a.Header=a.Header||_.default,a.Wrapper=a.Wrapper||b.default,a.Row=a.Row||y.default,r=(0,m.default)(((e={})[n+"table-lock"]=!0,e[n+"table-wrap-empty"]=!o.length,e[r]=r,e)),e=[h.default.createElement(c,(0,p.default)({},t,{dataSource:o,key:"lock-left",columns:l,className:n+"table-lock-left",lengths:d,prefix:n,lockType:"left",components:a,ref:this.saveLockLeftRef,loading:!1,"aria-hidden":!0})),h.default.createElement(c,(0,p.default)({},t,{dataSource:o,key:"lock-right",columns:u,className:n+"table-lock-right",lengths:d,prefix:n,lockType:"right",components:a,ref:this.saveLockRightRef,loading:!1,"aria-hidden":!0}))],h.default.createElement(c,(0,p.default)({},t,{tableWidth:i,dataSource:o,columns:s,prefix:n,lengths:d,wrapperContent:e,components:a,className:r}))):h.default.createElement(c,this.props)},a}(h.default.Component),t.LockRow=y.default,t.LockBody=v.default,t.LockHeader=_.default,t.propTypes=(0,p.default)({scrollToCol:a.default.number,scrollToRow:a.default.number},c.propTypes),t.defaultProps=(0,p.default)({},c.defaultProps),t.childContextTypes={getTableInstance:a.default.func,getLockNode:a.default.func,onLockBodyScroll:a.default.func,onRowMouseEnter:a.default.func,onRowMouseLeave:a.default.func};var e,t=e;return t.displayName="LockTable",(0,w.statics)(t,c),t},n(0)),h=c(l),u=n(24),a=c(n(5)),m=c(n(17)),d=c(n(177)),g=n(11),y=c(n(179)),v=c(n(403)),_=c(n(404)),b=c(n(135)),w=n(70);function c(e){return e&&e.__esModule?e:{default:e}}var M=g.env.ieVersion;function k(e){return function n(e){return e.map(function(e){var t=(0,p.default)({},e);return e.children&&(e.children=n(e.children)),t})}(e)}e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var l=s(n(12)),u=s(n(2)),r=s(n(4)),o=s(n(6)),i=s(n(7)),d=(t.default=function(s){e=t=function(n){function a(e,t){(0,r.default)(this,a);var d=(0,o.default)(this,n.call(this,e));return d.state={},d.updateOffsetArr=function(){var e=d.splitChildren||{},t=e.lockLeftChildren,n=e.lockRightChildren,e=e.originChildren,a=d.getFlatenChildren(t).length,r=d.getFlatenChildren(n).length,e=a+r+d.getFlatenChildren(e).length,a=0r.top-e.offset?(t?(l.position="absolute",l.top=a-(r.top-e.offset),u="relative"):(l.position="fixed",l.top=e.offset+n.top),d._setAffixStyle(l,!0),d._setContainerStyle(s)):e.bottom&&a{e=new Date(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}},function(e,t,n){"use strict";const a=n(182);class r extends Date{constructor(e){super(e+"Z"),this.isFloating=!0}toISOString(){return`${this.getUTCFullYear()}-${a(2,this.getUTCMonth()+1)}-`+a(2,this.getUTCDate())+"T"+(`${a(2,this.getUTCHours())}:${a(2,this.getUTCMinutes())}:${a(2,this.getUTCSeconds())}.`+a(3,this.getUTCMilliseconds()))}}e.exports=e=>{e=new r(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}},function(a,e,r){"use strict";!function(e){const t=r(182);class n extends e.Date{constructor(e){super(e),this.isDate=!0}toISOString(){return`${this.getUTCFullYear()}-${t(2,this.getUTCMonth()+1)}-`+t(2,this.getUTCDate())}}a.exports=e=>{e=new n(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}}.call(this,r(65))},function(e,t,n){"use strict";const a=n(182);class r extends Date{constructor(e){super(`0000-01-01T${e}Z`),this.isTime=!0}toISOString(){return`${a(2,this.getUTCHours())}:${a(2,this.getUTCMinutes())}:${a(2,this.getUTCSeconds())}.`+a(3,this.getUTCMilliseconds())}}e.exports=e=>{e=new r(e);if(isNaN(e))throw new TypeError("Invalid Datetime");return e}},function(e,t,n){"use strict";!function(s){e.exports=function(r,e){e=e||{};const n=e.blocksize||40960,o=new t;return new Promise((e,t)=>{s(i,0,n,e,t)});function i(e,t,n,a){if(e>=r.length)try{return n(o.finish())}catch(e){return a(l(e,r))}try{o.parse(r.slice(e,e+t)),s(i,e+t,t,n,a)}catch(e){a(l(e,r))}}};const t=n(181),l=n(183)}.call(this,n(408).setImmediate)},function(e,t,n){!function(e,p){!function(n,o){"use strict";var a,i,s,r,l,u,t,e;function d(e){delete i[e]}function c(e){if(s)setTimeout(c,0,e);else{var t=i[e];if(t){s=!0;try{var n=t,a=n.callback,r=n.args;switch(r.length){case 0:a();break;case 1:a(r[0]);break;case 2:a(r[0],r[1]);break;case 3:a(r[0],r[1],r[2]);break;default:a.apply(o,r)}}finally{d(e),s=!1}}}}function f(){function e(e){e.source===n&&"string"==typeof e.data&&0===e.data.indexOf(t)&&c(+e.data.slice(t.length))}var t="setImmediate$"+Math.random()+"$";n.addEventListener?n.addEventListener("message",e,!1):n.attachEvent("onmessage",e),l=function(e){n.postMessage(t+e,"*")}}n.setImmediate||(a=1,s=!(i={}),r=n.document,e=(e=Object.getPrototypeOf&&Object.getPrototypeOf(n))&&e.setTimeout?e:n,"[object process]"==={}.toString.call(n.process)?l=function(e){p.nextTick(function(){c(e)})}:!function(){var e,t;if(n.postMessage&&!n.importScripts)return e=!0,t=n.onmessage,n.onmessage=function(){e=!1},n.postMessage("","*"),n.onmessage=t,e}()?l=n.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):r&&"onreadystatechange"in r.createElement("script")?(u=r.documentElement,function(e){var t=r.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,u.removeChild(t),t=null},u.appendChild(t)}):function(e){setTimeout(c,0,e)}:f(),e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n{let n,a=!1,r=!1;function o(){if(a=!0,!n)try{e(l.finish())}catch(e){t(e)}}function i(e){r=!0,t(e)}s.once("end",o),s.once("error",i),function e(){n=!0;let t;for(;null!==(t=s.read());)try{l.parse(t)}catch(e){return i(e)}n=!1;if(a)return o();if(r)return;s.once("readable",e)}()})}(e):function(){const a=new o;return new r.Transform({objectMode:!0,transform(e,t,n){try{a.parse(e.toString(t))}catch(e){this.emit("error",e)}n()},flush(e){try{this.push(a.finish())}catch(e){this.emit("error",e)}e()}})}()};const r=n(691),o=n(181)},function(e,t,n){e.exports=a;var d=n(184).EventEmitter;function a(){d.call(this)}n(104)(a,d),a.Readable=n(185),a.Writable=n(701),a.Duplex=n(702),a.Transform=n(703),a.PassThrough=n(704),(a.Stream=a).prototype.pipe=function(t,e){var n=this;function a(e){t.writable&&!1===t.write(e)&&n.pause&&n.pause()}function r(){n.readable&&n.resume&&n.resume()}n.on("data",a),t.on("drain",r),t._isStdio||e&&!1===e.end||(n.on("end",i),n.on("close",s));var o=!1;function i(){o||(o=!0,t.end())}function s(){o||(o=!0,"function"==typeof t.destroy&&t.destroy())}function l(e){if(u(),0===d.listenerCount(this,"error"))throw e}function u(){n.removeListener("data",a),t.removeListener("drain",r),n.removeListener("end",i),n.removeListener("close",s),n.removeListener("error",l),t.removeListener("error",l),n.removeListener("end",u),n.removeListener("close",u),t.removeListener("close",u)}return n.on("error",l),t.on("error",l),n.on("end",u),n.on("close",u),t.on("close",u),t.emit("pipe",n),t}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){"use strict";t.byteLength=function(e){var e=d(e),t=e[0],e=e[1];return 3*(t+e)/4-e},t.toByteArray=function(e){var t,n,a=d(e),r=a[0],a=a[1],o=new u(function(e,t){return 3*(e+t)/4-t}(r,a)),i=0,s=0>16&255,o[i++]=t>>8&255,o[i++]=255&t;2===a&&(t=l[e.charCodeAt(n)]<<2|l[e.charCodeAt(n+1)]>>4,o[i++]=255&t);1===a&&(t=l[e.charCodeAt(n)]<<10|l[e.charCodeAt(n+1)]<<4|l[e.charCodeAt(n+2)]>>2,o[i++]=t>>8&255,o[i++]=255&t);return o},t.fromByteArray=function(e){for(var t,n=e.length,a=n%3,r=[],o=0,i=n-a;o>18&63]+s[e>>12&63]+s[e>>6&63]+s[63&e]}(a));return r.join("")}(e,o,i>2]+s[t<<4&63]+"==")):2==a&&(t=(e[n-2]<<8)+e[n-1],r.push(s[t>>10]+s[t>>4&63]+s[t<<2&63]+"="));return r.join("")};for(var s=[],l=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=0,o=a.length;r */ t.read=function(e,t,n,a,r){var o,i,s=8*r-a-1,l=(1<>1,d=-7,c=n?r-1:0,f=n?-1:1,r=e[t+c];for(c+=f,o=r&(1<<-d)-1,r>>=-d,d+=s;0>=-d,d+=a;0>1,c=23===r?Math.pow(2,-24)-Math.pow(2,-77):0,f=a?0:o-1,p=a?1:-1,o=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(s=isNaN(t)?1:0,i=u):(i=Math.floor(Math.log(t)/Math.LN2),t*(a=Math.pow(2,-i))<1&&(i--,a*=2),2<=(t+=1<=i+d?c/a:c*Math.pow(2,1-d))*a&&(i++,a/=2),u<=i+d?(s=0,i=u):1<=i+d?(s=(t*a-1)*Math.pow(2,r),i+=d):(s=t*Math.pow(2,d-1)*Math.pow(2,r),i=0));8<=r;e[n+f]=255&s,f+=p,s/=256,r-=8);for(i=i<>>0),r=this.head,o=0;r;)t=r.data,n=o,t.copy(a,n),o+=r.data.length,r=r.next;return a},r),a&&a.inspect&&a.inspect.custom&&(e.exports.prototype[a.inspect.custom]=function(){var e=a.inspect({length:this.length});return this.constructor.name+" "+e})},function(e,t){},function(e,t,n){!function(t){function a(e){try{if(!t.localStorage)return}catch(e){return}e=t.localStorage[e];return null!=e&&"true"===String(e).toLowerCase()}e.exports=function(e,t){if(a("noDeprecation"))return e;var n=!1;return function(){if(!n){if(a("throwDeprecation"))throw new Error(t);a("traceDeprecation")?console.trace(t):console.warn(t),n=!0}return e.apply(this,arguments)}}}.call(this,n(65))},function(e,t,n){"use strict";e.exports=r;var a=n(414),e=Object.create(n(117));function r(e){if(!(this instanceof r))return new r(e);a.call(this,e)}e.inherits=n(104),e.inherits(r,a),r.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){e.exports=n(186)},function(e,t,n){e.exports=n(91)},function(e,t,n){e.exports=n(185).Transform},function(e,t,n){e.exports=n(185).PassThrough},function(e,t,n){"use strict";function u(e){return new Error("Can only stringify objects, not "+e)}function d(t){return Object.keys(t).filter(e=>p(t[e]))}function c(e){var t,n=Array.isArray(e)?[]:Object.prototype.hasOwnProperty.call(e,"__proto__")?{["__proto__"]:void 0}:{};for(t of Object.keys(e))!e[t]||"function"!=typeof e[t].toJSON||"toISOString"in e[t]?n[t]=e[t]:n[t]=e[t].toJSON();return n}function f(t,e,n){var a,r,o=d(n=c(n));r=n,a=Object.keys(r).filter(e=>!p(r[e]));const i=[],s=e||"",l=(o.forEach(e=>{var t=h(n[e]);"undefined"!==t&&"null"!==t&&i.push(s+m(e)+" = "+g(n[e],!0))}),0{i.push(function(e,t,n,a){var r=h(a);{if("array"===r)return function(e,t,n,a){var r=h((a=c(a))[0]);if("table"!==r)throw u(r);const o=e+m(n);let i="";return a.forEach(e=>{0"\\u"+function(e,t){for(;t.lengths(e).replace(/"(?="")/g,'\\"')).join("\n");return'"'===e.slice(-1)&&(e+="\\\n"),'"""\n'+e+'"""';return}case"string":return i(t);case"string-literal":return"'"+t+"'";case"integer":return y(t);case"float":n=t;return n===1/0?"inf":n===-1/0?"-inf":Object.is(n,NaN)?"nan":Object.is(n,-0)?"-0.0":([n,a]=String(n).split("."),y(n)+"."+a);case"boolean":return String(t);case"datetime":return t.toISOString();case"array":{var a=t.filter(e=>"null"!==h(e)&&"undefined"!==h(e)&&"nan"!==h(e));a=c(a);let e="[";a=a.map(e=>l(e));60{o.push(m(e)+" = "+g(r[e],!1))}),"{ "+o.join(", ")+(0u&&!c&&(o=o.slice(0,u),e=C.default.createElement(m.default,{key:"_count",type:"primary",size:p,animation:!1},d(a,t))),0=P.KEYCODE.LEFT&&t<=P.KEYCODE.DOWN&&e.preventDefault(),e=void 0,t===P.KEYCODE.RIGHT||t===P.KEYCODE.DOWN?(e=o.getNextActiveKey(!0),o.handleTriggerEvent(o.props.triggerType,e)):t!==P.KEYCODE.LEFT&&t!==P.KEYCODE.UP||(e=o.getNextActiveKey(!1),o.handleTriggerEvent(o.props.triggerType,e)))},o.state={activeKey:o.getDefaultActiveKey(e)},o}s.displayName="Tab",t.default=(0,l.polyfill)(s),e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var _=c(n(2)),a=c(n(4)),r=c(n(6)),o=c(n(7)),b=c(n(0)),i=n(24),s=c(n(5)),w=c(n(17)),M=c(n(26)),l=c(n(64)),u=c(n(50)),k=(c(n(19)),c(n(82))),h=n(11),d=n(418);function c(e){return e&&e.__esModule?e:{default:e}}var f,S={float:"right",zIndex:1},E={float:"left",zIndex:1},p={dropdown:"arrow-down",prev:"arrow-left",next:"arrow-right"},m=l.default.Popup,l=(f=b.default.Component,(0,o.default)(g,f),g.prototype.componentDidMount=function(){this.props.animation||this.initialSettings(),h.events.on(window,"resize",this.onWindowResized)},g.prototype.componentDidUpdate=function(e){var t=this;clearTimeout(this.scrollTimer),this.scrollTimer=setTimeout(function(){t.scrollToActiveTab()},410),clearTimeout(this.slideTimer),this.slideTimer=setTimeout(function(){t.setSlideBtn()},410),"dropdown"!==this.props.excessMode||(0,d.tabsArrayShallowEqual)(this.props.tabs,e.tabs)||this.getDropdownItems(this.props)},g.prototype.componentWillUnmount=function(){h.events.off(window,"resize",this.onWindowResized)},g.prototype.initialSettings=function(){this.setSlideBtn(),this.getDropdownItems(this.props)},g.prototype.setOffset=function(e){var t=!(1n&&(t.current=n),this.setState(t),this.props.onPageSizeChange(e)},I.prototype.renderPageTotal=function(){var e=this.props,t=e.prefix,n=e.total,e=e.totalRender,a=this.state,r=a.currentPageSize,a=a.current;return N.default.createElement("div",{className:t+"pagination-total"},e(n,[(a-1)*r+1,a*r]))},I.prototype.renderPageItem=function(e){var t=this.props,n=t.prefix,a=t.size,r=t.link,o=t.pageNumberRender,i=t.total,s=t.pageSize,t=t.locale,l=this.state.current,i=Y(i,s),s=parseInt(e,10)===l,a={size:a,className:(0,P.default)(((l={})[n+"pagination-item"]=!0,l[n+"current"]=s,l)),onClick:s?m:this.onPageItemClick.bind(this,e)};return r&&(a.component="a",a.href=r.replace("{page}",e)),N.default.createElement(c.default,(0,D.default)({"aria-label":j.str.template(t.total,{current:e,total:i})},a,{key:e}),o(e))},I.prototype.renderPageFirst=function(e){var t=this.props,n=t.prefix,a=t.size,r=t.shape,t=t.locale,a={disabled:e<=1,size:a,className:(0,P.default)(((a={})[n+"pagination-item"]=!0,a[n+"prev"]=!0,a)),onClick:this.onPageItemClick.bind(this,e-1)},n=N.default.createElement(d.default,{type:"arrow-left",className:n+"pagination-icon-prev"});return N.default.createElement(c.default,(0,D.default)({},a,{"aria-label":j.str.template(t.labelPrev,{current:e})}),n,"arrow-only"===r||"arrow-prev-only"===r||"no-border"===r?"":t.prev)},I.prototype.renderPageLast=function(e,t){var n=this.props,a=n.prefix,r=n.size,o=n.shape,n=n.locale,r={disabled:t<=e,size:r,className:(0,P.default)(((t={})[a+"pagination-item"]=!0,t[a+"next"]=!0,t)),onClick:this.onPageItemClick.bind(this,e+1)},t=N.default.createElement(d.default,{type:"arrow-right",className:a+"pagination-icon-next"});return N.default.createElement(c.default,(0,D.default)({},r,{"aria-label":j.str.template(n.labelNext,{current:e})}),"arrow-only"===o||"no-border"===o?"":n.next,t)},I.prototype.renderPageEllipsis=function(e){var t=this.props.prefix;return N.default.createElement(d.default,{className:t+"pagination-ellipsis "+t+"pagination-icon-ellipsis",type:"ellipsis",key:"ellipsis-"+e})},I.prototype.renderPageJump=function(){var t=this,e=this.props,n=e.prefix,a=e.size,e=e.locale,r=this.state.inputValue;return[N.default.createElement("span",{className:n+"pagination-jump-text"},e.goTo),N.default.createElement(f.default,{className:n+"pagination-jump-input",type:"text","aria-label":e.inputAriaLabel,size:a,value:r,onChange:this.onInputChange.bind(this),onKeyDown:function(e){e.keyCode===j.KEYCODE.ENTER&&t.handleJump(e)}}),N.default.createElement("span",{className:n+"pagination-jump-text"},e.page),N.default.createElement(c.default,{className:n+"pagination-jump-go",size:a,onClick:this.handleJump},e.go)]},I.prototype.renderPageDisplay=function(e,t){var n=this.props,a=n.prefix,n=n.pageNumberRender;return N.default.createElement("span",{className:a+"pagination-display"},N.default.createElement("em",null,n(e)),"/",n(t))},I.prototype.renderPageList=function(e,t){var n=this.props,a=n.prefix,n=n.pageShowCount,r=[];if(t<=n)for(var o=1;o<=t;o++)r.push(this.renderPageItem(o));else{var n=n-3,i=parseInt(n/2,10),s=void 0,l=void 0;r.push(this.renderPageItem(1)),l=e+i,(s=e-i)<=1&&(l=(s=2)+n),2=e.length&&-1=this.props.children.length?(this.update(this.props),this.changeSlide({message:"index",index:this.props.children.length-this.props.slidesToShow,currentSlide:this.state.currentSlide})):(n=[],Object.keys(t).forEach(function(e){e in a.props&&t[e]!==a.props[e]&&n.push(e)}),1===n.length&&"children"===n[0]||l.obj.shallowEqual(t,this.props)||this.update(this.props)),this.adaptHeight()},p.prototype.componentWillUnmount=function(){this.animationEndCallback&&clearTimeout(this.animationEndCallback),l.events.off(window,"resize",this.onWindowResized),this.state.autoPlayTimer&&clearInterval(this.state.autoPlayTimer)},p.prototype.onWindowResized=function(){this.update(this.props),this.setState({animating:!1}),clearTimeout(this.animationEndCallback),delete this.animationEndCallback},p.prototype.slickGoTo=function(e){"number"==typeof e&&this.changeSlide({message:"index",index:e,currentSlide:this.state.currentSlide})},p.prototype.onEnterArrow=function(e){this.arrowHoverHandler(e)},p.prototype.onLeaveArrow=function(){this.arrowHoverHandler()},p.prototype._instanceRefHandler=function(e,t){this[e]=t},p.prototype.render=function(){var e=this.props,t=e.prefix,n=e.animation,a=e.arrows,r=e.arrowSize,o=e.arrowPosition,i=e.arrowDirection,s=e.dots,l=e.dotsClass,u=e.cssEase,d=e.speed,c=e.infinite,f=e.centerMode,p=e.centerPadding,h=e.lazyLoad,m=e.dotsDirection,g=e.rtl,y=e.slidesToShow,v=e.slidesToScroll,_=e.variableWidth,b=e.vertical,w=e.verticalSwiping,M=e.focusOnSelect,k=e.children,S=e.dotsRender,e=e.triggerType,E=this.state,x=E.currentSlide,C=E.lazyLoadedList,T=E.slideCount,L=E.slideWidth,O=E.slideHeight,D=E.trackStyle,N=E.listHeight,E=E.dragging,u={prefix:t,animation:n,cssEase:u,speed:d,infinite:c,centerMode:f,focusOnSelect:M?this.selectHandler:null,currentSlide:x,lazyLoad:h,lazyLoadedList:C,rtl:g,slideWidth:L,slideHeight:O,slidesToShow:y,slidesToScroll:v,slideCount:T,trackStyle:D,variableWidth:_,vertical:b,verticalSwiping:w,triggerType:e},d=void 0,h=(!0===s&&yt.startX?1:-1),!0===this.props.verticalSwiping&&(t.swipeLength=Math.round(Math.sqrt(Math.pow(t.curY-t.startY,2))),a=t.curY>t.startY?1:-1),r=this.state.currentSlide,s=Math.ceil(this.state.slideCount/this.props.slidesToScroll),o=this.swipeDirection(this.state.touchObject),i=t.swipeLength,!1===this.props.infinite&&(0===r&&"right"===o||s<=r+1&&"left"===o)&&(i=t.swipeLength*this.props.edgeFriction,!1===this.state.edgeDragged)&&this.props.edgeEvent&&(this.props.edgeEvent(o),this.setState({edgeDragged:!0})),!1===this.state.swiped&&this.props.swipeEvent&&(this.props.swipeEvent(o),this.setState({swiped:!0})),this.setState({touchObject:t,swipeLeft:s=n+i*a,trackStyle:(0,u.getTrackCSS)((0,l.default)({left:s},this.props,this.state))}),Math.abs(t.curX-t.startX)<.8*Math.abs(t.curY-t.startY))||4t[t.length-1])e=t[t.length-1];else for(var a in t){if(e-1*n.state.swipeLeft)return t=e,!1}else if(e.offsetLeft-a+(n.getWidth(e)||0)/2>-1*n.state.swipeLeft)return t=e,!1;return!0}),Math.abs(t.dataset.index-this.state.currentSlide)||1):this.props.slidesToScroll},swipeEnd:function(e){if(this.state.dragging){var t=this.state.touchObject,n=this.state.listWidth/this.props.touchThreshold,a=this.swipeDirection(t);if(this.props.verticalSwiping&&(n=this.state.listHeight/this.props.touchThreshold),this.setState({dragging:!1,edgeDragged:!1,swiped:!1,swipeLeft:null,touchObject:{}}),t.swipeLength)if(t.swipeLength>n){e.preventDefault();var r=void 0,o=void 0;switch(a){case"left":case"down":o=this.state.currentSlide+this.getSlideCount(),r=this.props.swipeToSlide?this.checkNavigable(o):o,this.setState({currentDirection:0});break;case"right":case"up":o=this.state.currentSlide-this.getSlideCount(),r=this.props.swipeToSlide?this.checkNavigable(o):o,this.setState({currentDirection:1});break;default:r=this.state.currentSlide}this.slideHandler(r)}else{t=(0,u.getTrackLeft)((0,l.default)({slideIndex:this.state.currentSlide,trackRef:this.track},this.props,this.state));this.setState({trackStyle:(0,u.getTrackAnimateCSS)((0,l.default)({left:t},this.props,this.state))})}}else this.props.swipe&&e.preventDefault()},onInnerSliderEnter:function(){this.props.autoplay&&this.props.pauseOnHover&&this.pause()},onInnerSliderLeave:function(){this.props.autoplay&&this.props.pauseOnHover&&this.autoPlay()}},e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var g=a(n(2)),d=a(n(0)),c=a(n(24)),y=n(421);function a(e){return e&&e.__esModule?e:{default:e}}t.default={initialize:function(t){var n=this,e=c.default.findDOMNode(this.list),a=d.default.Children.count(t.children),r=this.getWidth(e)||0,o=this.getWidth(c.default.findDOMNode(this.track))||0,i=void 0,e=(i=t.vertical?r:(r-(t.centerMode&&2*parseInt(t.centerPadding)))/t.slidesToShow,this.getHeight(e.querySelector('[data-index="0"]'))||0),s=e*t.slidesToShow,l=t.slidesToShow||1,u="activeIndex"in t?t.activeIndex:t.defaultActiveIndex,l=t.rtl?a-1-(l-1)-u:u;this.setState({slideCount:a,slideWidth:i,listWidth:r,trackWidth:o,currentSlide:l,slideHeight:e,listHeight:s},function(){var e=(0,y.getTrackLeft)((0,g.default)({slideIndex:n.state.currentSlide,trackRef:n.track},t,n.state)),e=(0,y.getTrackCSS)((0,g.default)({left:e},t,n.state));n.setState({trackStyle:e}),n.autoPlay()})},update:function(e){this.initialize(e)},getWidth:function(e){return"clientWidth"in e?e.clientWidth:e&&e.getBoundingClientRect().width},getHeight:function(e){return"clientHeight"in e?e.clientHeight:e&&e.getBoundingClientRect().height},adaptHeight:function(){var e,t;this.props.adaptiveHeight&&(t='[data-index="'+this.state.currentSlide+'"]',this.list)&&(t=(e=c.default.findDOMNode(this.list)).querySelector(t).offsetHeight,e.style.height=t+"px")},canGoNext:function(e){var t=!0;return e.infinite||(e.centerMode?e.currentSlide>=e.slideCount-1&&(t=!1):(e.slideCount<=e.slidesToShow||e.currentSlide>=e.slideCount-e.slidesToShow)&&(t=!1)),t},slideHandler:function(e){var t=this,n=this.props.rtl,a=void 0,r=void 0,o=void 0;if(!this.props.waitForAnimate||!this.state.animating){if("fade"===this.props.animation)return r=this.state.currentSlide,!1===this.props.infinite&&(e<0||e>=this.state.slideCount)?void 0:(a=e<0?e+this.state.slideCount:e>=this.state.slideCount?e-this.state.slideCount:e,this.props.lazyLoad&&this.state.lazyLoadedList.indexOf(a)<0&&this.setState({lazyLoadedList:this.state.lazyLoadedList.concat(a)}),o=function(){t.setState({animating:!1}),t.props.onChange(a),delete t.animationEndCallback},this.props.onBeforeChange(this.state.currentSlide,a),this.setState({animating:!0,currentSlide:a},function(){this.animationEndCallback=setTimeout(o,this.props.speed+20)}),void this.autoPlay());a=e,n?a<0?!1===this.props.infinite?r=0:this.state.slideCount%this.props.slidesToScroll!=0?a+this.props.slidesToScroll<=0?(r=this.state.slideCount+a,a=this.state.slideCount-this.props.slidesToScroll):r=a=0:r=this.state.slideCount+a:r=a>=this.state.slideCount?!1===this.props.infinite?this.state.slideCount-this.props.slidesToShow:this.state.slideCount%this.props.slidesToScroll!=0?0:a-this.state.slideCount:a:r=a<0?!1===this.props.infinite?0:this.state.slideCount%this.props.slidesToScroll!=0?this.state.slideCount-this.state.slideCount%this.props.slidesToScroll:this.state.slideCount+a:a>=this.state.slideCount?!1===this.props.infinite?this.state.slideCount-this.props.slidesToShow:this.state.slideCount%this.props.slidesToScroll!=0?0:a-this.state.slideCount:a;var i,e=(0,y.getTrackLeft)((0,g.default)({slideIndex:a,trackRef:this.track},this.props,this.state)),s=(0,y.getTrackLeft)((0,g.default)({slideIndex:r,trackRef:this.track},this.props,this.state));if(!1===this.props.infinite&&(e=s),this.props.lazyLoad){for(var l=!0,u=[],d=this.state.slideCount,c=a<0?d+a:r,f=c;f=l.activeIndex?"visible":"hidden",d.transition="opacity "+l.speed+"ms "+l.cssEase,d.WebkitTransition="opacity "+l.speed+"ms "+l.cssEase,l.vertical?d.top=-l.activeIndex*l.slideHeight:d.left=-l.activeIndex*l.slideWidth),l.vertical&&(d.width="100%"),d),u=(d=(0,v.default)({activeIndex:e},c),a=d.prefix,u=r=i=void 0,o=(u=d.rtl?d.slideCount-1-d.activeIndex:d.activeIndex)<0||u>=d.slideCount,d.centerMode?(n=Math.floor(d.slidesToShow/2),r=(u-d.currentSlide)%d.slideCount==0,u>d.currentSlide-n-1&&u<=d.currentSlide+n&&(i=!0)):i=d.currentSlide<=u&&u=u,u=(0,k.default)(((v={})[o+"upload-list-item"]=!0,v[o+"hidden"]=u,v)),v=this.props.children||i.card.addPhoto,c=r?S.func.prevent:c,_=S.obj.pickOthers(C.propTypes,this.props),b=S.obj.pickOthers(E.default.propTypes,_);if(h&&"function"==typeof m)return e=(0,k.default)(((e={})[o+"form-preview"]=!0,e[s]=!!s,e)),M.default.createElement("div",{style:l,className:e},m(this.state.value,this.props));return M.default.createElement(E.default,(0,w.default)({className:s,style:l,listType:"card",closable:!0,locale:i,value:this.state.value,onRemove:c,onCancel:f,onPreview:d,itemRender:g,isPreview:h,uploader:this.uploaderRef,reUpload:y,showDownload:n},_),M.default.createElement(x.default,(0,w.default)({},b,{shape:"card",prefix:o,disabled:r,action:a,timeout:p,isPreview:h,value:this.state.value,onProgress:this.onProgress,onChange:this.onChange,ref:function(e){return t.saveRef(e)},className:u}),v))},d=n=C,n.displayName="Card",n.propTypes={prefix:s.default.string,locale:s.default.object,children:s.default.object,value:s.default.oneOfType([s.default.array,s.default.object]),defaultValue:s.default.oneOfType([s.default.array,s.default.object]),onPreview:s.default.func,onChange:s.default.func,onRemove:s.default.func,onCancel:s.default.func,itemRender:s.default.func,reUpload:s.default.bool,showDownload:s.default.bool,onProgress:s.default.func,isPreview:s.default.bool,renderPreview:s.default.func},n.defaultProps={prefix:"next-",locale:u.default.Upload,showDownload:!0,onChange:S.func.noop,onPreview:S.func.noop,onProgress:S.func.noop},a=function(){var n=this;this.onProgress=function(e,t){n.setState({value:e}),n.props.onProgress(e,t)},this.onChange=function(e,t){"value"in n.props||n.setState({value:e}),n.props.onChange(e,t)}};var f,i=d;function C(e){(0,r.default)(this,C);var t=(0,o.default)(this,f.call(this,e)),n=(a.call(t),void 0),n="value"in e?e.value:e.defaultValue;return t.state={value:Array.isArray(n)?n:[],uploaderRef:t.uploaderRef},t}t.default=(0,l.polyfill)(i),e.exports=t.default},function(e,t,n){"use strict";t.__esModule=!0;var u=m(n(2)),d=m(n(12)),o=m(n(4)),i=m(n(6)),a=m(n(7)),c=m(n(0)),r=m(n(5)),f=m(n(17)),p=m(n(26)),s=n(11),l=m(n(44)),h=m(n(191));function m(e){return e&&e.__esModule?e:{default:e}}g=c.default.Component,(0,a.default)(y,g),y.prototype.abort=function(e){this.uploaderRef.abort(e)},y.prototype.startUpload=function(){this.uploaderRef.startUpload()},y.prototype.render=function(){var e=this.props,t=e.className,n=e.style,a=e.shape,r=e.locale,o=e.prefix,i=e.listType,e=(0,d.default)(e,["className","style","shape","locale","prefix","listType"]),s=o+"upload-drag",t=(0,f.default)(((l={})[s]=!0,l[s+"-over"]=this.state.dragOver,l[t]=!!t,l)),l=this.props.children||c.default.createElement("div",{className:t},c.default.createElement("p",{className:s+"-icon"},c.default.createElement(p.default,{size:"large",className:s+"-upload-icon"})),c.default.createElement("p",{className:s+"-text"},r.drag.text),c.default.createElement("p",{className:s+"-hint"},r.drag.hint));return c.default.createElement(h.default,(0,u.default)({},e,{prefix:o,shape:a,listType:i,dragable:!0,style:n,onDragOver:this.onDragOver,onDragLeave:this.onDragLeave,onDrop:this.onDrop,ref:this.saveUploaderRef}),l)},a=n=y,n.propTypes={prefix:r.default.string,locale:r.default.object,shape:r.default.string,onDragOver:r.default.func,onDragLeave:r.default.func,onDrop:r.default.func,limit:r.default.number,className:r.default.string,style:r.default.object,defaultValue:r.default.array,children:r.default.node,listType:r.default.string,timeout:r.default.number},n.defaultProps={prefix:"next-",onDragOver:s.func.noop,onDragLeave:s.func.noop,onDrop:s.func.noop,locale:l.default.Upload};var g,r=a;function y(){var e,t;(0,o.default)(this,y);for(var n=arguments.length,a=Array(n),r=0;rremi-sys remi-sys pom + 瑞米平台-身份权限 ai.remi.top.cloud remi-cloud - 3.1.0 + 3.1.1 diff --git a/remi-sys/remi-sys-api/pom.xml b/remi-sys/remi-sys-api/pom.xml index 00e8fa4..c36210b 100644 --- a/remi-sys/remi-sys-api/pom.xml +++ b/remi-sys/remi-sys-api/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-sys - 3.1.0 + 3.1.1 @@ -29,7 +29,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-feign diff --git a/remi-sys/remi-sys-app/pom.xml b/remi-sys/remi-sys-app/pom.xml index 16600f4..4f519be 100644 --- a/remi-sys/remi-sys-app/pom.xml +++ b/remi-sys/remi-sys-app/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-sys - 3.1.0 + 3.1.1 @@ -71,7 +71,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-feign diff --git a/remi-sys/remi-sys-domain/pom.xml b/remi-sys/remi-sys-domain/pom.xml index f3906f6..8a12cb6 100644 --- a/remi-sys/remi-sys-domain/pom.xml +++ b/remi-sys/remi-sys-domain/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-sys - 3.1.0 + 3.1.1 @@ -29,12 +29,12 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-domain - ai.remi.top.comm + ai.remi.top.cloud remi-comm-swagger diff --git a/remi-sys/remi-sys-infra/pom.xml b/remi-sys/remi-sys-infra/pom.xml index 362ccc8..6fe42d2 100644 --- a/remi-sys/remi-sys-infra/pom.xml +++ b/remi-sys/remi-sys-infra/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-sys - 3.1.0 + 3.1.1 @@ -29,7 +29,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-jdbc diff --git a/remi-sys/remi-sys-job/pom.xml b/remi-sys/remi-sys-job/pom.xml index d4c1911..c509abd 100644 --- a/remi-sys/remi-sys-job/pom.xml +++ b/remi-sys/remi-sys-job/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-sys - 3.1.0 + 3.1.1 @@ -29,7 +29,7 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-job diff --git a/remi-sys/remi-sys-server/pom.xml b/remi-sys/remi-sys-server/pom.xml index 50269bc..3a403cd 100644 --- a/remi-sys/remi-sys-server/pom.xml +++ b/remi-sys/remi-sys-server/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-sys - 3.1.0 + 3.1.1 @@ -34,23 +34,23 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-exception - ai.remi.top.comm + ai.remi.top.cloud remi-comm-minio - ai.remi.top.comm + ai.remi.top.cloud remi-comm-redis - ai.remi.top.comm + ai.remi.top.cloud remi-comm-thread - ai.remi.top.comm + ai.remi.top.cloud remi-comm-util diff --git a/remi-sys/remi-sys-web/pom.xml b/remi-sys/remi-sys-web/pom.xml index f475d33..9e546ab 100644 --- a/remi-sys/remi-sys-web/pom.xml +++ b/remi-sys/remi-sys-web/pom.xml @@ -9,7 +9,7 @@ ai.remi.top.cloud remi-sys - 3.1.0 + 3.1.1 @@ -71,13 +71,13 @@ - ai.remi.top.comm + ai.remi.top.cloud remi-comm-web - ai.remi.top.comm + ai.remi.top.cloud remi-comm-log @@ -98,6 +98,11 @@ ai.remi.top.cloud remi-sys-api + + + ai.remi.top.cloud + remi-comm-feign + diff --git a/remi-sys/remi-sys-web/src/main/resources/bootstrap.yml b/remi-sys/remi-sys-web/src/main/resources/bootstrap.yml index 665a999..9e1fac5 100644 --- a/remi-sys/remi-sys-web/src/main/resources/bootstrap.yml +++ b/remi-sys/remi-sys-web/src/main/resources/bootstrap.yml @@ -16,8 +16,8 @@ spring: password: nacos discovery: # 配置中心的地址 - server-addr: ${NACOS_HOST:192.168.31.18}:${NACOS_PORT:8848} - #server-addr: ${NACOS_HOST:192.168.31.200}:${NACOS_PORT:30888} + #server-addr: ${NACOS_HOST:192.168.31.204}:${NACOS_PORT:8848} + server-addr: ${NACOS_HOST:192.168.31.200}:${NACOS_PORT:30888} #命名空间 namespace: ${SERVICE_NAME_SPACE_NAME:dev} #namespace: ${SERVICE_NAME_SPACE_NAME:sit}