From f9cff00b2fd25e4c7d879930ad59f109403c1272 Mon Sep 17 00:00:00 2001 From: liushuang Date: Thu, 16 Oct 2025 16:06:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=96=B0=E5=A2=9EMap=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=86=99=E5=85=A5=E9=A3=9E=E4=B9=A6=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E5=8A=9F=E8=83=BD-=20=E6=96=B0=E5=A2=9EMapWriteBuilder?= =?UTF-8?q?=E7=B1=BB=EF=BC=8C=E6=94=AF=E6=8C=81=E9=93=BE=E5=BC=8F=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E5=86=99=E5=85=A5Map=E6=A0=BC=E5=BC=8F=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=20-=20=E6=96=B0=E5=A2=9EMapTableConfig=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=B1=BB=EF=BC=8C=E7=94=A8=E4=BA=8E=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=E6=B3=A8=E8=A7=A3=E9=85=8D=E7=BD=AE=20-=20=E6=96=B0=E5=A2=9EMa?= =?UTF-8?q?pDataUtil=E5=B7=A5=E5=85=B7=E7=B1=BB=EF=BC=8C=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E5=94=AF=E4=B8=80ID=E8=AE=A1=E7=AE=97=E5=92=8C=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=98=A0=E5=B0=84=E5=8A=9F=E8=83=BD-=20=E5=9C=A8FsHel?= =?UTF-8?q?per=E4=B8=AD=E6=96=B0=E5=A2=9EwriteMap=E5=92=8CwriteMapBuilder?= =?UTF-8?q?=E6=96=B9=E6=B3=95=20-=20=E5=9C=A8WriteBuilder=E4=B8=AD?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=A9=BA=E5=AD=97=E6=AE=B5=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91=20-=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=BD=8D=E7=BD=AE=E6=98=A0=E5=B0=84=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E9=81=BF=E5=85=8D=E7=A9=BA=E6=8C=87=E9=92=88?= =?UTF-8?q?=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/cn/isliu/FsHelper.java | 82 ++++ .../isliu/core/builder/MapWriteBuilder.java | 390 ++++++++++++++++++ .../cn/isliu/core/builder/WriteBuilder.java | 10 +- .../cn/isliu/core/config/MapTableConfig.java | 290 +++++++++++++ .../java/cn/isliu/core/utils/MapDataUtil.java | 134 ++++++ 5 files changed, 905 insertions(+), 1 deletion(-) create mode 100644 src/main/java/cn/isliu/core/builder/MapWriteBuilder.java create mode 100644 src/main/java/cn/isliu/core/config/MapTableConfig.java create mode 100644 src/main/java/cn/isliu/core/utils/MapDataUtil.java diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java index d422f31..ae59344 100644 --- a/src/main/java/cn/isliu/FsHelper.java +++ b/src/main/java/cn/isliu/FsHelper.java @@ -5,11 +5,13 @@ import cn.isliu.core.FileData; import cn.isliu.core.FsTableData; import cn.isliu.core.Sheet; import cn.isliu.core.annotation.TableConf; +import cn.isliu.core.builder.MapWriteBuilder; import cn.isliu.core.builder.ReadBuilder; import cn.isliu.core.builder.SheetBuilder; import cn.isliu.core.builder.WriteBuilder; import cn.isliu.core.client.FeishuClient; import cn.isliu.core.client.FsClient; +import cn.isliu.core.config.MapTableConfig; import cn.isliu.core.enums.ErrorCode; import cn.isliu.core.enums.FileType; import cn.isliu.core.logging.FsLogger; @@ -216,6 +218,11 @@ public class FsHelper { rowNum.set(rowNum.get() + 1); values.forEach((field, fieldValue) -> { String position = titlePostionMap.get(field); + + if (position == null || position.isEmpty()) { + return; + } + if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; String fileType = fileData.getFileType(); @@ -236,6 +243,11 @@ public class FsHelper { int rowCou = rowCount.incrementAndGet(); values.forEach((field, fieldValue) -> { String position = titlePostionMap.get(field); + + if (position == null || position.isEmpty()) { + return; + } + if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; fileData.setSheetId(sheetId); @@ -286,4 +298,74 @@ public class FsHelper { public static WriteBuilder writeBuilder(String sheetId, String spreadsheetToken, List dataList) { return new WriteBuilder<>(sheetId, spreadsheetToken, dataList); } + + /** + * 将 Map 数据写入飞书表格 + * + * 直接使用 Map 格式的数据写入飞书表格,无需定义实体类和注解。 + * 适用于动态字段、临时数据写入等场景。 + * + * 使用示例: + *
+     * List<Map<String, Object>> dataList = new ArrayList<>();
+     * Map<String, Object> data = new HashMap<>();
+     * data.put("字段名1", "值1");
+     * data.put("字段名2", "值2");
+     * dataList.add(data);
+     *
+     * MapTableConfig config = MapTableConfig.builder()
+     *     .titleRow(2)
+     *     .headLine(3)
+     *     .addUniKeyName("字段名1")
+     *     .enableCover(true)
+     *     .build();
+     *
+     * FsHelper.writeMap(sheetId, spreadsheetToken, dataList, config);
+     * 
+ * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + * @param dataList Map数据列表,每个Map的key为字段名,value为字段值 + * @param config 表格配置,包含标题行、数据起始行、唯一键等配置信息 + * @return 写入操作结果 + */ + public static Object writeMap(String sheetId, String spreadsheetToken, + List> dataList, MapTableConfig config) { + return new MapWriteBuilder(sheetId, spreadsheetToken, dataList) + .config(config) + .build(); + } + + /** + * 创建 Map 数据写入构建器 + * + * 返回一个 Map 数据写入构建器实例,支持链式调用和灵活配置。 + * 相比直接使用 writeMap 方法,构建器方式提供了更灵活的配置方式。 + * + * 使用示例: + *
+     * List<Map<String, Object>> dataList = new ArrayList<>();
+     * Map<String, Object> data = new HashMap<>();
+     * data.put("字段名1", "值1");
+     * data.put("字段名2", "值2");
+     * dataList.add(data);
+     *
+     * FsHelper.writeMapBuilder(sheetId, spreadsheetToken, dataList)
+     *     .titleRow(2)
+     *     .headLine(3)
+     *     .addUniKeyName("字段名1")
+     *     .enableCover(true)
+     *     .ignoreNotFound(false)
+     *     .build();
+     * 
+ * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + * @param dataList 要写入的Map数据列表,每个Map的key为字段名,value为字段值 + * @return MapWriteBuilder实例,支持链式调用 + */ + public static MapWriteBuilder writeMapBuilder(String sheetId, String spreadsheetToken, + List> dataList) { + return new MapWriteBuilder(sheetId, spreadsheetToken, dataList); + } } \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/builder/MapWriteBuilder.java b/src/main/java/cn/isliu/core/builder/MapWriteBuilder.java new file mode 100644 index 0000000..adeaf50 --- /dev/null +++ b/src/main/java/cn/isliu/core/builder/MapWriteBuilder.java @@ -0,0 +1,390 @@ +package cn.isliu.core.builder; + +import cn.isliu.core.*; +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.client.FsClient; +import cn.isliu.core.config.MapTableConfig; +import cn.isliu.core.enums.ErrorCode; +import cn.isliu.core.enums.FileType; +import cn.isliu.core.logging.FsLogger; +import cn.isliu.core.service.CustomValueService; +import cn.isliu.core.utils.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static cn.isliu.core.utils.FsTableUtil.*; + +/** + * Map 数据写入构建器 + * + * 提供链式调用方式写入 Map 格式的飞书表格数据 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapWriteBuilder { + + private final String sheetId; + private final String spreadsheetToken; + private final List> dataList; + private MapTableConfig config; + + /** + * 构造函数 + * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + * @param dataList 要写入的Map数据列表 + */ + public MapWriteBuilder(String sheetId, String spreadsheetToken, List> dataList) { + this.sheetId = sheetId; + this.spreadsheetToken = spreadsheetToken; + this.dataList = dataList; + this.config = MapTableConfig.createDefault(); + } + + /** + * 设置表格配置 + * + * @param config Map表格配置 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder config(MapTableConfig config) { + this.config = config; + return this; + } + + /** + * 设置标题行 + * + * @param titleRow 标题行行号 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder titleRow(int titleRow) { + this.config.setTitleRow(titleRow); + return this; + } + + /** + * 设置数据起始行 + * + * @param headLine 数据起始行号 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder headLine(int headLine) { + this.config.setHeadLine(headLine); + return this; + } + + /** + * 设置唯一键字段 + * + * @param uniKeyNames 唯一键字段名集合 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder uniKeyNames(Set uniKeyNames) { + this.config.setUniKeyNames(uniKeyNames); + return this; + } + + /** + * 添加唯一键字段 + * + * @param uniKeyName 唯一键字段名 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder addUniKeyName(String uniKeyName) { + this.config.addUniKeyName(uniKeyName); + return this; + } + + /** + * 设置是否覆盖已存在数据 + * + * @param enableCover 是否覆盖 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder enableCover(boolean enableCover) { + this.config.setEnableCover(enableCover); + return this; + } + + /** + * 设置是否忽略未找到的数据 + * + * @param ignoreNotFound 是否忽略 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder ignoreNotFound(boolean ignoreNotFound) { + this.config.setIgnoreNotFound(ignoreNotFound); + return this; + } + + /** + * 执行数据写入 + * + * @return 写入操作结果 + */ + public Object build() { + if (dataList.isEmpty()) { + FsLogger.warn("【Map写入】数据列表为空,跳过写入操作"); + return null; + } + + FeishuClient client = FsClient.getInstance().getClient(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); + + // 读取表格数据以获取字段位置映射和现有数据 + Map titlePostionMap = readFieldsPositionMap(sheet, client); + config.setFieldsPositionMap(titlePostionMap); + + // 读取现有数据用于匹配和更新 + Map currTableRowMap = readExistingData(sheet, client, titlePostionMap); + + // 计算下一个可用行号 + int nextAvailableRow = calculateNextAvailableRow(currTableRowMap, config.getHeadLine()); + + // 初始化批量插入对象 + CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = + CustomValueService.ValueRequest.batchPutValues(); + + List fileDataList = new ArrayList<>(); + AtomicInteger rowCount = new AtomicInteger(nextAvailableRow); + + // 处理每条数据 + for (Map data : dataList) { + String uniqueId = MapDataUtil.calculateUniqueId(data, config); + + AtomicReference rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); + + if (uniqueId != null && rowNum.get() != null) { + // 更新现有行 + rowNum.set(rowNum.get() + 1); + processDataRow(data, titlePostionMap, rowNum.get(), resultValuesBuilder, + fileDataList, config.isEnableCover()); + } else if (!config.isIgnoreNotFound()) { + // 插入新行 + int newRow = rowCount.incrementAndGet(); + processDataRow(data, titlePostionMap, newRow, resultValuesBuilder, + fileDataList, config.isEnableCover()); + } + } + + // 检查是否需要扩展行数 + ensureSufficientRows(sheet, rowCount.get(), client); + + // 上传文件 + uploadFiles(fileDataList, client); + + // 批量写入数据 + return batchWriteValues(resultValuesBuilder, client); + } + + /** + * 读取字段位置映射 + */ + private Map readFieldsPositionMap(Sheet sheet, FeishuClient client) { + int titleRow = config.getTitleRow(); + int colCount = sheet.getGridProperties().getColumnCount(); + + // 读取标题行数据 + ValuesBatch valuesBatch = FsApiUtil.getSheetData( + sheet.getSheetId(), spreadsheetToken, + "A" + titleRow, + getColumnName(colCount - 1) + titleRow, + client + ); + + Map fieldsPositionMap = new HashMap<>(); + + if (valuesBatch != null && valuesBatch.getValueRanges() != null) { + for (ValueRange valueRange : valuesBatch.getValueRanges()) { + if (valueRange.getValues() != null && !valueRange.getValues().isEmpty()) { + List titleRowValues = valueRange.getValues().get(0); + for (int i = 0; i < titleRowValues.size(); i++) { + Object value = titleRowValues.get(i); + if (value != null) { + String fieldName = value.toString(); + String columnPosition = getColumnName(i); + fieldsPositionMap.put(fieldName, columnPosition); + } + } + } + } + } + + return fieldsPositionMap; + } + + /** + * 读取现有数据 + */ + private Map readExistingData(Sheet sheet, FeishuClient client, Map titlePostionMap) { + int headLine = config.getHeadLine(); + int titleRow = config.getTitleRow(); + int totalRow = sheet.getGridProperties().getRowCount(); + int colCount = sheet.getGridProperties().getColumnCount(); + int startOffset = 1; + + // 批量读取数据 + int rowCountPerBatch = Math.min(totalRow, 100); + int actualRows = Math.max(0, totalRow - startOffset); + int batchCount = (actualRows + rowCountPerBatch - 1) / rowCountPerBatch; + + List> values = new LinkedList<>(); + for (int i = 0; i < batchCount; i++) { + int startRowIndex = startOffset + i * rowCountPerBatch; + int endRowIndex = Math.min(startRowIndex + rowCountPerBatch - 1, totalRow - 1); + + ValuesBatch valuesBatch = FsApiUtil.getSheetData( + sheet.getSheetId(), spreadsheetToken, + "A" + startRowIndex, + getColumnName(colCount - 1) + endRowIndex, + client + ); + + if (valuesBatch != null && valuesBatch.getValueRanges() != null) { + for (ValueRange valueRange : valuesBatch.getValueRanges()) { + if (valueRange.getValues() != null) { + values.addAll(valueRange.getValues()); + } + } + } + } + + // 处理表格数据 + TableData tableData = processSheetData(sheet, values); + List dataList = getFsTableData(tableData, new ArrayList<>()); + + // 获取标题映射 + Map titleMap = new HashMap<>(); + dataList.stream() + .filter(d -> d.getRow() == (titleRow - 1)) + .findFirst() + .ifPresent(d -> { + Map map = (Map) d.getData(); + titleMap.putAll(map); + }); + + // 转换为带字段名的数据,并计算唯一ID + return dataList.stream() + .filter(fsTableData -> fsTableData.getRow() >= headLine) + .map(item -> { + Map resultMap = new HashMap<>(); + Map map = (Map) item.getData(); + + map.forEach((k, v) -> { + String title = titleMap.get(k); + if (title != null) { + resultMap.put(title, v); + } + }); + + String uniqueId = MapDataUtil.calculateUniqueId(resultMap, config); + item.setUniqueId(uniqueId); + item.setFieldsPositionMap(titlePostionMap); + return item; + }) + .filter(item -> item.getUniqueId() != null) + .collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow, (v1, v2) -> v1)); + } + + /** + * 计算下一个可用行号 + */ + private int calculateNextAvailableRow(Map currTableRowMap, int headLine) { + if (currTableRowMap.isEmpty()) { + return headLine; + } + + return currTableRowMap.values().stream() + .max(Integer::compareTo) + .map(maxRow -> maxRow + 1) + .orElse(headLine); + } + + /** + * 处理单行数据 + */ + private void processDataRow(Map data, Map titlePostionMap, + int rowNum, CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder, + List fileDataList, boolean enableCover) { + data.forEach((field, fieldValue) -> { + String position = titlePostionMap.get(field); + + if (position == null || position.isEmpty()) { + return; + } + + // 处理文件数据 + if (fieldValue instanceof FileData) { + FileData fileData = (FileData) fieldValue; + String fileType = fileData.getFileType(); + if (fileType.equals(FileType.IMAGE.getType())) { + fileData.setSheetId(sheetId); + fileData.setSpreadsheetToken(spreadsheetToken); + fileData.setPosition(position + rowNum); + fileDataList.add(fileData); + } + } + + // 添加到批量写入 + if (enableCover || fieldValue != null) { + resultValuesBuilder.addRange(sheetId, position + rowNum, position + rowNum) + .addRow(GenerateUtil.getRowData(fieldValue)); + } + }); + } + + /** + * 确保行数足够 + */ + private void ensureSufficientRows(Sheet sheet, int requiredRows, FeishuClient client) { + int rowTotal = sheet.getGridProperties().getRowCount(); + if (requiredRows >= rowTotal) { + FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", requiredRows - rowTotal, client); + } + } + + /** + * 上传文件 + */ + private void uploadFiles(List fileDataList, FeishuClient client) { + fileDataList.forEach(fileData -> { + try { + FsApiUtil.imageUpload( + fileData.getImageData(), + fileData.getFileName(), + fileData.getPosition(), + fileData.getSheetId(), + fileData.getSpreadsheetToken(), + client + ); + } catch (Exception e) { + FsLogger.error(ErrorCode.BUSINESS_LOGIC_ERROR, + "【飞书表格】Map写入-文件上传异常! " + fileData.getFileUrl()); + } + }); + } + + /** + * 批量写入数据 + */ + private Object batchWriteValues(CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder, + FeishuClient client) { + CustomValueService.ValueRequest build = resultValuesBuilder.build(); + CustomValueService.ValueBatchUpdatePutRequest batchPutValues = build.getBatchPutValues(); + List valueRanges = batchPutValues.getValueRanges(); + + if (valueRanges != null && !valueRanges.isEmpty()) { + return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, build, client); + } + + FsLogger.warn("【Map写入】没有数据需要写入"); + return null; + } +} + diff --git a/src/main/java/cn/isliu/core/builder/WriteBuilder.java b/src/main/java/cn/isliu/core/builder/WriteBuilder.java index 51e537c..a2fe033 100644 --- a/src/main/java/cn/isliu/core/builder/WriteBuilder.java +++ b/src/main/java/cn/isliu/core/builder/WriteBuilder.java @@ -198,6 +198,10 @@ public class WriteBuilder { values.forEach((field, fieldValue) -> { String position = finalTitlePostionMap.get(field); + if (position == null || position.isEmpty()) { + return; + } + if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; String fileType = fileData.getFileType(); @@ -217,8 +221,12 @@ public class WriteBuilder { int rowCou = rowCount.incrementAndGet(); Map finalTitlePostionMap1 = titlePostionMap; values.forEach((field, fieldValue) -> { - String position = finalTitlePostionMap1.get(field); + + if (position == null || position.isEmpty()) { + return; + } + if (fieldValue instanceof FileData) { FileData fileData = (FileData) fieldValue; fileData.setSheetId(sheetId); diff --git a/src/main/java/cn/isliu/core/config/MapTableConfig.java b/src/main/java/cn/isliu/core/config/MapTableConfig.java new file mode 100644 index 0000000..dc3b7a8 --- /dev/null +++ b/src/main/java/cn/isliu/core/config/MapTableConfig.java @@ -0,0 +1,290 @@ +package cn.isliu.core.config; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Map方式表格配置类 + * + * 用于替代注解配置,支持使用Map方式操作飞书表格 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapTableConfig { + + /** + * 标题行行号(从1开始) + */ + private int titleRow = 1; + + /** + * 数据起始行行号(从1开始) + */ + private int headLine = 1; + + /** + * 唯一键字段名列表 + */ + private Set uniKeyNames = new HashSet<>(); + + /** + * 是否覆盖已存在数据 + */ + private boolean enableCover = false; + + /** + * 是否忽略未找到的数据 + */ + private boolean ignoreNotFound = false; + + /** + * 字段位置映射 (字段名 -> 列位置,如 "添加SPU" -> "A") + */ + private Map fieldsPositionMap = new HashMap<>(); + + /** + * 获取标题行行号 + * + * @return 标题行行号 + */ + public int getTitleRow() { + return titleRow; + } + + /** + * 设置标题行行号 + * + * @param titleRow 标题行行号 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig setTitleRow(int titleRow) { + this.titleRow = titleRow; + return this; + } + + /** + * 获取数据起始行行号 + * + * @return 数据起始行行号 + */ + public int getHeadLine() { + return headLine; + } + + /** + * 设置数据起始行行号 + * + * @param headLine 数据起始行行号 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig setHeadLine(int headLine) { + this.headLine = headLine; + return this; + } + + /** + * 获取唯一键字段名集合 + * + * @return 唯一键字段名集合 + */ + public Set getUniKeyNames() { + return uniKeyNames; + } + + /** + * 设置唯一键字段名集合 + * + * @param uniKeyNames 唯一键字段名集合 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig setUniKeyNames(Set uniKeyNames) { + this.uniKeyNames = uniKeyNames; + return this; + } + + /** + * 添加唯一键字段名 + * + * @param uniKeyName 唯一键字段名 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig addUniKeyName(String uniKeyName) { + this.uniKeyNames.add(uniKeyName); + return this; + } + + /** + * 是否覆盖已存在数据 + * + * @return true表示覆盖,false表示不覆盖 + */ + public boolean isEnableCover() { + return enableCover; + } + + /** + * 设置是否覆盖已存在数据 + * + * @param enableCover true表示覆盖,false表示不覆盖 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig setEnableCover(boolean enableCover) { + this.enableCover = enableCover; + return this; + } + + /** + * 是否忽略未找到的数据 + * + * @return true表示忽略,false表示不忽略 + */ + public boolean isIgnoreNotFound() { + return ignoreNotFound; + } + + /** + * 设置是否忽略未找到的数据 + * + * @param ignoreNotFound true表示忽略,false表示不忽略 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig setIgnoreNotFound(boolean ignoreNotFound) { + this.ignoreNotFound = ignoreNotFound; + return this; + } + + /** + * 获取字段位置映射 + * + * @return 字段位置映射 + */ + public Map getFieldsPositionMap() { + return fieldsPositionMap; + } + + /** + * 设置字段位置映射 + * + * @param fieldsPositionMap 字段位置映射 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig setFieldsPositionMap(Map fieldsPositionMap) { + this.fieldsPositionMap = fieldsPositionMap; + return this; + } + + /** + * 创建默认配置 + * + * @return 默认配置实例 + */ + public static MapTableConfig createDefault() { + return new MapTableConfig(); + } + + /** + * 创建配置构建器 + * + * @return 配置构建器实例 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * 配置构建器 + */ + public static class Builder { + private final MapTableConfig config = new MapTableConfig(); + + /** + * 设置标题行行号 + * + * @param titleRow 标题行行号 + * @return Builder实例 + */ + public Builder titleRow(int titleRow) { + config.titleRow = titleRow; + return this; + } + + /** + * 设置数据起始行行号 + * + * @param headLine 数据起始行行号 + * @return Builder实例 + */ + public Builder headLine(int headLine) { + config.headLine = headLine; + return this; + } + + /** + * 设置唯一键字段名集合 + * + * @param uniKeyNames 唯一键字段名集合 + * @return Builder实例 + */ + public Builder uniKeyNames(Set uniKeyNames) { + config.uniKeyNames = new HashSet<>(uniKeyNames); + return this; + } + + /** + * 添加唯一键字段名 + * + * @param uniKeyName 唯一键字段名 + * @return Builder实例 + */ + public Builder addUniKeyName(String uniKeyName) { + config.uniKeyNames.add(uniKeyName); + return this; + } + + /** + * 设置是否覆盖已存在数据 + * + * @param enableCover true表示覆盖,false表示不覆盖 + * @return Builder实例 + */ + public Builder enableCover(boolean enableCover) { + config.enableCover = enableCover; + return this; + } + + /** + * 设置是否忽略未找到的数据 + * + * @param ignoreNotFound true表示忽略,false表示不忽略 + * @return Builder实例 + */ + public Builder ignoreNotFound(boolean ignoreNotFound) { + config.ignoreNotFound = ignoreNotFound; + return this; + } + + /** + * 设置字段位置映射 + * + * @param fieldsPositionMap 字段位置映射 + * @return Builder实例 + */ + public Builder fieldsPositionMap(Map fieldsPositionMap) { + config.fieldsPositionMap = new HashMap<>(fieldsPositionMap); + return this; + } + + /** + * 构建配置对象 + * + * @return MapTableConfig实例 + */ + public MapTableConfig build() { + return config; + } + } +} + diff --git a/src/main/java/cn/isliu/core/utils/MapDataUtil.java b/src/main/java/cn/isliu/core/utils/MapDataUtil.java new file mode 100644 index 0000000..29a7835 --- /dev/null +++ b/src/main/java/cn/isliu/core/utils/MapDataUtil.java @@ -0,0 +1,134 @@ +package cn.isliu.core.utils; + +import cn.isliu.core.config.MapTableConfig; + +import java.util.*; + +/** + * Map 数据处理工具类 + * + * 提供Map格式数据的处理方法,包括唯一ID计算、字段位置映射等 + * + * @author Ls + * @since 2025-10-16 + */ +public class MapDataUtil { + + /** + * 计算 Map 数据的唯一ID + * + * 根据配置的唯一键字段计算数据的唯一标识。 + * 如果没有指定唯一键,则使用所有字段计算。 + * + * @param data Map数据 + * @param config 表格配置 + * @return 唯一ID(SHA256哈希值) + */ + public static String calculateUniqueId(Map data, MapTableConfig config) { + if (data == null || data.isEmpty()) { + return null; + } + + Set uniKeyNames = config.getUniKeyNames(); + + if (uniKeyNames == null || uniKeyNames.isEmpty()) { + // 如果没有指定唯一键,使用所有字段计算 + String jsonStr = StringUtil.mapToJson(data); + return StringUtil.getSHA256(jsonStr); + } + + // 使用指定的唯一键字段计算 + List uniKeyValues = new ArrayList<>(); + for (String key : uniKeyNames) { + if (data.containsKey(key)) { + uniKeyValues.add(data.get(key)); + } + } + + if (uniKeyValues.isEmpty()) { + return null; + } + + String jsonStr = StringUtil.listToJson(uniKeyValues); + return StringUtil.getSHA256(jsonStr); + } + + /** + * 从标题行数据构建字段位置映射 + * + * 将标题行的列位置与字段名称进行映射,用于后续数据写入时定位单元格位置。 + * + * @param titleRowData 标题行数据,key为列位置(如"A"、"B"),value为字段名称 + * @return 字段位置映射,key为字段名称,value为列位置 + */ + public static Map buildFieldsPositionMap(Map titleRowData) { + if (titleRowData == null || titleRowData.isEmpty()) { + return new HashMap<>(); + } + + Map fieldsPositionMap = new HashMap<>(); + titleRowData.forEach((position, fieldName) -> { + if (fieldName != null && !fieldName.isEmpty()) { + fieldsPositionMap.put(fieldName, position); + } + }); + return fieldsPositionMap; + } + + /** + * 验证数据字段是否与表格字段匹配 + * + * 检查数据中的字段是否在表格的字段位置映射中存在。 + * + * @param data 数据Map + * @param fieldsPositionMap 字段位置映射 + * @return 未匹配的字段列表 + */ + public static List validateDataFields(Map data, Map fieldsPositionMap) { + if (data == null || data.isEmpty()) { + return new ArrayList<>(); + } + + if (fieldsPositionMap == null || fieldsPositionMap.isEmpty()) { + return new ArrayList<>(data.keySet()); + } + + List unmatchedFields = new ArrayList<>(); + for (String fieldName : data.keySet()) { + if (!fieldsPositionMap.containsKey(fieldName)) { + unmatchedFields.add(fieldName); + } + } + + return unmatchedFields; + } + + /** + * 过滤数据字段 + * + * 只保留在字段位置映射中存在的字段。 + * + * @param data 原始数据Map + * @param fieldsPositionMap 字段位置映射 + * @return 过滤后的数据Map + */ + public static Map filterDataFields(Map data, Map fieldsPositionMap) { + if (data == null || data.isEmpty()) { + return new HashMap<>(); + } + + if (fieldsPositionMap == null || fieldsPositionMap.isEmpty()) { + return new HashMap<>(); + } + + Map filteredData = new HashMap<>(); + data.forEach((fieldName, fieldValue) -> { + if (fieldsPositionMap.containsKey(fieldName)) { + filteredData.put(fieldName, fieldValue); + } + }); + + return filteredData; + } +} +