浏览代码

新增elasticsearch模块;新增MySQL binlog处理逻辑;优化多服务打包处理逻辑;已知问题修复;

woody 8 月之前
父节点
当前提交
c42c4a4a28
共有 42 个文件被更改,包括 2035 次插入189 次删除
  1. 3 9
      framework-base/src/main/java/com/chelvc/framework/base/config/MultiserverMvcConfigurer.java
  2. 101 65
      framework-base/src/main/java/com/chelvc/framework/base/context/ApplicationContextHolder.java
  3. 19 0
      framework-base/src/main/java/com/chelvc/framework/base/converter/StringRangeConverter.java
  4. 31 0
      framework-common/src/main/java/com/chelvc/framework/common/model/Invoking.java
  5. 60 0
      framework-common/src/main/java/com/chelvc/framework/common/model/Range.java
  6. 49 1
      framework-common/src/main/java/com/chelvc/framework/common/util/DateUtils.java
  7. 1 1
      framework-common/src/main/java/com/chelvc/framework/common/util/ExcelUtils.java
  8. 178 10
      framework-common/src/main/java/com/chelvc/framework/common/util/JacksonUtils.java
  9. 34 6
      framework-common/src/main/java/com/chelvc/framework/common/util/NumberUtils.java
  10. 55 0
      framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java
  11. 47 22
      framework-common/src/main/java/com/chelvc/framework/common/util/StringUtils.java
  12. 3 0
      framework-database/src/main/java/com/chelvc/framework/database/config/DatabaseConfigurer.java
  13. 10 0
      framework-database/src/main/java/com/chelvc/framework/database/config/TypeHandlerConfigurer.java
  14. 14 0
      framework-database/src/main/java/com/chelvc/framework/database/context/DatabaseContextHolder.java
  15. 12 0
      framework-database/src/main/java/com/chelvc/framework/database/handler/RangeArrayTypeHandler.java
  16. 14 0
      framework-database/src/main/java/com/chelvc/framework/database/handler/RangeSetTypeHandler.java
  17. 39 0
      framework-database/src/main/java/com/chelvc/framework/database/handler/RangeTypeHandler.java
  18. 14 0
      framework-database/src/main/java/com/chelvc/framework/database/handler/RangesTypeHandler.java
  19. 80 46
      framework-database/src/main/java/com/chelvc/framework/database/support/Binlog.java
  20. 13 0
      framework-database/src/main/java/com/chelvc/framework/database/support/EnhanceService.java
  21. 1 1
      framework-database/src/main/java/com/chelvc/framework/database/support/Updates.java
  22. 34 0
      framework-elasticsearch/pom.xml
  23. 348 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/DefaultElasticsearchHandler.java
  24. 287 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/ElasticsearchHandler.java
  25. 85 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/ElasticsearchUtils.java
  26. 93 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/ModelContext.java
  27. 27 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/annotation/Document.java
  28. 19 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/annotation/Identity.java
  29. 57 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/config/ElasticsearchConfigurer.java
  30. 51 0
      framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/config/ElasticsearchProperties.java
  31. 2 1
      framework-email/src/main/java/com/chelvc/framework/email/context/EmailContextHolder.java
  32. 11 5
      framework-feign/src/main/java/com/chelvc/framework/feign/config/FeignConfigurer.java
  33. 45 0
      framework-feign/src/main/java/com/chelvc/framework/feign/config/FeignProperties.java
  34. 1 1
      framework-feign/src/main/java/com/chelvc/framework/feign/encoder/CustomizeEncoder.java
  35. 37 0
      framework-feign/src/main/java/com/chelvc/framework/feign/encoder/CustomizeQueryEncoder.java
  36. 70 11
      framework-feign/src/main/java/com/chelvc/framework/feign/interceptor/FeignInvokeInterceptor.java
  37. 4 8
      framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/config/RocketMQConfigurer.java
  38. 47 0
      framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/producer/CommonTransactionChecker.java
  39. 3 1
      framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/producer/RocketMQProducerFactory.java
  40. 34 0
      framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/producer/TransactionMessageChecker.java
  41. 1 1
      framework-security/src/main/java/com/chelvc/framework/security/config/AuthorizeConfigurer.java
  42. 1 0
      pom.xml

+ 3 - 9
framework-base/src/main/java/com/chelvc/framework/base/config/MultiserverMvcConfigurer.java

@@ -23,15 +23,9 @@ public class MultiserverMvcConfigurer implements WebMvcRegistrations {
         List<Resource> resources = ApplicationContextHolder.getApplicationResources();
         Map<String, Predicate<Class<?>>> prefixes = Maps.newHashMapWithExpectedSize(resources.size());
         for (Resource resource : resources) {
-            String prefix = ApplicationContextHolder.getResourcePrefix(resource);
-            if (StringUtils.notEmpty(prefix)) {
-                prefixes.put(prefix, clazz -> {
-                    if (ApplicationContextHolder.isResourceClass(resource, clazz)) {
-                        ApplicationContextHolder.cachingResourcePrefix(prefix, clazz);
-                        return true;
-                    }
-                    return false;
-                });
+            String server = ApplicationContextHolder.getApplicationName(resource);
+            if (StringUtils.notEmpty(server)) {
+                prefixes.put(server, clazz -> ApplicationContextHolder.isResourceClass(resource, clazz));
             }
         }
         RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();

+ 101 - 65
framework-base/src/main/java/com/chelvc/framework/base/context/ApplicationContextHolder.java

@@ -2,7 +2,6 @@ package com.chelvc.framework.base.context;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.Arrays;
@@ -78,6 +77,11 @@ import org.springframework.web.bind.annotation.RestController;
 @Component
 @Order(Ordered.HIGHEST_PRECEDENCE)
 public class ApplicationContextHolder implements ApplicationContextAware, PropertyRefreshListener {
+    /**
+     * 服务端口属性
+     */
+    public static final String SERVER_PORT_PROPERTY = "server.port";
+
     /**
      * 应用名称属性
      */
@@ -88,6 +92,18 @@ public class ApplicationContextHolder implements ApplicationContextAware, Proper
      */
     public static final String APPLICATION_PROFILE_PROPERTY = "spring.profiles.active";
 
+    /**
+     * bootstrap.yml文件搜索路径
+     */
+    public static final String BOOTSTRAP_YML_PATH =
+            ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "/bootstrap.yml";
+
+    /**
+     * bootstrap.properties文件搜索路径
+     */
+    public static final String BOOTSTRAP_PROPERTIES_PATH =
+            ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "/bootstrap.properties";
+
     /**
      * application.yml文件搜索路径
      */
@@ -105,11 +121,6 @@ public class ApplicationContextHolder implements ApplicationContextAware, Proper
      */
     private static final Map<String, Object> PROPERTY_OBJECT_MAPPING = Maps.newConcurrentMap();
 
-    /**
-     * 资源前缀/接口地址集合映射表
-     */
-    private static final Map<String, Set<String>> RESOURCE_PREFIX_MAPPING = Maps.newHashMapWithExpectedSize(0);
-
     /**
      * 应用上下文对象
      */
@@ -127,6 +138,41 @@ public class ApplicationContextHolder implements ApplicationContextAware, Proper
         }
     }
 
+    /**
+     * 获取服务端口
+     *
+     * @return 服务端口
+     */
+    public static Integer getPort() {
+        return getPort(getApplicationContext());
+    }
+
+    /**
+     * 根据 *.yml、*.properties文件资源获取服务端口
+     *
+     * @param resource *.yml、*.properties文件资源对象实例
+     * @return 服务端口
+     */
+    public static Integer getPort(@NonNull Resource resource) {
+        Properties properties = getResourceProperties(resource);
+        if (properties == null) {
+            return null;
+        }
+        String property = properties.getProperty(SERVER_PORT_PROPERTY);
+        return StringUtils.ifEmpty(property, Integer::parseInt);
+    }
+
+    /**
+     * 获取服务端口
+     *
+     * @param applicationContext 应用上下文实例
+     * @return 服务端口
+     */
+    public static Integer getPort(@NonNull ApplicationContext applicationContext) {
+        String property = applicationContext.getEnvironment().getProperty(SERVER_PORT_PROPERTY);
+        return StringUtils.ifEmpty(property, Integer::parseInt);
+    }
+
     /**
      * 获取环境标识
      *
@@ -136,6 +182,17 @@ public class ApplicationContextHolder implements ApplicationContextAware, Proper
         return getProfile(getApplicationContext());
     }
 
+    /**
+     * 根据 *.yml、*.properties文件资源获取环境标识
+     *
+     * @param resource *.yml、*.properties文件资源对象实例
+     * @return 环境标识
+     */
+    public static String getProfile(@NonNull Resource resource) {
+        Properties properties = getResourceProperties(resource);
+        return properties == null ? null : properties.getProperty(APPLICATION_PROFILE_PROPERTY);
+    }
+
     /**
      * 获取环境标识
      *
@@ -199,28 +256,13 @@ public class ApplicationContextHolder implements ApplicationContextAware, Proper
     }
 
     /**
-     * 根据application.yml/application.properties文件资源获取应用名称
+     * 根据 *.yml、*.properties文件资源获取应用名称
      *
-     * @param resource application.yml/application.properties文件资源对象实例
+     * @param resource *.yml、*.properties文件资源对象实例
      * @return 应用名称
      */
     public static String getApplicationName(@NonNull Resource resource) {
-        Properties properties;
-        String filename = resource.getFilename();
-        if (StringUtils.isEmpty(filename) || filename.endsWith(".yml")) {
-            // application.yml
-            YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
-            yaml.setResources(resource);
-            properties = yaml.getObject();
-        } else {
-            // application.properties
-            properties = new Properties();
-            try (InputStream input = resource.getInputStream()) {
-                properties.load(input);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
+        Properties properties = getResourceProperties(resource);
         return properties == null ? null : properties.getProperty(APPLICATION_NAME_PROPERTY);
     }
 
@@ -252,42 +294,37 @@ public class ApplicationContextHolder implements ApplicationContextAware, Proper
      * @return 资源对象数组
      */
     public static List<Resource> getApplicationResources() {
-        Resource[] ymls, properties;
+        List<Resource> resources = Lists.newLinkedList();
         ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
 
-        // 查找yml配置文件
+        // bootstrap.yml配置文件
         try {
-            ymls = resourcePatternResolver.getResources(APPLICATION_YML_PATH);
+            resources.addAll(Arrays.asList(resourcePatternResolver.getResources(BOOTSTRAP_YML_PATH)));
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
 
-        // 查找properties配置文件
+        // 查找application.yml配置文件
         try {
-            properties = resourcePatternResolver.getResources(APPLICATION_PROPERTIES_PATH);
+            resources.addAll(Arrays.asList(resourcePatternResolver.getResources(APPLICATION_YML_PATH)));
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
 
-        // 构建配置文件列表
-        if (ObjectUtils.isEmpty(ymls) && ObjectUtils.isEmpty(properties)) {
-            return Collections.emptyList();
+        // bootstrap.properties配置文件
+        try {
+            resources.addAll(Arrays.asList(resourcePatternResolver.getResources(BOOTSTRAP_PROPERTIES_PATH)));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
         }
-        List<Resource> resources = Lists.newArrayListWithCapacity(ymls.length + properties.length);
-        resources.addAll(Arrays.asList(ymls));
-        resources.addAll(Arrays.asList(properties));
-        return resources;
-    }
 
-    /**
-     * 获取资源前缀
-     *
-     * @param resource 资源对象
-     * @return 资源前缀标识
-     */
-    public static String getResourcePrefix(@NonNull Resource resource) {
-        String application = getApplicationName(resource);
-        return StringUtils.isEmpty(application) ? application : application.replace('.', '/');
+        // 查找application.properties配置文件
+        try {
+            resources.addAll(Arrays.asList(resourcePatternResolver.getResources(APPLICATION_PROPERTIES_PATH)));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return resources;
     }
 
     /**
@@ -307,28 +344,27 @@ public class ApplicationContextHolder implements ApplicationContextAware, Proper
     }
 
     /**
-     * 判断资源地址是否存在前缀
-     *
-     * @param prefix 前缀地址
-     * @param uri    资源地址
-     * @return true/false
-     */
-    public static boolean isResourcePrefixed(@NonNull String prefix, @NonNull String uri) {
-        Set<String> uris = RESOURCE_PREFIX_MAPPING.get(prefix);
-        return ObjectUtils.notEmpty(uris) && uris.contains(uri);
-    }
-
-    /**
-     * 缓存资源地址前缀
+     * 获取资源文件 .yml、.properties属性
      *
-     * @param prefix 前缀地址
-     * @param clazz  资源对象
+     * @param resource *.yml、*.properties文件资源对象实例
+     * @return 应用名称
      */
-    public synchronized static void cachingResourcePrefix(@NonNull String prefix, @NonNull Class<?> clazz) {
-        Set<String> uris = RESOURCE_PREFIX_MAPPING.computeIfAbsent(prefix, p -> Sets.newHashSet());
-        for (Method method : clazz.getDeclaredMethods()) {
-            uris.addAll(SpringUtils.getApis(method));
+    public static Properties getResourceProperties(@NonNull Resource resource) {
+        String filename = resource.getFilename();
+        if (StringUtils.isEmpty(filename) || filename.endsWith(".yml")) {
+            // yml
+            YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
+            yaml.setResources(resource);
+            return yaml.getObject();
+        }
+        // properties
+        Properties properties = new Properties();
+        try (InputStream input = resource.getInputStream()) {
+            properties.load(input);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
         }
+        return properties;
     }
 
     /**

+ 19 - 0
framework-base/src/main/java/com/chelvc/framework/base/converter/StringRangeConverter.java

@@ -0,0 +1,19 @@
+package com.chelvc.framework.base.converter;
+
+import com.chelvc.framework.common.model.Range;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.stereotype.Component;
+
+/**
+ * 字符串数字范围转换器
+ *
+ * @author Woody
+ * @date 2024/10/30
+ */
+@Component
+public class StringRangeConverter implements Converter<String, Range> {
+    @Override
+    public Range convert(String source) {
+        return Range.parse(source);
+    }
+}

+ 31 - 0
framework-common/src/main/java/com/chelvc/framework/common/model/Invoking.java

@@ -0,0 +1,31 @@
+package com.chelvc.framework.common.model;
+
+import lombok.Getter;
+
+/**
+ * 方法调用阶段枚举
+ *
+ * @author Woody
+ * @date 2024/11/7
+ */
+@Getter
+public enum Invoking implements Enumerable {
+    /**
+     * 请求
+     */
+    REQUEST("请求"),
+
+    /**
+     * 响应
+     */
+    RESPONSE("响应");
+
+    /**
+     * 阶段描述
+     */
+    private final String description;
+
+    Invoking(String description) {
+        this.description = description;
+    }
+}

+ 60 - 0
framework-common/src/main/java/com/chelvc/framework/common/model/Range.java

@@ -0,0 +1,60 @@
+package com.chelvc.framework.common.model;
+
+import java.io.Serializable;
+
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * 数字范围对象
+ *
+ * @author Woody
+ * @date 2024/10/30
+ */
+@Getter
+@Builder
+@EqualsAndHashCode
+@NoArgsConstructor
+@AllArgsConstructor
+public final class Range implements Serializable {
+    /**
+     * 最小值
+     */
+    private Integer min;
+
+    /**
+     * 最大值
+     */
+    private Integer max;
+
+    /**
+     * 将字符串转换成数字范围信息
+     * <p>
+     * 字符串以","号作为最小/大范围的分隔符,如果没有分隔符则默认匹配最小值
+     * <p>
+     * 示例:(1, 10)、(, 10)、(1, )
+     *
+     * @param text 范围信息字符串格式
+     * @return 范围信息实例
+     */
+    public static Range parse(String text) {
+        return StringUtils.decompose(text, Range::new, Integer::parseInt);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        if (StringUtils.notEmpty(this.min)) {
+            buffer.append(this.min);
+        }
+        buffer.append('\t');
+        if (StringUtils.notEmpty(this.max)) {
+            buffer.append(this.max);
+        }
+        return buffer.toString();
+    }
+}

+ 49 - 1
framework-common/src/main/java/com/chelvc/framework/common/util/DateUtils.java

@@ -1111,7 +1111,36 @@ public final class DateUtils {
     }
 
     /**
-     * 判断日期时间是否在置顶时间范围
+     * 判断当前时间是否在指定时间范围
+     *
+     * @param begin 开始时间
+     * @param end   结束时间
+     * @return true/false
+     */
+    public static boolean between(Date begin, Date end) {
+        return begin != null && end != null && between(new Date(), begin, end);
+    }
+
+    /**
+     * 判断当前时间是否在指定时间范围
+     *
+     * @param begin  开始时间
+     * @param end    结束时间
+     * @param offset 偏移量
+     * @return true/false
+     */
+    public static boolean between(Date begin, Date end, Duration offset) {
+        if (begin == null || end == null) {
+            return false;
+        } else if (offset == null) {
+            return between(new Date(), begin, end);
+        }
+        Date date = new Date(System.currentTimeMillis() + offset.toMillis());
+        return between(date, begin, end);
+    }
+
+    /**
+     * 判断日期时间是否在指定时间范围
      *
      * @param datetime 日期时间
      * @param begin    开始时间
@@ -1122,6 +1151,25 @@ public final class DateUtils {
         return datetime != null && begin != null && end != null && !datetime.before(begin) && !datetime.after(end);
     }
 
+    /**
+     * 判断日期时间是否在指定时间范围
+     *
+     * @param datetime 日期时间
+     * @param begin    开始时间
+     * @param end      结束时间
+     * @param offset   偏移量
+     * @return true/false
+     */
+    public static boolean between(Date datetime, Date begin, Date end, Duration offset) {
+        if (datetime == null || begin == null || end == null) {
+            return false;
+        } else if (offset == null) {
+            return between(datetime, begin, end);
+        }
+        datetime = add(datetime, offset);
+        return !datetime.before(begin) && !datetime.after(end);
+    }
+
     /**
      * 获取指定时间类型倒计时
      *

+ 1 - 1
framework-common/src/main/java/com/chelvc/framework/common/util/ExcelUtils.java

@@ -172,7 +172,7 @@ public final class ExcelUtils {
         if (value instanceof Date) {
             return DateUtils.format((Date) value);
         } else if (value instanceof Number) {
-            return BigDecimal.valueOf(((Number) value).doubleValue()).stripTrailingZeros().toPlainString();
+            return new BigDecimal(value.toString()).stripTrailingZeros().toPlainString();
         }
         return ObjectUtils.ifNull(value, Object::toString);
     }

+ 178 - 10
framework-common/src/main/java/com/chelvc/framework/common/util/JacksonUtils.java

@@ -21,12 +21,14 @@ import java.util.stream.Collectors;
 
 import com.chelvc.framework.common.model.Paging;
 import com.chelvc.framework.common.model.Period;
+import com.chelvc.framework.common.model.Range;
 import com.chelvc.framework.common.model.Region;
 import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.BeanDescription;
 import com.fasterxml.jackson.databind.DeserializationContext;
@@ -72,10 +74,15 @@ public final class JacksonUtils {
     private static volatile SimpleModule BASE_MODULE;
 
     /**
-     * Jackson处理安全模块
+     * Jackson安全处理模块
      */
     private static volatile SimpleModule SAFETY_MODULE;
 
+    /**
+     * Jackson数字处理模块
+     */
+    private static volatile SimpleModule NUMBER_MODULE;
+
     /**
      * Jackson null对象序列化处理器更新处理实现
      */
@@ -118,6 +125,66 @@ public final class JacksonUtils {
     private JacksonUtils() {
     }
 
+    /**
+     * 获取JSON长整形值
+     *
+     * @param parser Json Parser
+     * @return 长整形数字
+     * @throws IOException I/O异常
+     */
+    private static Long getLongValue(JsonParser parser) throws IOException {
+        JsonToken token = parser.getCurrentToken();
+        if (token.isNumeric()) {
+            JsonParser.NumberType type = parser.getNumberType();
+            if (type == JsonParser.NumberType.FLOAT || type == JsonParser.NumberType.DOUBLE) {
+                return new BigDecimal(parser.getValueAsString()).longValue();
+            }
+        } else if (token == JsonToken.VALUE_STRING) {
+            return StringUtils.ifEmpty(parser.getText(), text -> new BigDecimal(text).longValue());
+        }
+        return parser.getLongValue();
+    }
+
+    /**
+     * 获取JSON短整形值
+     *
+     * @param parser Json Parser
+     * @return 短整形数字
+     * @throws IOException I/O异常
+     */
+    private static Short getShortValue(JsonParser parser) throws IOException {
+        JsonToken token = parser.getCurrentToken();
+        if (token.isNumeric()) {
+            JsonParser.NumberType type = parser.getNumberType();
+            if (type == JsonParser.NumberType.FLOAT || type == JsonParser.NumberType.DOUBLE) {
+                return new BigDecimal(parser.getValueAsString()).shortValue();
+            }
+        } else if (token == JsonToken.VALUE_STRING) {
+            return StringUtils.ifEmpty(parser.getText(), text -> new BigDecimal(text).shortValue());
+        }
+        return parser.getShortValue();
+    }
+
+    /**
+     * 获取JSON整形值
+     *
+     * @param parser Json Parser
+     * @return 整形数字
+     * @throws IOException I/O异常
+     */
+    private static Integer getIntegerValue(JsonParser parser) throws IOException {
+        JsonToken token = parser.getCurrentToken();
+        if (token.isNumeric()) {
+            JsonParser.NumberType type = parser.getNumberType();
+            if (type == JsonParser.NumberType.FLOAT || type == JsonParser.NumberType.DOUBLE) {
+                return new BigDecimal(parser.getValueAsString()).intValue();
+            }
+        } else if (token == JsonToken.VALUE_STRING) {
+            return StringUtils.ifEmpty(parser.getText(), text -> new BigDecimal(text).intValue());
+        }
+        return parser.getIntValue();
+    }
+
     /**
      * 构建序列化处理器基础模块
      *
@@ -154,6 +221,20 @@ public final class JacksonUtils {
             }
         });
 
+        // 设置数字范围序列化处理器
+        module.addSerializer(new JsonSerializer<Range>() {
+            @Override
+            public Class<Range> handledType() {
+                return Range.class;
+            }
+
+            @Override
+            public void serialize(Range range, JsonGenerator generator, SerializerProvider provider)
+                    throws IOException {
+                generator.writeString(range.toString());
+            }
+        });
+
         // 设置时间周期序列化处理器
         module.addSerializer(new JsonSerializer<Period>() {
             @Override
@@ -164,9 +245,7 @@ public final class JacksonUtils {
             @Override
             public void serialize(Period period, JsonGenerator generator, SerializerProvider provider)
                     throws IOException {
-                String begin = ObjectUtils.ifNull(period.getBegin(), DateUtils::format, () -> StringUtils.EMPTY);
-                String end = ObjectUtils.ifNull(period.getEnd(), DateUtils::format, () -> StringUtils.EMPTY);
-                generator.writeString(begin + "," + end);
+                generator.writeString(period.toString());
             }
         });
 
@@ -180,7 +259,7 @@ public final class JacksonUtils {
             @Override
             public void serialize(Paging paging, JsonGenerator generator, SerializerProvider provider)
                     throws IOException {
-                generator.writeString(paging.getNumber() + "," + paging.getSize());
+                generator.writeString(paging.toString());
             }
         });
 
@@ -252,6 +331,19 @@ public final class JacksonUtils {
             }
         });
 
+        // 设置数字范围反序列化处理器
+        module.addDeserializer(Range.class, new JsonDeserializer<Range>() {
+            @Override
+            public Class<?> handledType() {
+                return Range.class;
+            }
+
+            @Override
+            public Range deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                return Range.parse(parser.getValueAsString());
+            }
+        });
+
         // 设置时间周期反序列化处理器
         module.addDeserializer(Period.class, new JsonDeserializer<Period>() {
             @Override
@@ -388,6 +480,52 @@ public final class JacksonUtils {
         return module;
     }
 
+    /**
+     * 构建序列化处理器配置模块
+     *
+     * @return 模块实例
+     */
+    private static SimpleModule generateConfigureModule() {
+        SimpleModule module = new SimpleModule();
+        module.addDeserializer(long.class, new JsonDeserializer<Long>() {
+            @Override
+            public Long deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                return getLongValue(parser);
+            }
+        });
+        module.addDeserializer(Long.class, new JsonDeserializer<Long>() {
+            @Override
+            public Long deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                return getLongValue(parser);
+            }
+        });
+        module.addDeserializer(short.class, new JsonDeserializer<Short>() {
+            @Override
+            public Short deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                return getShortValue(parser);
+            }
+        });
+        module.addDeserializer(Short.class, new JsonDeserializer<Short>() {
+            @Override
+            public Short deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                return getShortValue(parser);
+            }
+        });
+        module.addDeserializer(int.class, new JsonDeserializer<Integer>() {
+            @Override
+            public Integer deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                return getIntegerValue(parser);
+            }
+        });
+        module.addDeserializer(Integer.class, new JsonDeserializer<Integer>() {
+            @Override
+            public Integer deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+                return getIntegerValue(parser);
+            }
+        });
+        return module;
+    }
+
     /**
      * 构建null对象序列化处理器更新对象
      *
@@ -492,6 +630,22 @@ public final class JacksonUtils {
         return SAFETY_MODULE;
     }
 
+    /**
+     * 初始化序列化处理器数字模块
+     *
+     * @return 模块实例
+     */
+    private static SimpleModule initializeNumberModule() {
+        if (NUMBER_MODULE == null) {
+            synchronized (SimpleModule.class) {
+                if (NUMBER_MODULE == null) {
+                    NUMBER_MODULE = generateConfigureModule();
+                }
+            }
+        }
+        return NUMBER_MODULE;
+    }
+
     /**
      * 初始化null对象序列化处理器更新对象
      *
@@ -508,6 +662,23 @@ public final class JacksonUtils {
         return NULL_MODIFIER;
     }
 
+    /**
+     * 对象序列化处理器基础配置
+     *
+     * @param mapper 对象序列化处理器实例
+     * @return 对象序列化处理器实例
+     */
+    public static ObjectMapper configure(@NonNull ObjectMapper mapper) {
+        mapper = mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
+                .configure(SerializationFeature.INDENT_OUTPUT, false)
+                .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
+                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+                .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
+        mapper.registerModule(initializeNumberModule());
+        return mapper;
+    }
+
     /**
      * 初始化基础的对象序列化处理器
      *
@@ -545,11 +716,8 @@ public final class JacksonUtils {
      * @return 对象序列化处理器实例
      */
     public static ObjectMapper initializeSerializer(@NonNull ObjectMapper mapper, boolean safely) {
-        // 设置基本属性
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
-                .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
-                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
-                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        // 基本配置
+        mapper = configure(mapper);
 
         // 注册基础模型
         mapper.registerModule(initializeBaseModule());

+ 34 - 6
framework-common/src/main/java/com/chelvc/framework/common/util/NumberUtils.java

@@ -39,11 +39,41 @@ public final class NumberUtils {
      */
     public static boolean isZero(Number number) {
         if (number instanceof BigInteger) {
-            return BigInteger.ZERO.equals(number);
+            return number.equals(BigInteger.ZERO);
         } else if (number instanceof BigDecimal) {
-            return BigDecimal.ZERO.equals(number);
+            return number.equals(BigDecimal.ZERO);
         }
-        return number != null && number.intValue() == 0;
+        return number != null && number.doubleValue() == 0;
+    }
+
+    /**
+     * 判断数字是否小于0
+     *
+     * @param number 数字
+     * @return true/false
+     */
+    public static boolean isLessZero(Number number) {
+        if (number instanceof BigInteger) {
+            return ((BigInteger) number).compareTo(BigInteger.ZERO) < 0;
+        } else if (number instanceof BigDecimal) {
+            return ((BigDecimal) number).compareTo(BigDecimal.ZERO) < 0;
+        }
+        return number != null && number.doubleValue() < 0;
+    }
+
+    /**
+     * 判断数字是否大于0
+     *
+     * @param number 数字
+     * @return true/false
+     */
+    public static boolean isGreaterZero(Number number) {
+        if (number instanceof BigInteger) {
+            return ((BigInteger) number).compareTo(BigInteger.ZERO) > 0;
+        } else if (number instanceof BigDecimal) {
+            return ((BigDecimal) number).compareTo(BigDecimal.ZERO) > 0;
+        }
+        return number != null && number.doubleValue() > 0;
     }
 
     /**
@@ -59,10 +89,8 @@ public final class NumberUtils {
             return (BigDecimal) number;
         } else if (number instanceof BigInteger) {
             return new BigDecimal((BigInteger) number);
-        } else if (number instanceof Float || number instanceof Double) {
-            return BigDecimal.valueOf(number.doubleValue());
         }
-        return BigDecimal.valueOf(number.longValue());
+        return new BigDecimal(number.toString());
     }
 
     /**

+ 55 - 0
framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java

@@ -423,6 +423,21 @@ public final class ObjectUtils {
         return object == null ? supplier.get() : ifNull(function.apply(object), supplier);
     }
 
+    /**
+     * 获取对象实例
+     *
+     * @param clazz 类对象
+     * @param <T>   数据类型
+     * @return 对象实例
+     */
+    public static <T> T instance(@NonNull Class<T> clazz) {
+        try {
+            return clazz.newInstance();
+        } catch (InstantiationException | IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     /**
      * 获取对象字段
      *
@@ -510,6 +525,18 @@ public final class ObjectUtils {
         return object == null ? null : getValue(object, getField(object.getClass(), property));
     }
 
+    /**
+     * 批量设置对象字段值
+     *
+     * @param object 对象实例
+     * @param values 属性名/值映射表
+     */
+    public static void setValue(Object object, @NonNull Map<String, ?> values) {
+        if (object != null && notEmpty(values)) {
+            values.forEach((key, value) -> setValue(object, key, value));
+        }
+    }
+
     /**
      * 设置对象字段值
      *
@@ -578,6 +605,34 @@ public final class ObjectUtils {
         return map;
     }
 
+    /**
+     * 将对象转换成属性名/值映射表
+     *
+     * @param type   目标类型
+     * @param object 对象实例
+     * @return 对象属性名/值映射表
+     */
+    public static Map<String, Object> object2map(@NonNull Class<?> type, Object object) {
+        if (object == null) {
+            return null;
+        }
+        Map<String, Field> targets = getAllFields(type);
+        if (isEmpty(targets)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Field> fields = getAllFields(object.getClass());
+        if (isEmpty(fields)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Object> map = Maps.newHashMapWithExpectedSize(fields.size());
+        fields.forEach((name, field) -> {
+            if (targets.containsKey(name)) {
+                map.put(name, getValue(object, field));
+            }
+        });
+        return map.isEmpty() ? Collections.emptyMap() : map;
+    }
+
     /**
      * 将属性名/值映射表转换成对象实例
      *

+ 47 - 22
framework-common/src/main/java/com/chelvc/framework/common/util/StringUtils.java

@@ -14,6 +14,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import java.util.zip.CRC32;
 
 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
 import com.google.common.collect.Maps;
@@ -705,34 +706,43 @@ public final class StringUtils {
     public static <T, U, R> R decompose(String text, @NonNull BiFunction<T, U, R> builder,
                                         @NonNull Function<String, T> parser1,
                                         @NonNull BiFunction<String, T, U> parser2) {
+        if (isEmpty(text) || (text = text.trim()).isEmpty()) {
+            return null;
+        }
+
         T first = null;
         U second = null;
-        if (notEmpty(text) && !(text = text.trim()).isEmpty()) {
-            if (text.charAt(0) == '(' && text.charAt(text.length() - 1) == ')') {
-                text = text.substring(1, text.length() - 1);
-            }
-            if ((text = text.trim()).isEmpty()) {
-                return null;
+        if (text.charAt(0) == '(' && text.charAt(text.length() - 1) == ')') {
+            text = text.substring(1, text.length() - 1);
+        }
+        if ((text = text.trim()).isEmpty()) {
+            return null;
+        }
+        String item;
+        int delimiter = -1;
+        for (int i = 0, max = text.length(); i < max; i++) {
+            char c = text.charAt(i);
+            if (c == '\t' || c == ';' || c == ',') {
+                delimiter = i;
+                break;
             }
-            String item;
-            int delimiter = text.indexOf(',');
-            if (delimiter < 0) {
-                first = parser1.apply(text);
-            } else if (delimiter == 0 && !(item = text.substring(1).trim()).isEmpty()) {
-                second = parser2.apply(item, null);
-            } else if (delimiter == text.length() - 1
-                    && !(item = text.substring(0, text.length() - 1).trim()).isEmpty()) {
+        }
+        if (delimiter < 0) {
+            first = parser1.apply(text);
+        } else if (delimiter == 0 && !(item = text.substring(1).trim()).isEmpty()) {
+            second = parser2.apply(item, null);
+        } else if (delimiter == text.length() - 1
+                && !(item = text.substring(0, text.length() - 1).trim()).isEmpty()) {
+            first = parser1.apply(item);
+        } else if (delimiter > 0 && delimiter < text.length() - 1) {
+            if (!(item = text.substring(0, delimiter).trim()).isEmpty()) {
                 first = parser1.apply(item);
-            } else if (delimiter > 0 && delimiter < text.length() - 1) {
-                if (!(item = text.substring(0, delimiter).trim()).isEmpty()) {
-                    first = parser1.apply(item);
-                }
-                if (!(item = text.substring(delimiter + 1).trim()).isEmpty()) {
-                    second = parser2.apply(item, first);
-                }
+            }
+            if (!(item = text.substring(delimiter + 1).trim()).isEmpty()) {
+                second = parser2.apply(item, first);
             }
         }
-        return first == null && second == null ? null : builder.apply(first, second);
+        return builder.apply(first, second);
     }
 
     /**
@@ -1434,4 +1444,19 @@ public final class StringUtils {
         }
         return new String(sequence);
     }
+
+    /**
+     * 计算字符串CRC值
+     *
+     * @param source 源字符串
+     * @return CRC值
+     */
+    public static long crc32(String source) {
+        if (isEmpty(source)) {
+            return 0;
+        }
+        CRC32 crc = new CRC32();
+        crc.update(source.getBytes());
+        return Math.abs(crc.getValue());
+    }
 }

+ 3 - 0
framework-database/src/main/java/com/chelvc/framework/database/config/DatabaseConfigurer.java

@@ -15,6 +15,7 @@ import com.chelvc.framework.common.function.Provider;
 import com.chelvc.framework.common.model.File;
 import com.chelvc.framework.common.model.Modification;
 import com.chelvc.framework.common.model.Period;
+import com.chelvc.framework.common.model.Range;
 import com.chelvc.framework.common.model.Region;
 import com.chelvc.framework.database.context.CacheableDatabaseCryptoContext;
 import com.chelvc.framework.database.context.DatabaseCryptoContext;
@@ -23,6 +24,7 @@ import com.chelvc.framework.database.context.Transactor;
 import com.chelvc.framework.database.handler.FileTypeHandler;
 import com.chelvc.framework.database.handler.ModificationTypeHandler;
 import com.chelvc.framework.database.handler.PeriodTypeHandler;
+import com.chelvc.framework.database.handler.RangeTypeHandler;
 import com.chelvc.framework.database.handler.RegionTypeHandler;
 import com.chelvc.framework.database.interceptor.DeletedIsolateInterceptor;
 import com.chelvc.framework.database.interceptor.EnvIsolateInterceptor;
@@ -130,6 +132,7 @@ public class DatabaseConfigurer {
             // 注册自定义字段类型处理器
             TypeHandlerRegistry handlerRegistry = configuration.getTypeHandlerRegistry();
             handlerRegistry.register(File.class, JdbcType.VARCHAR, new FileTypeHandler());
+            handlerRegistry.register(Range.class, JdbcType.VARCHAR, new RangeTypeHandler());
             handlerRegistry.register(Period.class, JdbcType.VARCHAR, new PeriodTypeHandler());
             handlerRegistry.register(Region.class, JdbcType.VARCHAR, new RegionTypeHandler());
             handlerRegistry.register(Modification.class, JdbcType.VARCHAR, new ModificationTypeHandler());

+ 10 - 0
framework-database/src/main/java/com/chelvc/framework/database/config/TypeHandlerConfigurer.java

@@ -25,6 +25,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
 import com.chelvc.framework.common.model.File;
 import com.chelvc.framework.common.model.Modification;
 import com.chelvc.framework.common.model.Period;
+import com.chelvc.framework.common.model.Range;
 import com.chelvc.framework.common.model.Region;
 import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
@@ -59,6 +60,9 @@ import com.chelvc.framework.database.handler.ModificationsTypeHandler;
 import com.chelvc.framework.database.handler.PeriodArrayTypeHandler;
 import com.chelvc.framework.database.handler.PeriodSetTypeHandler;
 import com.chelvc.framework.database.handler.PeriodsTypeHandler;
+import com.chelvc.framework.database.handler.RangeArrayTypeHandler;
+import com.chelvc.framework.database.handler.RangeSetTypeHandler;
+import com.chelvc.framework.database.handler.RangesTypeHandler;
 import com.chelvc.framework.database.handler.RegionArrayTypeHandler;
 import com.chelvc.framework.database.handler.RegionSetTypeHandler;
 import com.chelvc.framework.database.handler.RegionsTypeHandler;
@@ -255,6 +259,8 @@ public class TypeHandlerConfigurer extends MybatisConfigurer {
                     return StringSetTypeHandler.class;
                 } else if (arg == File.class) {
                     return FileSetTypeHandler.class;
+                } else if (arg == Range.class) {
+                    return RangeSetTypeHandler.class;
                 } else if (arg == Period.class) {
                     return PeriodSetTypeHandler.class;
                 } else if (arg == Region.class) {
@@ -279,6 +285,8 @@ public class TypeHandlerConfigurer extends MybatisConfigurer {
                     return StringsTypeHandler.class;
                 } else if (arg == File.class) {
                     return FilesTypeHandler.class;
+                } else if (arg == Range.class) {
+                    return RangesTypeHandler.class;
                 } else if (arg == Period.class) {
                     return PeriodsTypeHandler.class;
                 } else if (arg == Region.class) {
@@ -311,6 +319,8 @@ public class TypeHandlerConfigurer extends MybatisConfigurer {
                 return StringArrayTypeHandler.class;
             } else if (component == File.class) {
                 return FileArrayTypeHandler.class;
+            } else if (component == Range.class) {
+                return RangeArrayTypeHandler.class;
             } else if (component == Period.class) {
                 return PeriodArrayTypeHandler.class;
             } else if (component == Region.class) {

+ 14 - 0
framework-database/src/main/java/com/chelvc/framework/database/context/DatabaseContextHolder.java

@@ -7,6 +7,7 @@ import java.lang.reflect.Modifier;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.sql.Connection;
+import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.time.Instant;
@@ -222,6 +223,19 @@ public final class DatabaseContextHolder {
         }
     }
 
+    /**
+     * 执行原生查询SQL
+     *
+     * @param sql SQL语句
+     * @return 查询结果集
+     * @throws SQLException SQL执行异常
+     */
+    public static ResultSet query(@NonNull String sql) throws SQLException {
+        try (Statement statement = getConnection().createStatement()) {
+            return statement.executeQuery(sql);
+        }
+    }
+
     /**
      * 事务执行方法
      *

+ 12 - 0
framework-database/src/main/java/com/chelvc/framework/database/handler/RangeArrayTypeHandler.java

@@ -0,0 +1,12 @@
+package com.chelvc.framework.database.handler;
+
+import com.chelvc.framework.common.model.Range;
+
+/**
+ * 数字范围数组字段处理实现
+ *
+ * @author Woody
+ * @date 2024/10/30
+ */
+public class RangeArrayTypeHandler extends JsonTypeHandler.Default<Range[]> {
+}

+ 14 - 0
framework-database/src/main/java/com/chelvc/framework/database/handler/RangeSetTypeHandler.java

@@ -0,0 +1,14 @@
+package com.chelvc.framework.database.handler;
+
+import java.util.Set;
+
+import com.chelvc.framework.common.model.Range;
+
+/**
+ * 数字范围集合字段处理实现
+ *
+ * @author Woody
+ * @date 2024/10/30
+ */
+public class RangeSetTypeHandler extends JsonTypeHandler.Default<Set<Range>> {
+}

+ 39 - 0
framework-database/src/main/java/com/chelvc/framework/database/handler/RangeTypeHandler.java

@@ -0,0 +1,39 @@
+package com.chelvc.framework.database.handler;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import com.chelvc.framework.common.model.Range;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+
+/**
+ * 数字范围字段处理实现
+ *
+ * @author Woody
+ * @date 2024/10/30
+ */
+public class RangeTypeHandler extends BaseTypeHandler<Range> {
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, Range parameter, JdbcType jdbcType)
+            throws SQLException {
+        ps.setString(i, parameter.toString());
+    }
+
+    @Override
+    public Range getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        return Range.parse(rs.getString(columnName));
+    }
+
+    @Override
+    public Range getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        return Range.parse(rs.getString(columnIndex));
+    }
+
+    @Override
+    public Range getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+        return Range.parse(cs.getString(columnIndex));
+    }
+}

+ 14 - 0
framework-database/src/main/java/com/chelvc/framework/database/handler/RangesTypeHandler.java

@@ -0,0 +1,14 @@
+package com.chelvc.framework.database.handler;
+
+import java.util.List;
+
+import com.chelvc.framework.common.model.Range;
+
+/**
+ * 数字范围列表字段处理实现
+ *
+ * @author Woody
+ * @date 2024/10/30
+ */
+public class RangesTypeHandler extends JsonTypeHandler.Default<List<Range>> {
+}

+ 80 - 46
framework-database/src/main/java/com/chelvc/framework/database/support/Binlog.java

@@ -2,16 +2,15 @@ package com.chelvc.framework.database.support;
 
 import java.io.Serializable;
 import java.lang.reflect.Field;
+import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
+import java.util.Date;
 import java.util.Map;
 import java.util.Objects;
 
-import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
-import com.baomidou.mybatisplus.core.metadata.TableInfo;
 import com.chelvc.framework.common.util.JacksonUtils;
 import com.chelvc.framework.common.util.ObjectUtils;
-import com.chelvc.framework.database.context.DatabaseContextHolder;
+import com.chelvc.framework.common.util.StringUtils;
 import com.google.common.collect.Maps;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -53,10 +52,25 @@ public class Binlog implements Serializable {
      * @return 转换后字段值
      */
     public static Object convert(@NonNull Field field, Object object) {
+        if (object == null) {
+            return null;
+        }
+
         Class<?> type = field.getType();
-        if (object instanceof String && (Map.class.isAssignableFrom(type) || List.class.isAssignableFrom(type))) {
+        if (StringUtils.isEmpty(object) && (type.isEnum() || type.isArray()
+                || Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type))) {
+            return null;
+        }
+        if (ObjectUtils.isMetaClass(type) || type.isEnum() || type == String.class
+                || Date.class.isAssignableFrom(type)) {
+            return JacksonUtils.convert(object, type);
+        } else if (object instanceof String && StringUtils.notEmpty(object)) {
             // binlog消息不支持JSON格式,所以需要单独反序列化处理
-            return JacksonUtils.deserialize((String) object, field.getGenericType());
+            String string = (String) object;
+            char first = string.charAt(0), last = string.charAt(string.length() - 1);
+            if ((first == '{' && last == '}') || (first == '[' && last == ']')) {
+                return JacksonUtils.deserialize(string, field.getGenericType());
+            }
         }
         return JacksonUtils.convert(object, type);
     }
@@ -70,53 +84,44 @@ public class Binlog implements Serializable {
      * @return 实体对象实例
      */
     public static <T> T convert(@NonNull Class<T> model, Map<?, ?> mapping) {
-        T entity;
-        try {
-            entity = model.newInstance();
-        } catch (InstantiationException | IllegalAccessException e) {
-            throw new RuntimeException(e);
-        }
-        TableInfo table = DatabaseContextHolder.getTable(model);
-        update(table, entity, mapping);
+        T entity = ObjectUtils.instance(model);
+        update(entity, mapping);
         return entity;
     }
 
     /**
-     * 更新实体属性
+     * 转换字段值
      *
-     * @param entity  实体对象实例
-     * @param mapping 字段名/值映射表
+     * @param model    数据模型
+     * @param property 模型属性
+     * @param object   字段原始值
+     * @return 转换后字段值
      */
-    public static void update(@NonNull Object entity, Map<?, ?> mapping) {
-        Class<?> model = entity.getClass();
-        TableInfo table = DatabaseContextHolder.getTable(model);
-        update(table, entity, mapping);
+    public static Object convert(@NonNull Class<?> model, @NonNull String property, Object object) {
+        return object == null ? null : convert(ObjectUtils.getField(model, property), object);
     }
 
     /**
      * 更新实体属性值
      *
-     * @param table   表信息
      * @param entity  实体对象实例
      * @param mapping 字段名/值映射表
      */
-    private static void update(TableInfo table, Object entity, Map<?, ?> mapping) {
+    public static void update(@NonNull Object entity, Map<?, ?> mapping) {
         if (ObjectUtils.isEmpty(mapping)) {
             return;
         }
 
-        // 更新主键字段值
-        if (table.havePK()) {
-            Object value = mapping.get(table.getKeyColumn());
-            value = JacksonUtils.convert(value, table.getKeyType());
-            ObjectUtils.setValue(entity, table.getKeyProperty(), value);
+        Class<?> model = entity.getClass();
+        Map<String, Field> fields = ObjectUtils.getAllFields(model);
+        if (ObjectUtils.isEmpty(fields)) {
+            return;
         }
-
-        // 更新普通字段值
-        for (TableFieldInfo field : table.getFieldList()) {
-            Object value = mapping.get(field.getColumn());
-            value = convert(field.getField(), value);
-            ObjectUtils.setValue(entity, field.getField(), value);
+        for (Map.Entry<String, Field> entry : fields.entrySet()) {
+            String column = StringUtils.hump2underscore(entry.getKey());
+            Object value = mapping.get(column);
+            value = convert(entry.getValue(), value);
+            ObjectUtils.setValue(entity, entry.getValue(), value);
         }
     }
 
@@ -158,13 +163,12 @@ public class Binlog implements Serializable {
             return false;
         }
 
-        TableInfo table = DatabaseContextHolder.getTable(model);
-        List<TableFieldInfo> fields = table.getFieldList();
+        Map<String, Field> fields = ObjectUtils.getAllFields(model);
         if (ObjectUtils.isEmpty(fields)) {
             return false;
         }
-        for (TableFieldInfo field : fields) {
-            String column = field.getColumn();
+        for (Map.Entry<String, Field> entry : fields.entrySet()) {
+            String column = StringUtils.hump2underscore(entry.getKey());
             Object a = ObjectUtils.ifNull(this.after, map -> map.get(column));
             Object b = ObjectUtils.ifNull(this.before, map -> map.get(column));
             if (!ObjectUtils.equals(a, b)) {
@@ -211,7 +215,7 @@ public class Binlog implements Serializable {
      * @return 实体对象实例
      */
     public <T> T after(@NonNull Class<T> model) {
-        return convert(model, this.after);
+        return ObjectUtils.isEmpty(this.after) ? null : convert(model, this.after);
     }
 
     /**
@@ -232,7 +236,38 @@ public class Binlog implements Serializable {
      * @return 实体对象实例
      */
     public <T> T before(@NonNull Class<T> model) {
-        return convert(model, this.before);
+        return ObjectUtils.isEmpty(this.before) ? null : convert(model, this.before);
+    }
+
+    /**
+     * 获取更新信息
+     *
+     * @param model 实体模型
+     * @param <T>   数据类型
+     * @return 实体模型实例
+     */
+    public <T> T modification(@NonNull Class<T> model) {
+        if (ObjectUtils.isEmpty(this.after)) {
+            return null;
+        }
+
+        Map<String, Field> fields = ObjectUtils.getAllFields(model);
+        if (ObjectUtils.isEmpty(fields)) {
+            return null;
+        }
+        T instance = null;
+        for (Map.Entry<String, Field> entry : fields.entrySet()) {
+            String column = StringUtils.hump2underscore(entry.getKey());
+            Object a = ObjectUtils.ifNull(this.after, map -> map.get(column));
+            Object b = ObjectUtils.ifNull(this.before, map -> map.get(column));
+            if (!ObjectUtils.equals(a, b)) {
+                if (instance == null) {
+                    instance = ObjectUtils.instance(model);
+                }
+                ObjectUtils.setValue(instance, entry.getValue(), convert(entry.getValue(), a));
+            }
+        }
+        return instance;
     }
 
     /**
@@ -246,18 +281,17 @@ public class Binlog implements Serializable {
             return Collections.emptyMap();
         }
 
-        Map<String, Object> different = Maps.newHashMap();
-        TableInfo table = DatabaseContextHolder.getTable(model);
-        List<TableFieldInfo> fields = table.getFieldList();
+        Map<String, Field> fields = ObjectUtils.getAllFields(model);
         if (ObjectUtils.isEmpty(fields)) {
             return Collections.emptyMap();
         }
-        for (TableFieldInfo field : fields) {
-            String column = field.getColumn();
+        Map<String, Object> different = Maps.newHashMap();
+        for (Map.Entry<String, Field> entry : fields.entrySet()) {
+            String column = StringUtils.hump2underscore(entry.getKey());
             Object a = ObjectUtils.ifNull(this.after, map -> map.get(column));
             Object b = ObjectUtils.ifNull(this.before, map -> map.get(column));
             if (!ObjectUtils.equals(a, b)) {
-                different.put(field.getProperty(), convert(field.getField(), a));
+                different.put(entry.getKey(), convert(entry.getValue(), a));
             }
         }
         return different;

+ 13 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/EnhanceService.java

@@ -4,6 +4,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.function.Function;
 
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.conditions.query.ChainQuery;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.chelvc.framework.common.model.Pagination;
@@ -90,6 +91,18 @@ public interface EnhanceService<T> extends IService<T> {
         return !entities.isEmpty() && this.getEnhanceMapper().batchCreateUpdate(entities, update) > 0;
     }
 
+    /**
+     * 判断指定字段值记录是否存在
+     *
+     * @param column 字段函数
+     * @param value  字段值
+     * @return true/false
+     */
+    default boolean exists(@NonNull SFunction<T, ?> column, @NonNull Object value) {
+        Integer count = this.lambdaQuery().eq(column, value).count();
+        return count != null && count > 0;
+    }
+
     /**
      * 分页查询
      *

+ 1 - 1
framework-database/src/main/java/com/chelvc/framework/database/support/Updates.java

@@ -609,7 +609,7 @@ public final class Updates {
          */
         public final Update<T> jsonMerge(@NonNull String... columns) {
             for (String column : columns) {
-                this.set(column, "JSON_MERGE_PATCH(VALUES(" + column + "), " + column + ")");
+                this.set(column, "JSON_MERGE_PATCH(" + column + ", VALUES(" + column + "))");
             }
             return this;
         }

+ 34 - 0
framework-elasticsearch/pom.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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>
+
+    <parent>
+        <groupId>com.chelvc.framework</groupId>
+        <artifactId>framework-dependencies</artifactId>
+        <version>1.0.0-RELEASE</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>framework-elasticsearch</artifactId>
+    <version>1.0.0-RELEASE</version>
+
+    <properties>
+        <framework-base.version>1.0.0-RELEASE</framework-base.version>
+        <elasticsearch-java.version>8.15.2</elasticsearch-java.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.chelvc.framework</groupId>
+            <artifactId>framework-base</artifactId>
+            <version>${framework-base.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>co.elastic.clients</groupId>
+            <artifactId>elasticsearch-java</artifactId>
+            <version>${elasticsearch-java.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 348 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/DefaultElasticsearchHandler.java

@@ -0,0 +1,348 @@
+package com.chelvc.framework.elasticsearch;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.elasticsearch._types.Conflicts;
+import co.elastic.clients.elasticsearch._types.Script;
+import co.elastic.clients.elasticsearch._types.ScriptLanguage;
+import co.elastic.clients.elasticsearch._types.query_dsl.Query;
+import co.elastic.clients.elasticsearch.core.BulkRequest;
+import co.elastic.clients.elasticsearch.core.BulkResponse;
+import co.elastic.clients.elasticsearch.core.CountRequest;
+import co.elastic.clients.elasticsearch.core.CountResponse;
+import co.elastic.clients.elasticsearch.core.CreateRequest;
+import co.elastic.clients.elasticsearch.core.CreateResponse;
+import co.elastic.clients.elasticsearch.core.DeleteRequest;
+import co.elastic.clients.elasticsearch.core.DeleteResponse;
+import co.elastic.clients.elasticsearch.core.ExistsRequest;
+import co.elastic.clients.elasticsearch.core.GetRequest;
+import co.elastic.clients.elasticsearch.core.GetResponse;
+import co.elastic.clients.elasticsearch.core.IndexRequest;
+import co.elastic.clients.elasticsearch.core.IndexResponse;
+import co.elastic.clients.elasticsearch.core.MgetRequest;
+import co.elastic.clients.elasticsearch.core.MgetResponse;
+import co.elastic.clients.elasticsearch.core.SearchRequest;
+import co.elastic.clients.elasticsearch.core.SearchResponse;
+import co.elastic.clients.elasticsearch.core.UpdateByQueryRequest;
+import co.elastic.clients.elasticsearch.core.UpdateByQueryResponse;
+import co.elastic.clients.elasticsearch.core.UpdateRequest;
+import co.elastic.clients.elasticsearch.core.UpdateResponse;
+import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
+import co.elastic.clients.elasticsearch.core.bulk.CreateOperation;
+import co.elastic.clients.elasticsearch.core.bulk.DeleteOperation;
+import co.elastic.clients.elasticsearch.core.bulk.IndexOperation;
+import co.elastic.clients.elasticsearch.core.bulk.UpdateOperation;
+import co.elastic.clients.json.JsonData;
+import co.elastic.clients.transport.ElasticsearchTransport;
+import co.elastic.clients.transport.endpoints.BooleanResponse;
+import co.elastic.clients.util.ObjectBuilder;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import lombok.NonNull;
+
+/**
+ * ES操作接口默认实现
+ *
+ * @author Woody
+ * @date 2024/10/17
+ */
+public class DefaultElasticsearchHandler implements ElasticsearchHandler {
+    private final ElasticsearchClient client;
+
+    public DefaultElasticsearchHandler(@NonNull ElasticsearchTransport transport) {
+        this.client = new ElasticsearchClient(transport);
+    }
+
+    @Override
+    public ElasticsearchClient getClient() {
+        return this.client;
+    }
+
+    @Override
+    public CreateResponse create(@NonNull Object document) {
+        ModelContext context = ModelContext.of(document.getClass());
+        CreateRequest<?> request = new CreateRequest.Builder<>().index(context.getIndex())
+                .id(context.getIdentity(document)).document(document).build();
+        try {
+            return this.client.create(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> BulkResponse create(@NonNull Collection<T> documents) {
+        List<BulkOperation> operations = Lists.newArrayListWithCapacity(documents.size());
+        documents.forEach(document -> {
+            ModelContext context = ModelContext.of(document.getClass());
+            CreateOperation<?> create = new CreateOperation.Builder<>().index(context.getIndex())
+                    .id(context.getIdentity(document)).document(document).build();
+            BulkOperation operation = new BulkOperation.Builder().create(create).build();
+            operations.add(operation);
+        });
+        BulkRequest request = new BulkRequest.Builder().operations(operations).build();
+        try {
+            return this.client.bulk(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public DeleteResponse delete(@NonNull Class<?> model, @NonNull String id) {
+        ModelContext context = ModelContext.of(model);
+        DeleteRequest request = new DeleteRequest.Builder().index(context.getIndex()).id(id).build();
+        try {
+            return this.client.delete(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public BulkResponse delete(@NonNull Class<?> model, @NonNull Collection<String> ids) {
+        ModelContext context = ModelContext.of(model);
+        List<BulkOperation> operations = Lists.newArrayListWithCapacity(ids.size());
+        ids.forEach(id -> {
+            DeleteOperation delete = new DeleteOperation.Builder().index(context.getIndex()).id(id).build();
+            BulkOperation operation = new BulkOperation.Builder().delete(delete).build();
+            operations.add(operation);
+        });
+        BulkRequest request = new BulkRequest.Builder().operations(operations).build();
+        try {
+            return this.client.bulk(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> UpdateResponse<T> update(@NonNull T document) {
+        return this.update(document, null, null);
+    }
+
+    @Override
+    public <T> BulkResponse update(@NonNull Collection<T> documents) {
+        List<BulkOperation> operations = Lists.newArrayListWithCapacity(documents.size());
+        documents.forEach(document -> {
+            ModelContext context = ModelContext.of(document.getClass());
+            UpdateOperation<T, Object> update = new UpdateOperation.Builder<T, Object>().index(context.getIndex())
+                    .id(context.getIdentity(document)).action(a -> a.doc(document)).retryOnConflict(3).build();
+            BulkOperation operation = new BulkOperation.Builder().update(update).build();
+            operations.add(operation);
+        });
+        BulkRequest request = new BulkRequest.Builder().operations(operations).build();
+        try {
+            return this.client.bulk(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> UpdateResponse<T> update(@NonNull T document, @NonNull Long primary, @NonNull Long sequence) {
+        Class<T> model = (Class<T>) document.getClass();
+        String id = ModelContext.of(model).getIdentity(document);
+        return this.update(model, id, document, primary, sequence);
+    }
+
+    @Override
+    public <T> UpdateResponse<T> update(@NonNull Class<T> model, @NonNull String id, @NonNull Object document) {
+        return this.update(model, id, document, null, null);
+    }
+
+    @Override
+    public <T> UpdateByQueryResponse update(@NonNull Class<T> model, @NonNull Query query, @NonNull T document) {
+        return this.update(model, query, ObjectUtils.object2map(document));
+    }
+
+    @Override
+    public UpdateByQueryResponse update(@NonNull Class<?> model, @NonNull Query query, @NonNull String field,
+                                        Object value) {
+        ModelContext context = ModelContext.of(model);
+        String source = "ctx._source." + field + (value == null ? "=null" : ("=params." + field));
+        Map<String, JsonData> params = value == null ? Collections.emptyMap() :
+                ImmutableMap.of(field, JsonData.of(value));
+        Script script = new Script.Builder().lang(ScriptLanguage.Painless).source(source).params(params).build();
+        UpdateByQueryRequest request = new UpdateByQueryRequest.Builder().index(context.getIndex()).script(script)
+                .query(query).conflicts(Conflicts.Proceed).build();
+        try {
+            return this.client.updateByQuery(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public UpdateByQueryResponse update(@NonNull Class<?> model, @NonNull Query query,
+                                        @NonNull Map<String, ?> parameters) {
+        ModelContext context = ModelContext.of(model);
+        StringBuilder source = new StringBuilder();
+        Map<String, JsonData> params = Maps.newHashMapWithExpectedSize(parameters.size());
+        parameters.forEach((key, value) -> {
+            if (source.length() > 0) {
+                source.append(";");
+            }
+            if (value == null) {
+                source.append("ctx._source.").append(key).append("=null");
+            } else {
+                source.append("ctx._source.").append(key).append("=params.").append(key);
+                params.put(key, JsonData.of(value));
+            }
+        });
+        Script script = new Script.Builder().lang(ScriptLanguage.Painless).source(source.toString())
+                .params(params).build();
+        UpdateByQueryRequest request = new UpdateByQueryRequest.Builder().index(context.getIndex()).script(script)
+                .query(query).conflicts(Conflicts.Proceed).build();
+        try {
+            return this.client.updateByQuery(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> UpdateResponse<T> update(@NonNull Class<T> model, @NonNull String id, @NonNull Object document,
+                                        Long primary, Long sequence) {
+        ModelContext context = ModelContext.of(model);
+        UpdateRequest<T, ?> request = new UpdateRequest.Builder<T, Object>().index(context.getIndex()).id(id)
+                .doc(document).ifPrimaryTerm(primary).ifSeqNo(sequence).retryOnConflict(3).build();
+        try {
+            return this.client.update(request, model);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public IndexResponse index(@NonNull Object document) {
+        return this.index(document, null, null);
+    }
+
+    @Override
+    public IndexResponse index(@NonNull Class<?> model, @NonNull String id, @NonNull Object document) {
+        return this.index(model, id, document, null, null);
+    }
+
+    @Override
+    public IndexResponse index(@NonNull Object document, Long primary, Long sequence) {
+        Class<?> model = document.getClass();
+        String id = ModelContext.of(model).getIdentity(document);
+        return this.index(model, id, document, primary, sequence);
+    }
+
+    @Override
+    public IndexResponse index(@NonNull Class<?> model, @NonNull String id, @NonNull Object document, Long primary,
+                               Long sequence) {
+        ModelContext context = ModelContext.of(model);
+        IndexRequest<?> request = new IndexRequest.Builder<>().index(context.getIndex())
+                .id(id).document(document).ifPrimaryTerm(primary).ifSeqNo(sequence).build();
+        try {
+            return this.client.index(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> BulkResponse index(@NonNull Collection<T> documents) {
+        return this.index(documents, null, null);
+    }
+
+    @Override
+    public <T> BulkResponse index(@NonNull Collection<T> documents, Long primary, Long sequence) {
+        List<BulkOperation> operations = Lists.newArrayListWithCapacity(documents.size());
+        documents.forEach(document -> {
+            ModelContext context = ModelContext.of(document.getClass());
+            IndexOperation<?> index = new IndexOperation.Builder<>().index(context.getIndex())
+                    .id(context.getIdentity(document)).document(document).ifPrimaryTerm(primary)
+                    .ifSeqNo(sequence).build();
+            BulkOperation operation = new BulkOperation.Builder().index(index).build();
+            operations.add(operation);
+        });
+        BulkRequest request = new BulkRequest.Builder().operations(operations).build();
+        try {
+            return this.client.bulk(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public CountResponse count(@NonNull Class<?> model, @NonNull Query query) {
+        ModelContext context = ModelContext.of(model);
+        CountRequest request = new CountRequest.Builder().index(context.getIndex()).query(query).build();
+        try {
+            return this.client.count(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public BooleanResponse exists(@NonNull Class<?> model, @NonNull String id) {
+        ModelContext context = ModelContext.of(model);
+        ExistsRequest request = new ExistsRequest.Builder().index(context.getIndex()).id(id).build();
+        try {
+            return this.client.exists(request);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> GetResponse<T> get(@NonNull Class<T> model, @NonNull String id) {
+        ModelContext context = ModelContext.of(model);
+        GetRequest request = new GetRequest.Builder().index(context.getIndex()).id(id).build();
+        try {
+            return this.client.get(request, model);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> MgetResponse<T> get(@NonNull Class<T> model, @NonNull List<String> ids) {
+        ModelContext context = ModelContext.of(model);
+        MgetRequest request = new MgetRequest.Builder().index(context.getIndex()).ids(ids).build();
+        try {
+            return this.client.mget(request, model);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public <T> SearchResponse<T> search(@NonNull Class<T> model, @NonNull Query query) {
+        ModelContext context = ModelContext.of(model);
+        SearchRequest request = new SearchRequest.Builder().index(context.getIndex()).query(query).build();
+        return this.search(model, request);
+    }
+
+    @Override
+    public <T> SearchResponse<T> search(@NonNull Class<T> model, @NonNull Query query,
+                                        @NonNull Function<SearchRequest.Builder, ObjectBuilder<SearchRequest>> function) {
+        ModelContext context = ModelContext.of(model);
+        SearchRequest.Builder builder = new SearchRequest.Builder().index(context.getIndex()).query(query);
+        SearchRequest request = function.apply(builder).build();
+        return this.search(model, request);
+    }
+
+    @Override
+    public <T> SearchResponse<T> search(@NonNull Class<T> model, @NonNull SearchRequest request) {
+        try {
+            return this.client.search(request, model);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 287 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/ElasticsearchHandler.java

@@ -0,0 +1,287 @@
+package com.chelvc.framework.elasticsearch;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.elasticsearch._types.query_dsl.Query;
+import co.elastic.clients.elasticsearch.core.BulkResponse;
+import co.elastic.clients.elasticsearch.core.CountResponse;
+import co.elastic.clients.elasticsearch.core.CreateResponse;
+import co.elastic.clients.elasticsearch.core.DeleteResponse;
+import co.elastic.clients.elasticsearch.core.GetResponse;
+import co.elastic.clients.elasticsearch.core.IndexResponse;
+import co.elastic.clients.elasticsearch.core.MgetResponse;
+import co.elastic.clients.elasticsearch.core.SearchRequest;
+import co.elastic.clients.elasticsearch.core.SearchResponse;
+import co.elastic.clients.elasticsearch.core.UpdateByQueryResponse;
+import co.elastic.clients.elasticsearch.core.UpdateResponse;
+import co.elastic.clients.transport.endpoints.BooleanResponse;
+import co.elastic.clients.util.ObjectBuilder;
+
+/**
+ * ES操作接口
+ *
+ * @author Woody
+ * @date 2024/10/16
+ */
+public interface ElasticsearchHandler {
+    /**
+     * 获取ES客户端
+     *
+     * @return ES客户端实例
+     */
+    ElasticsearchClient getClient();
+
+    /**
+     * 创建文档
+     *
+     * @param document 文档实例
+     * @return 响应结果
+     */
+    CreateResponse create(Object document);
+
+    /**
+     * 创建文档
+     *
+     * @param documents 文档实例集合
+     * @param <T>       数据类型
+     * @return 响应结果
+     */
+    <T> BulkResponse create(Collection<T> documents);
+
+    /**
+     * 删除文档
+     *
+     * @param model 数据模型
+     * @param id    文档ID
+     * @return 响应结果
+     */
+    DeleteResponse delete(Class<?> model, String id);
+
+    /**
+     * 删除文档
+     *
+     * @param model 数据模型
+     * @param ids   文档ID集合
+     * @return 响应结果
+     */
+    BulkResponse delete(Class<?> model, Collection<String> ids);
+
+    /**
+     * 更新文档
+     *
+     * @param document 文档实例
+     * @param <T>      数据类型
+     * @return 响应结果
+     */
+    <T> UpdateResponse<T> update(T document);
+
+    /**
+     * 更新文档
+     *
+     * @param documents 文档实例集合
+     * @param <T>       数据类型
+     * @return 响应结果
+     */
+    <T> BulkResponse update(Collection<T> documents);
+
+    /**
+     * 更新文档
+     *
+     * @param document 文档实例
+     * @param primary  主版本
+     * @param sequence 版本号
+     * @param <T>      数据类型
+     * @return 响应结果
+     */
+    <T> UpdateResponse<T> update(T document, Long primary, Long sequence);
+
+    /**
+     * 更新文档
+     *
+     * @param model    数据模型
+     * @param id       文档ID
+     * @param document 文档信息
+     * @param <T>      数据类型
+     * @return 响应结果
+     */
+    <T> UpdateResponse<T> update(Class<T> model, String id, Object document);
+
+    /**
+     * 更新文档
+     *
+     * @param model    数据模型
+     * @param query    过滤条件
+     * @param document 文档信息
+     * @param <T>      数据类型
+     * @return 响应结果
+     */
+    <T> UpdateByQueryResponse update(Class<T> model, Query query, T document);
+
+    /**
+     * 更新文档
+     *
+     * @param model 数据模型
+     * @param query 过滤条件
+     * @param field 字段名
+     * @param value 字段值
+     * @return 响应结果
+     */
+    UpdateByQueryResponse update(Class<?> model, Query query, String field, Object value);
+
+    /**
+     * 更新文档
+     *
+     * @param model      数据模型
+     * @param query      过滤条件
+     * @param parameters 更新参数
+     * @return 响应结果
+     */
+    UpdateByQueryResponse update(Class<?> model, Query query, Map<String, ?> parameters);
+
+    /**
+     * 更新文档
+     *
+     * @param model    数据模型
+     * @param id       文档ID
+     * @param document 文档信息
+     * @param primary  主版本
+     * @param sequence 版本号
+     * @param <T>      数据类型
+     * @return 响应结果
+     */
+    <T> UpdateResponse<T> update(Class<T> model, String id, Object document, Long primary, Long sequence);
+
+    /**
+     * 创建或更新文档
+     *
+     * @param document 文档信息
+     * @return 响应结果
+     */
+    IndexResponse index(Object document);
+
+    /**
+     * 创建或更新文档
+     *
+     * @param model    数据模型
+     * @param id       文档ID
+     * @param document 文档信息
+     * @return 响应结果
+     */
+    IndexResponse index(Class<?> model, String id, Object document);
+
+    /**
+     * 创建或更新文档
+     *
+     * @param document 文档信息
+     * @param primary  主版本
+     * @param sequence 版本号
+     * @return 响应结果
+     */
+    IndexResponse index(Object document, Long primary, Long sequence);
+
+    /**
+     * 创建或更新文档
+     *
+     * @param model    数据模型
+     * @param id       文档ID
+     * @param document 文档信息
+     * @param primary  主版本
+     * @param sequence 版本号
+     * @return 响应结果
+     */
+    IndexResponse index(Class<?> model, String id, Object document, Long primary, Long sequence);
+
+    /**
+     * 创建或更新文档
+     *
+     * @param documents 文档信息集合
+     * @param <T>       数据类型
+     * @return 响应结果
+     */
+    <T> BulkResponse index(Collection<T> documents);
+
+    /**
+     * 创建或更新文档
+     *
+     * @param documents 文档信息集合
+     * @param primary   主版本
+     * @param sequence  版本号
+     * @param <T>       数据类型
+     * @return 响应结果
+     */
+    <T> BulkResponse index(Collection<T> documents, Long primary, Long sequence);
+
+    /**
+     * 获取文档数量
+     *
+     * @param model 数据模型
+     * @param query 过滤条件
+     * @return 响应结果
+     */
+    CountResponse count(Class<?> model, Query query);
+
+    /**
+     * 判断文档是否存在
+     *
+     * @param model 数据模型
+     * @param id    文档ID
+     * @return 响应结果
+     */
+    BooleanResponse exists(Class<?> model, String id);
+
+    /**
+     * 获取文档信息
+     *
+     * @param model 数据模型
+     * @param id    文档ID
+     * @param <T>   数据类型
+     * @return 响应结果
+     */
+    <T> GetResponse<T> get(Class<T> model, String id);
+
+    /**
+     * 获取文档信息
+     *
+     * @param model 数据模型
+     * @param ids   文档ID列表
+     * @param <T>   数据类型
+     * @return 响应结果
+     */
+    <T> MgetResponse<T> get(Class<T> model, List<String> ids);
+
+    /**
+     * 检索文档
+     *
+     * @param model 数据模型
+     * @param query 过滤条件
+     * @param <T>   数据类型
+     * @return 响应结果
+     */
+    <T> SearchResponse<T> search(Class<T> model, Query query);
+
+    /**
+     * 检索文档
+     *
+     * @param model    数据模型
+     * @param query    过滤条件
+     * @param function 查询请求回调函数
+     * @param <T>      数据类型
+     * @return 响应结果
+     */
+    <T> SearchResponse<T> search(Class<T> model, Query query,
+                                 Function<SearchRequest.Builder, ObjectBuilder<SearchRequest>> function);
+
+    /**
+     * 检索文档
+     *
+     * @param model   数据模型
+     * @param request 查询请求
+     * @param <T>     数据类型
+     * @return 响应结果
+     */
+    <T> SearchResponse<T> search(Class<T> model, SearchRequest request);
+}

+ 85 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/ElasticsearchUtils.java

@@ -0,0 +1,85 @@
+package com.chelvc.framework.elasticsearch;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import co.elastic.clients.elasticsearch.core.CountResponse;
+import co.elastic.clients.elasticsearch.core.GetResponse;
+import co.elastic.clients.elasticsearch.core.MgetResponse;
+import co.elastic.clients.elasticsearch.core.SearchResponse;
+import co.elastic.clients.elasticsearch.core.search.Hit;
+import co.elastic.clients.transport.endpoints.BooleanResponse;
+import com.chelvc.framework.common.util.ObjectUtils;
+
+/**
+ * Elasticsearch工具类
+ *
+ * @author Woody
+ * @date 2024/11/9
+ */
+public final class ElasticsearchUtils {
+    private ElasticsearchUtils() {
+    }
+
+    /**
+     * 获取数量
+     *
+     * @param response 响应对象
+     * @return 数量
+     */
+    public static long count(CountResponse response) {
+        return response == null ? 0 : response.count();
+    }
+
+    /**
+     * 获取布尔值
+     *
+     * @param response 响应对象
+     * @return 布尔值
+     */
+    public static boolean bool(BooleanResponse response) {
+        return response != null && response.value();
+    }
+
+    /**
+     * 获取对象实例
+     *
+     * @param response 响应对象
+     * @param <T>      对象类型
+     * @return 对象实例
+     */
+    public static <T> T source(GetResponse<T> response) {
+        return ObjectUtils.ifNull(response, GetResponse::source);
+    }
+
+    /**
+     * 获取对象查询列表
+     *
+     * @param response 响应对象
+     * @param <T>      对象类型
+     * @return 对象列表
+     */
+    public static <T> List<T> sources(MgetResponse<T> response) {
+        if (response == null || ObjectUtils.isEmpty(response.docs())) {
+            return Collections.emptyList();
+        }
+        return response.docs().stream().map(doc -> doc.result().source())
+                .filter(Objects::nonNull).collect(Collectors.toList());
+    }
+
+    /**
+     * 获取对象查询列表
+     *
+     * @param response 响应对象
+     * @param <T>      对象类型
+     * @return 对象列表
+     */
+    public static <T> List<T> sources(SearchResponse<T> response) {
+        if (response == null || response.hits() == null || ObjectUtils.isEmpty(response.hits().hits())) {
+            return Collections.emptyList();
+        }
+        return response.hits().hits().stream().map(Hit::source).filter(Objects::nonNull).collect(Collectors.toList());
+    }
+}

+ 93 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/ModelContext.java

@@ -0,0 +1,93 @@
+package com.chelvc.framework.elasticsearch;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.Objects;
+
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.elasticsearch.annotation.Document;
+import com.chelvc.framework.elasticsearch.annotation.Identity;
+import com.google.common.collect.Maps;
+import lombok.AllArgsConstructor;
+import lombok.NonNull;
+
+/**
+ * ES数据模型上下文
+ *
+ * @author Woody
+ * @date 2024/10/17
+ */
+@AllArgsConstructor
+public class ModelContext {
+    /**
+     * 数据模型/上下文映射表
+     */
+    private static final Map<Class<?>, ModelContext> MODEL_CONTEXT_MAPPING = Maps.newConcurrentMap();
+
+    /**
+     * 索引名
+     */
+    private final String index;
+
+    /**
+     * ID字段
+     */
+    private final Field identity;
+
+    /**
+     * 获取索引名
+     *
+     * @return 索引名
+     */
+    public String getIndex() {
+        return this.index;
+    }
+
+    /**
+     * 获取文档ID
+     *
+     * @param document 文档对象实例
+     * @return 文档ID
+     */
+    public String getIdentity(Object document) {
+        Object value = ObjectUtils.getValue(document, this.identity);
+        return ObjectUtils.ifNull(value, String::valueOf);
+    }
+
+    /**
+     * 构建ES数据模型上下文
+     *
+     * @param model 数据模型
+     * @return 数据模型上下文实例
+     */
+    public static ModelContext of(@NonNull Class<?> model) {
+        ModelContext context = MODEL_CONTEXT_MAPPING.get(model);
+        return context != null ? context : MODEL_CONTEXT_MAPPING.computeIfAbsent(model, k -> {
+            // 获取索引名
+            String index = ObjectUtils.ifNull(model.getAnnotation(Document.class), Document::index);
+            if (StringUtils.isEmpty(index)) {
+                index = StringUtils.hump2underscore(model.getSimpleName());
+            }
+
+            // 查找ID字段
+            Field primary = null, backup = null;
+            Field[] fields = model.getDeclaredFields();
+            if (ObjectUtils.notEmpty(fields)) {
+                for (Field field : fields) {
+                    if (field.isAnnotationPresent(Identity.class)) {
+                        primary = field;
+                        break;
+                    } else if (Objects.equals(field.getName(), "id")) {
+                        backup = field;
+                    }
+                }
+            }
+            Field identity = ObjectUtils.ifNull(primary, backup);
+            if (identity == null) {
+                throw new IllegalStateException("Document model identity field not found: " + model.getName());
+            }
+            return new ModelContext(index, identity);
+        });
+    }
+}

+ 27 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/annotation/Document.java

@@ -0,0 +1,27 @@
+package com.chelvc.framework.elasticsearch.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.chelvc.framework.common.util.StringUtils;
+
+/**
+ * ES文档注解
+ *
+ * @author Woody
+ * @date 2024/10/17
+ */
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Document {
+    /**
+     * 获取文档索引
+     *
+     * @return 文档索引
+     */
+    String index() default StringUtils.EMPTY;
+}

+ 19 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/annotation/Identity.java

@@ -0,0 +1,19 @@
+package com.chelvc.framework.elasticsearch.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * ES文档ID注解
+ *
+ * @author Woody
+ * @date 2024/10/17
+ */
+@Documented
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Identity {
+}

+ 57 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/config/ElasticsearchConfigurer.java

@@ -0,0 +1,57 @@
+package com.chelvc.framework.elasticsearch.config;
+
+import co.elastic.clients.json.jackson.JacksonJsonpMapper;
+import co.elastic.clients.transport.ElasticsearchTransport;
+import co.elastic.clients.transport.rest_client.RestClientTransport;
+import com.chelvc.framework.common.util.JacksonUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.elasticsearch.DefaultElasticsearchHandler;
+import com.chelvc.framework.elasticsearch.ElasticsearchHandler;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.elasticsearch.client.RestClient;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * ES配置
+ *
+ * @author Woody
+ * @date 2024/10/17
+ */
+@Configuration
+public class ElasticsearchConfigurer {
+    @Bean(destroyMethod = "close")
+    public ElasticsearchTransport elasticsearchTransport(ElasticsearchProperties properties) {
+        HttpHost host = new HttpHost(properties.getHost(), properties.getPort(), properties.getScheme());
+        RestClient client = RestClient.builder(host).setHttpClientConfigCallback(builder -> {
+            if (properties.getMaxConnTotal() > 0) {
+                builder.setMaxConnTotal(properties.getMaxConnTotal());
+            }
+            if (properties.getMaxConnPerRoute() > 0) {
+                builder.setMaxConnPerRoute(properties.getMaxConnPerRoute());
+            }
+            if (StringUtils.notEmpty(properties.getUsername()) && StringUtils.notEmpty(properties.getPassword())) {
+                UsernamePasswordCredentials credentials =
+                        new UsernamePasswordCredentials(properties.getUsername(), properties.getPassword());
+                CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+                credentialsProvider.setCredentials(AuthScope.ANY, credentials);
+                builder.setDefaultCredentialsProvider(credentialsProvider);
+            }
+            return builder;
+        }).build();
+        ObjectMapper mapper = JacksonUtils.configure(new ObjectMapper());
+        return new RestClientTransport(client, new JacksonJsonpMapper(mapper));
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(ElasticsearchHandler.class)
+    public ElasticsearchHandler elasticsearchHandler(ElasticsearchTransport transport) {
+        return new DefaultElasticsearchHandler(transport);
+    }
+}

+ 51 - 0
framework-elasticsearch/src/main/java/com/chelvc/framework/elasticsearch/config/ElasticsearchProperties.java

@@ -0,0 +1,51 @@
+package com.chelvc.framework.elasticsearch.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * ES配置属性
+ *
+ * @author Woody
+ * @date 2024/10/16
+ */
+@Data
+@Configuration
+@ConfigurationProperties("elasticsearch")
+public class ElasticsearchProperties {
+    /**
+     * 主机地址
+     */
+    private String host;
+
+    /**
+     * 主机端口
+     */
+    private int port = 9200;
+
+    /**
+     * 协议类型
+     */
+    private String scheme = "http";
+
+    /**
+     * 认证账号
+     */
+    private String username;
+
+    /**
+     * 认证密码
+     */
+    private String password;
+
+    /**
+     * 最大连接数
+     */
+    private int maxConnTotal = 500;
+
+    /**
+     * 连接最大路由数
+     */
+    private int maxConnPerRoute = 300;
+}

+ 2 - 1
framework-email/src/main/java/com/chelvc/framework/email/context/EmailContextHolder.java

@@ -22,6 +22,7 @@ import javax.mail.internet.MimeUtility;
 import javax.mail.util.ByteArrayDataSource;
 
 import ch.qos.logback.core.util.ContentTypeUtil;
+import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.email.Body;
 import lombok.NonNull;
@@ -53,10 +54,10 @@ public final class EmailContextHolder {
      */
     public static Multipart part(@NonNull Body body) {
         // 构建邮件内容部分
-        String text = body.getContent();
         MimeMultipart part = new MimeMultipart();
         MimeBodyPart content = new MimeBodyPart();
         String type = StringUtils.ifEmpty(body.getType(), DEFAULT_MIME_TYPE);
+        String text = ObjectUtils.ifNull(body.getContent(), StringUtils.EMPTY);
         try {
             if (ContentTypeUtil.isTextual(type)) {
                 content.setText(text, StandardCharsets.UTF_8.name(), ContentTypeUtil.getSubType(type));

+ 11 - 5
framework-feign/src/main/java/com/chelvc/framework/feign/config/FeignConfigurer.java

@@ -1,16 +1,17 @@
 package com.chelvc.framework.feign.config;
 
 import com.chelvc.framework.base.context.ApplicationContextHolder;
+import com.chelvc.framework.feign.encoder.CustomizeEncoder;
+import com.chelvc.framework.feign.encoder.CustomizeQueryEncoder;
 import com.chelvc.framework.feign.interceptor.FeignFailureInterceptor;
 import com.chelvc.framework.feign.interceptor.FeignInvokeInterceptor;
+import feign.QueryMapEncoder;
 import feign.codec.Decoder;
 import feign.codec.Encoder;
 import feign.codec.ErrorDecoder;
 import feign.optionals.OptionalDecoder;
-import lombok.RequiredArgsConstructor;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.ObjectFactory;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.boot.autoconfigure.AutoConfigureBefore;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -19,6 +20,7 @@ import org.springframework.cloud.openfeign.FeignAutoConfiguration;
 import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
 import org.springframework.cloud.openfeign.support.SpringDecoder;
 import org.springframework.cloud.openfeign.support.SpringEncoder;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Primary;
@@ -32,7 +34,6 @@ import org.springframework.context.annotation.Scope;
  */
 @Configuration
 @AutoConfigureBefore(FeignAutoConfiguration.class)
-@RequiredArgsConstructor(onConstructor = @__(@Autowired))
 public class FeignConfigurer {
     /**
      * 定制消息转换处理器
@@ -52,6 +53,11 @@ public class FeignConfigurer {
         };
     }
 
+    @Bean
+    public QueryMapEncoder queryEncoder() {
+        return new CustomizeQueryEncoder();
+    }
+
     @Bean
     @Primary
     @Scope(BeanDefinition.SCOPE_PROTOTYPE)
@@ -73,7 +79,7 @@ public class FeignConfigurer {
 
     @Bean
     @ConditionalOnMissingBean(FeignInvokeInterceptor.class)
-    public FeignInvokeInterceptor feignInvokeInterceptor() {
-        return new FeignInvokeInterceptor();
+    public FeignInvokeInterceptor feignInvokeInterceptor(ApplicationContext applicationContext) {
+        return new FeignInvokeInterceptor(applicationContext);
     }
 }

+ 45 - 0
framework-feign/src/main/java/com/chelvc/framework/feign/config/FeignProperties.java

@@ -0,0 +1,45 @@
+package com.chelvc.framework.feign.config;
+
+import java.util.Collections;
+import java.util.List;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Feign配置属性
+ *
+ * @author Woody
+ * @date 2024/10/29
+ */
+@Data
+@Configuration
+@ConfigurationProperties("feign")
+public class FeignProperties {
+    /**
+     * 请求转发配置列表
+     */
+    private List<Forward> forwards = Collections.emptyList();
+
+    /**
+     * 请求转发配置
+     */
+    @Data
+    public static class Forward {
+        /**
+         * 原服务
+         */
+        private String source;
+
+        /**
+         * 目标服务
+         */
+        private String target;
+
+        /**
+         * 是否增加接口前缀
+         */
+        private boolean prefixed;
+    }
+}

+ 1 - 1
framework-feign/src/main/java/com/chelvc/framework/feign/config/CustomizeEncoder.java → framework-feign/src/main/java/com/chelvc/framework/feign/encoder/CustomizeEncoder.java

@@ -1,4 +1,4 @@
-package com.chelvc.framework.feign.config;
+package com.chelvc.framework.feign.encoder;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Type;

+ 37 - 0
framework-feign/src/main/java/com/chelvc/framework/feign/encoder/CustomizeQueryEncoder.java

@@ -0,0 +1,37 @@
+package com.chelvc.framework.feign.encoder;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.Map;
+
+import com.chelvc.framework.common.util.DateUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import feign.codec.EncodeException;
+import feign.querymap.FieldQueryMapEncoder;
+
+/**
+ * 自定义查询参数编码处理器
+ *
+ * @author Woody
+ * @date 2024/10/30
+ */
+public class CustomizeQueryEncoder extends FieldQueryMapEncoder {
+    @Override
+    public Map<String, Object> encode(Object object) throws EncodeException {
+        Map<String, Object> queries = super.encode(object);
+        if (ObjectUtils.notEmpty(queries)) {
+            for (Map.Entry<String, Object> entry : queries.entrySet()) {
+                Object value = entry.getValue();
+                if (value instanceof Date) {
+                    entry.setValue(((Date) value).getTime());
+                } else if (value instanceof LocalDate) {
+                    entry.setValue(DateUtils.convert((LocalDate) value).getTime());
+                } else if (value instanceof LocalDateTime) {
+                    entry.setValue(DateUtils.convert((LocalDateTime) value).getTime());
+                }
+            }
+        }
+        return queries;
+    }
+}

+ 70 - 11
framework-feign/src/main/java/com/chelvc/framework/feign/interceptor/FeignInvokeInterceptor.java

@@ -1,5 +1,12 @@
 package com.chelvc.framework.feign.interceptor;
 
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
 import com.chelvc.framework.base.context.ApplicationContextHolder;
 import com.chelvc.framework.base.context.Session;
 import com.chelvc.framework.base.context.SessionContextHolder;
@@ -7,9 +14,19 @@ import com.chelvc.framework.base.context.Using;
 import com.chelvc.framework.base.util.HttpUtils;
 import com.chelvc.framework.common.model.Platform;
 import com.chelvc.framework.common.model.Terminal;
+import com.chelvc.framework.common.util.HostUtils;
 import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.feign.config.FeignProperties;
+import com.google.common.collect.Lists;
 import feign.RequestInterceptor;
 import feign.RequestTemplate;
+import feign.Target;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
 
 /**
  * Feign调用处理拦截器
@@ -17,18 +34,36 @@ import feign.RequestTemplate;
  * @author Woody
  * @date 2024/1/30
  */
+@Slf4j
 public class FeignInvokeInterceptor implements RequestInterceptor {
-    /**
-     * 初始化请求前缀
-     *
-     * @param template 请求模版对象实例
-     */
-    protected void initializeRequestPrefix(RequestTemplate template) {
-        String path = template.path();
-        String server = template.feignTarget().name();
-        if (ApplicationContextHolder.isResourcePrefixed(server, path)) {
-            template.uri(HttpUtils.uri(server, path));
+    private final List<FeignProperties.Forward> forwards;
+
+    public FeignInvokeInterceptor(@NonNull ApplicationContext applicationContext) {
+        // 加载请求转发配置
+        FeignProperties properties = applicationContext.getBean(FeignProperties.class);
+        List<FeignProperties.Forward> forwards = ObjectUtils.ifNull(
+                properties.getForwards(), Lists::newArrayList, Lists::newArrayList
+        );
+        List<Resource> resources = ApplicationContextHolder.getApplicationResources();
+        RequestMappingHandlerMapping handlers = applicationContext.getBean(RequestMappingHandlerMapping.class);
+        Set<String> prefixes = ObjectUtils.ifNull(handlers.getPathPrefixes(), Map::keySet, Collections::emptySet);
+        if (ObjectUtils.notEmpty(resources)) {
+            int port = ApplicationContextHolder.getPort(applicationContext);
+            String server = ApplicationContextHolder.getApplicationName(applicationContext);
+            for (Resource resource : resources) {
+                String name = ApplicationContextHolder.getApplicationName(resource);
+                if (StringUtils.isEmpty(name) || ObjectUtils.equals(name, server)
+                        || forwards.stream().anyMatch(forward -> Objects.equals(forward.getSource(), name))) {
+                    continue;
+                }
+                FeignProperties.Forward forward = new FeignProperties.Forward();
+                forward.setSource(name);
+                forward.setTarget(HostUtils.DEFAULT_LOCAL_NAME + ":" + port);
+                forward.setPrefixed(prefixes.contains(name));
+                forwards.add(forward);
+            }
         }
+        this.forwards = forwards.isEmpty() ? Collections.emptyList() : Lists.newArrayList(forwards);
     }
 
     /**
@@ -60,7 +95,31 @@ public class FeignInvokeInterceptor implements RequestInterceptor {
 
     @Override
     public void apply(RequestTemplate template) {
-        this.initializeRequestPrefix(template);
+        // 初始化请求头
         this.initializeRequestHeader(template);
+
+        // 请求转发处理
+        if (ObjectUtils.notEmpty(this.forwards)) {
+            Target<?> target = template.feignTarget();
+            for (FeignProperties.Forward forward : this.forwards) {
+                if (ObjectUtils.equals(forward.getSource(), target.name())
+                        || ObjectUtils.equals(forward.getSource(), "*")) {
+                    if (forward.isPrefixed()) {
+                        template.uri(HttpUtils.uri(target.name(), template.path()));
+                    }
+                    if (!Objects.equals(forward.getTarget(), target.name())) {
+                        URI uri = URI.create(target.url());
+                        String url = uri.getScheme() + "://" + forward.getTarget();
+                        template.target(url);
+                        template.feignTarget(new Target.HardCodedTarget<>(target.type(), forward.getTarget(), url));
+                    }
+                    break;
+                }
+            }
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("Feign request: {}", template);
+        }
     }
 }

+ 4 - 8
framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/config/RocketMQConfigurer.java

@@ -1,8 +1,8 @@
 package com.chelvc.framework.rocketmq.config;
 
-import com.chelvc.framework.rocketmq.context.RocketMQContextHolder;
 import com.chelvc.framework.rocketmq.fallback.RocketMQFallback;
 import com.chelvc.framework.rocketmq.fallback.RocketMQMemoryProducer;
+import com.chelvc.framework.rocketmq.producer.CommonTransactionChecker;
 import com.chelvc.framework.rocketmq.producer.RocketMQProducerWrapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -12,10 +12,10 @@ import org.apache.rocketmq.client.apis.SessionCredentialsProvider;
 import org.apache.rocketmq.client.apis.StaticSessionCredentialsProvider;
 import org.apache.rocketmq.client.apis.producer.Producer;
 import org.apache.rocketmq.client.apis.producer.TransactionChecker;
-import org.apache.rocketmq.client.apis.producer.TransactionResolution;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -48,12 +48,8 @@ public class RocketMQConfigurer {
 
     @Bean
     @ConditionalOnMissingBean(TransactionChecker.class)
-    public TransactionChecker transactionChecker() {
-        return message -> {
-            String topic = RocketMQContextHolder.topic(message), body = RocketMQContextHolder.body(message);
-            log.error("RocketMQ transaction checker is missing: {}, {}", topic, body);
-            return TransactionResolution.UNKNOWN;
-        };
+    public TransactionChecker transactionChecker(ApplicationContext applicationContext) {
+        return new CommonTransactionChecker(applicationContext);
     }
 
     @Bean

+ 47 - 0
framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/producer/CommonTransactionChecker.java

@@ -0,0 +1,47 @@
+package com.chelvc.framework.rocketmq.producer;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.chelvc.framework.common.util.JacksonUtils;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.rocketmq.context.RocketMQContextHolder;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.client.apis.message.MessageView;
+import org.apache.rocketmq.client.apis.producer.TransactionChecker;
+import org.apache.rocketmq.client.apis.producer.TransactionResolution;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * RocketMQ事务本地检测公共处理器实现
+ *
+ * @author Woody
+ * @date 2024/10/29
+ */
+@Slf4j
+public class CommonTransactionChecker implements TransactionChecker {
+    private final Map<String, TransactionMessageChecker<?>> checkers;
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public CommonTransactionChecker(@NonNull ApplicationContext applicationContext) {
+        Map<String, TransactionMessageChecker> checkers =
+                applicationContext.getBeansOfType(TransactionMessageChecker.class);
+        this.checkers = ObjectUtils.isEmpty(checkers) ? Collections.emptyMap() :
+                checkers.values().stream().collect(Collectors.toMap(TransactionMessageChecker::topic, c -> c));
+    }
+
+    @Override
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public TransactionResolution check(MessageView message) {
+        String body = RocketMQContextHolder.body(message);
+        String topic = RocketMQContextHolder.topic(message);
+        TransactionMessageChecker checker = this.checkers.get(topic);
+        if (checker == null) {
+            log.error("Transaction message checker is missing: {}, {}", topic, body);
+            return TransactionResolution.UNKNOWN;
+        }
+        return checker.check(JacksonUtils.deserialize(body, checker.model()));
+    }
+}

+ 3 - 1
framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/producer/RocketMQProducerFactory.java

@@ -77,6 +77,8 @@ public class RocketMQProducerFactory implements FactoryBean<Producer>, Disposabl
 
     @Override
     public void destroy() throws Exception {
-        this.producer.close();
+        if (this.producer != null) {
+            this.producer.close();
+        }
     }
 }

+ 34 - 0
framework-rocketmq/src/main/java/com/chelvc/framework/rocketmq/producer/TransactionMessageChecker.java

@@ -0,0 +1,34 @@
+package com.chelvc.framework.rocketmq.producer;
+
+import org.apache.rocketmq.client.apis.producer.TransactionResolution;
+
+/**
+ * RocketMQ事务消息本地事务检测接口
+ *
+ * @param <T> 消息类型
+ * @author Woody
+ * @date 2024/10/29
+ */
+public interface TransactionMessageChecker<T> {
+    /**
+     * 获取消息主题
+     *
+     * @return 消息主题
+     */
+    String topic();
+
+    /**
+     * 获取消息类型
+     *
+     * @return 消息模型对象
+     */
+    Class<T> model();
+
+    /**
+     * 本地事务检测
+     *
+     * @param message 事务消息
+     * @return 本地事务检测处理决策
+     */
+    TransactionResolution check(T message);
+}

+ 1 - 1
framework-security/src/main/java/com/chelvc/framework/security/config/AuthorizeConfigurer.java

@@ -174,7 +174,7 @@ public class AuthorizeConfigurer extends WebSecurityConfigurerAdapter {
             String prefix = null;
             if (multiserver) {
                 Resource resource = ApplicationContextHolder.lookupClassResource(clazz, resources);
-                prefix = ObjectUtils.ifNull(resource, ApplicationContextHolder::getResourcePrefix);
+                prefix = resource == null ? null : ApplicationContextHolder.getApplicationName(resource);
             }
 
             // 遍历所有接口方法

+ 1 - 0
pom.xml

@@ -29,6 +29,7 @@
         <module>framework-upload</module>
         <module>framework-wechat</module>
         <module>framework-download</module>
+        <module>framework-elasticsearch</module>
         <module>framework-dependencies</module>
         <module>framework-cloud</module>
         <module>framework-cloud-nacos</module>