ソースを参照

feat(core): 添加账户相关实体和数据传输对象

- 新增 AccountActivityView 和 AccountAmountView 视图类
- 添加账户申请相关实体类包括 AccountApplyAddEntity、AccountApplyApproveEntity 等
- 创建账户申请数据传输对象如 AccountApplyDataBaseDto、AccountApplyDataDto 等
- 实现账户余额相关实体和 DTO 类
- 添加账户归属变更功能实体类
- 创建账户交易密码变更和重置相关实体
- 实现账户日常汇总统计相关表和视图类
kongxiangyang 5 ヶ月 前
コミット
bb3a0c82bd
100 ファイル変更12401 行追加4 行削除
  1. 3 3
      uacrd-manager-server/pom.xml
  2. 1 1
      uacrd-manager-server/src/main/resources/application.yml
  3. 200 0
      ucard-backend/pom.xml
  4. 297 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/CacheAspect.java
  5. 256 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/ControllerAop.java
  6. 177 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/ControllerExceptionAdvice.java
  7. 222 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/DaoMapperAop.java
  8. 106 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/DynamicDataSourceAspect.java
  9. 36 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/EntityScanPathResolver.java
  10. 86 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/HibernateQueryInterceptor.java
  11. 147 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/LimiterAspect.java
  12. 11 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/LogType.java
  13. 178 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/ServiceAop.java
  14. 69 0
      ucard-backend/src/main/java/com/crm/rely/backend/aspect/ServiceExceptionAop.java
  15. 29 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/ApplicationContextProvider.java
  16. 20 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/BaseActiveMqConfiguration.java
  17. 20 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/BigDecimalSerializer.java
  18. 52 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/CustomJsonHttpMessageConverter.java
  19. 73 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/DataSourceConfig.java
  20. 192 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/DefaultEnvironmentPostProcessor.java
  21. 144 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/GlobalConfiguration.java
  22. 59 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/GlobalFilterConfiguration.java
  23. 53 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/HibernateConfig.java
  24. 103 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/LocaleMessage.java
  25. 64 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/LogQuartzComponent.java
  26. 35 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/MongoDBAppender.java
  27. 61 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/RedisBean.java
  28. 40 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/TaskConfiguration.java
  29. 32 0
      ucard-backend/src/main/java/com/crm/rely/backend/configuration/TransactionManagerConfig.java
  30. 49 0
      ucard-backend/src/main/java/com/crm/rely/backend/controller/NotFoundExceptionController.java
  31. 9 0
      ucard-backend/src/main/java/com/crm/rely/backend/dao/repository/BaseRepository.java
  32. 23 0
      ucard-backend/src/main/java/com/crm/rely/backend/exception/ControllerException.java
  33. 25 0
      ucard-backend/src/main/java/com/crm/rely/backend/exception/DataNotFoundException.java
  34. 26 0
      ucard-backend/src/main/java/com/crm/rely/backend/exception/LoginException.java
  35. 25 0
      ucard-backend/src/main/java/com/crm/rely/backend/exception/ParameterException.java
  36. 33 0
      ucard-backend/src/main/java/com/crm/rely/backend/exception/PayValidatedException.java
  37. 59 0
      ucard-backend/src/main/java/com/crm/rely/backend/exception/ServiceException.java
  38. 26 0
      ucard-backend/src/main/java/com/crm/rely/backend/exception/ValidateException.java
  39. 89 0
      ucard-backend/src/main/java/com/crm/rely/backend/filter/GlobalFilter.java
  40. 47 0
      ucard-backend/src/main/java/com/crm/rely/backend/interceptor/LocaleInterceptor.java
  41. 90 0
      ucard-backend/src/main/java/com/crm/rely/backend/interceptor/NoRepeatSubmitInterceptor.java
  42. 9 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/BlackListLoginService.java
  43. 44 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/BlackListService.java
  44. 76 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/EmailService.java
  45. 51 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/MqSendService.java
  46. 206 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/RedisService.java
  47. 47 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/SmsService.java
  48. 18 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/SysCountryService.java
  49. 20 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/BlackListLoginServiceImpl.java
  50. 247 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/BlackListServiceImpl.java
  51. 316 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/EmailServiceImpl.java
  52. 172 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/MqSendServiceImpl.java
  53. 421 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/RedisServiceImpl.java
  54. 129 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/SmsServiceImpl.java
  55. 187 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/SysCountryServiceImpl.java
  56. 141 0
      ucard-backend/src/main/java/com/crm/rely/backend/service/impl/base/BaseUpload.java
  57. 196 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/AESUtil.java
  58. 223 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/AliasMethodUtil.java
  59. 886 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/BCrypt.java
  60. 50 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/Base64Util.java
  61. 130 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/BaseExpireMap.java
  62. 36 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/BeanUtilPlus.java
  63. 10 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/BigDecimalUtil.java
  64. 38 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/CidUtil.java
  65. 30 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/CmdUtil.java
  66. 124 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/CompareUtil.java
  67. 263 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/ConcurrentHashMapCacheUtils.java
  68. 28 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/CountryUtil.java
  69. 21 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/CurrencySymbolUtil.java
  70. 220 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/CustomInfoUtil.java
  71. 65 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DESedeUtil.java
  72. 13 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DataSourceNames.java
  73. 604 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DateUtil.java
  74. 12 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DefaultValueUtil.java
  75. 77 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DeleteDirectory.java
  76. 24 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/Difference.java
  77. 48 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DistanceUtil.java
  78. 121 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DwTypeUtil.java
  79. 24 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DynamicDataSourceContext.java
  80. 17 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/DynamicRoutingDataSource.java
  81. 151 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/ECDSAUtil.java
  82. 149 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/EasyExcelUti.java
  83. 95 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/ExcelListener.java
  84. 229 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/ExportUtil.java
  85. 336 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/FileProcessUtil.java
  86. 63 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/FloatFormat.java
  87. 252 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/FormatPage.java
  88. 321 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/GetIpAndMac.java
  89. 158 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/GetRandom.java
  90. 203 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/GlobalHttpServletRequestWrapper.java
  91. 583 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/GoogleAdminAPIHttp.java
  92. 110 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/GoogleAuthenticatorUtil.java
  93. 65 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/HAMCSHA1Util.java
  94. 107 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/HAMCSHA256Util.java
  95. 141 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/HAMCSHAUtil.java
  96. 132 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/HandShake.java
  97. 67 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/HexUtil.java
  98. 104 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/HttpServletRequestUtil.java
  99. 445 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/HttpUtil.java
  100. 79 0
      ucard-backend/src/main/java/com/crm/rely/backend/util/ImageBase64ConverterUtil.java

+ 3 - 3
uacrd-manager-server/pom.xml

@@ -34,9 +34,9 @@
         </dependency>
 
         <dependency>
-            <groupId>com.crm.login.backend</groupId>
-            <artifactId>crm-login-backend</artifactId>
-            <version>1.0.0</version>
+            <groupId>com.crm.uacrd</groupId>
+            <artifactId>ucard-login-backend</artifactId>
+            <version>1.0-SNAPSHOT</version>
         </dependency>
 
         <dependency>

+ 1 - 1
uacrd-manager-server/src/main/resources/application.yml

@@ -2,7 +2,7 @@ server:
   port: 8501
 spring:
   profiles:
-    active: test
+    active: dev
   application:
     name: ucard-manager-service
 mybatis:

+ 200 - 0
ucard-backend/pom.xml

@@ -0,0 +1,200 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>com.crm.uacrd</groupId>
+  <artifactId>ucard-backend</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>8</source>
+                    <target>8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <layout>ZIP</layout>
+                    <includes>
+                        <include>
+                            <groupId>com.crm.backend</groupId>
+                            <artifactId>crm-backend</artifactId>
+                        </include>
+                    </includes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <java.version>1.8</java.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.crm.uacrd</groupId>
+            <artifactId>ucard-core</artifactId>
+            <version>1.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-core</artifactId>
+            <version>8.5.31</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+            <version>4.3.17.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <version>4.3.17.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.8.11.1</version>
+        </dependency>
+
+
+        <!-- 连接池-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid</artifactId>
+            <version>1.0.29</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-activemq</artifactId>
+            <version>1.5.8.RELEASE</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <version>1.5.8.RELEASE</version>
+        </dependency>
+
+        <!--spring切面aop依赖-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+            <version>1.5.8.RELEASE</version>
+        </dependency>
+
+        <!-- 该依赖必加,里面有spring对schedule的支持 -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+            <version>4.3.17.RELEASE</version>
+        </dependency>
+
+        <!--mq连接池-->
+        <dependency>
+            <groupId>org.apache.activemq</groupId>
+            <artifactId>activemq-pool</artifactId>
+            <version>5.14.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>core</artifactId>
+            <version>3.2.1</version>
+        </dependency>
+
+        <!-- xss过滤组件 -->
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+            <version>1.11.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+            <version>2.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-redis</artifactId>
+            <version>1.8.3.RELEASE</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>18.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.codehaus.janino</groupId>
+            <artifactId>janino</artifactId>
+            <version>2.7.8</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+            <version>1.5.13.RELEASE</version>
+        </dependency>
+
+        <!-- 导出 -->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>3.17</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>3.17</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.8.1</version>
+        </dependency>
+        <dependency>
+            <groupId>net.renfei</groupId>
+            <artifactId>ip2location</artifactId>
+            <version>1.0.2</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.warrenstrange/googleauth -->
+        <dependency>
+            <groupId>com.warrenstrange</groupId>
+            <artifactId>googleauth</artifactId>
+            <version>1.5.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.larksuite.oapi</groupId>
+            <artifactId>oapi-sdk</artifactId>
+            <version>2.4.11-beta.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.10.0</version>
+        </dependency>
+
+    </dependencies>
+</project>

+ 297 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/CacheAspect.java

@@ -0,0 +1,297 @@
+package com.crm.rely.backend.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+
+/**
+ * 缓存切面配置类。
+ */
+@Slf4j
+@Aspect
+@Component
+public class CacheAspect {
+//
+//    private static final long CACHE_EXPIRY_MILLISECONDS = 60 * 1000;
+//
+//    @Autowired
+//    private RedisService redisService;
+//
+//    /**
+//     * 定义切点:拦截 Mapper 和 Repository 层的方法。
+//     */
+//    @Pointcut("execution(* com.crm.*.dao.mapper.*.*(..)) || execution(* com.crm.*.dao.repository.*.*(..))")
+//
+//    public void daoLayer() {
+//    }
+//
+//    /**
+//     * 环绕通知:处理缓存逻辑。
+//     */
+//    @Around("daoLayer()")
+//    public Object cacheHandler(ProceedingJoinPoint joinPoint) throws Throwable {
+//        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+//        Method method = signature.getMethod();
+//        Object[] args = joinPoint.getArgs();
+//
+//        String methodName = method.getName();
+//        Class<?> returnType = method.getReturnType();
+//
+//        if (isQueryMethod(methodName)) {
+//            return handleCacheQuery(joinPoint, method, args, returnType);
+//        } else if (isModifyMethod(methodName)) {
+//            return handleCacheModification(joinPoint, args);
+//        }
+//
+//        return joinPoint.proceed();
+//    }
+//
+//    /**
+//     * 查询逻辑处理。
+//     */
+//    private Object handleCacheQuery(ProceedingJoinPoint joinPoint, Method method, Object[] args, Class<?>
+//            returnType) throws Throwable {
+//        String cacheKey = generateCacheKeyForQuery(method, args);
+//
+//        try {
+//            if (List.class.isAssignableFrom(returnType)) {
+//                Class<?> elementType = getGenericReturnType(method); // 获取列表的泛型类型
+//                if (elementType != null) {
+//                    List<?> cachedList = redisService.getList(cacheKey, elementType);
+//                    if (cachedList != null) {
+//                        log.info("Cache hit for key: {}", cacheKey);
+//                        return cachedList;
+//                    }
+//                }
+//            } else {
+//                Object cachedEntity = redisService.getEntity(cacheKey, returnType);
+//                if (cachedEntity != null) {
+//                    log.info("Cache hit for key: {}", cacheKey);
+//                    return cachedEntity;
+//                }
+//            }
+//        } catch (Exception e) {
+//            log.warn("Failed to retrieve or deserialize cache for key: {}, proceeding with DB call. Error: {}",
+//                    cacheKey, e);
+//        }
+//
+//        Object result = joinPoint.proceed();
+//
+//        try {
+//            redisService.save(cacheKey, result, CACHE_EXPIRY_MILLISECONDS);
+//            log.info("Cache saved for key: {} with expiry {} milliseconds", cacheKey, CACHE_EXPIRY_MILLISECONDS);
+//        } catch (Exception e) {
+//            log.warn("Failed to save cache for key: {}", cacheKey, e);
+//        }
+//
+//        return result;
+//    }
+//
+//
+//    /**
+//     * 增删改逻辑处理。
+//     */
+//    private Object handleCacheModification(ProceedingJoinPoint joinPoint, Object[] args) throws Throwable {
+//
+//        // 生成所有相关的缓存键
+//        List<String> affectedKeys = generateCacheKeysForModification(args);
+//
+//        // 清理通配符键
+//        for (String key : affectedKeys) {
+//            try {
+//                redisService.remove(key);
+//                log.info("Cache evicted for key: {}", key);
+//            } catch (Exception e) {
+//                log.warn("Failed to evict cache for key: {}", key, e);
+//            }
+//        }
+//
+//        // 执行原始方法
+//        Object result = joinPoint.proceed();
+//
+//        // 再次清理缓存键,防止修改方法的延迟生效导致不一致
+//        for (String key : affectedKeys) {
+//            try {
+//                redisService.remove(key);
+//                log.info("Cache evicted post execution for key: {}", key);
+//            } catch (Exception e) {
+//                log.warn("Failed to evict cache post execution for key: {}", key, e);
+//            }
+//        }
+//
+//        return result;
+//    }
+//
+//    /**
+//     * 根据查询生成缓存键。
+//     */
+//    private String generateCacheKeyForQuery(Method method, Object[] args) {
+//        StringBuilder keyBuilder = new StringBuilder();
+//
+//        // 使用类名作为缓存键基础
+//        String baseKey = extractBaseKeyFromClass(method.getDeclaringClass());
+//        keyBuilder.append(baseKey);
+//
+//        // 添加方法名
+//        keyBuilder.append(":").append(method.getName());
+//
+//        // 添加参数部分作为缓存键
+//        keyBuilder.append(":").append(generateArgsKey(args));
+//        return keyBuilder.toString();
+//    }
+//
+//    private List<String> generateCacheKeysForModification(Object[] args) {
+//        List<String> keys = new ArrayList<>();
+//
+//        for (Object arg : args) {
+//            if (arg == null) {
+//                continue; // 忽略空参数
+//            }
+//            if (arg instanceof Collection<?>) {
+//                // 递归处理 Collection 类型的参数
+//                for (Object item : (Collection<?>) arg) {
+//                    keys.addAll(generateCacheKeysForModification(new Object[]{item}));
+//                }
+//                continue;
+//            }
+//
+//            String baseKey = extractBaseKeyFromClass(arg.getClass());
+//            if (baseKey == null || baseKey.isEmpty()) {
+//                continue; // 忽略无法解析基础键的参数
+//            }
+//            String uniqueIdentifier = getUniqueIdentifier(arg);
+//            if (uniqueIdentifier != null) {
+//                keys.add(baseKey + ":get*" + uniqueIdentifier);
+//                keys.add(baseKey + ":find*" + uniqueIdentifier);
+//            }
+//
+//            // 添加基础键和参数的 JSON 表示
+//            keys.add(baseKey + ":" + JSON.toJSONString(arg));
+//
+//            // 添加与列表查询和计数相关的键
+//            keys.add(baseKey + ":select*");
+//            keys.add(baseKey + ":count*");
+//        }
+//
+//        return keys;
+//    }
+//
+//    /**
+//     * 判断是否为查询方法。
+//     */
+//    private boolean isQueryMethod(String methodName) {
+//
+//        return !methodName.startsWith("find") && (methodName.startsWith("get") || methodName.startsWith("select")
+//        || methodName.startsWith("query") ||
+//                methodName.startsWith("count"));
+//    }
+//
+//    /**
+//     * 判断是否为增删改方法。
+//     */
+//    private boolean isModifyMethod(String methodName) {
+//        return methodName.startsWith("add") || methodName.startsWith("insert") ||
+//                methodName.startsWith("update") || methodName.startsWith("save") || methodName.startsWith("delete");
+//    }
+//
+//    private boolean isDeleteMethod(String methodName) {
+//        return methodName.startsWith("delete");
+//    }
+//
+//
+//    /**
+//     * 生成参数的唯一标识。
+//     */
+//    private String generateArgsKey(Object[] args) {
+//        StringBuilder keyBuilder = new StringBuilder();
+//        for (Object arg : args) {
+//            if (arg instanceof BaseTable) {
+//                keyBuilder.append(getUniqueIdentifier((BaseTable) arg));
+//            } else if (arg instanceof List) {
+//                for (Object item : (List<?>) arg) {
+//                    if (item instanceof BaseTable) {
+//                        keyBuilder.append(getUniqueIdentifier((BaseTable) item));
+//                    }
+//                }
+//            } else {
+//                keyBuilder.append(JSON.toJSONString(arg)); // 对于非实体类参数,保留其字符串形式
+//            }
+//            keyBuilder.append("|");
+//        }
+//
+//        // 移除最后一个 "|"
+//        if (keyBuilder.length() > 0) {
+//            keyBuilder.setLength(keyBuilder.length() - 1);
+//        }
+//
+//        return keyBuilder.toString();
+//    }
+//
+//    /**
+//     * 提取表名。
+//     */
+//    private String getTableName(Class<?> clazz) {
+//        if (clazz.isAnnotationPresent(Table.class)) {
+//            return clazz.getAnnotation(Table.class).name();
+//        }
+//        return clazz.getSimpleName();
+//    }
+//
+//    /**
+//     * 提取泛型类型。
+//     */
+//    private Class<?> getGenericReturnType(Method method) {
+//        try {
+//            java.lang.reflect.Type genericReturnType = method.getGenericReturnType();
+//            if (genericReturnType instanceof java.lang.reflect.ParameterizedType) {
+//                java.lang.reflect.Type[] typeArguments =
+//                        ((java.lang.reflect.ParameterizedType) genericReturnType).getActualTypeArguments();
+//                if (typeArguments.length > 0) {
+//                    return (Class<?>) typeArguments[0];
+//                }
+//            }
+//        } catch (Exception e) {
+//            log.warn("Failed to extract generic type for method: {}", method.getName(), e);
+//        }
+//        return null;
+//    }
+//
+//    /**
+//     * 获取 object 实例的唯一标识。
+//     * 通常通过主键字段(如 ID)生成。
+//     */
+//    private String getUniqueIdentifier(Object o) {
+//        try {
+//            // 尝试通过反射获取 ID 属性的值
+//            Method getIdMethod = o.getClass().getMethod("getId");
+//            Object id = getIdMethod.invoke(o);
+//            if (id != null) {
+//                return id.toString();
+//            }
+//        } catch (Exception e) {
+//            log.warn("Failed to extract unique identifier for object: {}", o.getClass().getSimpleName(),
+//                    e);
+//        }
+//        // 如果无法获取 ID,使用类名和 hashCode 作为唯一标识的备选
+//        return o.getClass().getSimpleName() + "@" + System.identityHashCode(o);
+//    }
+//
+//    /**
+//     * 从类名中提取缓存键的基础部分。
+//     * 移除尾部的 "Repository", "Mapper", "Dao" 后缀。
+//     */
+//    private String extractBaseKeyFromClass(Class<?> clazz) {
+//        String className = clazz.getSimpleName();
+//        if (className.endsWith("Repository")) {
+//            return className.substring(0, className.length() - "Repository" .length());
+//        } else if (className.endsWith("Mapper")) {
+//            return className.substring(0, className.length() - "Mapper" .length());
+//        } else if (className.endsWith("Dao")) {
+//            return className.substring(0, className.length() - "Dao" .length());
+//        }
+//        if (className.endsWith("Table")) {
+//            return className.substring(0, className.length() - "Table" .length());
+//        }
+//        return className; // 如果没有后缀,直接返回类名
+//    }
+}

+ 256 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/ControllerAop.java

@@ -0,0 +1,256 @@
+package com.crm.rely.backend.aspect;
+
+import com.crm.rely.backend.configuration.LocaleMessage;
+import com.crm.rely.backend.configuration.LogQuartzComponent;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.constant.FeignClientAnnotation;
+import com.crm.rely.backend.core.dto.base.BaseResultDto;
+import com.crm.rely.backend.core.entity.base.BaseDateAndPageSearchEntity;
+import com.crm.rely.backend.core.entity.base.DateBaseSearchEntity;
+import com.crm.rely.backend.core.pojo.BaseTable;
+import com.crm.rely.backend.exception.ControllerException;
+import com.crm.rely.backend.service.RedisService;
+import com.crm.rely.backend.util.DateUtil;
+import com.google.common.base.Strings;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author houn
+ */
+@Aspect
+@Component
+@Order(1)
+public class ControllerAop {
+
+    @Autowired
+    private LocaleMessage localeMessage;
+
+    @Autowired
+    private RedisService redisService;
+
+    private ThreadLocal<Map<String, Map<Long, Map<LogType, Object>>>> threadLocal = new ThreadLocal<>();
+
+
+    /**
+     * 添加到 threadLocal中
+     *
+     * @param className
+     * @param id
+     * @param logType
+     * @param object
+     * @author: houn
+     * @date: 2019/7/22 15:20
+     */
+    public void addThreadLocal(String className, Long id, LogType logType, Object object) {
+        //获取ThreadLocal中的值
+        Map<String, Map<Long, Map<LogType, Object>>> mapMapMap = threadLocal.get();
+        if (mapMapMap == null) {
+            mapMapMap = new HashMap<>(10);
+        }
+        //根据类获取数据
+        Map<Long, Map<LogType, Object>> mapMap = mapMapMap.get(className);
+        if (mapMap == null) {
+            mapMap = new HashMap<>(10);
+        }
+        Iterable iterable = null;
+        if (id == null && logType.equals(LogType.DELETE)) {
+            if (object instanceof Long) {
+                id = (Long) object;
+                object = null;
+            } else if (object instanceof Iterable) {
+                iterable = (Iterable<Long>) object;
+            }
+        }
+        if (iterable != null) {
+            LogType finalLogType = logType;
+            Map<Long, Map<LogType, Object>> finalMapMap = mapMap;
+            iterable.forEach(value -> {
+                if (value instanceof Long) {
+                    Long idIterable = (Long) value;
+                    Map<LogType, Object> map = getMap(finalMapMap, idIterable);
+                    map.put(finalLogType, null);
+                    finalMapMap.put(idIterable, map);
+                } else if (value instanceof BaseTable) {
+                    BaseTable tableIterable = (BaseTable) value;
+                    Map<LogType, Object> map = getMap(finalMapMap, tableIterable.getId());
+                    map.put(finalLogType, null);
+                    finalMapMap.put(tableIterable.getId(), map);
+                }
+            });
+        } else {
+            //根据类名 在根据id获取操作类型
+            Map<LogType, Object> map = getMap(mapMap, id);
+            //如果是更新 看一下是否存在LogType.INSERT 如果存在LogType.INSERT对应的数据 将logType改为LogType.INSERT
+            if (logType.equals(LogType.UPDATE)) {
+                Object mapObject = map.get(LogType.INSERT);
+                if (mapObject != null) {
+                    logType = LogType.INSERT;
+                }
+            }
+            //根据操作类型添加数据
+            map.put(logType, object);
+            mapMap.put(id, map);
+        }
+
+        mapMapMap.put(className, mapMap);
+        threadLocal.set(mapMapMap);
+    }
+
+    private Map<LogType, Object> getMap(Map<Long, Map<LogType, Object>> mapMap, Long id) {
+        Map<LogType, Object> map = mapMap.get(id);
+        if (map == null) {
+            map = new HashMap<>(4);
+        }
+        return map;
+    }
+
+    @Pointcut("execution(* com.*.*.controller.*.*(..))")
+    public void allController() {
+    }
+
+    @Pointcut("execution(* com.*.*.controller.*.*.*(..))")
+    public void allIbController() {
+    }
+
+    @Before(value = "allController()||allIbController()")
+    public void allControllerBefore(JoinPoint joinPoint) {
+        for (Object arg : joinPoint.getArgs()) {
+            if (arg instanceof Iterable) {
+                Iterable iterable = (Iterable) arg;
+                iterable.forEach(value -> {
+                    setInfo(value);
+                });
+            } else {
+                setInfo(arg);
+            }
+        }
+        clear();
+    }
+
+    private void setInfo(Object arg) {
+        if (arg instanceof BaseDateAndPageSearchEntity) {
+            BaseDateAndPageSearchEntity dateAndPageSearchEntity = (BaseDateAndPageSearchEntity) arg;
+            if (dateAndPageSearchEntity != null && dateAndPageSearchEntity.getTemporaryDate() == null) {
+                if (dateAndPageSearchEntity.getEndDate() != null) {
+                    dateAndPageSearchEntity.setTemporaryDate(dateAndPageSearchEntity.getEndDate());
+                    if (dateAndPageSearchEntity.getEndDate() != null) {
+                        Date endDate = DateUtil.operationDay(dateAndPageSearchEntity.getEndDate(), 1);
+                        dateAndPageSearchEntity.setEndDate(DateUtil.parseDate(endDate));
+                    }
+                }
+                if (dateAndPageSearchEntity.getStartDate() != null) {
+                    dateAndPageSearchEntity.setStartDate(DateUtil.parseDate(dateAndPageSearchEntity.getStartDate()));
+                }
+            }
+        } else if (arg instanceof DateBaseSearchEntity) {
+            DateBaseSearchEntity dateBaseSearchEntity = (DateBaseSearchEntity) arg;
+            if (dateBaseSearchEntity != null && dateBaseSearchEntity.getTemporaryDate() == null) {
+                if (dateBaseSearchEntity.getEndDate() != null) {
+                    dateBaseSearchEntity.setTemporaryDate(dateBaseSearchEntity.getEndDate());
+//                    if (dateBaseSearchEntity.getEndDate() != null) {
+//                        Date endDate = DateUtil.operationDay(dateBaseSearchEntity.getEndDate(), 1);
+//                        dateBaseSearchEntity.setEndDate(DateUtil.parseDate(endDate));
+//                    }
+                    if (dateBaseSearchEntity.getEndDate() == null) {
+                        dateBaseSearchEntity.setEndDate(DateUtil.getMonthEnd(dateBaseSearchEntity.getEndDate()));
+                    } else {
+                        dateBaseSearchEntity.setEndDate(DateUtil.addDate(dateBaseSearchEntity.getEndDate()));
+                    }
+                }
+                if (dateBaseSearchEntity.getStartDate() != null) {
+                    dateBaseSearchEntity.setStartDate(DateUtil.parseDate(dateBaseSearchEntity.getStartDate()));
+                }
+            }
+        }
+    }
+
+    @AfterReturning(value = "allController()||allIbController()", returning = "resultObject")
+    public void allControllerAfterReturning(Object resultObject) {
+
+        if (resultObject instanceof BaseResultDto) {
+            BaseResultDto baseResultDto = (BaseResultDto) resultObject;
+
+            baseResultDto.setMsg(localeMessage.getMessage(baseResultDto.getMsg(), baseResultDto.getMsg()));
+            if (baseResultDto.getCode() != Constants.SUCCESS_CODE) {
+                clear();
+                return;
+            }
+        }
+
+        addQueue();
+    }
+
+
+    /**
+     * @description: 添加到队列中
+     * @param: 无参数
+     * @return: 无返回值
+     * @author: houn
+     * @date: 2019/7/22 15:18
+     */
+    private void addQueue() {
+//        String url="";
+//        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+//        if (null != attributes) {
+//            HttpServletRequest request = attributes.getRequest();
+//           String url= request.getRequestURI();
+//        }
+
+        Map<String, Map<Long, Map<LogType, Object>>> mapMapMap = threadLocal.get();
+        if (mapMapMap == null) {
+            clear();
+            return;
+        }
+
+        clear();
+//        LogQuartzComponent.add(mapMapMap,url);
+        LogQuartzComponent.add(mapMapMap);
+    }
+
+    public void clear() {
+        threadLocal.remove();
+    }
+
+    /**
+     * feign调用Token验证
+     *
+     * @param joinPoint
+     * @param feignClientAnnotation
+     */
+    @Before(value = "@annotation(feignClientAnnotation)")
+    public void before(JoinPoint joinPoint, FeignClientAnnotation feignClientAnnotation) throws Exception {
+
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        HttpServletRequest request = attributes.getRequest();
+        String feignToken = request.getHeader(Constants.Feign_Token);
+        String accessToken = request.getHeader(Constants.ACCESS_TOKEN);
+
+        if (Strings.isNullOrEmpty(feignToken) || Strings.isNullOrEmpty(accessToken)) {
+            throw new ControllerException(Constants.NOT_PERMIT);
+        }
+
+        String feignTokenValue = redisService.getObject(feignToken);
+
+        if (Strings.isNullOrEmpty(feignTokenValue) || !accessToken.equals(feignTokenValue)) {
+            throw new ControllerException(Constants.NOT_PERMIT);
+        } else {
+            redisService.remove(feignToken);
+        }
+    }
+
+
+}

+ 177 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/ControllerExceptionAdvice.java

@@ -0,0 +1,177 @@
+package com.crm.rely.backend.aspect;
+
+import com.crm.rely.backend.configuration.LocaleMessage;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.exception.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+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.bind.annotation.ResponseStatus;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * controller异常捕获
+ */
+@ControllerAdvice
+public class ControllerExceptionAdvice {
+    private static final String CODE = "code";
+    private static final String MSG = "msg";
+    private static Boolean isDev = null;
+    @Autowired
+    private LocaleMessage localeMessage;
+    @Autowired
+    private ControllerAop controllerAop;
+    /**
+     * 获取配置 用于判断系统是否开发环境 若是开发环境这将异常完全抛到页面 无需打开日志查看
+     */
+    @Autowired
+    private Environment environment;
+
+    /**
+     * service运行时异常处理 自动捕获到controller的所有异常 并进行处理返回前端
+     *
+     * @param e 异常
+     * @return 返回到页面的处理结果
+     */
+    @ResponseBody
+    @ExceptionHandler(value = Exception.class)
+    public Map controllerExceptionHandler(Exception e) throws IOException {
+        if (isDev()) {
+            e.printStackTrace();
+        }
+        int code = Constants.FAILED_CODE;
+        String msg = null;
+        //判断是否404 500异常
+        if (e instanceof NoHandlerFoundException) {
+            code = Constants.FAILED_CODE_404;
+            msg = e.getMessage();
+        } else if (e instanceof ServletException) {
+            code = Constants.SYSTEM_ERROR_CODE;
+            msg = e.getMessage();
+        }
+        if (e instanceof HttpMessageNotReadableException) {
+            code = Constants.FAILED_CODE;
+            msg = Constants.PARAMETER_ERROR;
+        } else if (e instanceof RuntimeException) {
+            code = Constants.SYSTEM_ERROR_CODE;
+            msg = Constants.SYSTEM_ERROR;
+       }
+//        else if (e instanceof PayValidatedException) {
+//            msg = e.getMessage();
+//            //ISO-8859-1
+//            HttpServletResponse response = getServletResponse();
+//            msg = new String(msg.getBytes("UTF-8"), "iso-8859-1");
+//
+//            //跳转到支付-支付验证错误页面
+//            response.sendRedirect("/pay/pay-validated-fail.html?error=" + msg);
+//            return null;
+//        }
+
+        //判断登录异常 抛出登录过期 登录错误等 否则判断是否逻辑异常 如果异常 直接取到填写的结果
+        if (e instanceof LoginException) {
+            code = Constants.UNLOGIN_CODE;
+            msg = e.getMessage();
+
+        } else if (e instanceof ServiceException || e instanceof ControllerException||e instanceof PayValidatedException ||e instanceof DataNotFoundException) {
+            msg = e.getMessage();
+            if (e instanceof ServiceException) {
+                ServiceException serviceException = (ServiceException) e;
+                if (serviceException.getCode() > 0) {
+                    code = serviceException.getCode();
+                }
+            }
+        }
+
+        if (e instanceof RuntimeException) {
+            code = Constants.SYSTEM_ERROR_CODE;
+        }
+
+        Map map = new HashMap(2);
+        map.put(CODE, code);
+        map.put(MSG, localeMessage.getMessage(msg, msg));
+        controllerAop.clear();
+        return map;
+    }
+
+    /**
+     * 捕获验证异常 即通过validation验证框架中异常 controller的body接收model中有验证注解 其他无需处理
+     *
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    @ResponseBody
+    @ResponseStatus(HttpStatus.OK)
+    public Map validateErrorHandler(MethodArgumentNotValidException e) {
+        BindingResult bindingResult = e.getBindingResult();
+
+        String errorMsg = Constants.SYSTEM_ERROR;
+        if (bindingResult.hasErrors()) {
+            List<FieldError> errorList = bindingResult.getFieldErrors();
+            if (isDev()) {
+                /**
+                 * 在开发环境中返回具体到那个字段 那个注解未通过 方便调试
+                 */
+//                errorMsg = errorList.get(0).getField() + " 字段错误,错误原因:" + errorList.get(0).getDefaultMessage();
+                final String errorFormat = "%s字段错误,错误原因:%s\n";
+                StringBuffer errorMsgSB = new StringBuffer(errorList.size());
+                for (FieldError error : errorList) {
+                    errorMsgSB.append(String.format(errorFormat, error.getField(), error.getDefaultMessage()));
+                }
+                errorMsg = errorMsgSB.toString();
+            } else {
+                errorMsg = errorList.get(0).getDefaultMessage();
+            }
+        }
+        Map map = new HashMap(2);
+        map.put(CODE, Constants.FAILED_CODE);
+        map.put(MSG, errorMsg);
+        controllerAop.clear();
+        return map;
+    }
+
+    /**
+     * 判断是否是本地环境
+     *
+     * @return
+     */
+    private boolean isDev() {
+        if (isDev == null) {
+            if (environment.getActiveProfiles() == null || environment.getActiveProfiles().length <= 0) {
+                isDev = false;
+            }
+            for (String value : environment.getActiveProfiles()) {
+                if (value.equals("dev")) {
+                    isDev = true;
+                    return isDev;
+                }
+            }
+        }
+
+        if (isDev == null) {
+            isDev = false;
+        }
+
+        return isDev;
+    }
+
+    private HttpServletResponse getServletResponse() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
+    }
+}

+ 222 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/DaoMapperAop.java

@@ -0,0 +1,222 @@
+package com.crm.rely.backend.aspect;
+
+import com.crm.rely.backend.core.pojo.BaseTable;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author houn
+ */
+@Aspect
+@Component
+public class DaoMapperAop {
+
+    private final static String ERROR_FORMAT = "出错方法:%s---错误原因:%s";
+    private ThreadLocal<List<BaseTable>> threadLocal = new ThreadLocal<>();
+    private Logger logger = LoggerFactory.getLogger(DaoMapperAop.class);
+
+    @Autowired
+    private Environment environment;
+
+    @Autowired
+    ControllerAop controllerAop;
+
+    @Pointcut("execution(* com.*.*.dao.*.*.save*(..)) && !execution(* com.*.log.dao.*.*.save*(..))")
+    public void saveLog() {
+    }
+
+    @Pointcut("execution(* com.*.*.dao.*.*.add*(..)) && !execution(* com.*.log.dao.*.*.add*(..))")
+    public void addLog() {
+    }
+
+    @Pointcut("execution(* com.*.*.dao.*.*.update*(..)) && !execution(* com.*.log.dao.*.*.update*(..))")
+    public void updateLog() {
+    }
+
+    @Pointcut("execution(* com.*.*.dao.*.*.delete*(..))  && !execution(* com.*.log.dao.*.*.delete*(..))")
+    public void deleteLog() {
+    }
+    @Pointcut("execution(* com.*.*.dao.*.*.find*(..))")
+    public void findLog() {
+    }
+
+    @Pointcut("execution(* com.*.*.dao.*.*.*(..))")
+    public void exceptionLog() {
+    }
+
+    @Before(value = "saveLog()||addLog()||updateLog()")
+    public void beforeSave(JoinPoint joinPoint) {
+        List<BaseTable> tables = getTables(joinPoint);
+        if (tables == null) {
+            throw new RuntimeException("exception:dao aop beforeSave saveLog table is null");
+        }
+        tables.forEach(value -> {
+            if (value.getId() == null) {
+                threadLocal.set(tables);
+            } else {
+                addControllerAopThreadLocal(value, LogType.UPDATE);
+            }
+        });
+    }
+
+    @AfterReturning(value = "saveLog()||addLog()||updateLog()", returning = "resultObject")
+    public void afterReturningSave(Object resultObject) {
+        LogType logType;
+        List<BaseTable> tables = threadLocal.get();
+        if (tables == null) {
+            logType = LogType.UPDATE;
+            tables = getTables(resultObject);
+        } else {
+            logType = LogType.INSERT;
+            tables = getTables(resultObject);
+            threadLocal.remove();
+        }
+        if (tables == null) {
+            throw new RuntimeException("exception:dao aop afterReturningSave saveLog table is null");
+        }
+        tables.forEach(value -> addControllerAopThreadLocal(value, logType));
+    }
+
+    @After(value = "deleteLog()")
+    public void afterDelete(JoinPoint joinPoint) {
+        LogType logType = LogType.DELETE;
+        Object[] args = joinPoint.getArgs();
+        for (int i = args.length - 1; i >= 0; i--) {
+            if (args[i] instanceof Long) {
+                Long id = (Long) args[i];
+                String className = getT(joinPoint);
+                controllerAop.addThreadLocal(className, null, logType, id);
+                break;
+            } else if (args[i] instanceof Iterable) {
+                Iterable iterable = (Iterable) args[i];
+                iterable.forEach(value -> {
+                    if (value instanceof Long) {
+                        Long id = (Long) value;
+                        String className = getT(joinPoint);
+                        controllerAop.addThreadLocal(className, null, logType, id);
+                    } else {
+                        BaseTable table = getBaseTable(value);
+                        addControllerAopThreadLocal(table, logType);
+                    }
+                });
+                break;
+            }else if (args[i] instanceof BaseTable) {
+                BaseTable iterable = (BaseTable) args[i];
+                BaseTable table = getBaseTable(iterable);
+                addControllerAopThreadLocal(table, logType);
+
+            }
+        }
+    }
+
+    @AfterReturning(value = "findLog()", returning = "resultObject")
+    public void afterReturningFind(Object resultObject) {
+        addControllerAopThreadLocal(resultObject, LogType.FIND);
+    }
+
+    private void addControllerAopThreadLocal(BaseTable table, LogType logType) {
+
+        controllerAop.addThreadLocal(table.getClass().toString(), table.getId(), logType, table);
+    }
+
+    private void addControllerAopThreadLocal(Object resultObject, LogType logType) {
+
+        List<BaseTable> tables = getTables(resultObject);
+        if (tables != null && tables.size() > 0) {
+            tables.forEach(value -> controllerAop.addThreadLocal(value.getClass().toString(), value.getId(), logType, value));
+        }
+    }
+
+    private List<BaseTable> getTables(Object resultObject) {
+        List<BaseTable> tables = new ArrayList<>(10);
+        if ((resultObject instanceof BaseTable)) {
+            BaseTable table = getBaseTable(resultObject);
+            if (table != null) {
+                tables.add(table);
+            }
+        }
+
+        if ((resultObject instanceof Iterable)) {
+            Iterable iterable = (Iterable) resultObject;
+            iterable.forEach(value -> {
+                BaseTable table = getBaseTable(value);
+                if (table != null) {
+                    tables.add(table);
+                }
+            });
+        }
+        return tables;
+    }
+
+    private BaseTable getBaseTable(Object resultObject) {
+        Object object = null;
+        try {
+            object = resultObject.getClass().newInstance();
+            if (!(object instanceof BaseTable)) {
+                return null;
+            }
+        } catch (InstantiationException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+        BeanUtils.copyProperties(resultObject, object);
+        BaseTable table = (BaseTable) object;
+        return table;
+    }
+
+    private List<BaseTable> getTables(JoinPoint joinPoint) {
+        List<BaseTable> tables = new ArrayList<>();
+        Object[] args = joinPoint.getArgs();
+        for (int i = args.length - 1; i >= 0; i--) {
+            if (args[i] instanceof BaseTable) {
+                tables.add((BaseTable) args[i]);
+                break;
+            } else if (args[i] instanceof Iterable) {
+                tables.addAll(getTables(args[i]));
+                break;
+            }
+        }
+        return tables;
+    }
+
+    private String getT(JoinPoint joinPoint) {
+        Type[] types = joinPoint.getTarget().getClass().getGenericInterfaces();
+        Class controllerClass = (Class) types[0];
+        String className = ((ParameterizedType) controllerClass.getGenericInterfaces()[0]).getActualTypeArguments()[0].toString();
+        return className;
+    }
+
+    /**
+     * 异常处理方法
+     *
+     * @param joinPoint
+     * @param e
+     */
+    @AfterThrowing(value = "exceptionLog()", throwing = "e")
+    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
+        for (String value : environment.getActiveProfiles()) {
+            if (value.equals("dev")) {
+                e.printStackTrace();
+                break;
+            }
+        }
+
+        //日志打印出错方法+原因
+        logger.error(String.format(ERROR_FORMAT, joinPoint.getSignature().getName(), e.getMessage()));
+
+        //抛出异常
+        throw new RuntimeException("system error");
+    }
+}

+ 106 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/DynamicDataSourceAspect.java

@@ -0,0 +1,106 @@
+package com.crm.rely.backend.aspect;
+
+import com.crm.rely.backend.util.DataSourceNames;
+import com.crm.rely.backend.util.DynamicDataSourceContext;
+import lombok.extern.slf4j.Slf4j;
+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.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @Author houn
+ * @Date 2024/12/24 14:00
+ * @PackageName:com.crm.manager.configuration
+ * @ClassName: DynamicDataSourceAspect
+ * @Description: TODO
+ */
+@Aspect
+@Component
+@Slf4j
+public class DynamicDataSourceAspect {
+
+    // 数据源缓存,减少重复判断带来的性能开销
+    private final ConcurrentHashMap<Method, String> dataSourceCache = new ConcurrentHashMap<>();
+
+    @Value("${spring.dynamic-datasource.enable-auto-datasource-switch:false}")
+    private boolean enableAutoDataSourceSwitch;
+
+    @Pointcut("execution(* com.crm.*.dao.mapper.*.*(..)) || execution(* com.crm.*.dao.repository.*.*(..))")
+    public void dataSourcePointCut() {
+    }
+
+    @Around("dataSourcePointCut()")
+    public Object switchDataSource(ProceedingJoinPoint joinPoint) throws Throwable {
+        // 如果动态数据源切换未启用,直接执行目标方法
+        if (!enableAutoDataSourceSwitch) {
+            return joinPoint.proceed();
+        }
+
+        // 获取当前方法对象
+        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
+        String methodName = method.getName();
+        String className = method.getDeclaringClass().getSimpleName();
+
+        // 判断是否有事务激活
+        boolean transactionInherited = isTransactionInherited();
+
+        if (!transactionInherited) {
+            // 从缓存中获取数据源,未命中则动态解析
+            String dataSource = dataSourceCache.computeIfAbsent(method, this::determineDataSource);
+
+            log.info("切换数据源: {} -> 方法: {}.{}", dataSource, className, methodName);
+            DynamicDataSourceContext.setDataSource(dataSource);
+        } else {
+            log.debug("事务已激活,保持现有数据源 -> 方法: {}.{}", className, methodName);
+        }
+
+        try {
+            // 执行目标方法
+            return joinPoint.proceed();
+        } finally {
+            // 如果事务未激活,清理数据源上下文,避免线程污染
+            if (!transactionInherited) {
+                DynamicDataSourceContext.clear();
+                log.debug("清理数据源上下文 -> 方法: {}.{}", className, methodName);
+            }
+        }
+    }
+
+    /**
+     * 判断是否是事务传递。
+     *
+     * @return true 如果事务是从调用者方法传递过来的;false 否则
+     */
+    private boolean isTransactionInherited() {
+        return TransactionSynchronizationManager.isSynchronizationActive() &&
+                TransactionSynchronizationManager.isActualTransactionActive();
+    }
+
+    /**
+     * 根据方法名动态确定数据源。
+     *
+     * @param method 当前方法对象
+     * @return 数据源名称
+     */
+    private String determineDataSource(Method method) {
+        String methodName = method.getName().toLowerCase();
+
+        if (methodName.startsWith("add") || methodName.startsWith("insert") || methodName.startsWith("update") ||
+                methodName.startsWith("find") || methodName.startsWith("save") ||
+                methodName.startsWith("delete")) {
+            return DataSourceNames.MASTER;
+        } else if (methodName.startsWith("get") || methodName.startsWith("query") ||
+                methodName.startsWith("select") || methodName.startsWith("count")) {
+            return DataSourceNames.SLAVE;
+        }
+        return DataSourceNames.MASTER; // 默认使用主数据源
+    }
+}

+ 36 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/EntityScanPathResolver.java

@@ -0,0 +1,36 @@
+package com.crm.rely.backend.aspect;
+
+import org.springframework.beans.BeansException;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+
+@Component
+public class EntityScanPathResolver implements ApplicationContextAware {
+
+    private String[] entityScanPaths;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        Class<?> mainApplicationClass = Arrays.stream(applicationContext.getBeanDefinitionNames())
+                .map(applicationContext::getType)
+                .filter(type -> type != null && type.isAnnotationPresent(SpringBootApplication.class))
+                .findFirst()
+                .orElseThrow(() -> new IllegalStateException("Cannot find SpringBootApplication class"));
+
+        EntityScan annotation = AnnotationUtils.findAnnotation(mainApplicationClass, EntityScan.class);
+        if (annotation != null) {
+            entityScanPaths = annotation.value();
+        }
+    }
+
+    // 创建一个方法返回EntityScan的路径
+    public String[] getEntityScanPaths() {
+        return entityScanPaths;
+    }
+}

+ 86 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/HibernateQueryInterceptor.java

@@ -0,0 +1,86 @@
+package com.crm.rely.backend.aspect;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.EmptyInterceptor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @Author houn
+ * @Date 2024/12/24 17:43
+ * @PackageName:com.crm.rely.backend.aspect
+ * @ClassName: HibernateQueryInterceptor
+ * @Description: TODO
+ */
+@Slf4j
+@Component
+public class HibernateQueryInterceptor extends EmptyInterceptor {
+    @Value("${spring.jpa.hibernate.interceptor.enableDeleteCondition:false}")
+    private boolean enableDeleteCondition;
+
+    @Override
+    public String onPrepareStatement(String sql) {
+        if (!enableDeleteCondition) {
+            return sql;
+        }
+        // 检查是否已经包含 delete 条件
+        if (!sql.toLowerCase().contains("delete = 0")) {
+            // 修改 SQL,添加 delete 条件
+            sql = modifySqlWithDeleteCondition(sql);
+        }
+
+        log.debug("拦截并修改 SQL: {}", sql);
+        return super.onPrepareStatement(sql);
+    }
+
+    private String modifySqlWithDeleteCondition(String sql) {
+        if (sql.trim().toLowerCase().startsWith("select")) {
+            return handleSelectSql(sql);
+        } else if (sql.trim().toLowerCase().startsWith("update")) {
+            return handleUpdateSql(sql);
+        }
+        // 不处理其他类型的 SQL
+        return sql;
+    }
+
+    private String handleSelectSql(String sql) {
+        // 匹配 SELECT 查询中的 WHERE 子句
+        String regex = "(?i)(\\bfrom\\b.*?\\bwhere\\b)";
+        Pattern pattern = Pattern.compile(regex);
+        Matcher matcher = pattern.matcher(sql);
+
+        if (matcher.find()) {
+            String fromClause = matcher.group(1); // 获取匹配的 "FROM ... WHERE" 部分
+
+            // 提取表别名
+            String tableAliasRegex = "(?i)(\\bfrom\\b\\s+[^\\s]+\\s+(\\w+))";
+            Pattern aliasPattern = Pattern.compile(tableAliasRegex);
+            Matcher aliasMatcher = aliasPattern.matcher(fromClause);
+
+            if (aliasMatcher.find()) {
+                String alias = aliasMatcher.group(2); // 获取表别名
+                return sql.replaceFirst("(?i)where", "WHERE " + alias + ".`delete` = 0 AND ");
+            } else {
+                // 如果没有别名,直接添加 delete 条件
+                return sql.replaceFirst("(?i)where", "WHERE `delete` = 0 AND ");
+            }
+        } else {
+            // 如果没有 WHERE 子句,直接添加 WHERE 和条件
+            return sql + " WHERE `delete` = 0";
+        }
+    }
+
+    private String handleUpdateSql(String sql) {
+        // 检查是否已经存在 WHERE 子句
+        if (sql.toLowerCase().contains(" where ")) {
+            // 在 WHERE 子句前添加 delete 条件
+            return sql.replaceFirst("(?i)where", "WHERE `delete` = 0 AND ");
+        } else {
+            // 如果没有 WHERE 子句,直接添加条件
+            return sql + " WHERE `delete` = 0";
+        }
+    }
+}

+ 147 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/LimiterAspect.java

@@ -0,0 +1,147 @@
+package com.crm.rely.backend.aspect;
+
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.constant.LimitType;
+import com.crm.rely.backend.core.constant.RequestNumLimiter;
+import com.crm.rely.backend.core.constant.RequestSinglePointLimiter;
+import com.crm.rely.backend.core.entity.login.InfoEntity;
+import com.crm.rely.backend.exception.LoginException;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.RedisService;
+import com.crm.rely.backend.util.DateUtil;
+import com.crm.rely.backend.util.GetIpAndMac;
+import com.crm.rely.backend.util.HttpServletRequestUtil;
+import com.crm.rely.backend.util.UUIDUtil;
+import com.google.common.base.Strings;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.After;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+@Aspect
+@Component
+@Slf4j
+public class LimiterAspect {
+
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
+    @Autowired
+    private RedisScript<Long> limitScript;
+    @Autowired
+    private RedisService redisService;
+
+    private ThreadLocal<SinglePointLimiterEntity> singlePointLimiterThreadLocal = ThreadLocal.withInitial(() -> null);
+
+    @Pointcut("execution(* com.crm..controller..*.save*(..)) || " +
+            "execution(* com.crm..controller..*.add*(..)) || " +
+            "execution(* com.crm..controller..*.apply*(..))")
+    public void allControllerMethods() {
+    }
+
+
+    @Before("@annotation(requestNumLimiter)")
+    public void handleRequestNumLimiter(JoinPoint point, RequestNumLimiter requestNumLimiter) throws Throwable {
+        String combineKey = generateCombineKey(requestNumLimiter.key(), requestNumLimiter.limitType(), point);
+        List<Object> keys = Collections.singletonList(combineKey);
+
+        try {
+            Long currentRequestCount = redisTemplate.execute(limitScript, keys, requestNumLimiter.count(),
+                    requestNumLimiter.time());
+            if (currentRequestCount == null || currentRequestCount > requestNumLimiter.count()) {
+                throw new ServiceException("访问过于频繁,请稍候再试");
+            }
+            log.info("限流成功: [限制次数: {}, 当前请求次数: {}, 缓存Key: {}]", requestNumLimiter.count(), currentRequestCount,
+                    combineKey);
+        } catch (Exception e) {
+            log.error("限流脚本执行失败, Key: {}, 错误: {}", combineKey, e.getMessage(), e);
+            throw new RuntimeException("服务器限流异常,请稍候再试");
+        }
+    }
+
+    @Before("@annotation(requestSinglePointLimiter)")
+//    @Before("@annotation(requestSinglePointLimiter)||allController()||allIbController()")
+    public void handleRequestSinglePointLimiter(JoinPoint point, RequestSinglePointLimiter requestSinglePointLimiter) throws Throwable {
+        String combineKey = generateCombineKey(requestSinglePointLimiter.key(), requestSinglePointLimiter.limitType()
+                , point);
+        String requestId = UUIDUtil.getUUID();
+
+        if (!redisService.tryLock(combineKey, requestId)) {
+            log.info("单点限流触发: [时间: {}, 缓存Key: {}]", DateUtil.formatDateTime(), combineKey);
+            throw ServiceException.restrictRequest();
+        }
+
+        singlePointLimiterThreadLocal.set(new SinglePointLimiterEntity(combineKey, requestId));
+    }
+
+    @After("@annotation(requestSinglePointLimiter)")
+//    @After("@annotation(requestSinglePointLimiter)||allController()||allIbController()")
+    public void releaseRequestSinglePointLimiter(RequestSinglePointLimiter requestSinglePointLimiter) {
+        SinglePointLimiterEntity limiterEntity = singlePointLimiterThreadLocal.get();
+        if (limiterEntity != null) {
+            try {
+                redisService.unlock(limiterEntity.getKey(), limiterEntity.getRequest());
+            } catch (Exception e) {
+                log.error("释放锁失败, Key: {}, 错误: {}", limiterEntity.getKey(), e.getMessage(), e);
+            } finally {
+                singlePointLimiterThreadLocal.remove();
+            }
+        }
+    }
+
+    private String generateCombineKey(String baseKey, LimitType limitType, JoinPoint point) throws ServiceException {
+        StringBuilder keyBuilder = new StringBuilder(baseKey);
+        HttpServletRequest request =
+                ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
+
+        keyBuilder.append(resolveLimitTypeKey(limitType, request));
+
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        keyBuilder.append(method.getDeclaringClass().getName()).append("-").append(method.getName());
+
+        return keyBuilder.toString();
+    }
+
+    private String resolveLimitTypeKey(LimitType limitType, HttpServletRequest request) throws ServiceException {
+        switch (limitType) {
+            case IP:
+                return GetIpAndMac.getIp(request) + "-";
+            case CLIENT_IP:
+                return GetIpAndMac.getClientIp(request) + "-";
+            case TOKEN:
+                String accessToken = request.getHeader(Constants.ACCESS_TOKEN);
+                if (Strings.isNullOrEmpty(accessToken)) {
+                    throw LoginException.exception(Constants.INVALID);
+                }
+                return accessToken + "-";
+            case USER:
+                InfoEntity userInfo = HttpServletRequestUtil.getLoginInfo(request, redisService);
+                return userInfo.getPrefix().getCode() + "-" + userInfo.getId() + "-";
+            case NOT:
+            default:
+                return "";
+        }
+    }
+
+    @Data
+    @AllArgsConstructor
+    private static class SinglePointLimiterEntity {
+        private String key;
+        private String request;
+    }
+}

+ 11 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/LogType.java

@@ -0,0 +1,11 @@
+package com.crm.rely.backend.aspect;
+
+/**
+ * @program: demo
+ * @description:
+ * @author: houn
+ * @create: 2019-07-19 15:00
+ */
+public enum LogType {
+    INSERT,UPDATE,DELETE,FIND
+}

+ 178 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/ServiceAop.java

@@ -0,0 +1,178 @@
+package com.crm.rely.backend.aspect;
+
+import com.crm.rely.backend.core.constant.PrefixEnum;
+import com.crm.rely.backend.core.entity.base.BaseDateAndPageSearchEntity;
+import com.crm.rely.backend.core.entity.base.BaseInterfaceEntity;
+import com.crm.rely.backend.core.entity.base.DateBaseSearchEntity;
+import com.crm.rely.backend.core.entity.custom.info.InfoEntity;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.RedisService;
+import com.crm.rely.backend.service.SysCountryService;
+import com.crm.rely.backend.util.DateUtil;
+import com.crm.rely.backend.util.GetIpAndMac;
+import com.crm.rely.backend.util.HttpServletRequestUtil;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.beans.factory.annotation.Autowired;
+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.servlet.http.HttpServletRequest;
+import java.util.Date;
+
+/**
+ * @author houn
+ */
+@Aspect
+@Component
+public class ServiceAop {
+
+    @Autowired
+    private RedisService redisService;
+
+    @Autowired
+    private ControllerAop controllerAop;
+
+    @Autowired
+    private SysCountryService sysCountryService;
+
+    //    private ThreadLocal<InfoEntity> threadLocal = new ThreadLocal<>();
+    @Pointcut("execution(* com.*.*.service.*.*.*(..))")
+    public void allService() {
+    }
+
+    @Pointcut("execution(* com.*.*.service.*.*.*.*(..))")
+    public void allPayService() {
+    }
+
+
+    @Before("allService()||allPayService()")
+    public void allServiceBefore(JoinPoint joinPoint) throws Exception {
+        Long userId;
+        String ip;
+        String returnStr;
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+
+        if (requestAttributes != null) {
+            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
+            userId = getUserId(request);
+            ip = GetIpAndMac.getIp(request);
+            if ("127.0.0.1".equals(ip)) {
+                returnStr = "127.0.0.1-localhost";
+            } else {
+                returnStr = sysCountryService.getAdderByIp(ip);
+            }
+        } else {
+            returnStr = "127.0.0.1-localhost";
+            userId = -1L;
+        }
+
+        Date dateTime = new Date();
+        for (Object arg : joinPoint.getArgs()) {
+            setTimeInfo(arg);
+
+            if (arg instanceof Iterable) {
+                Iterable iterable = (Iterable) arg;
+                iterable.forEach(value -> {
+                    try {
+                        setInfo(returnStr, userId, dateTime, value);
+                    } catch (ServiceException e) {
+                        e.printStackTrace();
+                    }
+                });
+            } else {
+                setInfo(returnStr, userId, dateTime, arg);
+            }
+        }
+    }
+
+    private void setInfo(String ip, Long userId, Date dateTime, Object arg) throws ServiceException {
+        if (arg instanceof BaseInterfaceEntity) {
+            ((BaseInterfaceEntity) arg).populateMetadata(ip, userId, "", dateTime);
+        }
+    }
+
+    private Long getUserId(HttpServletRequest request) throws ServiceException {
+        InfoEntity loginInfoEntity = getLoginInfo(request);
+
+        Long userId = null;
+        if (PrefixEnum.PREFIX_CUSTOM.equals(loginInfoEntity.getPrefix())) {
+            if (loginInfoEntity.getCustomInfo() != null) {
+                userId = loginInfoEntity.getCustomInfo().getId();
+            }
+
+        } else {
+            if (loginInfoEntity.getIbInfo() != null) {
+                userId = loginInfoEntity.getIbInfo().getId();
+            }
+        }
+//        if (PrefixEnum.PREFIX_SYSTEM.equals(loginInfoEntity.getPrefix())
+//                || PrefixEnum.PREFIX_SALE.equals(loginInfoEntity.getPrefix())
+//                || PrefixEnum.PREFIX_USER.equals(loginInfoEntity.getPrefix())) {
+//
+//            if (loginInfoEntity.getIbInfo() != null) {
+//                userId = loginInfoEntity.getIbInfo().getId();
+//            }
+//
+//        } else {
+//            if (loginInfoEntity.getCustomInfo() != null) {
+//                userId = loginInfoEntity.getCustomInfo().getId();
+//            }
+//        }
+        return userId;
+    }
+
+    private void setTimeInfo(Object arg) {
+        if (arg instanceof BaseDateAndPageSearchEntity) {
+            BaseDateAndPageSearchEntity dateAndPageSearchEntity = (BaseDateAndPageSearchEntity) arg;
+            if (dateAndPageSearchEntity != null && dateAndPageSearchEntity.getTemporaryDate() == null) {
+                if (dateAndPageSearchEntity.getEndDate() != null) {
+                    dateAndPageSearchEntity.setTemporaryDate(dateAndPageSearchEntity.getEndDate());
+                    if (dateAndPageSearchEntity.getEndDate() != null) {
+                        Date endDate = DateUtil.operationDay(dateAndPageSearchEntity.getEndDate(), 1);
+                        dateAndPageSearchEntity.setEndDate(DateUtil.parseDate(endDate));
+                    }
+                }
+                if (dateAndPageSearchEntity.getStartDate() != null) {
+                    dateAndPageSearchEntity.setStartDate(DateUtil.parseDate(dateAndPageSearchEntity.getStartDate()));
+                }
+            }
+        } else if (arg instanceof DateBaseSearchEntity) {
+            DateBaseSearchEntity dateBaseSearchEntity = (DateBaseSearchEntity) arg;
+            if (dateBaseSearchEntity != null && dateBaseSearchEntity.getTemporaryDate() == null) {
+                if (dateBaseSearchEntity.getEndDate() != null) {
+                    dateBaseSearchEntity.setTemporaryDate(dateBaseSearchEntity.getEndDate());
+                    if (dateBaseSearchEntity.getEndDate() != null) {
+                        Date endDate = DateUtil.operationDay(dateBaseSearchEntity.getEndDate(), 1);
+                        dateBaseSearchEntity.setEndDate(DateUtil.parseDate(endDate));
+                    }
+                }
+                if (dateBaseSearchEntity.getStartDate() != null) {
+                    dateBaseSearchEntity.setStartDate(DateUtil.parseDate(dateBaseSearchEntity.getStartDate()));
+                }
+            }
+        }
+    }
+
+    /**
+     * 获取登录信息
+     *
+     * @param request
+     * @return
+     * @throws ServiceException
+     */
+    private InfoEntity getLoginInfo(HttpServletRequest request) throws ServiceException {
+
+        InfoEntity userInfoEntity = HttpServletRequestUtil.getLoginInfo(InfoEntity.class, request, redisService, true);
+
+        if (userInfoEntity == null) {
+            userInfoEntity = new InfoEntity();
+        }
+
+        return userInfoEntity;
+    }
+}

+ 69 - 0
ucard-backend/src/main/java/com/crm/rely/backend/aspect/ServiceExceptionAop.java

@@ -0,0 +1,69 @@
+package com.crm.rely.backend.aspect;
+
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.exception.*;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+@Aspect
+@Component
+public class ServiceExceptionAop {
+    Logger logger = LoggerFactory.getLogger(RuntimeException.class);
+
+    @Autowired
+    private Environment environment;
+
+    private static final String logStr = "class:%s%n,method: %s%n%s,row: %d";
+
+    @Pointcut("execution(* com.*.*.service.*.*(..))")
+    public void log() {
+    }
+
+    @AfterThrowing(value = "log()", throwing = "e")
+    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
+
+        /**
+         * 日志打印出错方法+原因
+         */
+        for (int i = 0; i < environment.getActiveProfiles().length; i++) {
+            if (environment.getActiveProfiles()[i].equals("dev")) {
+                e.printStackTrace();
+                break;
+            }
+        }
+        String declaringClass = joinPoint.getTarget().getClass().getName();
+        String methodName = joinPoint.getSignature().getName();
+
+        int lineNumber = -1;
+        for (int i = 0; i < e.getStackTrace().length; i++) {
+            if (declaringClass.equals(e.getStackTrace()[i].getClassName())
+                    && methodName.equals(e.getStackTrace()[i].getMethodName())) {
+
+                lineNumber = e.getStackTrace()[i].getLineNumber();
+                break;
+            }
+        }
+
+
+        logger.error(String.format(logStr, declaringClass, methodName, e, lineNumber));
+        if (!(e instanceof ServiceException)
+                && !(e instanceof ControllerException)
+                && !(e instanceof PayValidatedException)
+                && !(e instanceof DataNotFoundException)
+                && !(e instanceof LoginException)) {
+            /**
+             *
+             * 抛出系统异常
+             */
+            throw new RuntimeException(Constants.SYSTEM_ERROR);
+        }
+    }
+}
+

+ 29 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/ApplicationContextProvider.java

@@ -0,0 +1,29 @@
+package com.crm.rely.backend.configuration;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ApplicationContextProvider implements ApplicationContextAware {
+    private static ApplicationContext context;
+
+    public static ApplicationContext getApplicationContext() {
+        return context;
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext ac)
+            throws BeansException {
+        context = ac;
+    }
+
+    public static <T> T getBean(Class<T> tClass) {
+        return context.getBean(tClass);
+    }
+
+    public static <T> T getBean(String name, Class<T> tClass) {
+        return context.getBean(name, tClass);
+    }
+}

+ 20 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/BaseActiveMqConfiguration.java

@@ -0,0 +1,20 @@
+package com.crm.rely.backend.configuration;
+
+import com.crm.rely.backend.core.constant.Constants;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.springframework.context.annotation.Bean;
+
+import javax.jms.Queue;
+
+public class BaseActiveMqConfiguration {
+    @Bean(name = "logQueue")
+    public Queue logQueue() {
+        return new ActiveMQQueue(Constants.ACTIVEMQ_JMS_URL_LOG);
+    }
+
+    @Bean(name = "smsQueue")
+    public Queue smsQueue() {
+        return new ActiveMQQueue(Constants.ACTIVEMQ_JMS_URL_SMS);
+    }
+
+}

+ 20 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/BigDecimalSerializer.java

@@ -0,0 +1,20 @@
+package com.crm.rely.backend.configuration;
+
+import com.alibaba.fastjson.serializer.JSONSerializer;
+import com.alibaba.fastjson.serializer.ObjectSerializer;
+import com.alibaba.fastjson.serializer.SerializeWriter;
+
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+
+public  class BigDecimalSerializer implements ObjectSerializer {
+
+    @Override
+    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features){
+        SerializeWriter out = serializer.out;
+        BigDecimal value = (BigDecimal)object;
+        value= value.stripTrailingZeros();
+        out.write(value.toPlainString());
+    }
+
+}

+ 52 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/CustomJsonHttpMessageConverter.java

@@ -0,0 +1,52 @@
+package com.crm.rely.backend.configuration;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializeConfig;
+import com.alibaba.fastjson.serializer.SimpleDateFormatSerializer;
+import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+
+/**
+ * 返回的时间格式格式化
+ *
+ * @program: user-custom
+ * @description: account json converter
+ * @author: houn
+ * @create: 2019-06-05 17:29
+ */
+public class CustomJsonHttpMessageConverter extends FastJsonHttpMessageConverter {
+
+    private final static String DEFFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
+    private final static String DEFFAULT_SQL_DATE_FORMAT = "yyyy-MM-dd";
+    private final static String DEFFAULT_SQL_TIME_FORMAT = "HH:mm:ss";
+    private final static String DEFFAULT_SQL_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss";
+    //字符编码
+    private static final String CHARSET = "UTF-8";
+
+    @Override
+    protected void writeInternal(Object obj, HttpOutputMessage outputMessage)
+            throws IOException, HttpMessageNotWritableException {
+        OutputStream out = outputMessage.getBody();
+        String text = JSON.toJSONString(obj, getSerializeConfig(), super.getFastJsonConfig().getSerializerFeatures());
+        byte[] bytes = text.getBytes(CHARSET);
+        out.write(bytes);
+    }
+
+    /**
+     * @return
+     */
+    private SerializeConfig getSerializeConfig() {
+        SerializeConfig config = new SerializeConfig();
+        config.put(java.util.Date.class, new SimpleDateFormatSerializer(DEFFAULT_DATE_FORMAT));
+        config.put(java.sql.Date.class, new SimpleDateFormatSerializer(DEFFAULT_SQL_DATE_FORMAT));
+        config.put(java.sql.Timestamp.class, new SimpleDateFormatSerializer(DEFFAULT_SQL_TIMESTAMP_FORMAT));
+        config.put(java.sql.Time.class, new SimpleDateFormatSerializer(DEFFAULT_SQL_TIME_FORMAT));
+        config.put(BigDecimal.class, new BigDecimalSerializer());
+        return config;
+    }
+}

+ 73 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/DataSourceConfig.java

@@ -0,0 +1,73 @@
+package com.crm.rely.backend.configuration;
+
+import com.crm.rely.backend.util.DynamicRoutingDataSource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.*;
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @Author houn
+ * @Date 2024/12/24 13:59
+ * @PackageName:com.crm.manager.configuration
+ * @ClassName: DynamicDataSourceConfig
+ * @Description: TODO
+ */
+@Slf4j
+@Configuration
+public class DataSourceConfig {
+
+    @Value("${spring.dynamic-datasource.enable-auto-datasource-switch:false}")
+    private boolean enableAutoDataSourceSwitch;
+
+    @Lazy
+    @Bean(name = "defaultDataSource")
+    @ConfigurationProperties(prefix = "spring.datasource")
+    public DataSource defaultDataSource() {
+        return DataSourceBuilder.create().build();
+    }
+
+    @Bean(name = "masterDataSource")
+    @ConfigurationProperties(prefix = "spring.dynamic-datasource.datasource.master")
+    public DataSource masterDataSource() {
+        return DataSourceBuilder.create().build();
+    }
+
+    @Bean(name = "slaveDataSource")
+    @ConfigurationProperties(prefix = "spring.dynamic-datasource.datasource.slave")
+    public DataSource slaveDataSource() {
+        return DataSourceBuilder.create().build();
+    }
+
+    @Bean(name = "dynamicDataSource")
+    @Primary
+    @DependsOn({"defaultDataSource", "masterDataSource", "slaveDataSource"})
+    public DataSource dynamicDataSource(DataSource defaultDataSource,
+                                        DataSource masterDataSource,
+                                        DataSource slaveDataSource) {
+        log.info("Loading dynamic data sources...");
+        log.info("Default DataSource: {}", defaultDataSource);
+        log.info("Master DataSource: {}", masterDataSource);
+        log.info("Slave DataSource: {}", slaveDataSource);
+
+        if (!enableAutoDataSourceSwitch) {
+            return defaultDataSource; // 返回默认数据源
+        }
+
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put("master", masterDataSource);
+        targetDataSources.put("slave", slaveDataSource);
+
+        AbstractRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
+        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
+        dynamicDataSource.setTargetDataSources(targetDataSources);
+
+        return dynamicDataSource;
+    }
+}

+ 192 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/DefaultEnvironmentPostProcessor.java

@@ -0,0 +1,192 @@
+package com.crm.rely.backend.configuration;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.stereotype.Component;
+
+import java.util.Properties;
+
+/**
+ * @program: crm-backend
+ * @description: 默认配置
+ * @author: houn
+ * @create: 2019-07-26 13:48
+ */
+@Component
+public class DefaultEnvironmentPostProcessor implements EnvironmentPostProcessor {
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+
+        Properties properties = new Properties();
+
+        //文件缓存地址
+        properties.put("server.tomcat.basedir", "./tmp");
+
+        //配置多语言 加载messages数据 如果在messages中没有 则在defaultMessages获取 如果还是没有 则返回填写的内容
+        //就是messages覆盖defaultMessages messages为项目自己配置
+        properties.put("spring.messages.basename", "i18n/messages,i18n/defaultMessages");
+
+        properties.put("web.upload-path", "./upload");
+        properties.put("web.front-path", "./front");
+        properties.put("web.export-path", "./export");
+
+        properties.put("ip.ip2LocationLiteDb3", "../IP2LOCATION");
+
+        properties.put("spring.resources.static-locations", "file:${web.front-path},file:${web.upload-path}," +
+                "file:${web.export-path}");
+
+        /**
+         * 默认activemq pool配置
+         */
+        properties.put("spring.activemq.pool.create-connection-on-startup", true);
+        properties.put("spring.activemq.pool.enabled", true);
+        properties.put("spring.activemq.pool.expiry-timeout", 0);
+        properties.put("spring.activemq.pool.block-if-full", true);
+        properties.put("spring.activemq.pool.idle-timeout", 30000);
+        properties.put("spring.activemq.pool.max-connections", 100);
+        properties.put("spring.activemq.pool.maximum-active-session-per-connection", 5000);
+        properties.put("spring.activemq.pool.reconnect-on-exception", true);
+        properties.put("spring.activemq.pool.time-between-expiration-check", -1);
+        properties.put("spring.activemq.pool.use-anonymous-producers", true);
+
+        /**
+         * 默认activemq配置
+         */
+        properties.put("spring.activemq.broker-url", "tcp://localhost:61616");
+        properties.put("spring.activemq.close-timeout", 15);
+        properties.put("spring.activemq.in-memory", true);
+        properties.put("spring.activemq.non-blocking-redelivery", false);
+        properties.put("spring.activemq.user", "admin");
+        properties.put("spring.activemq.masterPassword", "admin");
+        properties.put("spring.activemq.send-timeout", 0);
+
+        /**
+         * 默认sql配置
+         */
+        properties.put("spring.datasource.type", "com.alibaba.druid.pool.DruidDataSource");
+        properties.put("spring.datasource.filters", "stat");
+        properties.put("spring.datasource.maxActive", 50);
+        properties.put("spring.datasource.initialSize", 1);
+        properties.put("spring.datasource.maxWait", 60000);
+        properties.put("spring.datasource.minIdle", 10);
+        properties.put("spring.datasource.testWhileIdle", true);
+        properties.put("spring.datasource.timeBetweenEvictionRunsMillis", 60000);
+        properties.put("spring.datasource.minEvictableIdleTimeMillis", 300000);
+        properties.put("spring.datasource.validationQuery", "SELECT 'x'");
+
+        properties.put("spring.datasource.tomcat.max-active", 50);
+        properties.put("spring.datasource.tomcat.max-idle", 30);
+        properties.put("spring.datasource.tomcat.min-idle", 10);
+        properties.put("spring.datasource.tomcat.initial-size", 10);
+        properties.put("spring.datasource.tomcat.max-wait", 60000);
+        properties.put("spring.datasource.tomcat.remove-abandoned", true);
+        properties.put("spring.datasource.tomcat.remove-abandoned-timeout", 60);
+        /**
+         * 默认redis配置
+         * spring:
+         *     redis:
+         *         database: 0
+         *         host: localhost
+         *         pool:
+         *             max-active: 8
+         *             max-idle: 8
+         *             max-wait: -1
+         *             min-idle: 0
+         *         port: 6379
+         *         timeout: 0
+         */
+        properties.put("spring.redis.database", 0);
+        properties.put("spring.redis.type", 1);
+        properties.put("spring.redis.host", "127.0.0.1");
+        properties.put("spring.redis.port", 6379);
+        properties.put("spring.redis.timeout", 5000);
+        properties.put("spring.redis.masterPassword", "");
+        properties.put("spring.redis.pool.max-active", 3000);
+        properties.put("spring.redis.pool.max-wait", 50);
+        properties.put("spring.redis.pool.max-idle", 10);
+        properties.put("spring.redis.pool.min-idle", 5);
+
+        properties.put("spring.redis.lettuce.pool.max-active", 3000);
+        properties.put("spring.redis.lettuce.pool.max-wait", 50);
+        properties.put("spring.redis.lettuce.pool.max-idle", 100);
+        properties.put("spring.redis.lettuce.pool.min-idle", 50);
+
+        /**
+         * db
+         */
+        properties.put("spring.data.mongodb.repositories.enabled", false);
+        properties.put("spring.data.redis.repositories.enabled", false);
+
+        properties.put("spring.freemarker.check-template-location", false);
+
+        //默认模版为false
+        properties.put("spring.thymeleaf.cache", false);
+
+        //默认文件上传
+        properties.put("spring.http.multipart.max-file-size", "150MB");
+        properties.put("spring.http.multipart.max-request-size", "150MB");
+
+        properties.put("spring.http.encoding.charset", "utf-8");
+        properties.put("spring.http.encoding.force", true);
+        properties.put("spring.http.encoding.enabled", true);
+        //默认session过期时间7200(秒,两小时)
+        properties.put("server.session.timeout", 7200);
+
+        /**
+         * 默认mybatis配置 在classpath:mapper/*.xml
+         */
+        properties.put("mybatis.mapper-locations", "classpath:mapper/*.xml");
+        properties.put("mybatis.configuration.map-underscore-users-camel-case", true);
+        properties.put("mybatis.configuration.use-column-label", true);
+        properties.put("mybatis.configuration.use-generated-keys", true);
+        properties.put("mybatis.configuration.map-underscore-to-camel-case", true);
+        properties.put("mybatis.configuration.log-impl", "org.apache.ibatis.logging.stdout.StdOutImpl");
+
+        properties.put("page.redirect.enable", false);
+        properties.put("page.redirect.url", "/error404.html");
+
+        /**
+         * eureka
+         */
+        properties.put("eureka.client.serviceUrl.defaultZone", "http://admin:admin123456@localhost:7000/eureka");
+        properties.put("eureka.instance.prefer-ip-address", true);
+        properties.put("eureka.instance.instance-id", "${spring.application.name}:${spring.cloud.client" +
+                ".ipAddress}:${spring.application.instance_id:${server.port}}");
+        properties.put("ribbon.ReadTimeout", 60000);
+        properties.put("ribbon.ConnectTimeout", 60000);
+        //默认打印日志等级
+//        properties.put("logging.level.root","error");
+        properties.put("login.single-sign-on", false);
+
+        //Feign Token
+        properties.put("feign-token", "b20a6a368a17d39f62f380f3a2830679");
+
+        properties.put("logging.level.com.netflix.discovery.shared.resolver.aws.ConfigClusterResolver", "WARN");
+        properties.put("logging.file.max-history", 30);
+        properties.put("logging.file.max-size", "100MB");
+
+        //springboot默认启动线程
+        //#最大并发数
+        //server.tomcat.max-threads=1000
+        //#接受和处理的最大连接数
+        //server.tomcat.max-connections=9
+        //#初始化时创建的线程数
+        //server.tomcat.min-SpareThreads=6
+        //#可以放到处理队列中的请求数
+        //server.tomcat.acceptCount=10
+        //# session最大超时时间(分钟),默认为30分钟
+        //server.session-timeout=60
+        properties.put("server.tomcat.max-connections", 500);
+        properties.put("server.tomcat.accept-count", 5000);//等待
+        properties.put("server.tomcat.max-threads", 5000);//最大线程
+        properties.put("server.tomcat.min-spare-threads", 500);//最小线程
+
+        PropertiesPropertySource propertySource = new PropertiesPropertySource("default", properties);
+
+        environment.getPropertySources().addLast(propertySource);
+    }
+
+}

+ 144 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/GlobalConfiguration.java

@@ -0,0 +1,144 @@
+package com.crm.rely.backend.configuration;
+
+import com.alibaba.fastjson.parser.Feature;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.alibaba.fastjson.support.config.FastJsonConfig;
+import com.crm.rely.backend.interceptor.LocaleInterceptor;
+import com.crm.rely.backend.interceptor.NoRepeatSubmitInterceptor;
+import com.crm.rely.backend.service.RedisService;
+import com.google.common.base.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * @author houn
+ */
+public class GlobalConfiguration extends WebMvcConfigurerAdapter {
+    @Autowired
+    private RedisService redisService;
+
+    @Autowired
+    private LocaleMessage localeMessage;
+
+    /**
+     * 设置返回
+     *
+     * @param converters
+     */
+    @Override
+    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+        super.configureMessageConverters(converters);
+
+        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
+        List<MediaType> mediaTypes = new ArrayList<>();
+        mediaTypes.add(MediaType.TEXT_PLAIN);
+        mediaTypes.add(MediaType.TEXT_HTML);
+        mediaTypes.add(MediaType.TEXT_XML);
+
+        stringHttpMessageConverter.setSupportedMediaTypes(mediaTypes);
+        stringHttpMessageConverter.setDefaultCharset(Charset.forName("UTF-8"));
+
+        converters.add(stringHttpMessageConverter);
+    }
+
+    /**
+     * fastJson格式化
+     *
+     * @return
+     */
+    @Bean
+    public HttpMessageConverters fastJsonHttpMessageConverters() {
+
+        /**
+         * 使用自定义时间格式化 继承FastJsonHttpMessageConverter
+         */
+        CustomJsonHttpMessageConverter fasHttpMessageConverter = new CustomJsonHttpMessageConverter();
+        List<MediaType> supportedMediaTypes = new ArrayList<>();
+        supportedMediaTypes.add(MediaType.APPLICATION_JSON);
+        supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
+        supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
+        supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
+        supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
+        supportedMediaTypes.add(MediaType.APPLICATION_PDF);
+        supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
+        supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
+        supportedMediaTypes.add(MediaType.APPLICATION_XML);
+        supportedMediaTypes.add(MediaType.IMAGE_GIF);
+        supportedMediaTypes.add(MediaType.IMAGE_JPEG);
+        supportedMediaTypes.add(MediaType.IMAGE_PNG);
+        supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
+        supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
+        fasHttpMessageConverter.setSupportedMediaTypes(supportedMediaTypes);
+
+        FastJsonConfig fastJsonConfig = new FastJsonConfig();
+
+        fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue,
+                SerializerFeature.WriteNullListAsEmpty);
+        fastJsonConfig.setFeatures( Feature.OrderedField);
+        fasHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
+
+        HttpMessageConverter<?> converter = fasHttpMessageConverter;
+        return new HttpMessageConverters(converter);
+    }
+
+    /**
+     * 设置跨域
+     *
+     * @param registry
+     */
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        //设置允许跨域的路径
+        registry.addMapping("/**")
+                //设置允许跨域请求的域名
+                .allowedOrigins("*")
+                //是否允许证书 不再默认开启
+                .allowCredentials(true)
+                //设置允许的方法
+                .allowedMethods("*")
+                .allowedHeaders("*")
+                //跨域允许时间
+                .maxAge(3600);
+    }
+//    @Override
+//    public void addInterceptors(InterceptorRegistry registry) {
+//
+//
+//        registry.addInterceptor(new NoRepeatSubmitInterceptor(redisService, localeMessage)).addPathPatterns("/**");
+//        super.addInterceptors(registry);
+//    }
+    @Bean
+    LocaleInterceptor localeInterceptor() {
+        return new LocaleInterceptor();
+    }
+
+    @Bean
+    public LocaleResolver localeResolver() {
+        final SessionLocaleResolver localeResolver = new SessionLocaleResolver();
+        localeResolver.setDefaultLocale(new Locale("zh", "CN"));
+        return localeResolver;
+    }
+    @Bean
+    public TaskScheduler scheduledExecutorService() {
+        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+        scheduler.setPoolSize(8);
+        scheduler.setThreadNamePrefix("scheduled-thread-");
+        return scheduler;
+    }
+}

+ 59 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/GlobalFilterConfiguration.java

@@ -0,0 +1,59 @@
+package com.crm.rely.backend.configuration;
+
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.util.SensitiveWordFilterUtil;
+import com.crm.rely.backend.util.SensitiveWordInitUtil;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.filter.GlobalFilter;
+import com.crm.rely.backend.service.RedisService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 全局过滤器
+ *
+ * @author houn
+ */
+public class GlobalFilterConfiguration {
+
+    @Autowired
+    private RedisService redisService;
+
+    /**
+     * 过滤敏感字和xss攻击
+     *
+     * @return
+     * @throws ServiceException
+     */
+//    @Bean
+//    public FilterRegistrationBean globalFilterRegistrationBean() throws ServiceException {
+//
+//        if (redisService.isConnect()) {
+//            /**
+//             * 获取敏感字词库 并将它们加入到  SensitiveWordFilterUtil.setSensitiveWordMap变量中 用于请求是过滤敏感字
+//             */
+//            Set<String> values = redisService.getObject(Constants.SENSITIVE_WORD_REDIS);
+//            if (values != null && values.size() > 0) {
+//                Map map = new SensitiveWordInitUtil().initKeyWord(values);
+//                SensitiveWordFilterUtil.setSensitiveWordMap(map);
+//            }
+//        }
+//
+//        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
+//        filterRegistrationBean.setFilter(new GlobalFilter());
+//        filterRegistrationBean.setOrder(1);
+//        filterRegistrationBean.setEnabled(true);
+//        filterRegistrationBean.addUrlPatterns("/*");
+//        Map<String, String> initParameters = new HashMap<>();
+//        initParameters.put("excludes", "/favicon.ico,/img/*,/js/*,/css/*");
+//        initParameters.put("isIncludeRichText", "true");
+//        filterRegistrationBean.setInitParameters(initParameters);
+//        return filterRegistrationBean;
+//    }
+}
+

+ 53 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/HibernateConfig.java

@@ -0,0 +1,53 @@
+package com.crm.rely.backend.configuration;
+
+import com.crm.rely.backend.aspect.EntityScanPathResolver;
+import com.crm.rely.backend.aspect.HibernateQueryInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@EnableTransactionManagement
+public class HibernateConfig {
+
+    @Autowired
+    private DataSource dataSource;
+
+    @Autowired
+    private EntityScanPathResolver entityScanPathResolver;
+
+    @Autowired
+    private HibernateQueryInterceptor queryInterceptor;
+
+    @Value("${spring.jpa.hibernate.interceptor.enableDeleteCondition:false}")
+    private boolean enableDeleteCondition;
+
+    @Bean
+    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
+        Map<String, Object> jpaProperties = new HashMap<>();
+        if (!enableDeleteCondition) {
+            jpaProperties.put("hibernate.ejb.interceptor", queryInterceptor);
+        }
+        jpaProperties.put("hibernate.show_sql", true);
+        jpaProperties.put("hibernate.format_sql", true);
+        // 使用自定义的命名策略
+        jpaProperties.put("hibernate.physical_naming_strategy",
+                "com.crm.rely.backend.util.SnakeCasePhysicalNamingStrategy");
+
+        return builder
+                .dataSource(dataSource)
+                .packages(entityScanPathResolver.getEntityScanPaths())
+                .persistenceUnit("default") // 自定义持久化单元
+                .properties(jpaProperties)
+                .build();
+    }
+
+}

+ 103 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/LocaleMessage.java

@@ -0,0 +1,103 @@
+package com.crm.rely.backend.configuration;
+
+import com.crm.rely.backend.core.constant.Constants;
+import com.google.common.base.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.MessageSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Locale;
+
+/**
+ * @program: crm-backend
+ * @description:
+ * @author: houn
+ * @create: 2019-07-25 17:25
+ */
+
+@Component
+public class LocaleMessage {
+
+    @Autowired
+    private MessageSource messageSource;
+
+
+    /**
+     * @param code:对应文本配置的key.
+     * @return 对应地区的语言消息字符串
+     */
+    public String getMessage(String code) {
+        return getMessage(code, new Object[]{});
+    }
+
+    public String getMessage(String code, String defaultMessage) {
+        Locale locale = getLocale();
+        return getMessage(code, null, defaultMessage, locale);
+    }
+
+    public String getMessage(String code, String defaultMessage, Locale locale) {
+        return getMessage(code, null, defaultMessage, locale);
+    }
+
+    public String getMessage(String code, Locale locale) {
+        return getMessage(code, null, "", locale);
+    }
+
+    public String getMessage(String code, Object[] args) {
+        return getMessage(code, args, "");
+    }
+
+    public String getMessage(String code, Object[] args, Locale locale) {
+        return getMessage(code, args, "", locale);
+    }
+
+    public String getMessage(String code, Object[] args, String defaultMessage) {
+        Locale locale = getLocale();
+        return getMessage(code, args, defaultMessage, locale);
+    }
+
+    public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
+        if (locale == null) {
+            locale = getLocale();
+        }
+        return messageSource.getMessage(code, args, defaultMessage, locale);
+    }
+
+    public String getMessageByLang(String code, String lang) {
+        Locale locale = null;
+        if ("en".equals(lang)) {
+            locale = new Locale(lang, "US");
+        } else {
+            locale = new Locale("zh", "CN");
+        }
+
+        return getMessage(code, locale);
+    }
+
+    private Locale getLocale() {
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        HttpServletRequest request = attributes.getRequest();
+        String language = request.getHeader(Constants.Language);
+        Locale locale = null;
+        if (Strings.isNullOrEmpty(language)) {
+            locale = getLocale("en", "US");
+        } else {
+            switch (language) {
+                case "cn":
+                    locale = getLocale("zh", "CN");
+                    break;
+                default:
+                    locale = getLocale("en", "US");
+                    break;
+            }
+        }
+        return locale;
+    }
+
+    private Locale getLocale(String language, String country) {
+        return new Locale(language, country);
+    }
+}

+ 64 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/LogQuartzComponent.java

@@ -0,0 +1,64 @@
+package com.crm.rely.backend.configuration;
+
+import com.alibaba.fastjson.JSON;
+import com.crm.rely.backend.aspect.LogType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * 日志定时发送服务
+ */
+@Component
+public class LogQuartzComponent {
+
+    @Autowired
+    private JmsTemplate jmsTemplate;
+
+    @Autowired
+    private BaseActiveMqConfiguration baseAcitveMqConfiguration;
+
+    /**
+     * 日志内存队列
+     */
+//    public static Queue<Map<String,Map<String, Map<Long, Map<LogType, Object>>>>> logQueue = new ConcurrentLinkedQueue<>();
+    public static Queue<Map<String, Map<Long, Map<LogType, Object>>>> logQueue = new ConcurrentLinkedQueue<>();
+    /**
+     * 向队列中添加日志
+     *
+     * @param log
+     */
+//    public static void add(Map<String, Map<Long, Map<LogType, Object>>> log,String url) {
+//        Map<String,Object> map=  new HashMap<>();
+//        map.put(url,log);
+//        logQueue.add(map);
+//    }
+    public static void add(Map<String, Map<Long, Map<LogType, Object>>> log) {
+
+        logQueue.add(log);
+    }
+
+    /**
+     * 每五秒先activemq发送操作日志
+     *
+     * @throws Exception
+     */
+    @Scheduled(cron = "*/5 * * * * ?")
+    public void activeMqSendByFive() throws Exception {
+        /**
+         * 取队列第一个 并删除 如果队列为空 返回null 用于下列判断
+         */
+//        Map<String,Map<String, Map<Long, Map<LogType, Object>>>> object = logQueue.poll();
+        Map<String, Map<Long, Map<LogType, Object>>> object = logQueue.poll();
+        while (object != null) {
+            jmsTemplate.convertAndSend(baseAcitveMqConfiguration.logQueue(), JSON.toJSONString(object));
+            object = logQueue.poll();
+        }
+    }
+}

+ 35 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/MongoDBAppender.java

@@ -0,0 +1,35 @@
+package com.crm.rely.backend.configuration;//package com.crm.rely.backend.configuration;
+//
+//import ch.qos.logback.classic.spi.ILoggingEvent;
+//import ch.qos.logback.core.UnsynchronizedAppenderBase;
+//import com.mongodb.BasicDBObject;
+//import org.springframework.data.mongodb.core.MongoTemplate;
+//
+//import java.util.Date;
+//
+///**
+// * @author houn
+// */
+//public class MongoDBAppender extends
+//        UnsynchronizedAppenderBase<ILoggingEvent> {
+//
+//    @Override
+//    protected void append(ILoggingEvent eventObject) {
+//
+//        //获取bean 需要通过ApplicationContextProvider去获取
+//        // MongoDBAppender没有注入到bean
+//        // MongoDBAppender在日志使用在启动的时候无法正常获取mongoTemplate
+//        MongoTemplate mongoTemplate = ApplicationContextProvider.getBean(MongoTemplate.class);
+//        if (mongoTemplate != null) {
+//            final BasicDBObject doc = new BasicDBObject();
+//            doc.append("level", eventObject.getLevel().toString());
+//            doc.append("logger", eventObject.getLoggerName());
+//            doc.append("time", new Date());
+//            doc.append("thread", eventObject.getThreadName());
+//            doc.append("message", eventObject.getFormattedMessage());
+//            mongoTemplate.insert(doc, "sensityveWord");
+//        }
+//    }
+//
+//}
+//

+ 61 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/RedisBean.java

@@ -0,0 +1,61 @@
+package com.crm.rely.backend.configuration;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.scripting.support.ResourceScriptSource;
+
+public class RedisBean {
+    @Bean
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+
+        // 使用Jackson2JsonRedisSerialize 替换默认序列化
+        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+
+        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
+
+        // 设置value的序列化规则和 key的序列化规则
+        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+    @Bean
+    public DefaultRedisScript<Long> limitScript() {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limiter.lua")));
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    @Bean
+    public DefaultRedisScript<Long> redisUnlockScript() {
+        DefaultRedisScript<Long> redisUnlockScript = new DefaultRedisScript<>();
+        redisUnlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/unLock.lua")));
+        redisUnlockScript.setResultType(Long.class);
+        return redisUnlockScript;
+    }
+    @Bean
+    public DefaultRedisScript<Long> redisLockScript() {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/lock.lua")));
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+}
+

+ 40 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/TaskConfiguration.java

@@ -0,0 +1,40 @@
+package com.crm.rely.backend.configuration;
+
+import org.apache.tomcat.util.threads.ThreadPoolExecutor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+/**
+ * @author houn
+ */
+@EnableAsync
+public class TaskConfiguration {
+    /**
+     * 线程
+     *
+     * @return
+     */
+    @Bean("taskExecutor")
+    public TaskExecutor taskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        // 设置核心线程数
+        executor.setCorePoolSize(5);
+        // 设置最大线程数
+        executor.setMaxPoolSize(10);
+        // 设置队列容量
+        executor.setQueueCapacity(30);
+        // 设置线程活跃时间(秒)
+        executor.setKeepAliveSeconds(60);
+        // 设置默认线程名称
+        executor.setThreadNamePrefix("");
+        // 设置拒绝策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        // 等待所有任务结束后再关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        return executor;
+    }
+}
+

+ 32 - 0
ucard-backend/src/main/java/com/crm/rely/backend/configuration/TransactionManagerConfig.java

@@ -0,0 +1,32 @@
+package com.crm.rely.backend.configuration;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+/**
+ * @Author houn
+ * @Date 2024/12/24 14:02
+ * @PackageName:com.crm.manager.configuration
+ * @ClassName: TransactionManagerConfig
+ * @Description: TODO
+ */
+@Configuration
+@EnableTransactionManagement
+public class TransactionManagerConfig {
+
+    @Bean(name = "transactionManager")
+    @Primary
+    public PlatformTransactionManager transactionManager(
+            @Qualifier("defaultDataSource") DataSource dataSource,
+            EntityManagerFactory entityManagerFactory) {
+        return new JpaTransactionManager(entityManagerFactory);
+    }
+}

+ 49 - 0
ucard-backend/src/main/java/com/crm/rely/backend/controller/NotFoundExceptionController.java

@@ -0,0 +1,49 @@
+package com.crm.rely.backend.controller;
+
+import com.crm.rely.backend.exception.DataNotFoundException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.web.ErrorController;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 在路由为空404的时候进入当前控制器
+ *
+ * @author houn
+ */
+@Controller
+public class NotFoundExceptionController implements ErrorController {
+
+    @Value("${page.redirect.enable:false}")
+    private boolean enable;
+
+    @Value("${page.redirect.url:/error404.html}")
+    private String url;
+
+    @Override
+    public String getErrorPath() {
+        return "/error";
+    }
+
+    /**
+     * 抛出访问成功但页面不存在(not found)的信息
+     *
+     * @param httpServletResponse
+     * @return
+     * @throws Exception
+     */
+    @GetMapping(value = {"/error"})
+    public Object error(HttpServletResponse httpServletResponse) throws Exception {
+        httpServletResponse.setStatus(200);
+        if (enable) {
+            //todo 可以重定向到特定页面
+            httpServletResponse.sendRedirect(url);
+        } else {
+            throw new DataNotFoundException("not found");
+        }
+        return null;
+    }
+}
+

+ 9 - 0
ucard-backend/src/main/java/com/crm/rely/backend/dao/repository/BaseRepository.java

@@ -0,0 +1,9 @@
+package com.crm.rely.backend.dao.repository;
+
+import com.crm.rely.backend.core.pojo.BaseTable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+public interface BaseRepository<T extends BaseTable> extends JpaRepository<T, Long>, JpaSpecificationExecutor<T> {
+
+}

+ 23 - 0
ucard-backend/src/main/java/com/crm/rely/backend/exception/ControllerException.java

@@ -0,0 +1,23 @@
+package com.crm.rely.backend.exception;
+
+import lombok.Data;
+
+/**
+ * 控制器异常
+ *
+ * @author houn
+ */
+@Data
+public class ControllerException extends Exception {
+    private int code;
+
+    public ControllerException(String msg) {
+        super(msg);
+    }
+
+
+    public ControllerException(int code, String msg) {
+        super(msg);
+        this.code = code;
+    }
+}

+ 25 - 0
ucard-backend/src/main/java/com/crm/rely/backend/exception/DataNotFoundException.java

@@ -0,0 +1,25 @@
+package com.crm.rely.backend.exception;
+
+/**
+ * 数据不存在异常
+ *
+ * @author houn
+ */
+public class DataNotFoundException extends ServiceException {
+
+    public DataNotFoundException(int code, String msg) {
+        super(code, msg);
+    }
+
+    public DataNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public DataNotFoundException(String msg, Object... args) {
+        super(msg, args);
+    }
+
+    public DataNotFoundException(int code, String msg, Object... args) {
+        super(code, msg, args);
+    }
+}

+ 26 - 0
ucard-backend/src/main/java/com/crm/rely/backend/exception/LoginException.java

@@ -0,0 +1,26 @@
+package com.crm.rely.backend.exception;
+
+/**
+ * 登录错误
+ *
+ * @author houn
+ */
+public class LoginException extends ServiceException {
+
+
+    public LoginException(String msg) {
+        super(msg);
+    }
+
+    public LoginException(int code, String msg) {
+        super(code, msg);
+    }
+
+    public LoginException(String msg, Object... args) {
+        super(msg, args);
+    }
+
+    public LoginException(int code, String msg, Object... args) {
+        super(code, msg, args);
+    }
+}

+ 25 - 0
ucard-backend/src/main/java/com/crm/rely/backend/exception/ParameterException.java

@@ -0,0 +1,25 @@
+package com.crm.rely.backend.exception;
+
+/**
+ * 参数异常
+ *
+ * @author houn
+ */
+public class ParameterException extends ServiceException {
+
+    public ParameterException(String msg) {
+        super(msg);
+    }
+
+    public ParameterException(String msg, Object... args) {
+        super(msg, args);
+    }
+
+    public ParameterException(int coed, String msg) {
+        super(coed, msg);
+    }
+
+    public ParameterException(int coed, String msg, Object... args) {
+        super(coed, msg, args);
+    }
+}

+ 33 - 0
ucard-backend/src/main/java/com/crm/rely/backend/exception/PayValidatedException.java

@@ -0,0 +1,33 @@
+package com.crm.rely.backend.exception;
+
+import lombok.Data;
+
+@Data
+public class PayValidatedException extends Exception {
+    private int code;
+
+    public PayValidatedException(String msg) {
+        super(msg);
+    }
+
+    public PayValidatedException(int code, String msg) {
+        super(msg);
+        this.code = code;
+    }
+
+    public static PayValidatedException exception(String msg) {
+        return new PayValidatedException(msg);
+    }
+
+    public static PayValidatedException exception(int code, String msg) {
+        return new PayValidatedException(code, msg);
+    }
+
+    public static ServiceException exception(String msg, Object... args) {
+        return new ServiceException(msg, args);
+    }
+
+    public static ServiceException exception(int code, String msg, Object... args) {
+        return new ServiceException(code, msg, args);
+    }
+}

+ 59 - 0
ucard-backend/src/main/java/com/crm/rely/backend/exception/ServiceException.java

@@ -0,0 +1,59 @@
+package com.crm.rely.backend.exception;
+
+import com.crm.rely.backend.core.constant.Constants;
+import lombok.Data;
+
+/**
+ * @author houn
+ */
+@Data
+
+public class ServiceException extends Exception {
+
+    private int code;
+
+    public ServiceException(int code, String msg) {
+        super(msg);
+        this.code = code;
+    }
+
+    public ServiceException(String msg) {
+        super(msg);
+        code = Constants.FAILED_CODE;
+    }
+
+    public ServiceException(String msg, Object... args) {
+
+        super(String.format(msg, args));
+        code = Constants.FAILED_CODE;
+    }
+
+    public ServiceException(int code, String msg, Object... args) {
+
+        this(code, String.format(msg, args));
+    }
+
+    public static ServiceException exception() {
+        return new ServiceException(Constants.SYSTEM_ERROR);
+    }
+
+    public static ServiceException notFoundException() {
+        return new ServiceException(Constants.INFO_NOT_FOUND);
+    }
+
+    public static ServiceException restrictRequest() {
+        return new ServiceException("restrict_request");
+    }
+
+    public static ServiceException exception(String msg) {
+        return new ServiceException(msg);
+    }
+
+    public static ServiceException exception(int code, String msg, Object... args) {
+        return new ServiceException(code, msg, args);
+    }
+
+    public static ServiceException exception(String msg, Object... args) {
+        return new ServiceException(msg, args);
+    }
+}

+ 26 - 0
ucard-backend/src/main/java/com/crm/rely/backend/exception/ValidateException.java

@@ -0,0 +1,26 @@
+package com.crm.rely.backend.exception;
+
+/**
+ * 验证
+ *
+ * @author houn
+ */
+public class ValidateException extends ServiceException {
+
+
+    public ValidateException(int code, String msg) {
+        super(code, msg);
+    }
+
+    public ValidateException(String msg) {
+        super(msg);
+    }
+
+    public ValidateException(int code, String msg, Object... args) {
+        super(code, msg, args);
+    }
+
+    public ValidateException(String msg, Object... args) {
+        super(msg, args);
+    }
+}

+ 89 - 0
ucard-backend/src/main/java/com/crm/rely/backend/filter/GlobalFilter.java

@@ -0,0 +1,89 @@
+package com.crm.rely.backend.filter;
+
+import com.crm.rely.backend.util.GlobalHttpServletRequestWrapper;
+import com.crm.rely.backend.util.ValidateUtil;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+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;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 拦截防止xss注入
+ */
+public class GlobalFilter implements Filter {
+    private static final Logger logger = LogManager.getLogger(GlobalFilter.class);
+
+    /**
+     * 是否过滤富文本内容
+     */
+    private static boolean IS_INCLUDE_RICH_TEXT = false;
+
+    public List<String> excludes = new ArrayList<>();
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,ServletException {
+        if(logger.isDebugEnabled()){
+            logger.debug("xss filter is open");
+        }
+        HttpServletRequest req = (HttpServletRequest) request;
+        HttpServletResponse resp = (HttpServletResponse) response;
+        if(handleExcludeURL(req, resp)||req.getMethod().equals(RequestMethod.OPTIONS.name())){
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        GlobalHttpServletRequestWrapper xssRequest = new GlobalHttpServletRequestWrapper((HttpServletRequest) request,IS_INCLUDE_RICH_TEXT);
+        filterChain.doFilter(xssRequest, response);
+    }
+
+    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
+
+        if (excludes == null || excludes.isEmpty()) {
+            return false;
+        }
+
+        String url = request.getServletPath();
+        for (String pattern : excludes) {
+            Pattern p = Pattern.compile("^" + pattern);
+            Matcher m = p.matcher(url);
+            if (m.find()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        if(logger.isDebugEnabled()){
+            logger.debug("xss filter init ====================");
+        }
+        String isIncludeRichText = filterConfig.getInitParameter("isIncludeRichText");
+        if(ValidateUtil.vStringNull(isIncludeRichText)){
+            IS_INCLUDE_RICH_TEXT = Boolean.parseBoolean(isIncludeRichText);
+        }
+
+        String temp = filterConfig.getInitParameter("excludes");
+        if (temp != null) {
+            String[] url = temp.split(",");
+            for (int i = 0; url != null && i < url.length; i++) {
+                excludes.add(url[i]);
+            }
+        }
+    }
+
+    @Override
+    public void destroy() {}
+
+}
+
+

+ 47 - 0
ucard-backend/src/main/java/com/crm/rely/backend/interceptor/LocaleInterceptor.java

@@ -0,0 +1,47 @@
+package com.crm.rely.backend.interceptor;
+
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.support.RequestContextUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Locale;
+
+/**
+ * Created by max on 2019/3/7.
+ */
+public class LocaleInterceptor implements HandlerInterceptor {
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
+
+        setLocale(request, response);
+
+        return true;
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
+
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {
+
+        setLocale(request, response);
+    }
+
+    private void setLocale(HttpServletRequest request, HttpServletResponse response) {
+        String lang = request.getParameter("lang");
+        LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
+        if (lang == null) {
+            lang = "zh";
+        }
+        if ("en".equals(lang)) {
+            localeResolver.setLocale(request, response, new Locale("en", "US"));
+        } else {
+            localeResolver.setLocale(request, response, new Locale("zh", "CN"));
+        }
+    }
+}

+ 90 - 0
ucard-backend/src/main/java/com/crm/rely/backend/interceptor/NoRepeatSubmitInterceptor.java

@@ -0,0 +1,90 @@
+package com.crm.rely.backend.interceptor;
+
+import com.crm.rely.backend.aspect.ControllerAop;
+import com.crm.rely.backend.configuration.LocaleMessage;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.constant.PrefixEnum;
+import com.crm.rely.backend.core.constant.RoleConstants;
+import com.crm.rely.backend.core.dto.base.BaseResultDto;
+import com.crm.rely.backend.core.entity.custom.info.InfoEntity;
+import com.crm.rely.backend.exception.LoginException;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.RedisService;
+import com.crm.rely.backend.util.GetIpAndMac;
+import com.crm.rely.backend.util.HttpServletRequestUtil;
+import com.crm.rely.backend.util.InteceptorResultUtil;
+import com.google.common.base.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.support.RequestContextUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+public class NoRepeatSubmitInterceptor implements HandlerInterceptor {
+    @Autowired
+    private RedisService redisService;
+
+    @Autowired
+    private LocaleMessage localeMessage;
+
+    private static final int EXPIRE_TIME_NOT_TOKEN = 500;
+
+    private static final int EXPIRE_TIME = 1 * 1000;
+
+    public NoRepeatSubmitInterceptor() {
+
+    }
+
+    public NoRepeatSubmitInterceptor(RedisService redisService,LocaleMessage localeMessage) {
+        this.redisService = redisService;
+        this.localeMessage = localeMessage;
+    }
+
+    @Override
+    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
+        //如果为OPTIONS直接返回成功
+        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
+            return true;
+        }
+        String uri = httpServletRequest.getRequestURI();
+        String token = httpServletRequest.getHeader(Constants.ACCESS_TOKEN);
+        String ip = GetIpAndMac.getIp(httpServletRequest);
+        String clientIp = GetIpAndMac.getClientIp(httpServletRequest);
+        if (Strings.isNullOrEmpty(token) && Strings.isNullOrEmpty(clientIp)) {
+            return true;
+        }
+        String key = token + ip + clientIp + uri;
+        String uuid = UUID.randomUUID().toString();
+        int expireTime;
+        if (Strings.isNullOrEmpty(token)) {
+            expireTime = EXPIRE_TIME_NOT_TOKEN;
+        } else {
+            expireTime = EXPIRE_TIME;
+        }
+        if (!redisService.tryLock(key, uuid, expireTime)) {
+            InteceptorResultUtil.returnResult(httpServletResponse, BaseResultDto.result(Constants.FAILED_CODE,
+                    localeMessage.getMessage("no_repeat_submit_error","no_repeat_submit_error")));
+            httpServletResponse.setStatus(Constants.FAILED_CODE);
+            return false;
+        }
+        return true;
+
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
+
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
+
+    }
+}

+ 9 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/BlackListLoginService.java

@@ -0,0 +1,9 @@
+package com.crm.rely.backend.service;
+
+import com.crm.rely.backend.exception.ServiceException;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+public interface BlackListLoginService  extends BlackListService{
+
+}

+ 44 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/BlackListService.java

@@ -0,0 +1,44 @@
+package com.crm.rely.backend.service;
+
+import com.crm.rely.backend.exception.ServiceException;
+
+import java.util.Date;
+
+public interface BlackListService {
+
+    void validate(String content, String type) throws ServiceException;
+
+    void validate(String content, String type, String ip) throws ServiceException;
+
+    boolean checkSeal(String content) throws ServiceException;
+
+    /**
+     * 根据黑名单验证
+     *
+     * @param content 黑名单内容
+     * @param now     当前发送时间
+     * @throws ServiceException
+     */
+    void checkFromList(String content, Long now) throws ServiceException;
+
+    /**
+     * 移除黑名单
+     *
+     * @param content 黑名单内容
+     * @throws ServiceException
+     */
+    void removeFromList(String content) throws ServiceException;
+
+    void removeFromList(String content, String type) throws ServiceException;
+
+    /**
+     * 添加至黑名单
+     *
+     * @param content 黑名单内容
+     * @param ip      ip
+     * @param nowDate
+     * @throws ServiceException
+     */
+    void addToList(String content, String ip, Date nowDate) throws ServiceException;
+
+}

+ 76 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/EmailService.java

@@ -0,0 +1,76 @@
+package com.crm.rely.backend.service;
+
+import com.crm.rely.backend.core.entity.system.email.SupplementSendEmailBatchEntity;
+import com.crm.rely.backend.core.entity.system.email.SupplementSendEmailEntity;
+import com.crm.rely.backend.core.entity.system.email.SysEmailSendEntity;
+import com.crm.rely.backend.exception.ServiceException;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public interface EmailService extends BlackListService {
+
+    /**
+     * 发送邮件 默认不验证
+     *
+     * @param sysEmailSendEntity 发送短信实体
+     * @throws ServiceException
+     */
+    void sendEmail(SysEmailSendEntity sysEmailSendEntity) throws ServiceException;
+
+    void sendEmail(SysEmailSendEntity sysEmailSendEntity,String queueName) throws ServiceException;
+
+    void sendEmail(List<SysEmailSendEntity> sysEmailSendEntities) throws ServiceException;
+
+    void sendEmail(SysEmailSendEntity sysEmailSendEntity, boolean validate, String queueName) throws ServiceException;
+
+    void sendEmail(List<SysEmailSendEntity> sysEmailSendEntities, boolean validate, String queueName) throws ServiceException;
+
+    /**
+     * 发送邮件
+     *
+     * @param sysEmailSendEntity 发送短信实体
+     * @param validate           是否验证盗刷
+     * @throws ServiceException
+     */
+    void sendEmail(SysEmailSendEntity sysEmailSendEntity, boolean validate) throws ServiceException;
+
+    void sendEmail(List<SysEmailSendEntity> sysEmailSendEntities, boolean validate) throws ServiceException;
+
+    void sendEmailSupplement(Long id) throws ServiceException;
+
+    void sendEmailSupplement(List<Long> ids) throws ServiceException;
+
+    void sendEmailSupplement(SupplementSendEmailEntity entity) throws ServiceException;
+
+    void sendEmailSupplement(SupplementSendEmailBatchEntity entity) throws ServiceException;
+
+    /**
+     * 发送邮件验证码并保存到redis 过期时间为五分钟
+     *
+     * @param sysEmailSendEntity 发送短信实体
+     * @param code               验证码
+     * @throws ServiceException
+     */
+    void sendCode(SysEmailSendEntity sysEmailSendEntity, String code) throws ServiceException;
+
+    /**
+     * 发送邮件验证码并保存到redis 过期时间为time 毫秒
+     *
+     * @param sysEmailSendEntity 发送邮件实体
+     * @param code               验证码
+     * @param time               过期时间
+     * @throws ServiceException
+     */
+    void sendCode(SysEmailSendEntity sysEmailSendEntity, String code, long time) throws ServiceException;
+
+    /**
+     * 验证验证码是否存在 是否过期
+     *
+     * @param email 邮箱
+     * @param code  邮件验证码
+     * @throws ServiceException
+     */
+    void validateCode(String email, String code) throws ServiceException;
+}

+ 51 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/MqSendService.java

@@ -0,0 +1,51 @@
+package com.crm.rely.backend.service;
+
+import javax.jms.Queue;
+
+/**
+ * @program: user-custom
+ * @description: mq统一发送
+ * @author: houn
+ * @create: 2019-05-22 09:41
+ */
+public interface MqSendService {
+
+    /**
+     * 发送消息(毫秒)
+     *
+     * @param queue 队列
+     * @param msg   消息内容
+     */
+    void send(Queue queue, String msg);
+    void send(Queue queue, Object msg);
+    /**
+     * 发送消息(毫秒)
+     *
+     * @param queueName 队列url(名称)
+     * @param msg       消息内容
+     */
+    void send(String queueName, String msg);
+    void send(String queueName, Object msg);
+    /**
+     * 发送延迟消息(毫秒)
+     *
+     * @param queue 队列
+     * @param msg   消息内容
+     * @param delay 延迟时间
+     */
+    void send(Queue queue, String msg, long delay);
+
+    void send(Queue queue, Object msg, long delay);
+
+    /**
+     * 发送延迟消息(毫秒)
+     *
+     * @param queueName 队列url(名称)
+     * @param msg       消息内容
+     * @param delay     延迟时间
+     */
+    void send(String queueName, String msg, long delay);
+
+    void send(String queueName, Object msg, long delay);
+
+}

+ 206 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/RedisService.java

@@ -0,0 +1,206 @@
+package com.crm.rely.backend.service;
+
+import com.crm.rely.backend.core.entity.login.InfoEntity;
+import com.crm.rely.backend.exception.ServiceException;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author houn
+ */
+public interface RedisService {
+    /**
+     * 无过期时间 保存转化为json 获取使用getEntity
+     *
+     * @param key
+     * @param t
+     * @param <T>
+     * @return 是否移除成功
+     * @throws ServiceException
+     */
+    <T> boolean save(String key, T t) throws ServiceException;
+
+    /**
+     * 保存转化为json 获取使用getEntity
+     *
+     * @param key
+     * @param t
+     * @param time
+     * @param <T>
+     * @return 是否移除成功
+     * @throws ServiceException
+     */
+    <T> boolean save(String key, T t, long time) throws ServiceException;
+
+    /**
+     * 保存对象 使用getObject获取
+     *
+     * @param key
+     * @param t
+     * @param <T>
+     * @return 是否移除成功
+     * @throws ServiceException
+     */
+    <T> boolean saveObject(String key, T t) throws ServiceException;
+
+    /**
+     * 保存对象 使用getObject获取
+     *
+     * @param key
+     * @param t
+     * @param time
+     * @param <T>
+     * @return 是否移除成功
+     * @throws ServiceException
+     */
+    <T> boolean saveObject(String key, T t, long time) throws ServiceException;
+
+    /**
+     * 获取对象 自动根据类型转化
+     *
+     * @param key
+     * @param <T>
+     * @return
+     * @throws ServiceException
+     */
+    <T> T getObject(String key) throws ServiceException;
+
+    /**
+     * 判断key是否存在
+     *
+     * @param key
+     * @return 标识是否存在
+     * @throws ServiceException
+     */
+    boolean hasKey(String key) throws ServiceException;
+
+    /**
+     * 获取对戏 根据clazz转化对象类型
+     *
+     * @param key   key
+     * @param clazz 对象的clazz
+     * @param <T>
+     * @return 返回得到的对象
+     * @throws ServiceException
+     */
+    <T> T getEntity(String key, Class<T> clazz) throws ServiceException;
+
+    /**
+     * 获取对戏列表 如果春进去的是一个数组或者list使用这个获取
+     *
+     * @param key
+     * @param clazz
+     * @param <T>
+     * @return
+     * @throws ServiceException
+     */
+    <T> List<T> getList(String key, Class<T> clazz) throws ServiceException;
+
+    /**
+     * 移除key对应的键值对
+     *
+     * @param key
+     * @return 是否移除成功
+     * @throws ServiceException
+     */
+    boolean remove(String key) throws ServiceException;
+
+    boolean remove(String... keys) throws ServiceException;
+
+    boolean remove(Collection<String> keys) throws ServiceException;
+
+    /**
+     * 获取字符串 使用saveObject存储字符串时可以直接用此方法获取 如果使用save 那在获取的字符串是json格式 之后通过json在转化一下才能使用
+     *
+     * @param key
+     * @return
+     * @throws ServiceException
+     */
+    String getString(String key) throws ServiceException;
+
+    /**
+     * 标识redis是否成功
+     *
+     * @return true 成功 false 失败
+     * @throws ServiceException
+     */
+    boolean isConnect() throws ServiceException;
+
+    /**
+     * 非阻塞锁
+     * 默认设置加锁时间1000*10ms
+     * 阻塞 如果获取锁失败啧休眠100ms 再次获取 非阻塞 获取锁失败则返回失败
+     *
+     * @param key
+     * @param request
+     * @return
+     */
+    boolean tryLock(String key, String request);
+
+    /**
+     * 阻塞锁
+     * 默认设置加锁时间1000*10ms
+     * 不设置阻塞时间 一直尝试获取 尽量设置阻塞时间
+     * 阻塞 如果获取锁失败啧休眠100ms 再次获取 非阻塞 获取锁失败则返回失败
+     *
+     * @param key
+     * @param request
+     * @return
+     * @throws InterruptedException
+     */
+    boolean lock(String key, String request) throws InterruptedException;
+
+    /**
+     * 锁阻塞锁
+     * 默认设置加锁时间1000*10ms
+     * 设置阻塞时间 如果达到阻塞时间还未获取锁 标识失败
+     * 阻塞 如果获取锁失败啧休眠100ms 再次获取 非阻塞 获取锁失败则返回失败
+     *
+     * @param key
+     * @param request
+     * @param blockTime
+     * @return
+     * @throws InterruptedException
+     */
+    boolean lock(String key, String request, int blockTime) throws InterruptedException;
+
+    /**
+     * 非阻塞锁
+     * 默认设置加锁时间1000*10ms
+     * 阻塞 如果获取锁失败啧休眠100ms 再次获取 非阻塞 获取锁失败则返回失败
+     *
+     * @param key
+     * @param request
+     * @param expireTime 加锁时间
+     * @return
+     */
+    boolean tryLock(String key, String request, int expireTime);
+
+    boolean tryLock(String key, Runnable statement);
+
+    void tryLock(String key, Runnable statement, String exceptionMsg) throws ServiceException;
+
+    void tryLock(InfoEntity infoEntity, HttpServletRequest httpServletRequest, Runnable statement, String exceptionMsg) throws ServiceException;
+
+    /**
+     * 解锁 为了实现原子性 需要利用lua去操作
+     * lua脚本放在lua/lock.lua中 名字已默认 代码中相对应 不要修改
+     *
+     * @param key
+     * @param request
+     * @return
+     */
+    boolean unlock(String key, String request);
+
+    /**
+     * 保存list
+     *
+     * @param key
+     * @param list
+     * @param <T>
+     * @throws Exception
+     */
+    <T> void saveList(String key, List<T> list) throws Exception;
+}

+ 47 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/SmsService.java

@@ -0,0 +1,47 @@
+package com.crm.rely.backend.service;
+
+import com.crm.rely.backend.core.constant.SmsSendEnum;
+import com.crm.rely.backend.core.entity.system.sms.SysSmsEntity;
+import com.crm.rely.backend.exception.ServiceException;
+
+public interface SmsService extends BlackListService {
+
+    /**
+     * 发送短信
+     *
+     * @param sysSmsEntity 发送短信实体
+     * @throws ServiceException
+     */
+    void sendSms(SysSmsEntity sysSmsEntity) throws ServiceException;
+
+    void sendSms(SysSmsEntity sysSmsEntity, boolean validate) throws ServiceException;
+    /**
+     * 发送短信验证码并保存到redis 过期时间为五分钟
+     *
+     * @param sysSmsEntity 发送短信实体
+     * @param code         验证码
+     * @throws ServiceException
+     */
+    void sendCode(SysSmsEntity sysSmsEntity, String code) throws ServiceException;
+
+    void sendCode(SysSmsEntity sysSmsEntity, String code, boolean validate) throws ServiceException;
+    /**
+     * 发送短信验证码并保存到redis 过期时间为time 毫秒
+     *
+     * @param sysSmsEntity 发送短信实体
+     * @param code         验证码
+     * @param time         过期时间
+     * @throws ServiceException
+     */
+    void sendCode(SysSmsEntity sysSmsEntity, String code, long time) throws ServiceException;
+
+    void sendCode(SysSmsEntity sysSmsEntity, String code, boolean validate, long time) throws ServiceException;
+    /**
+     * 验证验证码是否存在 是否过期
+     *
+     * @param phone 手机号码
+     * @param code  手机验证码
+     * @throws ServiceException
+     */
+    void validateCode(String phone, String code) throws ServiceException;
+}

+ 18 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/SysCountryService.java

@@ -0,0 +1,18 @@
+package com.crm.rely.backend.service;
+
+import com.crm.rely.backend.exception.ServiceException;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 国家
+ */
+public interface SysCountryService {
+
+    String getRequestByIp(HttpServletRequest request) throws ServiceException;
+
+    String getAdderByIp(String ip) throws ServiceException;
+    String getCountryByIp(HttpServletRequest request) throws ServiceException;
+
+    String getCountryByIp(String ip) throws ServiceException;
+}

+ 20 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/BlackListLoginServiceImpl.java

@@ -0,0 +1,20 @@
+package com.crm.rely.backend.service.impl;
+
+import com.crm.rely.backend.service.BlackListLoginService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class BlackListLoginServiceImpl extends BlackListServiceImpl implements BlackListLoginService {
+
+
+    public BlackListLoginServiceImpl() {
+        super("LOGIN_BLACKLIST_",
+                "LOGIN_BLACKLIST_BACK_",
+                60 * 1000L,
+                3 * 60 * 1000L,
+                5,
+                3,
+                3);
+    }
+
+}

+ 247 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/BlackListServiceImpl.java

@@ -0,0 +1,247 @@
+package com.crm.rely.backend.service.impl;
+
+import com.crm.rely.backend.configuration.LocaleMessage;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.entity.system.sms.BlackListEntity;
+import com.crm.rely.backend.core.entity.system.sms.SmsValidatorEntity;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.BlackListService;
+import com.crm.rely.backend.service.RedisService;
+import com.crm.rely.backend.util.DateUtil;
+import com.google.common.base.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.Date;
+
+public class BlackListServiceImpl implements BlackListService {
+
+    @Autowired
+    protected RedisService redisService;
+    /**
+     * 黑名单redis保存时key前缀
+     */
+    protected String KEY_PREFIX;
+    protected String KEY_PREFIX_BLACK;
+    @Autowired
+    private LocaleMessage localeMessage;
+    /**
+     * 每次发送时间间隔
+     */
+    private Long SINGLE_SEND_RANGE = 30 * 1000L;
+    /**
+     * 多次发送时间间隔
+     */
+    private Long MULTI_SEND_RANGE = 3 * 60 * 1000L;
+    /**
+     * 多次发送次数
+     */
+    private Integer MULTI_SEND_TIMES = 3;
+    /**
+     * 黑名单重试次数
+     */
+    private Integer BLACK_LIST_RETRY_TIMES = 3;
+    /**
+     * 黑名单持续时间(小时)
+     */
+    private Integer FORBID_HOURS = 1;
+
+
+    public BlackListServiceImpl() {
+
+        KEY_PREFIX = "BLACKLIST";
+    }
+
+    /**
+     * @param key                 黑名单redis保存时key前缀
+     * @param keyBack             黑名单redis保存时key前缀
+     * @param singleSendRange     每次发送时间间隔
+     * @param multiSendRange      多次发送时间间隔
+     * @param multiSendTimes      多次发送次数
+     * @param blackListRetryTimes 黑名单重试次数
+     * @param ForbidHours         黑名单持续时间(小时)
+     */
+    public BlackListServiceImpl(String key, String keyBack, Long singleSendRange, Long multiSendRange,
+                                Integer multiSendTimes, Integer blackListRetryTimes, Integer ForbidHours) {
+
+        KEY_PREFIX = key;
+        KEY_PREFIX_BLACK = keyBack;
+        if (singleSendRange != null) {
+            SINGLE_SEND_RANGE = singleSendRange;
+        }
+        if (multiSendRange != null) {
+            MULTI_SEND_RANGE = multiSendRange;
+        }
+        if (multiSendTimes != null) {
+            MULTI_SEND_TIMES = multiSendTimes;
+        }
+        if (blackListRetryTimes != null) {
+            BLACK_LIST_RETRY_TIMES = blackListRetryTimes;
+        }
+        if (ForbidHours != null) {
+            FORBID_HOURS = ForbidHours;
+        }
+    }
+
+    @Override
+    public void validate(String content, String type) throws ServiceException {
+
+        validate(content, type, "127.0.0.1");
+    }
+
+    /**
+     * 是否需要加入黑名单
+     *
+     * @param content 邮箱
+     * @throws ServiceException
+     */
+    @Override
+    public void validate(String content, String type, String ip) throws ServiceException {
+
+        String key = KEY_PREFIX + content;
+
+        if (Strings.isNullOrEmpty(type)) {
+            type = KEY_PREFIX_BLACK;
+        }
+        String typeKey = type + key;
+        /*
+            过滤黑名单
+         */
+        checkFromList(content, System.currentTimeMillis());
+
+        SmsValidatorEntity smsValidatorEntity = redisService.getObject(typeKey);
+
+        /*
+            根据类型获取记录信息
+         */
+        if (smsValidatorEntity == null) {
+            smsValidatorEntity = new SmsValidatorEntity();
+            smsValidatorEntity.setCount(1);
+            smsValidatorEntity.setTime(System.currentTimeMillis());
+            smsValidatorEntity.setTries(0);
+            redisService.saveObject(typeKey, smsValidatorEntity, MULTI_SEND_RANGE);
+            removeFromList(content);
+            return;
+        }
+
+        //添加重试次数
+        smsValidatorEntity.setTries(smsValidatorEntity.getTries() + 1);
+        redisService.saveObject(typeKey, smsValidatorEntity, MULTI_SEND_RANGE);
+        /*
+            判断时间,1分钟之内不可发送多次
+         */
+        Long timeRange = System.currentTimeMillis() - smsValidatorEntity.getTime();
+        if (timeRange < SINGLE_SEND_RANGE) {
+
+            /*
+                如果重试次数超过三次,则视为盗刷短信行为,入黑名单1天
+             */
+            if (smsValidatorEntity.getTries() >= BLACK_LIST_RETRY_TIMES) {
+                //加入黑名单
+                addToList(content, ip, new Date());
+            }
+//            throw new ServiceException(String.format(localeMessage.getMessage("please_try_again_in_seconds"),
+//                    (SINGLE_SEND_RANGE - timeRange) / 1000));
+        }
+
+        /*
+            判断次数
+         */
+        Integer count = smsValidatorEntity.getCount();
+        if (timeRange < MULTI_SEND_RANGE) {
+            if (count >= MULTI_SEND_TIMES) {
+                Long minutes = (MULTI_SEND_RANGE / 60) / 1000;
+                addToList(content, ip, new Date());
+                throw new ServiceException(localeMessage.getMessage("frequent_requests"));
+            } else {
+                smsValidatorEntity.setCount(smsValidatorEntity.getCount() + 1);
+                smsValidatorEntity.setTries(0);
+                redisService.saveObject(typeKey, smsValidatorEntity, MULTI_SEND_RANGE);
+                //有成功发送则移除黑名单
+                removeFromList(content);
+            }
+        } else {
+            smsValidatorEntity.setCount(1);
+            smsValidatorEntity.setTime(System.currentTimeMillis());
+            smsValidatorEntity.setTries(0);
+            redisService.saveObject(typeKey, smsValidatorEntity, MULTI_SEND_RANGE);
+            //有成功发送则移除黑名单
+            removeFromList(content);
+        }
+
+    }
+
+    @Override
+    public boolean checkSeal(String content) throws ServiceException {
+        String key = KEY_PREFIX_BLACK + content;
+        BlackListEntity blackListEntity = redisService.getEntity(key, BlackListEntity.class);
+        if (blackListEntity != null && blackListEntity.getEndTime().getTime() > System.currentTimeMillis()) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 根据黑名单验证
+     *
+     * @param content 黑名单内容
+     * @param now     当前发送时间
+     * @throws ServiceException
+     */
+    @Override
+    public void checkFromList(String content, Long now) throws ServiceException {
+
+        if (checkSeal(content)) {
+            throw new ServiceException(Constants.RESTRICT_REQUEST);
+        } else {
+            removeFromList(content);
+        }
+    }
+
+    /**
+     * 移除黑名单
+     *
+     * @param content 黑名单内容
+     * @throws ServiceException
+     */
+    @Override
+    public void removeFromList(String content) throws ServiceException {
+        removeFromList(content, null);
+    }
+
+    @Override
+    public void removeFromList(String content, String type) throws ServiceException {
+        String key = KEY_PREFIX_BLACK + content;
+        redisService.remove(key);
+
+        if (Strings.isNullOrEmpty(type)) {
+            type = KEY_PREFIX_BLACK;
+        }
+        String typeKey = type + KEY_PREFIX + content;
+
+        redisService.remove(typeKey);
+    }
+
+    /**
+     * 添加至黑名单
+     *
+     * @param content 黑名单内容
+     * @param ip      ip
+     * @param nowDate
+     * @throws ServiceException
+     */
+    @Override
+    public void addToList(String content, String ip, Date nowDate) throws ServiceException {
+        BlackListEntity blackListEntity = new BlackListEntity();
+        blackListEntity.setIp(ip);
+        blackListEntity.setContent(content);
+        blackListEntity.setValid(1);
+        blackListEntity.setStartTime(nowDate);
+        Date endTime = DateUtil.operationHour(blackListEntity.getStartTime(), FORBID_HOURS);
+        blackListEntity.setEndTime(endTime);
+
+        String key = KEY_PREFIX_BLACK + content;
+        //添加到redis中 设置过期时间为FORBID_HOURS * 60 * 60 * 1000L
+        redisService.save(key, blackListEntity, FORBID_HOURS * 60 * 60 * 1000L);
+    }
+}

+ 316 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/EmailServiceImpl.java

@@ -0,0 +1,316 @@
+package com.crm.rely.backend.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.entity.custom.info.InfoEntity;
+import com.crm.rely.backend.core.entity.system.email.SupplementSendEmailBatchEntity;
+import com.crm.rely.backend.core.entity.system.email.SupplementSendEmailEntity;
+import com.crm.rely.backend.core.entity.system.email.SysEmailSendEntity;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.EmailService;
+import com.crm.rely.backend.service.MqSendService;
+import com.crm.rely.backend.service.RedisService;
+import com.crm.rely.backend.service.SysCountryService;
+import com.crm.rely.backend.util.GetIpAndMac;
+import com.crm.rely.backend.util.HttpServletRequestUtil;
+import com.google.common.base.Strings;
+import lombok.extern.log4j.Log4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+import java.util.List;
+
+@Log4j
+@Service
+public class EmailServiceImpl extends BlackListServiceImpl implements EmailService {
+
+
+    private final static String KEY_PREFIX_CODE = "EMAIL_VALIDATOR_CODE";
+
+    @Autowired
+    private MqSendService mqSendService;
+
+    @Autowired
+    private RedisService redisService;
+
+    @Autowired
+    private SysCountryService sysCountryService;
+
+    public EmailServiceImpl() {
+        super("EMAIL_BLACKLIST_", "EMAIL_BLACKLIST_BACK_", 60 * 1000L, 3 * 60 * 1000L, 3, 3, 1);
+    }
+
+    /**
+     * 发送邮件
+     *
+     * @param sysEmailSendEntity 发送短信实体
+     * @throws ServiceException
+     */
+    @Override
+    public void sendEmail(SysEmailSendEntity sysEmailSendEntity) throws ServiceException {
+
+        sendEmail(sysEmailSendEntity, false, Constants.ACTIVEMQ_JMS_URL_EMAIL);
+    }
+
+    @Override
+    public void sendEmail(SysEmailSendEntity sysEmailSendEntity, String queueName) throws ServiceException {
+        sendEmail(sysEmailSendEntity, false, queueName);
+    }
+
+    @Override
+    public void sendEmail(List<SysEmailSendEntity> sysEmailSendEntities) throws ServiceException {
+
+        sendEmail(sysEmailSendEntities, false);
+    }
+
+    @Override
+    public void sendEmail(SysEmailSendEntity sysEmailSendEntity, boolean validate) throws ServiceException {
+
+        sendEmail(sysEmailSendEntity, validate, Constants.ACTIVEMQ_JMS_URL_EMAIL);
+    }
+
+    @Override
+    public void sendEmail(SysEmailSendEntity sysEmailSendEntity, boolean validate, String queueName) throws ServiceException {
+        if (validate) {
+            validate(sysEmailSendEntity.getUsers(), sysEmailSendEntity.getEmailSendEnum().getCode(),
+                    sysEmailSendEntity.getAddIp());
+        } else {
+//            removeFromList(sysEmailSendEntity.getUsers(), sysEmailSendEntity.getEmailSendEnum().getCode());
+        }
+
+        send(sysEmailSendEntity, queueName);
+    }
+
+    @Override
+    public void sendEmail(List<SysEmailSendEntity> sysEmailSendEntities, boolean validate) throws ServiceException {
+        sendEmail(sysEmailSendEntities, validate, Constants.ACTIVEMQ_JMS_URL_EMAIL);
+    }
+
+    @Override
+    public void sendEmail(List<SysEmailSendEntity> sysEmailSendEntities, boolean validate, String queueName) throws ServiceException {
+
+        if (validate) {
+            for (SysEmailSendEntity sysEmailSendEntity : sysEmailSendEntities) {
+                validate(sysEmailSendEntity.getUsers(), sysEmailSendEntity.getEmailSendEnum().getCode(),
+                        sysEmailSendEntity.getAddIp());
+            }
+        }
+        send(sysEmailSendEntities, queueName);
+    }
+
+    @Override
+    public void sendEmailSupplement(Long id) throws ServiceException {
+
+        SupplementSendEmailEntity entity = new SupplementSendEmailEntity();
+        entity.setId(id);
+
+        sendEmailSupplement(entity);
+    }
+
+    @Override
+    public void sendEmailSupplement(List<Long> ids) throws ServiceException {
+
+        for (Long id : ids) {
+            sendEmailSupplement(id);
+        }
+    }
+
+    @Override
+    public void sendEmailSupplement(SupplementSendEmailBatchEntity entity) throws ServiceException {
+
+        sendEmailSupplement(entity.getIds());
+    }
+
+    @Override
+    public void sendEmailSupplement(SupplementSendEmailEntity entity) throws ServiceException {
+
+        String content = JSON.toJSONString(entity, SerializerFeature.WriteMapNullValue);
+        mqSendService.send(Constants.ACTIVEMQ_JMS_URL_EMAIL_SUPPLEMENT, content);
+    }
+
+    /**
+     * 发送邮件验证码并保存到redis 默认过期时间为Constants.SMS_VALIDATECODE_TIME五分钟
+     *
+     * @param sysEmailSendEntity 发送短信实体
+     * @param code               验证码
+     * @throws ServiceException
+     */
+    @Override
+    public void sendCode(SysEmailSendEntity sysEmailSendEntity, String code) throws ServiceException {
+        //默认过期时间为五分钟
+        sendCode(sysEmailSendEntity, code, Constants.EMAIL_VALIDATECODE_TIME * 60 * 1000);
+    }
+
+    /**
+     * 发送邮件验证码并保存到redis 过期时间为time 毫秒
+     *
+     * @param sysEmailSendEntity 发送短信实体
+     * @param code               验证码
+     * @param time               过期时间
+     * @throws ServiceException
+     */
+    @Override
+    public void sendCode(SysEmailSendEntity sysEmailSendEntity, String code, long time) throws ServiceException {
+
+        //将所有的邮箱转化为小写
+        sysEmailSendEntity.setUsers(sysEmailSendEntity.getUsers().toLowerCase());
+        validate(sysEmailSendEntity.getUsers(), sysEmailSendEntity.getEmailSendEnum().getCode(),
+                sysEmailSendEntity.getAddIp());
+
+//        String key = KEY_PREFIX_CODE + sysEmailSendEntity.getUsers() + code;
+        String key = KEY_PREFIX_CODE + sysEmailSendEntity.getUsers();
+        redisService.saveObject(key, code, time);
+
+        send(sysEmailSendEntity);
+    }
+
+    /**
+     * 验证code
+     *
+     * @param email
+     * @param code
+     * @throws ServiceException
+     */
+    @Override
+    public void validateCode(String email, String code) throws ServiceException {
+
+        if (Strings.isNullOrEmpty(email)) {
+            throw new ServiceException(Constants.EMAIL_CODE_ERROR);
+        }
+        email = email.toLowerCase();
+//        String key = KEY_PREFIX_CODE + email + code;
+        String key = KEY_PREFIX_CODE + email;
+        //获取原来的验证码
+        String oldCode = redisService.getObject(key);
+        //判断验证码是否过期 验证码是否正确
+        if (Strings.isNullOrEmpty(oldCode) || Strings.isNullOrEmpty(code) || !oldCode.equals(code)) {
+            log.error(String.format("email:%s,code:%s,old code:%s",email,code,oldCode));
+            throw new ServiceException(Constants.EMAIL_CODE_ERROR);
+        }
+        //移除验证码
+        redisService.remove(key);
+    }
+
+    /**
+     * 发送邮件
+     *
+     * @param sysEmailSendEntity
+     * @throws ServiceException
+     */
+    private void send(SysEmailSendEntity sysEmailSendEntity) throws ServiceException {
+        InfoEntity userInfoEntity = getUserInfoEntity();
+
+        //获取ip和地点
+        String returnStr = getIpStr();
+
+        send(sysEmailSendEntity, userInfoEntity, returnStr);
+    }
+
+    private void send(SysEmailSendEntity sysEmailSendEntity, String queueName) throws ServiceException {
+        InfoEntity userInfoEntity = getUserInfoEntity();
+
+        //获取ip和地点
+        String returnStr = getIpStr();
+
+        send(sysEmailSendEntity, userInfoEntity, returnStr, queueName);
+    }
+
+    private void send(List<SysEmailSendEntity> sysEmailSendEntities) throws ServiceException {
+
+        send(sysEmailSendEntities, Constants.ACTIVEMQ_JMS_URL_EMAIL);
+    }
+
+    private void send(List<SysEmailSendEntity> sysEmailSendEntities, String queueName) throws ServiceException {
+
+        InfoEntity userInfoEntity = getUserInfoEntity();
+
+        //获取ip和地点
+        String returnStr = getIpStr();
+
+        for (SysEmailSendEntity sysEmailSendEntity : sysEmailSendEntities) {
+
+            send(sysEmailSendEntity, userInfoEntity, returnStr, queueName);
+        }
+    }
+
+    private void send(SysEmailSendEntity sysEmailSendEntity, InfoEntity userInfoEntity, String returnStr) throws ServiceException {
+
+        send(sysEmailSendEntity, userInfoEntity, returnStr, Constants.ACTIVEMQ_JMS_URL_EMAIL);
+    }
+
+    private void send(SysEmailSendEntity sysEmailSendEntity, InfoEntity userInfoEntity, String returnStr,
+                      String queueName) throws ServiceException {
+
+        if (Strings.isNullOrEmpty(queueName)) {
+            queueName = Constants.ACTIVEMQ_JMS_URL_EMAIL;
+        }
+        if (userInfoEntity != null) {
+            sysEmailSendEntity.setAddUser(userInfoEntity.getId());
+        }
+
+        if (!Strings.isNullOrEmpty(returnStr)) {
+            sysEmailSendEntity.setAddIp(returnStr);
+        }
+
+        sysEmailSendEntity.setAddTime(new Date());
+
+        if (sysEmailSendEntity != null && !Strings.isNullOrEmpty(sysEmailSendEntity.getUsers())) {
+            if (sysEmailSendEntity.getBcc() == 1) {
+                String content = JSON.toJSONString(sysEmailSendEntity, SerializerFeature.WriteMapNullValue);
+                mqSendService.send(queueName, content);
+            } else {
+                if (sysEmailSendEntity.getSetSent() == 0) {
+                    String[] users = sysEmailSendEntity.getUsers().split(",");
+                    for (String user : users) {
+                        sysEmailSendEntity.setUsers(user);
+                        String content = JSON.toJSONString(sysEmailSendEntity, SerializerFeature.WriteMapNullValue);
+
+                        mqSendService.send(queueName, content);
+                    }
+                } else {
+
+                    String content = JSON.toJSONString(sysEmailSendEntity, SerializerFeature.WriteMapNullValue);
+
+                    mqSendService.send(queueName, content);
+                }
+            }
+
+        }
+    }
+
+    private InfoEntity getUserInfoEntity() throws ServiceException {
+        InfoEntity userInfoEntity = null;
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (requestAttributes != null) {
+            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
+            if (request != null) {
+                userInfoEntity = HttpServletRequestUtil.getLoginInfo(InfoEntity.class, request, redisService, true);
+            }
+        }
+        return userInfoEntity;
+    }
+
+    private String getIpStr() throws ServiceException {
+        String ipStr = null;
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (requestAttributes != null) {
+            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
+            if (request != null) {
+                String ip = GetIpAndMac.getIp(request);
+                try {
+                    ipStr = ip + "-" + sysCountryService.getAdderByIp(ip);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        return ipStr;
+    }
+}

+ 172 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/MqSendServiceImpl.java

@@ -0,0 +1,172 @@
+package com.crm.rely.backend.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.crm.rely.backend.service.MqSendService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.activemq.ScheduledMessage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.jms.core.MessageCreator;
+import org.springframework.stereotype.Service;
+
+import javax.jms.JMSException;
+import javax.jms.Queue;
+import javax.jms.TextMessage;
+
+/**
+ * @program: user-custom
+ * @description: mq统一发送
+ * @author: houn
+ * @create: 2019-05-22 09:41
+ */
+@Slf4j
+@Service
+public class MqSendServiceImpl implements MqSendService {
+
+    @Autowired
+    private JmsTemplate jmsTemplate;
+
+    /**
+     * 发送消息(毫秒)
+     *
+     * @param queue 队列
+     * @param msg   消息内容
+     */
+    @Override
+    public void send(Queue queue, String msg) {
+
+        write(queue, msg, 0);
+        jmsTemplate.convertAndSend(queue, msg);
+    }
+
+    @Override
+    public void send(Queue queue, Object msg) {
+        send(queue, JSON.toJSONString(msg));
+    }
+
+    /**
+     * 发送消息(毫秒)
+     *
+     * @param queueName 队列url(名称)
+     * @param msg       消息内容
+     */
+    @Override
+    public void send(String queueName, String msg) {
+
+        write(queueName, msg, 0);
+        jmsTemplate.convertAndSend(queueName, msg);
+
+    }
+
+    @Override
+    public void send(String queueName, Object msg) {
+        send(queueName, JSON.toJSONString(msg));
+    }
+
+    /**
+     * 发送延迟消息(毫秒)
+     *
+     * @param queue 队列
+     * @param msg   消息内容
+     * @param delay 延迟时间
+     */
+    @Override
+    public void send(Queue queue, String msg, long delay) {
+
+        write(queue, msg, delay);
+
+        jmsTemplate.send(queue, getMessageCreator(msg, delay));
+
+    }
+
+    @Override
+    public void send(Queue queue, Object msg, long delay) {
+        send(queue, JSON.toJSONString(msg), delay);
+    }
+
+    /**
+     * 发送延迟消息(毫秒)
+     *
+     * @param queueName 队列url(名称)
+     * @param msg       消息内容
+     * @param delay     延迟时间
+     */
+    @Override
+    public void send(String queueName, String msg, long delay) {
+
+        write(queueName, msg, delay);
+
+        jmsTemplate.send(queueName, getMessageCreator(msg, delay));
+
+    }
+
+    @Override
+    public void send(String queueName, Object msg, long delay) {
+        send(queueName, JSON.toJSONString(msg), delay);
+    }
+
+    private MessageCreator getMessageCreator(String msg, long delay) {
+
+        return session -> {
+            TextMessage tm = session.createTextMessage(msg);
+            tm.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
+            tm.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, 1 * 1000);
+            tm.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, 1);
+            return tm;
+        };
+    }
+
+    /**
+     * 获取队列名称
+     *
+     * @param queue
+     * @return
+     */
+    private String getQueueName(Queue queue) {
+
+        String queueName;
+        try {
+            queueName = queue.getQueueName();
+        } catch (JMSException e) {
+            log.error(e.getMessage());
+            queueName = "";
+        }
+        return queueName;
+    }
+
+    /**
+     * 打印发送日志
+     *
+     * @param queue
+     * @param msg
+     * @param delay
+     */
+    private void write(Queue queue, String msg, long delay) {
+
+        String queueName = getQueueName(queue);
+        write(queueName, msg, delay);
+    }
+
+    /**
+     * 打印发送日志
+     *
+     * @param queueName 队列名
+     * @param msg       消息内容
+     * @param delay     延迟时间 0为非延时
+     */
+    private void write(String queueName, String msg, long delay) {
+        validateParams(queueName, msg);
+        if (delay <= 0) {
+
+            log.info("发送MQ即时消息: queueName={}, msg={}", queueName, msg);
+        } else {
+            log.info("发送MQ延时消息: queueName={}, msg={}, delay={}ms", queueName, msg, delay);
+        }
+    }
+
+    private void validateParams(Object queueOrName, String msg) {
+        if (queueOrName == null || msg == null || msg.trim().isEmpty()) {
+            throw new IllegalArgumentException("Queue/QueueName or message cannot be null or empty");
+        }
+    }
+}

+ 421 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/RedisServiceImpl.java

@@ -0,0 +1,421 @@
+package com.crm.rely.backend.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.entity.login.InfoEntity;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.RedisService;
+import com.crm.rely.backend.util.UUIDUtil;
+import lombok.extern.log4j.Log4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.RedisConnectionFailureException;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.data.redis.core.types.RedisClientInfo;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author houn
+ */
+@Log4j
+@Service
+public class RedisServiceImpl implements RedisService {
+
+    private static final String LOCK_MSG = "OK";
+    private static final Long UNLOCK_MSG = 1L;
+    private static final String SET_IF_NOT_EXIST = "NX";
+    private static final String SET_WITH_EXPIRE_TIME = "PX";
+    private static final String LOCK_PREFIX = "lock_";
+    private static final int SLEEP_TIME = 100;
+    /**
+     * time millisecond
+     */
+    private static final int TIME = 1000;
+    private Logger logger = LoggerFactory.getLogger(RedisServiceImpl.class);
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
+    @Value("${spring.redis.type:1}")
+    private int type;
+    @Value("${spring.redis.PREFIX:}")
+    private String PREFIX;
+    /**
+     * lua unlock script
+     */
+    @Autowired
+    private RedisScript<Long> redisUnlockScript;
+    /**
+     * lua lock
+     */
+    @Autowired
+    private RedisScript<Long> redisLockScript;
+
+    @Override
+    public <T> boolean save(String key, T t) throws ServiceException {
+
+        return save(key, t, -1);
+    }
+
+    @Override
+    public <T> boolean save(String key, T t, long time) throws ServiceException {
+
+        String objectStr = JSON.toJSONString(t, SerializerFeature.WriteMapNullValue);
+
+        return saveObject(key, objectStr, time);
+    }
+
+    @Override
+    public <T> boolean saveObject(String key, T t) throws ServiceException {
+
+        return saveObject(key, t, -1);
+    }
+
+    @Override
+    public <T> boolean saveObject(String key, T t, long time) throws ServiceException {
+
+        if (!isConnect()) {
+            return false;
+        }
+
+//        if (time < 0) {
+//
+//            redisTemplate.opsForValue().set(key, t);
+//        } else {
+//            redisTemplate.opsForValue().set(key, t, time, TimeUnit.MILLISECONDS);
+//        }
+        if (time < 0) {
+
+            redisTemplate.opsForValue().set(PREFIX + key, t);
+        } else {
+            redisTemplate.opsForValue().set(PREFIX + key, t, time, TimeUnit.MILLISECONDS);
+        }
+
+
+        return true;
+    }
+
+    @Override
+    public <T> T getObject(String key) throws ServiceException {
+
+        if (!isConnect()) {
+            return null;
+        }
+
+//        if (hasKey(key)) {
+//            return (T) redisTemplate.opsForValue().get(key);
+//        }
+
+
+        if (hasKey(key)) {
+            return (T) redisTemplate.opsForValue().get(PREFIX + key);
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean hasKey(String key) throws ServiceException {
+
+        if (!isConnect()) {
+            return false;
+        }
+
+
+        return redisTemplate.hasKey(PREFIX + key);
+
+//        return redisTemplate.hasKey(key);
+    }
+
+    @Override
+    public <T> T getEntity(String key, Class<T> clazz) throws ServiceException {
+
+        if (!isConnect()) {
+            return null;
+        }
+
+//        if (hasKey(key)) {
+//            String redisStr = this.getString(key);
+//            return JSON.parseObject(redisStr, clazz);
+//        }
+
+
+        if (hasKey(key)) {
+            String redisStr = getString(key);
+            return JSON.parseObject(redisStr, clazz);
+        }
+
+        return null;
+    }
+
+    @Override
+    public <T> List<T> getList(String key, Class<T> clazz) throws ServiceException {
+
+        if (!isConnect()) {
+            return null;
+        }
+
+//        if (hasKey(key)) {
+//            String redisStr = this.getString(key);
+//            return JSON.parseArray(redisStr, clazz);
+//        }
+
+        if (hasKey(key)) {
+            String redisStr = getString(key);
+            return JSON.parseArray(redisStr, clazz);
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean remove(String key) throws ServiceException {
+
+        if (!isConnect()) {
+            return false;
+        }
+
+        Set<Object> keys = redisTemplate.keys(PREFIX + key);
+
+        if (keys != null && !keys.isEmpty()) {
+            redisTemplate.delete(keys);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean remove(String... keys) throws ServiceException {
+
+        List<String> keyArrays = new ArrayList<>(keys.length);
+        for (String key : keys) {
+            keyArrays.add(key);
+        }
+
+        return remove(keyArrays);
+    }
+
+    @Override
+    public boolean remove(Collection<String> keys) throws ServiceException {
+        if (keys == null || keys.size() <= 0) {
+            return true;
+        }
+        if (!isConnect()) {
+            return false;
+        }
+        Collection kCollection = new ArrayList<>(keys.size());
+        for (String key : keys) {
+
+//            kCollection.add(PREFIX + key);
+
+            Set<Object> setKeys = redisTemplate.keys(PREFIX + key);
+            kCollection.add(setKeys);
+        }
+
+        redisTemplate.delete(kCollection);
+
+        return true;
+    }
+
+    @Override
+    public String getString(String key) throws ServiceException {
+
+        if (!isConnect()) {
+            return null;
+        }
+
+        if (hasKey(key)) {
+            String redisStr = (String) redisTemplate.opsForValue().get(PREFIX + key);
+            if (redisStr == null) {
+                return "";
+            } else {
+                return redisStr;
+            }
+        }
+
+
+        return null;
+    }
+
+    @Override
+    public boolean isConnect() throws ServiceException {
+
+        boolean isConnect = false;
+        int count = 0;
+        while (count < 3 && !isConnect) {
+            try {
+
+                List<RedisClientInfo> redisClientInfos = redisTemplate.getClientList();
+
+                if (redisClientInfos != null && redisClientInfos.size() > 0) {
+                    isConnect = true;
+                }
+            } catch (RedisConnectionFailureException e) {
+
+                logger.error(e.getMessage());
+            }
+        }
+
+        return isConnect;
+    }
+
+
+    /**
+     * 一直获取锁 知道获取到为止
+     *
+     * @param key     锁头
+     * @param request 锁内容 建议直接uuid
+     * @return
+     * @throws InterruptedException
+     */
+    @Override
+    public boolean lock(String key, String request) throws InterruptedException {
+
+        try {
+            for (; ; ) {
+
+                if (lock(10 * TIME, key, request)) {
+                    return true;
+                }
+
+                Thread.sleep(SLEEP_TIME);
+            }
+        } catch (RuntimeException e) {
+            logger.error(e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 加锁 重复获取blockTime时间 原理为 blockTime -= SLEEP_TIME 每次 Thread.sleep(SLEEP_TIME)时间
+     * 这里SLEEP_TIME为100毫秒即blockTime/SLEEP_TIME的次数
+     *
+     * @param key
+     * @param request
+     * @param blockTime 尝试时间
+     * @return
+     * @throws InterruptedException
+     */
+    @Override
+    public boolean lock(String key, String request, int blockTime) throws InterruptedException {
+
+        try {
+            while (blockTime >= 0) {
+
+                if (lock(10 * TIME, key, request)) {
+                    return true;
+                }
+
+                blockTime -= SLEEP_TIME;
+
+                Thread.sleep(SLEEP_TIME);
+            }
+        } catch (RuntimeException e) {
+            logger.error(e.getMessage());
+            return false;
+        }
+        return false;
+    }
+
+    private boolean lock(int expireTime, String key, String request) {
+        List keys = Collections.singletonList(PREFIX + LOCK_PREFIX + key);
+
+        Long result = redisTemplate.execute(redisLockScript, keys, request, expireTime);
+        if (UNLOCK_MSG.equals(result)) {
+            return true;
+        } else {
+            return false;
+        }
+
+    }
+
+    /**
+     * 加锁 只获取一次
+     *
+     * @param key
+     * @param request
+     * @return
+     */
+    @Override
+    public boolean tryLock(String key, String request) {
+        return tryLock(key, request, 10 * TIME);
+    }
+
+    /**
+     * 加锁 只获取一次 但自定义过期时间
+     *
+     * @param key
+     * @param request
+     * @param expireTime 加锁时间
+     * @return
+     */
+    @Override
+    public boolean tryLock(String key, String request, int expireTime) {
+
+        return lock(expireTime, key, request);
+    }
+
+    @Override
+    public boolean tryLock(String key, Runnable statement) {
+
+        String co = UUIDUtil.getUUID();
+        try {
+            if (tryLock(key, co, 10 * 60 * 1000)) {
+                statement.run();
+                return true;
+            }
+            return false;
+        } finally {
+            unlock(key, co);
+
+        }
+    }
+
+    @Override
+    public void tryLock(String key, Runnable statement, String exceptionMsg) throws ServiceException {
+        if (!tryLock(key, statement)) {
+            throw ServiceException.exception(exceptionMsg);
+        }
+    }
+
+    @Override
+    public void tryLock(InfoEntity infoEntity, HttpServletRequest httpServletRequest, Runnable statement,
+                        String exceptionMsg) throws ServiceException {
+        String uri = httpServletRequest.getRequestURI();
+        String key = httpServletRequest.getHeader(Constants.ACCESS_TOKEN) + infoEntity.getId() + uri;
+        tryLock(key, statement, exceptionMsg);
+    }
+
+    @Override
+    public boolean unlock(String key, String request) {
+        List keys = Collections.singletonList(PREFIX + LOCK_PREFIX + key);
+
+        Long result = redisTemplate.execute(redisUnlockScript, keys, request);
+        if (UNLOCK_MSG.equals(result)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public <T> void saveList(String key, List<T> list) throws Exception {
+        if (!isConnect()) {
+            return;
+        }
+        String listString = JSON.toJSONString(list);
+
+        if (redisTemplate.hasKey(PREFIX + key)) {
+            redisTemplate.delete(PREFIX + key);
+        }
+
+        redisTemplate.opsForValue().set(PREFIX + key, listString);
+    }
+
+}

+ 129 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/SmsServiceImpl.java

@@ -0,0 +1,129 @@
+package com.crm.rely.backend.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.crm.rely.backend.configuration.BaseActiveMqConfiguration;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.entity.system.sms.SysSmsEntity;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.MqSendService;
+import com.crm.rely.backend.service.SmsService;
+import com.google.common.base.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author Administrator
+ */
+@Service
+public class SmsServiceImpl extends BlackListServiceImpl implements SmsService {
+
+
+    private final static String KEY_PREFIX_CODE = "SMS_VALIDATOR_CODE";
+
+    @Autowired
+    private BaseActiveMqConfiguration baseAcitveMqConfiguration;
+
+    @Autowired
+    private MqSendService mqSendService;
+
+    public SmsServiceImpl() {
+        super("SMS_BLACKLIST_", "SMS_BLACKLIST__BACK_", 60 * 1000L, 3 * 60 * 1000L, 3, 3, 24);
+    }
+
+    /**
+     * 发送短信
+     *
+     * @param sysSmsEntity 发送短信实体
+     * @throws ServiceException
+     */
+    @Override
+    public void sendSms(SysSmsEntity sysSmsEntity) throws ServiceException {
+        sendSms(sysSmsEntity, true);
+    }
+
+    @Override
+    public void sendSms(SysSmsEntity sysSmsEntity, boolean validate) throws ServiceException {
+        if (validate) {
+            validate(sysSmsEntity.getReceive(), sysSmsEntity.getSmsSendEnum().getCode(), sysSmsEntity.getAddIp());
+        }
+        send(sysSmsEntity);
+    }
+
+    /**
+     * 发送短信验证码并保存到redis 默认过期时间为Constants.SMS_VALIDATECODE_TIME五分钟
+     *
+     * @param sysSmsEntity 发送短信实体
+     * @param code         验证码
+     * @throws ServiceException
+     */
+    @Override
+    public void sendCode(SysSmsEntity sysSmsEntity, String code) throws ServiceException {
+        sendCode(sysSmsEntity, code, true);
+    }
+
+    @Override
+    public void sendCode(SysSmsEntity sysSmsEntity, String code, boolean validate) throws ServiceException {
+        //默认过期时间为五分钟
+        sendCode(sysSmsEntity, code, validate,Constants.SMS_VALIDATECODE_TIME * 60 * 1000);
+    }
+
+    /**
+     * 发送短信验证码并保存到redis 过期时间为time 毫秒
+     *
+     * @param sysSmsEntity 发送短信实体
+     * @param code         验证码
+     * @param time         过期时间
+     * @throws ServiceException
+     */
+    @Override
+    public void sendCode(SysSmsEntity sysSmsEntity, String code, long time) throws ServiceException {
+        sendCode(sysSmsEntity, code, true, time);
+    }
+
+    @Override
+    public void sendCode(SysSmsEntity sysSmsEntity, String code, boolean validate, long time) throws ServiceException {
+        if (validate) {
+            validate(sysSmsEntity.getReceive(), sysSmsEntity.getSmsSendEnum().getCode(), sysSmsEntity.getAddIp());
+        }
+
+        String key = KEY_PREFIX_CODE + sysSmsEntity.getReceive();
+        redisService.save(key, code, time);
+
+        send(sysSmsEntity);
+    }
+
+    /**
+     * 验证code
+     *
+     * @param phone
+     * @param code
+     * @throws ServiceException
+     */
+    @Override
+    public void validateCode(String phone, String code) throws ServiceException {
+
+        String key = KEY_PREFIX_CODE + phone;
+        //获取原来的验证码
+        String oldCode = redisService.getObject(key);
+        //判断验证码是否过期 验证码是否正确
+        if (Strings.isNullOrEmpty(oldCode) || Strings.isNullOrEmpty(code) || !oldCode.equals(code)) {
+            throw new ServiceException(Constants.PHONE_CODE_ERROR);
+        }
+        //移除验证码
+        redisService.remove(key);
+    }
+
+    /**
+     * 发送短信
+     *
+     * @param sysSmsEntity
+     * @throws ServiceException
+     */
+    private void send(SysSmsEntity sysSmsEntity) throws ServiceException {
+        String content = JSON.toJSONString(sysSmsEntity, SerializerFeature.WriteMapNullValue);
+        mqSendService.send(baseAcitveMqConfiguration.smsQueue(), content);
+    }
+
+
+}

+ 187 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/SysCountryServiceImpl.java

@@ -0,0 +1,187 @@
+package com.crm.rely.backend.service.impl;
+
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.SysCountryService;
+import com.crm.rely.backend.util.*;
+import com.google.common.base.Strings;
+import lombok.extern.log4j.Log4j;
+import net.renfei.ip2location.IP2Location;
+import net.renfei.ip2location.IPResult;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+@Log4j
+@Service
+public class SysCountryServiceImpl implements SysCountryService {
+
+    private final static String ip2LocationLiteDb3DownloadUrl = "https://www.ip2location" +
+            ".com/download/?token=Om92KAuv7E1Hb06YW5xucIC9dHyqo2yDAZdkKBcxTDVE32MXBn8VYfR3XtSadpez&file=DB3LITEBIN";
+    private final static String ip2LocationLiteDb3DownloadName = "IP2LOCATION-LITE-DB3.BIN.ZIP";
+    //名称
+    private final static String ip2LocationLiteDb3 = "IP2LOCATION-LITE-DB3.BIN";
+    private static String path = null;
+    private static IP2Location loc = null;
+    private static long time = 0;
+    private static String yearMonth;
+    private static String absolutePath = null;
+    //路劲
+    @Value("${ip.ip2LocationLiteDb3:../}")
+    private String ip2LocationLiteDb3Child;
+
+    private synchronized String getAbsolutePath() {
+        if (Strings.isNullOrEmpty(absolutePath)) {
+            File rootFile = new File("");
+            File file = new File(rootFile.getAbsolutePath() + File.separator + ip2LocationLiteDb3Child);
+            absolutePath = file.getPath();
+        }
+        return absolutePath;
+
+    }
+
+    @Scheduled(fixedDelay = 5 * 60 * 60 * 1000)
+    private void scheduleDownload() throws Exception {
+        String newYearMonth = DateUtil.format("yyyyMM");
+        if (!newYearMonth.equals(yearMonth)) {
+            yearMonth = newYearMonth;
+            File file = new File(getPath(yearMonth));
+            path = FileProcessUtil.getPath(getPath()).toString();
+            if (!file.exists()) {
+
+                //下载存放目录
+                String targetDirectory =
+                        getAbsolutePath() + File.separator + "download" + File.separator + yearMonth + File.separator + UUIDUtil.getUUID();
+//                String targetDirectory =
+//                        getAbsolutePath() + File.separator + "download" + File.separator + month;
+                Path targetDirectoryPath = FileProcessUtil.getPath(targetDirectory);
+                Files.createDirectories(targetDirectoryPath);
+                targetDirectory = targetDirectoryPath.toString();
+                //如果不存在 进行下载文件
+                HttpUtil.download(ip2LocationLiteDb3DownloadUrl, targetDirectory,
+                        ip2LocationLiteDb3DownloadName);
+                //解压文件到指定目录
+                ZipUtil.unzip(targetDirectory + File.separator + ip2LocationLiteDb3DownloadName,
+                        FileProcessUtil.getPath(getAbsolutePath() + File.separator + yearMonth).toString());
+
+                FileProcessUtil.deleteFile(targetDirectory);
+
+            }
+        }
+
+    }
+
+    private String getPath() {
+        if (!Strings.isNullOrEmpty(path)) {
+            return path;
+        }
+        return getPath(yearMonth);
+    }
+
+    private String getPath(String month) {
+        path = getAbsolutePath() + File.separator + month + File.separator + ip2LocationLiteDb3;
+        return path;
+    }
+
+    private synchronized IP2Location getIP2Location() {
+        try {
+            long nowTime = System.currentTimeMillis();
+            if (loc == null || (nowTime - time) > 0) {
+                if (loc != null) {
+                    loc.Close();
+                }
+                loc = new IP2Location();
+                String path = getPath();
+                loc.Open(path, true);
+                time = nowTime + 60 * 60 * 1000;
+
+            }
+            return loc;
+
+        } catch (Exception e) {
+            log.error(e.getMessage());
+            return null;
+        }
+    }
+
+    @Override
+    public String getRequestByIp(HttpServletRequest request) throws ServiceException {
+        return GetIpAndMac.getIp(request);
+    }
+
+    @Override
+    public String getAdderByIp(String ip) throws ServiceException {
+
+        StringBuffer adderS = new StringBuffer(20);
+        adderS.append(ip);
+        try {
+            IP2Location loc = getIP2Location();
+            if (loc == null) {
+                return adderS.toString();
+            }
+            IPResult rec = loc.IPQuery(ip);
+            if ("OK".equals(rec.getStatus())) {
+                adderS.append("-");
+                adderS.append(rec.getCountryLong());
+                adderS.append("-");
+                adderS.append(rec.getRegion());
+                adderS.append("-");
+                adderS.append(rec.getCity());
+            } else if ("EMPTY_IP_ADDRESS".equals(rec.getStatus())) {
+                System.out.println("IP address cannot be blank.");
+            } else if ("INVALID_IP_ADDRESS".equals(rec.getStatus())) {
+                System.out.println("Invalid IP address.");
+            } else if ("MISSING_FILE".equals(rec.getStatus())) {
+                System.out.println("Invalid database path.");
+            } else if ("IPV6_NOT_SUPPORTED".equals(rec.getStatus())) {
+                System.out.println("This BIN does not contain IPv6 data.");
+            } else {
+                System.out.println("Unknown error." + rec.getStatus());
+            }
+        } catch (Exception e) {
+            System.out.println(e);
+            e.printStackTrace(System.out);
+        }
+        return adderS.toString();
+
+    }
+
+    @Override
+    public String getCountryByIp(HttpServletRequest request) throws ServiceException {
+        return getCountryByIp(getRequestByIp(request));
+    }
+
+    @Override
+    public String getCountryByIp(String ip) throws ServiceException {
+        StringBuffer adderS = new StringBuffer(20);
+        try {
+            IP2Location loc = getIP2Location();
+            if (loc == null) {
+                return adderS.toString();
+            }
+            IPResult rec = loc.IPQuery(ip);
+            if ("OK".equals(rec.getStatus())) {
+                adderS.append(rec.getCountryShort());
+            } else if ("EMPTY_IP_ADDRESS".equals(rec.getStatus())) {
+                System.out.println("IP address cannot be blank.");
+            } else if ("INVALID_IP_ADDRESS".equals(rec.getStatus())) {
+                System.out.println("Invalid IP address.");
+            } else if ("MISSING_FILE".equals(rec.getStatus())) {
+                System.out.println("Invalid database path.");
+            } else if ("IPV6_NOT_SUPPORTED".equals(rec.getStatus())) {
+                System.out.println("This BIN does not contain IPv6 data.");
+            } else {
+                System.out.println("Unknown error." + rec.getStatus());
+            }
+        } catch (Exception e) {
+            System.out.println(e);
+            e.printStackTrace(System.out);
+        }
+        return adderS.toString();
+    }
+
+}

+ 141 - 0
ucard-backend/src/main/java/com/crm/rely/backend/service/impl/base/BaseUpload.java

@@ -0,0 +1,141 @@
+package com.crm.rely.backend.service.impl.base;
+
+import com.crm.rely.backend.configuration.LocaleMessage;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.util.FileProcessUtil;
+import com.google.common.base.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.InputStream;
+
+public class BaseUpload {
+    private final static String FILE_COMMON = "/file/common/";
+    @Autowired
+    private LocaleMessage localeMessage;
+    @Value("${web.upload-path:./upload}")
+    private String UPLOAD;
+
+    private static boolean isSafeImage(InputStream inputStream) {
+        try {
+
+            // 检查文件头(魔数)
+            byte[] headerBytes = new byte[8];
+            inputStream.read(headerBytes, 0, 8);
+            if (!isValidImageHeader(headerBytes)) {
+                System.out.println("图片文件头不合法,可能存在风险");
+                return false;
+            }
+
+            // 验证图片内容
+            BufferedImage image = ImageIO.read(inputStream);
+            if (image == null) {
+                System.out.println("无法解析为有效图片");
+                return false;
+            }
+
+            // 检查图片尺寸限制
+            int width = image.getWidth();
+            int height = image.getHeight();
+            if (width > 5000 || height > 5000) {
+                System.out.println("图片尺寸过大: " + width + "x" + height);
+                return false;
+            }
+
+            return true;
+        } catch (Exception e) {
+            System.out.println("图片验证失败: " + e.getMessage());
+            return false;
+        }
+    }
+
+    // 验证文件头(魔数)是否是合法图片格式
+    private static boolean isValidImageHeader(byte[] headerBytes) {
+        if (headerBytes.length < 8) {
+            return false;
+        }
+
+        // JPEG 文件头 (FF D8 FF)
+        if (headerBytes[0] == (byte) 0xFF && headerBytes[1] == (byte) 0xD8 && headerBytes[2] == (byte) 0xFF) {
+            return true;
+        }
+
+        // PNG 文件头 (89 50 4E 47 0D 0A 1A 0A)
+        if (headerBytes[0] == (byte) 0x89 && headerBytes[1] == (byte) 0x50 && headerBytes[2] == (byte) 0x4E &&
+                headerBytes[3] == (byte) 0x47 && headerBytes[4] == (byte) 0x0D && headerBytes[5] == (byte) 0x0A &&
+                headerBytes[6] == (byte) 0x1A && headerBytes[7] == (byte) 0x0A) {
+            return true;
+        }
+
+        // GIF 文件头 (47 49 46 38)
+        if (headerBytes[0] == (byte) 0x47 && headerBytes[1] == (byte) 0x49 && headerBytes[2] == (byte) 0x46 &&
+                headerBytes[3] == (byte) 0x38) {
+            return true;
+        }
+
+        // BMP 文件头 (42 4D)
+        if (headerBytes[0] == (byte) 0x42 && headerBytes[1] == (byte) 0x4D) {
+            return true;
+        }
+
+        // WebP 文件头 (52 49 46 46 .... 57 45 42 50)
+        if (headerBytes[0] == (byte) 0x52 && headerBytes[1] == (byte) 0x49 && headerBytes[2] == (byte) 0x46 &&
+                headerBytes[3] == (byte) 0x46 && headerBytes[8] == (byte) 0x57 && headerBytes[9] == (byte) 0x45 &&
+                headerBytes[10] == (byte) 0x42 && headerBytes[11] == (byte) 0x50) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected void validated(MultipartFile file, int size, String regex) throws ServiceException {
+        //验证文件大小
+        if (file.getSize() > size * 1000 * 1024) {
+            throw new ServiceException(localeMessage.getMessage(Constants.UPLOAD_FILE_SIZE_MAX) + size + "MB");
+        }
+        //文件格式
+        String fileName = file.getOriginalFilename().toLowerCase();
+        if (!fileName
+                .substring(fileName.lastIndexOf(".") + 1, fileName.length())
+                .matches(regex)) {
+            throw ServiceException.exception(localeMessage.getMessage(Constants.UPLOAD_FILE_REGEX,
+                    Constants.UPLOAD_FILE_REGEX) + " " + regex);
+
+        }
+    }
+
+    protected String saveFile(String middlePath, String filename, InputStream inputStream) {
+        return saveFile(middlePath, filename, false, inputStream);
+    }
+
+    protected String saveFile(String middlePath, String filename, boolean isSaveFileName, InputStream inputStream) {
+        String savePath = getMiddlePath(middlePath);
+
+        String path = FileProcessUtil.upload(filename, isSaveFileName, inputStream, savePath);
+        return (Strings.isNullOrEmpty(middlePath) ? (FILE_COMMON) : middlePath) + path;
+    }
+
+    protected String saveFile(String middlePath, MultipartFile file) {
+        return saveFile(middlePath, false, file);
+    }
+
+    protected String saveFile(String middlePath, boolean isSaveFileName, MultipartFile file) {
+
+        String savePath = getMiddlePath(middlePath);
+
+        String path = FileProcessUtil.upload(file, isSaveFileName, savePath);
+        return (Strings.isNullOrEmpty(middlePath) ? (FILE_COMMON) : middlePath) + path;
+    }
+
+    private String getMiddlePath(String middlePath) {
+        if (!Strings.isNullOrEmpty(middlePath) && !middlePath.startsWith("/")) {
+            middlePath = "/" + middlePath;
+        }
+        String savePath = UPLOAD + (Strings.isNullOrEmpty(middlePath) ? (FILE_COMMON) : middlePath);
+        return savePath;
+    }
+}

+ 196 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/AESUtil.java

@@ -0,0 +1,196 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.core.constant.Constants;
+import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
+import org.springframework.util.StringUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+public class AESUtil {
+    private static final String KEY_ALGORITHM = "AES";
+
+    private static final String KEY_GCM_NO_PADDING_ALGORITHM = "AES/GCM/NoPadding";
+
+    private static final String SHA1PRNG = "SHA1PRNG";
+    private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
+
+    /**
+     * AES 加密操作
+     *
+     * @param content 待加密内容
+     * @return 返回Base64转码后的加密数据
+     */
+    public static String encrypt(String content) {
+        return AESUtil.encrypt(content, Constants.AES_KEY);
+    }
+
+    public static String encrypt(String content, String secureKey) {
+
+        return AESUtil.encrypt(content, secureKey, AESUtil.KEY_ALGORITHM);
+    }
+
+    public static String encrypt(String content, String secureKey, String algorithm) {
+
+        return AESUtil.encrypt(content, secureKey, algorithm, AESUtil.SHA1PRNG);
+    }
+
+    public static String encrypt(String content, String secureKey, String algorithm, String srAlgorithm) {
+        try {
+            if ((StringUtils.isEmpty(content)) ||
+                    (StringUtils.isEmpty(secureKey))) {
+                return null;
+            }
+            KeyGenerator kgen = KeyGenerator.getInstance(algorithm);
+
+            SecureRandom secureRandom = SecureRandom.getInstance(srAlgorithm);
+            secureRandom.setSeed(secureKey.getBytes());
+            kgen.init(128, secureRandom);
+            SecretKey secretKey = kgen.generateKey();
+            byte[] enCodeFormat = secretKey.getEncoded();
+            SecretKeySpec key = new SecretKeySpec(enCodeFormat, algorithm);
+            Cipher cipher = Cipher.getInstance(algorithm);
+            byte[] byteContent = content.getBytes("utf-8");
+            cipher.init(1, key);
+            byte[] result = cipher.doFinal(byteContent);
+            return AESUtil.encodeBASE64(result);
+        } catch (Exception ex) {
+//            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return null;
+    }
+
+    public static String decrypt(String content, String secureKey) {
+
+        return AESUtil.decrypt(content, secureKey, AESUtil.KEY_ALGORITHM);
+    }
+
+    public static String decrypt(String content, String secureKey, String algorithm) {
+
+        return AESUtil.decrypt(content, secureKey, algorithm, AESUtil.SHA1PRNG);
+    }
+
+    public static String decrypt(String content, String secureKey, String algorithm, String srAlgorithm) {
+        try {
+            if ((StringUtils.isEmpty(content)) || (StringUtils.isEmpty(secureKey))) {
+                return null;
+            }
+            KeyGenerator kgen = KeyGenerator.getInstance(algorithm);
+            SecureRandom secureRandom = SecureRandom.getInstance(srAlgorithm);
+            secureRandom.setSeed(secureKey.getBytes());
+            kgen.init(128, secureRandom);
+            SecretKey secretKey = kgen.generateKey();
+            byte[] enCodeFormat = secretKey.getEncoded();
+            SecretKeySpec key = new SecretKeySpec(enCodeFormat, algorithm);
+            Cipher cipher = Cipher.getInstance(algorithm);
+            cipher.init(2, key);
+            byte[] base64Dec = Base64.decode(content);
+            byte[] result = cipher.doFinal(base64Dec);
+            return new String(result);
+        } catch (Exception ex) {
+//            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return null;
+    }
+
+    /**
+     * AES 解密操作
+     *
+     * @param content
+     * @return
+     */
+    public static String decrypt(String content) {
+        return AESUtil.decrypt(content, Constants.AES_KEY);
+    }
+
+    public static String encodeBASE64(byte[] content)
+            throws Exception {
+        if ((content == null) || (content.length == 0)) {
+            return null;
+        }
+        try {
+            return Base64.encode(content);
+        } catch (Exception ex) {
+//            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return null;
+    }
+
+    public static String encryptPayload(String payload, String key) throws Exception {
+        SecureRandom r = new SecureRandom();
+
+        byte[] ivBytes = new byte[16];
+        r.nextBytes(ivBytes);
+
+        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
+        byte[] inputBytes = payload.getBytes(StandardCharsets.UTF_8);
+        byte[] encryptedBytes = AESUtil.encryptDataWithAes(inputBytes, keyBytes, ivBytes);
+
+        byte[] cipherTextBytes = Arrays.copyOfRange(encryptedBytes, 0, payload.length());
+        byte[] authTagBytes = Arrays.copyOfRange(encryptedBytes, payload.length(), encryptedBytes.length);
+
+        String ivHex = AESUtil.bytesToHex(ivBytes);
+        String encryptedHex = AESUtil.bytesToHex(cipherTextBytes);
+        String authTagHex = AESUtil.bytesToHex(authTagBytes);
+
+        String result = new StringBuilder()
+                .append(ivHex)
+                .append(":")
+                .append(encryptedHex)
+                .append(":")
+                .append(authTagHex)
+                .toString();
+        return result;
+    }
+
+    private static String bytesToHex(byte[] bytes) {
+        char[] hexChars = new char[bytes.length * 2];
+        for (int j = 0; j < bytes.length; j++) {
+            int v = bytes[j] & 0xFF;
+            hexChars[j * 2] = AESUtil.HEX_ARRAY[v >>> 4];
+            hexChars[j * 2 + 1] = AESUtil.HEX_ARRAY[v & 0x0F];
+        }
+        return new String(hexChars);
+    }
+
+    private static byte[] encryptDataWithAes(byte[] plainText, byte[] aesKey, byte[] aesIv) throws Exception {
+        GCMParameterSpec gcmSpec = new GCMParameterSpec(128, aesIv);
+        Cipher cipher = Cipher.getInstance(AESUtil.KEY_GCM_NO_PADDING_ALGORITHM);
+        SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, AESUtil.KEY_ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmSpec);
+        byte[] cipherText = cipher.doFinal(plainText);
+
+        return cipherText;
+    }
+
+    public static void main(String[] args) throws Exception {
+//        String content = "hello,您好";
+//
+//        System.out.println("content:" + content);
+//        String s1 = AESUtil.encrypt(content, Constants.AES_KEY);
+//        System.out.println("s1:" + s1);
+//        System.out.println("s2:" + AESUtil.decrypt(s1, Constants.AES_KEY));
+//        System.out.println(encrypt("b36232e2356b4f549d8ba65ec203b075!@#WEB"));
+//        System.out.println(decrypt("SDHmpqX6SSMKYLEpeUu9ivYd0CLJfcMRqn5cmL1X3WsP2WtHK7Fw3Fma82SSEs3b"));
+
+
+//        String customId = "20926";
+//        String en = encrypt(customId);
+//        System.out.println(en);
+//        System.out.println(decrypt(en));
+//        System.out.println(AESUtil.encrypt("123", "gvwt4pujpR5atJueUAFBTiM5Con3obhE"));
+//        System.out.println(AESUtil.encrypt("123", "gvwt4pujpR5atJueUAFBTiM5Con3obhE",
+//                AESUtil.KEY_ALGORITHM, AESUtil.SHA1PRNG));
+//        System.out.println(AESUtil.encrypt("123", "gvwt4pujpR5atJueUAFBTiM5Con3obhE"));
+//        System.out.println(AESUtil.encryptPayload("123", "gvwt4pujpR5atJueUAFBTiM5Con3obhE"));
+//        System.out.println(AESUtil.getSha512Hash("1,0,10"));
+
+    }
+
+}

+ 223 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/AliasMethodUtil.java

@@ -0,0 +1,223 @@
+package com.crm.rely.backend.util;
+
+import java.security.SecureRandom;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @Author houn
+ * @Date 2023/8/11 14:49
+ * @PackageName:com.crm.rely.backend.util
+ * @ClassName: AliasMethodUtil
+ * @Description: TODO
+ */
+public class AliasMethodUtil {
+
+    /*
+    The random
+    number generator
+    used to
+    sample from
+    the distribution.*/
+    private final Random random;
+
+    /* The probability and alias tables. */
+    private final int[] alias;
+    private final double[] probability;
+
+    /**
+     * Constructs a new AliasMethod to sample from a discrete distribution and
+     * hand back outcomes based on the probability distribution.
+     * <p/>
+     * Given as input a list of probabilities corresponding to outcomes 0, 1,
+     * ..., n - 1, this constructor creates the probability and alias tables
+     * needed to efficiently sample from this distribution.
+     *
+     * @param probabilities The list of probabilities.
+     */
+    public AliasMethodUtil(List<Double> probabilities) {
+        this(probabilities, new Random());
+    }
+
+    /**
+     * Constructs a new AliasMethod to sample from a discrete distribution and
+     * hand back outcomes based on the probability distribution.
+     * <p/>
+     * Given as input a list of probabilities corresponding to outcomes 0, 1,
+     * ..., n - 1, along with the random number generator that should be used
+     * as the underlying generator, this constructor creates the probability
+     * and alias tables needed to efficiently sample from this distribution.
+     *
+     * @param probabilities The list of probabilities.
+     * @param random        The random number generator
+     */
+    public AliasMethodUtil(List<Double> probabilities, Random random) {
+        /* Begin by doing basic structural checks on the inputs. */
+        if (probabilities == null || random == null) {
+            throw new NullPointerException();
+        }
+        if (probabilities.size() == 0) {
+            throw new IllegalArgumentException("Probability vector must be nonempty.");
+        }
+
+        /* Allocate space for the probability and alias tables. */
+        probability = new double[probabilities.size()];
+        alias = new int[probabilities.size()];
+
+        /* Store the underlying generator. */
+        this.random = random;
+
+        /* Compute the average probability and cache it for later use. */
+        double average = 1.0 / probabilities.size();
+
+        /* Make a copy of the probabilities list, since we will be making
+         * changes to it.
+         */
+        probabilities = new ArrayList<Double>(probabilities);
+
+        /* Create two stacks to act as worklists as we populate the tables. */
+        Deque<Integer> small = new ArrayDeque<Integer>();
+        Deque<Integer> large = new ArrayDeque<Integer>();
+
+        /* Populate the stacks with the input probabilities. */
+        for (int i = 0; i < probabilities.size(); ++i) {
+            /* If the probability is below the average probability, then we add
+             * it to the small list; otherwise we add it to the large list.
+             */
+            if (probabilities.get(i) >= average) {
+                large.add(i);
+            } else {
+                small.add(i);
+            }
+        }
+
+        /* As a note: in the mathematical specification of the algorithm, we
+         * will always exhaust the small list before the big list.  However,
+         * due to floating point inaccuracies, this is not necessarily true.
+         * Consequently, this inner loop (which tries to pair small and large
+         * elements) will have to check that both lists aren't empty.
+         */
+        while (!small.isEmpty() && !large.isEmpty()) {
+            /* Get the index of the small and the large probabilities. */
+            int less = small.removeLast();
+            int more = large.removeLast();
+
+            /* These probabilities have not yet been scaled up to be such that
+             * 1/n is given weight 1.0.  We do this here instead.
+             */
+            probability[less] = probabilities.get(less) * probabilities.size();
+            alias[less] = more;
+
+            /* Decrease the probability of the larger one by the appropriate
+             * amount.
+             */
+            probabilities.set(more,
+                    (probabilities.get(more) + probabilities.get(less)) - average);
+
+            /* If the new probability is less than the average, add it into the
+             * small list; otherwise add it to the large list.
+             */
+            if (probabilities.get(more) >= 1.0 / probabilities.size()) {
+                large.add(more);
+            } else {
+                small.add(more);
+            }
+        }
+
+        /* At this point, everything is in one list, which means that the
+         * remaining probabilities should all be 1/n.  Based on this, set them
+         * appropriately.  Due to numerical issues, we can't be sure which
+         * stack will hold the entries, so we empty both.
+         */
+        while (!small.isEmpty()) {
+            probability[small.removeLast()] = 1.0;
+        }
+        while (!large.isEmpty()) {
+            probability[large.removeLast()] = 1.0;
+        }
+    }
+
+    /**
+     * Samples a value from the underlying distribution.
+     *
+     * @return A random value sampled from the underlying distribution.
+     */
+    public int next() {
+        /* Generate a fair die roll to determine which column to inspect. */
+        int column = random.nextInt(probability.length);
+
+        /* Generate a biased coin toss to determine which option to pick. */
+        boolean coinToss = random.nextDouble() < probability[column];
+
+        /* Based on the outcome, return either the column or its alias. */
+       /* Log.i("1234","column="+column);
+        Log.i("1234","coinToss="+coinToss);
+        Log.i("1234","alias[column]="+coinToss);*/
+        return coinToss ? column : alias[column];
+    }
+
+    public static void main(String[] args) {
+        TreeMap<Integer, Double> map = new TreeMap<Integer, Double>();
+//        map.put(0, 0.0);
+//        map.put(1, 0.889);
+//        map.put(5, 0.08);
+//        map.put(10, 0.021);
+//        map.put(20, 0.01);
+//        map.put(50, 0.0);
+//        map.put(100, 0.0);
+//        map.put(200, 0.0);
+
+//        map.put(0, 0.55);
+//        map.put(1, 0.392);
+//        map.put(5, 0.035);
+//        map.put(10, 0.012);
+//        map.put(20, 0.008);
+//        map.put(50, 0.003);
+//        map.put(100, 0.0);
+//        map.put(200, 0.0);
+
+        map.put(0, 0.65);
+        map.put(1, 0.33);
+        map.put(5, 0.01);
+        map.put(10, 0.005);
+        map.put(20, 0.003);
+        map.put(50, 0.002);
+        map.put(100, 0.0);
+        map.put(200, 0.0);
+
+        List<Double> list = new ArrayList<Double>(map.values());
+        List<Integer> gifts = new ArrayList<Integer>(map.keySet());
+
+        AliasMethodUtil method = new AliasMethodUtil(list, new SecureRandom());
+
+        Map<Integer, AtomicInteger> resultMap = new HashMap<Integer, AtomicInteger>();
+        Integer tAmount = 0;
+        for (int i = 0; i < 100; i++) {
+            Integer amount = 0;
+            for (int j = 0; j < 10; j++) {
+                int index = method.next();
+                Integer key = gifts.get(index);
+                if (!resultMap.containsKey(key)) {
+                    amount += key;
+                }
+            }
+            tAmount += amount;
+            System.out.println(amount);
+        }
+        System.out.println(tAmount / 100.0);
+
+
+//        for (int i = 0; i < 10000000; i++) {
+//            int index = method.next();
+//            Integer key = gifts.get(index);
+//            if (!resultMap.containsKey(key)) {
+//                resultMap.put(key, new AtomicInteger());
+//            }
+//            resultMap.get(key).incrementAndGet();
+//        }
+//        for (Integer key : resultMap.keySet()) {
+//            System.out.println(key + "==" + resultMap.get(key));
+//        }
+
+    }
+}

+ 886 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/BCrypt.java

@@ -0,0 +1,886 @@
+package com.crm.rely.backend.util;
+
+import java.io.UnsupportedEncodingException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+public class BCrypt {
+    // BCrypt parameters
+    private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10;
+    private static final int BCRYPT_SALT_LEN = 16;
+
+    // Blowfish parameters
+    private static final int BLOWFISH_NUM_ROUNDS = 16;
+
+    // Initial contents of key schedule
+    private static final int P_orig[] = {
+            0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
+            0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
+            0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
+            0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
+            0x9216d5d9, 0x8979fb1b
+    };
+    private static final int S_orig[] = {
+            0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
+            0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
+            0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
+            0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
+            0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
+            0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
+            0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
+            0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
+            0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
+            0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
+            0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
+            0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
+            0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
+            0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
+            0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
+            0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
+            0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
+            0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
+            0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
+            0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
+            0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
+            0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
+            0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
+            0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
+            0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
+            0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
+            0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
+            0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
+            0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
+            0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
+            0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
+            0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
+            0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
+            0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
+            0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
+            0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
+            0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
+            0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
+            0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
+            0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
+            0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
+            0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
+            0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
+            0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
+            0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
+            0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
+            0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
+            0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
+            0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
+            0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
+            0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
+            0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
+            0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
+            0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
+            0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
+            0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
+            0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
+            0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
+            0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
+            0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
+            0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
+            0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
+            0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
+            0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a,
+            0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623,
+            0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
+            0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
+            0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
+            0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
+            0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
+            0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
+            0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
+            0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
+            0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
+            0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
+            0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
+            0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
+            0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
+            0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
+            0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
+            0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
+            0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
+            0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
+            0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
+            0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
+            0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
+            0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
+            0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
+            0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
+            0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
+            0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
+            0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
+            0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
+            0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
+            0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
+            0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
+            0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
+            0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
+            0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
+            0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
+            0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
+            0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
+            0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
+            0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
+            0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
+            0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
+            0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
+            0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
+            0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
+            0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
+            0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
+            0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
+            0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
+            0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
+            0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
+            0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
+            0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
+            0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
+            0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
+            0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
+            0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
+            0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
+            0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
+            0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
+            0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
+            0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
+            0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
+            0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7,
+            0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934,
+            0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
+            0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
+            0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
+            0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
+            0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
+            0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
+            0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
+            0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
+            0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
+            0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
+            0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
+            0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
+            0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
+            0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
+            0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
+            0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
+            0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
+            0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
+            0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
+            0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
+            0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
+            0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
+            0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
+            0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
+            0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
+            0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
+            0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
+            0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
+            0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
+            0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
+            0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
+            0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
+            0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
+            0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
+            0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
+            0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
+            0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
+            0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
+            0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
+            0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
+            0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
+            0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
+            0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
+            0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
+            0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
+            0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
+            0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
+            0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
+            0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
+            0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
+            0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
+            0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
+            0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
+            0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
+            0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
+            0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
+            0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
+            0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
+            0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
+            0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
+            0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
+            0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
+            0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0,
+            0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b,
+            0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
+            0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
+            0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
+            0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
+            0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
+            0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
+            0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
+            0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
+            0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
+            0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
+            0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
+            0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
+            0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
+            0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
+            0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
+            0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
+            0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
+            0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
+            0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
+            0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
+            0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
+            0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
+            0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
+            0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
+            0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
+            0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
+            0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
+            0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
+            0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
+            0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
+            0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
+            0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
+            0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
+            0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
+            0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
+            0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
+            0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
+            0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
+            0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
+            0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
+            0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
+            0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
+            0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
+            0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
+            0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
+            0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
+            0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
+            0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
+            0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
+            0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
+            0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
+            0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
+            0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
+            0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
+            0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
+            0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
+            0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
+            0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
+            0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
+            0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
+            0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
+            0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
+            0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
+    };
+
+    // bcrypt IV: "OrpheanBeholderScryDoubt"
+    static private final int bf_crypt_ciphertext[] = {
+            0x4f727068, 0x65616e42, 0x65686f6c,
+            0x64657253, 0x63727944, 0x6f756274
+    };
+
+    // Table for Base64 encoding
+    static private final char base64_code[] = {
+            '.', '/', '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', '0', '1', '2', '3', '4', '5',
+            '6', '7', '8', '9'
+    };
+
+    // Table for Base64 decoding
+    static private final byte index_64[] = {
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+            -1, -1, -1, -1, -1, -1, 0, 1, 54, 55,
+            56, 57, 58, 59, 60, 61, 62, 63, -1, -1,
+            -1, -1, -1, -1, -1, 2, 3, 4, 5, 6,
+            7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+            17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
+            -1, -1, -1, -1, -1, -1, 28, 29, 30,
+            31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+            41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
+            51, 52, 53, -1, -1, -1, -1, -1
+    };
+
+    // Expanded Blowfish key
+    private int P[];
+    private int S[];
+
+    /**
+     * Encode a byte array using bcrypt's slightly-modified base64
+     * encoding scheme. Note that this is *not* compatible with
+     * the standard MIME-base64 encoding.
+     *
+     * @param d   the byte array to encode
+     * @param len the number of bytes to encode
+     * @return base64-encoded string
+     * @throws IllegalArgumentException if the length is invalid
+     */
+    private static String encode_base64(byte d[], int len)
+            throws IllegalArgumentException {
+        int off = 0;
+        StringBuilder rs = new StringBuilder();
+        int c1, c2;
+
+        if (len <= 0 || len > d.length) {
+            throw new IllegalArgumentException("Invalid len");
+        }
+
+        while (off < len) {
+            c1 = d[off++] & 0xff;
+            rs.append(base64_code[(c1 >> 2) & 0x3f]);
+            c1 = (c1 & 0x03) << 4;
+            if (off >= len) {
+                rs.append(base64_code[c1 & 0x3f]);
+                break;
+            }
+            c2 = d[off++] & 0xff;
+            c1 |= (c2 >> 4) & 0x0f;
+            rs.append(base64_code[c1 & 0x3f]);
+            c1 = (c2 & 0x0f) << 2;
+            if (off >= len) {
+                rs.append(base64_code[c1 & 0x3f]);
+                break;
+            }
+            c2 = d[off++] & 0xff;
+            c1 |= (c2 >> 6) & 0x03;
+            rs.append(base64_code[c1 & 0x3f]);
+            rs.append(base64_code[c2 & 0x3f]);
+        }
+        return rs.toString();
+    }
+
+    /**
+     * Look up the 3 bits base64-encoded by the specified character,
+     * range-checking againt conversion table
+     *
+     * @param x the base64-encoded value
+     * @return the decoded value of x
+     */
+    private static byte char64(char x) {
+        if ((int) x < 0 || (int) x > index_64.length)
+            return -1;
+        return index_64[(int) x];
+    }
+
+    /**
+     * Decode a string encoded using bcrypt's base64 scheme to a
+     * byte array. Note that this is *not* compatible with
+     * the standard MIME-base64 encoding.
+     *
+     * @param s       the string to decode
+     * @param maxolen the maximum number of bytes to decode
+     * @return an array containing the decoded bytes
+     * @throws IllegalArgumentException if maxolen is invalid
+     */
+    private static byte[] decode_base64(String s, int maxolen)
+            throws IllegalArgumentException {
+        StringBuilder rs = new StringBuilder();
+        int off = 0, slen = s.length(), olen = 0;
+        byte ret[];
+        byte c1, c2, c3, c4, o;
+
+        if (maxolen <= 0) {
+            throw new IllegalArgumentException("Invalid maxolen");
+        }
+
+        while (off < slen - 1 && olen < maxolen) {
+            c1 = char64(s.charAt(off++));
+            c2 = char64(s.charAt(off++));
+            if (c1 == -1 || c2 == -1) {
+                break;
+            }
+            o = (byte) (c1 << 2);
+            o |= (c2 & 0x30) >> 4;
+            rs.append((char) o);
+            if (++olen >= maxolen || off >= slen) {
+                break;
+            }
+            c3 = char64(s.charAt(off++));
+            if (c3 == -1) {
+                break;
+            }
+            o = (byte) ((c2 & 0x0f) << 4);
+            o |= (c3 & 0x3c) >> 2;
+            rs.append((char) o);
+            if (++olen >= maxolen || off >= slen) {
+                break;
+            }
+            c4 = char64(s.charAt(off++));
+            o = (byte) ((c3 & 0x03) << 6);
+            o |= c4;
+            rs.append((char) o);
+            ++olen;
+        }
+
+        ret = new byte[olen];
+        for (off = 0; off < olen; off++) {
+            ret[off] = (byte) rs.charAt(off);
+        }
+        return ret;
+    }
+
+    /**
+     * Blowfish encipher a single 64-bit block encoded as
+     * two 32-bit halves
+     *
+     * @param lr  an array containing the two 32-bit half blocks
+     * @param off the position in the array of the blocks
+     */
+    private final void encipher(int lr[], int off) {
+        int i, n, l = lr[off], r = lr[off + 1];
+
+        l ^= P[0];
+        for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2; ) {
+            // Feistel substitution on left word
+            n = S[(l >> 24) & 0xff];
+            n += S[0x100 | ((l >> 16) & 0xff)];
+            n ^= S[0x200 | ((l >> 8) & 0xff)];
+            n += S[0x300 | (l & 0xff)];
+            r ^= n ^ P[++i];
+
+            // Feistel substitution on right word
+            n = S[(r >> 24) & 0xff];
+            n += S[0x100 | ((r >> 16) & 0xff)];
+            n ^= S[0x200 | ((r >> 8) & 0xff)];
+            n += S[0x300 | (r & 0xff)];
+            l ^= n ^ P[++i];
+        }
+        lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1];
+        lr[off + 1] = l;
+    }
+
+    /**
+     * Cycically extract a word of key material
+     *
+     * @param data  the string to extract the data from
+     * @param offp  a "pointer" (as a one-entry array) to the
+     *              current offset into data
+     * @param signp a "pointer" (as a one-entry array) to the
+     *              cumulative flag for non-benign sign extension
+     * @return correct and buggy next word of material from data as int[2]
+     */
+    private static int[] streamtowords(byte data[], int offp[], int signp[]) {
+        int i;
+        int words[] = {0, 0};
+        int off = offp[0];
+        int sign = signp[0];
+
+        for (i = 0; i < 4; i++) {
+            words[0] = (words[0] << 8) | (data[off] & 0xff);
+            words[1] = (words[1] << 8) | (int) data[off]; // sign extension bug
+            if (i > 0) {
+                sign |= words[1] & 0x80;
+            }
+            off = (off + 1) % data.length;
+        }
+
+        offp[0] = off;
+        signp[0] = sign;
+        return words;
+    }
+
+    /**
+     * Cycically extract a word of key material
+     *
+     * @param data the string to extract the data from
+     * @param offp a "pointer" (as a one-entry array) to the
+     *             current offset into data
+     * @return the next word of material from data
+     */
+    private static int streamtoword(byte data[], int offp[]) {
+        int signp[] = {0};
+        return streamtowords(data, offp, signp)[0];
+    }
+
+    /**
+     * Cycically extract a word of key material, with sign-extension bug
+     *
+     * @param data the string to extract the data from
+     * @param offp a "pointer" (as a one-entry array) to the
+     *             current offset into data
+     * @return the next word of material from data
+     */
+    private static int streamtoword_bug(byte data[], int offp[]) {
+        int signp[] = {0};
+        return streamtowords(data, offp, signp)[1];
+    }
+
+    /**
+     * Initialise the Blowfish key schedule
+     */
+    private void init_key() {
+        P = (int[]) P_orig.clone();
+        S = (int[]) S_orig.clone();
+    }
+
+    /**
+     * Key the Blowfish cipher
+     *
+     * @param key          an array containing the key
+     * @param sign_ext_bug true to implement the 2x bug
+     * @param safety       bit 16 is set when the safety measure is requested
+     */
+    private void key(byte key[], boolean sign_ext_bug, int safety) {
+        int i;
+        int koffp[] = {0};
+        int lr[] = {0, 0};
+        int plen = P.length, slen = S.length;
+
+        for (i = 0; i < plen; i++) {
+            if (!sign_ext_bug) {
+                P[i] = P[i] ^ streamtoword(key, koffp);
+            } else {
+                P[i] = P[i] ^ streamtoword_bug(key, koffp);
+            }
+        }
+
+        for (i = 0; i < plen; i += 2) {
+            encipher(lr, 0);
+            P[i] = lr[0];
+            P[i + 1] = lr[1];
+        }
+
+        for (i = 0; i < slen; i += 2) {
+            encipher(lr, 0);
+            S[i] = lr[0];
+            S[i + 1] = lr[1];
+        }
+    }
+
+    /**
+     * Perform the "enhanced key schedule" step described by
+     * Provos and Mazieres in "A Future-Adaptable Password Scheme"
+     * http://www.openbsd.org/papers/bcrypt-paper.ps
+     *
+     * @param data         salt information
+     * @param key          password information
+     * @param sign_ext_bug true to implement the 2x bug
+     * @param safety       bit 16 is set when the safety measure is requested
+     */
+    private void ekskey(byte data[], byte key[],
+                        boolean sign_ext_bug, int safety) {
+        int i;
+        int koffp[] = {0}, doffp[] = {0};
+        int lr[] = {0, 0};
+        int plen = P.length, slen = S.length;
+        int signp[] = {0}; // non-benign sign-extension flag
+        int diff = 0;        // zero iff correct and buggy are same
+
+        for (i = 0; i < plen; i++) {
+            int words[] = streamtowords(key, koffp, signp);
+            diff |= words[0] ^ words[1];
+            P[i] = P[i] ^ words[sign_ext_bug ? 1 : 0];
+        }
+
+        int sign = signp[0];
+
+        /*
+         * At this point, "diff" is zero iff the correct and buggy algorithms produced
+         * exactly the same result.  If so and if "sign" is non-zero, which indicates
+         * that there was a non-benign sign extension, this means that we have a
+         * collision between the correctly computed hash for this password and a set of
+         * passwords that could be supplied to the buggy algorithm.  Our safety measure
+         * is meant to protect from such many-buggy to one-correct collisions, by
+         * deviating from the correct algorithm in such cases.  Let's check for this.
+         */
+        diff |= diff >> 16; /* still zero iff exact match */
+        diff &= 0xffff; /* ditto */
+        diff += 0xffff; /* bit 16 set iff "diff" was non-zero (on non-match) */
+        sign <<= 9; /* move the non-benign sign extension flag to bit 16 */
+        sign &= ~diff & safety; /* action needed? */
+
+        /*
+         * If we have determined that we need to deviate from the correct algorithm,
+         * flip bit 16 in initial expanded key.  (The choice of 16 is arbitrary, but
+         * let's stick to it now.  It came out of the approach we used above, and it's
+         * not any worse than any other choice we could make.)
+         *
+         * It is crucial that we don't do the same to the expanded key used in the main
+         * Eksblowfish loop.  By doing it to only one of these two, we deviate from a
+         * state that could be directly specified by a password to the buggy algorithm
+         * (and to the fully correct one as well, but that's a side-effect).
+         */
+        P[0] ^= sign;
+
+        for (i = 0; i < plen; i += 2) {
+            lr[0] ^= streamtoword(data, doffp);
+            lr[1] ^= streamtoword(data, doffp);
+            encipher(lr, 0);
+            P[i] = lr[0];
+            P[i + 1] = lr[1];
+        }
+
+        for (i = 0; i < slen; i += 2) {
+            lr[0] ^= streamtoword(data, doffp);
+            lr[1] ^= streamtoword(data, doffp);
+            encipher(lr, 0);
+            S[i] = lr[0];
+            S[i + 1] = lr[1];
+        }
+    }
+
+    /**
+     * Perform the central password hashing step in the
+     * bcrypt scheme
+     *
+     * @param password     the password to hash
+     * @param salt         the binary salt to hash with the password
+     * @param log_rounds   the binary logarithm of the number
+     *                     of rounds of hashing to apply
+     * @param sign_ext_bug true to implement the 2x bug
+     * @param safety       bit 16 is set when the safety measure is requested
+     * @return an array containing the binary hashed password
+     */
+    private byte[] crypt_raw(byte password[], byte salt[], int log_rounds,
+                             boolean sign_ext_bug, int safety) {
+        int rounds, i, j;
+        int cdata[] = (int[]) bf_crypt_ciphertext.clone();
+        int clen = cdata.length;
+        byte ret[];
+
+        if (log_rounds < 4 || log_rounds > 31)
+            throw new IllegalArgumentException("Bad number of rounds");
+        rounds = 1 << log_rounds;
+        if (salt.length != BCRYPT_SALT_LEN) {
+            throw new IllegalArgumentException("Bad salt length");
+        }
+
+        init_key();
+        ekskey(salt, password, sign_ext_bug, safety);
+        for (i = 0; i < rounds; i++) {
+            key(password, sign_ext_bug, safety);
+            key(salt, false, safety);
+        }
+
+        for (i = 0; i < 64; i++) {
+            {
+                for (j = 0; j < (clen >> 1); j++) {
+                    encipher(cdata, j << 1);
+                }
+            }
+        }
+
+        ret = new byte[clen * 4];
+        for (i = 0, j = 0; i < clen; i++) {
+            ret[j++] = (byte) ((cdata[i] >> 24) & 0xff);
+            ret[j++] = (byte) ((cdata[i] >> 16) & 0xff);
+            ret[j++] = (byte) ((cdata[i] >> 8) & 0xff);
+            ret[j++] = (byte) (cdata[i] & 0xff);
+        }
+        return ret;
+    }
+
+    /**
+     * Hash a password using the OpenBSD bcrypt scheme
+     *
+     * @param password the password to hash
+     * @param salt     the salt to hash with (perhaps generated
+     *                 using BCrypt.gensalt)
+     * @return the hashed password
+     */
+    public static String hashpw(String password, String salt) {
+        byte passwordb[];
+
+        try {
+            passwordb = password.getBytes("UTF-8");
+        } catch (UnsupportedEncodingException uee) {
+            throw new AssertionError("UTF-8 is not supported");
+        }
+
+        return hashpw(passwordb, salt);
+    }
+
+    /**
+     * Hash a password using the OpenBSD bcrypt scheme
+     *
+     * @param passwordb the password to hash, as a byte array
+     * @param salt      the salt to hash with (perhaps generated
+     *                  using BCrypt.gensalt)
+     * @return the hashed password
+     */
+    public static String hashpw(byte passwordb[], String salt) {
+        BCrypt B;
+        String real_salt;
+        byte saltb[], hashed[];
+        char minor = (char) 0;
+        int rounds, off = 0;
+        StringBuilder rs = new StringBuilder();
+
+        if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
+            throw new IllegalArgumentException("Invalid salt version");
+        }
+        if (salt.charAt(2) == '$') {
+            off = 3;
+        } else {
+            minor = salt.charAt(2);
+            if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b')
+                    || salt.charAt(3) != '$') {
+                throw new IllegalArgumentException("Invalid salt revision");
+            }
+            off = 4;
+        }
+
+        // Extract number of rounds
+        if (salt.charAt(off + 2) > '$') {
+            throw new IllegalArgumentException("Missing salt rounds");
+        }
+        rounds = Integer.parseInt(salt.substring(off, off + 2));
+
+        real_salt = salt.substring(off + 3, off + 25);
+        saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
+
+        if (minor >= 'a') // add null terminator
+        {
+            passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
+        }
+
+        B = new BCrypt();
+        hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);
+
+        rs.append("$2");
+        if (minor >= 'a') {
+            rs.append(minor);
+        }
+        rs.append("$");
+        if (rounds < 10) {
+            rs.append("0");
+        }
+        rs.append(Integer.toString(rounds));
+        rs.append("$");
+        rs.append(encode_base64(saltb, saltb.length));
+        rs.append(encode_base64(hashed,
+                bf_crypt_ciphertext.length * 4 - 1));
+        return rs.toString();
+    }
+
+    /**
+     * Generate a salt for use with the BCrypt.hashpw() method
+     *
+     * @param prefix     the prefix value (default $2y)
+     * @param log_rounds the log2 of the number of rounds of
+     *                   hashing to apply - the work factor therefore increases as
+     *                   2**log_rounds.
+     * @param random     an instance of SecureRandom to use
+     * @return an encoded salt value
+     * @throws IllegalArgumentException if prefix or log_rounds is invalid
+     */
+    public static String gensalt(String prefix, int log_rounds, SecureRandom random)
+            throws IllegalArgumentException {
+        StringBuilder rs = new StringBuilder();
+        byte rnd[] = new byte[BCRYPT_SALT_LEN];
+
+        if (!prefix.startsWith("$2") ||
+                (prefix.charAt(2) != 'a' && prefix.charAt(2) != 'y' &&
+                        prefix.charAt(2) != 'b')) {
+            throw new IllegalArgumentException("Invalid prefix");
+        }
+        if (log_rounds < 4 || log_rounds > 31) {
+            throw new IllegalArgumentException("Invalid log_rounds");
+        }
+
+        random.nextBytes(rnd);
+
+        rs.append("$2");
+        rs.append(prefix.charAt(2));
+        rs.append("$");
+        if (log_rounds < 10) {
+            rs.append("0");
+        }
+        rs.append(Integer.toString(log_rounds));
+        rs.append("$");
+        rs.append(encode_base64(rnd, rnd.length));
+        return rs.toString();
+    }
+
+    /**
+     * Generate a salt for use with the BCrypt.hashpw() method
+     *
+     * @param prefix     the prefix value (default $2y)
+     * @param log_rounds the log2 of the number of rounds of
+     *                   hashing to apply - the work factor therefore increases as
+     *                   2**log_rounds.
+     * @return an encoded salt value
+     * @throws IllegalArgumentException if prefix or log_rounds is invalid
+     */
+    public static String gensalt(String prefix, int log_rounds)
+            throws IllegalArgumentException {
+        return gensalt(prefix, log_rounds, new SecureRandom());
+    }
+
+    /**
+     * Generate a salt for use with the BCrypt.hashpw() method
+     *
+     * @param log_rounds the log2 of the number of rounds of
+     *                   hashing to apply - the work factor therefore increases as
+     *                   2**log_rounds.
+     * @param random     an instance of SecureRandom to use
+     * @return an encoded salt value
+     * @throws IllegalArgumentException if log_rounds is invalid
+     */
+    public static String gensalt(int log_rounds, SecureRandom random)
+            throws IllegalArgumentException {
+        return gensalt("$2y", log_rounds, random);
+    }
+
+    /**
+     * Generate a salt for use with the BCrypt.hashpw() method
+     *
+     * @param log_rounds the log2 of the number of rounds of
+     *                   hashing to apply - the work factor therefore increases as
+     *                   2**log_rounds.
+     * @return an encoded salt value
+     * @throws IllegalArgumentException if log_rounds is invalid
+     */
+    public static String gensalt(int log_rounds)
+            throws IllegalArgumentException {
+        return gensalt(log_rounds, new SecureRandom());
+    }
+
+    /**
+     * Generate a salt for use with the BCrypt.hashpw() method,
+     * selecting a reasonable default for the number of hashing
+     * rounds to apply
+     *
+     * @return an encoded salt value
+     */
+    public static String gensalt() {
+        return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);
+    }
+
+    /**
+     * Check that a plaintext password matches a previously hashed
+     * one
+     *
+     * @param plaintext the plaintext password to verify
+     * @param hashed    the previously-hashed password
+     * @return true if the passwords match, false otherwise
+     */
+    public static boolean checkpw(String plaintext, String hashed) {
+        return (hashed.compareTo(hashpw(plaintext, hashed)) == 0);
+    }
+
+    /**
+     * Check that a plaintext byte[] password matches a previously hashed
+     * one
+     *
+     * @param plaintext the plaintext password to verify
+     * @param hashed    the previously-hashed password
+     * @return true if the passwords match, false otherwise
+     */
+    public static boolean checkpw(byte[] plaintext, String hashed) {
+        return (hashed.compareTo(hashpw(plaintext, hashed)) == 0);
+    }
+
+
+}

+ 50 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/Base64Util.java

@@ -0,0 +1,50 @@
+package com.crm.rely.backend.util;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Base64;
+
+/**
+ * @program: cwg-my-base
+ * @description:
+ * @author: houn
+ * @create: 2019-08-02 09:52
+ */
+public class Base64Util {
+
+    public static String toBase64String(String str) {
+
+        String base64Sign = null;
+        try {
+            base64Sign = Base64.getEncoder().encodeToString(str.getBytes("UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+
+        return base64Sign;
+    }
+
+    public static String toBase64String(byte[] byteArray) {
+
+        String base64Str = Base64.getEncoder().encodeToString(byteArray);
+
+        return base64Str;
+    }
+
+    public static byte[] toByteArray(String str) {
+
+        byte[] byteArray = Base64.getDecoder().decode(str);
+
+        return byteArray;
+    }
+
+    public static String toBase64DecoderString(byte[] byteArray) {
+        String base64Decodedr = null;
+        try {
+            base64Decodedr = new String(byteArray);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return base64Decodedr;
+    }
+}

+ 130 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/BaseExpireMap.java

@@ -0,0 +1,130 @@
+package com.crm.rely.backend.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @Author: houn
+ * @Date: 2020/12/22 23:57
+ * @Description:
+ */
+
+public abstract class BaseExpireMap<K, V> {
+    protected static final Logger logger = LoggerFactory.getLogger(BaseExpireMap.class);
+    private long expTime = 0L;
+    private TimeUnit unit = null;
+    /**
+     * 线程安全的map容器
+     */
+    ConcurrentHashMap<K, V> expireMap = null;
+    /**
+     * 控制过期时间
+     */
+    ConcurrentHashMap<K, Long> delayMap = null;
+
+    /**
+     * 将map提供给外部程序操作
+     * @Title: getDataMap
+     * @Description: TODO
+     * @return
+     * @return: ConcurrentHashMap<K,V>
+     */
+    public ConcurrentHashMap<K, V> getDataMap(){
+        return this.expireMap;
+    }
+
+    public BaseExpireMap(long expTime, TimeUnit unit) {
+        expireMap = new ConcurrentHashMap<K, V>();
+        delayMap = new ConcurrentHashMap<K, Long>();
+        this.expTime = expTime;
+        this.unit = unit;
+        // 启动监听线程
+        BaseExpireCheckTask task = new BaseExpireCheckTask(expireMap, delayMap) {
+            @Override
+            protected void expireEvent(K key,V val) {
+                baseExpireEvent(key,val);
+            }
+        };
+        task.setDaemon(false);
+        task.start();
+    }
+
+    /**
+     * 过期事件 子类实现
+     *
+     * @Title: baseExpireEvent
+     * @Description: TODO
+     * @param key
+     * @return: void
+     */
+    protected abstract void baseExpireEvent(K key,V val);
+
+    public V put(K key, V val) {
+        delayMap.put(key, getExpireTime());
+        return expireMap.put(key, val);
+    }
+
+    public V remove(K key) {
+        return expireMap.remove(key);
+    }
+
+    public V get(K key){
+        return expireMap.get(key);
+    }
+
+    private Long getExpireTime() {
+        return unit.toMillis(expTime) + System.currentTimeMillis();
+    }
+
+    public static void main(String[] args) {
+        System.out.println(TimeUnit.SECONDS.toMinutes(120));
+        System.out.println(TimeUnit.MICROSECONDS.toMillis(120));
+        System.out.println(TimeUnit.MILLISECONDS.toMillis(120));
+    }
+
+    /**
+     * 扫描线程 定期移除过期元素并触发过期事件
+     *
+     */
+    private abstract class BaseExpireCheckTask extends Thread {
+        ConcurrentHashMap<K, Long> delayMap = null;
+        ConcurrentHashMap<K, V> expireMap = null;
+
+        public BaseExpireCheckTask(ConcurrentHashMap<K, V> expireMap, ConcurrentHashMap<K, Long> delayMap) {
+            this.delayMap = delayMap;
+            this.expireMap = expireMap;
+        }
+
+        protected abstract void expireEvent(K key,V val);
+
+        public void run() {
+            Iterator<K> it = null;
+            K key = null;
+            while (true) {
+                if (delayMap != null && !delayMap.isEmpty()) {
+                    it = delayMap.keySet().iterator();
+                    while (it.hasNext()) {
+                        key = it.next();
+                        if (delayMap.get(key) <= System.currentTimeMillis()) {// 元素超时
+                            // 触发回调
+                            expireEvent(key,expireMap.get(key));
+                            // 移除
+                            it.remove();
+                            expireMap.remove(key);
+                            delayMap.remove(key);
+                        }
+                    }
+                }
+                try {
+                    TimeUnit.MILLISECONDS.sleep(200);
+                } catch (InterruptedException e) {
+                    logger.error(e.getMessage());
+                }
+            }
+        }
+    }
+}

+ 36 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/BeanUtilPlus.java

@@ -0,0 +1,36 @@
+package com.crm.rely.backend.util;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+
+import java.beans.PropertyDescriptor;
+import java.util.HashSet;
+import java.util.Set;
+
+public class BeanUtilPlus {
+    /**
+     * 复制非 null 属性
+     */
+    public static void copyPropertiesIgnoreNull(Object source, Object target) {
+        BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
+    }
+
+    /**
+     * 获取所有为 null 的属性名
+     */
+    private static String[] getNullPropertyNames(Object source) {
+        final BeanWrapper src = new BeanWrapperImpl(source);
+        PropertyDescriptor[] pds = src.getPropertyDescriptors();
+
+        Set<String> emptyNames = new HashSet<>();
+        for (PropertyDescriptor pd : pds) {
+            Object srcValue = src.getPropertyValue(pd.getName());
+            if (srcValue == null) {
+                emptyNames.add(pd.getName());
+            }
+        }
+        String[] result = new String[emptyNames.size()];
+        return emptyNames.toArray(result);
+    }
+}

+ 10 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/BigDecimalUtil.java

@@ -0,0 +1,10 @@
+package com.crm.rely.backend.util;
+
+import java.math.BigDecimal;
+
+public class BigDecimalUtil {
+
+    public static String toStripTrailingZerosString(BigDecimal decimal) {
+        return decimal == null ? "" : decimal.stripTrailingZeros().toPlainString();
+    }
+}

+ 38 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/CidUtil.java

@@ -0,0 +1,38 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.configuration.ApplicationContextProvider;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.RedisService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @description:
+ * @author: houn
+ * @create: 2020-07-08 14:25
+ **/
+public class CidUtil {
+    @Autowired
+    private static RedisService redisService;
+
+    public static Integer getCid() throws InterruptedException, ServiceException {
+        if (redisService == null) {
+            redisService = ApplicationContextProvider.getBean(RedisService.class);
+        }
+        String uuid = UUIDUtil.getUUID();
+        Integer autoNumber = null;
+        try {
+            redisService.tryLock(Constants.CID_CONTROL_AUTO_NUMBER, uuid);
+            autoNumber = redisService.getObject(Constants.CID_CONTROL_AUTO_NUMBER);
+            if (autoNumber == null) {
+                autoNumber = 0;
+            }
+            autoNumber++;
+            redisService.saveObject(Constants.CID_CONTROL_AUTO_NUMBER, autoNumber);
+        } finally {
+            redisService.unlock(Constants.CID_CONTROL_AUTO_NUMBER, uuid);
+        }
+        
+        return autoNumber;
+    }
+}

+ 30 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/CmdUtil.java

@@ -0,0 +1,30 @@
+package com.crm.rely.backend.util;
+
+/**
+ * Created by max on 2020/4/1.
+ */
+public class CmdUtil {
+
+    public static String translateCmd(Integer cmd) {
+        if (cmd.equals(0)) {
+            return "buy";
+        } else if (cmd.equals(1)) {
+            return "sell";
+        } else if (cmd.equals(2)) {
+            return "buy limit";
+        } else if (cmd.equals(3)) {
+            return "sell limit";
+        } else if (cmd.equals(4)) {
+            return "buy stop";
+        } else if (cmd.equals(5)) {
+            return "sell stop";
+        } else if (cmd.equals(6)) {
+            return "balance";
+        } else if (cmd.equals(7)) {
+            return "credit";
+        } else {
+            return "";
+        }
+    }
+
+}

+ 124 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/CompareUtil.java

@@ -0,0 +1,124 @@
+package com.crm.rely.backend.util;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @program: crm-backend
+ * @description:
+ * @author: houn
+ * @create: 2019-08-01 11:14
+ */
+public class CompareUtil {
+    public static List<Difference> compareValue(Object objA) {
+        List<Difference> differAttr = new ArrayList<>();
+        try {
+            Class<?> clazzA = objA.getClass();
+            Method[] methods = clazzA.getMethods();
+            Object result;
+            for (Method method : methods) {
+                Difference difference = new Difference();
+                if (method.getName().startsWith("get")) {
+                    result = method.invoke(objA, null);
+                    if (result == null) {
+                        continue;
+                    }
+                    if (result instanceof List) {
+                        List<Object> childList = (List) result;
+                        for (Object object : childList) {
+                            difference.setChildrenDifference(compareValue(object));
+                        }
+                    } else {
+                        String nameTrim = method.getName().substring(3);
+                        nameTrim = nameTrim.substring(0, 1).toLowerCase() + nameTrim.substring(1);
+                        difference.setAttr(nameTrim);
+                        difference.setAfter(result.toString());
+                    }
+                    differAttr.add(difference);
+                }
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return differAttr;
+    }
+
+    public static List<Difference> compareValue(Object objA, Object objB) {
+        List<Difference> differenceAttr = new ArrayList<>();
+        //值都为空,则返回
+        if (objA == null && objB == null) {
+            return null;
+        }
+        //objA不为空,objB为空,则把返回objA所有字段
+        if (objA != null && objB == null) {
+            return compareValue(objA);
+        }
+
+        try {
+            Class<?> clazzA = objA.getClass();
+            Class<?> clazzB = objB.getClass();
+            Method[] methods = clazzA.getMethods();
+            Object resultA = null;
+            Object resultB = null;
+            Method methodB = null;
+            for (Method method : methods) {
+                if (method.getName().startsWith("get")) {
+                    methodB = clazzB.getMethod(method.getName(), null);
+                    resultB = methodB.invoke(objB, null);
+                    resultA = method.invoke(objA, null);
+                    if (resultA == null && resultB == null) {
+                        continue;
+                    }
+                    Difference difference = new Difference();
+                    String nameTrim = method.getName().substring(3);
+                    nameTrim = nameTrim.substring(0, 1).toLowerCase() + nameTrim.substring(1);
+                    boolean isDifference = false;
+                    if (resultA == null && resultB != null) {
+                        difference.setAttr(nameTrim);
+                        difference.setBefore(resultB);
+                        isDifference = true;
+                    } else if (resultA != null && resultB == null) {
+                        difference.setAttr(nameTrim);
+                        difference.setAfter(resultA);
+                        isDifference = true;
+                    } else {
+                        if (!(resultA instanceof List)) {
+                            if (!resultA.equals(resultB)) {
+                                difference.setAttr(nameTrim);
+                                difference.setAfter(resultA);
+                                difference.setBefore(resultB);
+                                isDifference = true;
+                            }
+                        } else {
+                            List<Object> listA = (List<Object>) resultA;
+                            List<Object> listB = (List<Object>) resultB;
+
+                            for (int i = 0; i < listA.size(); i++) {
+                                List<Difference> childAttr;
+                                if (i >= listB.size()) {
+                                    childAttr = compareValue(listA.get(i), null);
+                                } else {
+                                    childAttr = compareValue(listA.get(i), listB.get(i));
+                                }
+                                difference.setChildrenDifference(childAttr);
+                                isDifference = true;
+                            }
+                        }
+                    }
+                    if (isDifference) {
+
+                        differenceAttr.add(difference);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        if (differenceAttr == null || differenceAttr.size() <= 0) {
+            return null;
+        }
+        return differenceAttr;
+    }
+}

+ 263 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/ConcurrentHashMapCacheUtils.java

@@ -0,0 +1,263 @@
+package com.crm.rely.backend.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @Author: houn
+ * @Date: 2020/12/23 0:11
+ * @Description:
+ */
+
+public class ConcurrentHashMapCacheUtils {
+
+    private static Logger LOGGER = LoggerFactory.getLogger(ConcurrentHashMapCacheUtils.class);
+
+    /**
+     * 缓存最大个数
+     */
+    private static final Integer CACHE_MAX_NUMBER = 1000;
+    /**
+     * 当前缓存个数
+     */
+    private static Integer CURRENT_SIZE = 0;
+    /**
+     * 时间一分钟
+     */
+    static final Long ONE_MINUTE = 60 * 1000L;
+    /**
+     * 缓存对象
+     */
+    private static final Map<String, CacheObj> CACHE_OBJECT_MAP = new ConcurrentHashMap<>();
+    /**
+     * 这个记录了缓存使用的最后一次的记录,最近使用的在最前面
+     */
+    private static final List<String> CACHE_USE_LOG_LIST = new LinkedList<>();
+    /**
+     * 清理过期缓存是否在运行
+     */
+    private static volatile Boolean CLEAN_THREAD_IS_RUN = false;
+
+
+    /**
+     * 设置缓存
+     */
+    public static void setCache(String cacheKey, Object cacheValue, long cacheTime) {
+        Long ttlTime = null;
+        if (cacheTime <= 0L) {
+            if (cacheTime == -1L) {
+                ttlTime = -1L;
+            } else {
+                return;
+            }
+        }
+        checkSize();
+        saveCacheUseLog(cacheKey);
+        CURRENT_SIZE = CURRENT_SIZE + 1;
+        if (ttlTime == null) {
+            ttlTime = System.currentTimeMillis() + cacheTime;
+        }
+        CacheObj cacheObj = new CacheObj(cacheValue, ttlTime);
+        CACHE_OBJECT_MAP.put(cacheKey, cacheObj);
+        LOGGER.info("have set key :" + cacheKey);
+    }
+
+    /**
+     * 设置缓存
+     */
+    public static void setCache(String cacheKey, Object cacheValue) {
+        setCache(cacheKey, cacheValue, -1L);
+    }
+
+    /**
+     * 获取缓存
+     */
+    public static Object getCache(String cacheKey) {
+        startCleanThread();
+        if (checkCache(cacheKey)) {
+            saveCacheUseLog(cacheKey);
+            return CACHE_OBJECT_MAP.get(cacheKey).getCacheValue();
+        }
+        return null;
+    }
+
+    public static boolean isExist(String cacheKey) {
+        return checkCache(cacheKey);
+    }
+
+    /**
+     * 删除所有缓存
+     */
+    public static void clear() {
+        LOGGER.info("have clean all key !");
+        CACHE_OBJECT_MAP.clear();
+        CURRENT_SIZE = 0;
+    }
+
+    /**
+     * 删除某个缓存
+     */
+    public static void deleteCache(String cacheKey) {
+        Object cacheValue = CACHE_OBJECT_MAP.remove(cacheKey);
+        if (cacheValue != null) {
+            LOGGER.info("have delete key :" + cacheKey);
+            CURRENT_SIZE = CURRENT_SIZE - 1;
+        }
+    }
+
+    /**
+     * 判断缓存在不在,过没过期
+     */
+    private static boolean checkCache(String cacheKey) {
+        CacheObj cacheObj = CACHE_OBJECT_MAP.get(cacheKey);
+        if (cacheObj == null) {
+            return false;
+        }
+        if (cacheObj.getTtlTime() == -1L) {
+            return true;
+        }
+        if (cacheObj.getTtlTime() < System.currentTimeMillis()) {
+            deleteCache(cacheKey);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 删除最近最久未使用的缓存
+     */
+    private static void deleteLRU() {
+        LOGGER.info("delete Least recently used run!");
+        String cacheKey = null;
+        synchronized (CACHE_USE_LOG_LIST) {
+            if (CACHE_USE_LOG_LIST.size() >= CACHE_MAX_NUMBER - 10) {
+                cacheKey = CACHE_USE_LOG_LIST.remove(CACHE_USE_LOG_LIST.size() - 1);
+            }
+        }
+        if (cacheKey != null) {
+            deleteCache(cacheKey);
+        }
+    }
+
+    /**
+     * 删除过期的缓存
+     */
+    static void deleteTimeOut() {
+        LOGGER.info("delete time out run!");
+        List<String> deleteKeyList = new LinkedList<>();
+        for (Map.Entry<String, CacheObj> entry : CACHE_OBJECT_MAP.entrySet()) {
+            if (entry.getValue().getTtlTime() < System.currentTimeMillis() && entry.getValue().getTtlTime() != -1L) {
+                deleteKeyList.add(entry.getKey());
+            }
+        }
+
+        for (String deleteKey : deleteKeyList) {
+            deleteCache(deleteKey);
+        }
+        LOGGER.info("delete cache count is :" + deleteKeyList.size());
+
+    }
+
+    /**
+     * 检查大小
+     * 当当前大小如果已经达到最大大小
+     * 首先删除过期缓存,如果过期缓存删除过后还是达到最大缓存数目
+     * 删除最久未使用缓存
+     */
+    private static void checkSize() {
+        if (CURRENT_SIZE >= CACHE_MAX_NUMBER) {
+            deleteTimeOut();
+        }
+        if (CURRENT_SIZE >= CACHE_MAX_NUMBER) {
+            deleteLRU();
+        }
+    }
+
+    /**
+     * 保存缓存的使用记录
+     */
+    private static synchronized void saveCacheUseLog(String cacheKey) {
+        synchronized (CACHE_USE_LOG_LIST) {
+            CACHE_USE_LOG_LIST.remove(cacheKey);
+            CACHE_USE_LOG_LIST.add(0, cacheKey);
+        }
+    }
+
+    /**
+     * 设置清理线程的运行状态为正在运行
+     */
+    static void setCleanThreadRun() {
+        CLEAN_THREAD_IS_RUN = true;
+    }
+
+    /**
+     * 开启清理过期缓存的线程
+     */
+    private static void startCleanThread() {
+        if (!CLEAN_THREAD_IS_RUN) {
+            CleanTimeOutThread cleanTimeOutThread = new CleanTimeOutThread();
+            Thread thread = new Thread(cleanTimeOutThread);
+            //设置为后台守护线程
+            thread.setDaemon(true);
+            thread.start();
+        }
+    }
+
+}
+
+class CacheObj {
+    /**
+     * 缓存对象
+     */
+    private Object CacheValue;
+    /**
+     * 缓存过期时间
+     */
+    private Long ttlTime;
+
+    CacheObj(Object cacheValue, Long ttlTime) {
+        CacheValue = cacheValue;
+        this.ttlTime = ttlTime;
+    }
+
+    Object getCacheValue() {
+        return CacheValue;
+    }
+
+    Long getTtlTime() {
+        return ttlTime;
+    }
+
+    @Override
+    public String toString() {
+        return "CacheObj {" +
+                "CacheValue = " + CacheValue +
+                ", ttlTime = " + ttlTime +
+                '}';
+    }
+}
+
+/**
+ * 每一分钟清理一次过期缓存
+ */
+class CleanTimeOutThread implements Runnable {
+
+    @Override
+    public void run() {
+        ConcurrentHashMapCacheUtils.setCleanThreadRun();
+        while (true) {
+            System.out.println("clean thread run ");
+            ConcurrentHashMapCacheUtils.deleteTimeOut();
+            try {
+                Thread.sleep(ConcurrentHashMapCacheUtils.ONE_MINUTE);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

+ 28 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/CountryUtil.java

@@ -0,0 +1,28 @@
+package com.crm.rely.backend.util;
+
+public class CountryUtil {
+
+    public static boolean isCN(String country) {
+        return "CN".equals(country);
+    }
+
+    public static boolean isTW(String country) {
+        return "TW".equals(country);
+    }
+
+    public static boolean isHK(String country) {
+        return "HK".equals(country);
+    }
+
+    public static boolean isMO(String country) {
+        return "MO".equals(country);
+    }
+
+    public static boolean isCTHM(String country) {
+        return isCN(country) || isTW(country) || isHK(country) || isMO(country);
+    }
+
+    public static boolean isChina(String country) {
+        return "China".equals(country);
+    }
+}

+ 21 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/CurrencySymbolUtil.java

@@ -0,0 +1,21 @@
+package com.crm.rely.backend.util;
+
+public class CurrencySymbolUtil {
+
+
+    public static String getSymbolByCurrency(String currency) {
+
+        if ("USD".equalsIgnoreCase(currency)) {
+            return "$";
+        } else if ("EUR".equalsIgnoreCase(currency)) {
+            return "€";
+        } else if ("GBP".equalsIgnoreCase(currency)) {
+            return "£";
+        } else if ("USC".equalsIgnoreCase(currency)) {
+            return "¢";
+        } else {
+            return "$";
+        }
+    }
+
+}

+ 220 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/CustomInfoUtil.java

@@ -0,0 +1,220 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.core.entity.custom.info.CustomInfoEntity;
+import com.crm.rely.backend.core.pojo.table.CustomInfoTable;
+import com.crm.rely.backend.core.pojo.view.CustomAccountExportView;
+import com.crm.rely.backend.core.pojo.view.CustomInfoGetCidView;
+import com.crm.rely.backend.core.pojo.view.CustomInfoGetEmailView;
+import com.crm.rely.backend.core.pojo.view.CustomView;
+import com.google.common.base.Strings;
+
+public class CustomInfoUtil {
+
+
+    /**
+     * ase 正序( firstName middle lastName) desc 倒序 正序(lastName middle firstName)
+     */
+    public static final String ORDER_ASE = "ASE";
+
+    public static final String ORDER_DESC = "DESC";
+
+    /**
+     * 获取客户姓名
+     *
+     * @param table
+     * @return
+     */
+    public static String getName(CustomInfoTable table) {
+        if (CountryUtil.isCTHM(table.getCountry())) {
+            if ((!Strings.isNullOrEmpty(table.getFirstName()) && table.getFirstName().contains(".")) || (!Strings.isNullOrEmpty(table.getLastName()) && table.getLastName().contains(".")) || ("" + table.getFirstName() + table.getLastName()).length() >= 5) {
+                return getName(table, ORDER_DESC);
+            } else {
+                return getName(table, ORDER_DESC);
+            }
+        } else {
+            return getName(table, ORDER_ASE);
+        }
+
+    }
+
+    /**
+     * @param table
+     * @param order 顺序 ace 正序( firstName middle lastName) desc 倒序 正序(lastName middle firstName)
+     * @return
+     */
+    public static String getName(CustomInfoTable table, String order) {
+        if (table == null) {
+            return null;
+        }
+        return getName(table.getFirstName(), table.getMiddle(), table.getLastName(), null, order);
+    }
+
+    public static String getName(CustomAccountExportView view) {
+        if (CountryUtil.isCTHM(view.getCountry())) {
+            return getName(view, ORDER_DESC);
+        } else {
+            return getName(view, ORDER_ASE);
+        }
+    }
+
+    public static String getName(CustomAccountExportView view, String order) {
+        if (view == null) {
+            return null;
+        }
+        return getName(view.getFirstName(), view.getMiddle(), view.getLastName(), null, order);
+    }
+
+    public static String getName(CustomInfoEntity entity) {
+        if (CountryUtil.isCTHM(entity.getCountry())) {
+            return getName(entity, ORDER_DESC);
+        } else {
+            return getName(entity, ORDER_ASE);
+        }
+    }
+
+    public static String getName(CustomInfoEntity entity, String order) {
+        if (entity == null) {
+            return null;
+        }
+        return getName(entity.getFirstName(), entity.getMiddle(), entity.getLastName(), null, order);
+    }
+
+    public static String getName(CustomView view) {
+        if (CountryUtil.isCTHM(view.getCountry())) {
+            return getName(view, ORDER_DESC);
+        } else {
+            return getName(view, ORDER_ASE);
+        }
+    }
+
+    public static String getName(CustomView view, String order) {
+        if (view == null) {
+            return null;
+        }
+        return getName(view.getFirstName(), view.getMiddle(), view.getLastName(), null, order);
+    }
+
+    public static String getName(CustomInfoGetEmailView view) {
+        if (CountryUtil.isCTHM(view.getCountry())) {
+            return getName(view, ORDER_DESC);
+        } else {
+            return getName(view, ORDER_ASE);
+        }
+
+    }
+
+    public static String getName(CustomInfoGetEmailView view, String order) {
+        if (view == null) {
+            return null;
+        }
+        return getName(view.getFirstName(), view.getMiddle(), view.getLastName(), null, order);
+    }
+
+    public static String getName(CustomInfoGetCidView view) {
+        if (CountryUtil.isCTHM(view.getCountry())) {
+            return getName(view, ORDER_DESC);
+        } else {
+            return getName(view, ORDER_ASE);
+        }
+    }
+
+    public static String getName(CustomInfoGetCidView view, String order) {
+        if (view == null) {
+            return null;
+        }
+        return getName(view.getFirstName(), view.getMiddle(), view.getLastName(), null, order);
+    }
+
+    public static String getName(String firstName, String middle, String lastName) {
+        return getName(firstName, middle, lastName, " ");
+    }
+
+    public static String getNameByCountry(String firstName, String middle, String lastName, String country) {
+        if (CountryUtil.isCTHM(country)) {
+            return getName(firstName, middle, lastName, null, ORDER_DESC);
+        } else {
+            return getName(firstName, middle, lastName, null, ORDER_ASE);
+
+        }
+    }
+
+    public static String getName(String firstName, String middle, String lastName, String fill) {
+        return getName(firstName, middle, lastName, fill, null);
+    }
+
+    public static String getName(String firstName, String middle, String lastName, String fill, String order) {
+
+        if (Strings.isNullOrEmpty(order)) {
+            order = ORDER_ASE;
+        }
+
+        if (fill == null) {
+            if (ORDER_ASE.equals(order)) {
+                fill = " ";
+            } else {
+                fill = "";
+            }
+        }
+
+        StringBuffer nameSbuff = new StringBuffer();
+
+        if (ORDER_ASE.equals(order)) {
+            append(nameSbuff, fill, firstName);
+        } else {
+            append(nameSbuff, fill, lastName);
+        }
+
+        if (!Strings.isNullOrEmpty(middle)) {
+            append(nameSbuff, fill, middle);
+        }
+
+        if (ORDER_ASE.equals(order)) {
+            append(nameSbuff, fill, lastName);
+        } else {
+            append(nameSbuff, fill, firstName);
+        }
+
+        return nameSbuff.toString();
+    }
+
+    private static void append(StringBuffer nameSbuff, String fill, String str) {
+        if (nameSbuff == null) {
+            nameSbuff = new StringBuffer(20);
+        }
+        if (!Strings.isNullOrEmpty(str)) {
+            if (nameSbuff.length() > 0) {
+                if (!Strings.isNullOrEmpty(fill)) {
+                    nameSbuff.append(fill);
+                }
+            }
+            nameSbuff.append(str);
+        }
+    }
+
+    public static Integer isXJ(CustomInfoTable table) {
+        String name = getName(table);
+        return isXJ(table.getCountry(), name);
+    }
+
+    public static Integer isXJ(CustomInfoEntity entity) {
+        String name = getName(entity);
+        return isXJ(entity.getCountry(), name);
+    }
+
+    private static Integer isXJ(String country, String name) {
+
+        Integer xj = 0;
+        if ("CN".equals(country)) {
+            if (!Strings.isNullOrEmpty(name) && name.trim().length() > 4 && (name.indexOf(".") > 0 || name.indexOf(
+                    "·") > 0)) {
+                xj = 1;
+            }
+        }
+        return xj;
+    }
+
+    public static void main(String[] args) throws Exception {
+        System.out.println("苏·托乎提".length());
+    }
+
+}

+ 65 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DESedeUtil.java

@@ -0,0 +1,65 @@
+package com.crm.rely.backend.util;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class DESedeUtil {
+    private static final String ENCODING_UTF_8 = "utf8";
+    private static final String DESEDE_ALGORITHM = "DESede";
+    private static final String DESEDE_CBC_PKCS5PEADDING_ALGORITHM = "DESede/CBC/PKCS5Padding";
+    public static String encrypt(String message, String secretKey) throws Exception {
+
+        return encrypt(message, secretKey, ENCODING_UTF_8);
+    }
+
+    public static String encrypt(String message, String secretKey, String encoding) throws Exception {
+        byte[] secretKeyBytes = secretKey.getBytes(encoding);
+        byte[] keyBytes = secretKey.substring(0, 8).getBytes(encoding);
+        final SecretKey key = new SecretKeySpec(secretKeyBytes, DESEDE_ALGORITHM);
+        final IvParameterSpec iv = new IvParameterSpec(keyBytes);
+        final Cipher cipher = Cipher.getInstance(DESEDE_CBC_PKCS5PEADDING_ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, key, iv);
+        final byte[] plainTextBytes = message.getBytes(encoding);
+        final byte[] cipherText = cipher.doFinal(plainTextBytes);
+        return bytes2HexString(cipherText);
+    }
+
+    public static String bytes2HexString(byte[] b) {
+        byte[] hex = "0123456789ABCDEF".getBytes();
+        byte[] buff = new byte[2 * b.length];
+        for (int i = 0; i < b.length; i++) {
+            buff[2 * i] = hex[(b[i] >> 4) & 0x0f];
+            buff[2 * i + 1] = hex[b[i] & 0x0f];
+        }
+        return new String(buff);
+    }
+
+    public static byte[] hexStringToBytes(String s) {
+        int len = s.length();
+        byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
+        }
+        return data;
+    }
+
+    public static String decrypt(String message, String secretKey) throws Exception {
+
+        return decrypt(message, secretKey, ENCODING_UTF_8);
+    }
+
+    public static String decrypt(String message, String secretKey, String encoding) throws Exception {
+        byte[] messageBytes = hexStringToBytes(message);
+        byte[] secretKeyBytes = secretKey.getBytes(encoding);
+        byte[] keyBytes = secretKey.substring(0, 8).getBytes(encoding);
+        final SecretKey key = new SecretKeySpec(secretKeyBytes, DESEDE_ALGORITHM);
+        final IvParameterSpec iv = new IvParameterSpec(keyBytes);
+        final Cipher decipher = Cipher.getInstance(DESEDE_CBC_PKCS5PEADDING_ALGORITHM);
+        decipher.init(Cipher.DECRYPT_MODE, key, iv);
+        final byte[] plainText = decipher.doFinal(messageBytes);
+        return new String(plainText, encoding);
+    }
+
+}

+ 13 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DataSourceNames.java

@@ -0,0 +1,13 @@
+package com.crm.rely.backend.util;
+
+/**
+ * @Author houn
+ * @Date 2024/12/24 14:22
+ * @PackageName:com.crm.manager.configuration
+ * @ClassName: DataSourceNames
+ * @Description: TODO
+ */
+public class DataSourceNames {
+    public static final String MASTER = "master";
+    public static final String SLAVE = "slave";
+}

+ 604 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DateUtil.java

@@ -0,0 +1,604 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.core.entity.base.DateIntervalEntity;
+import com.google.common.collect.Lists;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+public class DateUtil {
+
+
+    public static final String DATE_FORMAT = "yyyy-MM-dd";
+
+    public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+
+    public static final String DATE_TIME_FORMAT = "yyyyMMddHHmmss";
+
+    public static Date orientTime;
+
+    static {
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_FORMAT);
+        try {
+            orientTime = simpleDateFormat.parse("1970-01-01");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 格式转化为dateTime-yyyyMMddHHmmss
+     *
+     * @return
+     */
+    public static String formatDateTime() {
+        return formatDateTime(new Date());
+    }
+
+    /**
+     * 格式转化为dateTime-yyyyMMddHHmmss
+     *
+     * @param date 时间
+     * @return
+     */
+    public static String formatDateTime(Date date) {
+        return format(DATE_TIME_FORMAT, date);
+    }
+
+    public static String formatDateTime(String orient) {
+        Date date = parseTime(orient);
+        return format(DATE_TIME_FORMAT, date);
+    }
+
+    /**
+     * 格式转化-yyyy-MM-dd
+     *
+     * @return
+     */
+    public static String formatDate() {
+        return formatDate(new Date());
+    }
+
+    /**
+     * 格式转化-yyyy-MM-dd
+     *
+     * @param date 时间
+     * @return
+     */
+    public static String formatDate(Date date) {
+        return format(DATE_FORMAT, date);
+    }
+
+    /**
+     * 格式转化-yyyy-MM-dd HH:mm:ss
+     *
+     * @return
+     */
+    public static String formatTime() {
+
+        return formatTime(new Date());
+    }
+
+    /**
+     * 格式转化-yyyy-MM-dd HH:mm:ss
+     *
+     * @param date
+     * @return
+     */
+    public static String formatTime(Date date) {
+        return format(TIME_FORMAT, date);
+    }
+
+    /**
+     * 格式转化
+     *
+     * @param format 时间格式
+     * @return
+     */
+    public static String format(String format) {
+        return format(format, new Date());
+    }
+
+    /**
+     * 格式转化
+     *
+     * @param format 时间格式
+     * @param date   时间
+     * @return
+     */
+    public static String format(String format, Date date) {
+        if (date == null) {
+            return "";
+        }
+        SimpleDateFormat sdf = new SimpleDateFormat(format);
+        return sdf.format(date);
+    }
+
+    public static Date parseDate() {
+        return parseDate(new Date());
+    }
+
+    public static Date parseDate(String orient) {
+        return parse(DATE_FORMAT, orient);
+    }
+
+    public static Date parseDate(Date date) {
+
+        return new Date(date.getYear(), date.getMonth(), date.getDate());
+    }
+
+    public static Date parseDate(Date date, int num) {
+        date = DateUtil.operationDay(date, num);
+        return new Date(date.getYear(), date.getMonth(), date.getDate());
+    }
+
+    public static Date parseMonth() {
+        return parseMonth(new Date());
+    }
+
+    public static Date parseMonth(Date date) {
+
+        return new Date(date.getYear(), date.getMonth(), 1);
+    }
+
+    public static Date parseMonth(Date date, int num) {
+        date = DateUtil.operationMoth(date, num);
+        return new Date(date.getYear(), date.getMonth(), 1);
+    }
+
+    public static Date parseTime(String orient) {
+        return parse(TIME_FORMAT, orient);
+    }
+
+    public static Date parse(String pattern, String orient) {
+        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
+        Date date = null;
+        try {
+            date = sdf.parse(orient);
+        } catch (Exception e) {
+            return null;
+        }
+        return date;
+    }
+
+    public static Date parseSearchDate(String orient, int interval) {
+        SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
+        if (orient.equals("") || orient == null) {
+            return null;
+        }
+        Date date = null;
+        try {
+            date = sdf.parse(orient);
+        } catch (Exception e) {
+            date = new Date();
+        }
+
+        date = operationDay(date, interval);
+        return date;
+    }
+
+
+    public static Date operationYear(int num) {
+        return operationDate(new Date(), Calendar.YEAR, num);
+    }
+
+    public static Date operationYear(Date date, int num) {
+        return operationDate(date, Calendar.YEAR, num);
+    }
+
+    public static Date operationMoth(int num) {
+        return operationDate(new Date(), Calendar.MONTH, num);
+    }
+
+    public static Date operationMoth(Date date, int num) {
+        return operationDate(date, Calendar.MONTH, num);
+    }
+
+    public static Date operationDay(int num) {
+        return operationDate(new Date(), Calendar.DATE, num);
+    }
+
+    public static Date operationDay(Date date, int num) {
+        return operationDate(date, Calendar.DATE, num);
+    }
+
+    public static Date operationHour(int num) {
+        return operationDate(new Date(), Calendar.HOUR, num);
+    }
+
+    public static Date operationHour(Date date, int num) {
+        return operationDate(date, Calendar.HOUR, num);
+    }
+
+    public static Date operationMinute(int num) {
+        return operationDate(new Date(), Calendar.MINUTE, num);
+    }
+
+    public static Date operationMinute(Date date, int num) {
+        return operationDate(date, Calendar.MINUTE, num);
+    }
+
+    public static Date operationSecond(int num) {
+        return operationDate(new Date(), Calendar.SECOND, num);
+    }
+
+    public static Date operationSecond(Date date, int num) {
+        return operationDate(date, Calendar.SECOND, num);
+    }
+
+    public static Date operationMillisecond(int num) {
+        return operationDate(new Date(), Calendar.MILLISECOND, num);
+    }
+
+    public static Date operationMillisecond(Date date, int num) {
+        return operationDate(date, Calendar.MILLISECOND, num);
+    }
+
+    public static Date operationDate(int field, int num) {
+        GregorianCalendar gc = new GregorianCalendar();
+        gc.setTime(new Date());
+        gc.add(field, num);
+        return gc.getTime();
+    }
+
+    public static Date operationDate(Date date, int field, int num) {
+        GregorianCalendar gc = new GregorianCalendar();
+        gc.setTime(date);
+        gc.add(field, num);
+        return gc.getTime();
+    }
+
+    public static void main(String[] args) {
+
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Date endDate = sdf.parse("2024-12-08");
+            System.out.println(isMonday(endDate));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 两天之间的天数
+     *
+     * @param date1
+     * @param date2
+     * @return
+     */
+    public static int getDateBetween(Date date1, Date date2) {
+        Long date1Long = date1.getTime();
+        Long date2Long = date2.getTime();
+        Long betweenMilli = date2Long - date1Long;
+        Long day = betweenMilli / (3600 * 1000 * 24);
+        return Integer.valueOf(day + "");
+    }
+
+    public static Date getPureDate(Date date) throws Exception {
+
+        SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
+
+        String str = sdf.format(date);
+
+        return sdf.parse(str);
+    }
+
+    public static Date getMonthStart(Date date) {
+        if (date == null) {
+            date = new Date();
+        }
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.DATE, 1);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        return calendar.getTime();
+    }
+
+    public static Date getMonthEnd(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(DateUtil.getMonthStart(date));
+        calendar.add(Calendar.MONTH, 1);
+        return calendar.getTime();
+    }
+
+    public static Date addDate(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.DATE, 1);
+        return calendar.getTime();
+    }
+
+    public static Date addDate(Date date, Integer interval) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.DATE, interval);
+        return calendar.getTime();
+    }
+
+    public static Date addHour(Date date, Integer interval) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.HOUR, interval);
+        return calendar.getTime();
+    }
+
+    public static int getHour(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        return calendar.get(Calendar.HOUR_OF_DAY);
+    }
+
+    public static Date getThisWeekMonday(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        // 设置星期的第一天为周一
+        calendar.setFirstDayOfWeek(Calendar.MONDAY);
+        // 调整到本周的周一
+        calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+        // 设置为 00:00:00
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        return calendar.getTime();
+    }
+
+    public static Date addSecond(Date date, int seconds) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.SECOND, seconds);
+        return calendar.getTime();
+    }
+
+    // 判断是否是“周一凌晨 0点到1点前”的时间
+    public static boolean isEarlyMonday(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+
+        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); // 周日=1,周一=2...
+        int hour = cal.get(Calendar.HOUR_OF_DAY);       // 0-23
+        int minute = cal.get(Calendar.MINUTE);          // 0-59
+
+        // 周一凌晨 0:00 ~ 0:59
+        return (dayOfWeek == Calendar.MONDAY) && (hour == 0);
+    }
+
+    /**
+     * 时区转化
+     *
+     * @param timeZone   时区 比如GMT+8
+     * @param createTime 时间戳
+     * @return
+     */
+    public static String convertDateBetweenTimeZone(String timeZone, Long createTime) {
+        return convertDateBetweenTimeZone(timeZone, new Date(createTime));
+    }
+
+    /**
+     * 时区转化
+     *
+     * @param timeZone 时区 比如GMT+8
+     * @param date     时间
+     * @return
+     */
+    public static String convertDateBetweenTimeZone(String timeZone, Date date) {
+
+        return convertDateBetweenTimeZone(timeZone, date, TIME_FORMAT);
+    }
+
+    public static String convertDateBetweenTimeZone(String timeZone, Date date, String format) {
+        DateFormat dateFormat = new SimpleDateFormat("Z");
+        String localTimeZone = dateFormat.format(new Date());
+        TimeZone srcTimeZone = TimeZone.getTimeZone("GMT" + localTimeZone);
+        TimeZone destTimeZone = TimeZone.getTimeZone(timeZone);
+        String destDateTime = dateTransformBetweenTimeZone(date, format, srcTimeZone, destTimeZone);
+        return destDateTime;
+    }
+
+    public static long getTimestampByZero() {
+        return getTimestampByZero(new Date());
+    }
+
+    public static long getTimestampByZero(Date date) {
+        DateFormat dateFormat = new SimpleDateFormat("Z");
+        String localTimeZone = dateFormat.format(new Date());
+        TimeZone srcTimeZone = TimeZone.getTimeZone("GMT" + localTimeZone);
+        TimeZone destTimeZone = TimeZone.getTimeZone("GMT +0");
+        Long targetTime = date.getTime() - srcTimeZone.getRawOffset() + destTimeZone.getRawOffset();
+        return targetTime;
+    }
+
+    public static long getTimestampByChina() {
+
+        return getTimestampByLocale(Locale.CHINA);
+    }
+
+    public static long getTimestampByLocale(Locale aLocale) {
+        Calendar cal = Calendar.getInstance(aLocale);
+        return DateUtil.getTimestamp(cal.getTime());
+    }
+
+    public static long getTimestampByTimeZone(TimeZone zone) {
+        Calendar cal = Calendar.getInstance(zone);
+        return DateUtil.getTimestamp(cal.getTime());
+    }
+
+    /**
+     * @param sourceDate     转化时区时间
+     * @param format         转化后格式
+     * @param sourceTimeZone
+     * @param targetTimeZone
+     * @return
+     */
+    public static String dateTransformBetweenTimeZone(Date sourceDate, String format,
+                                                      TimeZone sourceTimeZone, TimeZone targetTimeZone) {
+        //计算出对应时区的时间
+        Long targetTime = sourceDate.getTime() - sourceTimeZone.getRawOffset() + targetTimeZone.getRawOffset();
+        //时间格式转化
+        return format(format, new Date(targetTime));
+    }
+
+    /**
+     * 获取时间戳
+     *
+     * @return
+     */
+    public static String getTimestamp() {
+        return String.valueOf(System.currentTimeMillis() / 1000);
+    }
+
+    public static long getTimestamp(Date sourceDate) {
+        if (sourceDate == null) {
+            return 0;
+        }
+        return sourceDate.getTime();
+    }
+
+    public static long getTimestamp(String orient) {
+
+        return getTimestamp(parseTime(orient));
+    }
+
+    public static List<String> shardingDate(Date startDate, Date endDate) throws Exception {
+
+        if (startDate == null) {
+            throw new Exception("Start date can not by empty");
+        }
+        if (endDate == null) {
+            throw new Exception("End date can not by empty");
+        }
+        if (endDate.getTime() < startDate.getTime()) {
+            throw new Exception("End date must greater than start date");
+        }
+
+        List<String> shardings = Lists.newArrayList();
+        Calendar sc = Calendar.getInstance();
+        sc.setTime(startDate);
+        Calendar ec = Calendar.getInstance();
+        ec.setTime(endDate);
+//        if (ec.get(Calendar.DATE) == 1) {
+//            ec.set(Calendar.DATE, -1);
+//        }
+
+        if (ec.get(Calendar.DAY_OF_MONTH) == 1) {
+            ec.add(Calendar.DAY_OF_MONTH, -1);
+        }
+
+        int sYear = sc.get(Calendar.YEAR);
+        int eYear = ec.get(Calendar.YEAR);
+        int sMonth = sc.get(Calendar.MONTH) + 1;
+        int eMonth = ec.get(Calendar.MONTH) + 1;
+
+        for (int y = sYear; y <= eYear; y++) {
+            for (int m = (y == sYear ? sMonth : 1); (y != eYear && m <= 12) || (y == eYear && m <= eMonth); m++) {
+                shardings.add(yearAndMonthToString(y, m));
+            }
+        }
+
+        Collections.reverse(shardings);
+        return shardings.stream().distinct().collect(Collectors.toList());
+    }
+
+    private static String yearAndMonthToString(int year, int month) throws Exception {
+        return String.format("%d%s", year, month < 10 ? String.format("0%d", month) : String.valueOf(month));
+    }
+
+    public static DateIntervalEntity generateYesterdayInterval(Date date) throws Exception {
+        DateIntervalEntity entity = new DateIntervalEntity();
+        Date pureDate = DateUtil.getPureDate(date);
+        entity.setEndDate(pureDate);
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(pureDate);
+        calendar.add(Calendar.DATE, -1);
+        entity.setStartDate(calendar.getTime());
+        return entity;
+    }
+
+    public static boolean isMonday(Date date) throws Exception {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        int dow = calendar.get(Calendar.DAY_OF_WEEK);
+        return dow == 2;
+    }
+
+    public static boolean isMonthStart(Date date) throws Exception {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        int dow = calendar.get(Calendar.DAY_OF_MONTH);
+        return dow == 1;
+    }
+
+    public static Integer getDayOfWeek(Date date) throws Exception {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        return calendar.get(Calendar.DAY_OF_WEEK);
+    }
+
+    public static Date addMonth(Date date, Integer interval) throws Exception {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.MONTH, interval);
+        return calendar.getTime();
+    }
+
+
+    public static Integer monthBetween(Date startDate, Date endDate) throws Exception {
+        Calendar startCalendar = Calendar.getInstance();
+        startCalendar.setTime(DateUtil.getPureDate(startDate));
+        Calendar endCalendar = Calendar.getInstance();
+        endCalendar.setTime(DateUtil.getPureDate(endDate));
+
+        int yearBetween = endCalendar.get(Calendar.YEAR) - startCalendar.get(Calendar.YEAR);
+        return (endCalendar.get(Calendar.MONTH) + 12 * yearBetween) - startCalendar.get(Calendar.MONTH);
+    }
+
+    public static Integer getMonth(Date date) throws Exception {
+        // 获取Calendar实例
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        // 获取月份,注意月份是从0开始的,即0代表1月,11代表12月
+        int month = calendar.get(Calendar.MONTH);
+        // 如果需要从1开始计数,可以加1
+        int monthPlusOne = month + 1;
+
+        return monthPlusOne;
+    }
+
+    /**
+     * 毫秒时间戳转换
+     *
+     * @author gao
+     * @date 2025/8/15 14:11
+     */
+    public static String formatTimestamp(Long timestamp) {
+        if (timestamp == null) {
+            return null;
+        }
+
+        // 创建 DateTimeFormatter
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+        // 转换时间戳为 LocalDateTime
+        LocalDateTime dateTime = LocalDateTime.ofInstant(
+                java.time.Instant.ofEpochMilli(timestamp),
+                ZoneId.systemDefault()
+        );
+
+        // 格式化输出
+        return dateTime.format(formatter);
+    }
+
+    public static String formatToYm(Date date) {
+        return format("yyyyMM", date);
+    }
+
+}

+ 12 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DefaultValueUtil.java

@@ -0,0 +1,12 @@
+package com.crm.rely.backend.util;
+
+public class DefaultValueUtil {
+
+    public static Integer defaultIfNullOrNonPositive(Integer value, int defaultValue) {
+        return (value == null || value <= 0) ? defaultValue : value;
+    }
+
+    public static Long defaultIfNullOrNonPositive(Long value, long defaultValue) {
+        return (value == null || value <= 0) ? defaultValue : value;
+    }
+}

+ 77 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DeleteDirectory.java

@@ -0,0 +1,77 @@
+package com.crm.rely.backend.util;
+
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
+
+class DeleteDirectory implements FileVisitor {
+
+    boolean deleteFileByFile(Path file) throws IOException {
+        return Files.deleteIfExists(file);
+    }
+
+    @Override
+    public FileVisitResult postVisitDirectory(Object dir, IOException exc)
+            throws IOException {
+
+        if (exc == null) {
+            System.out.println("Visited: " + (Path) dir);
+            boolean success = deleteFileByFile((Path) dir);
+
+            if (success) {
+                System.out.println("Deleted: " + (Path) dir);
+            } else {
+                System.out.println("Not deleted: " + (Path) dir);
+            }
+        } else {
+            throw exc;
+        }
+        return FileVisitResult.CONTINUE;
+    }
+
+    @Override
+    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs)
+            throws IOException {
+        return FileVisitResult.CONTINUE;
+    }
+
+    @Override
+    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs)
+            throws IOException {
+        boolean success = deleteFileByFile((Path) file);
+
+        if (success) {
+            System.out.println("Deleted: " + (Path) file);
+        } else {
+            System.out.println("Not deleted: " + (Path) file);
+        }
+
+        return FileVisitResult.CONTINUE;
+    }
+
+    @Override
+    public FileVisitResult visitFileFailed(Object file, IOException exc)
+            throws IOException {
+        //report an error if necessary
+
+        return FileVisitResult.CONTINUE;
+    }
+}
+
+//class Main {
+//
+//    public static void main(String[] args) throws IOException {
+//
+//        Path directory = Paths.get("C:/rafaelnadal");
+//        DeleteDirectory walk = new DeleteDirectory();
+//        EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
+//
+//        Files.walkFileTree(directory, opts, Integer.MAX_VALUE, walk);
+//    }
+//}

+ 24 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/Difference.java

@@ -0,0 +1,24 @@
+package com.crm.rely.backend.util;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @program: crm-backend
+ * @description:
+ * @author: houn
+ * @create: 2019-08-01 11:47
+ */
+@Data
+public class Difference {
+
+    private String attr;
+
+    private Object after;
+
+    private Object before;
+
+    private List<Difference> childrenDifference;
+
+}

+ 48 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DistanceUtil.java

@@ -0,0 +1,48 @@
+package com.crm.rely.backend.util;
+
+import java.io.IOException;
+
+/**
+ * 计算坐标之间的距离
+ *
+ * @program: user-custom
+ * @description:
+ * @author: houn
+ * @create: 2019-05-30 16:27
+ */
+public class DistanceUtil {
+    private static double EARTH_RADIUS = 6371.393;
+
+    /**
+     * 计算两个经纬度之间的距离
+     *
+     * @param lat1
+     * @param lng1
+     * @param lat2
+     * @param lng2
+     * @return
+     */
+    public static double getDistance(double lat1, double lng1, double lat2, double lng2) {
+        double radLat1 = rad(lat1);
+        double radLat2 = rad(lat2);
+        double a = radLat1 - radLat2;
+        double b = rad(lng1) - rad(lng2);
+        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
+                Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
+        s = s * EARTH_RADIUS;
+        s = Math.round(s * 1000);
+        return s;
+    }
+
+    private static double rad(double d) {
+        return d * Math.PI / 180.0;
+    }
+
+    public static void main(String[] args) throws IOException {
+
+        double distance = getDistance(30.2047700000, 120.2169740000, 30.2056210000, 120.2232890000);
+
+        System.out.println(distance);
+    }
+}
+

+ 121 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DwTypeUtil.java

@@ -0,0 +1,121 @@
+package com.crm.rely.backend.util;
+
+import org.springframework.util.StringUtils;
+
+public class DwTypeUtil {
+
+    private final static Long minLogin = 60000000L;
+
+    private final static Long maxLogin = 70000000L;
+
+    //入金
+    public final static String DEPOSIT = "DEPOSIT";
+    //入金
+    public final static String WITHDRAWAL = "WITHDRAWAL";
+    //信用金
+    public final static String CREDIT_IN = "CREDIT IN";
+
+    public final static String CREDIT_OUT = "CREDIT OUT";
+    //代理佣金内转
+    public final static String AGENT_COMMISSION_TRANSFER_HEAD = "PACT";
+    //入金
+    public final static String DEPOSIT_HEAD = "PD";
+    //出金
+    public final static String WITHDRAW_HEAD = "PW";
+    //出金退款
+    public final static String WITHDRAW_REFUND_HEAD = "PWR";
+    //代理出金
+    public final static String WITHDRAW_AGENT_HEAD = "PWC";
+    //内转
+    public final static String TRANSFER_HEAD = "PT";
+    //代理内转
+    public final static String FINANCE_AGENT_TRANSFER_HEAD = "PAT";
+    //异名内转
+    public final static String FINANCE_SYNONYM_TRANSFER_HEAD = "PST";
+    //赠金
+    public final static String DEPOSIT_GIVE_HEAD = "PDG";
+    //撤销信用金
+    public final static String REVOKE_CREDIT_HEAD = "PRC";
+    //跟单内转
+    public final static String TRANSFER_FOLLOW_HEAD = "PTF";
+    //免息组别持仓利息
+    public final static String TRADE_INTEREST_HEAD = "PTI";
+    //跟单出金(分润)
+    public final static String TRADE_FOLLOW_WITHDRAW_HEAD = "PFW";
+    //及时返佣 agent '1820290' - #46666030
+    public final static String AGENT = "AGENT";
+    //pamm账户主账户下单后分配给子账户的盈亏 from master trade #46678419
+    public final static String FROM_MASTER_TRADE = "FROM MASTER TRADE";
+    //销售绩效的调整
+    public final static String ADJ = "ADJ";
+    //销售薪资提取
+    public final static String SW = "SW";
+    //交易账户返现/现金嘉年华
+    public final static String PTC = "PTC";
+    //U卡出入金
+    public final static String CARD = "CARD";
+    //补零
+    public final static String PCM = "PCM";
+
+    public static Integer getDwType(Long login, String comment) throws Exception {
+        if (login == null || login <= 0 || StringUtils.isEmpty(comment)) {
+            return 0;
+        }
+        Integer type;
+        if (login < minLogin && login >= maxLogin) {
+            if (comment.toUpperCase().startsWith(AGENT)) {
+                type = 13;
+            } else {
+                type = 99;
+            }
+        } else {
+            if (comment.toUpperCase().startsWith(WITHDRAW_AGENT_HEAD)) {
+                type = 2;
+            } else if (comment.toUpperCase().startsWith(FINANCE_SYNONYM_TRANSFER_HEAD)) {
+                type = 4;
+            } else if (comment.toUpperCase().startsWith(FINANCE_AGENT_TRANSFER_HEAD)) {
+                type = 5;
+            } else if (comment.toUpperCase().startsWith(AGENT_COMMISSION_TRANSFER_HEAD)) {
+                type = 19;
+            } else if (comment.toUpperCase().startsWith(TRANSFER_FOLLOW_HEAD)) {
+                type = 7;
+            } else if (comment.toUpperCase().startsWith(DEPOSIT_GIVE_HEAD)) {
+                type = 8;
+            } else if (comment.toUpperCase().startsWith(WITHDRAW_REFUND_HEAD)) {
+                type = 9;
+            } else if (comment.toUpperCase().startsWith(REVOKE_CREDIT_HEAD)) {
+                type = 10;
+            } else if (comment.toUpperCase().startsWith(TRADE_INTEREST_HEAD)) {
+                type = 11;
+            } else if (comment.toUpperCase().startsWith(TRADE_FOLLOW_WITHDRAW_HEAD)) {
+                type = 12;
+            } else if (comment.toUpperCase().startsWith(AGENT)) {
+                type = 13;
+            } else if (comment.toUpperCase().startsWith(FROM_MASTER_TRADE)) {
+                type = 14;
+            } else if (comment.toUpperCase().startsWith(ADJ)) {
+                type = 15;
+            } else if (comment.toUpperCase().startsWith(SW)) {
+                type = 16;
+            } else if (comment.toUpperCase().startsWith(PTC)) {
+                type = 17;
+            } else if (comment.toUpperCase().startsWith(TRANSFER_HEAD)) {
+                type = 3;
+            } else if (comment.toUpperCase().startsWith(PCM)) {
+                type = 20;
+            } else if (comment.toUpperCase().startsWith(DEPOSIT_HEAD)
+                    || comment.toUpperCase().startsWith(WITHDRAW_HEAD)
+                    || comment.toUpperCase().startsWith(CREDIT_IN)
+                    || comment.toUpperCase().startsWith(CREDIT_OUT)
+                    || comment.toUpperCase().startsWith(CARD)) {
+                type = 1;
+            } else {
+                type = 99;
+            }
+
+        }
+
+        return type;
+    }
+
+}

+ 24 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DynamicDataSourceContext.java

@@ -0,0 +1,24 @@
+package com.crm.rely.backend.util;
+
+/**
+ * @Author houn
+ * @Date 2024/12/24 13:59
+ * @PackageName:com.crm.manager.configuration
+ * @ClassName: DynamicDataSourceContext
+ * @Description: TODO
+ */
+public class DynamicDataSourceContext {
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    public static String getDataSource() {
+        return CONTEXT_HOLDER.get();
+    }
+
+    public static void setDataSource(String dataSource) {
+        CONTEXT_HOLDER.set(dataSource);
+    }
+
+    public static void clear() {
+        CONTEXT_HOLDER.remove();
+    }
+}

+ 17 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/DynamicRoutingDataSource.java

@@ -0,0 +1,17 @@
+package com.crm.rely.backend.util;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+/**
+ * @Author houn
+ * @Date 2024/12/24 14:00
+ * @PackageName:com.crm.manager.configuration
+ * @ClassName: DynamicRoutingDataSource
+ * @Description: TODO
+ */
+public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
+    @Override
+    protected Object determineCurrentLookupKey() {
+        return DynamicDataSourceContext.getDataSource();
+    }
+}

+ 151 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/ECDSAUtil.java

@@ -0,0 +1,151 @@
+package com.crm.rely.backend.util;
+
+import javax.xml.bind.DatatypeConverter;
+import java.security.*;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * @Author houn
+ * @Date 2024/4/22 12:51
+ * @PackageName:com.crm.rely.backend.util
+ * @ClassName: ECDSAUtil
+ * @Description: TODO
+ */
+public class ECDSAUtil {
+
+    private static final String SIGNALGORITHMS = "SHA256withECDSA";
+    private static final String ALGORITHM = "EC";
+    private static final String SECP256K1 = "secp256k1";
+
+
+    public static void main(String[] args) throws Exception {
+
+//        生成公钥私钥
+        KeyPair keyPair1 = getKeyPair();
+        PublicKey publicKey1 = keyPair1.getPublic();
+        PrivateKey privateKey1 = keyPair1.getPrivate();
+        //密钥转16进制字符串
+        String publicKey = HexUtil.bytes2Hex(publicKey1.getEncoded());
+        String privateKey = HexUtil.bytes2Hex(privateKey1.getEncoded());
+        System.out.println("生成公钥:" + publicKey);
+        System.out.println("生成私钥:" + privateKey);
+//        //16进制字符串转密钥对象
+//        PrivateKey privateKey2 = getPrivateKey(privateKey);
+//        PublicKey publicKey2 = getPublicKey(publicKey);
+//        //加签验签
+//        String data = "需要签名的数据";
+//        String signECDSA = signECDSA(privateKey2, data);
+//        boolean verifyECDSA = verifyECDSA(publicKey2, signECDSA, data);
+//        System.out.println("验签结果:" + verifyECDSA);
+
+
+        System.out.println(signECDSA("bb06971bb6abb30da07f095a286e4e89ffd2b44cc83ea4d14fc68f85924884e1", "12"));
+    }
+
+    /**
+     * 加签
+     *
+     * @param privateKey 私钥
+     * @param data       数据
+     * @return
+     */
+    public static String signECDSA(String privateKey, String data) throws Exception {
+
+        return signECDSA(getPrivateKey(privateKey), data);
+    }
+
+    public static String signECDSA(PrivateKey privateKey, String data) {
+        String result = "";
+        try {
+            //执行签名
+            Signature signature = Signature.getInstance(SIGNALGORITHMS);
+            signature.initSign(privateKey);
+            signature.update(data.getBytes());
+            byte[] sign = signature.sign();
+            return HexUtil.bytes2Hex(sign);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    public static boolean verifyECDSA(String publicKey, String signed, String data) throws Exception {
+
+        return verifyECDSA(getPublicKey(publicKey), signed, data);
+    }
+
+    /**
+     * 验签
+     *
+     * @param publicKey 公钥
+     * @param signed    签名
+     * @param data      数据
+     * @return
+     */
+    public static boolean verifyECDSA(PublicKey publicKey, String signed, String data) {
+        try {
+            //验证签名
+            Signature signature = Signature.getInstance(SIGNALGORITHMS);
+            signature.initVerify(publicKey);
+            signature.update(data.getBytes());
+            byte[] hex = HexUtil.decode(signed);
+            boolean bool = signature.verify(hex);
+            // System.out.println("验证:" + bool);
+            return bool;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+
+    /**
+     * 从string转private key
+     *
+     * @param key 私钥的字符串
+     * @return
+     * @throws Exception
+     */
+    public static PrivateKey getPrivateKey(String key) throws Exception {
+
+        byte[] bytes = DatatypeConverter.parseHexBinary(key);
+        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
+        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
+        return keyFactory.generatePrivate(keySpec);
+    }
+
+    /**
+     * 从string转publicKey
+     *
+     * @param key 公钥的字符串
+     * @return
+     * @throws Exception
+     */
+    public static PublicKey getPublicKey(String key) throws Exception {
+
+        byte[] bytes = DatatypeConverter.parseHexBinary(key);
+        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
+        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
+        return keyFactory.generatePublic(keySpec);
+    }
+
+
+    /**
+     * 生成密钥对
+     *
+     * @return
+     * @throws Exception
+     */
+    public static KeyPair getKeyPair() throws Exception {
+
+        ECGenParameterSpec ecSpec = new ECGenParameterSpec(SECP256K1);
+        KeyPairGenerator kf = KeyPairGenerator.getInstance(ALGORITHM);
+        kf.initialize(ecSpec, new SecureRandom());
+        KeyPair keyPair = kf.generateKeyPair();
+        return keyPair;
+    }
+
+
+}
+

+ 149 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/EasyExcelUti.java

@@ -0,0 +1,149 @@
+package com.crm.rely.backend.util;
+
+import com.alibaba.excel.ExcelReader;
+import com.alibaba.excel.metadata.BaseRowModel;
+import com.alibaba.excel.metadata.Sheet;
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.entity.base.PageEntity;
+import com.crm.rely.backend.exception.ServiceException;
+import org.springframework.beans.BeanUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 导入工具
+ *
+ * @Author: houn
+ * @Date: 2020/12/16 21:36
+ * @Description:
+ */
+public class EasyExcelUti {
+
+    /**
+     * 读取某个 sheet 的 Excel
+     *
+     * @param excel    文件
+     * @param rowModel 实体类映射,继承 BaseRowModel 类
+     * @return Excel 数据 list
+     */
+    public static List<Object> readExcel(MultipartFile excel, BaseRowModel rowModel) throws IOException {
+        return readExcel(excel, rowModel, 1, 1);
+    }
+
+    /**
+     * 读取某个 sheet 的 Excel
+     *
+     * @param excel    文件
+     * @param rowModel 实体类映射,继承 BaseRowModel 类
+     * @param sheetNo  sheet 的序号 从1开始
+     * @return Excel 数据 list
+     */
+    public static <T extends BaseRowModel> List<T> readExcel(MultipartFile excel, T rowModel, int sheetNo) throws IOException {
+
+        ExcelListener excelListener=  getExcelListener(excel,rowModel,sheetNo);
+
+        return excelListener.getDatas();
+    }
+
+    public static <T> List<T> readExcel(MultipartFile excel, BaseRowModel rowModel, int sheetNo, Class<T> clazz) throws IOException, ServiceException, IllegalAccessException, InstantiationException {
+
+        ExcelListener excelListener=  getExcelListener(excel,rowModel,sheetNo);
+
+        if (!excelListener.getImportHeads().equals(excelListener.getModelHeads())) {
+            throw ServiceException.exception(Constants.UPLOAD_EXCEL_ERROR);
+        }
+        if (excelListener.getDatas() != null && excelListener.getDatas().size() > 0) {
+            List<T> list = new LinkedList<>();
+            for (Object o : excelListener.getDatas()) {
+                BaseRowModel model = (BaseRowModel) o;
+                T t = clazz.newInstance();
+                BeanUtils.copyProperties(model, t);
+                list.add(t);
+
+            }
+            return list;
+        } else {
+            throw ServiceException.exception(Constants.UPLOAD_EXCEL_ERROR);
+        }
+    }
+
+    private static <T extends BaseRowModel>ExcelListener getExcelListener(MultipartFile excel, T rowModel, int sheetNo) throws IOException {
+        ExcelListener excelListener = new ExcelListener();
+        ExcelReader reader = getReader(excel, excelListener);
+        if (reader == null) {
+            return null;
+        }
+        reader.read(new Sheet(sheetNo, 0, rowModel.getClass()));
+        return excelListener;
+    }
+    /**
+     * 读取某个 sheet 的 Excel
+     *
+     * @param excel       文件
+     * @param rowModel    实体类映射,继承 BaseRowModel 类
+     * @param sheetNo     sheet 的序号 从1开始
+     * @param headLineNum 表头行数,默认为1
+     * @return Excel 数据 list
+     */
+    public static List<Object> readExcel(MultipartFile excel, BaseRowModel rowModel, int sheetNo, int headLineNum) throws IOException {
+        ExcelListener excelListener = new ExcelListener();
+        ExcelReader reader = getReader(excel, excelListener);
+        if (reader == null) {
+            return null;
+        }
+        reader.read(new Sheet(sheetNo, headLineNum, rowModel.getClass()));
+        return excelListener.getDatas();
+    }
+
+    /**
+     * 读取指定sheetName的Excel(多个 sheet)
+     *
+     * @param excel    文件
+     * @param rowModel 实体类映射,继承 BaseRowModel 类
+     * @return Excel 数据 list
+     * @throws IOException
+     */
+    public static List<Object> readExcel(MultipartFile excel, BaseRowModel rowModel, String sheetName) throws IOException {
+        ExcelListener excelListener = new ExcelListener();
+        ExcelReader reader = getReader(excel, excelListener);
+        if (reader == null) {
+            return null;
+        }
+        for (Sheet sheet : reader.getSheets()) {
+            if (rowModel != null) {
+                sheet.setClazz(rowModel.getClass());
+            }
+            //读取指定名称的sheet
+            if (sheet.getSheetName().contains(sheetName)) {
+                reader.read(sheet);
+                break;
+            }
+        }
+        return excelListener.getDatas();
+    }
+
+    /**
+     * 返回 ExcelReader
+     *
+     * @param excel         需要解析的 Excel 文件
+     * @param excelListener new ExcelListener()
+     * @throws IOException
+     */
+    private static ExcelReader getReader(MultipartFile excel, ExcelListener excelListener) throws IOException {
+        String filename = excel.getOriginalFilename();
+        if (filename != null && (filename.toLowerCase().endsWith(".xls") || filename.toLowerCase().endsWith(".xlsx"))) {
+            InputStream is = new BufferedInputStream(excel.getInputStream());
+            return new ExcelReader(is, null, excelListener, false);
+        } else {
+            return null;
+        }
+    }
+
+}

+ 95 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/ExcelListener.java

@@ -0,0 +1,95 @@
+package com.crm.rely.backend.util;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.alibaba.excel.metadata.BaseRowModel;
+import com.alibaba.excel.metadata.ExcelHeadProperty;
+
+/**
+ * @Author: houn
+ * @Date: 2020/12/16 21:36
+ * @Description:
+ */
+public class  ExcelListener<T extends BaseRowModel>  extends AnalysisEventListener<T> {
+
+    //自定义用于暂时存储data
+    private List<T> datas = new ArrayList<>();
+    //导入表头
+    private String importHeads = "";
+    //模版表头
+    private String modelHeads = "";
+
+    /**
+     * 通过 AnalysisContext 对象还可以获取当前 sheet,当前行等数据
+     */
+    @Override
+    public void invoke(T o, AnalysisContext analysisContext) {
+        Integer currentRowNum = analysisContext.getCurrentRowNum();
+        //获取导入表头,默认第一行为表头
+        if(currentRowNum == 0){
+            try {
+                Map<String,Object> m = objToMap(o);
+                for (Object v : m.values()) {
+                    importHeads += String.valueOf(v).trim() + ",";
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }else{
+            datas.add(o);
+        }
+    }
+
+    /**
+     * 读取完之后的操作
+     */
+    @Override
+    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+        //获取模版表头
+        ExcelHeadProperty ehp = analysisContext.getExcelHeadProperty();
+        for(List<String> s : ehp.getHead()){
+            modelHeads += s.get(0) + ",";
+        }
+    }
+
+    public List<T> getDatas() {
+        return datas;
+    }
+
+    public void setDatas(List<T> datas) {
+        this.datas = datas;
+    }
+
+    public String getImportHeads() {
+        return importHeads;
+    }
+
+    public void setImportHeads(String importHeads) {
+        this.importHeads = importHeads;
+    }
+
+    public String getModelHeads() {
+        return modelHeads;
+    }
+
+    public void setModelHeads(String modelHeads) {
+        this.modelHeads = modelHeads;
+    }
+
+    //Object转换为Map
+    public  Map<String,Object> objToMap(Object obj) throws Exception{
+        Map<String,Object> map = new LinkedHashMap<String, Object>();
+        Field[] fields = obj.getClass().getDeclaredFields();
+        for(Field field : fields){
+            field.setAccessible(true);
+            map.put(field.getName(), field.get(obj));
+        }
+        return map;
+    }
+}

+ 229 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/ExportUtil.java

@@ -0,0 +1,229 @@
+package com.crm.rely.backend.util;
+
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.metadata.BaseRowModel;
+import com.alibaba.excel.metadata.Sheet;
+import com.alibaba.excel.support.ExcelTypeEnum;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Created by max on 2019/12/12.
+ */
+public class ExportUtil {
+
+
+    public static <T extends BaseRowModel> void transferToResponse(String fileName, List<T> t, Class<T> clazz, HttpServletResponse response) throws Exception {
+
+        setResponse(fileName, response);
+
+        OutputStream outputStream = response.getOutputStream();
+
+        outputStream(outputStream, t, clazz);
+    }
+
+    public static <T extends BaseRowModel> void transferListToResponse(String fileName, List<List<T>> t,  List<String> sheetNameList,  Class<T> clazz, HttpServletResponse response) throws Exception {
+
+        setResponse(fileName, response);
+
+        OutputStream outputStream = response.getOutputStream();
+
+        outputListStream(outputStream, t, clazz, sheetNameList);
+    }
+
+    public static <T extends BaseRowModel> void transferToFile(String path, String fileName, List<T> t, Class<T> clazz) throws Exception {
+
+        String fileFullPath = path + File.separator + fileName;
+        FileOutputStream fos = new FileOutputStream(new File(fileFullPath));
+        ExcelWriter excelWriter = new ExcelWriter(fos, ExcelTypeEnum.XLSX);
+        Sheet sheet = new Sheet(1, 0, clazz);
+        excelWriter.write(t, sheet);
+
+        excelWriter.finish();
+        if (fos != null) {
+            fos.close();
+        }
+
+    }
+
+    public static <T extends BaseRowModel> void transferToFile(String path, String fileName, String sheetName, List<T> t, Class<T> clazz) throws Exception {
+        String fileFullPath = path + File.separator + fileName;
+        FileOutputStream fos = new FileOutputStream(new File(fileFullPath));
+        ExcelWriter excelWriter = new ExcelWriter(fos, ExcelTypeEnum.XLSX);
+        Sheet sheet = new Sheet(1, 0, clazz);
+        sheet.setSheetName(sheetName);
+        excelWriter.write(t, sheet);
+
+        excelWriter.finish();
+        if (fos != null) {
+            fos.close();
+        }
+    }
+
+    public static <T extends BaseRowModel> void transferToFile(String path, String fileName, List<String> sheetNameList, List<List<T>> t, Class<T> clazz) throws Exception {
+        String fileFullPath = path + File.separator + fileName;
+        FileOutputStream fos = new FileOutputStream(new File(fileFullPath));
+        ExcelWriter excelWriter = new ExcelWriter(fos, ExcelTypeEnum.XLSX);
+
+        for (int i = 0; i < t.size(); i++) {
+            List<T> list = t.get(i);
+            Sheet sheet = new Sheet(i+1, 0, clazz);
+            sheet.setSheetName(sheetNameList.get(i));
+            excelWriter.write(list, sheet);
+        }
+
+        excelWriter.finish();
+        if (fos != null) {
+            fos.close();
+        }
+    }
+
+    public static <T extends BaseRowModel> void transferToExcelResponse(String fileNamePrefix, List<T> t, Class<T> clazz, HttpServletResponse response) throws Exception {
+        transferToResponse(genFileName(fileNamePrefix), t, clazz, response);
+    }
+
+    public static <T extends BaseRowModel> byte[] getDataToByte(List<T> t, Class<T> clazz) {
+
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+        outputStream(os, t, clazz);
+
+        return os.toByteArray();
+    }
+
+    public static void transferToResponse(String fileName, byte[] data, HttpServletResponse response) throws Exception {
+
+        setResponse(fileName, response);
+
+        OutputStream outputStream = response.getOutputStream();
+
+        try {
+            outputStream.write(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static void transferToResponse(String fileName, String absolutePath, HttpServletResponse response) throws Exception {
+
+        setResponse(fileName, response);
+
+        OutputStream outputStream = response.getOutputStream();
+        String filePath = absolutePath + File.separator + fileName;
+        try (FileInputStream fileInputStream = new FileInputStream(filePath)) {
+            byte[] buffer = new byte[1024];
+            int bytesRead;
+            while ((bytesRead = fileInputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 设置response 导出文件的格式和文件名称
+     *
+     * @param fileName
+     * @param response
+     * @throws Exception
+     */
+    private static void setResponse(String fileName, HttpServletResponse response) throws Exception {
+        response.setContentType("application/force-download");
+        response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
+
+    }
+
+    /**
+     * 吧T数据转化为excel 并把数据写到outputStream流中
+     *
+     * @param outputStream
+     * @param t
+     * @param clazz
+     * @param <T>
+     */
+    private static <T extends BaseRowModel> void outputStream(OutputStream outputStream, List<T> t, Class<T> clazz) {
+        try {
+            ExcelWriter excelWriter = new ExcelWriter(outputStream, ExcelTypeEnum.XLSX);
+            Sheet sheet = new Sheet(1, 0, clazz);
+            excelWriter.write(t, sheet);
+            excelWriter.finish();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 吧T数据转化为excel 并把数据写到outputStream流中
+     *
+     * @param outputStream
+     * @param t
+     * @param clazz
+     * @param <T>
+     */
+    private static <T extends BaseRowModel> void outputListStream(OutputStream outputStream, List<List<T>> t, Class<T> clazz, List<String> sheetNameList) {
+        try {
+            ExcelWriter excelWriter = new ExcelWriter(outputStream, ExcelTypeEnum.XLSX);
+
+            for (int i = 0; i < t.size(); i++) {
+                List<T> list = t.get(i);
+                Sheet sheet = new Sheet(i+1, 0, clazz);
+                sheet.setSheetName(sheetNameList.get(i));
+                excelWriter.write(list, sheet);
+            }
+            excelWriter.finish();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static String genFileName(String fileNamePrefix) {
+        StringBuffer stringBuffer = new StringBuffer();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        stringBuffer.append(fileNamePrefix);
+        stringBuffer.append("_");
+        stringBuffer.append(sdf.format(new Date()));
+        stringBuffer.append(".xlsx");
+        String fileName = stringBuffer.toString();
+        return fileName;
+    }
+}

+ 336 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/FileProcessUtil.java

@@ -0,0 +1,336 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.exception.ServiceException;
+import com.google.common.base.Strings;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.nio.file.FileVisitOption;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
+/**
+ * 文件处理工具类
+ */
+public class FileProcessUtil {
+    private static String encoding = "UTF-8";
+    private static DeleteDirectory walk;
+    private static EnumSet opts;
+
+    static {
+        walk = new DeleteDirectory();
+        opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
+    }
+
+    /**
+     * 普通文件上传
+     * 1.获取文件扩展名
+     * 2.生成新文件名(UUID)
+     * 3.检测是否存在目录
+     * 4.保存文件
+     * 5.返回新文件相对路径
+     *
+     * @param file     文件
+     * @param savePath 保存路径
+     * @return 保存后的相对路径
+     */
+    public static String upload(MultipartFile file, String savePath) {
+
+        return upload(file, false, savePath);
+    }
+
+    public static String upload(MultipartFile file, boolean isSaveFileName, String savePath) {
+
+        /**
+         * 获取文件名
+         */
+        String fileName = file.getOriginalFilename();
+
+        fileName = getFileName(fileName, isSaveFileName);
+
+        try {
+            return upload(file.getBytes(), fileName, savePath);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        return "/" + fileName;
+    }
+
+    public static String upload(String fileName, InputStream inputStream, String savePath) {
+
+        return upload(fileName, false, inputStream, savePath);
+    }
+
+    public static String upload(String fileName, boolean isSaveFileName, InputStream inputStream, String savePath) {
+
+
+        fileName = getFileName(fileName, isSaveFileName);
+
+
+        try {
+            return upload(readAllBytes(inputStream), fileName, savePath);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static String upload(byte[] bytes, String fileName, String savePath) {
+        savePath += "/" + fileName;
+        try {
+            Path path = getPath(savePath);
+            Files.createDirectories(path.getParent());
+            Files.createFile(path);
+            Files.write(path, bytes);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        return "/" + fileName;
+    }
+
+    public static byte[] readAllBytes(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        byte[] temp = new byte[1024]; // 每次读取的缓冲区大小
+        int bytesRead;
+        while ((bytesRead = inputStream.read(temp)) != -1) {
+            buffer.write(temp, 0, bytesRead);
+        }
+        return buffer.toByteArray();
+    }
+
+    private static String getFileName(String fileName, boolean isSaveFileName) {
+        if (isSaveFileName) {
+            fileName = fileName.substring(0, fileName.lastIndexOf("."));
+        } else {
+            /**
+             * 获取文件的后缀名
+             */
+            String suffixName = fileName.substring(fileName.lastIndexOf("."));
+            fileName = UUIDUtil.getUUID() + suffixName;
+        }
+        return fileName;
+    }
+
+    /**
+     * 删除空目录
+     *
+     * @param path 将要删除的目录路径
+     */
+    public static void deleteFile(String path) throws Exception {
+        if (Strings.isNullOrEmpty(path) || Strings.isNullOrEmpty(path.trim())) {
+            return;
+        }
+        Path directory = getPath(path);
+
+        deleteFile(directory);
+    }
+
+    /**
+     * 批量删除文件
+     *
+     * @param paths
+     * @throws Exception
+     */
+    public static void deleteFiles(List<String> paths) throws Exception {
+        for (String path : paths) {
+
+            deleteFile(path);
+        }
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param path
+     * @throws Exception
+     */
+    public static void deleteFile(Path path) throws Exception {
+
+        Files.walkFileTree(path, opts, Integer.MAX_VALUE, walk);
+    }
+
+    /**
+     * 文件移动
+     *
+     * @param sourcePath
+     * @param destPath
+     * @throws Exception
+     */
+    public static void move(String sourcePath, String destPath) throws Exception {
+
+        Path source = getPath(sourcePath);
+
+        Path dest = getPath(destPath);
+        Files.createDirectories(dest.getParent());
+        move(source, dest);
+
+    }
+
+    /**
+     * 文件移动
+     *
+     * @param source
+     * @param dest
+     * @throws Exception
+     */
+    public static void move(Path source, Path dest) throws Exception {
+
+        Files.move(source, dest, REPLACE_EXISTING, ATOMIC_MOVE);
+
+    }
+
+    /**
+     * 读取文件内容
+     *
+     * @param filePathName
+     * @return
+     */
+    public static String getFileContent(String filePathName) {
+
+        try {
+            Path path = getPath(filePathName);
+            byte[] filecontent = Files.readAllBytes(path);
+            String content = new String(filecontent, encoding);
+            return content;
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+            throw new RuntimeException("文件读取失败");
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 通过相对或绝对路径获取path真实路径
+     *
+     * @param savePath
+     * @return
+     * @throws IOException
+     */
+    public static Path getPath(String savePath) throws IOException {
+        Path path = null;
+        if (savePath.startsWith("./") || savePath.startsWith("../")) {
+
+            File rootFile = new File("");
+            File file = new File(rootFile.getAbsolutePath(), savePath);
+            path = Paths.get(file.getCanonicalPath()).normalize();
+
+        } else {
+            path = Paths.get(savePath).normalize();
+        }
+
+        return path;
+    }
+
+    public static File getFile(String savePath) throws IOException {
+        
+        return getPath(savePath).toFile();
+    }
+
+    public static File getFile(Path path) {
+
+        return path.toFile();
+    }
+
+    /**
+     * 获取文件内容并替换
+     *
+     * @param filePathName 文件路径及名称
+     * @param map          替换内容
+     * @return
+     */
+    public static String getFileContentAndReplace(String filePathName, Map<String, String> map) {
+        String content = getFileContent(filePathName);
+
+        return TemplateUtil.replace(content, map);
+    }
+
+    /**
+     * 生成导出文件名
+     *
+     * @param namePrefix 名称前缀
+     * @return
+     * @throws Exception
+     */
+    public static String genExportFileName(String namePrefix) {
+        StringBuffer stringBuffer = new StringBuffer();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        stringBuffer.append(namePrefix == null ? "IB_EXPORT" : namePrefix);
+        stringBuffer.append("_");
+        stringBuffer.append(sdf.format(new Date()));
+        stringBuffer.append(".xlsx");
+        String fileName = stringBuffer.toString();
+        return fileName;
+    }
+
+    /**
+     * 压缩文件
+     *
+     * @param inputPath     来源文件的绝对路径
+     * @param files         需要压缩的文件名列表
+     * @param zipOutputPath 压缩后文件的绝对路径及名称
+     * @throws Exception
+     */
+    public static void zipFiles(String inputPath, List<String> files, String zipOutputPath) throws Exception {
+
+        try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipOutputPath))) {
+            for (String file : files) {
+                // 创建ZipEntry对象并设置文件名
+                ZipEntry entry = new ZipEntry(file);
+                zipOutputStream.putNextEntry(entry);
+
+                // 读取文件内容并写入Zip文件
+                try (FileInputStream fileInputStream = new FileInputStream(inputPath + File.separator + file)) {
+                    byte[] buffer = new byte[1024];
+                    int bytesRead;
+                    while ((bytesRead = fileInputStream.read(buffer)) != -1) {
+                        zipOutputStream.write(buffer, 0, bytesRead);
+                    }
+                }
+
+                // 完成当前文件的压缩
+                zipOutputStream.closeEntry();
+            }
+
+        } catch (Exception e) {
+            throw ServiceException.exception(e.getMessage());
+        }
+
+    }
+
+    public static void main(String[] args) throws Exception {
+//        List<String> paths = new ArrayList<String>();
+//        paths.add("./upload/3/12/image - 副本 - 副本/");
+//        paths.add("./upload/3/12/image - 副本/");
+//        deleteFiles(paths);
+////        move("./upload/3/9/image/", "./upload/3/12/image/");
+
+//        Path path1 = getPath("../upload/abc.jpg");
+//        byte[] bytes = Files.readAllBytes(path1);
+//        String str = "F:\\iemans-backend\\target\\classes\\upload\\1\\2f5aa03113a84d239f532b80fb4077e3.png";
+//        Path path = getPath(str);
+//
+//        Files.createDirectories(path.getParent());
+//        Files.createFile(path);
+//        Files.write(path, bytes);
+
+//        System.out.println(getFileContent("../123123.txt"));
+        deleteFile("G:\\crm\\crm-back\\IP2LOCATION\\download\\202206");
+
+    }
+
+
+}

+ 63 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/FloatFormat.java

@@ -0,0 +1,63 @@
+package com.crm.rely.backend.util;
+
+
+/**
+ * Created by max on 2017/9/25.
+ */
+public class FloatFormat {
+
+    public static float formatFloat(float source, int save) {
+        float dest = 0F;
+        String formatPattern = "%." + save + "f";
+        String sourceStr = String.format(formatPattern, source);
+        dest = Float.parseFloat(sourceStr);
+        return dest;
+    }
+
+    public static float formatFloat(float source) {
+        return formatFloat(source, 2);
+    }
+
+    public static String formatToPercent(Float source) {
+        if (source == null) source = 0F;
+        try {
+            if (source == 0) {
+                return "0%";
+            } else {
+                return String.format("%.2f%s", source, "%");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "";
+        }
+
+    }
+
+    public static double formatDouble(double source, int save) {
+        double dest = 0F;
+        String formatPattern = "%." + save + "f";
+        String sourceStr = String.format(formatPattern, source);
+        dest = Double.parseDouble(sourceStr);
+        return dest;
+    }
+
+    public static double formatDouble(double source) {
+        return formatDouble(source, 2);
+    }
+
+    public static String formatDoubleToPercent(Double source) {
+        if (source == null) source = 0D;
+        try {
+            if (source == 0) {
+                return "0%";
+            } else {
+                return String.format("%.2f%s", source, "%");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "";
+        }
+    }
+
+
+}

+ 252 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/FormatPage.java

@@ -0,0 +1,252 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.core.dto.base.PageDto;
+import com.crm.rely.backend.core.entity.base.BaseIbReportListEntity;
+import com.crm.rely.backend.core.entity.base.BaseSearchPageEntity;
+import com.crm.rely.backend.core.entity.base.PageEntity;
+import com.crm.rely.backend.core.entity.ib.trade.TradeShardingCountEntity;
+import com.crm.rely.backend.core.entity.ib.trade.TradeShardingPageEntity;
+import com.google.common.collect.Lists;
+import org.springframework.beans.BeanUtils;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 分页处理工具
+ */
+public class FormatPage {
+
+    /**
+     * 格式化分页 返回分页dto
+     *
+     * @param current  单前页
+     * @param row      每页显示条数
+     * @param rowTotal 总条数
+     * @return
+     */
+    public static PageDto format(int current, int row, int rowTotal) {
+
+        int pageTotal = (rowTotal % row) == 0 ? (rowTotal / row) : (rowTotal / row) + 1;
+
+        if (current > pageTotal) {
+            current = pageTotal;
+        }
+        return format(current, row, pageTotal, rowTotal);
+    }
+
+    /**
+     * 格式化分页 返回分页dto
+     *
+     * @param page     page com.crm.rely.backend.entity(current未处理过 错误数据)
+     * @param rowTotal 总条数
+     * @param <T>
+     * @return
+     */
+    public static <T extends PageEntity> PageDto format(T page, Integer rowTotal) {
+        if (rowTotal == null) {
+            rowTotal = 0;
+        }
+
+        return format(page, (int) rowTotal);
+    }
+
+    public static <T extends BaseSearchPageEntity> PageDto format(T searchPageEntity, Integer rowTotal) {
+
+        if (rowTotal == null) {
+            rowTotal = 0;
+        }
+
+        return format(searchPageEntity, (int) rowTotal);
+    }
+
+    public static <T extends BaseSearchPageEntity> PageDto format(T searchPageEntity, int rowTotal) {
+
+        return format(searchPageEntity.getPage(), rowTotal);
+    }
+
+    /**
+     * 格式化分页 返回分页dto
+     *
+     * @param page     分页参数
+     * @param rowTotal 总条数
+     * @param <T>
+     * @return
+     */
+    public static <T extends PageEntity> PageDto format(T page, int rowTotal) {
+
+        int pageTotal = 0;
+        int current = page.getCurrent();
+
+        if (rowTotal == 0) {
+            pageTotal = 1;
+        } else {
+            pageTotal = (rowTotal % page.getRow()) == 0 ? (rowTotal / page.getRow()) : (rowTotal / page.getRow()) + 1;
+        }
+
+        if (current > pageTotal) {
+            current = pageTotal;
+        }
+
+        return format(current, page.getRow(), pageTotal, rowTotal);
+    }
+
+    /**
+     * 格式化 并返回分页dto
+     *
+     * @param page
+     * @param rowTotal
+     * @param <T>
+     * @return
+     */
+    public static <T extends PageEntity> PageDto format(T page, Long rowTotal) {
+        if (rowTotal == null) {
+            rowTotal = 0L;
+        }
+
+        return format(page, (long) rowTotal);
+    }
+
+    /**
+     * 格式化 并返回分页dto
+     *
+     * @param page
+     * @param rowTotal
+     * @param <T>
+     * @return
+     */
+    public static <T extends PageEntity> PageDto format(T page, long rowTotal) {
+
+        return format(page, (int) rowTotal);
+    }
+
+    /**
+     * 格式化 并返回分页dto
+     *
+     * @param current   当前页
+     * @param row       每页显示条数
+     * @param pageTotal 总页数
+     * @param rowTotal  总条数
+     * @return
+     */
+    private static PageDto format(int current, int row, int pageTotal, int rowTotal) {
+        PageDto page = new PageDto();
+        page.setCurrent(current);
+        page.setRow(row);
+        page.setPageTotal(pageTotal);
+        page.setRowTotal(rowTotal);
+        return page;
+    }
+
+    /**
+     * 构建分页数据
+     *
+     * @param pageEntity
+     * @param <T>
+     * @return
+     */
+    public static <T extends PageEntity> Pageable formatPageable(T pageEntity) {
+
+        return formatPageable(pageEntity, null);
+    }
+
+    /**
+     * 构建分页及排序
+     *
+     * @param pageEntity 分页参数 前端传输的数据
+     * @param map        排序参数 如使用默认排序 不指定特定排序
+     * @param <T>
+     * @return
+     */
+    public static <T extends PageEntity> Pageable formatPageable(T pageEntity, Map<String, String> map) {
+        PageRequest request = null;
+        if (map != null && map.size() > 0) {
+            /**
+             * 排序
+             */
+            List<Sort.Order> olist = new ArrayList(map.size());
+            for (Map.Entry<String, String> stringStringEntry : map.entrySet()) {
+                olist.add(new Sort.Order(stringStringEntry.getValue().toLowerCase().equals("asc") ? Sort.Direction.ASC : Sort.Direction.DESC, stringStringEntry.getKey()));
+            }
+            Sort sort = new Sort(olist);
+            //分页
+            request = new PageRequest(pageEntity.getCurrent() - 1, pageEntity.getRow(), sort);
+        } else {
+            request = new PageRequest(pageEntity.getCurrent() - 1, pageEntity.getRow());
+        }
+
+        return request;
+    }
+
+    /**
+     * 分表分页工具:根据各月份行数拆分分页查询实体
+     *
+     * 功能增强:
+     * 1. 移除反序导致的分页错乱问题;
+     * 2. 增加 distinct 过滤;
+     * 3. 增加详细日志输出,方便定位分页异常;
+     * 4. 防止 offset 与 row 累计越界。
+     */
+    public static <T extends BaseIbReportListEntity> List<T> getShardingEntities(
+            T entity, List<TradeShardingCountEntity> counts) throws Exception {
+
+        List<T> entities = Lists.newArrayList();
+
+        if (counts == null || counts.isEmpty()) {
+            return entities;
+        }
+
+        // 月份去重 + 按时间升序排序(假设 yearMonth 格式为 yyyyMM)
+        counts = counts.stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.collectingAndThen(
+                        Collectors.toMap(TradeShardingCountEntity::getYm, v -> v, (v1, v2) -> v1, LinkedHashMap::new),
+                        map -> map.values().stream()
+                                .sorted(Comparator.comparing(TradeShardingCountEntity::getYm))
+                                .collect(Collectors.toList())
+                ));
+
+        // 分页控制变量
+        int shardingOffset = entity.getPage().getOffset();
+        int increasing = 0;
+        int remainRows = entity.getPage().getRow();
+
+        for (TradeShardingCountEntity countEntity : counts) {
+            increasing += countEntity.getCount();
+
+            // 当累计行数超过 offset 时,说明该分表有数据需要查询
+            if (increasing > shardingOffset) {
+                int before = increasing - countEntity.getCount();
+                int localOffset = Math.max(shardingOffset - before, 0);
+                int localRow = Math.min(countEntity.getCount() - localOffset, remainRows);
+
+                if (localRow <= 0) continue;
+
+                T clone = (T) entity.getClass().newInstance();
+                BeanUtils.copyProperties(entity, clone);
+
+                TradeShardingPageEntity shardingPageEntity = new TradeShardingPageEntity();
+                shardingPageEntity.setOffset(localOffset);
+                shardingPageEntity.setRow(localRow);
+
+                clone.setShardingPage(shardingPageEntity);
+                clone.setYm(countEntity.getYm());
+
+                entities.add(clone);
+
+                remainRows -= localRow;
+                if (remainRows <= 0) break;
+            }
+        }
+
+        // 去重(防止同一月份被多次加入)
+        Map<String, T> distinctMap = entities.stream()
+                .collect(Collectors.toMap(BaseIbReportListEntity::getYm, v -> v, (v1, v2) -> v1, LinkedHashMap::new));
+
+        return new ArrayList<>(distinctMap.values());
+    }
+
+}

+ 321 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/GetIpAndMac.java

@@ -0,0 +1,321 @@
+package com.crm.rely.backend.util;
+
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.*;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+
+public class GetIpAndMac {
+    private static final String[] HEADERS_TO_TRY = {
+            "X-Forwarded-For",
+            "Proxy-Client-IP",
+            "WL-Proxy-Client-IP",
+            "HTTP_X_FORWARDED_FOR",
+            "HTTP_X_FORWARDED",
+            "HTTP_X_CLUSTER_CLIENT_IP",
+            "HTTP_CLIENT_IP",
+            "HTTP_FORWARDED_FOR",
+            "HTTP_FORWARDED",
+            "HTTP_VIA",
+            "REMOTE_ADDR"
+    };
+
+    private static final List<IPRange> LOCAL_RANGES = Arrays.asList(
+            IPRange.parse("10.0.0.0/8"),
+            IPRange.parse("172.16.0.0/12"),
+            IPRange.parse("192.168.0.0/16"),
+            IPRange.parse("127.0.0.1/32"),
+            IPRange.parse("::1/128")
+    );
+
+
+    /**
+     * 获取IP地址
+     *
+     * @param request
+     * @return
+     */
+    public static String getIp(HttpServletRequest request) {
+        String ip = null;
+        for (String header : HEADERS_TO_TRY) {
+            ip = request.getHeader(header);
+            if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
+                break;
+            }
+        }
+
+        // 如果请求来自Cloudflare代理,确认客户端真实IP地址
+        if (ip != null && !ip.isEmpty() && request.getHeader("CF-Connecting-IP") != null) {
+            String cfConnectingIP = request.getHeader("CF-Connecting-IP");
+            if (!cfConnectingIP.isEmpty() && !"unknown".equalsIgnoreCase(cfConnectingIP) && isIPAddress(cfConnectingIP) && !isLocalAddress(cfConnectingIP)) {
+                ip = cfConnectingIP;
+            }
+        }
+
+        // 如果请求来自Nginx代理,确认客户端真实IP地址
+        if (ip != null && !ip.isEmpty() && request.getHeader("X-Real-IP") != null) {
+            String xRealIP = request.getHeader("X-Real-IP");
+            if (!xRealIP.isEmpty() && !"unknown".equalsIgnoreCase(xRealIP) && isIPAddress(xRealIP) && !isLocalAddress(xRealIP)) {
+                ip = xRealIP;
+            }
+        }
+
+        // 如果HTTP头中存在多个IP地址,选择第一个非本地地址作为客户端真实IP地址
+        if (ip != null && ip.contains(",")) {
+            String[] ips = ip.split(",");
+            if (ips.length > 0) {
+                ip = ips[0];
+            }
+            for (String i : ips) {
+                i = i.trim();
+                if (!i.isEmpty() && !"unknown".equalsIgnoreCase(i) && isIPAddress(i) && !isLocalAddress(i)) {
+                    ip = i;
+                    break;
+                }
+            }
+        }
+        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        if (!IPRange.isIPv4Address(ip)) {
+            ip = getIpv4(ip);
+        }
+
+        return ip;
+    }
+
+    private static boolean isIPAddress(String ipAddress) {
+//        return IPRange.isIPv4Address(ipAddress) || IPRange.isIPv6Address(ipAddress);
+        return IPRange.isIPv4Address(ipAddress);
+    }
+
+    private static boolean isLocalAddress(String ipAddress) {
+        for (IPRange range : LOCAL_RANGES) {
+            if (range.contains(ipAddress)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    /**
+     * unicode 转换成 中文
+     */
+    private static String decodeUnicode(String theString) {
+        char aChar;
+        int len = theString.length();
+        StringBuffer outBuffer = new StringBuffer(len);
+        for (int x = 0; x < len; ) {
+            aChar = theString.charAt(x++);
+            if (aChar == '\\') {
+                aChar = theString.charAt(x++);
+                if (aChar == 'u') {
+                    int value = 0;
+                    for (int i = 0; i < 4; i++) {
+                        aChar = theString.charAt(x++);
+                        switch (aChar) {
+                            case '0':
+                            case '1':
+                            case '2':
+                            case '3':
+                            case '4':
+                            case '5':
+                            case '6':
+                            case '7':
+                            case '8':
+                            case '9':
+                                value = (value << 4) + aChar - '0';
+                                break;
+                            case 'a':
+                            case 'b':
+                            case 'c':
+                            case 'd':
+                            case 'e':
+                            case 'f':
+                                value = (value << 4) + 10 + aChar - 'a';
+                                break;
+                            case 'A':
+                            case 'B':
+                            case 'C':
+                            case 'D':
+                            case 'E':
+                            case 'F':
+                                value = (value << 4) + 10 + aChar - 'A';
+                                break;
+                            default:
+                                throw new IllegalArgumentException(
+                                        "Malformed      encoding.");
+                        }
+                    }
+                    outBuffer.append((char) value);
+                } else {
+                    if (aChar == 't') {
+                        aChar = '\t';
+                    } else if (aChar == 'r') {
+                        aChar = '\r';
+                    } else if (aChar == 'n') {
+                        aChar = '\n';
+                    } else if (aChar == 'f') {
+                        aChar = '\f';
+                    }
+                    outBuffer.append(aChar);
+                }
+            } else {
+                outBuffer.append(aChar);
+            }
+        }
+        return outBuffer.toString();
+    }
+
+    //获取ip
+    public static String getLocalIP() {
+        String localIP = "127.0.0.1";
+        try {
+            OK:
+            for (Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements(); ) {
+                NetworkInterface networkInterface = interfaces.nextElement();
+                if (networkInterface.isLoopback() || networkInterface.isVirtual() || !networkInterface.isUp()) {
+                    continue;
+                }
+                Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
+                while (addresses.hasMoreElements()) {
+                    InetAddress address = addresses.nextElement();
+                    if (address instanceof Inet4Address) {
+                        localIP = address.getHostAddress();
+                        break OK;
+                    }
+                }
+            }
+        } catch (SocketException e) {
+            e.printStackTrace();
+        }
+        return localIP;
+    }
+
+    public static String getIpv4(String ipv6) {
+        String ipv4 = null;
+        try {
+            InetAddress inet6 = InetAddress.getByName(ipv6);
+            byte[] bytes = inet6.getAddress();
+            if (bytes.length == 16) {
+                byte[] b = new byte[4];
+                System.arraycopy(bytes, 12, b, 0, 4);
+                InetAddress inet4 = InetAddress.getByAddress(b);
+                ipv4 = inet4.getHostAddress();
+            }
+        } catch (UnknownHostException e) {
+            e.printStackTrace();
+        }
+        return ipv4;
+    }
+
+    //通过request获取ip
+    public static String getIp() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getServerName();
+    }
+
+    //通过request获取端口
+    public static String getLocalPort() {
+        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getServerPort() + "";
+    }
+
+    public static String getIpAndPort() {
+        return getLocalIP() + ":" + getLocalPort();
+    }
+
+    public static String getClientIp(HttpServletRequest req) {
+        return req.getHeader("CLIENT");
+    }
+
+
+    public static void main(String[] args) {
+//        String ip = "240e:304:2c80:e037:962:a9aa:f1ec:62d";
+        String ip = "2408:8456:f11c:883d::1";
+        try {
+            if (!IPRange.isIPv4Address(ip)) {
+                ip = getIpv4(ip);
+            }
+            System.out.println(ip);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
+
+class IPRange {
+    private final long start;
+    private final long end;
+
+    private IPRange(long start, long end) {
+        this.start = start;
+        this.end = end;
+    }
+
+    public static IPRange parse(String cidr) {
+        String[] parts = cidr.split("/");
+        String ip = parts[0];
+        int prefixLength = Integer.parseInt(parts[1]);
+        long start = ipToLong(ip);
+        long end = start | ((1L << (32 - prefixLength)) - 1);
+        return new IPRange(start, end);
+    }
+
+    public boolean contains(String ipAddress) {
+        long ip = ipToLong(ipAddress);
+        return ip >= start && ip <= end;
+    }
+
+    private static long ipToLong(String ipAddress) {
+        if (isIPv4Address(ipAddress)) {
+            String[] parts = ipAddress.split("\\.");
+            return (Long.parseLong(parts[0]) << 24) +
+                    (Long.parseLong(parts[1]) << 16) +
+                    (Long.parseLong(parts[2]) << 8) +
+                    Long.parseLong(parts[3]);
+        } else if (isIPv6Address(ipAddress)) {
+            String[] parts = ipAddress.split(":");
+            long hi = 0;
+            long lo = 0;
+            boolean inHi = true;
+            for (String part : parts) {
+                if (part.isEmpty()) {
+                    inHi = false;
+                    continue;
+                }
+                long value = Long.parseLong(part, 16);
+                if (inHi) {
+                    hi = (hi << 16) | value;
+                } else {
+                    lo = (lo << 16) | value;
+                }
+            }
+            return (hi << 64) | lo;
+        } else {
+            throw new IllegalArgumentException("Invalid IP address: " + ipAddress);
+        }
+    }
+
+    protected static boolean isIPv4Address(String ipAddress) {
+        return ipAddress.matches("^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+                "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+                "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+                "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
+    }
+
+    protected static boolean isIPv6Address(String ipAddress) {
+        return ipAddress.matches("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$") ||
+                ipAddress.matches("^([0-9a-fA-F]{1,4}:){6}:([0-9a-fA-F]{1,4}:){1,2}[0-9a-fA-F]{1,4}$") ||
+                ipAddress.matches("^([0-9a-fA-F]{1,4}:){5}:([0-9a-fA-F]{1,4}:){1,3}[0-9a-fA-F]{1,4}$") ||
+                ipAddress.matches("^([0-9a-fA-F]{1,4}:){4}:([0-9a-fA-F]{1,4}:){1,4}[0-9a-fA-F]{1,4}$") ||
+                ipAddress.matches("^([0-9a-fA-F]{1,4}:){3}:([0-9a-fA-F]{1,4}:){1,5}[0-9a-fA-F]{1,4}$") ||
+                ipAddress.matches("^([0-9a-fA-F]{1,4}:){2}:([0-9a-fA-F]{1,4}:){1,6}[0-9a-fA-F]{1,4}$") ||
+                ipAddress.matches("^([0-9a-fA-F]{1,4}:){1}:([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}$") ||
+                ipAddress.matches("^:((:[0-9a-fA-F]{1,4}){1,7}|:)$");
+    }
+}

+ 158 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/GetRandom.java

@@ -0,0 +1,158 @@
+package com.crm.rely.backend.util;
+
+
+import com.google.common.base.Strings;
+
+import java.util.Date;
+import java.util.Random;
+
+/**
+ * 获取随机数
+ */
+public class GetRandom {
+
+    private static int randomNum = 6;
+    private static int randomPasswordNum = 8;
+    private static String randNumString = "0123456789";
+    private static String randLowerCaseLString = "abcdefghijklmnopqrstuvwxyz";
+    private static String randUpperCaseString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    private static String randSpecialString = "!\"#$%&'()*+,-./:;=>?@[\\]^_`{|}~";
+    private static Random random = new Random();
+
+    /**
+     * 返回默认六位数字符串
+     *
+     * @return String
+     */
+    public static String getRandom() {
+
+        return getRandom(randomNum);
+    }
+
+    /**
+     * 返回指定位数字符串
+     *
+     * @param num 指定位数
+     * @return
+     */
+    public static String getRandom(int num) {
+        return getRandom(randNumString, num);
+    }
+
+    public static String getOrderNo(String head) {
+
+        return getOrderNoByFormat("yyyyMMddHHmmssSSS", head);
+    }
+
+    public static String getOrderNo() {
+        return getOrderNo(null);
+    }
+
+    public static synchronized String getOrderNoByFormat(String format, String head) {
+        try {
+            Thread.sleep(10);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        String orderNo = DateUtil.format(format, new Date()) + getRandom(3);
+        if (Strings.isNullOrEmpty(head)) {
+            return orderNo;
+        }
+        return head + orderNo;
+    }
+
+    public static String getSerial(String head) {
+
+        return getOrderNo(head);
+    }
+
+    public static String getSerial() {
+        return getOrderNo(null);
+    }
+
+    /**
+     * 返回指定位数字符串
+     *
+     * @param randString 字符串取值范围
+     * @param num        获取的字符串指定位数(最后返回的长度)
+     * @return
+     */
+    public static String getRandom(String randString, int num) {
+        StringBuffer randomString = new StringBuffer(num);
+        for (int i = 0; i < num; i++) {
+            String rand = String.valueOf(getRandomString(randString, random.nextInt(randString
+                    .length())));
+            randomString.append(rand);
+        }
+        return randomString.toString();
+    }
+
+    /**
+     * 获取随机的字符
+     *
+     * @param randString 字符串取值范围
+     * @param num        字符串所在位置
+     * @return
+     */
+    public static String getRandomString(String randString, int num) {
+        return String.valueOf(randString.charAt(num));
+    }
+
+    public static synchronized String getRandomPasswordString() {
+        return getRandomPasswordString(randomPasswordNum);
+    }
+
+    public static String getRandomPasswordString(int num) {
+        int[] nums = {0, 0, 0, 0};
+        StringBuffer randomString = new StringBuffer(num);
+        String rand;
+        for (int i = 0; i < num; i++) {
+            int caseNum = -1;
+            if (i >= num - 2) {
+                for (int j = 0; j < 4; j++) {
+                    if (nums[j] == 0) {
+                        caseNum = j;
+                        break;
+                    }
+                }
+            }
+            if (caseNum < 0) {
+                caseNum = random.nextInt(4);
+            }
+            switch (caseNum) {
+                case 0:
+                    nums[0] = 1;
+                    rand = String.valueOf(getRandomString(randNumString, random.nextInt(randNumString
+                            .length())));
+                    break;
+                case 1:
+                    nums[1] = 1;
+                    rand = String.valueOf(getRandomString(randLowerCaseLString, random.nextInt(randLowerCaseLString
+                            .length())));
+                    break;
+                case 2:
+                    nums[2] = 1;
+                    rand = String.valueOf(getRandomString(randUpperCaseString, random.nextInt(randUpperCaseString
+                            .length())));
+                    break;
+                case 3:
+                    nums[3] = 1;
+                    rand = String.valueOf(getRandomString(randSpecialString, random.nextInt(randSpecialString
+                            .length())));
+                    break;
+                default:
+                    rand = "";
+                    break;
+            }
+            randomString.append(rand);
+
+
+        }
+        return randomString.toString();
+    }
+
+    public static void main(String[] args) {
+        System.out.println(GetRandom.getRandomPasswordString(8));
+
+    }
+}

+ 203 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/GlobalHttpServletRequestWrapper.java

@@ -0,0 +1,203 @@
+package com.crm.rely.backend.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.crm.rely.backend.core.constant.Constants;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class GlobalHttpServletRequestWrapper extends HttpServletRequestWrapper {
+    private HttpServletRequest orgRequest = null;
+
+    private boolean isIncludeRichText;
+
+    public GlobalHttpServletRequestWrapper(HttpServletRequest request, boolean isIncludeRichText) {
+        super(request);
+        orgRequest = request;
+        this.isIncludeRichText = isIncludeRichText;
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+        String result = readBody(orgRequest);
+        JSONObject jsonObject = JSON.parseObject(result);
+
+        for (Map.Entry<String, Object> stringObjectEntry : jsonObject.entrySet()) {
+            if (stringObjectEntry.getValue() instanceof String) {
+                stringObjectEntry.setValue(clean(stringObjectEntry.getValue().toString()));
+            }
+        }
+        String content=jsonObject.toJSONString();
+        return new CustomServletInputStream(content);
+    }
+
+    public static String readBody(ServletRequest request) {
+        StringBuilder sb = new StringBuilder();
+        String inputLine;
+        BufferedReader br = null;
+        try {
+            br = request.getReader();
+            while ((inputLine = br.readLine()) != null) {
+                sb.append(inputLine);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed users read body.", e);
+        } finally {
+            if (br != null) {
+                try {
+                    br.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 覆盖getParameter方法,将参数名和参数值都过滤。
+     * 如果需要获得原始的值,则通过super.getParameterValues(name)来获取
+     * getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
+     */
+    @Override
+    public String getParameter(String name) {
+
+        Boolean flag = ("content".equals(name) || name.endsWith("WithHtml"));
+        if (flag && !isIncludeRichText) {
+            return super.getParameter(name);
+        }
+        name = clean(name);
+        String value = super.getParameter(name);
+        if (ValidateUtil.vStringNull(value)) {
+            value = clean(value);
+        }
+        return value;
+    }
+
+    @Override
+    public Map getParameterMap() {
+        Map map = super.getParameterMap();
+        // 返回值Map
+        Map<String, String> returnMap = new HashMap<String, String>();
+        Iterator entries = map.entrySet().iterator();
+        Map.Entry entry;
+        String name = "";
+        String value = "";
+        while (entries.hasNext()) {
+            entry = (Map.Entry) entries.next();
+            name = (String) entry.getKey();
+            Object valueObj = entry.getValue();
+            if (null == valueObj) {
+                value = "";
+            } else if (valueObj instanceof String[]) {
+                String[] values = (String[]) valueObj;
+                for (int i = 0; i < values.length; i++) {
+                    value = values[i] + ",";
+                }
+                value = value.substring(0, value.length() - 1);
+            } else {
+                value = valueObj.toString();
+            }
+            returnMap.put(name, clean(value).trim());
+        }
+        return returnMap;
+    }
+
+    @Override
+    public String[] getParameterValues(String name) {
+        String[] arr = super.getParameterValues(name);
+        if (arr != null) {
+            for (int i = 0; i < arr.length; i++) {
+                arr[i] = clean(arr[i]);
+            }
+        }
+        return arr;
+    }
+
+    /**
+     * 覆盖getHeader方法,将参数名和参数值都过滤。
+     * 如果需要获得原始的值,则通过super.getHeaders(name)来获取
+     * getHeaderNames 也可能需要覆盖
+     */
+    @Override
+    public String getHeader(String name) {
+
+        name = clean(name);
+        String value = super.getHeader(name);
+        if (ValidateUtil.vStringNull(value)) {
+            value = clean(value);
+        }
+        return value;
+    }
+
+    /**
+     * 获取最原始的request
+     *
+     * @return
+     */
+    public HttpServletRequest getOrgRequest() {
+        return orgRequest;
+    }
+
+    /**
+     * 获取最原始的request的静态方法
+     *
+     * @return
+     */
+    public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
+        if (req instanceof GlobalHttpServletRequestWrapper) {
+            return ((GlobalHttpServletRequestWrapper) req).getOrgRequest();
+        }
+
+        return req;
+    }
+
+    public String clean(String content) {
+        String result = JsoupUtil.clean(content);
+        if (SensitiveWordFilterUtil.getSensitiveWordMap() != null && SensitiveWordFilterUtil.getSensitiveWordMap().size() > 0) {
+            result = SensitiveWordFilterUtil.replaceSensitiveWord(result, Constants.MIN_MACH_TYPE, "");
+        }
+
+        return result;
+    }
+
+    private class CustomServletInputStream extends ServletInputStream {
+        private ByteArrayInputStream buffer;
+
+        public CustomServletInputStream(String body) {
+            body = body == null ? "" : body;
+            this.buffer = new ByteArrayInputStream(body.getBytes());
+        }
+
+        @Override
+        public int read() throws IOException {
+            return buffer.read();
+        }
+
+        @Override
+        public boolean isFinished() {
+            return buffer.available() == 0;
+        }
+
+        @Override
+        public boolean isReady() {
+            return true;
+        }
+
+        @Override
+        public void setReadListener(ReadListener listener) {
+            throw new RuntimeException("Not implemented");
+        }
+    }
+
+}
+
+
+

+ 583 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/GoogleAdminAPIHttp.java

@@ -0,0 +1,583 @@
+package com.crm.rely.backend.util;
+
+
+import com.alibaba.fastjson.JSON;
+import com.crm.rely.backend.core.pojo.table.UserGoogleEmailTable;
+import com.crm.rely.backend.exception.ServiceException;
+import com.google.common.base.Strings;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.*;
+
+/**
+ * @Author houn
+ * @Date 2025/2/20 17:30
+ * @PackageName:com.crm.manager
+ * @ClassName: GoogleAdminAPIHttp
+ * @Description: TODO
+ */
+@Slf4j
+public class GoogleAdminAPIHttp {
+    private static final String ADMIN_DIRECTORY_USER_API_URL = "https://admin.googleapis" +
+            ".com/admin/directory/v1/users";
+    private static final String TOKEN_URL = "https://oauth2.googleapis.com/token";
+    private static final String SCOPE = String.join(" ",
+            "https://www.googleapis.com/auth/admin.directory.user",
+            "https://www.googleapis.com/auth/admin.directory.group"
+    );
+
+    public static String getAccessToken(String adminEmail, String serviceAccountJson) throws Exception {
+
+        if (Strings.isNullOrEmpty(adminEmail) || Strings.isNullOrEmpty(serviceAccountJson)) {
+            throw ServiceException.exception("admin email or service account json is null");
+        }
+        // 解析 JSON 文件
+        Map<String, Object> jsonMap = JSON.parseObject(serviceAccountJson, Map.class);
+
+        String clientEmail = (String) jsonMap.get("client_email");
+        String privateKeyPem = (String) jsonMap.get("private_key");
+
+        // 解析私钥
+        privateKeyPem = privateKeyPem.replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replaceAll("\\s", "");
+
+        return getAccessToken(adminEmail, clientEmail, privateKeyPem);
+    }
+
+    public static String getAccessToken(String adminEmail, String clientEmail, String privateKeyPemString) throws Exception {
+        if (Strings.isNullOrEmpty(adminEmail) || Strings.isNullOrEmpty(clientEmail) || Strings.isNullOrEmpty(privateKeyPemString)) {
+            throw ServiceException.exception("admin email or clientEmail or privateKeyPem is null");
+        }
+        // 解析私钥
+        String privateKeyPem = privateKeyPemString.replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replaceAll("\\s", "");
+
+        byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyPem);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
+
+        // 生成 JWT Header
+        Map<String, Object> header = new HashMap<>();
+        header.put("alg", "RS256");
+        header.put("typ", "JWT");
+
+        // 生成 JWT Payload
+        long now = System.currentTimeMillis() / 1000;
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("iss", clientEmail);
+        payload.put("sub", adminEmail); // 委派管理权限
+        payload.put("scope", SCOPE);
+        payload.put("aud", TOKEN_URL);
+        payload.put("exp", now + 3600);
+        payload.put("iat", now);
+
+        // Base64 编码 Header 和 Payload
+        String encodedHeader =
+                Base64.getUrlEncoder().withoutPadding().encodeToString(JSON.toJSONBytes(header));
+        String encodedPayload =
+                Base64.getUrlEncoder().withoutPadding().encodeToString(JSON.toJSONBytes(payload));
+
+        // 签名
+        String message = encodedHeader + "." + encodedPayload;
+        Signature signature = Signature.getInstance("SHA256withRSA");
+        signature.initSign((PrivateKey) privateKey);
+        signature.update(message.getBytes(StandardCharsets.UTF_8));
+        String encodedSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signature.sign());
+
+        String jwt = message + "." + encodedSignature;
+
+        // 发送 HTTP POST 请求获取 Access Token
+        URL url = new URL(TOKEN_URL);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        conn.setDoOutput(true);
+
+        String requestBody = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + jwt;
+        try (OutputStream os = conn.getOutputStream()) {
+            os.write(requestBody.getBytes(StandardCharsets.UTF_8));
+        }
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 200) {
+            String responseBody = getResponseBody(conn);
+            Map<String, Object> response = JSON.parseObject(responseBody, Map.class);
+            return (String) response.get("access_token");
+        } else {
+            return "Error: " + responseCode + " - " + getErrorResponse(conn);
+        }
+    }
+
+    public static Integer createUser(String accessToken, String email, String firstName, String lastName,
+                                     String password, String extraPhone, String extraEmail) throws Exception {
+        validToken(accessToken);
+        String urlString = ADMIN_DIRECTORY_USER_API_URL;
+        URL url = new URL(urlString);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+        conn.setRequestProperty("Content-Type", "application/json");
+        conn.setDoOutput(true);
+
+
+        // 1️⃣ 构造用户 JSON 数据
+        Map<String, Object> userMap = new HashMap<>();
+        Map<String, String> nameMap = new HashMap<>();
+        nameMap.put("givenName", firstName);
+        nameMap.put("familyName", lastName);
+        userMap.put("name", nameMap);
+        userMap.put("password", password);
+        userMap.put("primaryEmail", email);
+
+        // 2️⃣ 添加额外的电话和邮箱
+        if (extraPhone != null && !extraPhone.isEmpty()) {
+            Map<String, String> phoneMap = new HashMap<>();
+            phoneMap.put("type", "work");
+            phoneMap.put("value", extraPhone);
+            userMap.put("phones", new Map[]{phoneMap});
+        }
+
+        if (extraEmail != null && !extraEmail.isEmpty()) {
+            Map<String, String> emailMap = new HashMap<>();
+            emailMap.put("address", extraEmail);
+            emailMap.put("type", "work");
+            userMap.put("emails", new Map[]{emailMap});
+        }
+
+        try (OutputStream os = conn.getOutputStream()) {
+            byte[] input = JSON.toJSONString(userMap).getBytes("utf-8");
+            os.write(input, 0, input.length);
+        }
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 200) {
+            log.info("User create successfully!");
+        } else {
+            log.info("User create Error: " + responseCode + " - " + getErrorResponse(conn));
+        }
+        return responseCode;
+    }
+
+    public static Integer deleteUser(String accessToken, String userEmail) throws Exception {
+        validToken(accessToken);
+        String urlString = ADMIN_DIRECTORY_USER_API_URL + "/" + userEmail;
+        URL url = new URL(urlString);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("DELETE");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 204) {
+            log.info("User deleted successfully!");
+        } else {
+            log.info("User deleted Error: " + responseCode + " - " + getErrorResponse(conn));
+        }
+        return responseCode;
+    }
+
+    /**
+     * @param accessToken
+     * @param userEmail
+     * @return {  "kind": "admin#directory#user",  "id": "112207665165230017342",  "etag":
+     * "\"CmwXoj9NwmeYWUSny9qYPKAHjB8pLfe5jMuk_1lM7Aw/PFha8YbuHDbkCYrZxK3ifyBd35s\"",  "primaryEmail": "john.doe
+     * .test@cwgmarkets.com",  "name": {    "givenName": "New",    "familyName": "User2",    "fullName": "New User2"
+     * },  "isAdmin": false,  "isDelegatedAdmin": false,  "lastLoginTime": "1970-01-01T00:00:00.000Z",
+     * "creationTime": "2025-02-21T08:13:45.000Z",  "agreedToTerms": false,  "suspended": false,  "archived": false,
+     * "changePasswordAtNextLogin": false,  "ipWhitelisted": false,  "emails": [    {      "address": "john.doe
+     * .test@gmail.com",      "type": "work"    },    {      "address": "john.doe.test@cwgmarkets.com",
+     * "primary": true    },    {      "address": "john.doe.test@cwgmarkets.com.test-google-a.com"    }  ],
+     * "phones": [    {      "value": "123123123",      "type": "work"    }  ],  "languages": [    {
+     * "languageCode": "en-GB",      "preference": "preferred"    }  ],  "nonEditableAliases": [    "john.doe
+     * .test@cwgmarkets.com.test-google-a.com"  ],  "customerId": "C03fblpx8",  "orgUnitPath": "/",
+     * "isMailboxSetup": true,  "isEnrolledIn2Sv": false,  "isEnforcedIn2Sv": false,  "includeInGlobalAddressList":
+     * true}
+     * @throws Exception
+     */
+    public static String getUser(String accessToken, String userEmail) throws Exception {
+        validToken(accessToken);
+        String urlString = ADMIN_DIRECTORY_USER_API_URL + "/" + userEmail;
+        URL url = new URL(urlString);
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("GET");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+        conn.setRequestProperty("Accept", "application/json;charset=UTF-8");
+
+        int responseCode = conn.getResponseCode();
+
+        if (responseCode == 200) {
+            String responseBody = getResponseBody(conn);
+            log.info("User updated successfully!\n" + responseBody);
+            return responseBody;
+        } else {
+            log.info("Error: " + responseCode + " - " + getErrorResponse(conn));
+            return null;
+        }
+    }
+
+    public static Integer updateUser(String accessToken, String userEmail, String firstName, String lastName,
+                                    String extraPhone, String extraEmail) throws Exception {
+        validToken(accessToken);
+        String urlString = ADMIN_DIRECTORY_USER_API_URL + "/" + userEmail;
+        URL url = new URL(urlString);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("PUT");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+        conn.setRequestProperty("Content-Type", "application/json");
+        conn.setDoOutput(true);
+
+        // 构造 JSON 数据
+        if (Strings.isNullOrEmpty(userEmail)) {
+            throw ServiceException.exception("User email is null or empty");
+        }
+
+        Map<String, Object> userMap = new HashMap<>();
+        if (!Strings.isNullOrEmpty(firstName) || !Strings.isNullOrEmpty(lastName)) {
+            Map<String, String> nameMap = new HashMap<>();
+            nameMap.put("givenName", firstName);
+            nameMap.put("familyName", lastName);
+            userMap.put("name", nameMap);
+        }
+
+        List<Map<String, String>> phones = new ArrayList<>();
+        if (!Strings.isNullOrEmpty(extraPhone)) {
+            Map<String, String> phoneMap = new HashMap<>();
+            phoneMap.put("type", "work");
+            phoneMap.put("value", extraPhone);
+            phones.add(phoneMap);
+        }
+        if (!phones.isEmpty()) {
+            userMap.put("phones", phones);
+        }
+
+        List<Map<String, String>> emails = new ArrayList<>();
+        if (!Strings.isNullOrEmpty(extraEmail)) {
+            Map<String, String> emailMap = new HashMap<>();
+            emailMap.put("address", extraEmail);
+            emailMap.put("type", "work");
+            emails.add(emailMap);
+        }
+        if (!emails.isEmpty()) {
+            userMap.put("emails", emails);
+        }
+        conn.getOutputStream().write(JSON.toJSONString(userMap).getBytes("UTF-8"));
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 200) {
+            log.info("User updated successfully!");
+        } else {
+            log.info("User updated Error: " + responseCode + " - " + getErrorResponse(conn));
+        }
+        return responseCode;
+
+    }
+
+    public static String renameUser(String accessToken, String oldEmail, String newEmail) throws Exception {
+        validToken(accessToken);
+
+        if (Strings.isNullOrEmpty(oldEmail) || Strings.isNullOrEmpty(newEmail)) {
+            throw ServiceException.exception("Old or new email is null or empty");
+        }
+
+        // API 地址
+        String urlString = ADMIN_DIRECTORY_USER_API_URL + "/" + oldEmail;
+        URL url = new URL(urlString);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+        // PATCH 方法比 PUT 更安全(只更新传入字段)
+        conn.setRequestMethod("PUT");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+        conn.setRequestProperty("Content-Type", "application/json");
+        conn.setDoOutput(true);
+
+        // 构造请求体
+        Map<String, Object> userMap = new HashMap<>();
+        userMap.put("primaryEmail", newEmail);
+
+        // 写入请求数据
+        conn.getOutputStream().write(JSON.toJSONString(userMap).getBytes("UTF-8"));
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 200) {
+            return "User renamed successfully!\n" + getResponseBody(conn);
+        } else {
+            return "Error: " + responseCode + " - " + getErrorResponse(conn);
+        }
+    }
+
+    public static Integer resetUserPassword(String accessToken, String userEmail, String newPassword) throws Exception {
+        log.info("accessToken:" + accessToken + ",userEmail:" + userEmail);
+        validToken(accessToken);
+        String urlString = ADMIN_DIRECTORY_USER_API_URL + "/" + userEmail;
+        URL url = new URL(urlString);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("PUT");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+        conn.setRequestProperty("Content-Type", "application/json");
+        conn.setDoOutput(true);
+
+        // 构造 JSON 数据
+        String jsonPayload = "{ \"password\": \"" + newPassword + "\" }";
+
+        conn.getOutputStream().write(jsonPayload.getBytes("UTF-8"));
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 200) {
+            log.info("User password reset successfully!");
+        } else {
+            log.info("User password reset Error: " + responseCode + " - " + getErrorResponse(conn));
+        }
+        return responseCode;
+
+    }
+
+    /**
+     * 将用户添加到 Google 群组
+     *
+     * @param accessToken OAuth2 访问令牌
+     * @param groupEmail  群组邮箱,例如 team@cwgmarkets.com
+     * @param userEmail   要添加的用户邮箱,例如 john.doe@cwgmarkets.com
+     * @param role        在群组里的角色,可选 MEMBER / MANAGER / OWNER
+     */
+    public static Integer addUserToGroup(String accessToken, String groupEmail, String userEmail, String role) throws Exception {
+        validToken(accessToken);
+
+        if (Strings.isNullOrEmpty(groupEmail) || Strings.isNullOrEmpty(userEmail)) {
+            throw ServiceException.exception("groupEmail or userEmail is null");
+        }
+        if (Strings.isNullOrEmpty(role)) {
+            role = "MEMBER"; // 默认成员
+        }
+
+        String urlString = "https://admin.googleapis.com/admin/directory/v1/groups/" + groupEmail + "/members";
+        URL url = new URL(urlString);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+        conn.setRequestProperty("Content-Type", "application/json");
+        conn.setDoOutput(true);
+
+        Map<String, String> memberData = new HashMap<>();
+        memberData.put("email", userEmail);
+        memberData.put("role", role);
+
+        try (OutputStream os = conn.getOutputStream()) {
+            os.write(JSON.toJSONString(memberData).getBytes(StandardCharsets.UTF_8));
+        }
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 200) {
+            log.info("✅ User added to group successfully!");
+        } else {
+            if (responseCode == 409) {
+                return 200;
+            }
+            log.error("❌ Error adding user to group: " + responseCode + " - " + getErrorResponse(conn));
+        }
+        return responseCode;
+    }
+
+    public static List<Map<String, Object>> listUsers(String accessToken) throws Exception {
+        return listUsers(accessToken, "");
+    }
+
+    public static List<Map<String, Object>> listUsers(String accessToken, String customerId) throws Exception {
+        validToken(accessToken);
+
+        if (Strings.isNullOrEmpty(customerId)) {
+            customerId = "my_customer"; // 默认获取整个 Workspace 下的用户
+        }
+
+        String urlString = ADMIN_DIRECTORY_USER_API_URL
+                + "?customer=" + customerId
+                + "&maxResults=500&orderBy=email";
+
+        List<Map<String, Object>> users = new ArrayList<>();
+        String nextPageToken = null;
+
+        do {
+            String requestUrl = urlString;
+            if (nextPageToken != null) {
+                requestUrl += "&pageToken=" + nextPageToken;
+            }
+
+            URL url = new URL(requestUrl);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("GET");
+            conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+            conn.setRequestProperty("Accept", "application/json;charset=UTF-8");
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode == 200) {
+                String responseBody = getResponseBody(conn);
+                Map<String, Object> response = JSON.parseObject(responseBody, Map.class);
+
+                // 提取用户信息
+                List<Map<String, Object>> items = (List<Map<String, Object>>) response.get("users");
+                if (items != null) {
+                    users.addAll(items);
+                }
+
+                // 检查是否有下一页
+                nextPageToken = (String) response.get("nextPageToken");
+            } else {
+                throw new RuntimeException("Error: " + responseCode + " - " + getErrorResponse(conn));
+            }
+        } while (nextPageToken != null);
+
+        return users;
+    }
+
+    /**
+     * 从 Google 群组移除用户
+     *
+     * @param accessToken OAuth2 访问令牌
+     * @param groupEmail  群组邮箱,例如 team@cwgmarkets.com
+     * @param userEmail   要移除的用户邮箱,例如 john.doe@cwgmarkets.com
+     */
+    public static Integer removeUserFromGroup(String accessToken, String groupEmail, String userEmail) throws Exception {
+        validToken(accessToken);
+
+        if (Strings.isNullOrEmpty(groupEmail) || Strings.isNullOrEmpty(userEmail)) {
+            throw ServiceException.exception("groupEmail or userEmail is null");
+        }
+
+        String urlString = "https://admin.googleapis.com/admin/directory/v1/groups/"
+                + groupEmail + "/members/" + userEmail;
+        URL url = new URL(urlString);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("DELETE");
+        conn.setRequestProperty("Authorization", "Bearer " + accessToken);
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode == 204) { // 删除成功返回 204 No Content
+            log.info("✅ User removed from group successfully!");
+        } else {
+            log.error("❌ Error removing user from group: " + responseCode + " - " + getErrorResponse(conn));
+        }
+        return responseCode;
+    }
+
+    private static String getResponseBody(HttpURLConnection conn) throws Exception {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(),"UTF-8"));
+        StringBuilder response = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            response.append(line);
+        }
+        reader.close();
+        return response.toString();
+    }
+
+    private static String getErrorResponse(HttpURLConnection conn) throws Exception {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
+        StringBuilder response = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            response.append(line);
+        }
+        reader.close();
+        return response.toString();
+    }
+
+    private static void validToken(String token) throws Exception {
+        if (Strings.isNullOrEmpty(token)) {
+            throw ServiceException.exception("token is null");
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+
+        String jaon = "{\n" +
+                "  \"type\": \"service_account\",\n" +
+                "  \"project_id\": \"manageruser-451509\",\n" +
+                "  \"private_key_id\": \"e61cbc61eec9e7f81d0fa8d3312997e90c3598ec\",\n" +
+                "  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+Mpn6cD1Ln0tg\\nM4oALgRA31lAVKyq9In41Bp1oR/2rzCZXj8ZVp0D/AwHp0P9PybqvlE4wyFcJrII\\nnfxivZnEwfA76gVGFHYz8Ulp5vgJX5xNJ4poNJB5Zp9FfL//2czZGHo8sFDahTOJ\\nILHOOAwjtiL6ya826LpE+S2cyRtHWXugfZ02K9W7SphN8BZfTTdeXmT+C265EIQw\\nBQ3Q4QPwjnxzlO0EEZLr25Qz9XiPJoUFpChHU3ByRBinoz3tb53hSwNQ271Dg23u\\np/RRUE6NL/1uZtOGaFAtmkt+9SPSDI+iAhO0fn7D2D5D0FWR/TQuCgI0Vw3OZLnj\\nhuKRy1I7AgMBAAECggEAFCya35EbN1PAs4YT+7vl0JnARk4U+JcysVWSLur10H05\\nqvLvv3jL89m+zusDj6+sqZahV+N+vN8P4FEdrb2Nu2yU1k4W3B2cghr0XgrVQW1G\\nT3rDuBTnRDFJ8J1wYiuQinCJurk7AkrBsihpI8YXJ4yQvkg5URozc9B4cZp1TcZ1\\nkk1pHg/NoIVRMG8Iwi4W/hmZDzgSQMTM55hupNkx5EH7On7VR4cFJ+/ViTZwlPAp\\n3B7mQ1oj91WEWrE2t1mlTpfmg6y7i9nzoA1ox4uUPJl8eWeufSpInT3sm5uw7+jx\\n6ximBuO6dL1237EONKIunPP5GTSeCWvCIS7vmkgzOQKBgQDxUhm8+6HqGWY/Yf3G\\nPLlpD/7SxxlbCuFKzsvKYQDBWau5xj3A2pQPjU/trZ8jnJD8CziigAh4mYx5/cQZ\\n3LjG8CF0/lzuOwowK8oNOvMuEoaKhEQudky5h+EHnHw4FCcMyOvDBQKt6oYigThF\\nlpw4ueyvaGyGO7uOjwND8zK74wKBgQDJxGbOerGOq4+evFJQFyFAHJlPMm32aL74\\nZzISzBPKQqGYqKlJIZ3lOBlSx4VsFEV7RKvYL+b9nGClmQCoDKZkCMdovOpCs8p7\\nR6TPK5AMMxTLsGdnZwbhsvFpr5qUJWIxoB0txcEcWDWsky6v24vgSftUoZzj5U6l\\nzJD+25aPyQKBgFtzhZV8tsuEfJ8gf4+Ui58pFKTEFvPNl7tFyy0SvbqIRO/OFLD1\\nJI8QOm4UEe7o8njRt9C9PHiXmAjTBnHWl5+seMFhc85ESYQf1B5XbLjVBG+R2FTT\\nOBhHShwcTWbjHDlYGyB40Z8e9V42IiEdBm7o5q1CoX3tPRK1V8N0cpGJAoGBAKF3\\noop1F+lEFKCEtx7PE9X5zRrbEbMTaljiROlUFjZUEnxX5XnYBEMvHNzpPTYcFuqq\\nLxfIZgPphFKh2iuiS/pcKDECljqhJVZ5JESC6TlKG8dSOb4/bftrN9VLKdqK/EUY\\njKFX0836LaIY+1DS0uLAcfjtiN+8X69BwXujgZ85AoGAFOXNwAA4pJwfh4tjMdCH\\nvBR/BDCp7kLu1+XZeSkwxpb4fqBhqtSaGPsHMs1VjZfuTJ+W64hdqPXwLmKQ8yvV\\n8zzvxkgh87rDNIjNUXaKntTEQkSbhR90w6JRstUM0EdpxsXy3Ml00dOeeBb0WtdB\\nzh0XoW/f0Xsa+Ygst/n5uSw=\\n-----END PRIVATE KEY-----\\n\",\n" +
+                "  \"client_email\": \"manageruser@manageruser-451509.iam.gserviceaccount.com\",\n" +
+                "  \"client_id\": \"108768971673921067265\",\n" +
+                "  \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n" +
+                "  \"token_uri\": \"https://oauth2.googleapis.com/token\",\n" +
+                "  \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n" +
+                "  \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/manageruser%40manageruser-451509.iam.gserviceaccount.com\",\n" +
+                "  \"universe_domain\": \"googleapis.com\",\n" +
+                "  \"admin_email\": \"landon@cwgmarkets.com\"\n" +
+                "}";
+        String adminEmail = "landon@cwgmarkets.com";
+        String accessToken = getAccessToken(adminEmail, jaon);
+        System.out.println(accessToken);
+
+        String firstName = "NG";
+        String lastName = "SOOK HUEYT";
+        String password = GetRandom.getRandomPasswordString();
+        System.out.println(password);
+        String extraEmail = "1640ww23@163.com";
+        String extraPhone = "011-133 221346";
+
+        String userEmail = "yxcd@cwgmarkets.com";
+
+        System.out.println("userEmail:" + userEmail);
+        System.out.println("password:" + password);
+        List<Map<String, Object>> maps = listUsers(accessToken, "");
+        System.out.println(maps);
+
+//        String content = "Roy.S@cwgmarkets.com,Roy.Shen@cwgmarkets.com\n" +
+//                "Jony.Y@cwgmarkets.com,Jony.Yang@cwgmarkets.com\n" +
+//                "Shawn.Y@cwgmarkets.com,Shawn.Yang@cwgmarkets.com\n" +
+//                "kimi.s@cwgmarkets.com,Kimi.Shi@cwgmarkets.com\n" +
+//                "Selina.He@cwgmarkets.com,Selina.He@cwgmarkets.com\n" +
+//                "Mike.z@cwgmarkets.com,Mike.Zhang@cwgmarkets.com\n" +
+//                "Lucky.Zhang@cwgmarkets.com,Lucky.Zhang@cwgmarkets.com\n" +
+//                "Sean.Yang@cwgmarkets.com,Sean.Yang@cwgmarkets.com\n" +
+//                "Gavin.Guo@cwgmarkets.com,Gavin.Guo@cwgmarkets.com\n" +
+//                "Sunny.Z@cwgmarkets.com,Sunny.Zhou@cwgmarkets.com";
+//        String[] renameUsers = content.split("\n");
+//        if (renameUsers.length <= 0) {
+//            return;
+//        }
+//        for (String renameUser : renameUsers) {
+//            String[] rename = renameUser.split(",");
+//            if (rename.length < 2) {
+//                break;
+//            }
+//
+//            System.out.println(rename[0] + "--" + rename[1]);
+//            renameUser(accessToken, rename[0], rename[1]);
+//        }
+//        System.out.println(getUser(accessToken, "kevin.chen@cwgmarkets.com"));
+
+//        addUserToGroup(accessToken, "abuse@cwgmarkets.com", "landon@cwgmarkets.com", "MEMBER");
+// test.f.l@cwgmarkets.com
+//        System.out.println("User Info: \n" + deleteUser(accessToken, "john.doe.test@cwgmarkets.com"));
+//        System.out.println("User Info: \n" + createUser(accessToken, userEmail, firstName, lastName, password,
+//                extraPhone, extraEmail));
+//        System.out.println("User Info: \n" + deleteUser(accessToken, userEmail));
+//        String userEmail = "landon@cwgmarkets.com";
+//        getUser(accessToken, userEmail);
+//        System.out.println("User Info: \n" + userInfo);
+//        System.out.println("User Info: \n" + createUser(accessToken, userEmail, firstName, lastName, password,
+//                extraPhone, extraEmail));
+//        System.out.println("User Info: \n" + updateUser(accessToken, userEmail, firstName, lastName, extraPhone,
+//                extraEmail));
+//        System.out.println("User Info: \n" + resetUserPassword(accessToken, userEmail, password));
+
+//        String userEmail = "john.doe.test@cwgmarkets.com";
+//        System.out.println("User Info: \n" + getUser(accessToken, userEmail));
+//        System.out.println("User Info: \n" + updateUser(accessToken, userEmail, "New", "User2"));
+//        System.out.println("User Info: \n" + resetUserPassword(accessToken, userEmail, "Secur@#ePass123!"));
+//        System.out.println("User Info: \n" + deleteUser(accessToken, userEmail));
+//
+//        createUser(accessToken, "john.doe.test@cwgmarkets.com", "New", "User", "Secur@#ePass123!");
+    }
+}

+ 110 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/GoogleAuthenticatorUtil.java

@@ -0,0 +1,110 @@
+package com.crm.rely.backend.util;
+
+import com.google.common.base.Strings;
+import com.warrenstrange.googleauth.GoogleAuthenticator;
+import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
+import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
+
+/**
+ * @Author houn
+ * @Date 2024/12/3 17:24
+ * @PackageName:com.crm.rely.backend.util
+ * @ClassName: GoogleAuthenticatorUtil
+ * @Description: TODO
+ */
+public class GoogleAuthenticatorUtil {
+
+    private final static String DEFAULT_ISSUER = "GOOGLE";
+
+    public static void main(String[] args) {
+        String secretKey = generateSecretKey();
+        System.out.println(secretKey);
+        System.out.println(getOtpAuthURL(secretKey));
+
+//
+//        String secretKey = "5FC5O2Z4ESVWPAXWPGOZM5KZMTZPHNE4";
+//        System.out.println(verifyCode(secretKey, 111111));
+    }
+
+    /**
+     * 生成 Google Authenticator 应用的二维码 URL
+     *
+     * @param secretKey
+     * @return
+     */
+    public static String getOtpAuthURL(String secretKey) {
+
+        return getOtpAuthURL(secretKey, null);
+    }
+
+    /**
+     * 生成 Google Authenticator 应用的二维码 URL
+     *
+     * @param secretKey
+     * @param accountName
+     * @return
+     */
+    public static String getOtpAuthURL(String secretKey, String accountName) {
+
+        return getOtpAuthURL(secretKey, accountName, null);
+    }
+
+    /**
+     * 生成 Google Authenticator 应用的二维码 URL
+     *
+     * @param secretKey
+     * @param accountName
+     * @param issuer
+     * @return
+     */
+    public static String getOtpAuthURL(String secretKey, String accountName, String issuer) {
+
+        if (Strings.isNullOrEmpty(accountName)) {
+            accountName = DEFAULT_ISSUER;
+        }
+
+        GoogleAuthenticatorKey key = createGoogleAuthenticatorKey(secretKey);
+
+        // 生成 Google Authenticator 应用的二维码 URL
+        String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(issuer, accountName, key);
+
+        return qrCodeUrl;
+    }
+
+    /**
+     * 生成用户的 Google Authenticator 密钥
+     */
+    public static String generateSecretKey() {
+
+
+        GoogleAuthenticator gAuth = new GoogleAuthenticator();
+        GoogleAuthenticatorKey key = gAuth.createCredentials();
+
+        // 将密钥保存到用户账户(数据库或其他存储)
+        String secretKey = key.getKey();
+
+        return secretKey;
+    }
+
+    /**
+     * 根据已有的 secretKey 生成 GoogleAuthenticatorKey
+     *
+     * @param secretKey
+     * @return
+     */
+    public static GoogleAuthenticatorKey createGoogleAuthenticatorKey(String secretKey) {
+        return new GoogleAuthenticatorKey.Builder(secretKey).build();
+    }
+
+    /**
+     * 验证绑定时的 Google Authenticator 动态验证码
+     *
+     * @param secretKey
+     * @param verificationCode
+     * @return
+     */
+    public static boolean verifyCode(String secretKey, int verificationCode) {
+        GoogleAuthenticator gAuth = new GoogleAuthenticator();
+        return gAuth.authorize(secretKey, verificationCode);
+    }
+}

+ 65 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/HAMCSHA1Util.java

@@ -0,0 +1,65 @@
+package com.crm.rely.backend.util;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+
+/**
+ * @program: cwg-my-base
+ * @description:
+ * @author: houn
+ * @create: 2019-05-07 11:08
+ */
+public class HAMCSHA1Util {
+
+
+    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
+
+    /**
+     * hamcsha1加密
+     * @param data 数据
+     * @param key 秘钥
+     * @return
+     */
+    public static String hamcsha1(String data, String key) {
+        try {
+            SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
+            Mac mac = Mac.getInstance("HmacSHA1");
+            mac.init(signingKey);
+            return byte2hex(mac.doFinal(data.getBytes()));
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        } catch (InvalidKeyException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    //二行制转字符串
+    public static String byte2hex(byte[] b) {
+        StringBuilder hs = new StringBuilder();
+        String stmp;
+        for (int n = 0; b != null && n < b.length; n++) {
+            stmp = Integer.toHexString(b[n] & 0XFF);
+            if (stmp.length() == 1)
+                hs.append('0');
+            hs.append(stmp);
+        }
+        return hs.toString().toLowerCase();
+    }
+
+    /**
+     * 测试
+     *
+     * @param args
+     */
+    public static void main(String[] args) {
+        String genHMAC = hamcsha1("getMethods?key=911b4f6a-a4d2-48cb-9c8f-68a77c3ec39c&timestamp=1606814088",
+                "mJtkXX2x53uix563S2d8H7KqPv6wF83dPIL5");
+        System.out.println(genHMAC.length());
+        System.out.println(genHMAC);
+    }
+}

+ 107 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/HAMCSHA256Util.java

@@ -0,0 +1,107 @@
+package com.crm.rely.backend.util;
+
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+
+/**
+ * @program: cwg-my-base
+ * @description:
+ * @author: houn
+ * @create: 2019-05-07 11:08
+ */
+public class HAMCSHA256Util {
+
+
+    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA256";
+
+    public static String HMACSHA256(String data, String key) {
+        try {
+            Mac sha256_HMAC = Mac.getInstance(HMAC_SHA1_ALGORITHM);
+            SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("utf-8"), HMAC_SHA1_ALGORITHM);
+            sha256_HMAC.init(secretKey);
+            byte[] hash = sha256_HMAC.doFinal(data.getBytes("utf-8"));
+            return Base64.encodeBase64String(hash);
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        } catch (InvalidKeyException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    public static String sha256_HMAC(String message, String secret) {
+        return sha256_HMAC(message, secret.getBytes());
+    }
+
+    public static String sha256_HMAC(String message, byte[] secret) {
+        String hash = "";
+        try {
+            Mac sha256_HMAC = Mac.getInstance(HMAC_SHA1_ALGORITHM);
+            SecretKeySpec secret_key = new SecretKeySpec(secret, HMAC_SHA1_ALGORITHM);
+            sha256_HMAC.init(secret_key);
+            byte[] bytes = sha256_HMAC.doFinal(message.getBytes());
+            StringBuilder sb = new StringBuilder();
+            for (byte item : bytes) {
+                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
+            }
+            hash = sb.toString();
+        } catch (Exception e) {
+            System.out.println("Error HmacSHA256 ===========" + e.getMessage());
+        }
+        return hash;
+    }
+
+    /**
+     * 测试
+     *
+     * @param args
+     */
+    public static void main(String[] args) throws Exception {
+
+//        String genHMAC = HMACSHA256("apiKey%3Dc8de0fbc398643a59043f561ec37c578%26signVersion%3D1%26timestamp%3D2021
+//        -09-09T14%3A47%3A44",
+//                "a2699ccbb4c442c2866a13b274924a99f0530f2b1a3b777eedbfcd11131d03c6");
+
+//        String data = "2021-12-14T07:33:16.421542ZeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
+//        .eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTYzOTQ4ODc5NiwianRpIjoiNTEyNzdlMzJjYjVjNGFiN2JhYmEyY2M5OWYzNjNjOTMiLCJ1c2VyX2lkIjo0MjZ9.JhXN3R1F6jGhDrSbKbH0qoTMeB9N5nL45hc-3P3aFvQ";
+//        String secretString = "pHXivvW5pA9Y58t3eK7QnuZVSTMqXyip";
+//
+//        String genHMAC = HMACSHA256(data, secretString);
+//        String sha256_HMAC = sha256_HMAC(data, secretString);
+////        System.out.println(genHMAC);
+//        System.out.println(sha256_HMAC);
+
+
+//        System.out.println(HMACSHA256("buy=7.2216&legal_currency_max_amount=50000.00&legal_currency_min_amount=100" +
+//                        ".00&sell=7.10861&symbol=USDT&timestamp=1586487661938",
+//                "02d82ba350d6f95e4a6005f9c17d5c0a7dc4c013ed09c4fd04971660f1d0d8c299"));
+//
+
+//        System.out.println(HMACSHA256("count=100&created_time=2024-04-25%2015%3A26%3A16&currency=USDT&legal_amount" +
+//                        "=737.00000000&legal_currency=CNY&merchant_order_no=PD202404251527169849_66753851" +
+//                        "&merchant_profit=0.00000000&mobile=null&nonce=1714030080312&pass_time=2024-04-25%2015%3A27" +
+//                        "%3A51&platform_fee=0.60000000&side=BUY&status=COMPLETE",
+//                "02ad904d8600b1f7d33823ba54b2f533983659c68c55da075f6d0948e4931625e9"));
+//
+//        System.out.println(HMACSHA256("count=100&created_time=2024-04-25%2015%3A26%3A16&currency=USDT" +
+//                        "&merchant_order_no=PD202404251527169849_66753851&merchant_profit=0" +
+//                        ".00000000&nonce=1714030080312&platform_fee=0.60000000&side=BUY&status=COMPLETE",
+//                "02d82ba350d6f95e4a6005f9c17d5c0a7dc4c013ed09c4fd04971660f1d0d8c299"));
+
+        System.out.println(HMACSHA256("count=100&created_time=2024-04-25%2015%3A26%3A16&currency=USDT&legal_amount" +
+                        "=737.00000000&legal_currency=CNY&merchant_order_no=PD202404251527169849_66753851" +
+                        "&merchant_profit=0.00000000&mobile=null&nonce=1714030080312&pass_time=2024-04-25%2015%3A27" +
+                        "%3A51&platform_fee=0.60000000&side=BUY&status=COMPLETE",
+                "0329a81f77955ec6a8b697e25d413c56fd71d084d7882280ff5ebdf3c1c6c67dc7"));
+        //yJghhncCqfqhcDZw16G5BughLVRVvEbS8bo8NW3lKj4=
+    }
+}

+ 141 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/HAMCSHAUtil.java

@@ -0,0 +1,141 @@
+package com.crm.rely.backend.util;
+
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+
+/**
+ * @program: cwg-my-base
+ * @description:
+ * @author: houn
+ * @create: 2019-05-07 11:08
+ */
+public class HAMCSHAUtil {
+    /**
+     * 定义加密方式
+     * MAC算法可选以下多种算法
+     * <pre>
+     * HmacMD5
+     * HmacSHA1
+     * HmacSHA256
+     * HmacSHA384
+     * HmacSHA512
+     * </pre>
+     */
+    private static final String HMAC_SHA_ALGORITHM = "HmacSHA1";
+
+    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+
+    private static final String HMAC_SHA512_ALGORITHM = "HmacSHA512";
+
+    public static String sha1HMAC(String data, String secret) {
+        return shaHMAC(data, secret, HMAC_SHA_ALGORITHM);
+    }
+
+    public static String sha1HMAC(String data, byte[] secret) {
+        return shaHMAC(data, secret, HMAC_SHA_ALGORITHM);
+    }
+
+    public static String sha256HMAC(String data, String secret) {
+        return shaHMAC(data, secret, HMAC_SHA256_ALGORITHM);
+    }
+
+    public static String sha256HMAC(String data, byte[] secret) {
+        return shaHMAC(data, secret, HMAC_SHA256_ALGORITHM);
+    }
+
+    public static String sha512HMAC(String data, String secret) throws Exception {
+        return shaHMAC(data, secret, HMAC_SHA512_ALGORITHM);
+    }
+
+    public static String sha512HMACBase64(String data, String secret) {
+
+        byte[] hash = shaHMACByte(data, secret, HMAC_SHA512_ALGORITHM);
+        return Base64.encodeBase64String(hash);
+    }
+
+    public static String sha512HMAC(String data, byte[] secret) throws Exception {
+        return shaHMAC(data, secret, HMAC_SHA512_ALGORITHM);
+    }
+
+    public static String sha256HMACBase64(String data, String secret) {
+
+        byte[] hash = shaHMACByte(data, secret, HMAC_SHA256_ALGORITHM);
+        return Base64.encodeBase64String(hash);
+    }
+
+    public static byte[] shaHMACByte(String data, String secret, String algorithm) {
+        return shaHMACByte(data, secret.getBytes(), algorithm);
+    }
+
+    public static byte[] shaHMACByte(String data, byte[] secret, String algorithm) {
+        try {
+            SecretKeySpec signingKey = new SecretKeySpec(secret, algorithm);
+            Mac mac = Mac.getInstance(algorithm);
+            mac.init(signingKey);
+            byte[] hash = mac.doFinal(data.getBytes());
+            return hash;
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        } catch (InvalidKeyException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static String shaHMAC(String data, String secret, String algorithm) {
+        byte[] re = shaHMACByte(data, secret, algorithm);
+        return byte2hex(re);
+    }
+
+    public static String shaHMAC(String data, byte[] secret, String algorithm) {
+        byte[] re = shaHMACByte(data, secret, algorithm);
+        return byte2hex(re);
+    }
+
+    private static String byte2hex(byte[] b) {
+        String hs = "";
+        String stmp = "";
+        for (int n = 0; n < b.length; n++) {
+            // 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式。
+            stmp = (Integer.toHexString(b[n] & 0xFF));
+            if (stmp.length() == 1) {
+                hs = hs + "0" + stmp;
+            } else {
+                hs = hs + stmp;
+            }
+        }
+        return hs;
+    }
+
+    /**
+     * @param args
+     */
+    public static void main(String[] args) {
+        try {
+
+//            System.out.println(getSignature("{\"currency\":\"BTC\",\"foreign_id\":\"123456\"}", "AbCdEfG123456"));
+//            String signature = sha512HMAC("amount=18000&currency=RMB&customer=10001&merchant=ME2333&txcode=TX3888",
+//                    "bipipaykey");
+//            System.out.println(signature);
+
+            System.out.println(HAMCSHA256Util.HMACSHA256("count=100&created_time=2024-04-25 " +
+                            "15:26:16&currency=USDT&merchant_order_no=PD202404251527169849_66753851&merchant_profit=0" +
+                            ".00000000&nonce=1714030080312&platform_fee=0.60000000&side=BUY&status=COMPLETE",
+                    "02ad904d8600b1f7d33823ba54b2f533983659c68c55da075f6d0948e4931625e9"));
+            System.out.println(HAMCSHA256Util.HMACSHA256("count=96.02194813&created_time=2020-04-10%2010%3A29%3A58" +
+                            "&currency=USDT&merchant_order_no=51116897781651&merchant_profit=0" +
+                            ".00000000&nonce=1586488141021&order_no=undefined&platform_fee=0" +
+                            ".28806584&side=BUY&status=CANCEL",
+                    "02ad904d8600b1f7d33823ba54b2f533983659c68c55da075f6d0948e4931625e9"));
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+}

+ 132 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/HandShake.java

@@ -0,0 +1,132 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.exception.ServiceException;
+import com.google.common.base.Strings;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESedeKeySpec;
+import java.net.URLEncoder;
+import java.security.spec.KeySpec;
+import java.text.SimpleDateFormat;
+import java.util.Base64;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * @Author: houn
+ * @Date: 2020/11/23 16:22
+ * @Description:
+ */
+
+public class HandShake {
+
+    public static final String HAND_SHAKE = "HAND_SHAKE";
+
+
+    public static final long HAND_SHAKE_TIME = 15 * 60 * 1000L;
+
+    private static final String UNICODE_FORMAT = "UTF8";
+    public static final String DESEDE_ENCRYPTION_SCHEME = "DESede";
+    private KeySpec myKeySpec;
+    private SecretKeyFactory mySecretKeyFactory;
+    private Cipher cipher;
+    byte[] keyAsBytes;
+    SecretKey key;
+
+    private static final String encryptionKey = "F03yMFT!GjWFWfR=G$3L$+d@";
+    private static final String baseURL = "https://site.recognia.com/cwgmarkets/serve.shtml?tkn=";
+
+    public static final String featured_forex_page = "featured_forex";
+    public static final String economic_calendar_page = "economic_calendar";
+    //    private static final String userID = "testUser";        /* USER ID must be that of the user logged into your website */
+    private static final String defaultLanguage = "en";   /* en (English), cs(Simplified Chinese) */
+
+    private String language;
+
+
+    public HandShake() throws Exception {
+        keyAsBytes = encryptionKey.getBytes(UNICODE_FORMAT);
+        myKeySpec = new DESedeKeySpec(keyAsBytes);
+        mySecretKeyFactory = SecretKeyFactory.getInstance(DESEDE_ENCRYPTION_SCHEME);
+        cipher = Cipher.getInstance(DESEDE_ENCRYPTION_SCHEME);
+        key = mySecretKeyFactory.generateSecret(myKeySpec);
+        language = defaultLanguage;
+    }
+
+    public HandShake(String language) throws Exception {
+        keyAsBytes = encryptionKey.getBytes(UNICODE_FORMAT);
+        myKeySpec = new DESedeKeySpec(keyAsBytes);
+        mySecretKeyFactory = SecretKeyFactory.getInstance(DESEDE_ENCRYPTION_SCHEME);
+        cipher = Cipher.getInstance(DESEDE_ENCRYPTION_SCHEME);
+        key = mySecretKeyFactory.generateSecret(myKeySpec);
+        this.language = language;
+    }
+
+    public HandShake(String encryptionKey, String language) throws Exception {
+        keyAsBytes = encryptionKey.getBytes(UNICODE_FORMAT);
+        myKeySpec = new DESedeKeySpec(keyAsBytes);
+        mySecretKeyFactory = SecretKeyFactory.getInstance(DESEDE_ENCRYPTION_SCHEME);
+        cipher = Cipher.getInstance(DESEDE_ENCRYPTION_SCHEME);
+        key = mySecretKeyFactory.generateSecret(myKeySpec);
+        this.language = language;
+    }
+
+    /**
+     * Method To Encrypt The String
+     */
+    public String encrypt(String unencryptedString) {
+        String encryptedString = null;
+        try {
+            cipher.init(Cipher.ENCRYPT_MODE, key);
+            byte[] plainText = unencryptedString.getBytes(UNICODE_FORMAT);
+            byte[] encryptedText = cipher.doFinal(plainText);
+
+            encryptedString = URLEncoder.encode(Base64.getEncoder().encodeToString(encryptedText), UNICODE_FORMAT);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return encryptedString;
+    }
+
+    /**
+     * Method To Create The Handshake URL
+     */
+    public String handShakeURL() throws ServiceException {
+
+        return handShakeURL(language);
+    }
+
+    public String handShakeURL(String language) throws ServiceException {
+
+        String hanShakeURL = handShakeURL(language,featured_forex_page);
+
+        return hanShakeURL;
+    }
+
+    public String handShakeURL(String language, String page) throws ServiceException {
+
+        final Date currentTime = new Date();
+        final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+        String userID = UUIDUtil.getUUID();
+        String parameters =
+                "aci=" + sdf.format(currentTime) + "&page=" + (Strings.isNullOrEmpty(page) ? featured_forex_page : page) +
+                        "&usi=" + userID +
+                        "&lang=" + language;
+
+        System.out.println(sdf.format(currentTime));
+        String hanShakeURL = baseURL + encrypt(parameters);
+
+        return hanShakeURL;
+    }
+
+    /**
+     * Testing
+     */
+    public static void main(String args[]) throws Exception {
+        HandShake handshake = new HandShake();
+        System.out.println(handshake.handShakeURL());
+    }
+}

+ 67 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/HexUtil.java

@@ -0,0 +1,67 @@
+package com.crm.rely.backend.util;
+
+/**
+ * @Author houn
+ * @Date 2024/4/22 12:52
+ * @PackageName:com.crm.rely.backend.util
+ * @ClassName: HexUtil
+ * @Description: TODO
+ */
+public final class HexUtil {
+    private static final char[] HEX = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd'
+            , 'e', 'f'};
+
+    public HexUtil() {
+    }
+
+    /**
+     * byte数组转16进制字符串
+     *
+     * @param bytes
+     * @return
+     */
+    public static String bytes2Hex(byte[] src) {
+        if (src == null || src.length <= 0) {
+            return null;
+        }
+
+        StringBuilder stringBuilder = new StringBuilder("");
+        for (int i = 0; i < src.length; i++) {
+            // 之所以用byte和0xff相与,是因为int是32位,与0xff相与后就舍弃前面的24位,只保留后8位
+            String str = Integer.toHexString(src[i] & 0xff);
+            if (str.length() < 2) { // 不足两位要补0
+                stringBuilder.append(0);
+            }
+            stringBuilder.append(str);
+        }
+        return stringBuilder.toString();
+    }
+
+    /**
+     * 16进制字符串转byte数组
+     *
+     * @param s 字符串
+     * @return
+     */
+    public static byte[] decode(CharSequence s) {
+        int nChars = s.length();
+        if (nChars % 2 != 0) {
+            throw new IllegalArgumentException("Hex-encoded string must have an even number of characters");
+        } else {
+            byte[] result = new byte[nChars / 2];
+
+            for (int i = 0; i < nChars; i += 2) {
+                int msb = Character.digit(s.charAt(i), 16);
+                int lsb = Character.digit(s.charAt(i + 1), 16);
+                if (msb < 0 || lsb < 0) {
+                    throw new IllegalArgumentException("Detected a Non-hex character at " + (i + 1) + " or " + (i + 2) + " position");
+                }
+
+                result[i / 2] = (byte) (msb << 4 | lsb);
+            }
+
+            return result;
+        }
+    }
+
+}

+ 104 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/HttpServletRequestUtil.java

@@ -0,0 +1,104 @@
+package com.crm.rely.backend.util;
+
+import com.crm.rely.backend.core.constant.Constants;
+import com.crm.rely.backend.core.entity.login.InfoEntity;
+import com.crm.rely.backend.exception.LoginException;
+import com.crm.rely.backend.exception.ServiceException;
+import com.crm.rely.backend.service.RedisService;
+import com.google.common.base.Strings;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @program: crm-backend
+ * @description:
+ * @author: houn
+ * @create: 2019-08-13 15:05
+ */
+public class HttpServletRequestUtil {
+
+    public static InfoEntity getLoginInfo(HttpServletRequest request, RedisService redisService) throws ServiceException {
+
+        return getLoginInfo(InfoEntity.class, request, redisService, false);
+    }
+
+    public static <T extends InfoEntity> T getLoginInfo(Class<?> clazz,
+                                                        HttpServletRequest request, RedisService redisService) throws ServiceException {
+
+        return getLoginInfo(clazz, request, redisService, false);
+    }
+
+    public static <T extends InfoEntity> T getLoginInfo(Class<?> clazz, HttpServletRequest request,
+                                                        RedisService redisService,
+                                                        boolean isDefaultResultNull) throws ServiceException {
+        T userUserInfoEntity = null;
+        String accessToken = request.getHeader(Constants.ACCESS_TOKEN);
+        if (Strings.isNullOrEmpty(accessToken) && isDefaultResultNull) {
+            return userUserInfoEntity;
+        }
+        userUserInfoEntity = getLoginInfo(clazz, accessToken, redisService, isDefaultResultNull);
+
+        return userUserInfoEntity;
+    }
+
+    public static InfoEntity getLoginInfo(HttpServletRequest request, RedisService redisService,
+                                          boolean isDefaultResultNull) throws ServiceException {
+        InfoEntity userUserInfoEntity = null;
+        String accessToken = request.getHeader(Constants.ACCESS_TOKEN);
+        if (Strings.isNullOrEmpty(accessToken) && isDefaultResultNull) {
+            return userUserInfoEntity;
+        }
+        userUserInfoEntity = getLoginInfo(InfoEntity.class, accessToken, redisService, isDefaultResultNull);
+
+        return userUserInfoEntity;
+    }
+
+    public static InfoEntity getLoginInfo(String accessToken, RedisService redisService) throws ServiceException {
+        return getLoginInfo(InfoEntity.class, accessToken, redisService, false);
+    }
+
+    public static <T extends InfoEntity> T getLoginInfo(Class<?> clazz, String accessToken,
+                                                        RedisService redisService) throws ServiceException {
+        return getLoginInfo(clazz, accessToken, redisService, false);
+    }
+
+    public static InfoEntity getLoginInfo(String accessToken, RedisService redisService, boolean isDefaultResultNull) throws ServiceException {
+
+        InfoEntity userUserInfoEntity = getLoginInfo(InfoEntity.class, accessToken, redisService, isDefaultResultNull);
+        return userUserInfoEntity;
+    }
+
+    public static <T extends InfoEntity> T getLoginInfo(Class<?> clazz, String accessToken, RedisService redisService,
+                                                        boolean isDefaultResultNull) throws ServiceException {
+
+        T userUserInfoEntity = null;
+
+        if (!Strings.isNullOrEmpty(accessToken)) {
+            String decryptString = AESUtil.decrypt(accessToken);
+            if (!Strings.isNullOrEmpty(decryptString)) {
+                String[] sp = decryptString.split(Constants.LOGIN_LINK);
+                if (sp != null && sp.length >= 2) {
+
+                    String token = sp[0];
+//                    String source = sp[1];
+                    String value = redisService.getObject(token);
+                    if (!Strings.isNullOrEmpty(value)) {
+//                        userUserInfoEntity = redisService.getObject(value);
+                        if (clazz == null) {
+                            userUserInfoEntity = (T) redisService.getEntity(value, InfoEntity.class);
+                        } else {
+                            userUserInfoEntity = (T) redisService.getEntity(value, clazz);
+                        }
+                        if (userUserInfoEntity == null) {
+                            redisService.remove(token);
+                        }
+                    }
+                }
+            }
+        }
+        if (userUserInfoEntity == null && !isDefaultResultNull) {
+            throw new LoginException(Constants.INVALID);
+        }
+        return userUserInfoEntity;
+    }
+}

+ 445 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/HttpUtil.java

@@ -0,0 +1,445 @@
+package com.crm.rely.backend.util;
+
+
+import com.alibaba.fastjson.JSON;
+import com.google.common.base.Strings;
+import org.jsoup.Connection;
+import org.jsoup.Connection.Method;
+import org.jsoup.Connection.Response;
+import org.jsoup.Jsoup;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509TrustManager;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * @program: cwg-my-base
+ * @description:
+ * @author: houn
+ * @create: 2019-08-02 09:42
+ */
+public class HttpUtil {
+
+    /**
+     * 请求超时时间
+     */
+    private static final int TIME_OUT = 300000;
+
+    /**
+     * Https请求
+     */
+    private static final String HTTPS = "https";
+
+    /**
+     * Content-Type
+     */
+    private static final String CONTENT_TYPE = "Content-Type";
+
+    /**
+     * 表单提交方式Content-Type
+     */
+    private static final String FORM_TYPE = "application/x-www-form-urlencoded;charset=UTF-8";
+
+    /**
+     * JSON提交方式Content-Type
+     */
+    private static final String JSON_TYPE = "application/json;charset=UTF-8";
+
+    /**
+     * 发送Get请求
+     *
+     * @param url 请求URL
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response get(String url) throws IOException {
+        return get(url, null);
+    }
+
+    public static Response getForm(String url, Map<String, String> data) throws IOException {
+        return get(url, null, data);
+    }
+
+    /**
+     * 发送Get请求
+     *
+     * @param url     请求URL
+     * @param headers 请求头参数
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response get(String url, Map<String, String> headers) throws IOException {
+
+        return get(url, headers, null);
+    }
+
+    public static Response get(String url, Map<String, String> headers, Map<String, String> data) throws IOException {
+
+        Connection connection = getConnection(url, headers, Method.GET, data);
+        Response response = connection.execute();
+        return response;
+    }
+
+    public static Response put(String url, Map<String, String> headers, Map<String, String> data) throws IOException {
+
+        Connection connection = getConnection(url, headers, Method.PUT, data);
+        Response response = connection.execute();
+        return response;
+    }
+
+    public static Response post(String url) throws IOException {
+        return doPostRequest(url, null, null);
+    }
+
+    public static Response post(String url, Map<String, String> headers, Object ob) throws IOException {
+        return doPostRequest(url, headers, JSON.toJSONString(ob));
+    }
+
+    /**
+     * 发送JSON格式参数POST请求
+     *
+     * @param url    请求路径
+     * @param params JSON格式请求参数
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response post(String url, String params) throws IOException {
+        return doPostRequest(url, null, params);
+    }
+
+    /**
+     * 发送JSON格式参数POST请求
+     *
+     * @param url     请求路径
+     * @param headers 请求头中设置的参数
+     * @param params  JSON格式请求参数
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response post(String url, Map<String, String> headers, String params) throws IOException {
+        return doPostRequest(url, headers, params);
+    }
+
+    /**
+     * 字符串参数post请求
+     *
+     * @param url      请求URL地址
+     * @param paramMap 请求字符串参数集合
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response post(String url, Map<String, String> paramMap) throws IOException {
+        return doPostRequest(url, null, paramMap, null);
+    }
+
+    /**
+     * 带请求头的普通表单提交方式post请求
+     *
+     * @param headers  请求头参数
+     * @param url      请求URL地址
+     * @param paramMap 请求字符串参数集合
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response post(Map<String, String> headers, String url, Map<String, String> paramMap) throws IOException {
+        return doPostRequest(url, headers, paramMap, null);
+    }
+
+    /**
+     * 带上传文件的post请求
+     *
+     * @param url      请求URL地址
+     * @param paramMap 请求字符串参数集合
+     * @param fileMap  请求文件参数集合
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response post(String url, Map<String, String> paramMap, Map<String, File> fileMap) throws IOException {
+        return doPostRequest(url, null, paramMap, fileMap);
+    }
+
+    /**
+     * 带请求头的上传文件post请求
+     *
+     * @param url      请求URL地址
+     * @param headers  请求头参数
+     * @param paramMap 请求字符串参数集合
+     * @param fileMap  请求文件参数集合
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    public static Response post(String url, Map<String, String> headers, Map<String, String> paramMap, Map<String,
+            File> fileMap) throws IOException {
+        return doPostRequest(url, headers, paramMap, fileMap);
+    }
+
+    /**
+     * 执行post请求
+     *
+     * @param url        请求URL地址
+     * @param headers    请求头
+     * @param jsonParams 请求JSON格式字符串参数
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    private static Response doPostRequest(String url, Map<String, String> headers, String jsonParams) throws IOException {
+        if (null == url || url.isEmpty()) {
+            throw new RuntimeException("The request URL is blank.");
+        }
+
+        // 如果是Https请求
+        if (url.startsWith(HTTPS)) {
+            getTrust();
+        }
+
+        Connection connection = Jsoup.connect(url);
+        connection.method(Method.POST);
+        connection.timeout(getTimeOut(headers));
+        connection.ignoreHttpErrors(true);
+        connection.ignoreContentType(true);
+        connection.maxBodySize(0);
+        String jsonType = null;
+        if (null != headers) {
+            connection.headers(headers);
+            for (Entry<String, String> header : headers.entrySet()) {
+                if (CONTENT_TYPE.equals(header.getKey())) {
+                    jsonType = header.getValue();
+                    break;
+                }
+            }
+
+        }
+        if (jsonType == null) {
+            jsonType = JSON_TYPE;
+        }
+        if (headers != null && headers.size() > 0) {
+
+            connection.headers(headers);
+        }
+        connection.header(CONTENT_TYPE, jsonType);
+        if (!Strings.isNullOrEmpty(jsonParams)) {
+
+            connection.requestBody(jsonParams);
+        }
+
+        Response response = connection.execute();
+        return response;
+    }
+
+    /**
+     * 普通表单方式发送POST请求
+     *
+     * @param url      请求URL地址
+     * @param headers  请求头
+     * @param paramMap 请求字符串参数集合
+     * @param fileMap  请求文件参数集合
+     * @return HTTP响应对象
+     * @throws IOException 程序异常时抛出,由调用者处理
+     */
+    private static Response doPostRequest(String url, Map<String, String> headers, Map<String, String> paramMap,
+                                          Map<String, File> fileMap) throws IOException {
+
+        Connection connection = getConnection(url, headers, Method.POST, null);
+
+        // 收集上传文件输入流,最终全部关闭
+        List<InputStream> inputStreamList = null;
+        try {
+
+            // 添加文件参数
+            if (null != fileMap && !fileMap.isEmpty()) {
+                inputStreamList = new ArrayList<InputStream>();
+                InputStream in = null;
+                File file = null;
+                for (Entry<String, File> e : fileMap.entrySet()) {
+                    file = e.getValue();
+                    in = new FileInputStream(file);
+                    inputStreamList.add(in);
+                    connection.data(e.getKey(), file.getName(), in);
+                }
+            }
+
+            // 普通表单提交方式
+            else {
+                boolean isType = false;
+                if (null != headers) {
+                    connection.headers(headers);
+                    for (Entry<String, String> header : headers.entrySet()) {
+                        if (CONTENT_TYPE.equals(header.getKey())) {
+                            connection.header(CONTENT_TYPE, header.getValue());
+                            isType = true;
+                            break;
+                        }
+                    }
+
+                }
+                if (!isType) {
+
+                    connection.header(CONTENT_TYPE, FORM_TYPE);
+                }
+            }
+
+            // 添加字符串类参数
+            if (null != paramMap && !paramMap.isEmpty()) {
+
+                for (Entry<String, String> mapEntrySet : paramMap.entrySet()) {
+                    if (mapEntrySet.getValue() == null) {
+                        mapEntrySet.setValue("");
+                    }
+                }
+                connection.data(paramMap);
+            }
+
+            Response response = connection.execute();
+            return response;
+        }
+
+        // 关闭上传文件的输入流
+        finally {
+            closeStream(inputStreamList);
+        }
+    }
+
+    /**
+     * 关流
+     *
+     * @param streamList 流集合
+     */
+    private static void closeStream(List<? extends Closeable> streamList) {
+        if (null != streamList) {
+            for (Closeable stream : streamList) {
+                try {
+                    stream.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     * 获取服务器信任
+     */
+    private static void getTrust() {
+        try {
+            HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
+            SSLContext context = SSLContext.getInstance("TLS");
+            context.init(null, new X509TrustManager[]{new X509TrustManager() {
+
+                @Override
+                public void checkClientTrusted(X509Certificate[] chain, String authType) {
+                }
+
+                @Override
+                public void checkServerTrusted(X509Certificate[] chain, String authType) {
+                }
+
+                @Override
+                public X509Certificate[] getAcceptedIssuers() {
+                    return new X509Certificate[0];
+                }
+            }}, new SecureRandom());
+            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取连接
+     *
+     * @param url
+     * @param headers
+     * @param method
+     * @return
+     */
+    private static Connection getConnection(String url, Map<String, String> headers, Method method, Map<String,
+            String> data) {
+        if (null == url || url.isEmpty()) {
+            throw new RuntimeException("The request URL is blank.");
+        }
+
+        // 如果是Https请求
+        if (url.startsWith(HTTPS)) {
+//            getTrust();
+        }
+        Connection connection = Jsoup.connect(url);
+        connection.method(method);
+
+        connection.timeout(getTimeOut(headers));
+
+        connection.ignoreHttpErrors(true);
+        connection.ignoreContentType(true);
+        connection.maxBodySize(0);
+
+        if (null != headers) {
+            connection.headers(headers);
+        }
+
+        if (null != data) {
+            if (Method.PUT.equals(method)) {
+                connection.requestBody(JSON.toJSONString(data));
+            } else {
+                connection.data(data);
+            }
+        }
+        return connection;
+    }
+
+    private static int getTimeOut(Map<String, String> headers) {
+        int time = 0;
+        try {
+            if (headers != null && !headers.isEmpty()) {
+                String timeOut = headers.get("TIME_OUT");
+                if (!Strings.isNullOrEmpty(timeOut)) {
+                    time = Integer.parseInt(timeOut);
+                }
+            }
+        } finally {
+            if (time <= 0) {
+                time = TIME_OUT;
+            }
+        }
+        return time;
+    }
+
+    public static boolean download(String urlPath, String targetDirectory, String fileName) throws Exception {
+        // 解决url中可能有中文情况
+        URL url = new URL(urlPath);
+        HttpURLConnection http = (HttpURLConnection) url.openConnection();
+        http.setConnectTimeout(3000);
+        // 设置 User-Agent 避免被拦截
+        http.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)");
+        String contentType = http.getContentType();
+        // 获取文件大小
+        long length = http.getContentLengthLong();
+        // 获取文件名
+        InputStream inputStream = http.getInputStream();
+        byte[] buff = new byte[1024 * 10];
+        File file = new File(targetDirectory, fileName);
+        if (!file.exists()) {
+            OutputStream out = new FileOutputStream(file);
+            int len;
+            int count = 0; // 计数
+            while ((len = inputStream.read(buff)) != -1) {
+                out.write(buff, 0, len);
+                out.flush();
+                ++count;
+            }
+            // 关闭资源
+            out.close();
+            inputStream.close();
+            http.disconnect();
+            return true;
+        }
+        return false;
+    }
+}

ファイルの差分が大きいため隠しています
+ 79 - 0
ucard-backend/src/main/java/com/crm/rely/backend/util/ImageBase64ConverterUtil.java


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません