feat(constool): 添加数据对比同步工具并重构动态表格构建器

- 新增 KitDiffSyncConstTool 工具类,支持集合数据的对比同步功能
- 添加数据同步回调接口和完整的增删改查批量操作支持
- 重命名 KitDynamicTableUtil 为 KitDynamicTableBuilder 并调整方法名称
- 修改 renderOrder 方法为 ofOrder,renderAll 方法为 ofAll
- 更新前端 FormatDuration 函数,支持年、天、时、分、秒格式化显示
- 添加字节格式化方法 renderBytes 到 KitStringUtil 工具类
- 在首页视图中集成 Pipeline 组件用于展示任务流程
- 添加窗口打开工具函数 OpenWindowInCurrent 和 OpenWindowInNew
- 创建 MybatisMapperBatchOpRender 批量操作生成器
This commit is contained in:
2026-03-06 19:19:34 +08:00
parent afbdbbdea8
commit a14809e203
22 changed files with 1749 additions and 275 deletions
@@ -0,0 +1,168 @@
package cn.odboy.constool;
import cn.hutool.core.collection.CollUtil;
import cn.odboy.base.KitBaseUserTimeTb;
import cn.odboy.framework.exception.BadRequestException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 数据对比同步工具
* <p>
* 适用场景:对比两个相同类型的对象集合,以对照组(compareRecords)为基准:
* <p>
* - 本地不存在的数据 → 新增
* <p>
* - 本地存在的数据 → 更新
* <p>
* - 对照组不存在的数据 → 删除
*
* @param <T> 数据类型
* @author odboy
*/
public class KitDiffSyncConstTool<T> {
/**
* 数据同步回调接口
*
* @param <T> 数据类型
*/
public interface SyncCallback<T> {
/**
* 全量存储对照组数据
*
* @param records 需要保存的数据列表
*/
void doSaveBatch(List<T> records);
/**
* 批量更新数据
*
* @param localList 本地已存在的数据列表
* @param compareList 对照组对应的数据列表
*/
void doUpdateBatch(List<T> localList, List<T> compareList);
/**
* 批量删除数据
*
* @param records 需要删除的数据列表
*/
void doDeleteBatch(List<T> records);
}
/**
* 本地数据源
*/
private final List<T> localRecords;
/**
* 对照数据源(以对照数据源为准)
*/
private final List<T> compareRecords;
/**
* 构造函数
*
* @param localRecords 本地数据源
* @param compareRecords 对照数据源(基准数据)
*/
public KitDiffSyncConstTool(List<T> localRecords, List<T> compareRecords) {
this.localRecords = localRecords;
this.compareRecords = compareRecords;
}
/**
* 执行数据对比同步
*
* @param keyMapper 唯一键映射函数,用于比较数据唯一性(如:Entity::getId)
* @param callback 同步回调接口,处理具体的增删改操作
* @param <K> 唯一键类型
*/
public <K> void sync(Function<? super T, ? extends K> keyMapper, SyncCallback<T> callback) {
if (keyMapper == null) {
throw new BadRequestException("参数 keyMapper 必填");
}
if (callback == null) {
throw new BadRequestException("参数 callback 必填");
}
// 如果本地数据为空,直接全量保存对照数据
if (this.localRecords.isEmpty()) {
callback.doSaveBatch(this.compareRecords);
return;
}
// 构建本地数据的键值映射
Map<K, T> localMap = localRecords.stream().collect(Collectors.toMap(keyMapper, i -> i));
List<T> insertList = new ArrayList<>();
List<T> updateCompareList = new ArrayList<>();
List<T> updateLocalList = new ArrayList<>();
List<T> deleteList = new ArrayList<>();
// 遍历对照数据,识别新增和更新
for (T compareRecord : compareRecords) {
K key = keyMapper.apply(compareRecord);
T local = localMap.get(key);
if (local == null) {
// 本地不存在,需要新增
insertList.add(compareRecord);
} else {
// 本地已存在,需要更新
updateLocalList.add(local);
updateCompareList.add(compareRecord);
// 标记为已处理
localMap.remove(key);
}
}
// 剩余的就是本地存在但对照组中不存在的,需要删除
for (Map.Entry<K, T> entry : localMap.entrySet()) {
deleteList.add(entry.getValue());
}
// 执行批量操作
if (CollUtil.isNotEmpty(insertList)) {
callback.doSaveBatch(insertList);
}
if (CollUtil.isNotEmpty(updateLocalList)) {
callback.doUpdateBatch(updateLocalList, updateCompareList);
}
if (CollUtil.isNotEmpty(deleteList)) {
callback.doDeleteBatch(deleteList);
}
}
/**
* 使用示例
*/
public static void main(String[] args) {
List<KitBaseUserTimeTb> localList = new ArrayList<>();
List<KitBaseUserTimeTb> onlineList = new ArrayList<>();
KitDiffSyncConstTool<KitBaseUserTimeTb> diffSyncTool = new KitDiffSyncConstTool<>(localList, onlineList);
diffSyncTool.sync(KitBaseUserTimeTb::getCreateBy, new SyncCallback<>() {
@Override
public void doSaveBatch(List<KitBaseUserTimeTb> records) {
// 全量保存
}
@Override
public void doUpdateBatch(List<KitBaseUserTimeTb> localList, List<KitBaseUserTimeTb> compareList) {
// 将 compareList 中的数据,填充到对应的 localList 数据中,更新 localList 数据
}
@Override
public void doDeleteBatch(List<KitBaseUserTimeTb> records) {
// 条件删除
}
});
}
}
@@ -0,0 +1,4 @@
/**
* 存放常用的业务工具
*/
package cn.odboy.constool;
@@ -27,7 +27,7 @@ import lombok.experimental.UtilityClass;
* @author odboy
*/
@UtilityClass
public class KitDynamicTableUtil {
public class KitDynamicTableBuilder {
/**
* 渲染排序规则
@@ -37,7 +37,7 @@ public class KitDynamicTableUtil {
* @param <T> 渲染对象
* @return /
*/
public static <M, T> QueryWrapper<M> renderOrder(KitPageArgs<T> pageArgs) {
public static <M, T> QueryWrapper<M> ofOrder(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = new QueryWrapper<>();
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
@@ -54,7 +54,7 @@ public class KitDynamicTableUtil {
* @param <T> 渲染对象
* @return /
*/
public static <M, T> LambdaQueryWrapper<M> renderAll(KitPageArgs<T> pageArgs) {
public static <M, T> LambdaQueryWrapper<M> ofAll(KitPageArgs<T> pageArgs) {
QueryWrapper<M> wrapper = KitMpQUtil.build(pageArgs.getArgs());
KitPageArgs.OrderBy orderBy = pageArgs.getOrderBy();
if (orderBy != null) {
@@ -25,74 +25,110 @@ import lombok.extern.slf4j.Slf4j;
@UtilityClass
public final class KitStringUtil {
private static final char SEPARATOR = '_';
private static final char SEPARATOR = '_';
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCamelCase(String s) {
if (s == null) {
return null;
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCamelCase(String s) {
if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCapitalizeCamelCase(String s) {
if (s == null) {
return null;
}
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
return sb.toString();
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCapitalizeCamelCase(String s) {
if (s == null) {
return null;
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toUnderScoreCase(String s) {
if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
/**
* 格式化字节数
*
* @param storage 字节数
* @return /
*/
private String renderBytes(Long storage) {
if (storage == null || storage <= 0) {
return "0 Byte";
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld" toCapitalizeCamelCase("hello_world") == "HelloWorld" toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toUnderScoreCase(String s) {
if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
double value = storage.doubleValue();
final long KB = 1024L;
final long MB = KB * 1024L;
final long GB = MB * 1024L;
final long TB = GB * 1024L;
final long PB = TB * 1024L;
if (value >= PB) {
return String.format("%.2f PB", value / PB);
} else if (value >= TB) {
return String.format("%.2f TB", value / TB);
} else if (value >= GB) {
return String.format("%.2f GB", value / GB);
} else if (value >= MB) {
return String.format("%.2f MB", value / MB);
} else if (value >= KB) {
return String.format("%.2f KB", value / KB);
} else {
return String.format("%d Byte", storage);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
}