From c8e2e2dc5ecb383bbd31f0444deb57aecb9f67a1 Mon Sep 17 00:00:00 2001 From: liushuang Date: Sun, 28 Sep 2025 16:03:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=E4=BC=98=E5=8C=96=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E9=80=BB=E8=BE=91=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E8=BF=87=E6=BB=A4=E4=B8=8E=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `FsTableUtil` 中移除冗余的 `getHeadTemplateBuilder` 重载方法,统一构建入口 - 新增 `getIncludeFieldHeaders` 方法,增强字段包含逻辑,支持驼峰与下划线格式匹配 -修复表头排序问题,确保启用描述时正确应用字段顺序 - 在 `PropertyUtil` 中新增 `getHeaders`重载方法,支持传入字段过滤列表 -`优化 `SheetBuilder 构建流程,使其能正确传递 `includeFields` 并应用字段描述映射 - 移除不必要的包导入与空行,提升代码可读性- 增加 `@NotNull` 注解以强化静态检查能力 --- .../cn/isliu/core/builder/SheetBuilder.java | 86 +++++++++--------- .../java/cn/isliu/core/utils/FsTableUtil.java | 71 ++++++++++----- .../cn/isliu/core/utils/PropertyUtil.java | 87 ++++++++++++++++--- 3 files changed, 165 insertions(+), 79 deletions(-) diff --git a/src/main/java/cn/isliu/core/builder/SheetBuilder.java b/src/main/java/cn/isliu/core/builder/SheetBuilder.java index 6d37e44..bac3724 100644 --- a/src/main/java/cn/isliu/core/builder/SheetBuilder.java +++ b/src/main/java/cn/isliu/core/builder/SheetBuilder.java @@ -24,17 +24,17 @@ import java.util.stream.Collectors; * 提供链式调用方式创建飞书表格,支持字段过滤等高级功能。 */ public class SheetBuilder { - + private final String sheetName; private final String spreadsheetToken; private final Class clazz; private List includeFields; private final Map customProperties = new HashMap<>(); private final Map fieldDescriptions = new HashMap<>(); - + /** * 构造函数 - * + * * @param sheetName 工作表名称 * @param spreadsheetToken 电子表格Token * @param clazz 实体类Class对象 @@ -44,12 +44,12 @@ public class SheetBuilder { this.spreadsheetToken = spreadsheetToken; this.clazz = clazz; } - + /** * 设置包含的字段列表 - * + * * 指定要包含在表格中的字段名称列表。如果不设置,则包含所有带有@TableProperty注解的字段。 - * + * * @param fields 要包含的字段名称列表 * @return SheetBuilder实例,支持链式调用 */ @@ -57,12 +57,12 @@ public class SheetBuilder { this.includeFields = new ArrayList<>(fields); return this; } - + /** * 设置自定义属性 - * + * * 添加一个自定义属性,可以在构建表格时使用 - * + * * @param key 属性键 * @param value 属性值 * @return SheetBuilder实例,支持链式调用 @@ -71,12 +71,12 @@ public class SheetBuilder { this.customProperties.put(key, value); return this; } - + /** * 批量设置自定义属性 - * + * * 批量添加自定义属性,可以在构建表格时使用 - * + * * @param properties 自定义属性映射 * @return SheetBuilder实例,支持链式调用 */ @@ -84,34 +84,34 @@ public class SheetBuilder { 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实例,支持链式调用 */ @@ -119,12 +119,12 @@ public class SheetBuilder { this.fieldDescriptions.putAll(fieldDescriptions); return this; } - + /** * 设置单个字段描述 - * + * * 为指定字段设置自定义描述信息。 - * + * * @param fieldName 字段名 * @param description 描述文本 * @return SheetBuilder实例,支持链式调用 @@ -133,60 +133,60 @@ public class SheetBuilder { this.fieldDescriptions.put(fieldName, description); return this; } - + /** * 获取字段描述映射 - * + * * @return 包含所有字段描述的映射 */ public Map getFieldDescriptions() { return new HashMap<>(this.fieldDescriptions); } - + /** * 构建表格并返回工作表ID - * + * * 根据配置的参数创建飞书表格,包括表头、样式、单元格格式和下拉选项等。 - * + * * @return 创建成功返回工作表ID */ public String build() { // 获取所有字段映射 Map allFieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); - + // 根据includeFields过滤字段映射 Map fieldsMap = filterFieldsMap(allFieldsMap); - + // 生成表头 - List headers = PropertyUtil.getHeaders(fieldsMap); - + List headers = PropertyUtil.getHeaders(fieldsMap, includeFields); + // 获取表格配置 TableConf tableConf = PropertyUtil.getTableConf(clazz); - + // 创建飞书客户端 FeishuClient client = FsClient.getInstance().getClient(); - + // 1、创建sheet String sheetId = FsApiUtil.createSheet(sheetName, client, spreadsheetToken); - + // 2、添加表头数据 - FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, includeFields, tableConf, fieldDescriptions), client); - + FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, fieldDescriptions), client); + // 3、设置表格样式 FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, fieldsMap, tableConf), client, spreadsheetToken); - + // 4、合并单元格 List mergeCell = FsTableUtil.getMergeCell(sheetId, fieldsMap); if (!mergeCell.isEmpty()) { mergeCell.forEach(cell -> FsApiUtil.mergeCells(cell, client, spreadsheetToken)); } - + // 5、设置单元格为文本格式 if (tableConf.isText()) { String column = FsTableUtil.getColumnNameByNuNumber(headers.size()); FsApiUtil.setCellType(sheetId, "@", "A1", column + 200, client, spreadsheetToken); } - + // 6、设置表格下拉 try { FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc(), customProperties); @@ -196,10 +196,10 @@ public class SheetBuilder { return sheetId; } - + /** * 根据包含字段列表过滤字段映射 - * + * * @param allFieldsMap 所有字段映射 * @return 过滤后的字段映射 */ @@ -208,7 +208,7 @@ public class SheetBuilder { if (includeFields == null || includeFields.isEmpty()) { return allFieldsMap; } - + // 根据字段名过滤,保留指定的字段 return allFieldsMap.entrySet().stream() .filter(entry -> { diff --git a/src/main/java/cn/isliu/core/utils/FsTableUtil.java b/src/main/java/cn/isliu/core/utils/FsTableUtil.java index cdb05d8..1cc401a 100644 --- a/src/main/java/cn/isliu/core/utils/FsTableUtil.java +++ b/src/main/java/cn/isliu/core/utils/FsTableUtil.java @@ -14,6 +14,7 @@ import cn.isliu.core.service.CustomValueService; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; +import org.jetbrains.annotations.NotNull; import java.lang.reflect.InvocationTargetException; import java.util.*; @@ -356,18 +357,18 @@ public class FsTableUtil { 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, TableConf tableConf) { - return getHeadTemplateBuilder(sheetId, headers, fieldsMap, null, tableConf); + return getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf, null); } 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) { + Map fieldsMap, TableConf tableConf, Map fieldDescriptions) { String position = FsTableUtil.getColumnNameByNuNumber(headers.size()); @@ -378,22 +379,15 @@ public class FsTableUtil { int maxLevel = getMaxLevel(fieldsMap); if (maxLevel == 1) { - // 单层级表头:按order排序的headers - 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, fieldDescriptions)); + batchPutValuesBuilder.addRow(headers.toArray()); + batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap, fieldDescriptions)); } else { batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow); - batchPutValuesBuilder.addRow(sortedHeaders.toArray()); + batchPutValuesBuilder.addRow(headers.toArray()); } } else { @@ -421,21 +415,47 @@ public class FsTableUtil { // 如果启用了描述,在最后一行添加描述 if (tableConf.enableDesc()) { - 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, fieldDescriptions)); + batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap, fieldDescriptions)); } } return batchPutValuesBuilder.build(); } + @NotNull + private static List getIncludeFieldHeaders(List headers, Map fieldsMap, List includeFields) { + return includeFields.stream() + .map(includeField -> { + // 查找匹配的fieldsMap key + for (Map.Entry entry : fieldsMap.entrySet()) { + FieldProperty fieldProperty = entry.getValue(); + if (fieldProperty != null && fieldProperty.getTableProperty() != null) { + String field = fieldProperty.getField(); + if (field != null) { + // 获取最后一个属性并转换为下划线格式 + String[] split = field.split("\\."); + String lastValue = split[split.length - 1]; + String underscoreFormat = StringUtil.toUnderscoreCase(lastValue); + // 如果匹配,返回fieldsMap的key + if (underscoreFormat.equals(includeField)) { + return entry.getKey(); + } + + if (lastValue.equals(includeField)) { + return entry.getKey(); + } + } + } + } + // 如果没有匹配到,返回原始值 + return includeField; + }) + .sorted(Comparator.comparingInt(includeFields::indexOf)) + .collect(Collectors.toList()); + } + /** * 获取按order排序的表头列表 * @@ -480,6 +500,9 @@ public class FsTableUtil { String fieldPath = fieldProperty.getField(); String fieldName = fieldPath.substring(fieldPath.lastIndexOf(".") + 1); desc = fieldDescriptions.get(fieldName); + if (desc == null) { + desc = fieldDescriptions.get(StringUtil.toUnderscoreCase(fieldName)); + } } // 如果映射中没有找到,则从注解中获取 diff --git a/src/main/java/cn/isliu/core/utils/PropertyUtil.java b/src/main/java/cn/isliu/core/utils/PropertyUtil.java index dc08b9c..50626e0 100644 --- a/src/main/java/cn/isliu/core/utils/PropertyUtil.java +++ b/src/main/java/cn/isliu/core/utils/PropertyUtil.java @@ -26,14 +26,14 @@ public class PropertyUtil { /** * 获取类及其嵌套类上@TableProperty注解的字段映射关系 - * + * * 此方法是入口方法,用于获取一个类及其所有嵌套类中, * 被@TableProperty注解标记的字段的映射关系。 * 注解中的值作为key,FieldProperty对象作为value返回。 * * 对于嵌套属性,使用'.'连接符来表示层级关系。 * 该方法会过滤掉有子级的字段,只返回最底层的字段映射。 - * + * * @param clazz 要处理的类 * @return 包含所有@TableProperty注解字段映射关系的Map,嵌套属性使用'.'连接 */ @@ -59,7 +59,7 @@ public class PropertyUtil { * 收集所有被@TableProperty注解标记的字段信息。 * * 方法会处理循环引用问题,并限制递归深度,防止栈溢出。 - * + * * @param clazz 当前处理的类 * @param result 存储结果的Map * @param keyPrefix key的前缀(使用注解中的值构建) @@ -73,7 +73,7 @@ public class PropertyUtil { if (!isTargetPackageClass(clazz)) { return; } - + // 检测循环引用,限制递归深度 Integer currentDepth = depthMap.getOrDefault(clazz, 0); if (currentDepth > 5) { // 限制最大递归深度为5 @@ -129,14 +129,14 @@ public class PropertyUtil { if (clazz == null) { return false; } - + String className = clazz.getName(); // 只处理用户自定义的类,排除系统类 - return !className.startsWith("java.") && - !className.startsWith("javax.") && - !className.startsWith("sun.") && - !className.startsWith("com.sun.") && - !className.startsWith("jdk."); + return !className.startsWith("java.") && + !className.startsWith("javax.") && + !className.startsWith("sun.") && + !className.startsWith("com.sun.") && + !className.startsWith("jdk."); } /** @@ -276,7 +276,7 @@ public class PropertyUtil { // 构建新的前缀 String newKeyPrefix; String newValuePrefix = valuePrefix.isEmpty() ? field.getName() : valuePrefix + "." + field.getName(); - + // 关键修改:如果父节点没有注解,则不拼接父节点字段名 if (parentHasAnnotation) { // 父节点有注解,需要拼接 @@ -349,7 +349,7 @@ public class PropertyUtil { clazz.equals(Character.class) || clazz.equals(Byte.class) || clazz.equals(Short.class) || - clazz.equals(java.util.Date.class) || + clazz.equals(Date.class) || clazz.equals(java.time.LocalDate.class) || clazz.equals(java.time.LocalDateTime.class)); } @@ -365,7 +365,70 @@ public class PropertyUtil { */ @NotNull public static List getHeaders(Map fieldsMap) { + return getSortedHeaders(fieldsMap); + } + + /** + * 从字段属性映射中提取表头列表 + * + * 此方法根据字段的@TableProperty注解中的order属性对字段进行排序, + * 返回按顺序排列的表头列表,用于数据展示时的列顺序。 + * + * @param fieldsMap 字段属性映射 + * @return 按顺序排列的表头列表 + */ + @NotNull + public static List getHeaders(Map fieldsMap, List includeFields) { + List sortedHeaders; + if (includeFields != null && !includeFields.isEmpty()) { + sortedHeaders = getIncludeFieldHeaders(fieldsMap, includeFields); + } else { + sortedHeaders = getSortedHeaders(fieldsMap); + } + return sortedHeaders; + } + + @NotNull + private static List getIncludeFieldHeaders(Map fieldsMap, List includeFields) { + return includeFields.stream() + .map(includeField -> { + // 查找匹配的fieldsMap key + for (Map.Entry entry : fieldsMap.entrySet()) { + FieldProperty fieldProperty = entry.getValue(); + if (fieldProperty != null && fieldProperty.getTableProperty() != null) { + String field = fieldProperty.getField(); + if (field != null) { + // 获取最后一个属性并转换为下划线格式 + String[] split = field.split("\\."); + String lastValue = split[split.length - 1]; + String underscoreFormat = StringUtil.toUnderscoreCase(lastValue); + // 如果匹配,返回fieldsMap的key + if (underscoreFormat.equals(includeField)) { + return entry.getKey(); + } + + if (lastValue.equals(includeField)) { + return entry.getKey(); + } + } + } + } + // 如果没有匹配到,返回原始值 + return includeField; + }) + .sorted(Comparator.comparingInt(includeFields::indexOf)) + .collect(Collectors.toList()); + } + + /** + * 获取按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());