feat(core): 新增Map数据写入飞书表格功能- 新增MapWriteBuilder类,支持链式调用写入Map格式数据

- 新增MapTableConfig配置类,用于替代注解配置
- 新增MapDataUtil工具类,提供唯一ID计算和字段映射功能- 在FsHelper中新增writeMap和writeMapBuilder方法
- 在WriteBuilder中增加空字段位置检查逻辑
- 完善字段位置映射处理,避免空指针异常
This commit is contained in:
liushuang 2025-10-16 16:06:38 +08:00
parent 9500a1df12
commit f9cff00b2f
5 changed files with 905 additions and 1 deletions

@ -5,11 +5,13 @@ import cn.isliu.core.FileData;
import cn.isliu.core.FsTableData; import cn.isliu.core.FsTableData;
import cn.isliu.core.Sheet; import cn.isliu.core.Sheet;
import cn.isliu.core.annotation.TableConf; import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.builder.MapWriteBuilder;
import cn.isliu.core.builder.ReadBuilder; import cn.isliu.core.builder.ReadBuilder;
import cn.isliu.core.builder.SheetBuilder; import cn.isliu.core.builder.SheetBuilder;
import cn.isliu.core.builder.WriteBuilder; import cn.isliu.core.builder.WriteBuilder;
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.enums.ErrorCode; import cn.isliu.core.enums.ErrorCode;
import cn.isliu.core.enums.FileType; import cn.isliu.core.enums.FileType;
import cn.isliu.core.logging.FsLogger; import cn.isliu.core.logging.FsLogger;
@ -216,6 +218,11 @@ public class FsHelper {
rowNum.set(rowNum.get() + 1); rowNum.set(rowNum.get() + 1);
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
String position = titlePostionMap.get(field); String position = titlePostionMap.get(field);
if (position == null || position.isEmpty()) {
return;
}
if (fieldValue instanceof FileData) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
String fileType = fileData.getFileType(); String fileType = fileData.getFileType();
@ -236,6 +243,11 @@ public class FsHelper {
int rowCou = rowCount.incrementAndGet(); int rowCou = rowCount.incrementAndGet();
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
String position = titlePostionMap.get(field); String position = titlePostionMap.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);
@ -286,4 +298,74 @@ public class FsHelper {
public static <T> WriteBuilder<T> writeBuilder(String sheetId, String spreadsheetToken, List<T> dataList) { public static <T> WriteBuilder<T> writeBuilder(String sheetId, String spreadsheetToken, List<T> dataList) {
return new WriteBuilder<>(sheetId, spreadsheetToken, dataList); return new WriteBuilder<>(sheetId, spreadsheetToken, dataList);
} }
/**
* Map 数据写入飞书表格
*
* 直接使用 Map 格式的数据写入飞书表格无需定义实体类和注解
* 适用于动态字段临时数据写入等场景
*
* 使用示例
* <pre>
* List&lt;Map&lt;String, Object&gt;&gt; dataList = new ArrayList&lt;&gt;();
* Map&lt;String, Object&gt; data = new HashMap&lt;&gt;();
* data.put("字段名1", "值1");
* data.put("字段名2", "值2");
* dataList.add(data);
*
* MapTableConfig config = MapTableConfig.builder()
* .titleRow(2)
* .headLine(3)
* .addUniKeyName("字段名1")
* .enableCover(true)
* .build();
*
* FsHelper.writeMap(sheetId, spreadsheetToken, dataList, config);
* </pre>
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param dataList Map数据列表每个Map的key为字段名value为字段值
* @param config 表格配置包含标题行数据起始行唯一键等配置信息
* @return 写入操作结果
*/
public static Object writeMap(String sheetId, String spreadsheetToken,
List<Map<String, Object>> dataList, MapTableConfig config) {
return new MapWriteBuilder(sheetId, spreadsheetToken, dataList)
.config(config)
.build();
}
/**
* 创建 Map 数据写入构建器
*
* 返回一个 Map 数据写入构建器实例支持链式调用和灵活配置
* 相比直接使用 writeMap 方法构建器方式提供了更灵活的配置方式
*
* 使用示例
* <pre>
* List&lt;Map&lt;String, Object&gt;&gt; dataList = new ArrayList&lt;&gt;();
* Map&lt;String, Object&gt; data = new HashMap&lt;&gt;();
* data.put("字段名1", "值1");
* data.put("字段名2", "值2");
* dataList.add(data);
*
* FsHelper.writeMapBuilder(sheetId, spreadsheetToken, dataList)
* .titleRow(2)
* .headLine(3)
* .addUniKeyName("字段名1")
* .enableCover(true)
* .ignoreNotFound(false)
* .build();
* </pre>
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param dataList 要写入的Map数据列表每个Map的key为字段名value为字段值
* @return MapWriteBuilder实例支持链式调用
*/
public static MapWriteBuilder writeMapBuilder(String sheetId, String spreadsheetToken,
List<Map<String, Object>> dataList) {
return new MapWriteBuilder(sheetId, spreadsheetToken, dataList);
}
} }

@ -0,0 +1,390 @@
package cn.isliu.core.builder;
import cn.isliu.core.*;
import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient;
import cn.isliu.core.config.MapTableConfig;
import cn.isliu.core.enums.ErrorCode;
import cn.isliu.core.enums.FileType;
import cn.isliu.core.logging.FsLogger;
import cn.isliu.core.service.CustomValueService;
import cn.isliu.core.utils.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static cn.isliu.core.utils.FsTableUtil.*;
/**
* Map 数据写入构建器
*
* 提供链式调用方式写入 Map 格式的飞书表格数据
*
* @author Ls
* @since 2025-10-16
*/
public class MapWriteBuilder {
private final String sheetId;
private final String spreadsheetToken;
private final List<Map<String, Object>> dataList;
private MapTableConfig config;
/**
* 构造函数
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param dataList 要写入的Map数据列表
*/
public MapWriteBuilder(String sheetId, String spreadsheetToken, List<Map<String, Object>> dataList) {
this.sheetId = sheetId;
this.spreadsheetToken = spreadsheetToken;
this.dataList = dataList;
this.config = MapTableConfig.createDefault();
}
/**
* 设置表格配置
*
* @param config Map表格配置
* @return MapWriteBuilder实例
*/
public MapWriteBuilder config(MapTableConfig config) {
this.config = config;
return this;
}
/**
* 设置标题行
*
* @param titleRow 标题行行号
* @return MapWriteBuilder实例
*/
public MapWriteBuilder titleRow(int titleRow) {
this.config.setTitleRow(titleRow);
return this;
}
/**
* 设置数据起始行
*
* @param headLine 数据起始行号
* @return MapWriteBuilder实例
*/
public MapWriteBuilder headLine(int headLine) {
this.config.setHeadLine(headLine);
return this;
}
/**
* 设置唯一键字段
*
* @param uniKeyNames 唯一键字段名集合
* @return MapWriteBuilder实例
*/
public MapWriteBuilder uniKeyNames(Set<String> uniKeyNames) {
this.config.setUniKeyNames(uniKeyNames);
return this;
}
/**
* 添加唯一键字段
*
* @param uniKeyName 唯一键字段名
* @return MapWriteBuilder实例
*/
public MapWriteBuilder addUniKeyName(String uniKeyName) {
this.config.addUniKeyName(uniKeyName);
return this;
}
/**
* 设置是否覆盖已存在数据
*
* @param enableCover 是否覆盖
* @return MapWriteBuilder实例
*/
public MapWriteBuilder enableCover(boolean enableCover) {
this.config.setEnableCover(enableCover);
return this;
}
/**
* 设置是否忽略未找到的数据
*
* @param ignoreNotFound 是否忽略
* @return MapWriteBuilder实例
*/
public MapWriteBuilder ignoreNotFound(boolean ignoreNotFound) {
this.config.setIgnoreNotFound(ignoreNotFound);
return this;
}
/**
* 执行数据写入
*
* @return 写入操作结果
*/
public Object build() {
if (dataList.isEmpty()) {
FsLogger.warn("【Map写入】数据列表为空跳过写入操作");
return null;
}
FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
// 读取表格数据以获取字段位置映射和现有数据
Map<String, String> titlePostionMap = readFieldsPositionMap(sheet, client);
config.setFieldsPositionMap(titlePostionMap);
// 读取现有数据用于匹配和更新
Map<String, Integer> currTableRowMap = readExistingData(sheet, client, titlePostionMap);
// 计算下一个可用行号
int nextAvailableRow = calculateNextAvailableRow(currTableRowMap, config.getHeadLine());
// 初始化批量插入对象
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder =
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);
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()) {
// 插入新行
int newRow = rowCount.incrementAndGet();
processDataRow(data, titlePostionMap, newRow, resultValuesBuilder,
fileDataList, config.isEnableCover());
}
}
// 检查是否需要扩展行数
ensureSufficientRows(sheet, rowCount.get(), client);
// 上传文件
uploadFiles(fileDataList, client);
// 批量写入数据
return batchWriteValues(resultValuesBuilder, client);
}
/**
* 读取字段位置映射
*/
private Map<String, String> readFieldsPositionMap(Sheet sheet, FeishuClient client) {
int titleRow = config.getTitleRow();
int colCount = sheet.getGridProperties().getColumnCount();
// 读取标题行数据
ValuesBatch valuesBatch = FsApiUtil.getSheetData(
sheet.getSheetId(), spreadsheetToken,
"A" + titleRow,
getColumnName(colCount - 1) + titleRow,
client
);
Map<String, String> fieldsPositionMap = new HashMap<>();
if (valuesBatch != null && valuesBatch.getValueRanges() != null) {
for (ValueRange valueRange : valuesBatch.getValueRanges()) {
if (valueRange.getValues() != null && !valueRange.getValues().isEmpty()) {
List<Object> titleRowValues = valueRange.getValues().get(0);
for (int i = 0; i < titleRowValues.size(); i++) {
Object value = titleRowValues.get(i);
if (value != null) {
String fieldName = value.toString();
String columnPosition = getColumnName(i);
fieldsPositionMap.put(fieldName, columnPosition);
}
}
}
}
}
return fieldsPositionMap;
}
/**
* 读取现有数据
*/
private Map<String, Integer> readExistingData(Sheet sheet, FeishuClient client, Map<String, String> titlePostionMap) {
int headLine = config.getHeadLine();
int titleRow = config.getTitleRow();
int totalRow = sheet.getGridProperties().getRowCount();
int colCount = sheet.getGridProperties().getColumnCount();
int startOffset = 1;
// 批量读取数据
int rowCountPerBatch = Math.min(totalRow, 100);
int actualRows = Math.max(0, totalRow - startOffset);
int batchCount = (actualRows + rowCountPerBatch - 1) / rowCountPerBatch;
List<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 = 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);
});
// 转换为带字段名的数据并计算唯一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));
}
/**
* 计算下一个可用行号
*/
private int calculateNextAvailableRow(Map<String, Integer> currTableRowMap, int headLine) {
if (currTableRowMap.isEmpty()) {
return headLine;
}
return currTableRowMap.values().stream()
.max(Integer::compareTo)
.map(maxRow -> maxRow + 1)
.orElse(headLine);
}
/**
* 处理单行数据
*/
private void processDataRow(Map<String, Object> data, Map<String, String> titlePostionMap,
int rowNum, CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder,
List<FileData> fileDataList, boolean enableCover) {
data.forEach((field, fieldValue) -> {
String position = titlePostionMap.get(field);
if (position == null || position.isEmpty()) {
return;
}
// 处理文件数据
if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue;
String fileType = fileData.getFileType();
if (fileType.equals(FileType.IMAGE.getType())) {
fileData.setSheetId(sheetId);
fileData.setSpreadsheetToken(spreadsheetToken);
fileData.setPosition(position + rowNum);
fileDataList.add(fileData);
}
}
// 添加到批量写入
if (enableCover || fieldValue != null) {
resultValuesBuilder.addRange(sheetId, position + rowNum, position + rowNum)
.addRow(GenerateUtil.getRowData(fieldValue));
}
});
}
/**
* 确保行数足够
*/
private void ensureSufficientRows(Sheet sheet, int requiredRows, FeishuClient client) {
int rowTotal = sheet.getGridProperties().getRowCount();
if (requiredRows >= rowTotal) {
FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", requiredRows - rowTotal, client);
}
}
/**
* 上传文件
*/
private void uploadFiles(List<FileData> fileDataList, FeishuClient client) {
fileDataList.forEach(fileData -> {
try {
FsApiUtil.imageUpload(
fileData.getImageData(),
fileData.getFileName(),
fileData.getPosition(),
fileData.getSheetId(),
fileData.getSpreadsheetToken(),
client
);
} catch (Exception e) {
FsLogger.error(ErrorCode.BUSINESS_LOGIC_ERROR,
"【飞书表格】Map写入-文件上传异常! " + fileData.getFileUrl());
}
});
}
/**
* 批量写入数据
*/
private Object batchWriteValues(CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder,
FeishuClient client) {
CustomValueService.ValueRequest build = resultValuesBuilder.build();
CustomValueService.ValueBatchUpdatePutRequest batchPutValues = build.getBatchPutValues();
List<CustomValueService.ValueRangeItem> valueRanges = batchPutValues.getValueRanges();
if (valueRanges != null && !valueRanges.isEmpty()) {
return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, build, client);
}
FsLogger.warn("【Map写入】没有数据需要写入");
return null;
}
}

@ -198,6 +198,10 @@ public class WriteBuilder<T> {
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) { if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue; FileData fileData = (FileData) fieldValue;
String fileType = fileData.getFileType(); String fileType = fileData.getFileType();
@ -217,8 +221,12 @@ public class WriteBuilder<T> {
int rowCou = rowCount.incrementAndGet(); int rowCou = rowCount.incrementAndGet();
Map<String, String> finalTitlePostionMap1 = titlePostionMap; Map<String, String> finalTitlePostionMap1 = titlePostionMap;
values.forEach((field, fieldValue) -> { values.forEach((field, fieldValue) -> {
String position = finalTitlePostionMap1.get(field); 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);

@ -0,0 +1,290 @@
package cn.isliu.core.config;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Map方式表格配置类
*
* 用于替代注解配置支持使用Map方式操作飞书表格
*
* @author Ls
* @since 2025-10-16
*/
public class MapTableConfig {
/**
* 标题行行号从1开始
*/
private int titleRow = 1;
/**
* 数据起始行行号从1开始
*/
private int headLine = 1;
/**
* 唯一键字段名列表
*/
private Set<String> uniKeyNames = new HashSet<>();
/**
* 是否覆盖已存在数据
*/
private boolean enableCover = false;
/**
* 是否忽略未找到的数据
*/
private boolean ignoreNotFound = false;
/**
* 字段位置映射 (字段名 -> 列位置 "添加SPU" -> "A")
*/
private Map<String, String> fieldsPositionMap = new HashMap<>();
/**
* 获取标题行行号
*
* @return 标题行行号
*/
public int getTitleRow() {
return titleRow;
}
/**
* 设置标题行行号
*
* @param titleRow 标题行行号
* @return MapTableConfig实例支持链式调用
*/
public MapTableConfig setTitleRow(int titleRow) {
this.titleRow = titleRow;
return this;
}
/**
* 获取数据起始行行号
*
* @return 数据起始行行号
*/
public int getHeadLine() {
return headLine;
}
/**
* 设置数据起始行行号
*
* @param headLine 数据起始行行号
* @return MapTableConfig实例支持链式调用
*/
public MapTableConfig setHeadLine(int headLine) {
this.headLine = headLine;
return this;
}
/**
* 获取唯一键字段名集合
*
* @return 唯一键字段名集合
*/
public Set<String> getUniKeyNames() {
return uniKeyNames;
}
/**
* 设置唯一键字段名集合
*
* @param uniKeyNames 唯一键字段名集合
* @return MapTableConfig实例支持链式调用
*/
public MapTableConfig setUniKeyNames(Set<String> uniKeyNames) {
this.uniKeyNames = uniKeyNames;
return this;
}
/**
* 添加唯一键字段名
*
* @param uniKeyName 唯一键字段名
* @return MapTableConfig实例支持链式调用
*/
public MapTableConfig addUniKeyName(String uniKeyName) {
this.uniKeyNames.add(uniKeyName);
return this;
}
/**
* 是否覆盖已存在数据
*
* @return true表示覆盖false表示不覆盖
*/
public boolean isEnableCover() {
return enableCover;
}
/**
* 设置是否覆盖已存在数据
*
* @param enableCover true表示覆盖false表示不覆盖
* @return MapTableConfig实例支持链式调用
*/
public MapTableConfig setEnableCover(boolean enableCover) {
this.enableCover = enableCover;
return this;
}
/**
* 是否忽略未找到的数据
*
* @return true表示忽略false表示不忽略
*/
public boolean isIgnoreNotFound() {
return ignoreNotFound;
}
/**
* 设置是否忽略未找到的数据
*
* @param ignoreNotFound true表示忽略false表示不忽略
* @return MapTableConfig实例支持链式调用
*/
public MapTableConfig setIgnoreNotFound(boolean ignoreNotFound) {
this.ignoreNotFound = ignoreNotFound;
return this;
}
/**
* 获取字段位置映射
*
* @return 字段位置映射
*/
public Map<String, String> getFieldsPositionMap() {
return fieldsPositionMap;
}
/**
* 设置字段位置映射
*
* @param fieldsPositionMap 字段位置映射
* @return MapTableConfig实例支持链式调用
*/
public MapTableConfig setFieldsPositionMap(Map<String, String> fieldsPositionMap) {
this.fieldsPositionMap = fieldsPositionMap;
return this;
}
/**
* 创建默认配置
*
* @return 默认配置实例
*/
public static MapTableConfig createDefault() {
return new MapTableConfig();
}
/**
* 创建配置构建器
*
* @return 配置构建器实例
*/
public static Builder builder() {
return new Builder();
}
/**
* 配置构建器
*/
public static class Builder {
private final MapTableConfig config = new MapTableConfig();
/**
* 设置标题行行号
*
* @param titleRow 标题行行号
* @return Builder实例
*/
public Builder titleRow(int titleRow) {
config.titleRow = titleRow;
return this;
}
/**
* 设置数据起始行行号
*
* @param headLine 数据起始行行号
* @return Builder实例
*/
public Builder headLine(int headLine) {
config.headLine = headLine;
return this;
}
/**
* 设置唯一键字段名集合
*
* @param uniKeyNames 唯一键字段名集合
* @return Builder实例
*/
public Builder uniKeyNames(Set<String> uniKeyNames) {
config.uniKeyNames = new HashSet<>(uniKeyNames);
return this;
}
/**
* 添加唯一键字段名
*
* @param uniKeyName 唯一键字段名
* @return Builder实例
*/
public Builder addUniKeyName(String uniKeyName) {
config.uniKeyNames.add(uniKeyName);
return this;
}
/**
* 设置是否覆盖已存在数据
*
* @param enableCover true表示覆盖false表示不覆盖
* @return Builder实例
*/
public Builder enableCover(boolean enableCover) {
config.enableCover = enableCover;
return this;
}
/**
* 设置是否忽略未找到的数据
*
* @param ignoreNotFound true表示忽略false表示不忽略
* @return Builder实例
*/
public Builder ignoreNotFound(boolean ignoreNotFound) {
config.ignoreNotFound = ignoreNotFound;
return this;
}
/**
* 设置字段位置映射
*
* @param fieldsPositionMap 字段位置映射
* @return Builder实例
*/
public Builder fieldsPositionMap(Map<String, String> fieldsPositionMap) {
config.fieldsPositionMap = new HashMap<>(fieldsPositionMap);
return this;
}
/**
* 构建配置对象
*
* @return MapTableConfig实例
*/
public MapTableConfig build() {
return config;
}
}
}

@ -0,0 +1,134 @@
package cn.isliu.core.utils;
import cn.isliu.core.config.MapTableConfig;
import java.util.*;
/**
* Map 数据处理工具类
*
* 提供Map格式数据的处理方法包括唯一ID计算字段位置映射等
*
* @author Ls
* @since 2025-10-16
*/
public class MapDataUtil {
/**
* 计算 Map 数据的唯一ID
*
* 根据配置的唯一键字段计算数据的唯一标识
* 如果没有指定唯一键则使用所有字段计算
*
* @param data Map数据
* @param config 表格配置
* @return 唯一IDSHA256哈希值
*/
public static String calculateUniqueId(Map<String, Object> data, MapTableConfig config) {
if (data == null || data.isEmpty()) {
return null;
}
Set<String> uniKeyNames = config.getUniKeyNames();
if (uniKeyNames == null || uniKeyNames.isEmpty()) {
// 如果没有指定唯一键使用所有字段计算
String jsonStr = StringUtil.mapToJson(data);
return StringUtil.getSHA256(jsonStr);
}
// 使用指定的唯一键字段计算
List<Object> uniKeyValues = new ArrayList<>();
for (String key : uniKeyNames) {
if (data.containsKey(key)) {
uniKeyValues.add(data.get(key));
}
}
if (uniKeyValues.isEmpty()) {
return null;
}
String jsonStr = StringUtil.listToJson(uniKeyValues);
return StringUtil.getSHA256(jsonStr);
}
/**
* 从标题行数据构建字段位置映射
*
* 将标题行的列位置与字段名称进行映射用于后续数据写入时定位单元格位置
*
* @param titleRowData 标题行数据key为列位置"A""B"value为字段名称
* @return 字段位置映射key为字段名称value为列位置
*/
public static Map<String, String> buildFieldsPositionMap(Map<String, String> titleRowData) {
if (titleRowData == null || titleRowData.isEmpty()) {
return new HashMap<>();
}
Map<String, String> fieldsPositionMap = new HashMap<>();
titleRowData.forEach((position, fieldName) -> {
if (fieldName != null && !fieldName.isEmpty()) {
fieldsPositionMap.put(fieldName, position);
}
});
return fieldsPositionMap;
}
/**
* 验证数据字段是否与表格字段匹配
*
* 检查数据中的字段是否在表格的字段位置映射中存在
*
* @param data 数据Map
* @param fieldsPositionMap 字段位置映射
* @return 未匹配的字段列表
*/
public static List<String> validateDataFields(Map<String, Object> data, Map<String, String> fieldsPositionMap) {
if (data == null || data.isEmpty()) {
return new ArrayList<>();
}
if (fieldsPositionMap == null || fieldsPositionMap.isEmpty()) {
return new ArrayList<>(data.keySet());
}
List<String> unmatchedFields = new ArrayList<>();
for (String fieldName : data.keySet()) {
if (!fieldsPositionMap.containsKey(fieldName)) {
unmatchedFields.add(fieldName);
}
}
return unmatchedFields;
}
/**
* 过滤数据字段
*
* 只保留在字段位置映射中存在的字段
*
* @param data 原始数据Map
* @param fieldsPositionMap 字段位置映射
* @return 过滤后的数据Map
*/
public static Map<String, Object> filterDataFields(Map<String, Object> data, Map<String, String> fieldsPositionMap) {
if (data == null || data.isEmpty()) {
return new HashMap<>();
}
if (fieldsPositionMap == null || fieldsPositionMap.isEmpty()) {
return new HashMap<>();
}
Map<String, Object> filteredData = new HashMap<>();
data.forEach((fieldName, fieldValue) -> {
if (fieldsPositionMap.containsKey(fieldName)) {
filteredData.put(fieldName, fieldValue);
}
});
return filteredData;
}
}