From 8a49e5280b49b03a32b04f39337186f6e6924de0 Mon Sep 17 00:00:00 2001 From: liushuang Date: Tue, 26 Aug 2025 18:24:52 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=E9=87=8D=E6=9E=84=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=A0=BC=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 FeishuClient 类,优化自定义服务管理 - 新增 ServiceManager 类,统一管理自定义服务 - 优化 CustomCellService 中的单元格操作逻辑- 移除不必要的请求类型和参数处理 - 新增合并单元格功能 - 调整表格样式设置接口 - 优化 FsClient 类,使用 ThreadLocal管理客户端实例 --- .gitignore | 1 + src/main/java/cn/isliu/FsHelper.java | 12 +- .../cn/isliu/core/client/FeishuClient.java | 113 ++-- .../java/cn/isliu/core/client/FsClient.java | 70 +-- .../cn/isliu/core/client/ServiceManager.java | 36 ++ .../service/AbstractFeishuApiService.java | 2 +- .../isliu/core/service/CustomCellService.java | 150 ++---- .../java/cn/isliu/core/utils/FsApiUtil.java | 28 +- .../java/cn/isliu/core/utils/FsTableUtil.java | 494 ++++++++++++++++-- 9 files changed, 645 insertions(+), 261 deletions(-) create mode 100644 src/main/java/cn/isliu/core/client/ServiceManager.java diff --git a/.gitignore b/.gitignore index 186de29..076aa8b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ test/ *.ipr .idea .kiro +src/main/java/cn/isliu/example/ ### Eclipse ### .apt_generated diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java index 77300d5..289db17 100644 --- a/src/main/java/cn/isliu/FsHelper.java +++ b/src/main/java/cn/isliu/FsHelper.java @@ -11,6 +11,7 @@ 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.CustomCellService; import cn.isliu.core.service.CustomValueService; import cn.isliu.core.utils.*; import com.google.gson.JsonObject; @@ -54,7 +55,13 @@ public class FsHelper { FsApiUtil.putValues(spreadsheetToken, FsTableUtil.getHeadTemplateBuilder(sheetId, headers, fieldsMap, tableConf), client); // 3 设置表格样式 - FsApiUtil.setTableStyle(FsTableUtil.getDefaultTableStyle(sheetId, headers.size(), tableConf), sheetId, client, spreadsheetToken); + 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)); + } // 4 设置单元格为文本格式 if (tableConf.isText()) { @@ -84,9 +91,10 @@ public class FsHelper { FeishuClient client = FsClient.getInstance().getClient(); Sheet sheet = FsApiUtil.getSheetMetadata(sheetId, client, spreadsheetToken); TableConf tableConf = PropertyUtil.getTableConf(clazz); - List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf); Map fieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); + List fsTableDataList = FsTableUtil.getFsTableData(sheet, spreadsheetToken, tableConf); + List fieldPathList = fieldsMap.values().stream().map(FieldProperty::getField).collect(Collectors.toList()); fsTableDataList.forEach(tableData -> { diff --git a/src/main/java/cn/isliu/core/client/FeishuClient.java b/src/main/java/cn/isliu/core/client/FeishuClient.java index a166a14..2a36c8f 100644 --- a/src/main/java/cn/isliu/core/client/FeishuClient.java +++ b/src/main/java/cn/isliu/core/client/FeishuClient.java @@ -1,6 +1,5 @@ package cn.isliu.core.client; -import cn.isliu.core.service.*; import com.lark.oapi.Client; import com.lark.oapi.core.enums.AppType; import com.lark.oapi.service.drive.DriveService; @@ -10,8 +9,13 @@ import okhttp3.OkHttpClient; import java.util.concurrent.TimeUnit; +import cn.isliu.core.service.*; + /** - * 飞书扩展客户端 封装官方SDK客户端并提供额外API支持 + * 飞书客户端,用于与飞书API进行交互 + *

+ * 该客户端整合了官方SDK和自定义扩展功能,提供了对飞书表格的完整操作能力。 + * 包括官方提供的基础功能和项目自定义的扩展功能。 */ public class FeishuClient { private final Client officialClient; @@ -19,13 +23,8 @@ public class FeishuClient { private final String appId; private final String appSecret; - // 自定义服务,处理官方SDK未覆盖的API - private volatile CustomSheetService customSheetService; - private volatile CustomDimensionService customDimensionService; - private volatile CustomCellService customCellService; - private volatile CustomValueService customValueService; - private volatile CustomDataValidationService customDataValidationService; - private volatile CustomProtectedDimensionService customProtectedDimensionService; + // 服务管理器,用于统一管理自定义服务实例 + private final ServiceManager serviceManager = new ServiceManager<>(this); private FeishuClient(String appId, String appSecret, Client officialClient, OkHttpClient httpClient) { this.appId = appId; @@ -36,8 +35,8 @@ public class FeishuClient { /** * 创建客户端构建器 - * - * @param appId 应用ID + * + * @param appId 应用ID * @param appSecret 应用密钥 * @return 构建器 */ @@ -47,7 +46,7 @@ public class FeishuClient { /** * 获取官方表格服务 - * + * * @return 官方表格服务 */ public SheetsService sheets() { @@ -56,7 +55,7 @@ public class FeishuClient { /** * 获取官方驱动服务 - * + * * @return 官方驱动服务 */ public DriveService drive() { @@ -65,103 +64,61 @@ public class FeishuClient { /** * 获取扩展表格服务 - * + * * @return 扩展表格服务 */ public CustomSheetService customSheets() { - if (customSheetService == null) { - synchronized (this) { - if (customSheetService == null) { - customSheetService = new CustomSheetService(this); - } - } - } - return customSheetService; + return serviceManager.getService(CustomSheetService.class, () -> new CustomSheetService(this)); } /** * 获取扩展行列服务 - * + * * @return 扩展行列服务 */ public CustomDimensionService customDimensions() { - if (customDimensionService == null) { - synchronized (this) { - if (customDimensionService == null) { - customDimensionService = new CustomDimensionService(this); - } - } - } - return customDimensionService; + return serviceManager.getService(CustomDimensionService.class, () -> new CustomDimensionService(this)); } /** * 获取扩展单元格服务 - * + * * @return 扩展单元格服务 */ public CustomCellService customCells() { - if (customCellService == null) { - synchronized (this) { - if (customCellService == null) { - customCellService = new CustomCellService(this); - } - } - } - return customCellService; + return serviceManager.getService(CustomCellService.class, () -> new CustomCellService(this)); } /** * 获取扩展数据值服务 - * + * * @return 扩展数据值服务 */ public CustomValueService customValues() { - if (customValueService == null) { - synchronized (this) { - if (customValueService == null) { - customValueService = new CustomValueService(this); - } - } - } - return customValueService; + return serviceManager.getService(CustomValueService.class, () -> new CustomValueService(this)); } /** * 获取自定义数据验证服务 - * + * * @return 自定义数据验证服务 */ public CustomDataValidationService customDataValidations() { - if (customDataValidationService == null) { - synchronized (this) { - if (customDataValidationService == null) { - customDataValidationService = new CustomDataValidationService(this); - } - } - } - return customDataValidationService; + return serviceManager.getService(CustomDataValidationService.class, () -> new CustomDataValidationService(this)); } /** * 获取扩展保护范围服务 - * + * * @return 扩展保护范围服务 */ public CustomProtectedDimensionService customProtectedDimensions() { - if (customProtectedDimensionService == null) { - synchronized (this) { - if (customProtectedDimensionService == null) { - customProtectedDimensionService = new CustomProtectedDimensionService(this); - } - } - } - return customProtectedDimensionService; + return serviceManager.getService(CustomProtectedDimensionService.class, () -> new CustomProtectedDimensionService(this)); } /** * 获取官方客户端 - * + * * @return 官方Client实例 */ public Client getOfficialClient() { @@ -170,7 +127,7 @@ public class FeishuClient { /** * 获取HTTP客户端 - * + * * @return OkHttp客户端实例 */ public OkHttpClient getHttpClient() { @@ -179,7 +136,7 @@ public class FeishuClient { /** * 获取应用ID - * + * * @return 应用ID */ public String getAppId() { @@ -188,7 +145,7 @@ public class FeishuClient { /** * 获取应用密钥 - * + * * @return 应用密钥 */ public String getAppSecret() { @@ -210,13 +167,13 @@ public class FeishuClient { this.appSecret = appSecret; // 默认OkHttp配置 this.httpClientBuilder = - new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES) - .writeTimeout(10, TimeUnit.MINUTES).connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)); + new OkHttpClient.Builder().connectTimeout(10, TimeUnit.MINUTES).readTimeout(10, TimeUnit.MINUTES) + .writeTimeout(10, TimeUnit.MINUTES).connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)); } /** * 配置HTTP客户端 - * + * * @param builder OkHttp客户端构建器 * @return 当前构建器 */ @@ -227,7 +184,7 @@ public class FeishuClient { /** * 设置应用类型 - * + * * @param appType 应用类型 * @return 当前构建器 */ @@ -238,7 +195,7 @@ public class FeishuClient { /** * 是否在debug级别打印请求 - * + * * @param logReqAtDebug 是否打印 * @return 当前构建器 */ @@ -249,13 +206,13 @@ public class FeishuClient { /** * 构建FeishuClient实例 - * + * * @return FeishuClient实例 */ public FeishuClient build() { // 构建官方Client Client officialClient = - Client.newBuilder(appId, appSecret).appType(appType).logReqAtDebug(logReqAtDebug).build(); + Client.newBuilder(appId, appSecret).appType(appType).logReqAtDebug(logReqAtDebug).build(); // 构建OkHttpClient OkHttpClient httpClient = httpClientBuilder.build(); diff --git a/src/main/java/cn/isliu/core/client/FsClient.java b/src/main/java/cn/isliu/core/client/FsClient.java index a7a1d79..3e64305 100644 --- a/src/main/java/cn/isliu/core/client/FsClient.java +++ b/src/main/java/cn/isliu/core/client/FsClient.java @@ -2,21 +2,20 @@ package cn.isliu.core.client; /** * 线程安全的飞书客户端管理器 - * 使用双重检查锁定单例模式确保线程安全 + * 使用ThreadLocal为每个线程维护独立的客户端实例 */ -public class FsClient { - +public class FsClient implements AutoCloseable { + private static volatile FsClient instance; - private volatile FeishuClient client; - private final Object lock = new Object(); - + private final ThreadLocal clientHolder = new ThreadLocal<>(); + // 私有构造函数防止外部实例化 private FsClient() { } - + /** * 获取单例实例 - 使用双重检查锁定模式 - * @return FeishuClientManager实例 + * @return FsClient实例 */ public static FsClient getInstance() { if (instance == null) { @@ -28,22 +27,23 @@ public class FsClient { } return instance; } - + /** * 线程安全的客户端获取 * @return FeishuClient实例 * @throws IllegalStateException 如果客户端未初始化 */ public FeishuClient getClient() { - FeishuClient currentClient = client; + FeishuClient currentClient = clientHolder.get(); if (currentClient == null) { throw new IllegalStateException("FeishuClient not initialized. Please call initializeClient first."); } return currentClient; } - + /** * 线程安全的客户端初始化 + * 每个线程调用此方法会创建并维护自己的客户端实例 * @param appId 飞书应用ID * @param appSecret 飞书应用密钥 * @return 初始化的FeishuClient实例 @@ -55,43 +55,53 @@ public class FsClient { if (appSecret == null || appSecret.trim().isEmpty()) { throw new IllegalArgumentException("appSecret cannot be null or empty"); } - - if (client == null) { - synchronized (lock) { - if (client == null) { - client = FeishuClient.newBuilder(appId, appSecret).build(); - } - } - } + + FeishuClient client = FeishuClient.newBuilder(appId, appSecret).build(); + clientHolder.set(client); return client; } - + /** * 设置客户端实例(用于外部已构建的客户端) + * 每个线程调用此方法会设置自己的客户端实例 * @param feishuClient 外部构建的FeishuClient实例 */ public void setClient(FeishuClient feishuClient) { if (feishuClient == null) { throw new IllegalArgumentException("FeishuClient cannot be null"); } - - synchronized (lock) { - this.client = feishuClient; - } + + clientHolder.set(feishuClient); } - + /** - * 检查客户端是否已初始化 - * @return true如果客户端已初始化,否则false + * 检查当前线程的客户端是否已初始化 + * @return true如果当前线程客户端已初始化,否则false */ public boolean isInitialized() { - return client != null; + return clientHolder.get() != null; } - + + /** + * 清除当前线程的客户端实例(主要用于资源清理) + */ + public void clearClient() { + clientHolder.remove(); + } + /** * 重置客户端(主要用于测试) */ public synchronized void resetForTesting() { - client = null; + clientHolder.remove(); + } + + /** + * 实现AutoCloseable接口,用于try-with-resources语句 + * 清理当前线程的客户端实例 + */ + @Override + public void close() { + clearClient(); } } \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/client/ServiceManager.java b/src/main/java/cn/isliu/core/client/ServiceManager.java new file mode 100644 index 0000000..8502ef3 --- /dev/null +++ b/src/main/java/cn/isliu/core/client/ServiceManager.java @@ -0,0 +1,36 @@ +package cn.isliu.core.client; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * 服务管理器,用于统一管理FeishuClient中的各种服务实例 + * + * @param 服务类型 + */ +class ServiceManager { + private final ConcurrentHashMap, Object> services = new ConcurrentHashMap<>(); + private final FeishuClient client; + + /** + * 构造函数 + * + * @param client FeishuClient实例 + */ + ServiceManager(FeishuClient client) { + this.client = client; + } + + /** + * 获取指定类型的服务实例 + * + * @param serviceClass 服务类 + * @param supplier 服务实例提供者 + * @param 服务类型 + * @return 服务实例 + */ + @SuppressWarnings("unchecked") + T getService(Class serviceClass, Supplier supplier) { + return (T) services.computeIfAbsent(serviceClass, k -> supplier.get()); + } +} \ No newline at end of file diff --git a/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java b/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java index fac253f..3679b97 100644 --- a/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java +++ b/src/main/java/cn/isliu/core/service/AbstractFeishuApiService.java @@ -45,7 +45,7 @@ public abstract class AbstractFeishuApiService { */ protected String getTenantAccessToken() throws IOException { try { - return tokenManager.getCachedTenantAccessToken(); + return tokenManager.getTenantAccessToken(); } catch (FsHelperException e) { throw new IOException("Failed to get tenant access token: " + e.getMessage(), e); } diff --git a/src/main/java/cn/isliu/core/service/CustomCellService.java b/src/main/java/cn/isliu/core/service/CustomCellService.java index 5c585c6..9d38bc2 100644 --- a/src/main/java/cn/isliu/core/service/CustomCellService.java +++ b/src/main/java/cn/isliu/core/service/CustomCellService.java @@ -1,6 +1,5 @@ package cn.isliu.core.service; - import cn.isliu.core.client.FeishuClient; import cn.isliu.core.pojo.ApiResponse; import okhttp3.Request; @@ -51,22 +50,15 @@ public class CustomCellService extends AbstractFeishuApiService { if (cellRequest.getMergeCells() != null) { String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/merge_cells"; - String params; - - String type = cellRequest.getMergeCells().getType(); - if (type != null && !type.isEmpty() && "JSON_STR".equals(type)) { - params = cellRequest.getMergeCells().getParams(); - } else { - // 获取合并单元格范围 - String range = cellRequest.getMergeCells().getRange(); - if (range == null) { - ApiResponse errorResponse = new ApiResponse(); - errorResponse.setCode(400); - errorResponse.setMsg("Invalid cell range"); - return errorResponse; - } - params = gson.toJson(new MergeCellsRequestBody(range, cellRequest.getMergeCells().getMergeType())); + // 获取合并单元格范围 + String range = cellRequest.getMergeCells().getRange(); + if (range == null) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Invalid cell range"); + return errorResponse; } + String params = gson.toJson(new MergeCellsRequestBody(range, cellRequest.getMergeCells().getMergeType())); // 构建合并单元格请求体 RequestBody body = RequestBody.create(params, JSON_MEDIA_TYPE); @@ -133,32 +125,25 @@ public class CustomCellService extends AbstractFeishuApiService { else if (cellRequest.getStyleCellsBatch() != null) { String url = BASE_URL + "/sheets/v2/spreadsheets/" + spreadsheetToken + "/styles_batch_update"; - String type = cellRequest.getStyleCellsBatch().getType(); - String params = ""; - if (type != null && !type.isEmpty() && "JSON_STR".equals(type)) { - params = cellRequest.getStyleCellsBatch().getParams(); - } else { - // 获取单元格范围和样式 - List ranges = cellRequest.getStyleCellsBatch().getRanges(); - Style style = cellRequest.getStyleCellsBatch().getStyle(); + // 获取单元格范围和样式 + List ranges = cellRequest.getStyleCellsBatch().getRanges(); + Style style = cellRequest.getStyleCellsBatch().getStyle(); - if (ranges == null || ranges.isEmpty()) { - ApiResponse errorResponse = new ApiResponse(); - errorResponse.setCode(400); - errorResponse.setMsg("Invalid cell ranges"); - return errorResponse; - } - - // 构建批量设置样式请求体 - StyleBatchUpdateRequest styleBatchRequest = new StyleBatchUpdateRequest(); - StyleBatchData styleBatchData = new StyleBatchData(); - styleBatchData.setRanges(ranges); - styleBatchData.setStyle(style); - styleBatchRequest.getData().add(styleBatchData); - params = gson.toJson(styleBatchRequest); + if (ranges == null || ranges.isEmpty()) { + ApiResponse errorResponse = new ApiResponse(); + errorResponse.setCode(400); + errorResponse.setMsg("Invalid cell ranges"); + return errorResponse; } - RequestBody body = RequestBody.create(params, JSON_MEDIA_TYPE); + // 构建批量设置样式请求体 + StyleBatchUpdateRequest styleBatchRequest = new StyleBatchUpdateRequest(); + StyleBatchData styleBatchData = new StyleBatchData(); + styleBatchData.setRanges(ranges); + styleBatchData.setStyle(style); + styleBatchRequest.getData().add(styleBatchData); + + RequestBody body = RequestBody.create(gson.toJson(styleBatchRequest), JSON_MEDIA_TYPE); Request httpRequest = createAuthenticatedRequest(url, "PUT", body).build(); response = executeRequest(httpRequest, ApiResponse.class); @@ -371,16 +356,6 @@ public class CustomCellService extends AbstractFeishuApiService { request.setMergeCells(mergeCells); } - public MergeCellsBuilder setReqType(String reqType) { - mergeCells.setType(reqType); - return this; - } - - public MergeCellsBuilder setReqParams(String reqParams) { - mergeCells.setParams(reqParams); - return this; - } - /** * 设置要合并的单元格所在的工作表ID * @@ -519,10 +494,7 @@ public class CustomCellService extends AbstractFeishuApiService { /** * 合并方式 可选值: MERGE_ALL:合并所有单元格 MERGE_ROWS:按行合并 MERGE_COLUMNS:按列合并 */ - private String mergeType; - - private String type; - private String params; + private String mergeType = "MERGE_ALL"; /** * 获取要合并的单元格范围 @@ -612,23 +584,6 @@ public class CustomCellService extends AbstractFeishuApiService { public void setMergeType(String mergeType) { this.mergeType = mergeType; } - - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getParams() { - return params; - } - - public void setParams(String params) { - this.params = params; - } } /** @@ -859,16 +814,16 @@ public class CustomCellService extends AbstractFeishuApiService { * 单元格样式 */ public static class Style { - private Font font; - private Integer textDecoration; - private String formatter; - private Integer hAlign; - private Integer vAlign; - private String foreColor; - private String backColor; - private String borderType; - private String borderColor; - private Boolean clean; + private Font font = new Font(); + private Integer textDecoration = 0; + private String formatter= ""; + private Integer hAlign = 1; + private Integer vAlign = 1; + private String foreColor = "#ffffff"; + private String backColor = "#000000"; + private String borderType = "FULL_BORDER"; + private String borderColor = "#6d6d6d"; + private Boolean clean = false; /** * 获取字体样式 @@ -1056,10 +1011,10 @@ public class CustomCellService extends AbstractFeishuApiService { * 字体样式 */ public static class Font { - private Boolean bold; - private Boolean italic; - private String fontSize; - private Boolean clean; + private Boolean bold = true; + private Boolean italic = false; + private String fontSize = "10pt/1.5"; + private Boolean clean = false; /** * 获取是否加粗 @@ -1778,8 +1733,6 @@ public class CustomCellService extends AbstractFeishuApiService { private List ranges; private List cellRanges; private Style style; - private String type; - private String params; public StyleCellsBatchRequest() { this.ranges = new ArrayList<>(); @@ -1851,21 +1804,6 @@ public class CustomCellService extends AbstractFeishuApiService { public void setStyle(Style style) { this.style = style; } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getParams() { - return params; - } - public void setParams(String params) { - this.params = params; - } } /** @@ -1968,16 +1906,6 @@ public class CustomCellService extends AbstractFeishuApiService { request.setStyleCellsBatch(styleCellsBatch); } - public StyleCellsBatchBuilder setReqType(String reqType) { - styleCellsBatch.setType(reqType); - return this; - } - - public StyleCellsBatchBuilder setParams(String params) { - styleCellsBatch.setParams(params); - return this; - } - /** * 添加要设置样式的单元格范围 * diff --git a/src/main/java/cn/isliu/core/utils/FsApiUtil.java b/src/main/java/cn/isliu/core/utils/FsApiUtil.java index 79ac4e7..59d66d6 100644 --- a/src/main/java/cn/isliu/core/utils/FsApiUtil.java +++ b/src/main/java/cn/isliu/core/utils/FsApiUtil.java @@ -125,25 +125,41 @@ public class FsApiUtil { } } - public static void setTableStyle(String style, String sheetId, FeishuClient client, String spreadsheetToken) { + public static void setTableStyle(CustomCellService.StyleCellsBatchBuilder styleCellsBatchBuilder, FeishuClient client, String spreadsheetToken) { try { CustomCellService.CellBatchUpdateRequest batchUpdateRequest = CustomCellService.CellBatchUpdateRequest.newBuilder() - .addRequest(CustomCellService.CellRequest.styleCellsBatch().setReqType(REQ_TYPE) - .setParams(style.replaceAll("%SHEET_ID%", sheetId)) - .build()) + .addRequest(styleCellsBatchBuilder.build()) .build(); ApiResponse apiResponse = client.customCells().cellsBatchUpdate(spreadsheetToken, batchUpdateRequest); if (!apiResponse.success()) { - FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", style, apiResponse.getMsg()); + FsLogger.warn("【飞书表格】 写入表格样式数据异常!参数:{},错误信息:{}", styleCellsBatchBuilder, apiResponse.getMsg()); throw new FsHelperException("【飞书表格】 写入表格样式数据异常!"); } } catch (Exception e) { - FsLogger.warn("【飞书表格】 写入表格样式异常!参数:{},错误信息:{}", style, e.getMessage()); + FsLogger.warn("【飞书表格】 写入表格样式异常!参数:{},错误信息:{}", styleCellsBatchBuilder, e.getMessage()); throw new FsHelperException("【飞书表格】 写入表格样式异常!"); } } + public static void mergeCells(CustomCellService.CellRequest cellRequest, FeishuClient client, String spreadsheetToken) { + try { + CustomCellService.CellBatchUpdateRequest batchMergeRequest = CustomCellService.CellBatchUpdateRequest.newBuilder() + .addRequest(cellRequest) + .build(); + + ApiResponse batchMergeResp = client.customCells().cellsBatchUpdate(spreadsheetToken, batchMergeRequest); + + if (!batchMergeResp.success()) { + FsLogger.warn("【飞书表格】 合并单元格请求异常!参数:{},错误信息:{}", cellRequest.toString(), batchMergeResp.getMsg()); + throw new FsHelperException("【飞书表格】 合并单元格请求异常!"); + } + } catch (Exception e) { + FsLogger.warn("【飞书表格】 合并单元格异常!参数:{},错误信息:{}", cellRequest.toString(), e.getMessage(), e); + throw new FsHelperException("【飞书表格】 合并单元格异常!"); + } + } + public static String createSheet(String title, FeishuClient client, String spreadsheetToken) { String sheetId = null; try { diff --git a/src/main/java/cn/isliu/core/utils/FsTableUtil.java b/src/main/java/cn/isliu/core/utils/FsTableUtil.java index e754b4d..daf4281 100644 --- a/src/main/java/cn/isliu/core/utils/FsTableUtil.java +++ b/src/main/java/cn/isliu/core/utils/FsTableUtil.java @@ -9,6 +9,7 @@ import cn.isliu.core.converters.OptionsValueProcess; import cn.isliu.core.enums.BaseEnum; import cn.isliu.core.enums.TypeEnum; import cn.isliu.core.pojo.FieldProperty; +import cn.isliu.core.service.CustomCellService; import cn.isliu.core.service.CustomValueService; import com.google.gson.JsonElement; import com.google.gson.JsonParser; @@ -337,31 +338,71 @@ public class FsTableUtil { = CustomValueService.ValueRequest.batchPutValues(); // 获取父级表头 -// int maxLevel = getMaxLevel(fieldsMap); -// -// Map> levelListMap = groupFieldsByLevel(fieldsMap); -// for (int i = maxLevel; i >= 1; i--) { -// List values = levelListMap.get(i); -// batchPutValuesBuilder.addRange(sheetId + "!A" + i + ":" + position + i); -// -// } -// -// int titleRow = maxLevel; + int maxLevel = getMaxLevel(fieldsMap); - int titleRow = tableConf.titleRow(); - if (tableConf.enableDesc()) { - int descRow = titleRow + 1; - batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + descRow); - batchPutValuesBuilder.addRow(headers.toArray()); - batchPutValuesBuilder.addRow(getDescArray(headers, fieldsMap)); + if (maxLevel == 1) { + // 单层级表头:按order排序的headers + List 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)); + } else { + batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow); + batchPutValuesBuilder.addRow(sortedHeaders.toArray()); + } } else { - batchPutValuesBuilder.addRange(sheetId + "!A" + titleRow + ":" + position + titleRow); - batchPutValuesBuilder.addRow(headers.toArray()); + + // 多层级表头:构建层级结构并处理合并单元格 + 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()); + // 对于合并单元格,添加空值占位符 + for (int span = 1; span < cell.getColSpan(); span++) { + 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); + int descRow = maxLevel + 1; + batchPutValuesBuilder.addRange(sheetId + "!A" + descRow + ":" + position + descRow); + batchPutValuesBuilder.addRow(getDescArray(finalHeaders, fieldsMap)); + } } - + return batchPutValuesBuilder.build(); } + /** + * 获取按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()); + } + private static int getMaxLevel(Map fieldsMap) { AtomicInteger maxLevel = new AtomicInteger(1); fieldsMap.forEach((field, fieldProperty) -> { @@ -389,6 +430,7 @@ public class FsTableUtil { } else if (element.isJsonArray()) { descArray[i] = element.getAsJsonArray(); } else { +// desc = addLineBreaksPer8Chars(desc); descArray[i] = desc; } } catch (JsonSyntaxException e) { @@ -404,40 +446,426 @@ public class FsTableUtil { return descArray; } - public static String getDefaultTableStyle(String sheetId, int size, TableConf tableConf) { - int row = tableConf.titleRow(); + public static String getDefaultTableStyle(String sheetId, int size, Map fieldsMap, TableConf tableConf) { + int maxLevel = getMaxLevel(fieldsMap); String colorTemplate = "{\"data\": [{\"style\": {\"font\": {\"bold\": true, \"clean\": false, \"italic\": false, \"fontSize\": \"10pt/1.5\"}, \"clean\": false, \"hAlign\": 1, \"vAlign\": 1, \"backColor\": \"#000000\", \"foreColor\": \"#ffffff\", \"formatter\": \"\", \"borderType\": \"FULL_BORDER\", \"borderColor\": \"#000000\", \"textDecoration\": 0}, \"ranges\": [\"SHEET_ID!RANG\"]}]}"; colorTemplate = colorTemplate.replace("SHEET_ID", sheetId); - colorTemplate = colorTemplate.replace("RANG", "A" + row + ":" + FsTableUtil.getColumnNameByNuNumber(size) + row); + colorTemplate = colorTemplate.replace("RANG", "A1:" + FsTableUtil.getColumnNameByNuNumber(size) + maxLevel); colorTemplate = colorTemplate.replace("FORE_COLOR", tableConf.headFontColor()) .replace("BACK_COLOR", tableConf.headBackColor()); return colorTemplate; } + public static CustomCellService.StyleCellsBatchBuilder getDefaultTableStyle(String sheetId, Map fieldsMap, TableConf tableConf) { + int maxLevel = getMaxLevel(fieldsMap); + CustomCellService.StyleCellsBatchBuilder styleCellsBatchBuilder = CustomCellService.CellRequest.styleCellsBatch() + .addRange(sheetId, "A1", FsTableUtil.getColumnNameByNuNumber(fieldsMap.size()) + maxLevel) + .backColor(tableConf.headBackColor()) + .foreColor(tableConf.headFontColor()); + + return styleCellsBatchBuilder; + } + /** - * 根据层级分组字段属性 + * 根据层级分组字段属性,并按order排序 * * @param fieldsMap 字段属性映射 - * @return 按层级分组的映射,key为层级,value为该层级的字段名数组 + * @return 按层级分组的映射,key为层级,value为该层级的字段名数组(已按order排序) */ public static Map> groupFieldsByLevel(Map fieldsMap) { Map> levelMap = new HashMap<>(); - for (Map.Entry entry : fieldsMap.entrySet()) { + // 按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()); + + for (Map.Entry entry : sortedEntries) { FieldProperty fieldProperty = entry.getValue(); - if (fieldProperty != null && fieldProperty.getTableProperty() != null) { - String[] values = fieldProperty.getTableProperty().value(); - for (int i = 0; i < values.length; i++) { - levelMap.computeIfAbsent(i, k -> new ArrayList<>()).add(values[i]); - } + String[] values = fieldProperty.getTableProperty().value(); + for (int i = 0; i < values.length; i++) { + levelMap.computeIfAbsent(i, k -> new ArrayList<>()).add(values[i]); } } return levelMap; } - public static void main(String[] args) { - String str ="支持1~3个搜索"; - System.out.println(str.length()); + /** + * 构建多层级表头结构,支持按层级排序和合并 + * 根据需求实现层级分组和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) { + currentValue = values[0]; + } + } else { + // 多层级字段:需要对齐到maxLevel + // 计算从当前层级到值数组的映射 + int valueIndex = level - (maxLevel - values.length); + if (valueIndex >= 0 && valueIndex < values.length) { + currentValue = values[valueIndex]; + } + } + + headerCell.setValue(currentValue); + currentRow.add(headerCell); + } + } + + return headerRows; + } + + /** + * 获取排序后的字段列表,基于最子级字段排序的新规则 + * 核心规则: + * 1. 根据最子级字段的order进行主排序 + * 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); + + allGroups.add(new GroupInfo(groupEntry.getKey(), minOrder, fieldsInGroup)); + } + + // 新的排序逻辑:分组作为整体参与全局order排序 + // 创建排序单元列表(每个单元可能是单个字段或一个分组) + List sortUnits = new ArrayList<>(); + + for (GroupInfo group : allGroups) { + if ("default".equals(group.getGroupKey())) { + // 单层级字段:每个字段都是独立的排序单元 + for (Map.Entry field : group.getFields()) { + int order = field.getValue().getTableProperty().order(); + sortUnits.add(new SortUnit(order, Arrays.asList(field), false)); + } + } else { + // 多层级分组:整个分组作为一个排序单元,使用最小order + 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); + if (currentValue.equals(nextCell.getValue())) { + endCol++; + i++; // 跳过已经处理的单元格 + } else { + 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; + } + + /** + * 分组信息类,用于辅助排序 + */ + private static class GroupInfo { + private final String groupKey; + 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; } + } + + /** + * 排序项类,用于全局排序 + */ + private static class SortItem { + 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; } + } + + /** + * 排序单元类,用于分组整体排序 + * 一个排序单元可以是单个字段或一个完整的分组 + */ + private static class SortUnit { + 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) { + // 单层级字段放入默认分组 + groupKey = "default"; + } else { + // 多层级字段按完整路径分组(除最后一级) + StringBuilder pathBuilder = new StringBuilder(); + for (int i = 0; i < values.length - 1; i++) { + if (i > 0) pathBuilder.append("|"); + pathBuilder.append(values[i]); + } + groupKey = pathBuilder.toString(); + } + + groupedFields.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(entry); + } + } + + return groupedFields; + } + + /** + * 验证组内字段order的连续性 + * 三级及以上层级要求同一分组内的字段order必须连续 + * + * @param groupKey 分组key + * @param fieldsInGroup 分组内的字段列表(已按order排序) + */ + private static void validateOrderContinuity(String groupKey, List> fieldsInGroup) { + 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必须连续。", + groupKey, prevFieldName, prevOrder, currentFieldName, currentOrder) + ); + } + } + } + + /** + * 表头单元格类,用于支持合并单元格 + */ + public static class HeaderCell { + private String value; + 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 添加换行符后的文本 + */ + public static String addLineBreaks(String text, int charsPerLine) { + if (text == null || text.isEmpty()) { + return text; + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < text.length(); i += charsPerLine) { + if (i > 0) { + result.append("\n"); + } + int endIndex = Math.min(i + charsPerLine, text.length()); + result.append(text.substring(i, endIndex)); + } + return result.toString(); + } + + /** + * 每8个字符添加一个换行符(默认方法) + * + * @param text 需要处理的文本 + * @return 添加换行符后的文本 + */ + public static String addLineBreaksPer8Chars(String text) { + return addLineBreaks(text, 8); } } \ No newline at end of file