feat(core): 支持表格数据 Upsert 模式写入

- 在 MapTableConfig 和 MapSheetConfig 中新增 upsert 配置项,控制是否启用 Upsert 模式
- MapWriteBuilder 新增 upsert 方法用于设置 Upsert 模式开关- 实现根据 upsert 配置决定是否读取现有数据进行匹配更新或直接追加写入
- 添加分组字段支持,允许按指定字段对数据进行分组处理
-优化数据写入逻辑,区分 Upsert 模式和纯追加模式的数据读取与行号计算
- 增强异常处理和日志记录,提升系统稳定性与可维护性
This commit is contained in:
liushuang 2025-11-05 11:32:01 +08:00
parent 3b0b8712a8
commit 7ad1060adf
7 changed files with 673 additions and 327 deletions

@ -65,4 +65,15 @@ public @interface TableConf {
* @return 背景颜色
*/
String headBackColor() default "#cccccc";
/**
* 是否启用 Upsert 模式
*
* true默认根据唯一键匹配存在则更新不存在则追加
* false不匹配唯一键所有数据直接追加到表格末尾
*
* @return 是否启用 Upsert 模式
*/
boolean upsert() default true;
}

@ -233,9 +233,9 @@ public class MapSheetBuilder {
// 生成表头
List<String> 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();
@ -249,8 +249,8 @@ public class MapSheetBuilder {
// 2添加表头数据
Map<String, String> fieldDescriptions = buildFieldDescriptions();
FsApiUtil.putValues(spreadsheetToken,
FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions),
client);
FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions),
client);
// 3设置单元格为文本格式
if (config.isText()) {
@ -260,8 +260,8 @@ public class MapSheetBuilder {
// 4设置表格样式
FsApiUtil.setTableStyle(
FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf),
client, spreadsheetToken);
FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf),
client, spreadsheetToken);
// 5合并单元格
List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap);
@ -274,10 +274,10 @@ public class MapSheetBuilder {
// 准备自定义属性包含字段的 options 配置
Map<String, Object> customProps = prepareCustomProperties(fieldsMap);
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId,
config.isEnableDesc(), customProps);
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;
@ -292,9 +292,9 @@ public class MapSheetBuilder {
// 生成表头
List<String> 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();
@ -310,9 +310,9 @@ public class MapSheetBuilder {
List<String> headerList = FsTableUtil.getGroupHeaders(groupFieldList, headers);
Map<String, String> 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()) {
@ -323,8 +323,8 @@ public class MapSheetBuilder {
// 4设置表格样式分组模式
Map<String, String[]> positions = FsTableUtil.calculateGroupPositions(headers, groupFieldList);
positions.forEach((key, value) ->
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf),
client, spreadsheetToken));
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf),
client, spreadsheetToken));
// 5合并单元格
List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, positions.values());
@ -338,10 +338,10 @@ public class MapSheetBuilder {
// 准备自定义属性包含字段的 options 配置
Map<String, Object> customProps = prepareCustomProperties(fieldsMap);
FsTableUtil.setTableOptions(spreadsheetToken, headerWithColumnIdentifiers, fieldsMap,
sheetId, config.isEnableDesc(), customProps);
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;
@ -482,6 +482,12 @@ public class MapSheetBuilder {
public String headBackColor() {
return config.getHeadBackColor();
}
@Override
public boolean upsert() {
// MapSheetConfig 继承自 MapTableConfig支持 upsert 配置
return config.isUpsert();
}
};
}
@ -522,4 +528,3 @@ public class MapSheetBuilder {
return customProps;
}
}

@ -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;
@ -31,6 +33,7 @@ public class MapWriteBuilder {
private final String spreadsheetToken;
private final List<Map<String, Object>> dataList;
private MapTableConfig config;
private String groupField;
/**
* 构造函数
@ -123,6 +126,34 @@ public class MapWriteBuilder {
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;
}
/**
* 执行数据写入
*
@ -141,35 +172,80 @@ public class MapWriteBuilder {
Map<String, String> titlePostionMap = readFieldsPositionMap(sheet, client);
config.setFieldsPositionMap(titlePostionMap);
// 读取现有数据用于匹配和更新
Map<String, Integer> currTableRowMap = readExistingData(sheet, client, titlePostionMap);
// 根据 upsert 配置决定是否需要读取现有数据用于匹配
Map<String, Integer> currTableRowMap = new HashMap<>();
int nextAvailableRow = config.getHeadLine();
int headLine = config.getHeadLine();
int titleRow = config.getTitleRow();
List<FsTableData> fsTableDataList;
// 计算下一个可用行号
int nextAvailableRow = calculateNextAvailableRow(currTableRowMap, config.getHeadLine());
if (config.isUpsert()) {
// Upsert 模式读取现有数据用于匹配和更新
fsTableDataList = readExistingData(sheet, client, groupField);
if (!fsTableDataList.isEmpty()) {
Map<String, String> 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<String, String> 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.batchPutValues();
List<FileData> fileDataList = new ArrayList<>();
AtomicInteger rowCount = new AtomicInteger(nextAvailableRow);
// 处理每条数据
for (Map<String, Object> data : dataList) {
String uniqueId = MapDataUtil.calculateUniqueId(data, config);
if (config.isUpsert()) {
// Upsert 模式计算 uniqueId 并匹配更新或追加
for (Map<String, Object> data : dataList) {
String uniqueId = MapDataUtil.calculateUniqueId(data, config);
AtomicReference<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId));
AtomicReference<Integer> 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 (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<String, Object> data : dataList) {
int newRow = rowCount.incrementAndGet();
processDataRow(data, titlePostionMap, newRow, resultValuesBuilder,
fileDataList, config.isEnableCover());
fileDataList, config.isEnableCover());
}
}
@ -183,6 +259,44 @@ public class MapWriteBuilder {
return batchWriteValues(resultValuesBuilder, client);
}
@NotNull
private Map<String, Integer> getCurrTableRowMap(List<FsTableData> fsTableDataList, int titleRow,
Map<String, String> titlePostionMap, int headLine) {
Map<String, Integer> currTableRowMap;
// 获取标题映射
Map<String, String> titleMap = new HashMap<>();
fsTableDataList.stream()
.filter(d -> d.getRow() == (titleRow - 1))
.findFirst()
.ifPresent(d -> {
Map<String, String> map = (Map<String, String>) d.getData();
titleMap.putAll(map);
});
// 转换为带字段名的数据并计算唯一ID
currTableRowMap = fsTableDataList.stream()
.filter(fsTableData -> fsTableData.getRow() >= headLine)
.map(item -> {
Map<String, Object> resultMap = new HashMap<>();
Map<String, Object> map = (Map<String, Object>) 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;
}
/**
* 读取字段位置映射
*/
@ -192,10 +306,10 @@ public class MapWriteBuilder {
// 读取标题行数据
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<String, String> fieldsPositionMap = new HashMap<>();
@ -221,10 +335,10 @@ public class MapWriteBuilder {
/**
* 读取现有数据
*
* @param groupField 分组字段名称如果为null则读取全部数据
*/
private Map<String, Integer> readExistingData(Sheet sheet, FeishuClient client, Map<String, String> titlePostionMap) {
int headLine = config.getHeadLine();
int titleRow = config.getTitleRow();
private List<FsTableData> readExistingData(Sheet sheet, FeishuClient client, String groupField) {
int totalRow = sheet.getGridProperties().getRowCount();
int colCount = sheet.getGridProperties().getColumnCount();
int startOffset = 1;
@ -240,10 +354,10 @@ public class MapWriteBuilder {
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) {
@ -257,39 +371,23 @@ public class MapWriteBuilder {
// 处理表格数据
TableData tableData = processSheetData(sheet, values);
List<FsTableData> dataList = getFsTableData(tableData, new ArrayList<>());
// 获取标题映射
Map<String, String> titleMap = new HashMap<>();
dataList.stream()
.filter(d -> d.getRow() == (titleRow - 1))
.findFirst()
.ifPresent(d -> {
Map<String, String> map = (Map<String, String>) d.getData();
titleMap.putAll(map);
});
// 根据是否有分组字段选择不同的处理方式
List<FsTableData> dataList;
if (groupField == null || groupField.isEmpty()) {
// 无分组读取全部数据
dataList = getFsTableData(tableData, new ArrayList<>());
} else {
// 有分组需要重新调用完整的分组读取方法
// 创建临时的 TableConf 用于分组读取
TableConf tempTableConf = createTempTableConf();
Map<String, List<FsTableData>> groupDataMap = FsTableUtil.getGroupFsTableData(
sheet, spreadsheetToken, tempTableConf, new ArrayList<>(), new HashMap<>()
);
dataList = groupDataMap.getOrDefault(groupField, new ArrayList<>());
}
// 转换为带字段名的数据并计算唯一ID
return dataList.stream()
.filter(fsTableData -> fsTableData.getRow() >= headLine)
.map(item -> {
Map<String, Object> resultMap = new HashMap<>();
Map<String, Object> map = (Map<String, Object>) 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 dataList;
}
/**
@ -301,17 +399,133 @@ public class MapWriteBuilder {
}
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<FsTableData> 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<List<Object>> 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<FsTableData> dataList;
if (groupField == null || groupField.isEmpty()) {
// 无分组读取全部数据
dataList = getFsTableData(tableData, new ArrayList<>());
} else {
// 有分组需要重新调用完整的分组读取方法
// 创建临时的 TableConf 用于分组读取
TableConf tempTableConf = createTempTableConf();
Map<String, List<FsTableData>> 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<? extends java.lang.annotation.Annotation> 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<String, Object> data, Map<String, String> titlePostionMap,
int rowNum, CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder,
List<FileData> fileDataList, boolean enableCover) {
int rowNum, CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder,
List<FileData> fileDataList, boolean enableCover) {
data.forEach((field, fieldValue) -> {
String position = titlePostionMap.get(field);
@ -332,9 +546,9 @@ public class MapWriteBuilder {
}
// 添加到批量写入
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));
}
});
}
@ -356,16 +570,16 @@ 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());
"【飞书表格】Map写入-文件上传异常! " + fileData.getFileUrl());
}
});
}

@ -32,6 +32,7 @@ public class WriteBuilder<T> {
private Class<?> clazz;
private boolean ignoreNotFound;
private String groupField;
private Boolean upsert;
/**
* 构造函数
@ -106,6 +107,22 @@ public class WriteBuilder<T> {
return this;
}
/**
* 设置是否启用 Upsert 模式
*
* 此方法设置的值会覆盖 @TableConf 注解中的配置
*
* true默认根据唯一键匹配存在则更新不存在则追加
* false不匹配唯一键所有数据直接追加到表格末尾
*
* @param upsert true Upsert 模式false 为纯追加模式
* @return WriteBuilder实例支持链式调用
*/
public WriteBuilder<T> upsert(boolean upsert) {
this.upsert = upsert;
return this;
}
/**
* 执行数据写入并返回操作结果
*
@ -133,6 +150,10 @@ public class WriteBuilder<T> {
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<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
Set<String> keys = titlePostionMap.keySet();
@ -161,12 +182,18 @@ public class WriteBuilder<T> {
}
}
Map<String, Integer> currTableRowMap = fsTableDataList.stream()
.filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine())
.collect(Collectors.toMap(
FsTableData::getUniqueId,
FsTableData::getRow,
(existing, replacement) -> existing));
// 根据 finalUpsert 决定是否构建映射表
Map<String, Integer> 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<T> {
AtomicInteger rowCount = new AtomicInteger(row[0]);
for (T data : dataList) {
Map<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap);
if (finalUpsert) {
// Upsert 模式计算 uniqueId 并匹配更新或追加
for (T data : dataList) {
Map<String, Object> 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<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId));
if (uniqueId != null && rowNum.get() != null) {
// 找到匹配的行 更新
rowNum.set(rowNum.get() + 1);
Map<String, String> 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<String, String> 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<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap);
AtomicReference<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId));
if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1);
int rowCou = rowCount.incrementAndGet();
Map<String, String> 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<String, String> 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);

@ -251,6 +251,22 @@ public class MapSheetConfig extends MapTableConfig {
return this;
}
/**
* 设置是否启用 Upsert 模式
*
* true默认根据唯一键匹配存在则更新不存在则追加
* false不匹配唯一键所有数据直接追加到表格末尾
*
* @param upsert true Upsert 模式false 为纯追加模式
* @return SheetBuilder实例
*/
public SheetBuilder upsert(boolean upsert) {
config.setUpsert(upsert);
return this;
}
/**
* 设置字段定义列表
*

@ -40,6 +40,13 @@ public class MapTableConfig {
*/
private boolean ignoreNotFound = false;
/**
* 是否启用 Upsert 模式
* true默认根据唯一键匹配存在则更新不存在则追加
* false不匹配唯一键所有数据直接追加到表格末尾
*/
private boolean upsert = true;
/**
* 字段位置映射 (字段名 -> 列位置 "添加SPU" -> "A")
*/
@ -156,6 +163,30 @@ public class MapTableConfig {
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;
}
/**
* 获取字段位置映射
*
@ -266,6 +297,20 @@ public class MapTableConfig {
return this;
}
/**
* 设置是否启用 Upsert 模式
*
* true默认根据唯一键匹配存在则更新不存在则追加
* false不匹配唯一键所有数据直接追加到表格末尾
*
* @param upsert true Upsert 模式false 为纯追加模式
* @return Builder实例
*/
public Builder upsert(boolean upsert) {
config.upsert = upsert;
return this;
}
/**
* 设置字段位置映射
*

@ -484,6 +484,12 @@ public class PropertyUtil {
public String headBackColor() {
return "#cccccc";
}
@Override
public boolean upsert() {
return true;
}
};
}
return tableConf;