```
feat(core): 增强飞书表格功能与工具类支持 -为 ConvertFieldUtil、FileUtil、FsApiUtil 和 FsTableUtil 类添加更详细的 JavaDoc 注释,提升代码可读性- 在 FileUtil 中优化图片文件判断逻辑并增强 getFileName 方法以支持 URL 场景 - 扩展 FsApiUtil 支持更多飞书 API 参数格式化及异常处理 - 在 FsHelper 中引入 ReadBuilder 和 WriteBuilder 构建器模式,提供链式调用方式操作飞书表格读写 - FsTableUtil 新增对忽略唯一字段的支持,并扩展表头模板构建能力,支持字段描述自定义- GenerateUtil优化数字转换精度控制,并修复部分日志提示文案 - OptionsValueProcess 接口增加泛型参数支持,提高灵活性和类型安全性```
This commit is contained in:
		
							parent
							
								
									3e25a91aed
								
							
						
					
					
						commit
						891b019ae3
					
				@ -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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -133,6 +141,22 @@ 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 将数据写入飞书表格
 | 
			
		||||
     *
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								src/main/java/cn/isliu/core/builder/ReadBuilder.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										130
									
								
								src/main/java/cn/isliu/core/builder/ReadBuilder.java
									
									
									
									
									
										Normal file
									
								
							@ -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,7 +188,11 @@ 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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										293
									
								
								src/main/java/cn/isliu/core/builder/WriteBuilder.java
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										293
									
								
								src/main/java/cn/isliu/core/builder/WriteBuilder.java
									
									
									
									
									
										Normal file
									
								
							@ -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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -148,6 +148,9 @@ public class FileUtil {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,19 @@ public class FsTableUtil {
 | 
			
		||||
     * @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()
 | 
			
		||||
@ -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,21 +331,43 @@ 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 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) {
 | 
			
		||||
                                                                         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());
 | 
			
		||||
 | 
			
		||||
@ -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());
 | 
			
		||||
@ -379,10 +421,15 @@ public class FsTableUtil {
 | 
			
		||||
 | 
			
		||||
            // 如果启用了描述,在最后一行添加描述
 | 
			
		||||
            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));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -397,10 +444,10 @@ public class FsTableUtil {
 | 
			
		||||
     */
 | 
			
		||||
    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);
 | 
			
		||||
@ -477,9 +542,9 @@ public class FsTableUtil {
 | 
			
		||||
 | 
			
		||||
        // 按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();
 | 
			
		||||
@ -587,9 +652,9 @@ public class FsTableUtil {
 | 
			
		||||
 | 
			
		||||
            // 计算组的最小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));
 | 
			
		||||
        }
 | 
			
		||||
@ -681,6 +746,10 @@ public class FsTableUtil {
 | 
			
		||||
        return mergeRequests;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TableConf getTableConf(Class<?> zClass) {
 | 
			
		||||
        return zClass.getAnnotation(TableConf.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 分组信息类,用于辅助排序
 | 
			
		||||
     */
 | 
			
		||||
@ -806,8 +875,8 @@ public class FsTableUtil {
 | 
			
		||||
                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)
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,6 @@ import java.util.stream.Collectors;
 | 
			
		||||
public class GenerateUtil {
 | 
			
		||||
 | 
			
		||||
    // 使用统一的FsLogger替代java.util.logging.Logger
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据配置和数据生成DTO对象(通用版本)
 | 
			
		||||
     *
 | 
			
		||||
@ -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);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
@ -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) {
 | 
			
		||||
@ -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 下划线命名的字符串
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user