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 背景颜色 * @return 背景颜色
*/ */
String headBackColor() default "#cccccc"; String headBackColor() default "#cccccc";
/**
* 是否启用 Upsert 模式
*
* true默认根据唯一键匹配存在则更新不存在则追加
* false不匹配唯一键所有数据直接追加到表格末尾
*
* @return 是否启用 Upsert 模式
*/
boolean upsert() default true;
} }

@ -31,11 +31,11 @@ import java.util.stream.Collectors;
* @since 2025-10-16 * @since 2025-10-16
*/ */
public class MapSheetBuilder { public class MapSheetBuilder {
private final String sheetName; private final String sheetName;
private final String spreadsheetToken; private final String spreadsheetToken;
private MapSheetConfig config; private MapSheetConfig config;
/** /**
* 构造函数 * 构造函数
* *
@ -47,7 +47,7 @@ public class MapSheetBuilder {
this.spreadsheetToken = spreadsheetToken; this.spreadsheetToken = spreadsheetToken;
this.config = MapSheetConfig.createDefault(); this.config = MapSheetConfig.createDefault();
} }
/** /**
* 设置表格配置 * 设置表格配置
* *
@ -58,7 +58,7 @@ public class MapSheetBuilder {
this.config = config; this.config = config;
return this; return this;
} }
/** /**
* 设置标题行 * 设置标题行
* *
@ -69,7 +69,7 @@ public class MapSheetBuilder {
this.config.setTitleRow(titleRow); this.config.setTitleRow(titleRow);
return this; return this;
} }
/** /**
* 设置数据起始行 * 设置数据起始行
* *
@ -80,7 +80,7 @@ public class MapSheetBuilder {
this.config.setHeadLine(headLine); this.config.setHeadLine(headLine);
return this; return this;
} }
/** /**
* 设置唯一键字段 * 设置唯一键字段
* *
@ -91,7 +91,7 @@ public class MapSheetBuilder {
this.config.setUniKeyNames(uniKeyNames); this.config.setUniKeyNames(uniKeyNames);
return this; return this;
} }
/** /**
* 添加唯一键字段 * 添加唯一键字段
* *
@ -102,7 +102,7 @@ public class MapSheetBuilder {
this.config.addUniKeyName(uniKeyName); this.config.addUniKeyName(uniKeyName);
return this; return this;
} }
/** /**
* 设置字段列表 * 设置字段列表
* *
@ -113,7 +113,7 @@ public class MapSheetBuilder {
this.config.setFields(new ArrayList<>(fields)); this.config.setFields(new ArrayList<>(fields));
return this; return this;
} }
/** /**
* 添加单个字段 * 添加单个字段
* *
@ -124,7 +124,7 @@ public class MapSheetBuilder {
this.config.addField(field); this.config.addField(field);
return this; return this;
} }
/** /**
* 批量添加字段 * 批量添加字段
* *
@ -135,7 +135,7 @@ public class MapSheetBuilder {
this.config.addFields(fields); this.config.addFields(fields);
return this; return this;
} }
/** /**
* 批量添加字段可变参数 * 批量添加字段可变参数
* *
@ -146,7 +146,7 @@ public class MapSheetBuilder {
this.config.addFields(fields); this.config.addFields(fields);
return this; return this;
} }
/** /**
* 设置表头样式 * 设置表头样式
* *
@ -159,7 +159,7 @@ public class MapSheetBuilder {
this.config.setHeadBackColor(backColor); this.config.setHeadBackColor(backColor);
return this; return this;
} }
/** /**
* 设置是否为纯文本格式 * 设置是否为纯文本格式
* *
@ -170,7 +170,7 @@ public class MapSheetBuilder {
this.config.setText(isText); this.config.setText(isText);
return this; return this;
} }
/** /**
* 设置是否启用字段描述 * 设置是否启用字段描述
* *
@ -181,7 +181,7 @@ public class MapSheetBuilder {
this.config.setEnableDesc(enableDesc); this.config.setEnableDesc(enableDesc);
return this; return this;
} }
/** /**
* 设置分组字段 * 设置分组字段
* *
@ -192,7 +192,7 @@ public class MapSheetBuilder {
this.config.setGroupFields(Arrays.asList(groupFields)); this.config.setGroupFields(Arrays.asList(groupFields));
return this; return this;
} }
/** /**
* 添加自定义属性 * 添加自定义属性
* *
@ -204,7 +204,7 @@ public class MapSheetBuilder {
this.config.addCustomProperty(key, value); this.config.addCustomProperty(key, value);
return this; return this;
} }
/** /**
* 构建表格并返回工作表ID * 构建表格并返回工作表ID
* *
@ -215,7 +215,7 @@ public class MapSheetBuilder {
if (config.getFields().isEmpty()) { if (config.getFields().isEmpty()) {
throw new IllegalArgumentException("字段定义列表不能为空"); throw new IllegalArgumentException("字段定义列表不能为空");
} }
// 判断是否为分组表格 // 判断是否为分组表格
if (config.getGroupFields() != null && !config.getGroupFields().isEmpty()) { if (config.getGroupFields() != null && !config.getGroupFields().isEmpty()) {
return buildGroupSheet(); return buildGroupSheet();
@ -223,145 +223,145 @@ public class MapSheetBuilder {
return buildNormalSheet(); return buildNormalSheet();
} }
} }
/** /**
* 构建普通表格 * 构建普通表格
*/ */
private String buildNormalSheet() { private String buildNormalSheet() {
// 转换字段定义为 FieldProperty // 转换字段定义为 FieldProperty
Map<String, FieldProperty> fieldsMap = convertToFieldsMap(config.getFields()); Map<String, FieldProperty> fieldsMap = convertToFieldsMap(config.getFields());
// 生成表头 // 生成表头
List<String> headers = config.getFields().stream() List<String> headers = config.getFields().stream()
.sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder))
.map(MapFieldDefinition::getFieldName) .map(MapFieldDefinition::getFieldName)
.collect(Collectors.toList()); .collect(Collectors.toList());
// 创建 TableConf // 创建 TableConf
TableConf tableConf = createTableConf(); TableConf tableConf = createTableConf();
// 创建飞书客户端 // 创建飞书客户端
FeishuClient client = FsClient.getInstance().getClient(); FeishuClient client = FsClient.getInstance().getClient();
// 1创建sheet // 1创建sheet
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2添加表头数据 // 2添加表头数据
Map<String, String> fieldDescriptions = buildFieldDescriptions(); Map<String, String> fieldDescriptions = buildFieldDescriptions();
FsApiUtil.putValues(spreadsheetToken, FsApiUtil.putValues(spreadsheetToken,
FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions),
client); client);
// 3设置单元格为文本格式 // 3设置单元格为文本格式
if (config.isText()) { if (config.isText()) {
String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); String column = FsTableUtil.getColumnNameByNuNumber(headers.size());
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
} }
// 4设置表格样式 // 4设置表格样式
FsApiUtil.setTableStyle( FsApiUtil.setTableStyle(
FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf),
client, spreadsheetToken); client, spreadsheetToken);
// 5合并单元格 // 5合并单元格
List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap); List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap);
if (!mergeCell.isEmpty()) { if (!mergeCell.isEmpty()) {
mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken));
} }
// 6设置表格下拉 // 6设置表格下拉
try { try {
// 准备自定义属性包含字段的 options 配置 // 准备自定义属性包含字段的 options 配置
Map<String, Object> customProps = prepareCustomProperties(fieldsMap); Map<String, Object> customProps = prepareCustomProperties(fieldsMap);
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId,
config.isEnableDesc(), customProps); config.isEnableDesc(), customProps);
} catch (Exception e) { } catch (Exception e) {
Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE, Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE,
"【Map表格构建器】设置表格下拉异常sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); "【Map表格构建器】设置表格下拉异常sheetId:" + sheetId + ", 错误信息:{}", e.getMessage());
} }
return sheetId; return sheetId;
} }
/** /**
* 构建分组表格 * 构建分组表格
*/ */
private String buildGroupSheet() { private String buildGroupSheet() {
// 转换字段定义为 FieldProperty // 转换字段定义为 FieldProperty
Map<String, FieldProperty> fieldsMap = convertToFieldsMap(config.getFields()); Map<String, FieldProperty> fieldsMap = convertToFieldsMap(config.getFields());
// 生成表头 // 生成表头
List<String> headers = config.getFields().stream() List<String> headers = config.getFields().stream()
.sorted(Comparator.comparingInt(MapFieldDefinition::getOrder)) .sorted(Comparator.comparingInt(MapFieldDefinition::getOrder))
.map(MapFieldDefinition::getFieldName) .map(MapFieldDefinition::getFieldName)
.collect(Collectors.toList()); .collect(Collectors.toList());
// 创建 TableConf // 创建 TableConf
TableConf tableConf = createTableConf(); TableConf tableConf = createTableConf();
// 创建飞书客户端 // 创建飞书客户端
FeishuClient client = FsClient.getInstance().getClient(); FeishuClient client = FsClient.getInstance().getClient();
// 1创建sheet // 1创建sheet
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2添加表头数据分组模式 // 2添加表头数据分组模式
List<String> groupFieldList = config.getGroupFields(); List<String> groupFieldList = config.getGroupFields();
List<String> headerList = FsTableUtil.getGroupHeaders(groupFieldList, headers); List<String> headerList = FsTableUtil.getGroupHeaders(groupFieldList, headers);
Map<String, String> fieldDescriptions = buildFieldDescriptions(); Map<String, String> fieldDescriptions = buildFieldDescriptions();
FsApiUtil.putValues(spreadsheetToken, FsApiUtil.putValues(spreadsheetToken,
FsTableUtil.getHeadTemplateBuilder(sheetId, headers, headerList, fieldsMap, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, headerList, fieldsMap,
tableConf, fieldDescriptions, groupFieldList), tableConf, fieldDescriptions, groupFieldList),
client); client);
// 3设置单元格为文本格式 // 3设置单元格为文本格式
if (config.isText()) { if (config.isText()) {
String column = FsTableUtil.getColumnNameByNuNumber(headerList.size()); String column = FsTableUtil.getColumnNameByNuNumber(headerList.size());
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
} }
// 4设置表格样式分组模式 // 4设置表格样式分组模式
Map<String, String[]> positions = FsTableUtil.calculateGroupPositions(headers, groupFieldList); Map<String, String[]> positions = FsTableUtil.calculateGroupPositions(headers, groupFieldList);
positions.forEach((key, value) -> positions.forEach((key, value) ->
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf), FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, value, tableConf),
client, spreadsheetToken)); client, spreadsheetToken));
// 5合并单元格 // 5合并单元格
List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, positions.values()); List<CustomCellService.CellRequest> mergeCell = FsTableUtil.getMergeCell(sheetId, positions.values());
if (!mergeCell.isEmpty()) { if (!mergeCell.isEmpty()) {
mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken));
} }
// 6设置表格下拉 // 6设置表格下拉
try { try {
String[] headerWithColumnIdentifiers = FsTableUtil.generateHeaderWithColumnIdentifiers(headers, groupFieldList); String[] headerWithColumnIdentifiers = FsTableUtil.generateHeaderWithColumnIdentifiers(headers, groupFieldList);
// 准备自定义属性包含字段的 options 配置 // 准备自定义属性包含字段的 options 配置
Map<String, Object> customProps = prepareCustomProperties(fieldsMap); Map<String, Object> customProps = prepareCustomProperties(fieldsMap);
FsTableUtil.setTableOptions(spreadsheetToken, headerWithColumnIdentifiers, fieldsMap, FsTableUtil.setTableOptions(spreadsheetToken, headerWithColumnIdentifiers, fieldsMap,
sheetId, config.isEnableDesc(), customProps); sheetId, config.isEnableDesc(), customProps);
} catch (Exception e) { } catch (Exception e) {
Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE, Logger.getLogger(MapSheetBuilder.class.getName()).log(Level.SEVERE,
"【Map表格构建器】设置表格下拉异常sheetId:" + sheetId + ", 错误信息:{}", e.getMessage()); "【Map表格构建器】设置表格下拉异常sheetId:" + sheetId + ", 错误信息:{}", e.getMessage());
} }
return sheetId; return sheetId;
} }
/** /**
* MapFieldDefinition 列表转换为 FieldProperty Map * MapFieldDefinition 列表转换为 FieldProperty Map
*/ */
private Map<String, FieldProperty> convertToFieldsMap(List<MapFieldDefinition> fields) { private Map<String, FieldProperty> convertToFieldsMap(List<MapFieldDefinition> fields) {
Map<String, FieldProperty> fieldsMap = new LinkedHashMap<>(); Map<String, FieldProperty> fieldsMap = new LinkedHashMap<>();
for (MapFieldDefinition field : fields) { for (MapFieldDefinition field : fields) {
TableProperty tableProperty = createTableProperty(field); TableProperty tableProperty = createTableProperty(field);
FieldProperty fieldProperty = new FieldProperty(field.getFieldName(), tableProperty); FieldProperty fieldProperty = new FieldProperty(field.getFieldName(), tableProperty);
fieldsMap.put(field.getFieldName(), fieldProperty); fieldsMap.put(field.getFieldName(), fieldProperty);
} }
return fieldsMap; return fieldsMap;
} }
/** /**
* 根据 MapFieldDefinition 创建 TableProperty 注解实例 * 根据 MapFieldDefinition 创建 TableProperty 注解实例
*/ */
@ -371,67 +371,67 @@ public class MapSheetBuilder {
public Class<? extends Annotation> annotationType() { public Class<? extends Annotation> annotationType() {
return TableProperty.class; return TableProperty.class;
} }
@Override @Override
public String[] value() { public String[] value() {
return new String[]{field.getFieldName()}; return new String[]{field.getFieldName()};
} }
@Override @Override
public String desc() { public String desc() {
return field.getDescription() != null ? field.getDescription() : ""; return field.getDescription() != null ? field.getDescription() : "";
} }
@Override @Override
public String field() { public String field() {
return field.getFieldName(); return field.getFieldName();
} }
@Override @Override
public int order() { public int order() {
return field.getOrder(); return field.getOrder();
} }
@Override @Override
public TypeEnum type() { public TypeEnum type() {
return field.getType() != null ? field.getType() : TypeEnum.TEXT; return field.getType() != null ? field.getType() : TypeEnum.TEXT;
} }
@Override @Override
public Class<? extends BaseEnum> enumClass() { public Class<? extends BaseEnum> enumClass() {
// 优先级1如果配置了 enumClass直接返回 // 优先级1如果配置了 enumClass直接返回
if (field.getEnumClass() != null && field.getEnumClass() != BaseEnum.class) { if (field.getEnumClass() != null && field.getEnumClass() != BaseEnum.class) {
return field.getEnumClass(); return field.getEnumClass();
} }
// 优先级2如果没有配置 enumClass 但配置了 options创建动态枚举类 // 优先级2如果没有配置 enumClass 但配置了 options创建动态枚举类
// 注意这里返回 BaseEnum.class实际的 options 通过 optionsClass 处理 // 注意这里返回 BaseEnum.class实际的 options 通过 optionsClass 处理
return BaseEnum.class; return BaseEnum.class;
} }
@Override @Override
public Class<? extends FieldValueProcess> fieldFormatClass() { public Class<? extends FieldValueProcess> fieldFormatClass() {
return FieldValueProcess.class; return FieldValueProcess.class;
} }
@Override @Override
public Class<? extends OptionsValueProcess> optionsClass() { public Class<? extends OptionsValueProcess> optionsClass() {
// 优先级1如果配置了 optionsClass直接返回 // 优先级1如果配置了 optionsClass直接返回
if (field.getOptionsClass() != null && field.getOptionsClass() != OptionsValueProcess.class) { if (field.getOptionsClass() != null && field.getOptionsClass() != OptionsValueProcess.class) {
return field.getOptionsClass(); return field.getOptionsClass();
} }
// 优先级2如果配置了 options 但没有 optionsClass创建动态的处理类 // 优先级2如果配置了 options 但没有 optionsClass创建动态的处理类
if (field.getOptions() != null && !field.getOptions().isEmpty()) { if (field.getOptions() != null && !field.getOptions().isEmpty()) {
return MapOptionsUtil.createDynamicOptionsClass(field.getOptions()); return MapOptionsUtil.createDynamicOptionsClass(field.getOptions());
} }
// 优先级3返回默认值 // 优先级3返回默认值
return OptionsValueProcess.class; return OptionsValueProcess.class;
} }
}; };
} }
/** /**
* 创建 TableConf 注解实例 * 创建 TableConf 注解实例
*/ */
@ -441,50 +441,56 @@ public class MapSheetBuilder {
public Class<? extends Annotation> annotationType() { public Class<? extends Annotation> annotationType() {
return TableConf.class; return TableConf.class;
} }
@Override @Override
public String[] uniKeys() { public String[] uniKeys() {
Set<String> uniKeyNames = config.getUniKeyNames(); Set<String> uniKeyNames = config.getUniKeyNames();
return uniKeyNames != null ? uniKeyNames.toArray(new String[0]) : new String[0]; return uniKeyNames != null ? uniKeyNames.toArray(new String[0]) : new String[0];
} }
@Override @Override
public int headLine() { public int headLine() {
return config.getHeadLine(); return config.getHeadLine();
} }
@Override @Override
public int titleRow() { public int titleRow() {
return config.getTitleRow(); return config.getTitleRow();
} }
@Override @Override
public boolean enableCover() { public boolean enableCover() {
return config.isEnableCover(); return config.isEnableCover();
} }
@Override @Override
public boolean isText() { public boolean isText() {
return config.isText(); return config.isText();
} }
@Override @Override
public boolean enableDesc() { public boolean enableDesc() {
return config.isEnableDesc(); return config.isEnableDesc();
} }
@Override @Override
public String headFontColor() { public String headFontColor() {
return config.getHeadFontColor(); return config.getHeadFontColor();
} }
@Override @Override
public String headBackColor() { public String headBackColor() {
return config.getHeadBackColor(); return config.getHeadBackColor();
} }
@Override
public boolean upsert() {
// MapSheetConfig 继承自 MapTableConfig支持 upsert 配置
return config.isUpsert();
}
}; };
} }
/** /**
* 构建字段描述映射 * 构建字段描述映射
*/ */
@ -497,20 +503,20 @@ public class MapSheetBuilder {
} }
return descriptions; return descriptions;
} }
/** /**
* 准备自定义属性 * 准备自定义属性
* *
* 将字段配置的 options 放入 customProperties DynamicOptionsProcess 使用 * 将字段配置的 options 放入 customProperties DynamicOptionsProcess 使用
*/ */
private Map<String, Object> prepareCustomProperties(Map<String, FieldProperty> fieldsMap) { private Map<String, Object> prepareCustomProperties(Map<String, FieldProperty> fieldsMap) {
Map<String, Object> customProps = new HashMap<>(); Map<String, Object> customProps = new HashMap<>();
// 复制原有的自定义属性 // 复制原有的自定义属性
if (config.getCustomProperties() != null) { if (config.getCustomProperties() != null) {
customProps.putAll(config.getCustomProperties()); customProps.putAll(config.getCustomProperties());
} }
// 为每个配置了 options 的字段添加选项到 customProperties // 为每个配置了 options 的字段添加选项到 customProperties
for (MapFieldDefinition field : config.getFields()) { for (MapFieldDefinition field : config.getFields()) {
if (field.getOptions() != null && !field.getOptions().isEmpty()) { if (field.getOptions() != null && !field.getOptions().isEmpty()) {
@ -518,8 +524,7 @@ public class MapSheetBuilder {
customProps.put("_dynamicOptions_" + field.getFieldName(), field.getOptions()); customProps.put("_dynamicOptions_" + field.getFieldName(), field.getOptions());
} }
} }
return customProps; return customProps;
} }
} }

@ -1,6 +1,7 @@
package cn.isliu.core.builder; package cn.isliu.core.builder;
import cn.isliu.core.*; import cn.isliu.core.*;
import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.client.FeishuClient; import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient; import cn.isliu.core.client.FsClient;
import cn.isliu.core.config.MapTableConfig; 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.logging.FsLogger;
import cn.isliu.core.service.CustomValueService; import cn.isliu.core.service.CustomValueService;
import cn.isliu.core.utils.*; import cn.isliu.core.utils.*;
import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -26,12 +28,13 @@ import static cn.isliu.core.utils.FsTableUtil.*;
* @since 2025-10-16 * @since 2025-10-16
*/ */
public class MapWriteBuilder { public class MapWriteBuilder {
private final String sheetId; private final String sheetId;
private final String spreadsheetToken; private final String spreadsheetToken;
private final List<Map<String, Object>> dataList; private final List<Map<String, Object>> dataList;
private MapTableConfig config; private MapTableConfig config;
private String groupField;
/** /**
* 构造函数 * 构造函数
* *
@ -45,7 +48,7 @@ public class MapWriteBuilder {
this.dataList = dataList; this.dataList = dataList;
this.config = MapTableConfig.createDefault(); this.config = MapTableConfig.createDefault();
} }
/** /**
* 设置表格配置 * 设置表格配置
* *
@ -56,7 +59,7 @@ public class MapWriteBuilder {
this.config = config; this.config = config;
return this; return this;
} }
/** /**
* 设置标题行 * 设置标题行
* *
@ -67,7 +70,7 @@ public class MapWriteBuilder {
this.config.setTitleRow(titleRow); this.config.setTitleRow(titleRow);
return this; return this;
} }
/** /**
* 设置数据起始行 * 设置数据起始行
* *
@ -78,7 +81,7 @@ public class MapWriteBuilder {
this.config.setHeadLine(headLine); this.config.setHeadLine(headLine);
return this; return this;
} }
/** /**
* 设置唯一键字段 * 设置唯一键字段
* *
@ -89,7 +92,7 @@ public class MapWriteBuilder {
this.config.setUniKeyNames(uniKeyNames); this.config.setUniKeyNames(uniKeyNames);
return this; return this;
} }
/** /**
* 添加唯一键字段 * 添加唯一键字段
* *
@ -100,7 +103,7 @@ public class MapWriteBuilder {
this.config.addUniKeyName(uniKeyName); this.config.addUniKeyName(uniKeyName);
return this; return this;
} }
/** /**
* 设置是否覆盖已存在数据 * 设置是否覆盖已存在数据
* *
@ -111,7 +114,7 @@ public class MapWriteBuilder {
this.config.setEnableCover(enableCover); this.config.setEnableCover(enableCover);
return this; return this;
} }
/** /**
* 设置是否忽略未找到的数据 * 设置是否忽略未找到的数据
* *
@ -122,7 +125,35 @@ public class MapWriteBuilder {
this.config.setIgnoreNotFound(ignoreNotFound); this.config.setIgnoreNotFound(ignoreNotFound);
return this; 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写入】数据列表为空跳过写入操作"); FsLogger.warn("【Map写入】数据列表为空跳过写入操作");
return null; return null;
} }
FeishuClient client = FsClient.getInstance().getClient(); FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
// 读取表格数据以获取字段位置映射和现有数据 // 读取表格数据以获取字段位置映射和现有数据
Map<String, String> titlePostionMap = readFieldsPositionMap(sheet, client); Map<String, String> titlePostionMap = readFieldsPositionMap(sheet, client);
config.setFieldsPositionMap(titlePostionMap); config.setFieldsPositionMap(titlePostionMap);
// 读取现有数据用于匹配和更新 // 根据 upsert 配置决定是否需要读取现有数据用于匹配
Map<String, Integer> currTableRowMap = readExistingData(sheet, client, titlePostionMap); Map<String, Integer> currTableRowMap = new HashMap<>();
int nextAvailableRow = config.getHeadLine();
// 计算下一个可用行号 int headLine = config.getHeadLine();
int nextAvailableRow = calculateNextAvailableRow(currTableRowMap, config.getHeadLine()); int titleRow = config.getTitleRow();
List<FsTableData> fsTableDataList;
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.BatchPutValuesBuilder resultValuesBuilder =
CustomValueService.ValueRequest.batchPutValues(); CustomValueService.ValueRequest.batchPutValues();
List<FileData> fileDataList = new ArrayList<>(); List<FileData> fileDataList = new ArrayList<>();
AtomicInteger rowCount = new AtomicInteger(nextAvailableRow); AtomicInteger rowCount = new AtomicInteger(nextAvailableRow);
// 处理每条数据 if (config.isUpsert()) {
for (Map<String, Object> data : dataList) { // Upsert 模式计算 uniqueId 并匹配更新或追加
String uniqueId = MapDataUtil.calculateUniqueId(data, config); 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) {
// 更新现有行 if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1); // 更新现有行
processDataRow(data, titlePostionMap, rowNum.get(), resultValuesBuilder, rowNum.set(rowNum.get() + 1);
fileDataList, config.isEnableCover()); processDataRow(data, titlePostionMap, rowNum.get(), resultValuesBuilder,
} else if (!config.isIgnoreNotFound()) { 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(); int newRow = rowCount.incrementAndGet();
processDataRow(data, titlePostionMap, newRow, resultValuesBuilder, processDataRow(data, titlePostionMap, newRow, resultValuesBuilder,
fileDataList, config.isEnableCover()); fileDataList, config.isEnableCover());
} }
} }
// 检查是否需要扩展行数 // 检查是否需要扩展行数
ensureSufficientRows(sheet, rowCount.get(), client); ensureSufficientRows(sheet, rowCount.get(), client);
// 上传文件 // 上传文件
uploadFiles(fileDataList, client); uploadFiles(fileDataList, client);
// 批量写入数据 // 批量写入数据
return batchWriteValues(resultValuesBuilder, client); 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;
}
/** /**
* 读取字段位置映射 * 读取字段位置映射
*/ */
private Map<String, String> readFieldsPositionMap(Sheet sheet, FeishuClient client) { private Map<String, String> readFieldsPositionMap(Sheet sheet, FeishuClient client) {
int titleRow = config.getTitleRow(); int titleRow = config.getTitleRow();
int colCount = sheet.getGridProperties().getColumnCount(); int colCount = sheet.getGridProperties().getColumnCount();
// 读取标题行数据 // 读取标题行数据
ValuesBatch valuesBatch = FsApiUtil.getSheetData( ValuesBatch valuesBatch = FsApiUtil.getSheetData(
sheet.getSheetId(), spreadsheetToken, sheet.getSheetId(), spreadsheetToken,
"A" + titleRow, "A" + titleRow,
getColumnName(colCount - 1) + titleRow, getColumnName(colCount - 1) + titleRow,
client client
); );
Map<String, String> fieldsPositionMap = new HashMap<>(); Map<String, String> fieldsPositionMap = new HashMap<>();
if (valuesBatch != null && valuesBatch.getValueRanges() != null) { if (valuesBatch != null && valuesBatch.getValueRanges() != null) {
for (ValueRange valueRange : valuesBatch.getValueRanges()) { for (ValueRange valueRange : valuesBatch.getValueRanges()) {
if (valueRange.getValues() != null && !valueRange.getValues().isEmpty()) { if (valueRange.getValues() != null && !valueRange.getValues().isEmpty()) {
@ -215,37 +329,37 @@ public class MapWriteBuilder {
} }
} }
} }
return fieldsPositionMap; return fieldsPositionMap;
} }
/** /**
* 读取现有数据 * 读取现有数据
*
* @param groupField 分组字段名称如果为null则读取全部数据
*/ */
private Map<String, Integer> readExistingData(Sheet sheet, FeishuClient client, Map<String, String> titlePostionMap) { private List<FsTableData> readExistingData(Sheet sheet, FeishuClient client, String groupField) {
int headLine = config.getHeadLine();
int titleRow = config.getTitleRow();
int totalRow = sheet.getGridProperties().getRowCount(); int totalRow = sheet.getGridProperties().getRowCount();
int colCount = sheet.getGridProperties().getColumnCount(); int colCount = sheet.getGridProperties().getColumnCount();
int startOffset = 1; int startOffset = 1;
// 批量读取数据 // 批量读取数据
int rowCountPerBatch = Math.min(totalRow, 100); int rowCountPerBatch = Math.min(totalRow, 100);
int actualRows = Math.max(0, totalRow - startOffset); int actualRows = Math.max(0, totalRow - startOffset);
int batchCount = (actualRows + rowCountPerBatch - 1) / rowCountPerBatch; int batchCount = (actualRows + rowCountPerBatch - 1) / rowCountPerBatch;
List<List<Object>> values = new LinkedList<>(); List<List<Object>> values = new LinkedList<>();
for (int i = 0; i < batchCount; i++) { for (int i = 0; i < batchCount; i++) {
int startRowIndex = startOffset + i * rowCountPerBatch; int startRowIndex = startOffset + i * rowCountPerBatch;
int endRowIndex = Math.min(startRowIndex + rowCountPerBatch - 1, totalRow - 1); int endRowIndex = Math.min(startRowIndex + rowCountPerBatch - 1, totalRow - 1);
ValuesBatch valuesBatch = FsApiUtil.getSheetData( ValuesBatch valuesBatch = FsApiUtil.getSheetData(
sheet.getSheetId(), spreadsheetToken, sheet.getSheetId(), spreadsheetToken,
"A" + startRowIndex, "A" + startRowIndex,
getColumnName(colCount - 1) + endRowIndex, getColumnName(colCount - 1) + endRowIndex,
client client
); );
if (valuesBatch != null && valuesBatch.getValueRanges() != null) { if (valuesBatch != null && valuesBatch.getValueRanges() != null) {
for (ValueRange valueRange : valuesBatch.getValueRanges()) { for (ValueRange valueRange : valuesBatch.getValueRanges()) {
if (valueRange.getValues() != null) { if (valueRange.getValues() != null) {
@ -254,44 +368,28 @@ public class MapWriteBuilder {
} }
} }
} }
// 处理表格数据 // 处理表格数据
TableData tableData = processSheetData(sheet, values); TableData tableData = processSheetData(sheet, values);
List<FsTableData> dataList = getFsTableData(tableData, new ArrayList<>());
// 根据是否有分组字段选择不同的处理方式
// 获取标题映射 List<FsTableData> dataList;
Map<String, String> titleMap = new HashMap<>(); if (groupField == null || groupField.isEmpty()) {
dataList.stream() // 无分组读取全部数据
.filter(d -> d.getRow() == (titleRow - 1)) dataList = getFsTableData(tableData, new ArrayList<>());
.findFirst() } else {
.ifPresent(d -> { // 有分组需要重新调用完整的分组读取方法
Map<String, String> map = (Map<String, String>) d.getData(); // 创建临时的 TableConf 用于分组读取
titleMap.putAll(map); TableConf tempTableConf = createTempTableConf();
}); Map<String, List<FsTableData>> groupDataMap = FsTableUtil.getGroupFsTableData(
sheet, spreadsheetToken, tempTableConf, new ArrayList<>(), new HashMap<>()
// 转换为带字段名的数据并计算唯一ID );
return dataList.stream() dataList = groupDataMap.getOrDefault(groupField, new ArrayList<>());
.filter(fsTableData -> fsTableData.getRow() >= headLine) }
.map(item -> {
Map<String, Object> resultMap = new HashMap<>(); return dataList;
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));
} }
/** /**
* 计算下一个可用行号 * 计算下一个可用行号
*/ */
@ -299,26 +397,142 @@ public class MapWriteBuilder {
if (currTableRowMap.isEmpty()) { if (currTableRowMap.isEmpty()) {
return headLine; return headLine;
} }
return currTableRowMap.values().stream() return currTableRowMap.values().stream()
.max(Integer::compareTo) .max(Integer::compareTo)
.map(maxRow -> maxRow + 1) .map(maxRow -> maxRow + 1)
.orElse(headLine); .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, private void processDataRow(Map<String, Object> data, Map<String, String> titlePostionMap,
int rowNum, CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder, int rowNum, CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder,
List<FileData> fileDataList, boolean enableCover) { List<FileData> fileDataList, boolean enableCover) {
data.forEach((field, fieldValue) -> { data.forEach((field, fieldValue) -> {
String position = titlePostionMap.get(field); String position = titlePostionMap.get(field);
if (position == null || position.isEmpty()) { if (position == null || position.isEmpty()) {
return; return;
} }
// 处理文件数据 // 处理文件数据
if (fieldValue instanceof FileData) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
@ -330,15 +544,15 @@ public class MapWriteBuilder {
fileDataList.add(fileData); fileDataList.add(fileData);
} }
} }
// 添加到批量写入 // 添加到批量写入
if (enableCover || fieldValue != null) { if (enableCover || (fieldValue != null && !(fieldValue instanceof FileData))) {
resultValuesBuilder.addRange(sheetId, position + rowNum, position + rowNum) 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); FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", Math.abs(requiredRows - rowTotal), client);
} }
} }
/** /**
* 上传文件 * 上传文件
*/ */
@ -356,20 +570,20 @@ public class MapWriteBuilder {
fileDataList.forEach(fileData -> { fileDataList.forEach(fileData -> {
try { try {
FsApiUtil.imageUpload( FsApiUtil.imageUpload(
fileData.getImageData(), fileData.getImageData(),
fileData.getFileName(), fileData.getFileName(),
fileData.getPosition(), fileData.getPosition(),
fileData.getSheetId(), fileData.getSheetId(),
fileData.getSpreadsheetToken(), fileData.getSpreadsheetToken(),
client client
); );
} catch (Exception e) { } catch (Exception e) {
FsLogger.error(ErrorCode.BUSINESS_LOGIC_ERROR, FsLogger.error(ErrorCode.BUSINESS_LOGIC_ERROR,
"【飞书表格】Map写入-文件上传异常! " + fileData.getFileUrl()); "【飞书表格】Map写入-文件上传异常! " + fileData.getFileUrl());
} }
}); });
} }
/** /**
* 批量写入数据 * 批量写入数据
*/ */
@ -378,11 +592,11 @@ public class MapWriteBuilder {
CustomValueService.ValueRequest build = resultValuesBuilder.build(); CustomValueService.ValueRequest build = resultValuesBuilder.build();
CustomValueService.ValueBatchUpdatePutRequest batchPutValues = build.getBatchPutValues(); CustomValueService.ValueBatchUpdatePutRequest batchPutValues = build.getBatchPutValues();
List<CustomValueService.ValueRangeItem> valueRanges = batchPutValues.getValueRanges(); List<CustomValueService.ValueRangeItem> valueRanges = batchPutValues.getValueRanges();
if (valueRanges != null && !valueRanges.isEmpty()) { if (valueRanges != null && !valueRanges.isEmpty()) {
return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, build, client); return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, build, client);
} }
FsLogger.warn("【Map写入】没有数据需要写入"); FsLogger.warn("【Map写入】没有数据需要写入");
return null; return null;
} }

@ -32,6 +32,7 @@ public class WriteBuilder<T> {
private Class<?> clazz; private Class<?> clazz;
private boolean ignoreNotFound; private boolean ignoreNotFound;
private String groupField; private String groupField;
private Boolean upsert;
/** /**
* 构造函数 * 构造函数
@ -106,6 +107,22 @@ public class WriteBuilder<T> {
return this; 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); Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
TableConf tableConf = aClass != null ? PropertyUtil.getTableConf(aClass) : PropertyUtil.getTableConf(sourceClass); 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); Map<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
Set<String> keys = titlePostionMap.keySet(); Set<String> keys = titlePostionMap.keySet();
@ -161,12 +182,18 @@ public class WriteBuilder<T> {
} }
} }
Map<String, Integer> currTableRowMap = fsTableDataList.stream() // 根据 finalUpsert 决定是否构建映射表
.filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine()) Map<String, Integer> currTableRowMap = new HashMap<>();
.collect(Collectors.toMap( if (finalUpsert) {
FsTableData::getUniqueId, // Upsert 模式构建 uniqueId 到行号的映射表
FsTableData::getRow, currTableRowMap = fsTableDataList.stream()
(existing, replacement) -> existing)); .filter(fsTableData -> fsTableData.getRow() >= tableConf.headLine())
.collect(Collectors.toMap(
FsTableData::getUniqueId,
FsTableData::getRow,
(existing, replacement) -> existing
));
}
final Integer[] row = {tableConf.headLine()}; final Integer[] row = {tableConf.headLine()};
fsTableDataList.forEach(fsTableData -> { fsTableDataList.forEach(fsTableData -> {
@ -182,54 +209,76 @@ public class WriteBuilder<T> {
AtomicInteger rowCount = new AtomicInteger(row[0]); AtomicInteger rowCount = new AtomicInteger(row[0]);
for (T data : dataList) { if (finalUpsert) {
Map<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap); // Upsert 模式计算 uniqueId 并匹配更新或追加
for (T data : dataList) {
Map<String, Object> values = GenerateUtil.getFieldValue(data, fieldMap);
// 计算唯一标识如果data类型与aClass相同使用忽略字段逻辑否则直接从data获取uniqueId // 计算唯一标识如果data类型与aClass相同使用忽略字段逻辑否则直接从data获取uniqueId
String uniqueId; String uniqueId;
if (data.getClass().equals(aClass)) { if (data.getClass().equals(aClass)) {
// 类型相同使用忽略字段逻辑计算唯一标识 // 类型相同使用忽略字段逻辑计算唯一标识
uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, tableConf); uniqueId = calculateUniqueIdWithIgnoreFields(data, processedIgnoreFields, tableConf);
} else { } else {
uniqueId = GenerateUtil.getUniqueId(data, tableConf); 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)); int rowCou = rowCount.incrementAndGet();
if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1);
Map<String, String> finalTitlePostionMap = titlePostionMap; Map<String, String> finalTitlePostionMap = titlePostionMap;
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
String position = finalTitlePostionMap.get(field); 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) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
fileData.setSheetId(sheetId); fileData.setSheetId(sheetId);

@ -12,27 +12,27 @@ import java.util.*;
* @since 2025-10-16 * @since 2025-10-16
*/ */
public class MapSheetConfig extends MapTableConfig { public class MapSheetConfig extends MapTableConfig {
/** /**
* 字段定义列表 * 字段定义列表
*/ */
private List<MapFieldDefinition> fields = new ArrayList<>(); private List<MapFieldDefinition> fields = new ArrayList<>();
/** /**
* 表头字体颜色十六进制 #ffffff * 表头字体颜色十六进制 #ffffff
*/ */
private String headFontColor = "#ffffff"; private String headFontColor = "#ffffff";
/** /**
* 表头背景颜色十六进制 #000000 * 表头背景颜色十六进制 #000000
*/ */
private String headBackColor = "#000000"; private String headBackColor = "#000000";
/** /**
* 是否将单元格设置为纯文本格式 * 是否将单元格设置为纯文本格式
*/ */
private boolean isText = false; private boolean isText = false;
/** /**
* 是否启用字段描述行 * 是否启用字段描述行
*/ */
@ -98,12 +98,12 @@ public class MapSheetConfig extends MapTableConfig {
* 分组字段列表用于创建分组表格 * 分组字段列表用于创建分组表格
*/ */
private List<String> groupFields = new ArrayList<>(); private List<String> groupFields = new ArrayList<>();
/** /**
* 自定义属性映射用于传递额外配置 * 自定义属性映射用于传递额外配置
*/ */
private Map<String, Object> customProperties = new HashMap<>(); private Map<String, Object> customProperties = new HashMap<>();
/** /**
* 创建默认配置 * 创建默认配置
* *
@ -112,7 +112,7 @@ public class MapSheetConfig extends MapTableConfig {
public static MapSheetConfig createDefault() { public static MapSheetConfig createDefault() {
return new MapSheetConfig(); return new MapSheetConfig();
} }
/** /**
* 创建表格配置构建器 * 创建表格配置构建器
* *
@ -121,7 +121,7 @@ public class MapSheetConfig extends MapTableConfig {
public static SheetBuilder sheetBuilder() { public static SheetBuilder sheetBuilder() {
return new SheetBuilder(); return new SheetBuilder();
} }
/** /**
* 添加单个字段 * 添加单个字段
* *
@ -132,7 +132,7 @@ public class MapSheetConfig extends MapTableConfig {
this.fields.add(field); this.fields.add(field);
return this; return this;
} }
/** /**
* 批量添加字段 * 批量添加字段
* *
@ -143,7 +143,7 @@ public class MapSheetConfig extends MapTableConfig {
this.fields.addAll(fields); this.fields.addAll(fields);
return this; return this;
} }
/** /**
* 批量添加字段可变参数 * 批量添加字段可变参数
* *
@ -154,7 +154,7 @@ public class MapSheetConfig extends MapTableConfig {
this.fields.addAll(Arrays.asList(fields)); this.fields.addAll(Arrays.asList(fields));
return this; return this;
} }
/** /**
* 添加分组字段 * 添加分组字段
* *
@ -165,7 +165,7 @@ public class MapSheetConfig extends MapTableConfig {
this.groupFields.add(groupField); this.groupFields.add(groupField);
return this; return this;
} }
/** /**
* 添加自定义属性 * 添加自定义属性
* *
@ -195,7 +195,7 @@ public class MapSheetConfig extends MapTableConfig {
*/ */
public static class SheetBuilder { public static class SheetBuilder {
private final MapSheetConfig config = new MapSheetConfig(); private final MapSheetConfig config = new MapSheetConfig();
/** /**
* 设置标题行行号 * 设置标题行行号
* *
@ -206,7 +206,7 @@ public class MapSheetConfig extends MapTableConfig {
config.setTitleRow(titleRow); config.setTitleRow(titleRow);
return this; return this;
} }
/** /**
* 设置数据起始行行号 * 设置数据起始行行号
* *
@ -217,7 +217,7 @@ public class MapSheetConfig extends MapTableConfig {
config.setHeadLine(headLine); config.setHeadLine(headLine);
return this; return this;
} }
/** /**
* 设置唯一键字段名集合 * 设置唯一键字段名集合
* *
@ -228,7 +228,7 @@ public class MapSheetConfig extends MapTableConfig {
config.setUniKeyNames(uniKeyNames); config.setUniKeyNames(uniKeyNames);
return this; return this;
} }
/** /**
* 添加唯一键字段名 * 添加唯一键字段名
* *
@ -239,7 +239,7 @@ public class MapSheetConfig extends MapTableConfig {
config.addUniKeyName(uniKeyName); config.addUniKeyName(uniKeyName);
return this; return this;
} }
/** /**
* 设置是否覆盖已存在数据 * 设置是否覆盖已存在数据
* *
@ -250,7 +250,23 @@ public class MapSheetConfig extends MapTableConfig {
config.setEnableCover(enableCover); config.setEnableCover(enableCover);
return this; 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); config.fields = new ArrayList<>(fields);
return this; return this;
} }
/** /**
* 添加单个字段 * 添加单个字段
* *
@ -272,7 +288,7 @@ public class MapSheetConfig extends MapTableConfig {
config.fields.add(field); config.fields.add(field);
return this; return this;
} }
/** /**
* 批量添加字段 * 批量添加字段
* *
@ -283,7 +299,7 @@ public class MapSheetConfig extends MapTableConfig {
config.fields.addAll(fields); config.fields.addAll(fields);
return this; return this;
} }
/** /**
* 批量添加字段可变参数 * 批量添加字段可变参数
* *
@ -294,7 +310,7 @@ public class MapSheetConfig extends MapTableConfig {
config.fields.addAll(Arrays.asList(fields)); config.fields.addAll(Arrays.asList(fields));
return this; return this;
} }
/** /**
* 设置表头字体颜色 * 设置表头字体颜色
* *
@ -305,7 +321,7 @@ public class MapSheetConfig extends MapTableConfig {
config.headFontColor = headFontColor; config.headFontColor = headFontColor;
return this; return this;
} }
/** /**
* 设置表头背景颜色 * 设置表头背景颜色
* *
@ -316,7 +332,7 @@ public class MapSheetConfig extends MapTableConfig {
config.headBackColor = headBackColor; config.headBackColor = headBackColor;
return this; return this;
} }
/** /**
* 设置表头样式 * 设置表头样式
* *
@ -329,7 +345,7 @@ public class MapSheetConfig extends MapTableConfig {
config.headBackColor = backColor; config.headBackColor = backColor;
return this; return this;
} }
/** /**
* 设置是否将单元格设置为纯文本 * 设置是否将单元格设置为纯文本
* *
@ -340,7 +356,7 @@ public class MapSheetConfig extends MapTableConfig {
config.isText = isText; config.isText = isText;
return this; return this;
} }
/** /**
* 设置是否启用字段描述行 * 设置是否启用字段描述行
* *
@ -351,7 +367,7 @@ public class MapSheetConfig extends MapTableConfig {
config.enableDesc = enableDesc; config.enableDesc = enableDesc;
return this; return this;
} }
/** /**
* 设置分组字段列表 * 设置分组字段列表
* *
@ -362,7 +378,7 @@ public class MapSheetConfig extends MapTableConfig {
config.groupFields = new ArrayList<>(groupFields); config.groupFields = new ArrayList<>(groupFields);
return this; return this;
} }
/** /**
* 设置分组字段可变参数 * 设置分组字段可变参数
* *
@ -373,7 +389,7 @@ public class MapSheetConfig extends MapTableConfig {
config.groupFields = Arrays.asList(groupFields); config.groupFields = Arrays.asList(groupFields);
return this; return this;
} }
/** /**
* 添加分组字段 * 添加分组字段
* *
@ -384,7 +400,7 @@ public class MapSheetConfig extends MapTableConfig {
config.groupFields.add(groupField); config.groupFields.add(groupField);
return this; return this;
} }
/** /**
* 设置自定义属性映射 * 设置自定义属性映射
* *
@ -395,7 +411,7 @@ public class MapSheetConfig extends MapTableConfig {
config.customProperties = new HashMap<>(customProperties); config.customProperties = new HashMap<>(customProperties);
return this; return this;
} }
/** /**
* 添加自定义属性 * 添加自定义属性
* *
@ -407,7 +423,7 @@ public class MapSheetConfig extends MapTableConfig {
config.customProperties.put(key, value); config.customProperties.put(key, value);
return this; return this;
} }
/** /**
* 构建配置对象 * 构建配置对象
* *
@ -418,7 +434,7 @@ public class MapSheetConfig extends MapTableConfig {
if (config.fields.isEmpty()) { if (config.fields.isEmpty()) {
throw new IllegalArgumentException("字段定义列表不能为空"); throw new IllegalArgumentException("字段定义列表不能为空");
} }
return config; return config;
} }
} }

@ -14,37 +14,44 @@ import java.util.Set;
* @since 2025-10-16 * @since 2025-10-16
*/ */
public class MapTableConfig { public class MapTableConfig {
/** /**
* 标题行行号从1开始 * 标题行行号从1开始
*/ */
private int titleRow = 1; private int titleRow = 1;
/** /**
* 数据起始行行号从1开始 * 数据起始行行号从1开始
*/ */
private int headLine = 1; private int headLine = 1;
/** /**
* 唯一键字段名列表 * 唯一键字段名列表
*/ */
private Set<String> uniKeyNames = new HashSet<>(); private Set<String> uniKeyNames = new HashSet<>();
/** /**
* 是否覆盖已存在数据 * 是否覆盖已存在数据
*/ */
private boolean enableCover = false; private boolean enableCover = false;
/** /**
* 是否忽略未找到的数据 * 是否忽略未找到的数据
*/ */
private boolean ignoreNotFound = false; private boolean ignoreNotFound = false;
/**
* 是否启用 Upsert 模式
* true默认根据唯一键匹配存在则更新不存在则追加
* false不匹配唯一键所有数据直接追加到表格末尾
*/
private boolean upsert = true;
/** /**
* 字段位置映射 (字段名 -> 列位置 "添加SPU" -> "A") * 字段位置映射 (字段名 -> 列位置 "添加SPU" -> "A")
*/ */
private Map<String, String> fieldsPositionMap = new HashMap<>(); private Map<String, String> fieldsPositionMap = new HashMap<>();
/** /**
* 获取标题行行号 * 获取标题行行号
* *
@ -53,7 +60,7 @@ public class MapTableConfig {
public int getTitleRow() { public int getTitleRow() {
return titleRow; return titleRow;
} }
/** /**
* 设置标题行行号 * 设置标题行行号
* *
@ -64,7 +71,7 @@ public class MapTableConfig {
this.titleRow = titleRow; this.titleRow = titleRow;
return this; return this;
} }
/** /**
* 获取数据起始行行号 * 获取数据起始行行号
* *
@ -73,7 +80,7 @@ public class MapTableConfig {
public int getHeadLine() { public int getHeadLine() {
return headLine; return headLine;
} }
/** /**
* 设置数据起始行行号 * 设置数据起始行行号
* *
@ -84,7 +91,7 @@ public class MapTableConfig {
this.headLine = headLine; this.headLine = headLine;
return this; return this;
} }
/** /**
* 获取唯一键字段名集合 * 获取唯一键字段名集合
* *
@ -93,7 +100,7 @@ public class MapTableConfig {
public Set<String> getUniKeyNames() { public Set<String> getUniKeyNames() {
return uniKeyNames; return uniKeyNames;
} }
/** /**
* 设置唯一键字段名集合 * 设置唯一键字段名集合
* *
@ -104,7 +111,7 @@ public class MapTableConfig {
this.uniKeyNames = uniKeyNames; this.uniKeyNames = uniKeyNames;
return this; return this;
} }
/** /**
* 添加唯一键字段名 * 添加唯一键字段名
* *
@ -115,7 +122,7 @@ public class MapTableConfig {
this.uniKeyNames.add(uniKeyName); this.uniKeyNames.add(uniKeyName);
return this; return this;
} }
/** /**
* 是否覆盖已存在数据 * 是否覆盖已存在数据
* *
@ -124,7 +131,7 @@ public class MapTableConfig {
public boolean isEnableCover() { public boolean isEnableCover() {
return enableCover; return enableCover;
} }
/** /**
* 设置是否覆盖已存在数据 * 设置是否覆盖已存在数据
* *
@ -135,7 +142,7 @@ public class MapTableConfig {
this.enableCover = enableCover; this.enableCover = enableCover;
return this; return this;
} }
/** /**
* 是否忽略未找到的数据 * 是否忽略未找到的数据
* *
@ -144,7 +151,7 @@ public class MapTableConfig {
public boolean isIgnoreNotFound() { public boolean isIgnoreNotFound() {
return ignoreNotFound; return ignoreNotFound;
} }
/** /**
* 设置是否忽略未找到的数据 * 设置是否忽略未找到的数据
* *
@ -155,7 +162,31 @@ public class MapTableConfig {
this.ignoreNotFound = ignoreNotFound; this.ignoreNotFound = ignoreNotFound;
return this; 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<String, String> getFieldsPositionMap() { public Map<String, String> getFieldsPositionMap() {
return fieldsPositionMap; return fieldsPositionMap;
} }
/** /**
* 设置字段位置映射 * 设置字段位置映射
* *
@ -175,7 +206,7 @@ public class MapTableConfig {
this.fieldsPositionMap = fieldsPositionMap; this.fieldsPositionMap = fieldsPositionMap;
return this; return this;
} }
/** /**
* 创建默认配置 * 创建默认配置
* *
@ -184,7 +215,7 @@ public class MapTableConfig {
public static MapTableConfig createDefault() { public static MapTableConfig createDefault() {
return new MapTableConfig(); return new MapTableConfig();
} }
/** /**
* 创建配置构建器 * 创建配置构建器
* *
@ -193,13 +224,13 @@ public class MapTableConfig {
public static Builder builder() { public static Builder builder() {
return new Builder(); return new Builder();
} }
/** /**
* 配置构建器 * 配置构建器
*/ */
public static class Builder { public static class Builder {
private final MapTableConfig config = new MapTableConfig(); private final MapTableConfig config = new MapTableConfig();
/** /**
* 设置标题行行号 * 设置标题行行号
* *
@ -210,7 +241,7 @@ public class MapTableConfig {
config.titleRow = titleRow; config.titleRow = titleRow;
return this; return this;
} }
/** /**
* 设置数据起始行行号 * 设置数据起始行行号
* *
@ -221,7 +252,7 @@ public class MapTableConfig {
config.headLine = headLine; config.headLine = headLine;
return this; return this;
} }
/** /**
* 设置唯一键字段名集合 * 设置唯一键字段名集合
* *
@ -232,7 +263,7 @@ public class MapTableConfig {
config.uniKeyNames = new HashSet<>(uniKeyNames); config.uniKeyNames = new HashSet<>(uniKeyNames);
return this; return this;
} }
/** /**
* 添加唯一键字段名 * 添加唯一键字段名
* *
@ -243,7 +274,7 @@ public class MapTableConfig {
config.uniKeyNames.add(uniKeyName); config.uniKeyNames.add(uniKeyName);
return this; return this;
} }
/** /**
* 设置是否覆盖已存在数据 * 设置是否覆盖已存在数据
* *
@ -254,7 +285,7 @@ public class MapTableConfig {
config.enableCover = enableCover; config.enableCover = enableCover;
return this; return this;
} }
/** /**
* 设置是否忽略未找到的数据 * 设置是否忽略未找到的数据
* *
@ -265,7 +296,21 @@ public class MapTableConfig {
config.ignoreNotFound = ignoreNotFound; config.ignoreNotFound = ignoreNotFound;
return this; 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); config.fieldsPositionMap = new HashMap<>(fieldsPositionMap);
return this; return this;
} }
/** /**
* 构建配置对象 * 构建配置对象
* *

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