feat(core): 增强飞书表格功能与工具类支持

-为 ConvertFieldUtil、FileUtil、FsApiUtil 和 FsTableUtil 类添加更详细的 JavaDoc 注释,提升代码可读性- 在 FileUtil 中优化图片文件判断逻辑并增强 getFileName 方法以支持 URL 场景
- 扩展 FsApiUtil 支持更多飞书 API 参数格式化及异常处理
- 在 FsHelper 中引入 ReadBuilder 和 WriteBuilder 构建器模式,提供链式调用方式操作飞书表格读写
- FsTableUtil 新增对忽略唯一字段的支持,并扩展表头模板构建能力,支持字段描述自定义- GenerateUtil优化数字转换精度控制,并修复部分日志提示文案
- OptionsValueProcess 接口增加泛型参数支持,提高灵活性和类型安全性```
This commit is contained in:
liushuang 2025-09-26 19:15:06 +08:00
parent 3e25a91aed
commit 891b019ae3
11 changed files with 823 additions and 160 deletions

@ -5,7 +5,9 @@ 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.ReadBuilder;
import cn.isliu.core.builder.SheetBuilder; import cn.isliu.core.builder.SheetBuilder;
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.enums.ErrorCode; import cn.isliu.core.enums.ErrorCode;
@ -20,6 +22,8 @@ import com.google.gson.JsonObject;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -70,8 +74,12 @@ public class FsHelper {
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
} }
try {
// 5 设置表格下拉 // 5 设置表格下拉
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc()); FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc());
} catch (Exception e) {
Logger.getLogger(SheetBuilder.class.getName()).log(Level.SEVERE,"【表格构建器】设置表格下拉异常sheetId:" + sheetId + ", 错误信息:{}", e.getMessage());
}
return sheetId; return sheetId;
} }
@ -133,6 +141,22 @@ public class FsHelper {
return results; return results;
} }
/**
* 创建飞书表格数据读取构建器
*
* 返回一个数据读取构建器实例支持链式调用和高级配置选项
* 如忽略唯一字段等功能
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param clazz 实体类Class对象用于数据映射
* @param <T> 实体类泛型
* @return ReadBuilder实例支持链式调用
*/
public static <T> ReadBuilder<T> readBuilder(String sheetId, String spreadsheetToken, Class<T> clazz) {
return new ReadBuilder<>(sheetId, spreadsheetToken, clazz);
}
/** /**
* 将数据写入飞书表格 * 将数据写入飞书表格
* *
@ -244,4 +268,20 @@ public class FsHelper {
return resp; return resp;
} }
/**
* 创建飞书表格数据写入构建器
*
* 返回一个数据写入构建器实例支持链式调用和高级配置选项
* 如忽略唯一字段等功能
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param dataList 要写入的数据列表
* @param <T> 实体类泛型
* @return WriteBuilder实例支持链式调用
*/
public static <T> WriteBuilder<T> writeBuilder(String sheetId, String spreadsheetToken, List<T> dataList) {
return new WriteBuilder<>(sheetId, spreadsheetToken, dataList);
}
} }

@ -0,0 +1,130 @@
package cn.isliu.core.builder;
import cn.isliu.core.BaseEntity;
import cn.isliu.core.FsTableData;
import cn.isliu.core.Sheet;
import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient;
import cn.isliu.core.pojo.FieldProperty;
import cn.isliu.core.utils.*;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 数据读取构建器
*
* 提供链式调用方式读取飞书表格数据支持忽略唯一字段等高级功能
*/
public class ReadBuilder<T> {
private final String sheetId;
private final String spreadsheetToken;
private final Class<T> clazz;
private List<String> ignoreUniqueFields;
/**
* 构造函数
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param clazz 实体类Class对象
*/
public ReadBuilder(String sheetId, String spreadsheetToken, Class<T> clazz) {
this.sheetId = sheetId;
this.spreadsheetToken = spreadsheetToken;
this.clazz = clazz;
}
/**
* 设置计算唯一标识时忽略的字段列表
*
* 指定在计算数据行唯一标识时要忽略的字段名称列表
* 这些字段的值变化不会影响数据行的唯一性判断
*
* @param fields 要忽略的字段名称列表
* @return ReadBuilder实例支持链式调用
*/
public ReadBuilder<T> ignoreUniqueFields(List<String> fields) {
this.ignoreUniqueFields = new ArrayList<>(fields);
return this;
}
/**
* 执行数据读取并返回实体类对象列表
*
* 根据配置的参数从飞书表格中读取数据并映射到实体类对象列表中
*
* @return 映射后的实体类对象列表
*/
public List<T> build() {
List<T> results = new ArrayList<>();
FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
TableConf tableConf = PropertyUtil.getTableConf(clazz);
Map<String, FieldProperty> fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
// 处理忽略字段名称映射
List<String> processedIgnoreFields = processIgnoreFields(fieldsMap);
// 使用支持忽略字段的方法获取表格数据
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields);
List<String> fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList());
fsTableDataList.forEach(tableData -> {
Object data = tableData.getData();
if (data instanceof HashMap) {
Map<String, Object> rowData = (HashMap<String, Object>) data;
JsonObject jsonObject = JSONUtil.convertMapToJsonObject(rowData);
Map<String, Object> dataMap = ConvertFieldUtil.convertPositionToField(jsonObject, fieldsMap);
T t = GenerateUtil.generateInstance(fieldPathList, clazz, dataMap);
if (t instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) t;
baseEntity.setUniqueId(tableData.getUniqueId());
baseEntity.setRow(tableData.getRow());
baseEntity.setRowData(rowData);
}
results.add(t);
}
});
return results;
}
/**
* 处理忽略字段名称映射
*
* 将实体字段名称映射为表格列名称
*
* @param fieldsMap 字段映射
* @return 处理后的忽略字段列表
*/
private List<String> processIgnoreFields(Map<String, FieldProperty> fieldsMap) {
if (ignoreUniqueFields == null || ignoreUniqueFields.isEmpty()) {
return new ArrayList<>();
}
List<String> processedFields = new ArrayList<>();
// 遍历字段映射找到对应的表格列名
for (Map.Entry<String, FieldProperty> entry : fieldsMap.entrySet()) {
String fieldName = entry.getValue().getField();
// 获取字段的最后一部分名称去掉嵌套路径
String simpleFieldName = fieldName.substring(fieldName.lastIndexOf(".") + 1);
// 如果忽略字段列表中包含此字段名则添加对应的表格列名
if (ignoreUniqueFields.contains(simpleFieldName)) {
String tableColumnName = entry.getKey(); // 表格列名注解中的value值
processedFields.add(tableColumnName);
}
}
return processedFields;
}
}

@ -8,10 +8,14 @@ import cn.isliu.core.service.CustomCellService;
import cn.isliu.core.utils.FsApiUtil; import cn.isliu.core.utils.FsApiUtil;
import cn.isliu.core.utils.FsTableUtil; import cn.isliu.core.utils.FsTableUtil;
import cn.isliu.core.utils.PropertyUtil; import cn.isliu.core.utils.PropertyUtil;
import cn.isliu.core.utils.StringUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -25,6 +29,8 @@ public class SheetBuilder<T> {
private final String spreadsheetToken; private final String spreadsheetToken;
private final Class<T> clazz; private final Class<T> clazz;
private List<String> includeFields; private List<String> includeFields;
private final Map<String, Object> customProperties = new HashMap<>();
private final Map<String, String> fieldDescriptions = new HashMap<>();
/** /**
* 构造函数 * 构造函数
@ -52,6 +58,91 @@ public class SheetBuilder<T> {
return this; return this;
} }
/**
* 设置自定义属性
*
* 添加一个自定义属性可以在构建表格时使用
*
* @param key 属性键
* @param value 属性值
* @return SheetBuilder实例支持链式调用
*/
public SheetBuilder<T> addCustomProperty(String key, Object value) {
this.customProperties.put(key, value);
return this;
}
/**
* 批量设置自定义属性
*
* 批量添加自定义属性可以在构建表格时使用
*
* @param properties 自定义属性映射
* @return SheetBuilder实例支持链式调用
*/
public SheetBuilder<T> addCustomProperties(Map<String, Object> properties) {
this.customProperties.putAll(properties);
return this;
}
/**
* 获取自定义属性
*
* 根据键获取已设置的自定义属性值
*
* @param key 属性键
* @return 属性值如果不存在则返回null
*/
public Object getCustomProperty(String key) {
return this.customProperties.get(key);
}
/**
* 获取所有自定义属性
*
* @return 包含所有自定义属性的映射
*/
public Map<String, Object> getCustomProperties() {
return new HashMap<>(this.customProperties);
}
/**
* 设置字段描述映射
*
* 为实体类字段设置自定义描述信息用于在表格描述行中显示
* 如果字段在映射中存在描述则使用映射中的描述否则使用注解中的描述
*
* @param fieldDescriptions 字段名到描述的映射key为字段名value为描述文本
* @return SheetBuilder实例支持链式调用
*/
public SheetBuilder<T> fieldDescription(Map<String, String> fieldDescriptions) {
this.fieldDescriptions.putAll(fieldDescriptions);
return this;
}
/**
* 设置单个字段描述
*
* 为指定字段设置自定义描述信息
*
* @param fieldName 字段名
* @param description 描述文本
* @return SheetBuilder实例支持链式调用
*/
public SheetBuilder<T> fieldDescription(String fieldName, String description) {
this.fieldDescriptions.put(fieldName, description);
return this;
}
/**
* 获取字段描述映射
*
* @return 包含所有字段描述的映射
*/
public Map<String, String> getFieldDescriptions() {
return new HashMap<>(this.fieldDescriptions);
}
/** /**
* 构建表格并返回工作表ID * 构建表格并返回工作表ID
* *
@ -79,7 +170,7 @@ public class SheetBuilder<T> {
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2添加表头数据 // 2添加表头数据
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf), client); FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, includeFields, tableConf, fieldDescriptions), client);
// 3设置表格样式 // 3设置表格样式
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken); FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken);
@ -97,7 +188,11 @@ public class SheetBuilder<T> {
} }
// 6设置表格下拉 // 6设置表格下拉
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc()); try {
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties);
} catch (Exception e) {
Logger.getLogger(SheetBuilder.class.getName()).log(Level.SEVERE,"【表格构建器】设置表格下拉异常sheetId:" + sheetId + ", 错误信息:{}", e.getMessage());
}
return sheetId; return sheetId;
} }
@ -116,7 +211,14 @@ public class SheetBuilder<T> {
// 根据字段名过滤保留指定的字段 // 根据字段名过滤保留指定的字段
return allFieldsMap.entrySet().stream() return allFieldsMap.entrySet().stream()
.filter(entry -> includeFields.contains(entry.getValue().getField())) .filter(entry -> {
String field = entry.getValue().getField();
field = field.substring(field.lastIndexOf(".") + 1);
if (field.isEmpty()) {
return false;
}
return includeFields.contains(StringUtil.toUnderscoreCase(field));
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
} }

@ -0,0 +1,293 @@
package cn.isliu.core.builder;
import cn.isliu.core.FileData;
import cn.isliu.core.FsTableData;
import cn.isliu.core.Sheet;
import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient;
import cn.isliu.core.enums.ErrorCode;
import cn.isliu.core.enums.FileType;
import cn.isliu.core.logging.FsLogger;
import cn.isliu.core.pojo.FieldProperty;
import cn.isliu.core.service.CustomValueService;
import cn.isliu.core.utils.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
* 数据写入构建器
*
* 提供链式调用方式写入飞书表格数据支持忽略唯一字段等高级功能
*/
public class WriteBuilder<T> {
private final String sheetId;
private final String spreadsheetToken;
private final List<T> dataList;
private List<String> ignoreUniqueFields;
private Class<?> clazz;
private boolean ignoreNotFound;
/**
* 构造函数
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param dataList 要写入的数据列表
*/
public WriteBuilder(String sheetId, String spreadsheetToken, List<T> dataList) {
this.sheetId = sheetId;
this.spreadsheetToken = spreadsheetToken;
this.dataList = dataList;
this.clazz = null;
this.ignoreNotFound = false;
}
/**
* 设置计算唯一标识时忽略的字段列表
*
* 指定在计算数据行唯一标识时要忽略的字段名称列表
* 这些字段的值变化不会影响数据行的唯一性判断
*
* @param fields 要忽略的字段名称列表
* @return WriteBuilder实例支持链式调用
*/
public WriteBuilder<T> ignoreUniqueFields(List<String> fields) {
this.ignoreUniqueFields = new ArrayList<>(fields);
return this;
}
/**
* 设置用于解析注解的实体类
*
* 指定用于解析@TableProperty注解的实体类这个类可以与数据列表的类型不同
* 主要用于获取字段映射关系和表格配置信息
*
* @param clazz 用于解析注解的实体类
* @return WriteBuilder实例支持链式调用
*/
public WriteBuilder<T> clazz(Class<?> clazz) {
this.clazz = clazz;
return this;
}
/**
* 设置是否忽略未找到的数据
*
* 指定在写入数据时是否忽略未找到的字段如果设置为true
* 当数据中包含表格中不存在的字段时写入操作将继续执行
* 而不会抛出异常默认值为false
*
* @param ignoreNotFound 是否忽略未找到的字段默认值为false
* @return WriteBuilder实例支持链式调用
*/
public WriteBuilder<T> ignoreNotFound(boolean ignoreNotFound) {
this.ignoreNotFound = ignoreNotFound;
return this;
}
/**
* 执行数据写入并返回操作结果
*
* 根据配置的参数将数据写入到飞书表格中支持新增和更新操作
*
* @return 写入操作结果
*/
public Object build() {
if (dataList.isEmpty()) {
return null;
}
Class<?> aClass = clazz;
Map<String, FieldProperty> fieldsMap;
TableConf tableConf = PropertyUtil.getTableConf(aClass);
Map<String, String> fieldMap = new HashMap<>();
Class<?> sourceClass = dataList.get(0).getClass();
if (aClass.equals(sourceClass)) {
fieldsMap = PropertyUtil.getTablePropertyFieldsMap(aClass);
} else {
fieldsMap = PropertyUtil.getTablePropertyFieldsMap(sourceClass);
}
fieldsMap.forEach((field, fieldProperty) -> fieldMap.put(field, fieldProperty.getField()));
// 处理忽略字段名称映射
List<String> processedIgnoreFields = processIgnoreFields(fieldsMap);
FeishuClient client = FsClient.getInstance().getClient();
Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken);
// 使用支持忽略字段的方法获取表格数据
List<FsTableData> fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields);
Map<String, Integer> currTableRowMap = fsTableDataList.stream().collect(Collectors.toMap(FsTableData::getUniqueId, FsTableData::getRow));
final Integer[] row = {0};
fsTableDataList.forEach(fsTableData -> {
if (fsTableData.getRow() > row[0]) {
row[0] = fsTableData.getRow();
}
});
Map<String, String> titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf);
// 初始化批量插入对象
CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues();
List<FileData> fileDataList = new ArrayList<>();
AtomicInteger rowCount = new AtomicInteger(row[0] + 1);
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, aClass);
} else {
uniqueId = GenerateUtil.getUniqueId(data);
}
AtomicReference<Integer> rowNum = new AtomicReference<>(currTableRowMap.get(uniqueId));
if (uniqueId != null && rowNum.get() != null) {
rowNum.set(rowNum.get() + 1);
values.forEach((field, fieldValue) -> {
if (!tableConf.enableCover() && fieldValue == null) {
return;
}
String position = titlePostionMap.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);
}
}
resultValuesBuilder.addRange(sheetId, position + rowNum.get(), position + rowNum.get())
.addRow(GenerateUtil.getRowData(fieldValue));
});
} else if (!ignoreNotFound) {
int rowCou = rowCount.incrementAndGet();
values.forEach((field, fieldValue) -> {
if (!tableConf.enableCover() && fieldValue == null) {
return;
}
String position = titlePostionMap.get(field);
if (fieldValue instanceof FileData) {
FileData fileData = (FileData) fieldValue;
fileData.setSheetId(sheetId);
fileData.setSpreadsheetToken(spreadsheetToken);
fileData.setPosition(position + rowCou);
fileDataList.add(fileData);
}
resultValuesBuilder.addRange(sheetId, position + rowCou, position + rowCou)
.addRow(GenerateUtil.getRowData(fieldValue));
});
}
}
int rowTotal = sheet.getGridProperties().getRowCount();
int rowNum = rowCount.get();
if (rowNum > rowTotal) {
FsApiUtil.addRowColumns(sheetId, spreadsheetToken, "ROWS", rowTotal - rowNum, 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, "【飞书表格】 文件上传-文件上传异常! " + fileData.getFileUrl());
}
});
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, resultValuesBuilder.build(), client);
}
return null;
}
/**
* 处理忽略字段名称映射
*
* 将实体字段名称映射为表格列名称
*
* @param fieldsMap 字段映射
* @return 处理后的忽略字段列表
*/
private List<String> processIgnoreFields(Map<String, FieldProperty> fieldsMap) {
if (ignoreUniqueFields == null || ignoreUniqueFields.isEmpty()) {
return new ArrayList<>();
}
List<String> processedFields = new ArrayList<>();
// 遍历字段映射找到对应的表格列名
for (Map.Entry<String, FieldProperty> entry : fieldsMap.entrySet()) {
String fieldName = entry.getValue().getField();
// 获取字段的最后一部分名称去掉嵌套路径
String simpleFieldName = fieldName.substring(fieldName.lastIndexOf(".") + 1);
// 如果忽略字段列表中包含此字段名则添加对应的表格列名
if (ignoreUniqueFields.contains(simpleFieldName)) {
String tableColumnName = entry.getKey(); // 表格列名注解中的value值
processedFields.add(tableColumnName);
}
}
return processedFields;
}
/**
* 计算考虑忽略字段的唯一标识
*
* 根据忽略字段列表计算数据的唯一标识
*
* @param data 数据对象
* @param processedIgnoreFields 处理后的忽略字段列表
* @param clazz 用于解析注解的实体类
* @return 唯一标识
*/
private String calculateUniqueIdWithIgnoreFields(T data, List<String> processedIgnoreFields, Class<?> clazz) {
try {
// 获取所有字段值
Map<String, Object> allFieldValues = GenerateUtil.getFieldValue(data, new HashMap<>());
// 如果不需要忽略字段使用原有逻辑
if (processedIgnoreFields.isEmpty()) {
return GenerateUtil.getUniqueId(data);
}
// 移除忽略字段后计算唯一标识
Map<String, Object> filteredValues = new HashMap<>(allFieldValues);
processedIgnoreFields.forEach(filteredValues::remove);
// 将过滤后的值转换为JSON字符串并计算SHA256
String jsonStr = StringUtil.mapToJson(filteredValues);
return StringUtil.getSHA256(jsonStr);
} catch (Exception e) {
// 如果计算失败回退到原有逻辑
return GenerateUtil.getUniqueId(data);
}
}
}

@ -1,6 +1,7 @@
package cn.isliu.core.converters; package cn.isliu.core.converters;
public interface OptionsValueProcess<T> { public interface OptionsValueProcess<T, R> {
T process(R r);
T process();
} }

@ -148,6 +148,9 @@ public class FileUtil {
} }
public static String getFileName(String path) { public static String getFileName(String path) {
if (path.startsWith("http")) {
return path.substring(path.lastIndexOf("/") + 1);
}
return Paths.get(path).getFileName().toString(); return Paths.get(path).getFileName().toString();
} }
} }

@ -38,7 +38,7 @@ public class FsApiUtil {
/** /**
* 获取工作表数据 * 获取工作表数据
* * <p>
* 从指定的飞书表格中读取指定范围的数据 * 从指定的飞书表格中读取指定范围的数据
* *
* @param sheetId 工作表ID * @param sheetId 工作表ID
@ -78,7 +78,7 @@ public class FsApiUtil {
/** /**
* 获取工作表元数据 * 获取工作表元数据
* * <p>
* 获取指定工作表的元数据信息包括行列数工作表名称等 * 获取指定工作表的元数据信息包括行列数工作表名称等
* *
* @param sheetId 工作表ID * @param sheetId 工作表ID
@ -157,7 +157,7 @@ public class FsApiUtil {
/** /**
* 获取根目录Token * 获取根目录Token
* * <p>
* 调用飞书开放平台API获取当前租户的根目录token用于后续的文件夹和文件操作 * 调用飞书开放平台API获取当前租户的根目录token用于后续的文件夹和文件操作
* API接口: GET https://open.feishu.cn/open-apis/drive/v1/files/root_folder/meta * API接口: GET https://open.feishu.cn/open-apis/drive/v1/files/root_folder/meta
* *

@ -38,6 +38,19 @@ public class FsTableUtil {
* @return 飞书表格数据列表 * @return 飞书表格数据列表
*/ */
public static List<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf) { public static List<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf) {
return getFsTableData(sheet, spreadsheetToken, tableConf, new ArrayList<>());
}
/**
* 获取飞书表格数据支持忽略唯一字段
*
* @param sheet 工作表对象
* @param spreadsheetToken 电子表格Token
* @param tableConf 表格配置
* @param ignoreUniqueFields 计算唯一标识时忽略的字段列表
* @return 飞书表格数据列表
*/
public static List<FsTableData> getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List<String> ignoreUniqueFields) {
// 计算数据范围 // 计算数据范围
GridProperties gridProperties = sheet.getGridProperties(); GridProperties gridProperties = sheet.getGridProperties();
@ -70,7 +83,7 @@ public class FsTableUtil {
// 获取飞书表格数据 // 获取飞书表格数据
TableData tableData = processSheetData(sheet, values); TableData tableData = processSheetData(sheet, values);
List<FsTableData> dataList = getFsTableData(tableData); List<FsTableData> dataList = getFsTableData(tableData, ignoreUniqueFields);
Map<String, String> titleMap = new HashMap<>(); Map<String, String> titleMap = new HashMap<>();
dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst() dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst()
@ -293,7 +306,9 @@ public class FsTableUtil {
return resultMap; return resultMap;
} }
public static void setTableOptions(String spreadsheetToken, List<String> headers, Map<String, FieldProperty> fieldsMap, String sheetId, boolean enableDesc) { public static void setTableOptions(String spreadsheetToken, List<String> headers, Map<String, FieldProperty> fieldsMap,
String sheetId, boolean enableDesc, Map<String, Object> customProperties) {
List<Object> list = Arrays.asList(headers.toArray()); List<Object> list = Arrays.asList(headers.toArray());
int line = getMaxLevel(fieldsMap) + (enableDesc ? 2 : 1); int line = getMaxLevel(fieldsMap) + (enableDesc ? 2 : 1);
fieldsMap.forEach((field, fieldProperty) -> { fieldsMap.forEach((field, fieldProperty) -> {
@ -316,21 +331,43 @@ public class FsTableUtil {
List<String> result; List<String> result;
Class<? extends OptionsValueProcess> optionsClass = tableProperty.optionsClass(); Class<? extends OptionsValueProcess> optionsClass = tableProperty.optionsClass();
try { try {
Map<String, Object> properties = new HashMap<>();
if (customProperties == null) {
properties.put("_field", fieldProperty);
} else {
customProperties.put("_field", fieldProperty);
}
OptionsValueProcess optionsValueProcess = optionsClass.getDeclaredConstructor().newInstance(); OptionsValueProcess optionsValueProcess = optionsClass.getDeclaredConstructor().newInstance();
result = (List<String>) optionsValueProcess.process(); result = (List<String>) optionsValueProcess.process(customProperties == null ? properties : customProperties);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (result != null && !result.isEmpty()) {
FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200, FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200,
result); result);
} }
} }
}
}); });
} }
public static void setTableOptions(String spreadsheetToken, List<String> headers, Map<String, FieldProperty> fieldsMap, String sheetId, boolean enableDesc) {
setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, enableDesc, null);
}
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers, public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, TableConf tableConf) { Map<String, FieldProperty> fieldsMap, TableConf tableConf) {
return getHeadTemplateBuilder(sheetId, headers, fieldsMap, null, tableConf);
}
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, List<String> includeFields, TableConf tableConf) {
return getHeadTemplateBuilder(sheetId, headers, fieldsMap, includeFields, tableConf, null);
}
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, List<String> includeFields, TableConf tableConf, Map<String, String> fieldDescriptions) {
String position = FsTableUtil.getColumnNameByNuNumber(headers.size()); String position = FsTableUtil.getColumnNameByNuNumber(headers.size());
@ -342,13 +379,18 @@ public class FsTableUtil {
if (maxLevel == 1) { if (maxLevel == 1) {
// 单层级表头按order排序的headers // 单层级表头按order排序的headers
List<String> sortedHeaders = getSortedHeaders(fieldsMap); List<String> sortedHeaders;
if (includeFields != null && !includeFields.isEmpty()) {
sortedHeaders = includeFields.stream().sorted(Comparator.comparingInt(headers::indexOf)).collect(Collectors.toList());
} else {
sortedHeaders = getSortedHeaders(fieldsMap);
}
int titleRow = tableConf.titleRow(); int titleRow = tableConf.titleRow();
if (tableConf.enableDesc()) { if (tableConf.enableDesc()) {
int descRow = titleRow + 1; int descRow = titleRow + 1;
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow); batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow);
batchPutValuesBuilder.addRow(sortedHeaders.toArray()); batchPutValuesBuilder.addRow(sortedHeaders.toArray());
batchPutValuesBuilder.addRow(getDescArray(sortedHeaders, fieldsMap)); batchPutValuesBuilder.addRow(getDescArray(sortedHeaders, fieldsMap, fieldDescriptions));
} else { } else {
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow); batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow);
batchPutValuesBuilder.addRow(sortedHeaders.toArray()); batchPutValuesBuilder.addRow(sortedHeaders.toArray());
@ -379,10 +421,15 @@ public class FsTableUtil {
// 如果启用了描述在最后一行添加描述 // 如果启用了描述在最后一行添加描述
if (tableConf.enableDesc()) { if (tableConf.enableDesc()) {
List<String> finalHeaders = getSortedHeaders(fieldsMap); List<String> finalHeaders;
if (includeFields != null && !includeFields.isEmpty()) {
finalHeaders = includeFields.stream().sorted(Comparator.comparingInt(headers::indexOf)).collect(Collectors.toList());
} else {
finalHeaders = getSortedHeaders(fieldsMap);
}
int descRow = maxLevel + 1; int descRow = maxLevel + 1;
batchPutValuesBuilder.addRange(sheetId + "!A" + descRow + ":" + position + descRow); batchPutValuesBuilder.addRange(sheetId + "!A" + descRow + ":" + position + descRow);
batchPutValuesBuilder.addRow(getDescArray(finalHeaders, fieldsMap)); batchPutValuesBuilder.addRow(getDescArray(finalHeaders, fieldsMap, fieldDescriptions));
} }
} }
@ -416,12 +463,30 @@ public class FsTableUtil {
} }
private static Object[] getDescArray(List<String> headers, Map<String, FieldProperty> fieldsMap) { private static Object[] getDescArray(List<String> headers, Map<String, FieldProperty> fieldsMap) {
return getDescArray(headers, fieldsMap, null);
}
private static Object[] getDescArray(List<String> headers, Map<String, FieldProperty> fieldsMap, Map<String, String> fieldDescriptions) {
Object[] descArray = new String[headers.size()]; Object[] descArray = new String[headers.size()];
for (int i = 0; i < headers.size(); i++) { for (int i = 0; i < headers.size(); i++) {
String header = headers.get(i); String header = headers.get(i);
FieldProperty fieldProperty = fieldsMap.get(header); FieldProperty fieldProperty = fieldsMap.get(header);
if (fieldProperty != null && fieldProperty.getTableProperty() != null) { if (fieldProperty != null && fieldProperty.getTableProperty() != null) {
String desc = fieldProperty.getTableProperty().desc(); String desc = null;
// 优先从字段描述映射中获取描述
if (fieldDescriptions != null && !fieldDescriptions.isEmpty()) {
// 从字段路径中提取字段名最后一个.后面的部分
String fieldPath = fieldProperty.getField();
String fieldName = fieldPath.substring(fieldPath.lastIndexOf(".") + 1);
desc = fieldDescriptions.get(fieldName);
}
// 如果映射中没有找到则从注解中获取
if (desc == null || desc.isEmpty()) {
desc = fieldProperty.getTableProperty().desc();
}
if (desc != null && !desc.isEmpty()) { if (desc != null && !desc.isEmpty()) {
try { try {
JsonElement element = JsonParser.parseString(desc); JsonElement element = JsonParser.parseString(desc);
@ -681,6 +746,10 @@ public class FsTableUtil {
return mergeRequests; return mergeRequests;
} }
public static TableConf getTableConf(Class<?> zClass) {
return zClass.getAnnotation(TableConf.class);
}
/** /**
* 分组信息类用于辅助排序 * 分组信息类用于辅助排序
*/ */

@ -24,7 +24,6 @@ import java.util.stream.Collectors;
public class GenerateUtil { public class GenerateUtil {
// 使用统一的FsLogger替代java.util.logging.Logger // 使用统一的FsLogger替代java.util.logging.Logger
/** /**
* 根据配置和数据生成DTO对象通用版本 * 根据配置和数据生成DTO对象通用版本
* *
@ -49,7 +48,7 @@ public class GenerateUtil {
try { try {
setNestedField(t, fieldPath, value); setNestedField(t, fieldPath, value);
} catch (Exception e) { } catch (Exception e) {
FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "巨量广告助手】 获取字段值异常!参数:" + fieldPath + ",异常:" + e.getMessage(), "generateList", e); FsLogger.error(ErrorCode.DATA_CONVERSION_ERROR, "飞书助手】 获取字段值异常!参数:" + fieldPath + ",异常:" + e.getMessage(), "generateList", e);
} }
} }
}); });
@ -179,11 +178,15 @@ public class GenerateUtil {
Class<?> fieldType = field.getType(); Class<?> fieldType = field.getType();
// 简单类型转换 // 简单类型转换
if (value != null) { if (value != null && value != "") {
if (fieldType == String.class) { if (fieldType == String.class) {
field.set(target, convertStrValue(value)); field.set(target, convertStrValue(value));
} else if (fieldType == Integer.class || fieldType == int.class) { } else if (fieldType == Integer.class || fieldType == int.class) {
field.set(target, Integer.parseInt(convertValue(value))); String val = convertValue(value);
BigDecimal bd = new BigDecimal(val);
bd = bd.setScale(0, BigDecimal.ROUND_DOWN);
String result = bd.toPlainString();
field.set(target, Integer.parseInt(result));
} else if (fieldType == Double.class || fieldType == double.class) { } else if (fieldType == Double.class || fieldType == double.class) {
field.set(target, Double.parseDouble(convertValue(value))); field.set(target, Double.parseDouble(convertValue(value)));
} else if (fieldType == Boolean.class || fieldType == boolean.class) { } else if (fieldType == Boolean.class || fieldType == boolean.class) {
@ -456,7 +459,7 @@ public class GenerateUtil {
return fieldValue; return fieldValue;
} }
public static <T> @Nullable String getUniqueId(T data) { public static <T> String getUniqueId(T data) {
String uniqueId = null; String uniqueId = null;
try { try {
Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId"); Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId");

@ -56,6 +56,28 @@ public class StringUtil {
return uniqueId; return uniqueId;
} }
/**
* 驼峰转下划线命名
* @param camelCaseName 驼峰命名的字符串
* @return 下划线命名的字符串
*/
public static String toUnderscoreCase(String camelCaseName) {
if (camelCaseName == null || camelCaseName.isEmpty()) {
return camelCaseName;
}
StringBuilder result = new StringBuilder();
for (char c : camelCaseName.toCharArray()) {
if (Character.isUpperCase(c)) {
result.append('_').append(Character.toLowerCase(c));
} else {
result.append(c);
}
}
return result.toString();
}
/** /**
* 下划线转驼峰命名 * 下划线转驼峰命名
* @param underscoreName 下划线命名的字符串 * @param underscoreName 下划线命名的字符串