woody 6 kuukautta sitten
vanhempi
commit
035df74711

+ 24 - 2
framework-common/src/main/java/com/chelvc/framework/common/util/DateUtils.java

@@ -1,5 +1,7 @@
 package com.chelvc.framework.common.util;
 
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -109,10 +111,10 @@ public final class DateUtils {
                     new DatetimeParser(DateTimeFormatter.ofPattern("yyyy/M/d H:m:s"))
             ).put(
                     StringUtils.getPattern("^\\d{4}-\\d{1,2}-\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}\\.\\d{1,3}$"),
-                    new DatetimeParser(DateTimeFormatter.ofPattern("yyyy-M-d H:m:s.SSS"))
+                    new SimpleDatetimeParser("yyyy-M-d H:m:s.SSS")
             ).put(
                     StringUtils.getPattern("^\\d{4}/\\d{1,2}/\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}\\.\\d{1,3}$"),
-                    new DatetimeParser(DateTimeFormatter.ofPattern("yyyy-M-d H:m:s.SSS"))
+                    new SimpleDatetimeParser("yyyy-M-d H:m:s.SSS")
             ).put(
                     StringUtils.getPattern("^\\d{4}-\\d{1,2}-\\d{1,2}T\\d{1,2}:\\d{1,2}:\\d{1,2}\\.\\d{1,3}\\S+$"),
                     new ZonedDatetimeParser(DateTimeFormatter.ofPattern("yyyy-M-d'T'H:m:s.SSSXXX"))
@@ -199,6 +201,26 @@ public final class DateUtils {
         }
     }
 
+    /**
+     * 基于SimpleDateFormat日期时间文本解析实现
+     */
+    public static class SimpleDatetimeParser implements Parser {
+        private final String format;
+
+        public SimpleDatetimeParser(@NonNull String format) {
+            this.format = format;
+        }
+
+        @Override
+        public Date parse(String text) {
+            try {
+                return new SimpleDateFormat(this.format).parse(text);
+            } catch (ParseException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     /**
      * 查找日期文本对应的解析器
      *

+ 50 - 0
framework-common/src/main/java/com/chelvc/framework/common/util/JacksonUtils.java

@@ -1147,4 +1147,54 @@ public final class JacksonUtils {
         }
         return maps.stream().map(map -> deserialize(mapper, map, type, function)).collect(Collectors.toList());
     }
+
+    /**
+     * 对象类型转换
+     *
+     * @param object 原对象实例
+     * @param type   目标对象类型
+     * @param <T>    数据类型
+     * @return 对象实例
+     */
+    public static <T> T convert(Object object, @NonNull Class<T> type) {
+        return convert(getDefaultSerializer(), object, type);
+    }
+
+    /**
+     * 对象类型转换
+     *
+     * @param object 原对象实例
+     * @param type   目标对象类型
+     * @param <T>    数据类型
+     * @return 对象实例
+     */
+    public static <T> T convert(Object object, @NonNull TypeReference<T> type) {
+        return convert(getDefaultSerializer(), object, type);
+    }
+
+    /**
+     * 对象类型转换
+     *
+     * @param mapper 对象序列化处理器实例
+     * @param object 原对象实例
+     * @param type   目标对象类型
+     * @param <T>    数据类型
+     * @return 对象实例
+     */
+    public static <T> T convert(@NonNull ObjectMapper mapper, Object object, @NonNull Class<T> type) {
+        return object == null ? null : mapper.convertValue(object, type);
+    }
+
+    /**
+     * 对象类型转换
+     *
+     * @param mapper 对象序列化处理器实例
+     * @param object 原对象实例
+     * @param type   目标对象类型
+     * @param <T>    数据类型
+     * @return 对象实例
+     */
+    public static <T> T convert(@NonNull ObjectMapper mapper, Object object, @NonNull TypeReference<T> type) {
+        return object == null ? null : mapper.convertValue(object, type);
+    }
 }

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

@@ -102,9 +102,9 @@ public final class StringUtils {
     public static final String PLAINTEXT_REGEX = "[^\\w\u4e00-\u9fa5\\.\\-—~~_\\+]";
 
     /**
-     * 数字链接符号正则表达式
+     * 链接符号正则表达式
      */
-    public static final String NUMBER_LINK_REGEX = "[\\.\\-—~~_]";
+    public static final String LINK_REGEX = "[\\.\\-—~~_·]";
 
     /**
      * 非数字符号正则表达式
@@ -112,9 +112,9 @@ public final class StringUtils {
     public static final String NON_NUMBER_SYMBOL_REGEX = "[\\.\\-—~~_](?![0-9])";
 
     /**
-     * 数字链接正则表达式
+     * 数字链接符号正则表达式
      */
-    public static final String NON_NUMBER_LINK_REGEX = "[0-9]*([\\.\\-—~~_][0-9]+){2,}";
+    public static final String NUMBER_LINK_REGEX = "[0-9]*([\\.\\-—~~_·][0-9]+){2,}";
 
     /**
      * 数字正则表达式
@@ -1148,30 +1148,58 @@ public final class StringUtils {
     }
 
     /**
-     * 清理文本
+     * 清理数字
      *
      * @param original 原始信息
      * @return 清理后的信息
      */
-    public static String clear(String original) {
-        if (notEmpty(original)) {
-            // 过滤表情符号
-            original = clear(original, EMOJI_REGEX);
+    private static String clearNumber(String original) {
+        if (isEmpty(original)) {
+            return original;
+        }
 
-            // 过滤特殊符号
-            original = clear(original, PLAINTEXT_REGEX);
+        Matcher matcher = getPattern(NUMBER_LINK_REGEX).matcher(original);
+        while (matcher.find()) {
+            String group = matcher.group();
+            original = getPattern(group).matcher(original).replaceAll(clear(group, LINK_REGEX));
+        }
+        return original;
+    }
 
-            // 过滤非数字符号
-            original = clear(original, NON_NUMBER_SYMBOL_REGEX);
+    /**
+     * 清理文本
+     *
+     * @param original 原始信息
+     * @return 清理后的信息
+     */
+    public static String clear(String original) {
+        if (isEmpty(original)) {
+            return original;
+        }
 
-            // 过滤非数字链接符号
-            Matcher matcher = getPattern(NON_NUMBER_LINK_REGEX).matcher(original);
-            while (matcher.find()) {
-                String group = matcher.group();
-                original = getPattern(group).matcher(original).replaceAll(clear(group, NUMBER_LINK_REGEX));
+        // 过滤表情符号
+        original = clear(original, EMOJI_REGEX);
+
+        // 过滤特殊符号
+        original = clear(original, PLAINTEXT_REGEX);
+
+        // 过滤非数字符号
+        original = clear(original, NON_NUMBER_SYMBOL_REGEX);
+
+        // 过滤数字链接符号
+        try {
+            return clearNumber(original);
+        } catch (StackOverflowError e) {
+            // 堆栈溢出后分段重试,当设置 -Xss160K时,文本长度超过600堆栈溢出,所以取值500
+            int size = 500, max = original.length();
+            StringBuilder buffer = new StringBuilder(max);
+            int group = (int) Math.ceil((double) max / size);
+            for (int i = 0; i < group; i++) {
+                int begin = i * size, end = Math.min((i + 1) * size, max);
+                buffer.append(clearNumber(original.substring(begin, end)));
             }
+            return buffer.toString();
         }
-        return original;
     }
 
     /**

+ 2 - 2
framework-database/src/main/java/com/chelvc/framework/database/support/BatchCreateUpdateMethod.java

@@ -18,11 +18,11 @@ public class BatchCreateUpdateMethod extends AbstractCreateMethod {
     protected String initializeMethodScript(@NonNull TableInfo table) {
         return String.format(
                 "<script>\nINSERT INTO %s (%s) VALUES\n" +
-                        "<foreach collection=\"entities\" item=\"entity\" separator=\",\">(%s)</foreach>\n" +
+                        "<foreach collection=\"coll\" item=\"et\" separator=\",\">(%s)</foreach>\n" +
                         "ON DUPLICATE KEY UPDATE \n" +
                         "<foreach collection=\"update.expressions.entrySet()\" index=\"column\" item=\"expression\" " +
                         "separator=\",\">${column} = ${expression}</foreach>\n</script>",
-                table.getTableName(), this.getColumns(table), this.getProperties(table, "entity.")
+                table.getTableName(), this.getColumns(table), this.getProperties(table, "et.")
         );
     }
 }

+ 296 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/Binlog.java

@@ -0,0 +1,296 @@
+package com.chelvc.framework.database.support;
+
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+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.google.common.collect.Maps;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * binlog日志
+ *
+ * @author Woody
+ * @date 2024/10/15
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Binlog implements Serializable {
+    /**
+     * 操作类型(c -> 新增、u -> 修改、 d -> 删除)
+     */
+    private String op;
+
+    /**
+     * 更新前字段/值映射表
+     */
+    private Map<String, Object> before;
+
+    /**
+     * 更新后字段/值映射表
+     */
+    private Map<String, Object> after;
+
+    /**
+     * 转换字段值
+     *
+     * @param field  字段对象
+     * @param object 字段原始值
+     * @return 转换后字段值
+     */
+    public static Object convert(@NonNull Field field, Object object) {
+        Class<?> type = field.getType();
+        if (object instanceof String && (Map.class.isAssignableFrom(type) || List.class.isAssignableFrom(type))) {
+            // binlog消息不支持JSON格式,所以需要单独反序列化处理
+            return JacksonUtils.deserialize((String) object, field.getGenericType());
+        }
+        return JacksonUtils.convert(object, type);
+    }
+
+    /**
+     * 转换实体对象实例
+     *
+     * @param model   实体模型
+     * @param mapping 字段名/值映射表
+     * @param <T>     实体类型
+     * @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);
+        return entity;
+    }
+
+    /**
+     * 更新实体属性值
+     *
+     * @param entity  实体对象实例
+     * @param mapping 字段名/值映射表
+     */
+    public static void update(@NonNull Object entity, Map<?, ?> mapping) {
+        Class<?> model = entity.getClass();
+        TableInfo table = DatabaseContextHolder.getTable(model);
+        update(table, entity, mapping);
+    }
+
+    /**
+     * 更新实体属性值
+     *
+     * @param table   表信息
+     * @param entity  实体对象实例
+     * @param mapping 字段名/值映射表
+     */
+    private static void update(TableInfo table, 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);
+        }
+
+        // 更新普通字段值
+        for (TableFieldInfo field : table.getFieldList()) {
+            Object value = mapping.get(field.getColumn());
+            value = convert(field.getField(), value);
+            ObjectUtils.setValue(entity, field.getField(), value);
+        }
+    }
+
+    /**
+     * 是否新增
+     *
+     * @return true/false
+     */
+    public boolean isInsert() {
+        return Objects.equals(this.op, "c");
+    }
+
+    /**
+     * 是否修改
+     *
+     * @return true/false
+     */
+    public boolean isUpdate() {
+        return Objects.equals(this.op, "u");
+    }
+
+    /**
+     * 是否删除
+     *
+     * @return true/false
+     */
+    public boolean isDelete() {
+        return Objects.equals(this.op, "d");
+    }
+
+    /**
+     * 判断数据是否更新
+     *
+     * @param model 数据模型
+     * @return true/false
+     */
+    public boolean isModified(@NonNull Class<?> model) {
+        if (ObjectUtils.isEmpty(this.after)) {
+            return false;
+        }
+
+        TableInfo table = DatabaseContextHolder.getTable(model);
+        List<TableFieldInfo> fields = table.getFieldList();
+        if (ObjectUtils.isEmpty(fields)) {
+            return false;
+        }
+        for (TableFieldInfo field : fields) {
+            String column = field.getColumn();
+            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)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断数据是否更新
+     *
+     * @param columns 数据库字段数组
+     * @return true/false
+     */
+    public boolean isModified(@NonNull String... columns) {
+        if (ObjectUtils.notEmpty(this.after) && ObjectUtils.notEmpty(columns)) {
+            for (String column : columns) {
+                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)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 赋值实体对象更新后属性值
+     *
+     * @param entity 实体对象实例
+     * @param <T>    实体类型
+     */
+    public <T> void after(@NonNull T entity) {
+        update(entity, this.after);
+    }
+
+    /**
+     * 赋值实体对象更新后属性值
+     *
+     * @param model 实体模型
+     * @param <T>   实体类型
+     * @return 实体对象实例
+     */
+    public <T> T after(@NonNull Class<T> model) {
+        return convert(model, this.after);
+    }
+
+    /**
+     * 赋值实体对象更新前属性值
+     *
+     * @param entity 实体对象实例
+     * @param <T>    实体类型
+     */
+    public <T> void before(@NonNull T entity) {
+        update(entity, this.before);
+    }
+
+    /**
+     * 赋值实体对象更新前属性值
+     *
+     * @param model 实体模型
+     * @param <T>   实体类型
+     * @return 实体对象实例
+     */
+    public <T> T before(@NonNull Class<T> model) {
+        return convert(model, this.before);
+    }
+
+    /**
+     * 获取差异属性值
+     *
+     * @param model 实体模型
+     * @return 实体对象属性名/值映射表
+     */
+    public Map<String, Object> different(@NonNull Class<?> model) {
+        if (ObjectUtils.isEmpty(this.after)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, Object> different = Maps.newHashMap();
+        TableInfo table = DatabaseContextHolder.getTable(model);
+        List<TableFieldInfo> fields = table.getFieldList();
+        if (ObjectUtils.isEmpty(fields)) {
+            return Collections.emptyMap();
+        }
+        for (TableFieldInfo field : fields) {
+            String column = field.getColumn();
+            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));
+            }
+        }
+        return different;
+    }
+
+    /**
+     * 获取差异字段值
+     *
+     * @param columns 数据库字段数组
+     * @return 数据库字段名/值映射表
+     */
+    public Map<String, Object> different(@NonNull String... columns) {
+        if (ObjectUtils.isEmpty(this.after)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, Object> different = Maps.newHashMap();
+        if (ObjectUtils.isEmpty(columns)) {
+            this.after.forEach((key, a) -> {
+                Object b = ObjectUtils.ifNull(this.before, map -> map.get(key));
+                if (!ObjectUtils.equals(a, b)) {
+                    different.put(key, a);
+                }
+            });
+        } else {
+            for (String column : columns) {
+                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(column, a);
+                }
+            }
+        }
+        return different;
+    }
+}

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

@@ -20,7 +20,7 @@ public class CreateUpdateMethod extends AbstractCreateMethod {
                 "<script>\nINSERT INTO %s (%s) VALUES (%s)\n ON DUPLICATE KEY UPDATE \n" +
                         "<foreach collection=\"update.expressions.entrySet()\" index=\"column\" item=\"expression\" " +
                         "separator=\",\">${column} = ${expression}</foreach>\n</script>",
-                table.getTableName(), this.getColumns(table), this.getProperties(table, "entity.")
+                table.getTableName(), this.getColumns(table), this.getProperties(table, "et.")
         );
     }
 }

+ 2 - 2
framework-database/src/main/java/com/chelvc/framework/database/support/EnhanceMapper.java

@@ -27,7 +27,7 @@ public interface EnhanceMapper<T> extends BaseMapper<T> {
      * @param update 数据更新对象实例
      * @return 影响行数
      */
-    int createUpdate(@Param("entity") T entity, @Param("update") Updates.Update<T> update);
+    int createUpdate(@Param("et") T entity, @Param("update") Updates.Update<T> update);
 
     /**
      * 批量创建或忽略实体
@@ -44,5 +44,5 @@ public interface EnhanceMapper<T> extends BaseMapper<T> {
      * @param update   数据更新对象实例
      * @return 影响行数
      */
-    int batchCreateUpdate(@Param("entities") Collection<T> entities, @Param("update") Updates.Update<T> update);
+    int batchCreateUpdate(@Param("coll") Collection<T> entities, @Param("update") Updates.Update<T> update);
 }

+ 3 - 1
framework-kafka/src/main/java/com/chelvc/framework/kafka/context/KafkaContextHolder.java

@@ -130,8 +130,10 @@ public final class KafkaContextHolder {
         Object value = record.value();
         if (value instanceof byte[]) {
             return JacksonUtils.deserialize((byte[]) value, type);
+        } else if (value == null || (value instanceof String && type == String.class)) {
+            return value;
         }
-        return value == null ? null : JacksonUtils.deserialize(String.valueOf(value), type);
+        return JacksonUtils.deserialize(String.valueOf(value), type);
     }
 
     /**