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.Sheet;
import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.builder.ReadBuilder;
import cn.isliu.core.builder.SheetBuilder;
import cn.isliu.core.builder.WriteBuilder;
import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient;
import cn.isliu.core.enums.ErrorCode;
@ -20,6 +22,8 @@ import com.google.gson.JsonObject;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
@ -32,10 +36,10 @@ public class FsHelper {
/**
* 创建飞书表格
*
*
* 根据传入的实体类结构在指定的电子表格中创建一个新的工作表
* 并设置表头样式单元格格式和下拉选项等
*
*
* @param sheetName 工作表名称
* @param spreadsheetToken 电子表格Token
* @param clazz 实体类Class对象用于解析表头和字段属性
@ -70,8 +74,12 @@ public class FsHelper {
FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken);
}
// 5 设置表格下拉
FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc());
try {
// 5 设置表格下拉
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;
}
@ -94,9 +102,9 @@ public class FsHelper {
/**
* 从飞书表格中读取数据
*
*
* 根据指定的工作表ID和电子表格Token读取表格数据并映射到实体类对象列表中
*
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param clazz 实体类Class对象用于数据映射
@ -133,11 +141,27 @@ public class FsHelper {
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);
}
/**
* 将数据写入飞书表格
*
*
* 将实体类对象列表写入到指定的飞书表格中支持新增和更新操作
*
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param dataList 实体类对象列表
@ -244,4 +268,20 @@ public class FsHelper {
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.FsTableUtil;
import cn.isliu.core.utils.PropertyUtil;
import cn.isliu.core.utils.StringUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
@ -25,6 +29,8 @@ public class SheetBuilder<T> {
private final String spreadsheetToken;
private final Class<T> clazz;
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;
}
/**
* 设置自定义属性
*
* 添加一个自定义属性可以在构建表格时使用
*
* @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
*
@ -79,7 +170,7 @@ public class SheetBuilder<T> {
String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken);
// 2添加表头数据
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf), client);
FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, includeFields, tableConf, fieldDescriptions), client);
// 3设置表格样式
FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken);
@ -97,8 +188,12 @@ public class SheetBuilder<T> {
}
// 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;
}
@ -116,7 +211,14 @@ public class SheetBuilder<T> {
// 根据字段名过滤保留指定的字段
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));
}
}
}

@ -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;
public interface OptionsValueProcess<T> {
public interface OptionsValueProcess<T, R> {
T process(R r);
T process();
}

@ -29,9 +29,9 @@ public class ConvertFieldUtil {
/**
* 将位置键转换为字段名
*
*
* 根据字段属性映射关系将表格中的位置键"A1"转换为实体类字段名
*
*
* @param jsonObject 包含位置键值对的JSON对象
* @param fieldsMap 字段属性映射关系Map
* @return 转换后的字段名值映射Map
@ -62,9 +62,9 @@ public class ConvertFieldUtil {
/**
* 根据字段规则获取值
*
*
* 根据字段类型和配置规则处理字段值
*
*
* @param tableProperty 表格属性注解
* @param value 原始值
* @return 处理后的值
@ -116,9 +116,9 @@ public class ConvertFieldUtil {
/**
* 获取文本链接
*
*
* 从JSON元素中提取文本链接信息
*
*
* @param value JSON元素
* @return 文本链接列表
*/

@ -110,13 +110,13 @@ public class FileUtil {
if (!tempDir.endsWith(File.separator)) {
tempDir += File.separator;
}
// 确保目录存在
File dir = new File(tempDir);
if (!dir.exists()) {
dir.mkdirs();
}
return tempDir;
}
@ -133,21 +133,24 @@ public class FileUtil {
// 常见的图片文件扩展名
String[] imageExtensions = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".ico"};
// 转换为小写进行比较
String path = filePath.toLowerCase();
// 检查URL或本地路径是否以图片扩展名结尾
for (String extension : imageExtensions) {
if (path.endsWith(extension)) {
return true;
}
}
return false;
}
public static String getFileName(String path) {
if (path.startsWith("http")) {
return path.substring(path.lastIndexOf("/") + 1);
}
return Paths.get(path).getFileName().toString();
}
}

@ -38,14 +38,14 @@ public class FsApiUtil {
/**
* 获取工作表数据
*
* <p>
* 从指定的飞书表格中读取指定范围的数据
*
* @param sheetId 工作表ID
*
* @param sheetId 工作表ID
* @param spreadsheetToken 电子表格Token
* @param startPosition 起始位置"A1"
* @param endPosition 结束位置"Z100"
* @param client 飞书客户端
* @param startPosition 起始位置"A1"
* @param endPosition 结束位置"Z100"
* @param client 飞书客户端
* @return 表格数据对象
*/
public static ValuesBatch getSheetData(String sheetId, String spreadsheetToken, String startPosition, String endPosition, FeishuClient client) {
@ -78,11 +78,11 @@ public class FsApiUtil {
/**
* 获取工作表元数据
*
* <p>
* 获取指定工作表的元数据信息包括行列数工作表名称等
*
* @param sheetId 工作表ID
* @param client 飞书客户端
*
* @param sheetId 工作表ID
* @param client 飞书客户端
* @param spreadsheetToken 电子表格Token
* @return 工作表对象
*/
@ -157,7 +157,7 @@ public class FsApiUtil {
/**
* 获取根目录Token
*
* <p>
* 调用飞书开放平台API获取当前租户的根目录token用于后续的文件夹和文件操作
* API接口: GET https://open.feishu.cn/open-apis/drive/v1/files/root_folder/meta
*
@ -266,7 +266,7 @@ public class FsApiUtil {
String message = e.getMessage();
FsLogger.warn("【飞书表格】 创建 sheet 异常!错误信息:{}", message);
throw new FsHelperException(message != null && message.contains("403")? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!");
throw new FsHelperException(message != null && message.contains("403") ? "请按照上方操作,当前智投无法操作对应文档哦" : "【飞书表格】 创建 sheet 异常!");
}
return sheetId;
}
@ -400,7 +400,7 @@ public class FsApiUtil {
}
}
public static String downloadTmpMaterialUrl(String fileToken, FeishuClient client) {
public static String downloadTmpMaterialUrl(String fileToken, FeishuClient client) {
String tmpUrl = "";
try {
BatchGetTmpDownloadUrlMediaReq req = BatchGetTmpDownloadUrlMediaReq.newBuilder()
@ -450,8 +450,8 @@ public class FsApiUtil {
try {
CustomValueService.ValueBatchUpdateRequest batchPutDataRequest =
CustomValueService.ValueBatchUpdateRequest.newBuilder()
.addRequest(batchPutRequest)
.build();
.addRequest(batchPutRequest)
.build();
ApiResponse batchPutResp = client.customValues().valueBatchUpdate(spreadsheetToken, batchPutDataRequest);
if (batchPutResp.success()) {
@ -466,7 +466,7 @@ public class FsApiUtil {
}
}
public static Object addRowColumns(String sheetId, String spreadsheetToken, String type, int length,FeishuClient client) {
public static Object addRowColumns(String sheetId, String spreadsheetToken, String type, int length, FeishuClient client) {
CustomDimensionService.DimensionBatchUpdateRequest batchRequest = CustomDimensionService.DimensionBatchUpdateRequest.newBuilder()
.addRequest(CustomDimensionService.DimensionRequest.addDimension()
@ -512,7 +512,7 @@ public class FsApiUtil {
}
/**
* 字符串类型 formatter: "@"
* 字符串类型 formatter: "@"
*/
public static void setCellType(String sheetId, String formatter, String startPosition, String endPosition, FeishuClient client, String spreadsheetToken) {
try {
@ -533,7 +533,7 @@ public class FsApiUtil {
}
}
public static Object imageUpload(byte[] imageData, String fileName, String position ,String sheetId, String spreadsheetToken, FeishuClient client) {
public static Object imageUpload(byte[] imageData, String fileName, String position, String sheetId, String spreadsheetToken, FeishuClient client) {
try {
CustomValueService.ValueRequest imageRequest = CustomValueService.ValueRequest.imageValues()
@ -553,7 +553,7 @@ public class FsApiUtil {
}
return imageResp.getData();
} catch (Exception e) {
FsLogger.error(ErrorCode.API_SERVER_ERROR,"【飞书表格】 文件上传异常!" + e.getMessage(), fileName, e);
FsLogger.error(ErrorCode.API_SERVER_ERROR, "【飞书表格】 文件上传异常!" + e.getMessage(), fileName, e);
}
return null;

@ -30,14 +30,27 @@ public class FsTableUtil {
/**
* 获取飞书表格数据
*
*
* 从指定的工作表中读取并处理表格数据
*
*
* @param sheet 工作表对象
* @param spreadsheetToken 电子表格Token
* @return 飞书表格数据列表
*/
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();
@ -70,7 +83,7 @@ public class FsTableUtil {
// 获取飞书表格数据
TableData tableData = processSheetData(sheet, values);
List<FsTableData> dataList = getFsTableData(tableData);
List<FsTableData> dataList = getFsTableData(tableData, ignoreUniqueFields);
Map<String, String> titleMap = new HashMap<>();
dataList.stream().filter(d -> d.getRow() == (tableConf.titleRow() - 1)).findFirst()
@ -95,7 +108,7 @@ public class FsTableUtil {
/**
* 获取飞书表格数据
*
*
* @param tableData 表格数据对象
* @return 飞书表格数据列表
*/
@ -105,7 +118,7 @@ public class FsTableUtil {
/**
* 获取飞书表格数据
*
*
* @param tableData 表格数据对象
* @param ignoreUniqueFields 忽略的唯一字段列表
* @return 飞书表格数据列表
@ -293,7 +306,9 @@ public class FsTableUtil {
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());
int line = getMaxLevel(fieldsMap) + (enableDesc ? 2 : 1);
fieldsMap.forEach((field, fieldProperty) -> {
@ -316,25 +331,47 @@ public class FsTableUtil {
List<String> result;
Class<? extends OptionsValueProcess> optionsClass = tableProperty.optionsClass();
try {
Map<String, Object> properties = new HashMap<>();
if (customProperties == null) {
properties.put("_field", fieldProperty);
} else {
customProperties.put("_field", fieldProperty);
}
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) {
throw new RuntimeException(e);
}
FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200,
result);
if (result != null && !result.isEmpty()) {
FsApiUtil.setOptions(sheetId, FsClient.getInstance().getClient(), spreadsheetToken, tableProperty.type() == TypeEnum.MULTI_SELECT, position + line, position + 200,
result);
}
}
}
});
}
public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List<String> headers,
Map<String, FieldProperty> fieldsMap, TableConf tableConf) {
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,
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());
CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder
CustomValueService.ValueRequest.BatchPutValuesBuilder batchPutValuesBuilder
= CustomValueService.ValueRequest.batchPutValues();
// 获取父级表头
@ -342,13 +379,18 @@ public class FsTableUtil {
if (maxLevel == 1) {
// 单层级表头按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();
if (tableConf.enableDesc()) {
int descRow = titleRow + 1;
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow);
batchPutValuesBuilder.addRow(sortedHeaders.toArray());
batchPutValuesBuilder.addRow(getDescArray(sortedHeaders, fieldsMap));
batchPutValuesBuilder.addRow(getDescArray(sortedHeaders, fieldsMap, fieldDescriptions));
} else {
batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow);
batchPutValuesBuilder.addRow(sortedHeaders.toArray());
@ -357,12 +399,12 @@ public class FsTableUtil {
// 多层级表头构建层级结构并处理合并单元格
List<List<HeaderCell>> hierarchicalHeaders = buildHierarchicalHeaders(fieldsMap);
// 处理每一行表头
for (int rowIndex = 0; rowIndex < hierarchicalHeaders.size(); rowIndex++) {
List<HeaderCell> headerRow = hierarchicalHeaders.get(rowIndex);
List<Object> rowValues = new ArrayList<>();
// 将HeaderCell转换为字符串值并处理合并单元格
for (HeaderCell cell : headerRow) {
rowValues.add(cell.getValue());
@ -371,18 +413,23 @@ public class FsTableUtil {
rowValues.add(""); // 合并单元格的占位符
}
}
int actualRow = rowIndex + 1; // 从第1行开始
batchPutValuesBuilder.addRange(sheetId + "!A" + actualRow + ":" + position + actualRow);
batchPutValuesBuilder.addRow(rowValues.toArray());
}
// 如果启用了描述在最后一行添加描述
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;
batchPutValuesBuilder.addRange(sheetId + "!A" + descRow + ":" + position + descRow);
batchPutValuesBuilder.addRow(getDescArray(finalHeaders, fieldsMap));
batchPutValuesBuilder.addRow(getDescArray(finalHeaders, fieldsMap, fieldDescriptions));
}
}
@ -391,16 +438,16 @@ public class FsTableUtil {
/**
* 获取按order排序的表头列表
*
*
* @param fieldsMap 字段属性映射
* @return 按order排序的表头列表
*/
private static List<String> getSortedHeaders(Map<String, FieldProperty> fieldsMap) {
return fieldsMap.entrySet().stream()
.filter(entry -> entry.getValue() != null && entry.getValue().getTableProperty() != null)
.sorted(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
.filter(entry -> entry.getValue() != null && entry.getValue().getTableProperty() != null)
.sorted(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
private static int getMaxLevel(Map<String, FieldProperty> fieldsMap) {
@ -416,12 +463,30 @@ public class FsTableUtil {
}
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()];
for (int i = 0; i < headers.size(); i++) {
String header = headers.get(i);
FieldProperty fieldProperty = fieldsMap.get(header);
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()) {
try {
JsonElement element = JsonParser.parseString(desc);
@ -468,19 +533,19 @@ public class FsTableUtil {
/**
* 根据层级分组字段属性并按order排序
*
*
* @param fieldsMap 字段属性映射
* @return 按层级分组的映射key为层级value为该层级的字段名数组已按order排序
*/
public static Map<Integer, List<String>> groupFieldsByLevel(Map<String, FieldProperty> fieldsMap) {
Map<Integer, List<String>> levelMap = new HashMap<>();
// 按order排序的字段条目
List<Map.Entry<String, FieldProperty>> sortedEntries = fieldsMap.entrySet().stream()
.filter(entry -> entry.getValue() != null && entry.getValue().getTableProperty() != null)
.sorted(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order()))
.collect(Collectors.toList());
.filter(entry -> entry.getValue() != null && entry.getValue().getTableProperty() != null)
.sorted(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order()))
.collect(Collectors.toList());
for (Map.Entry<String, FieldProperty> entry : sortedEntries) {
FieldProperty fieldProperty = entry.getValue();
String[] values = fieldProperty.getTableProperty().value();
@ -488,7 +553,7 @@ public class FsTableUtil {
levelMap.computeIfAbsent(i, k -> new ArrayList<>()).add(values[i]);
}
}
return levelMap;
}
@ -497,40 +562,40 @@ public class FsTableUtil {
* 根据需求实现层级分组和order排序
* 1. 按全局order排序但确保同一分组的字段相邻
* 2. 同一分组内的字段能够正确合并
*
*
* @param fieldsMap 字段属性映射
* @return 多层级表头结构外层为行内层为列
*/
public static List<List<HeaderCell>> buildHierarchicalHeaders(Map<String, FieldProperty> fieldsMap) {
int maxLevel = getMaxLevel(fieldsMap);
List<List<HeaderCell>> headerRows = new ArrayList<>();
// 初始化每行的表头列表
for (int i = 0; i < maxLevel; i++) {
headerRows.add(new ArrayList<>());
}
// 获取排序后的字段列表按照特殊规则排序
// 1. 相同第一层级的字段必须相邻
// 2. 在满足条件1的情况下尽可能按order排序
List<Map.Entry<String, FieldProperty>> sortedFields = getSortedFieldsWithGrouping(fieldsMap);
// 按排序后的顺序处理每个字段
for (Map.Entry<String, FieldProperty> entry : sortedFields) {
String[] values = entry.getValue().getTableProperty().value();
// 统一处理所有字段都对齐到maxLevel层级
// 核心思路最后一个值总是出现在最后一行前面的值依次向上排列
for (int level = 0; level < maxLevel; level++) {
List<HeaderCell> currentRow = headerRows.get(level);
HeaderCell headerCell = new HeaderCell();
headerCell.setLevel(level);
headerCell.setColSpan(1);
// 计算当前层级应该显示的值
String currentValue = "";
if (values.length == 1) {
// 单层级字段只在最后一行显示
if (level == maxLevel - 1) {
@ -544,15 +609,15 @@ public class FsTableUtil {
currentValue = values[valueIndex];
}
}
headerCell.setValue(currentValue);
currentRow.add(headerCell);
}
}
return headerRows;
}
/**
* 获取排序后的字段列表基于最子级字段排序的新规则
* 核心规则
@ -560,44 +625,44 @@ public class FsTableUtil {
* 2. 相同父级字段形成分组组内按子级order排序
* 3. 分组按组内最小order值参与全局排序
* 4. 三级及以上层级遵循约定大于配置要求order连续
*
*
* @param fieldsMap 字段属性映射
* @return 排序后的字段列表
*/
private static List<Map.Entry<String, FieldProperty>> getSortedFieldsWithGrouping(Map<String, FieldProperty> fieldsMap) {
int maxLevel = getMaxLevel(fieldsMap);
// 统一的分组排序逻辑适用于所有层级
// 按层级路径分组
Map<String, List<Map.Entry<String, FieldProperty>>> groupedFields = groupFieldsByFirstLevel(fieldsMap);
// 创建分组信息列表
List<GroupInfo> allGroups = new ArrayList<>();
for (Map.Entry<String, List<Map.Entry<String, FieldProperty>>> groupEntry : groupedFields.entrySet()) {
List<Map.Entry<String, FieldProperty>> fieldsInGroup = groupEntry.getValue();
// 在组内按order排序基于最子级字段
fieldsInGroup.sort(Comparator.comparingInt(entry -> entry.getValue().getTableProperty().order()));
// 验证组内order连续性仅对需要合并的分组进行检查且仅在三级及以上时检查
if (maxLevel >= 3 && fieldsInGroup.size() > 1 && !"default".equals(groupEntry.getKey())) {
validateOrderContinuity(groupEntry.getKey(), fieldsInGroup);
}
// 计算组的最小order用于组间排序
int minOrder = fieldsInGroup.stream()
.mapToInt(entry -> entry.getValue().getTableProperty().order())
.min()
.orElse(Integer.MAX_VALUE);
.mapToInt(entry -> entry.getValue().getTableProperty().order())
.min()
.orElse(Integer.MAX_VALUE);
allGroups.add(new GroupInfo(groupEntry.getKey(), minOrder, fieldsInGroup));
}
// 新的排序逻辑分组作为整体参与全局order排序
// 创建排序单元列表每个单元可能是单个字段或一个分组
List<SortUnit> sortUnits = new ArrayList<>();
for (GroupInfo group : allGroups) {
if ("default".equals(group.getGroupKey())) {
// 单层级字段每个字段都是独立的排序单元
@ -610,45 +675,45 @@ public class FsTableUtil {
sortUnits.add(new SortUnit(group.getMinOrder(), group.getFields(), true));
}
}
// 按order排序所有排序单元实现真正的全局排序
sortUnits.sort(Comparator.comparingInt(SortUnit::getOrder));
// 展开为字段列表
List<Map.Entry<String, FieldProperty>> result = new ArrayList<>();
for (SortUnit unit : sortUnits) {
result.addAll(unit.getFields());
}
return result;
}
public static List<CustomCellService.CellRequest> getMergeCell(String sheetId, Map<String, FieldProperty> fieldsMap) {
List<CustomCellService.CellRequest> mergeRequests = new ArrayList<>();
// 构建层级表头结构
List<List<HeaderCell>> headerRows = buildHierarchicalHeaders(fieldsMap);
// 遍历每一行查找需要合并的单元格
for (int rowIndex = 0; rowIndex < headerRows.size(); rowIndex++) {
List<HeaderCell> headerRow = headerRows.get(rowIndex);
// 查找连续的相同值区域
int colIndex = 0;
for (int i = 0; i < headerRow.size(); i++) {
HeaderCell currentCell = headerRow.get(i);
String currentValue = currentCell.getValue();
// 跳过空值空值不需要合并
if (currentValue == null || currentValue.trim().isEmpty()) {
colIndex++;
continue;
}
// 查找相同值的连续区域
int startCol = colIndex;
int endCol = startCol;
// 向后查找相同值
for (int j = i + 1; j < headerRow.size(); j++) {
HeaderCell nextCell = headerRow.get(j);
@ -659,28 +724,32 @@ public class FsTableUtil {
break;
}
}
// 如果跨越多列则需要合并
if (endCol > startCol) {
String startPosition = getColumnName(startCol) + (rowIndex + 1);
String endPosition = getColumnName(endCol) + (rowIndex + 1);
CustomCellService.CellRequest mergeRequest = CustomCellService.CellRequest.mergeCells()
.sheetId(sheetId)
.startPosition(startPosition)
.endPosition(endPosition)
.build();
mergeRequests.add(mergeRequest);
}
colIndex = endCol + 1;
}
}
return mergeRequests;
}
public static TableConf getTableConf(Class<?> zClass) {
return zClass.getAnnotation(TableConf.class);
}
/**
* 分组信息类用于辅助排序
*/
@ -689,24 +758,24 @@ public class FsTableUtil {
private final int minOrder;
private final List<Map.Entry<String, FieldProperty>> fields;
private final int groupDepth;
public GroupInfo(String groupKey, int minOrder, List<Map.Entry<String, FieldProperty>> fields) {
this(groupKey, minOrder, fields, 1);
}
public GroupInfo(String groupKey, int minOrder, List<Map.Entry<String, FieldProperty>> fields, int groupDepth) {
this.groupKey = groupKey;
this.minOrder = minOrder;
this.fields = fields;
this.groupDepth = groupDepth;
}
public String getGroupKey() { return groupKey; }
public int getMinOrder() { return minOrder; }
public List<Map.Entry<String, FieldProperty>> getFields() { return fields; }
public int getGroupDepth() { return groupDepth; }
}
/**
* 排序项类用于全局排序
*/
@ -714,18 +783,18 @@ public class FsTableUtil {
private final int order;
private final List<Map.Entry<String, FieldProperty>> fields;
private final boolean isGroup;
public SortItem(int order, List<Map.Entry<String, FieldProperty>> fields, boolean isGroup) {
this.order = order;
this.fields = fields;
this.isGroup = isGroup;
}
public int getOrder() { return order; }
public List<Map.Entry<String, FieldProperty>> getFields() { return fields; }
public boolean isGroup() { return isGroup; }
}
/**
* 排序单元类用于分组整体排序
* 一个排序单元可以是单个字段或一个完整的分组
@ -734,36 +803,36 @@ public class FsTableUtil {
private final int order;
private final List<Map.Entry<String, FieldProperty>> fields;
private final boolean isGroup;
public SortUnit(int order, List<Map.Entry<String, FieldProperty>> fields, boolean isGroup) {
this.order = order;
this.fields = fields;
this.isGroup = isGroup;
}
public int getOrder() { return order; }
public List<Map.Entry<String, FieldProperty>> getFields() { return fields; }
public boolean isGroup() { return isGroup; }
}
/**
* 按层级路径分组字段
* 根据需求
* 1. 单层级字段"部门"放入"default"分组
* 2. 多层级字段按完整的层级路径分组除最后一级
* 例如["ID", "员工信息", "姓名"] 分组key为 "ID|员工信息"
*
*
* @param fieldsMap 字段属性映射
* @return 按层级路径分组的字段映射
*/
private static Map<String, List<Map.Entry<String, FieldProperty>>> groupFieldsByFirstLevel(Map<String, FieldProperty> fieldsMap) {
Map<String, List<Map.Entry<String, FieldProperty>>> groupedFields = new LinkedHashMap<>();
for (Map.Entry<String, FieldProperty> entry : fieldsMap.entrySet()) {
FieldProperty fieldProperty = entry.getValue();
if (fieldProperty != null && fieldProperty.getTableProperty() != null) {
String[] values = fieldProperty.getTableProperty().value();
String groupKey;
if (values.length == 1) {
// 单层级字段放入默认分组
@ -777,18 +846,18 @@ public class FsTableUtil {
}
groupKey = pathBuilder.toString();
}
groupedFields.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(entry);
}
}
return groupedFields;
}
/**
* 验证组内字段order的连续性
* 三级及以上层级要求同一分组内的字段order必须连续
*
*
* @param groupKey 分组key
* @param fieldsInGroup 分组内的字段列表已按order排序
*/
@ -796,24 +865,24 @@ public class FsTableUtil {
if (fieldsInGroup.size() <= 1) {
return; // 单个字段无需验证
}
for (int i = 1; i < fieldsInGroup.size(); i++) {
int prevOrder = fieldsInGroup.get(i - 1).getValue().getTableProperty().order();
int currentOrder = fieldsInGroup.get(i).getValue().getTableProperty().order();
if (currentOrder != prevOrder + 1) {
String prevFieldName = fieldsInGroup.get(i - 1).getKey();
String currentFieldName = fieldsInGroup.get(i).getKey();
throw new IllegalArgumentException(
String.format("分组 '%s' 中的字段order不连续: %s(order=%d) 和 %s(order=%d). " +
"三级及以上层级要求同一分组内的order必须连续。",
String.format("分组 '%s' 中的字段order不连续: %s(order=%d) 和 %s(order=%d). " +
"三级及以上层级要求同一分组内的order必须连续。",
groupKey, prevFieldName, prevOrder, currentFieldName, currentOrder)
);
}
}
}
/**
* 表头单元格类用于支持合并单元格
*/
@ -822,23 +891,23 @@ public class FsTableUtil {
private int level;
private int colSpan = 1;
private int rowSpan = 1;
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
public int getLevel() { return level; }
public void setLevel(int level) { this.level = level; }
public int getColSpan() { return colSpan; }
public void setColSpan(int colSpan) { this.colSpan = colSpan; }
public int getRowSpan() { return rowSpan; }
public void setRowSpan(int rowSpan) { this.rowSpan = rowSpan; }
}
/**
* 按指定字符数给文本添加换行符
*
*
* @param text 需要处理的文本
* @param charsPerLine 每行字符数
* @return 添加换行符后的文本
@ -847,7 +916,7 @@ public class FsTableUtil {
if (text == null || text.isEmpty()) {
return text;
}
StringBuilder result = new StringBuilder();
for (int i = 0; i < text.length(); i += charsPerLine) {
if (i > 0) {
@ -861,7 +930,7 @@ public class FsTableUtil {
/**
* 每8个字符添加一个换行符默认方法
*
*
* @param text 需要处理的文本
* @return 添加换行符后的文本
*/

@ -24,10 +24,9 @@ import java.util.stream.Collectors;
public class GenerateUtil {
// 使用统一的FsLogger替代java.util.logging.Logger
/**
* 根据配置和数据生成DTO对象通用版本
*
*
* @param fieldPathList 字段路径列表
* @param clazz 实体类Class对象
* @param dataMap 数据映射Map
@ -49,7 +48,7 @@ public class GenerateUtil {
try {
setNestedField(t, fieldPath, value);
} 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);
}
}
});
@ -59,7 +58,7 @@ public class GenerateUtil {
/**
* 递归设置嵌套字段值支持List类型处理
*
*
* @param target 目标对象
* @param fieldPath 字段路径
* @param value 字段值
@ -73,7 +72,7 @@ public class GenerateUtil {
/**
* 递归设置嵌套字段值
*
*
* @param target 目标对象
* @param parts 字段路径分段数组
* @param index 当前处理的字段索引
@ -179,11 +178,15 @@ public class GenerateUtil {
Class<?> fieldType = field.getType();
// 简单类型转换
if (value != null) {
if (value != null && value != "") {
if (fieldType == String.class) {
field.set(target, convertStrValue(value));
} 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) {
field.set(target, Double.parseDouble(convertValue(value)));
} else if (fieldType == Boolean.class || fieldType == boolean.class) {
@ -253,7 +256,7 @@ public class GenerateUtil {
// 继续在父类中查找
}
}
// 如果直接查找失败尝试使用下划线转驼峰的方式查找字段
String camelCaseFieldName = StringUtil.toCamelCase(fieldName);
for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
@ -263,7 +266,7 @@ public class GenerateUtil {
// 继续在父类中查找
}
}
return null;
}
@ -347,7 +350,7 @@ public class GenerateUtil {
for (Map.Entry<String, String> entry : fieldMap.entrySet()) {
String fieldName = entry.getKey();
String fieldPath = entry.getValue();
try {
Object value = getNestedFieldValue(target, fieldPath);
if (value != null) {
@ -456,7 +459,7 @@ public class GenerateUtil {
return fieldValue;
}
public static <T> @Nullable String getUniqueId(T data) {
public static <T> String getUniqueId(T data) {
String uniqueId = null;
try {
Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId");

@ -56,6 +56,28 @@ public class StringUtil {
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 下划线命名的字符串
@ -92,7 +114,7 @@ public class StringUtil {
if (collection == null || collection.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
Iterator<?> iterator = collection.iterator();
while (iterator.hasNext()) {
@ -114,7 +136,7 @@ public class StringUtil {
if (array == null || array.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.length; i++) {
sb.append(array[i] != null ? array[i].toString() : "");