From 7ad1060adf8f2745213b642e00185ac67e890501 Mon Sep 17 00:00:00 2001 From: liushuang Date: Wed, 5 Nov 2025 11:32:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=94=AF=E6=8C=81=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E6=95=B0=E6=8D=AE=20Upsert=20=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 MapTableConfig 和 MapSheetConfig 中新增 upsert 配置项,控制是否启用 Upsert 模式 - MapWriteBuilder 新增 upsert 方法用于设置 Upsert 模式开关- 实现根据 upsert 配置决定是否读取现有数据进行匹配更新或直接追加写入 - 添加分组字段支持,允许按指定字段对数据进行分组处理 -优化数据写入逻辑,区分 Upsert 模式和纯追加模式的数据读取与行号计算 - 增强异常处理和日志记录,提升系统稳定性与可维护性 --- .../cn/isliu/core/annotation/TableConf.java | 11 + .../isliu/core/builder/MapSheetBuilder.java | 193 ++++---- .../isliu/core/builder/MapWriteBuilder.java | 458 +++++++++++++----- .../cn/isliu/core/builder/WriteBuilder.java | 143 ++++-- .../cn/isliu/core/config/MapSheetConfig.java | 84 ++-- .../cn/isliu/core/config/MapTableConfig.java | 105 ++-- .../cn/isliu/core/utils/PropertyUtil.java | 6 + 7 files changed, 673 insertions(+), 327 deletions(-) diff --git a/src/main/java/cn/isliu/core/annotation/TableConf.java b/src/main/java/cn/isliu/core/annotation/TableConf.java index cfc6be8..a9f3d27 100644 --- a/src/main/java/cn/isliu/core/annotation/TableConf.java +++ b/src/main/java/cn/isliu/core/annotation/TableConf.java @@ -65,4 +65,15 @@ public @interface TableConf { * @return 背景颜色 */ String headBackColor() default "#cccccc"; + + + /** + * 是否启用 Upsert 模式 + * + * true(默认):根据唯一键匹配,存在则更新,不存在则追加 + * false:不匹配唯一键,所有数据直接追加到表格末尾 + * + * @return 是否启用 Upsert 模式 + */ + boolean upsert() default true; } \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/builder/MapSheetBuilder.java b/src/main/java/cn/isliu/core/builder/MapSheetBuilder.java index d113b62..3180d4c 100644 --- a/src/main/java/cn/isliu/core/builder/MapSheetBuilder.java +++ b/src/main/java/cn/isliu/core/builder/MapSheetBuilder.java @@ -31,11 +31,11 @@ import java.util.stream.Collectors; * @since 2025-10-16 */ public class MapSheetBuilder { - + private final String sheetName; private final String spreadsheetToken; private MapSheetConfig config; - + /** * 构造函数 * @@ -47,7 +47,7 @@ public class MapSheetBuilder { this.spreadsheetToken = spreadsheetToken; this.config = MapSheetConfig.createDefault(); } - + /** * 设置表格配置 * @@ -58,7 +58,7 @@ public class MapSheetBuilder { this.config = config; return this; } - + /** * 设置标题行 * @@ -69,7 +69,7 @@ public class MapSheetBuilder { this.config.setTitleRow(titleRow); return this; } - + /** * 设置数据起始行 * @@ -80,7 +80,7 @@ public class MapSheetBuilder { this.config.setHeadLine(headLine); return this; } - + /** * 设置唯一键字段 * @@ -91,7 +91,7 @@ public class MapSheetBuilder { this.config.setUniKeyNames(uniKeyNames); return this; } - + /** * 添加唯一键字段 * @@ -102,7 +102,7 @@ public class MapSheetBuilder { this.config.addUniKeyName(uniKeyName); return this; } - + /** * 设置字段列表 * @@ -113,7 +113,7 @@ public class MapSheetBuilder { this.config.setFields(new ArrayList<>(fields)); return this; } - + /** * 添加单个字段 * @@ -124,7 +124,7 @@ public class MapSheetBuilder { this.config.addField(field); return this; } - + /** * 批量添加字段 * @@ -135,7 +135,7 @@ public class MapSheetBuilder { this.config.addFields(fields); return this; } - + /** * 批量添加字段(可变参数) * @@ -146,7 +146,7 @@ public class MapSheetBuilder { this.config.addFields(fields); return this; } - + /** * 设置表头样式 * @@ -159,7 +159,7 @@ public class MapSheetBuilder { this.config.setHeadBackColor(backColor); return this; } - + /** * 设置是否为纯文本格式 * @@ -170,7 +170,7 @@ public class MapSheetBuilder { this.config.setText(isText); return this; } - + /** * 设置是否启用字段描述 * @@ -181,7 +181,7 @@ public class MapSheetBuilder { this.config.setEnableDesc(enableDesc); return this; } - + /** * 设置分组字段 * @@ -192,7 +192,7 @@ public class MapSheetBuilder { this.config.setGroupFields(Arrays.asList(groupFields)); return this; } - + /** * 添加自定义属性 * @@ -204,7 +204,7 @@ public class MapSheetBuilder { this.config.addCustomProperty(key, value); return this; } - + /** * 构建表格并返回工作表ID * @@ -215,7 +215,7 @@ public class MapSheetBuilder { if (config.getFields().isEmpty()) { throw new IllegalArgumentException("字段定义列表不能为空"); } - + // 判断是否为分组表格 if (config.getGroupFields() != null && !config.getGroupFields().isEmpty()) { return buildGroupSheet(); @@ -223,145 +223,145 @@ public class MapSheetBuilder { return buildNormalSheet(); } } - + /** * 构建普通表格 */ private String buildNormalSheet() { // 转换字段定义为 FieldProperty Map fieldsMap = convertToFieldsMap(config.getFields()); - + // 生成表头 List headers = config.getFields().stream() - .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) - .map(MapFieldDefinition::getFieldName) - .collect(Collectors.toList()); - + .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) + .map(MapFieldDefinition::getFieldName) + .collect(Collectors.toList()); + // 创建 TableConf TableConf tableConf = createTableConf(); - + // 创建飞书客户端 FeishuClient client = FsClient.getInstance().getClient(); - + // 1、创建sheet String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); - + // 2、添加表头数据 Map fieldDescriptions = buildFieldDescriptions(); - FsApiUtil.putValues(spreadsheetToken, - FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), - client); - + FsApiUtil.putValues(spreadsheetToken, + FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), + client); + // 3、设置单元格为文本格式 if (config.isText()) { String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); } - + // 4、设置表格样式 FsApiUtil.setTableStyle( - FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), - client, spreadsheetToken); - + FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), + client, spreadsheetToken); + // 5、合并单元格 List mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap); if (!mergeCell.isEmpty()) { mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); } - + // 6、设置表格下拉 try { // 准备自定义属性,包含字段的 options 配置 Map customProps = prepareCustomProperties(fieldsMap); - FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, - config.isEnableDesc(), customProps); + FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, + config.isEnableDesc(), customProps); } catch (Exception e) { Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE, - "【Map表格构建器】设置表格下拉异常!sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); + "【Map表格构建器】设置表格下拉异常!sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); } - + return sheetId; } - + /** * 构建分组表格 */ private String buildGroupSheet() { // 转换字段定义为 FieldProperty Map fieldsMap = convertToFieldsMap(config.getFields()); - + // 生成表头 List headers = config.getFields().stream() - .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) - .map(MapFieldDefinition::getFieldName) - .collect(Collectors.toList()); - + .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) + .map(MapFieldDefinition::getFieldName) + .collect(Collectors.toList()); + // 创建 TableConf TableConf tableConf = createTableConf(); - + // 创建飞书客户端 FeishuClient client = FsClient.getInstance().getClient(); - + // 1、创建sheet String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); - + // 2、添加表头数据(分组模式) List groupFieldList = config.getGroupFields(); List headerList = FsTableUtil.getGroupHeaders(groupFieldList, headers); Map fieldDescriptions = buildFieldDescriptions(); FsApiUtil.putValues(spreadsheetToken, - FsTableUtil.getHeadTemplateBuilder(sheetId, headers, headerList, fieldsMap, - tableConf, fieldDescriptions, groupFieldList), - client); - + FsTableUtil.getHeadTemplateBuilder(sheetId, headers, headerList, fieldsMap, + tableConf, fieldDescriptions, groupFieldList), + client); + // 3、设置单元格为文本格式 if (config.isText()) { String column = FsTableUtil.getColumnNameByNuNumber(headerList.size()); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); } - + // 4、设置表格样式(分组模式) Map positions = FsTableUtil.calculateGroupPositions(headers, groupFieldList); - positions.forEach((key, value) -> - FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf), - client, spreadsheetToken)); - + positions.forEach((key, value) -> + FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf), + client, spreadsheetToken)); + // 5、合并单元格 List mergeCell = FsTableUtil.getMergeCell(sheetId, positions.values()); if (!mergeCell.isEmpty()) { mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); } - + // 6、设置表格下拉 try { String[] headerWithColumnIdentifiers = FsTableUtil.generateHeaderWithColumnIdentifiers(headers, groupFieldList); // 准备自定义属性,包含字段的 options 配置 Map customProps = prepareCustomProperties(fieldsMap); - FsTableUtil.setTableOptions(spreadsheetToken, headerWithColumnIdentifiers, fieldsMap, - sheetId, config.isEnableDesc(), customProps); + FsTableUtil.setTableOptions(spreadsheetToken, headerWithColumnIdentifiers, fieldsMap, + sheetId, config.isEnableDesc(), customProps); } catch (Exception e) { Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE, - "【Map表格构建器】设置表格下拉异常!sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); + "【Map表格构建器】设置表格下拉异常!sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); } - + return sheetId; } - + /** * 将 MapFieldDefinition 列表转换为 FieldProperty Map */ private Map convertToFieldsMap(List fields) { Map fieldsMap = new LinkedHashMap<>(); - + for (MapFieldDefinition field : fields) { TableProperty tableProperty = createTableProperty(field); FieldProperty fieldProperty = new FieldProperty(field.getFieldName(), tableProperty); fieldsMap.put(field.getFieldName(), fieldProperty); } - + return fieldsMap; } - + /** * 根据 MapFieldDefinition 创建 TableProperty 注解实例 */ @@ -371,67 +371,67 @@ public class MapSheetBuilder { public Class annotationType() { return TableProperty.class; } - + @Override public String[] value() { return new String[]{field.getFieldName()}; } - + @Override public String desc() { return field.getDescription() != null ? field.getDescription() : ""; } - + @Override public String field() { return field.getFieldName(); } - + @Override public int order() { return field.getOrder(); } - + @Override public TypeEnum type() { return field.getType() != null ? field.getType() : TypeEnum.TEXT; } - + @Override public Class enumClass() { // 优先级1:如果配置了 enumClass,直接返回 if (field.getEnumClass() != null && field.getEnumClass() != BaseEnum.class) { return field.getEnumClass(); } - + // 优先级2:如果没有配置 enumClass 但配置了 options,创建动态枚举类 // 注意:这里返回 BaseEnum.class,实际的 options 通过 optionsClass 处理 return BaseEnum.class; } - + @Override public Class fieldFormatClass() { return FieldValueProcess.class; } - + @Override public Class optionsClass() { // 优先级1:如果配置了 optionsClass,直接返回 if (field.getOptionsClass() != null && field.getOptionsClass() != OptionsValueProcess.class) { return field.getOptionsClass(); } - + // 优先级2:如果配置了 options 但没有 optionsClass,创建动态的处理类 if (field.getOptions() != null && !field.getOptions().isEmpty()) { return MapOptionsUtil.createDynamicOptionsClass(field.getOptions()); } - + // 优先级3:返回默认值 return OptionsValueProcess.class; } }; } - + /** * 创建 TableConf 注解实例 */ @@ -441,50 +441,56 @@ public class MapSheetBuilder { public Class annotationType() { return TableConf.class; } - + @Override public String[] uniKeys() { Set uniKeyNames = config.getUniKeyNames(); return uniKeyNames != null ? uniKeyNames.toArray(new String[0]) : new String[0]; } - + @Override public int headLine() { return config.getHeadLine(); } - + @Override public int titleRow() { return config.getTitleRow(); } - + @Override public boolean enableCover() { return config.isEnableCover(); } - + @Override public boolean isText() { return config.isText(); } - + @Override public boolean enableDesc() { return config.isEnableDesc(); } - + @Override public String headFontColor() { return config.getHeadFontColor(); } - + @Override public String headBackColor() { return config.getHeadBackColor(); } + + @Override + public boolean upsert() { + // MapSheetConfig 继承自 MapTableConfig,支持 upsert 配置 + return config.isUpsert(); + } }; } - + /** * 构建字段描述映射 */ @@ -497,20 +503,20 @@ public class MapSheetBuilder { } return descriptions; } - + /** * 准备自定义属性 - * + * * 将字段配置的 options 放入 customProperties,供 DynamicOptionsProcess 使用 */ private Map prepareCustomProperties(Map fieldsMap) { Map customProps = new HashMap<>(); - + // 复制原有的自定义属性 if (config.getCustomProperties() != null) { customProps.putAll(config.getCustomProperties()); } - + // 为每个配置了 options 的字段添加选项到 customProperties for (MapFieldDefinition field : config.getFields()) { if (field.getOptions() != null && !field.getOptions().isEmpty()) { @@ -518,8 +524,7 @@ public class MapSheetBuilder { customProps.put("_dynamicOptions_" + field.getFieldName(), field.getOptions()); } } - + return customProps; } } - diff --git a/src/main/java/cn/isliu/core/builder/MapWriteBuilder.java b/src/main/java/cn/isliu/core/builder/MapWriteBuilder.java index 6dfbea7..85c68aa 100644 --- a/src/main/java/cn/isliu/core/builder/MapWriteBuilder.java +++ b/src/main/java/cn/isliu/core/builder/MapWriteBuilder.java @@ -1,6 +1,7 @@ package cn.isliu.core.builder; import cn.isliu.core.*; +import cn.isliu.core.annotation.TableConf; import cn.isliu.core.client.FeishuClient; import cn.isliu.core.client.FsClient; import cn.isliu.core.config.MapTableConfig; @@ -9,6 +10,7 @@ import cn.isliu.core.enums.FileType; import cn.isliu.core.logging.FsLogger; import cn.isliu.core.service.CustomValueService; import cn.isliu.core.utils.*; +import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -26,12 +28,13 @@ import static cn.isliu.core.utils.FsTableUtil.*; * @since 2025-10-16 */ public class MapWriteBuilder { - + private final String sheetId; private final String spreadsheetToken; private final List> dataList; private MapTableConfig config; - + private String groupField; + /** * 构造函数 * @@ -45,7 +48,7 @@ public class MapWriteBuilder { this.dataList = dataList; this.config = MapTableConfig.createDefault(); } - + /** * 设置表格配置 * @@ -56,7 +59,7 @@ public class MapWriteBuilder { this.config = config; return this; } - + /** * 设置标题行 * @@ -67,7 +70,7 @@ public class MapWriteBuilder { this.config.setTitleRow(titleRow); return this; } - + /** * 设置数据起始行 * @@ -78,7 +81,7 @@ public class MapWriteBuilder { this.config.setHeadLine(headLine); return this; } - + /** * 设置唯一键字段 * @@ -89,7 +92,7 @@ public class MapWriteBuilder { this.config.setUniKeyNames(uniKeyNames); return this; } - + /** * 添加唯一键字段 * @@ -100,7 +103,7 @@ public class MapWriteBuilder { this.config.addUniKeyName(uniKeyName); return this; } - + /** * 设置是否覆盖已存在数据 * @@ -111,7 +114,7 @@ public class MapWriteBuilder { this.config.setEnableCover(enableCover); return this; } - + /** * 设置是否忽略未找到的数据 * @@ -122,7 +125,35 @@ public class MapWriteBuilder { this.config.setIgnoreNotFound(ignoreNotFound); return this; } - + + /** + * 设置是否启用 Upsert 模式 + * + * true(默认):根据唯一键匹配,存在则更新,不存在则追加 + * false:不匹配唯一键,所有数据直接追加到表格末尾 + * + * @param upsert true 为 Upsert 模式,false 为纯追加模式 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder upsert(boolean upsert) { + this.config.setUpsert(upsert); + return this; + } + + /** + * 设置分组字段 + * + * 配置分组字段,用于处理数据行分组。 + * 当数据行存在分组字段时,将按照分组字段进行分组,并分别处理每个分组。 + * + * @param groupField 分组字段名称 + * @return MapWriteBuilder实例 + */ + public MapWriteBuilder groupField(String groupField) { + this.groupField = groupField; + return this; + } + /** * 执行数据写入 * @@ -133,73 +164,156 @@ public class MapWriteBuilder { 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()); - + + // 根据 upsert 配置决定是否需要读取现有数据用于匹配 + Map currTableRowMap = new HashMap<>(); + int nextAvailableRow = config.getHeadLine(); + int headLine = config.getHeadLine(); + int titleRow = config.getTitleRow(); + List fsTableDataList; + + if (config.isUpsert()) { + // Upsert 模式:读取现有数据用于匹配和更新 + fsTableDataList = readExistingData(sheet, client, groupField); + + if (!fsTableDataList.isEmpty()) { + Map fieldsPositionMap = fsTableDataList.get(0).getFieldsPositionMap(); + if (fieldsPositionMap != null) { + titlePostionMap = fieldsPositionMap; + } + } + + currTableRowMap = getCurrTableRowMap(fsTableDataList, titleRow, titlePostionMap, headLine); + + nextAvailableRow = calculateNextAvailableRow(currTableRowMap, config.getHeadLine()); + } else { + // 纯追加模式:只需要读取现有数据获取最大行号 + fsTableDataList = readMaxRowNumber(sheet, client, groupField); + + if (!fsTableDataList.isEmpty()) { + Map fieldsPositionMap = fsTableDataList.get(0).getFieldsPositionMap(); + if (fieldsPositionMap != null) { + titlePostionMap = fieldsPositionMap; + } + } + + // 找到数据行中的最大行号 + int maxRow = fsTableDataList.stream() + .filter(fsTableData -> fsTableData.getRow() >= headLine) + .mapToInt(FsTableData::getRow) + .max() + .orElse(headLine - 1); + + nextAvailableRow = maxRow + 1; + } + // 初始化批量插入对象 - CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = - CustomValueService.ValueRequest.batchPutValues(); - + 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()) { - // 插入新行 + + if (config.isUpsert()) { + // Upsert 模式:计算 uniqueId 并匹配更新或追加 + 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()); + } + } + } else { + // 纯追加模式:不计算 uniqueId,所有数据直接追加到表格末尾 + for (Map data : dataList) { int newRow = rowCount.incrementAndGet(); - processDataRow(data, titlePostionMap, newRow, resultValuesBuilder, - fileDataList, config.isEnableCover()); + processDataRow(data, titlePostionMap, newRow, resultValuesBuilder, + fileDataList, config.isEnableCover()); } } - + // 检查是否需要扩展行数 ensureSufficientRows(sheet, rowCount.get(), client); - + // 上传文件 uploadFiles(fileDataList, client); - + // 批量写入数据 return batchWriteValues(resultValuesBuilder, client); } - + + @NotNull + private Map getCurrTableRowMap(List fsTableDataList, int titleRow, + Map titlePostionMap, int headLine) { + Map currTableRowMap; + // 获取标题映射 + Map titleMap = new HashMap<>(); + fsTableDataList.stream() + .filter(d -> d.getRow() == (titleRow - 1)) + .findFirst() + .ifPresent(d -> { + Map map = (Map) d.getData(); + titleMap.putAll(map); + }); + + // 转换为带字段名的数据,并计算唯一ID + currTableRowMap = fsTableDataList.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)); + return currTableRowMap; + } + /** * 读取字段位置映射 */ 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 + 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()) { @@ -215,37 +329,37 @@ public class MapWriteBuilder { } } } - + return fieldsPositionMap; } - + /** * 读取现有数据 + * + * @param groupField 分组字段名称,如果为null则读取全部数据 */ - private Map readExistingData(Sheet sheet, FeishuClient client, Map titlePostionMap) { - int headLine = config.getHeadLine(); - int titleRow = config.getTitleRow(); + private List readExistingData(Sheet sheet, FeishuClient client, String groupField) { 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 + 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) { @@ -254,44 +368,28 @@ public class MapWriteBuilder { } } } - + // 处理表格数据 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)); + + // 根据是否有分组字段,选择不同的处理方式 + List dataList; + if (groupField == null || groupField.isEmpty()) { + // 无分组:读取全部数据 + dataList = getFsTableData(tableData, new ArrayList<>()); + } else { + // 有分组:需要重新调用完整的分组读取方法 + // 创建临时的 TableConf 用于分组读取 + TableConf tempTableConf = createTempTableConf(); + Map> groupDataMap = FsTableUtil.getGroupFsTableData( + sheet, spreadsheetToken, tempTableConf, new ArrayList<>(), new HashMap<>() + ); + dataList = groupDataMap.getOrDefault(groupField, new ArrayList<>()); + } + + return dataList; } - + /** * 计算下一个可用行号 */ @@ -299,26 +397,142 @@ public class MapWriteBuilder { if (currTableRowMap.isEmpty()) { return headLine; } - + return currTableRowMap.values().stream() - .max(Integer::compareTo) - .map(maxRow -> maxRow + 1) - .orElse(headLine); + .max(Integer::compareTo) + .map(maxRow -> maxRow + 1) + .orElse(headLine); } - + + /** + * 读取表格最大行号(用于纯追加模式) + * + * 只读取数据获取最大行号,不计算 uniqueId 和构建映射表 + * + * @param groupField 分组字段名称,如果为null则读取全部数据 + */ + private List readMaxRowNumber(Sheet sheet, FeishuClient client, String groupField) { + 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; + if (groupField == null || groupField.isEmpty()) { + // 无分组:读取全部数据 + dataList = getFsTableData(tableData, new ArrayList<>()); + } else { + // 有分组:需要重新调用完整的分组读取方法 + // 创建临时的 TableConf 用于分组读取 + TableConf tempTableConf = createTempTableConf(); + Map> groupDataMap = FsTableUtil.getGroupFsTableData( + sheet, spreadsheetToken, tempTableConf, new ArrayList<>(), new HashMap<>() + ); + dataList = groupDataMap.getOrDefault(groupField, new ArrayList<>()); + } + + return dataList; + } + + /** + * 创建临时的 TableConf 对象(用于分组读取) + */ + private TableConf createTempTableConf() { + return new TableConf() { + @Override + public Class annotationType() { + return TableConf.class; + } + + @Override + public String[] uniKeys() { + return config.getUniKeyNames().toArray(new String[0]); + } + + @Override + public int headLine() { + return config.getHeadLine(); + } + + @Override + public int titleRow() { + return config.getTitleRow(); + } + + @Override + public boolean enableCover() { + return config.isEnableCover(); + } + + @Override + public boolean isText() { + return false; + } + + @Override + public boolean enableDesc() { + return false; + } + + @Override + public String headFontColor() { + return "#ffffff"; + } + + @Override + public String headBackColor() { + return "#000000"; + } + + @Override + public boolean upsert() { + return config.isUpsert(); + } + }; + } + /** * 处理单行数据 */ private void processDataRow(Map data, Map titlePostionMap, - int rowNum, CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder, - List fileDataList, boolean enableCover) { + 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; @@ -330,15 +544,15 @@ public class MapWriteBuilder { fileDataList.add(fileData); } } - + // 添加到批量写入 - if (enableCover || fieldValue != null) { + if (enableCover || (fieldValue != null && !(fieldValue instanceof FileData))) { resultValuesBuilder.addRange(sheetId, position + rowNum, position + rowNum) - .addRow(GenerateUtil.getRowData(fieldValue)); + .addRow(GenerateUtil.getRowData(fieldValue)); } }); } - + /** * 确保行数足够 */ @@ -348,7 +562,7 @@ public class MapWriteBuilder { FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", Math.abs(requiredRows - rowTotal), client); } } - + /** * 上传文件 */ @@ -356,20 +570,20 @@ public class MapWriteBuilder { fileDataList.forEach(fileData -> { try { FsApiUtil.imageUpload( - fileData.getImageData(), - fileData.getFileName(), - fileData.getPosition(), - fileData.getSheetId(), - fileData.getSpreadsheetToken(), - client + fileData.getImageData(), + fileData.getFileName(), + fileData.getPosition(), + fileData.getSheetId(), + fileData.getSpreadsheetToken(), + client ); } catch (Exception e) { - FsLogger.error(ErrorCode.BUSINESS_LOGIC_ERROR, - "【飞书表格】Map写入-文件上传异常! " + fileData.getFileUrl()); + FsLogger.error(ErrorCode.BUSINESS_LOGIC_ERROR, + "【飞书表格】Map写入-文件上传异常! " + fileData.getFileUrl()); } }); } - + /** * 批量写入数据 */ @@ -378,11 +592,11 @@ public class MapWriteBuilder { 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 f258fbf..f1ed952 100644 --- a/src/main/java/cn/isliu/core/builder/WriteBuilder.java +++ b/src/main/java/cn/isliu/core/builder/WriteBuilder.java @@ -32,6 +32,7 @@ public class WriteBuilder { private Class clazz; private boolean ignoreNotFound; private String groupField; + private Boolean upsert; /** * 构造函数 @@ -106,6 +107,22 @@ public class WriteBuilder { return this; } + /** + * 设置是否启用 Upsert 模式 + * + * 此方法设置的值会覆盖 @TableConf 注解中的配置。 + * + * true(默认):根据唯一键匹配,存在则更新,不存在则追加 + * false:不匹配唯一键,所有数据直接追加到表格末尾 + * + * @param upsert true 为 Upsert 模式,false 为纯追加模式 + * @return WriteBuilder实例,支持链式调用 + */ + public WriteBuilder upsert(boolean upsert) { + this.upsert = upsert; + return this; + } + /** * 执行数据写入并返回操作结果 * @@ -133,6 +150,10 @@ public class WriteBuilder { Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); TableConf tableConf = aClass != null ? PropertyUtil.getTableConf(aClass) : PropertyUtil.getTableConf(sourceClass); + + // 确定最终的 upsert 值:Builder 方法参数优先,否则使用注解配置 + boolean finalUpsert = (this.upsert != null) ? this.upsert : tableConf.upsert(); + Map titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf); Set keys = titlePostionMap.keySet(); @@ -161,12 +182,18 @@ public class WriteBuilder { } } - Map currTableRowMap = fsTableDataList.stream() - .filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()) - .collect(Collectors.toMap( - FsTableData::getUniqueId, - FsTableData::getRow, - (existing, replacement) -> existing)); + // 根据 finalUpsert 决定是否构建映射表 + Map currTableRowMap = new HashMap<>(); + if (finalUpsert) { + // Upsert 模式:构建 uniqueId 到行号的映射表 + currTableRowMap = fsTableDataList.stream() + .filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()) + .collect(Collectors.toMap( + FsTableData::getUniqueId, + FsTableData::getRow, + (existing, replacement) -> existing + )); + } final Integer[] row = {tableConf.headLine()}; fsTableDataList.forEach(fsTableData -> { @@ -182,54 +209,76 @@ public class WriteBuilder { AtomicInteger rowCount = new AtomicInteger(row[0]); - for (T data : dataList) { - Map values = GenerateUtil.getFieldValue(data, fieldMap); + if (finalUpsert) { + // Upsert 模式:计算 uniqueId 并匹配更新或追加 + for (T data : dataList) { + Map values = GenerateUtil.getFieldValue(data, fieldMap); - // 计算唯一标识:如果data类型与aClass相同,使用忽略字段逻辑;否则直接从data获取uniqueId - String uniqueId; - if (data.getClass().equals(aClass)) { - // 类型相同,使用忽略字段逻辑计算唯一标识 - uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, tableConf); - } else { - uniqueId = GenerateUtil.getUniqueId(data, tableConf); + // 计算唯一标识:如果data类型与aClass相同,使用忽略字段逻辑;否则直接从data获取uniqueId + String uniqueId; + if (data.getClass().equals(aClass)) { + // 类型相同,使用忽略字段逻辑计算唯一标识 + uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, tableConf); + } else { + uniqueId = GenerateUtil.getUniqueId(data, tableConf); + } + + AtomicReference rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); + if (uniqueId != null && rowNum.get() != null) { + // 找到匹配的行 → 更新 + rowNum.set(rowNum.get() + 1); + Map finalTitlePostionMap = titlePostionMap; + values.forEach((field, fieldValue) -> { + String position = finalTitlePostionMap.get(field); + + 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.get()); + fileDataList.add(fileData); + } + } + if (tableConf.enableCover() || fieldValue != null) { + resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) + .addRow(GenerateUtil.getRowData(fieldValue)); + } + }); + } else if (!ignoreNotFound) { + // 未找到 && ignoreNotFound = false → 追加 + int rowCou = rowCount.incrementAndGet(); + Map finalTitlePostionMap1 = titlePostionMap; + values.forEach((field, fieldValue) -> { + + String position = finalTitlePostionMap1.get(field); + if (fieldValue instanceof FileData) { + FileData fileData = (FileData) fieldValue; + fileData.setSheetId(sheetId); + fileData.setSpreadsheetToken(spreadsheetToken); + fileData.setPosition(position + rowCou); + fileDataList.add(fileData); + } + + if (tableConf.enableCover() || fieldValue != null) { + resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou) + .addRow(GenerateUtil.getRowData(fieldValue)); + } + }); + } + // else: 未找到 && ignoreNotFound = true → 跳过该数据 } + } else { + // 纯追加模式:不计算 uniqueId,所有数据直接追加到表格末尾 + for (T data : dataList) { + Map values = GenerateUtil.getFieldValue(data, fieldMap); - AtomicReference rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId)); - if (uniqueId != null && rowNum.get() != null) { - rowNum.set(rowNum.get() + 1); + int rowCou = rowCount.incrementAndGet(); Map finalTitlePostionMap = titlePostionMap; 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(); - if (fileType.equals(FileType.IMAGE.getType())) { - fileData.setSheetId(sheetId); - fileData.setSpreadsheetToken(spreadsheetToken); - fileData.setPosition(position + rowNum.get()); - fileDataList.add(fileData); - } - } - if (tableConf.enableCover() || fieldValue != null) { - resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get()) - .addRow(GenerateUtil.getRowData(fieldValue)); - } - }); - } else if (!ignoreNotFound) { - 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/MapSheetConfig.java b/src/main/java/cn/isliu/core/config/MapSheetConfig.java index 167eba3..ca2eb4c 100644 --- a/src/main/java/cn/isliu/core/config/MapSheetConfig.java +++ b/src/main/java/cn/isliu/core/config/MapSheetConfig.java @@ -12,27 +12,27 @@ import java.util.*; * @since 2025-10-16 */ public class MapSheetConfig extends MapTableConfig { - + /** * 字段定义列表 */ private List fields = new ArrayList<>(); - + /** * 表头字体颜色(十六进制,如 #ffffff) */ private String headFontColor = "#ffffff"; - + /** * 表头背景颜色(十六进制,如 #000000) */ private String headBackColor = "#000000"; - + /** * 是否将单元格设置为纯文本格式 */ private boolean isText = false; - + /** * 是否启用字段描述行 */ @@ -98,12 +98,12 @@ public class MapSheetConfig extends MapTableConfig { * 分组字段列表(用于创建分组表格) */ private List groupFields = new ArrayList<>(); - + /** * 自定义属性映射(用于传递额外配置) */ private Map customProperties = new HashMap<>(); - + /** * 创建默认配置 * @@ -112,7 +112,7 @@ public class MapSheetConfig extends MapTableConfig { public static MapSheetConfig createDefault() { return new MapSheetConfig(); } - + /** * 创建表格配置构建器 * @@ -121,7 +121,7 @@ public class MapSheetConfig extends MapTableConfig { public static SheetBuilder sheetBuilder() { return new SheetBuilder(); } - + /** * 添加单个字段 * @@ -132,7 +132,7 @@ public class MapSheetConfig extends MapTableConfig { this.fields.add(field); return this; } - + /** * 批量添加字段 * @@ -143,7 +143,7 @@ public class MapSheetConfig extends MapTableConfig { this.fields.addAll(fields); return this; } - + /** * 批量添加字段(可变参数) * @@ -154,7 +154,7 @@ public class MapSheetConfig extends MapTableConfig { this.fields.addAll(Arrays.asList(fields)); return this; } - + /** * 添加分组字段 * @@ -165,7 +165,7 @@ public class MapSheetConfig extends MapTableConfig { this.groupFields.add(groupField); return this; } - + /** * 添加自定义属性 * @@ -195,7 +195,7 @@ public class MapSheetConfig extends MapTableConfig { */ public static class SheetBuilder { private final MapSheetConfig config = new MapSheetConfig(); - + /** * 设置标题行行号 * @@ -206,7 +206,7 @@ public class MapSheetConfig extends MapTableConfig { config.setTitleRow(titleRow); return this; } - + /** * 设置数据起始行行号 * @@ -217,7 +217,7 @@ public class MapSheetConfig extends MapTableConfig { config.setHeadLine(headLine); return this; } - + /** * 设置唯一键字段名集合 * @@ -228,7 +228,7 @@ public class MapSheetConfig extends MapTableConfig { config.setUniKeyNames(uniKeyNames); return this; } - + /** * 添加唯一键字段名 * @@ -239,7 +239,7 @@ public class MapSheetConfig extends MapTableConfig { config.addUniKeyName(uniKeyName); return this; } - + /** * 设置是否覆盖已存在数据 * @@ -250,7 +250,23 @@ public class MapSheetConfig extends MapTableConfig { config.setEnableCover(enableCover); return this; } - + + + /** + * 设置是否启用 Upsert 模式 + * + * true(默认):根据唯一键匹配,存在则更新,不存在则追加 + * false:不匹配唯一键,所有数据直接追加到表格末尾 + * + * @param upsert true 为 Upsert 模式,false 为纯追加模式 + * @return SheetBuilder实例 + */ + public SheetBuilder upsert(boolean upsert) { + config.setUpsert(upsert); + return this; + } + + /** * 设置字段定义列表 * @@ -261,7 +277,7 @@ public class MapSheetConfig extends MapTableConfig { config.fields = new ArrayList<>(fields); return this; } - + /** * 添加单个字段 * @@ -272,7 +288,7 @@ public class MapSheetConfig extends MapTableConfig { config.fields.add(field); return this; } - + /** * 批量添加字段 * @@ -283,7 +299,7 @@ public class MapSheetConfig extends MapTableConfig { config.fields.addAll(fields); return this; } - + /** * 批量添加字段(可变参数) * @@ -294,7 +310,7 @@ public class MapSheetConfig extends MapTableConfig { config.fields.addAll(Arrays.asList(fields)); return this; } - + /** * 设置表头字体颜色 * @@ -305,7 +321,7 @@ public class MapSheetConfig extends MapTableConfig { config.headFontColor = headFontColor; return this; } - + /** * 设置表头背景颜色 * @@ -316,7 +332,7 @@ public class MapSheetConfig extends MapTableConfig { config.headBackColor = headBackColor; return this; } - + /** * 设置表头样式 * @@ -329,7 +345,7 @@ public class MapSheetConfig extends MapTableConfig { config.headBackColor = backColor; return this; } - + /** * 设置是否将单元格设置为纯文本 * @@ -340,7 +356,7 @@ public class MapSheetConfig extends MapTableConfig { config.isText = isText; return this; } - + /** * 设置是否启用字段描述行 * @@ -351,7 +367,7 @@ public class MapSheetConfig extends MapTableConfig { config.enableDesc = enableDesc; return this; } - + /** * 设置分组字段列表 * @@ -362,7 +378,7 @@ public class MapSheetConfig extends MapTableConfig { config.groupFields = new ArrayList<>(groupFields); return this; } - + /** * 设置分组字段(可变参数) * @@ -373,7 +389,7 @@ public class MapSheetConfig extends MapTableConfig { config.groupFields = Arrays.asList(groupFields); return this; } - + /** * 添加分组字段 * @@ -384,7 +400,7 @@ public class MapSheetConfig extends MapTableConfig { config.groupFields.add(groupField); return this; } - + /** * 设置自定义属性映射 * @@ -395,7 +411,7 @@ public class MapSheetConfig extends MapTableConfig { config.customProperties = new HashMap<>(customProperties); return this; } - + /** * 添加自定义属性 * @@ -407,7 +423,7 @@ public class MapSheetConfig extends MapTableConfig { config.customProperties.put(key, value); return this; } - + /** * 构建配置对象 * @@ -418,7 +434,7 @@ public class MapSheetConfig extends MapTableConfig { if (config.fields.isEmpty()) { throw new IllegalArgumentException("字段定义列表不能为空"); } - + return config; } } diff --git a/src/main/java/cn/isliu/core/config/MapTableConfig.java b/src/main/java/cn/isliu/core/config/MapTableConfig.java index dc3b7a8..4a7a7d2 100644 --- a/src/main/java/cn/isliu/core/config/MapTableConfig.java +++ b/src/main/java/cn/isliu/core/config/MapTableConfig.java @@ -14,37 +14,44 @@ import java.util.Set; * @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; - + + /** + * 是否启用 Upsert 模式 + * true(默认):根据唯一键匹配,存在则更新,不存在则追加 + * false:不匹配唯一键,所有数据直接追加到表格末尾 + */ + private boolean upsert = true; + /** * 字段位置映射 (字段名 -> 列位置,如 "添加SPU" -> "A") */ private Map fieldsPositionMap = new HashMap<>(); - + /** * 获取标题行行号 * @@ -53,7 +60,7 @@ public class MapTableConfig { public int getTitleRow() { return titleRow; } - + /** * 设置标题行行号 * @@ -64,7 +71,7 @@ public class MapTableConfig { this.titleRow = titleRow; return this; } - + /** * 获取数据起始行行号 * @@ -73,7 +80,7 @@ public class MapTableConfig { public int getHeadLine() { return headLine; } - + /** * 设置数据起始行行号 * @@ -84,7 +91,7 @@ public class MapTableConfig { this.headLine = headLine; return this; } - + /** * 获取唯一键字段名集合 * @@ -93,7 +100,7 @@ public class MapTableConfig { public Set getUniKeyNames() { return uniKeyNames; } - + /** * 设置唯一键字段名集合 * @@ -104,7 +111,7 @@ public class MapTableConfig { this.uniKeyNames = uniKeyNames; return this; } - + /** * 添加唯一键字段名 * @@ -115,7 +122,7 @@ public class MapTableConfig { this.uniKeyNames.add(uniKeyName); return this; } - + /** * 是否覆盖已存在数据 * @@ -124,7 +131,7 @@ public class MapTableConfig { public boolean isEnableCover() { return enableCover; } - + /** * 设置是否覆盖已存在数据 * @@ -135,7 +142,7 @@ public class MapTableConfig { this.enableCover = enableCover; return this; } - + /** * 是否忽略未找到的数据 * @@ -144,7 +151,7 @@ public class MapTableConfig { public boolean isIgnoreNotFound() { return ignoreNotFound; } - + /** * 设置是否忽略未找到的数据 * @@ -155,7 +162,31 @@ public class MapTableConfig { this.ignoreNotFound = ignoreNotFound; return this; } - + + + /** + * 是否启用 Upsert 模式 + * + * @return true 为 Upsert 模式,false 为纯追加模式 + */ + public boolean isUpsert() { + return upsert; + } + + /** + * 设置是否启用 Upsert 模式 + * + * true(默认):根据唯一键匹配,存在则更新,不存在则追加 + * false:不匹配唯一键,所有数据直接追加到表格末尾 + * + * @param upsert true 为 Upsert 模式,false 为纯追加模式 + * @return MapTableConfig实例,支持链式调用 + */ + public MapTableConfig setUpsert(boolean upsert) { + this.upsert = upsert; + return this; + } + /** * 获取字段位置映射 * @@ -164,7 +195,7 @@ public class MapTableConfig { public Map getFieldsPositionMap() { return fieldsPositionMap; } - + /** * 设置字段位置映射 * @@ -175,7 +206,7 @@ public class MapTableConfig { this.fieldsPositionMap = fieldsPositionMap; return this; } - + /** * 创建默认配置 * @@ -184,7 +215,7 @@ public class MapTableConfig { public static MapTableConfig createDefault() { return new MapTableConfig(); } - + /** * 创建配置构建器 * @@ -193,13 +224,13 @@ public class MapTableConfig { public static Builder builder() { return new Builder(); } - + /** * 配置构建器 */ public static class Builder { private final MapTableConfig config = new MapTableConfig(); - + /** * 设置标题行行号 * @@ -210,7 +241,7 @@ public class MapTableConfig { config.titleRow = titleRow; return this; } - + /** * 设置数据起始行行号 * @@ -221,7 +252,7 @@ public class MapTableConfig { config.headLine = headLine; return this; } - + /** * 设置唯一键字段名集合 * @@ -232,7 +263,7 @@ public class MapTableConfig { config.uniKeyNames = new HashSet<>(uniKeyNames); return this; } - + /** * 添加唯一键字段名 * @@ -243,7 +274,7 @@ public class MapTableConfig { config.uniKeyNames.add(uniKeyName); return this; } - + /** * 设置是否覆盖已存在数据 * @@ -254,7 +285,7 @@ public class MapTableConfig { config.enableCover = enableCover; return this; } - + /** * 设置是否忽略未找到的数据 * @@ -265,7 +296,21 @@ public class MapTableConfig { config.ignoreNotFound = ignoreNotFound; return this; } - + + /** + * 设置是否启用 Upsert 模式 + * + * true(默认):根据唯一键匹配,存在则更新,不存在则追加 + * false:不匹配唯一键,所有数据直接追加到表格末尾 + * + * @param upsert true 为 Upsert 模式,false 为纯追加模式 + * @return Builder实例 + */ + public Builder upsert(boolean upsert) { + config.upsert = upsert; + return this; + } + /** * 设置字段位置映射 * @@ -276,7 +321,7 @@ public class MapTableConfig { config.fieldsPositionMap = new HashMap<>(fieldsPositionMap); return this; } - + /** * 构建配置对象 * diff --git a/src/main/java/cn/isliu/core/utils/PropertyUtil.java b/src/main/java/cn/isliu/core/utils/PropertyUtil.java index f6a0410..41e7e19 100644 --- a/src/main/java/cn/isliu/core/utils/PropertyUtil.java +++ b/src/main/java/cn/isliu/core/utils/PropertyUtil.java @@ -484,6 +484,12 @@ public class PropertyUtil { public String headBackColor() { return "#cccccc"; } + + + @Override + public boolean upsert() { + return true; + } }; } return tableConf;