From 891b019ae3afc6846077484387dfac948d8c31e8 Mon Sep 17 00:00:00 2001 From: liushuang Date: Fri, 26 Sep 2025 19:15:06 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(core):=20=E5=A2=9E=E5=BC=BA=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6=E8=A1=A8=E6=A0=BC=E5=8A=9F=E8=83=BD=E4=B8=8E=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=B1=BB=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -为 ConvertFieldUtil、FileUtil、FsApiUtil 和 FsTableUtil 类添加更详细的 JavaDoc 注释,提升代码可读性- 在 FileUtil 中优化图片文件判断逻辑并增强 getFileName 方法以支持 URL 场景 - 扩展 FsApiUtil 支持更多飞书 API 参数格式化及异常处理 - 在 FsHelper 中引入 ReadBuilder 和 WriteBuilder 构建器模式,提供链式调用方式操作飞书表格读写 - FsTableUtil 新增对忽略唯一字段的支持,并扩展表头模板构建能力,支持字段描述自定义- GenerateUtil优化数字转换精度控制,并修复部分日志提示文案 - OptionsValueProcess 接口增加泛型参数支持,提高灵活性和类型安全性``` --- src/main/java/cn/isliu/FsHelper.java | 56 +++- .../cn/isliu/core/builder/ReadBuilder.java | 130 ++++++++ .../cn/isliu/core/builder/SheetBuilder.java | 112 ++++++- .../cn/isliu/core/builder/WriteBuilder.java | 293 ++++++++++++++++++ .../core/converters/OptionsValueProcess.java | 5 +- .../cn/isliu/core/utils/ConvertFieldUtil.java | 12 +- .../java/cn/isliu/core/utils/FileUtil.java | 13 +- .../java/cn/isliu/core/utils/FsApiUtil.java | 38 +-- .../java/cn/isliu/core/utils/FsTableUtil.java | 273 ++++++++++------ .../cn/isliu/core/utils/GenerateUtil.java | 25 +- .../java/cn/isliu/core/utils/StringUtil.java | 26 +- 11 files changed, 823 insertions(+), 160 deletions(-) create mode 100644 src/main/java/cn/isliu/core/builder/ReadBuilder.java create mode 100644 src/main/java/cn/isliu/core/builder/WriteBuilder.java diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java index b717bdf..54f44b8 100644 --- a/src/main/java/cn/isliu/FsHelper.java +++ b/src/main/java/cn/isliu/FsHelper.java @@ -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 实体类泛型 + * @return ReadBuilder实例,支持链式调用 + */ + public static ReadBuilder readBuilder(String sheetId, String spreadsheetToken, Class 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 实体类泛型 + * @return WriteBuilder实例,支持链式调用 + */ + public static WriteBuilder writeBuilder(String sheetId, String spreadsheetToken, List dataList) { + return new WriteBuilder<>(sheetId, spreadsheetToken, dataList); + } } \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/builder/ReadBuilder.java b/src/main/java/cn/isliu/core/builder/ReadBuilder.java new file mode 100644 index 0000000..c0d0d52 --- /dev/null +++ b/src/main/java/cn/isliu/core/builder/ReadBuilder.java @@ -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 { + + private final String sheetId; + private final String spreadsheetToken; + private final Class clazz; + private List ignoreUniqueFields; + + /** + * 构造函数 + * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + * @param clazz 实体类Class对象 + */ + public ReadBuilder(String sheetId, String spreadsheetToken, Class clazz) { + this.sheetId = sheetId; + this.spreadsheetToken = spreadsheetToken; + this.clazz = clazz; + } + + /** + * 设置计算唯一标识时忽略的字段列表 + * + * 指定在计算数据行唯一标识时要忽略的字段名称列表。 + * 这些字段的值变化不会影响数据行的唯一性判断。 + * + * @param fields 要忽略的字段名称列表 + * @return ReadBuilder实例,支持链式调用 + */ + public ReadBuilder ignoreUniqueFields(List fields) { + this.ignoreUniqueFields = new ArrayList<>(fields); + return this; + } + + /** + * 执行数据读取并返回实体类对象列表 + * + * 根据配置的参数从飞书表格中读取数据并映射到实体类对象列表中。 + * + * @return 映射后的实体类对象列表 + */ + public List build() { + List results = new ArrayList<>(); + FeishuClient client = FsClient.getInstance().getClient(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); + TableConf tableConf = PropertyUtil.getTableConf(clazz); + + Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); + + // 处理忽略字段名称映射 + List processedIgnoreFields = processIgnoreFields(fieldsMap); + + // 使用支持忽略字段的方法获取表格数据 + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields); + + List fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); + + fsTableDataList.forEach(tableData -> { + Object data = tableData.getData(); + if (data instanceof HashMap) { + Map rowData = (HashMap) data; + JsonObject jsonObject = JSONUtil.convertMapToJsonObject(rowData); + Map 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 processIgnoreFields(Map fieldsMap) { + if (ignoreUniqueFields == null || ignoreUniqueFields.isEmpty()) { + return new ArrayList<>(); + } + + List processedFields = new ArrayList<>(); + + // 遍历字段映射,找到对应的表格列名 + for (Map.Entry 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; + } +} diff --git a/src/main/java/cn/isliu/core/builder/SheetBuilder.java b/src/main/java/cn/isliu/core/builder/SheetBuilder.java index a09a9ed..6d37e44 100644 --- a/src/main/java/cn/isliu/core/builder/SheetBuilder.java +++ b/src/main/java/cn/isliu/core/builder/SheetBuilder.java @@ -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 { private final String spreadsheetToken; private final Class clazz; private List includeFields; + private final Map customProperties = new HashMap<>(); + private final Map fieldDescriptions = new HashMap<>(); /** * 构造函数 @@ -52,6 +58,91 @@ public class SheetBuilder { return this; } + /** + * 设置自定义属性 + * + * 添加一个自定义属性,可以在构建表格时使用 + * + * @param key 属性键 + * @param value 属性值 + * @return SheetBuilder实例,支持链式调用 + */ + public SheetBuilder addCustomProperty(String key, Object value) { + this.customProperties.put(key, value); + return this; + } + + /** + * 批量设置自定义属性 + * + * 批量添加自定义属性,可以在构建表格时使用 + * + * @param properties 自定义属性映射 + * @return SheetBuilder实例,支持链式调用 + */ + public SheetBuilder addCustomProperties(Map properties) { + this.customProperties.putAll(properties); + return this; + } + + /** + * 获取自定义属性 + * + * 根据键获取已设置的自定义属性值 + * + * @param key 属性键 + * @return 属性值,如果不存在则返回null + */ + public Object getCustomProperty(String key) { + return this.customProperties.get(key); + } + + /** + * 获取所有自定义属性 + * + * @return 包含所有自定义属性的映射 + */ + public Map getCustomProperties() { + return new HashMap<>(this.customProperties); + } + + /** + * 设置字段描述映射 + * + * 为实体类字段设置自定义描述信息,用于在表格描述行中显示。 + * 如果字段在映射中存在描述,则使用映射中的描述;否则使用注解中的描述。 + * + * @param fieldDescriptions 字段名到描述的映射,key为字段名,value为描述文本 + * @return SheetBuilder实例,支持链式调用 + */ + public SheetBuilder fieldDescription(Map fieldDescriptions) { + this.fieldDescriptions.putAll(fieldDescriptions); + return this; + } + + /** + * 设置单个字段描述 + * + * 为指定字段设置自定义描述信息。 + * + * @param fieldName 字段名 + * @param description 描述文本 + * @return SheetBuilder实例,支持链式调用 + */ + public SheetBuilder fieldDescription(String fieldName, String description) { + this.fieldDescriptions.put(fieldName, description); + return this; + } + + /** + * 获取字段描述映射 + * + * @return 包含所有字段描述的映射 + */ + public Map getFieldDescriptions() { + return new HashMap<>(this.fieldDescriptions); + } + /** * 构建表格并返回工作表ID * @@ -79,7 +170,7 @@ public class SheetBuilder { 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 { } // 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 { // 根据字段名过滤,保留指定的字段 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)); } -} +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/builder/WriteBuilder.java b/src/main/java/cn/isliu/core/builder/WriteBuilder.java new file mode 100644 index 0000000..b23fef2 --- /dev/null +++ b/src/main/java/cn/isliu/core/builder/WriteBuilder.java @@ -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 { + + private final String sheetId; + private final String spreadsheetToken; + private final List dataList; + private List ignoreUniqueFields; + private Class clazz; + private boolean ignoreNotFound; + + /** + * 构造函数 + * + * @param sheetId 工作表ID + * @param spreadsheetToken 电子表格Token + * @param dataList 要写入的数据列表 + */ + public WriteBuilder(String sheetId, String spreadsheetToken, List dataList) { + this.sheetId = sheetId; + this.spreadsheetToken = spreadsheetToken; + this.dataList = dataList; + this.clazz = null; + this.ignoreNotFound = false; + } + + /** + * 设置计算唯一标识时忽略的字段列表 + * + * 指定在计算数据行唯一标识时要忽略的字段名称列表。 + * 这些字段的值变化不会影响数据行的唯一性判断。 + * + * @param fields 要忽略的字段名称列表 + * @return WriteBuilder实例,支持链式调用 + */ + public WriteBuilder ignoreUniqueFields(List fields) { + this.ignoreUniqueFields = new ArrayList<>(fields); + return this; + } + + /** + * 设置用于解析注解的实体类 + * + * 指定用于解析@TableProperty注解的实体类。这个类可以与数据列表的类型不同, + * 主要用于获取字段映射关系和表格配置信息。 + * + * @param clazz 用于解析注解的实体类 + * @return WriteBuilder实例,支持链式调用 + */ + public WriteBuilder clazz(Class clazz) { + this.clazz = clazz; + return this; + } + + /** + * 设置是否忽略未找到的数据 + * + * 指定在写入数据时是否忽略未找到的字段。如果设置为true, + * 当数据中包含表格中不存在的字段时,写入操作将继续执行, + * 而不会抛出异常。默认值为false。 + * + * @param ignoreNotFound 是否忽略未找到的字段,默认值为false + * @return WriteBuilder实例,支持链式调用 + */ + public WriteBuilder ignoreNotFound(boolean ignoreNotFound) { + this.ignoreNotFound = ignoreNotFound; + return this; + } + + /** + * 执行数据写入并返回操作结果 + * + * 根据配置的参数将数据写入到飞书表格中,支持新增和更新操作。 + * + * @return 写入操作结果 + */ + public Object build() { + if (dataList.isEmpty()) { + return null; + } + + Class aClass = clazz; + Map fieldsMap; + TableConf tableConf = PropertyUtil.getTableConf(aClass); + + Map 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 processedIgnoreFields = processIgnoreFields(fieldsMap); + + FeishuClient client = FsClient.getInstance().getClient(); + Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); + + // 使用支持忽略字段的方法获取表格数据 + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf, processedIgnoreFields); + Map 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 titlePostionMap = FsTableUtil.getTitlePostionMap(sheet, spreadsheetToken, tableConf); + + + + // 初始化批量插入对象 + CustomValueService.ValueRequest.BatchPutValuesBuilder resultValuesBuilder = CustomValueService.ValueRequest.batchPutValues(); + + List fileDataList = new ArrayList<>(); + + AtomicInteger rowCount = new AtomicInteger(row[0] + 1); + + for (T data : dataList) { + Map 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 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 valueRanges = batchPutValues.getValueRanges(); + if (valueRanges != null && !valueRanges.isEmpty()) { + return FsApiUtil.batchPutValues(sheetId, spreadsheetToken, resultValuesBuilder.build(), client); + } + return null; + } + + /** + * 处理忽略字段名称映射 + * + * 将实体字段名称映射为表格列名称 + * + * @param fieldsMap 字段映射 + * @return 处理后的忽略字段列表 + */ + private List processIgnoreFields(Map fieldsMap) { + if (ignoreUniqueFields == null || ignoreUniqueFields.isEmpty()) { + return new ArrayList<>(); + } + + List processedFields = new ArrayList<>(); + + // 遍历字段映射,找到对应的表格列名 + for (Map.Entry 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 processedIgnoreFields, Class clazz) { + try { + // 获取所有字段值 + Map allFieldValues = GenerateUtil.getFieldValue(data, new HashMap<>()); + + // 如果不需要忽略字段,使用原有逻辑 + if (processedIgnoreFields.isEmpty()) { + return GenerateUtil.getUniqueId(data); + } + + // 移除忽略字段后计算唯一标识 + Map 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); + } + } +} diff --git a/src/main/java/cn/isliu/core/converters/OptionsValueProcess.java b/src/main/java/cn/isliu/core/converters/OptionsValueProcess.java index 50e8afa..66353c7 100644 --- a/src/main/java/cn/isliu/core/converters/OptionsValueProcess.java +++ b/src/main/java/cn/isliu/core/converters/OptionsValueProcess.java @@ -1,6 +1,7 @@ package cn.isliu.core.converters; -public interface OptionsValueProcess { +public interface OptionsValueProcess { + + T process(R r); - T process(); } diff --git a/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java b/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java index 9403aed..3372488 100644 --- a/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java +++ b/src/main/java/cn/isliu/core/utils/ConvertFieldUtil.java @@ -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 文本链接列表 */ diff --git a/src/main/java/cn/isliu/core/utils/FileUtil.java b/src/main/java/cn/isliu/core/utils/FileUtil.java index 0ba5611..30a73ce 100644 --- a/src/main/java/cn/isliu/core/utils/FileUtil.java +++ b/src/main/java/cn/isliu/core/utils/FileUtil.java @@ -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(); } } diff --git a/src/main/java/cn/isliu/core/utils/FsApiUtil.java b/src/main/java/cn/isliu/core/utils/FsApiUtil.java index be0a868..fc1c1d6 100644 --- a/src/main/java/cn/isliu/core/utils/FsApiUtil.java +++ b/src/main/java/cn/isliu/core/utils/FsApiUtil.java @@ -38,14 +38,14 @@ public class FsApiUtil { /** * 获取工作表数据 - * + *

* 从指定的飞书表格中读取指定范围的数据 - * - * @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 { /** * 获取工作表元数据 - * + *

* 获取指定工作表的元数据信息,包括行列数、工作表名称等 - * - * @param sheetId 工作表ID - * @param client 飞书客户端 + * + * @param sheetId 工作表ID + * @param client 飞书客户端 * @param spreadsheetToken 电子表格Token * @return 工作表对象 */ @@ -157,7 +157,7 @@ public class FsApiUtil { /** * 获取根目录Token - * + *

* 调用飞书开放平台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; diff --git a/src/main/java/cn/isliu/core/utils/FsTableUtil.java b/src/main/java/cn/isliu/core/utils/FsTableUtil.java index daf4281..cdb05d8 100644 --- a/src/main/java/cn/isliu/core/utils/FsTableUtil.java +++ b/src/main/java/cn/isliu/core/utils/FsTableUtil.java @@ -30,14 +30,27 @@ public class FsTableUtil { /** * 获取飞书表格数据 - * + * * 从指定的工作表中读取并处理表格数据 - * + * * @param sheet 工作表对象 * @param spreadsheetToken 电子表格Token * @return 飞书表格数据列表 */ public static List 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 getFsTableData(Sheet sheet, String spreadsheetToken, TableConf tableConf, List ignoreUniqueFields) { // 计算数据范围 GridProperties gridProperties = sheet.getGridProperties(); @@ -70,7 +83,7 @@ public class FsTableUtil { // 获取飞书表格数据 TableData tableData = processSheetData(sheet, values); - List dataList = getFsTableData(tableData); + List dataList = getFsTableData(tableData, ignoreUniqueFields); Map 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 headers, Map fieldsMap, String sheetId, boolean enableDesc) { + public static void setTableOptions(String spreadsheetToken, List headers, Map fieldsMap, + String sheetId, boolean enableDesc, Map customProperties) { + List list = Arrays.asList(headers.toArray()); int line = getMaxLevel(fieldsMap) + (enableDesc ? 2 : 1); fieldsMap.forEach((field, fieldProperty) -> { @@ -316,25 +331,47 @@ public class FsTableUtil { List result; Class optionsClass = tableProperty.optionsClass(); try { + Map properties = new HashMap<>(); + if (customProperties == null) { + properties.put("_field", fieldProperty); + } else { + customProperties.put("_field", fieldProperty); + } OptionsValueProcess optionsValueProcess = optionsClass.getDeclaredConstructor().newInstance(); - result = (List) optionsValueProcess.process(); + result = (List) 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 headers, - Map fieldsMap, TableConf tableConf) { - + public static void setTableOptions(String spreadsheetToken, List headers, Map fieldsMap, String sheetId, boolean enableDesc) { + setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, enableDesc, null); + } + + public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List headers, + Map fieldsMap, TableConf tableConf) { + return getHeadTemplateBuilder(sheetId, headers, fieldsMap, null, tableConf); + } + + public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List headers, + Map fieldsMap, List includeFields, TableConf tableConf) { + return getHeadTemplateBuilder(sheetId, headers, fieldsMap, includeFields, tableConf, null); + } + + public static CustomValueService.ValueRequest getHeadTemplateBuilder(String sheetId, List headers, + Map fieldsMap, List includeFields, TableConf tableConf, Map 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 sortedHeaders = getSortedHeaders(fieldsMap); + List 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> hierarchicalHeaders = buildHierarchicalHeaders(fieldsMap); - + // 处理每一行表头 for (int rowIndex = 0; rowIndex < hierarchicalHeaders.size(); rowIndex++) { List headerRow = hierarchicalHeaders.get(rowIndex); List 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 finalHeaders = getSortedHeaders(fieldsMap); + List 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 getSortedHeaders(Map 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 fieldsMap) { @@ -416,12 +463,30 @@ public class FsTableUtil { } private static Object[] getDescArray(List headers, Map fieldsMap) { + return getDescArray(headers, fieldsMap, null); + } + + private static Object[] getDescArray(List headers, Map fieldsMap, Map 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> groupFieldsByLevel(Map fieldsMap) { Map> levelMap = new HashMap<>(); - + // 按order排序的字段条目 List> 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 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> buildHierarchicalHeaders(Map fieldsMap) { int maxLevel = getMaxLevel(fieldsMap); List> headerRows = new ArrayList<>(); - + // 初始化每行的表头列表 for (int i = 0; i < maxLevel; i++) { headerRows.add(new ArrayList<>()); } - + // 获取排序后的字段列表,按照特殊规则排序: // 1. 相同第一层级的字段必须相邻 // 2. 在满足条件1的情况下,尽可能按order排序 List> sortedFields = getSortedFieldsWithGrouping(fieldsMap); - + // 按排序后的顺序处理每个字段 for (Map.Entry entry : sortedFields) { String[] values = entry.getValue().getTableProperty().value(); - + // 统一处理:所有字段都对齐到maxLevel层级 // 核心思路:最后一个值总是出现在最后一行,前面的值依次向上排列 - + for (int level = 0; level < maxLevel; level++) { List 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> getSortedFieldsWithGrouping(Map fieldsMap) { int maxLevel = getMaxLevel(fieldsMap); - + // 统一的分组排序逻辑,适用于所有层级 // 按层级路径分组 Map>> groupedFields = groupFieldsByFirstLevel(fieldsMap); - + // 创建分组信息列表 List allGroups = new ArrayList<>(); - + for (Map.Entry>> groupEntry : groupedFields.entrySet()) { List> 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 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> result = new ArrayList<>(); for (SortUnit unit : sortUnits) { result.addAll(unit.getFields()); } - + return result; } public static List getMergeCell(String sheetId, Map fieldsMap) { List mergeRequests = new ArrayList<>(); - + // 构建层级表头结构 List> headerRows = buildHierarchicalHeaders(fieldsMap); - + // 遍历每一行,查找需要合并的单元格 for (int rowIndex = 0; rowIndex < headerRows.size(); rowIndex++) { List 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> fields; private final int groupDepth; - + public GroupInfo(String groupKey, int minOrder, List> fields) { this(groupKey, minOrder, fields, 1); } - + public GroupInfo(String groupKey, int minOrder, List> 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> getFields() { return fields; } public int getGroupDepth() { return groupDepth; } } - + /** * 排序项类,用于全局排序 */ @@ -714,18 +783,18 @@ public class FsTableUtil { private final int order; private final List> fields; private final boolean isGroup; - + public SortItem(int order, List> fields, boolean isGroup) { this.order = order; this.fields = fields; this.isGroup = isGroup; } - + public int getOrder() { return order; } public List> getFields() { return fields; } public boolean isGroup() { return isGroup; } } - + /** * 排序单元类,用于分组整体排序 * 一个排序单元可以是单个字段或一个完整的分组 @@ -734,36 +803,36 @@ public class FsTableUtil { private final int order; private final List> fields; private final boolean isGroup; - + public SortUnit(int order, List> fields, boolean isGroup) { this.order = order; this.fields = fields; this.isGroup = isGroup; } - + public int getOrder() { return order; } public List> getFields() { return fields; } public boolean isGroup() { return isGroup; } } - + /** * 按层级路径分组字段 * 根据需求: * 1. 单层级字段(如"部门")放入"default"分组 * 2. 多层级字段按完整的层级路径分组(除最后一级) * 例如:["ID", "员工信息", "姓名"] → 分组key为 "ID|员工信息" - * + * * @param fieldsMap 字段属性映射 * @return 按层级路径分组的字段映射 */ private static Map>> groupFieldsByFirstLevel(Map fieldsMap) { Map>> groupedFields = new LinkedHashMap<>(); - + for (Map.Entry 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 添加换行符后的文本 */ diff --git a/src/main/java/cn/isliu/core/utils/GenerateUtil.java b/src/main/java/cn/isliu/core/utils/GenerateUtil.java index 86e0c8d..ebc0cb2 100644 --- a/src/main/java/cn/isliu/core/utils/GenerateUtil.java +++ b/src/main/java/cn/isliu/core/utils/GenerateUtil.java @@ -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 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 @Nullable String getUniqueId(T data) { + public static String getUniqueId(T data) { String uniqueId = null; try { Object uniqueIdObj = GenerateUtil.getNestedFieldValue(data, "uniqueId"); diff --git a/src/main/java/cn/isliu/core/utils/StringUtil.java b/src/main/java/cn/isliu/core/utils/StringUtil.java index 4669aa6..a84dc1d 100644 --- a/src/main/java/cn/isliu/core/utils/StringUtil.java +++ b/src/main/java/cn/isliu/core/utils/StringUtil.java @@ -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() : "");