From 3e25a91aede92dcb430b9398fe0b037ebbc67227 Mon Sep 17 00:00:00 2001 From: liushuang Date: Thu, 28 Aug 2025 18:47:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=96=B0=E5=A2=9E=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6=E6=96=87=E4=BB=B6=E6=9C=8D=E5=8A=A1=E5=92=8C=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E6=9E=84=E5=BB=BA=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CustomFileService 类,用于获取根目录元数据等文件操作 - 新增 RootFolderMetaResponse 类,用于解析根目录元数据响应 - 新增 SheetBuilder 类,提供链式调用方式创建飞书表格 - 在 FeishuClient 中添加 customFiles 方法,返回 CustomFileService 实例 - 在 FsApiUtil 中添加创建文件夹和表格的方法 - 在 FsHelper 中添加创建表格构建器方法 --- src/main/java/cn/isliu/FsHelper.java | 17 ++ .../cn/isliu/core/builder/SheetBuilder.java | 122 ++++++++ .../cn/isliu/core/client/FeishuClient.java | 10 + .../core/pojo/RootFolderMetaResponse.java | 280 ++++++++++++++++++ .../isliu/core/service/CustomFileService.java | 44 +++ .../java/cn/isliu/core/utils/FsApiUtil.java | 88 +++++- 6 files changed, 553 insertions(+), 8 deletions(-) create mode 100644 src/main/java/cn/isliu/core/builder/SheetBuilder.java create mode 100644 src/main/java/cn/isliu/core/pojo/RootFolderMetaResponse.java create mode 100644 src/main/java/cn/isliu/core/service/CustomFileService.java diff --git a/src/main/java/cn/isliu/FsHelper.java b/src/main/java/cn/isliu/FsHelper.java index 289db17..b717bdf 100644 --- a/src/main/java/cn/isliu/FsHelper.java +++ b/src/main/java/cn/isliu/FsHelper.java @@ -5,6 +5,7 @@ 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.SheetBuilder; import cn.isliu.core.client.FeishuClient; import cn.isliu.core.client.FsClient; import cn.isliu.core.enums.ErrorCode; @@ -74,6 +75,22 @@ public class FsHelper { return sheetId; } + /** + * 创建飞书表格构建器 + * + * 返回一个表格构建器实例,支持链式调用和高级配置选项, + * 如字段过滤等功能。 + * + * @param sheetName 工作表名称 + * @param spreadsheetToken 电子表格Token + * @param clazz 实体类Class对象,用于解析表头和字段属性 + * @param 实体类泛型 + * @return SheetBuilder实例,支持链式调用 + */ + public static SheetBuilder createBuilder(String sheetName, String spreadsheetToken, Class clazz) { + return new SheetBuilder<>(sheetName, spreadsheetToken, clazz); + } + /** * 从飞书表格中读取数据 diff --git a/src/main/java/cn/isliu/core/builder/SheetBuilder.java b/src/main/java/cn/isliu/core/builder/SheetBuilder.java new file mode 100644 index 0000000..a09a9ed --- /dev/null +++ b/src/main/java/cn/isliu/core/builder/SheetBuilder.java @@ -0,0 +1,122 @@ +package cn.isliu.core.builder; + +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.service.CustomCellService; +import cn.isliu.core.utils.FsApiUtil; +import cn.isliu.core.utils.FsTableUtil; +import cn.isliu.core.utils.PropertyUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 表格构建器 + * + * 提供链式调用方式创建飞书表格,支持字段过滤等高级功能。 + */ +public class SheetBuilder { + + private final String sheetName; + private final String spreadsheetToken; + private final Class clazz; + private List includeFields; + + /** + * 构造函数 + * + * @param sheetName 工作表名称 + * @param spreadsheetToken 电子表格Token + * @param clazz 实体类Class对象 + */ + public SheetBuilder(String sheetName, String spreadsheetToken, Class clazz) { + this.sheetName = sheetName; + this.spreadsheetToken = spreadsheetToken; + this.clazz = clazz; + } + + /** + * 设置包含的字段列表 + * + * 指定要包含在表格中的字段名称列表。如果不设置,则包含所有带有@TableProperty注解的字段。 + * + * @param fields 要包含的字段名称列表 + * @return SheetBuilder实例,支持链式调用 + */ + public SheetBuilder includeColumnField(List fields) { + this.includeFields = new ArrayList<>(fields); + return this; + } + + /** + * 构建表格并返回工作表ID + * + * 根据配置的参数创建飞书表格,包括表头、样式、单元格格式和下拉选项等。 + * + * @return 创建成功返回工作表ID + */ + public String build() { + // 获取所有字段映射 + Map allFieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz); + + // 根据includeFields过滤字段映射 + Map fieldsMap = filterFieldsMap(allFieldsMap); + + // 生成表头 + List headers = PropertyUtil.getHeaders(fieldsMap); + + // 获取表格配置 + 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, tableConf), 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、设置表格下拉 + FsTableUtil.setTableOptions(spreadsheetToken, headers, fieldsMap, sheetId, tableConf.enableDesc()); + + return sheetId; + } + + /** + * 根据包含字段列表过滤字段映射 + * + * @param allFieldsMap 所有字段映射 + * @return 过滤后的字段映射 + */ + private Map filterFieldsMap(Map allFieldsMap) { + // 如果没有指定包含字段,返回所有字段 + if (includeFields == null || includeFields.isEmpty()) { + return allFieldsMap; + } + + // 根据字段名过滤,保留指定的字段 + return allFieldsMap.entrySet().stream() + .filter(entry -> includeFields.contains(entry.getValue().getField())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/src/main/java/cn/isliu/core/client/FeishuClient.java b/src/main/java/cn/isliu/core/client/FeishuClient.java index 2a36c8f..9e595d7 100644 --- a/src/main/java/cn/isliu/core/client/FeishuClient.java +++ b/src/main/java/cn/isliu/core/client/FeishuClient.java @@ -116,6 +116,16 @@ public class FeishuClient { return serviceManager.getService(CustomProtectedDimensionService.class, () -> new CustomProtectedDimensionService(this)); } + /** + * 获取扩展文件服务 + * + * @return 扩展文件服务 + */ + public CustomFileService customFiles() { + return serviceManager.getService(CustomFileService.class, () -> new CustomFileService(this)); + } + + /** * 获取官方客户端 * diff --git a/src/main/java/cn/isliu/core/pojo/RootFolderMetaResponse.java b/src/main/java/cn/isliu/core/pojo/RootFolderMetaResponse.java new file mode 100644 index 0000000..6e9ff70 --- /dev/null +++ b/src/main/java/cn/isliu/core/pojo/RootFolderMetaResponse.java @@ -0,0 +1,280 @@ +package cn.isliu.core.pojo; + +import com.google.gson.annotations.SerializedName; + +/** + * 飞书API获取根目录元数据的响应模型类 + * + * 对应飞书API返回的JSON格式: + * { + * "code": 0, + * "msg": "success", + * "data": { + * "token": "fldbc0k5Zws8AQBpfzlFMKCpN4z", + * "id": "fldbc0k5Zws8AQBpfzlFMKCpN4z", + * "user_id": "ou_xxxxx", + * "name": "我的空间" + * } + * } + * + * @author FsHelper + * @since 1.0 + */ +public class RootFolderMetaResponse { + + /** + * 响应状态码 + * 0表示成功,非0表示失败 + */ + @SerializedName("code") + private int code; + + /** + * 响应消息 + * 通常成功时为"success",失败时包含错误描述 + */ + @SerializedName("msg") + private String msg; + + /** + * 根目录元数据 + */ + @SerializedName("data") + private RootFolderMeta data; + + /** + * 默认构造函数 + */ + public RootFolderMetaResponse() { + } + + /** + * 完整构造函数 + * + * @param code 响应状态码 + * @param msg 响应消息 + * @param data 根目录元数据 + */ + public RootFolderMetaResponse(int code, String msg, RootFolderMeta data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + /** + * 获取响应状态码 + * + * @return 响应状态码,0表示成功 + */ + public int getCode() { + return code; + } + + /** + * 设置响应状态码 + * + * @param code 响应状态码 + */ + public void setCode(int code) { + this.code = code; + } + + /** + * 获取响应消息 + * + * @return 响应消息 + */ + public String getMsg() { + return msg; + } + + /** + * 设置响应消息 + * + * @param msg 响应消息 + */ + public void setMsg(String msg) { + this.msg = msg; + } + + /** + * 获取根目录元数据 + * + * @return 根目录元数据 + */ + public RootFolderMeta getData() { + return data; + } + + /** + * 设置根目录元数据 + * + * @param data 根目录元数据 + */ + public void setData(RootFolderMeta data) { + this.data = data; + } + + /** + * 检查响应是否成功 + * + * @return true表示API调用成功,false表示失败 + */ + public boolean isSuccess() { + return code == 0; + } + + /** + * 检查是否包含有效的根目录数据 + * + * @return true表示包含有效的根目录数据 + */ + public boolean hasValidData() { + return isSuccess() && + data != null && + data.getToken() != null && + !data.getToken().trim().isEmpty(); + } + + /** + * 根目录元数据内部类 + */ + public static class RootFolderMeta { + + /** + * 文件夹token + */ + @SerializedName("token") + private String token; + + /** + * 文件夹ID + */ + @SerializedName("id") + private String id; + + /** + * 用户ID + */ + @SerializedName("user_id") + private String userId; + + /** + * 文件夹名称 + */ + @SerializedName("name") + private String name; + + /** + * 默认构造函数 + */ + public RootFolderMeta() { + } + + /** + * 完整构造函数 + * + * @param token 文件夹token + * @param id 文件夹ID + * @param userId 用户ID + * @param name 文件夹名称 + */ + public RootFolderMeta(String token, String id, String userId, String name) { + this.token = token; + this.id = id; + this.userId = userId; + this.name = name; + } + + /** + * 获取文件夹token + * + * @return 文件夹token + */ + public String getToken() { + return token; + } + + /** + * 设置文件夹token + * + * @param token 文件夹token + */ + public void setToken(String token) { + this.token = token; + } + + /** + * 获取文件夹ID + * + * @return 文件夹ID + */ + public String getId() { + return id; + } + + /** + * 设置文件夹ID + * + * @param id 文件夹ID + */ + public void setId(String id) { + this.id = id; + } + + /** + * 获取用户ID + * + * @return 用户ID + */ + public String getUserId() { + return userId; + } + + /** + * 设置用户ID + * + * @param userId 用户ID + */ + public void setUserId(String userId) { + this.userId = userId; + } + + /** + * 获取文件夹名称 + * + * @return 文件夹名称 + */ + public String getName() { + return name; + } + + /** + * 设置文件夹名称 + * + * @param name 文件夹名称 + */ + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "RootFolderMeta{" + + "token='" + token + '\'' + + ", id='" + id + '\'' + + ", userId='" + userId + '\'' + + ", name='" + name + '\'' + + '}'; + } + } + + @Override + public String toString() { + return "RootFolderMetaResponse{" + + "code=" + code + + ", msg='" + msg + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/cn/isliu/core/service/CustomFileService.java b/src/main/java/cn/isliu/core/service/CustomFileService.java new file mode 100644 index 0000000..3dfcee5 --- /dev/null +++ b/src/main/java/cn/isliu/core/service/CustomFileService.java @@ -0,0 +1,44 @@ +package cn.isliu.core.service; + +import cn.isliu.core.client.FeishuClient; +import cn.isliu.core.pojo.RootFolderMetaResponse; +import okhttp3.Request; + +import java.io.IOException; + +/** + * 飞书文件服务 + * + * 处理飞书云盘相关的API调用,包括获取根目录元数据等功能 + * + * @author FsHelper + * @since 1.0 + */ +public class CustomFileService extends AbstractFeishuApiService { + + /** + * 构造函数 + * + * @param feishuClient 飞书客户端 + */ + public CustomFileService(FeishuClient feishuClient) { + super(feishuClient); + } + + /** + * 获取根目录元数据 + * + * 调用飞书开放平台API获取当前租户的根目录token和相关信息 + * API接口: GET https://open.feishu.cn/open-apis/drive/explorer/v2/root_folder/meta + * + * @return 根目录元数据响应 + * @throws IOException 网络请求异常 + */ + public RootFolderMetaResponse getRootFolderMeta() throws IOException { + String url = BASE_URL + "/drive/explorer/v2/root_folder/meta"; + + Request request = createAuthenticatedRequest(url, "GET", null).build(); + + return executeRequest(request, RootFolderMetaResponse.class); + } +} diff --git a/src/main/java/cn/isliu/core/utils/FsApiUtil.java b/src/main/java/cn/isliu/core/utils/FsApiUtil.java index 59d66d6..be0a868 100644 --- a/src/main/java/cn/isliu/core/utils/FsApiUtil.java +++ b/src/main/java/cn/isliu/core/utils/FsApiUtil.java @@ -9,18 +9,13 @@ import cn.isliu.core.client.FeishuClient; import cn.isliu.core.exception.FsHelperException; import cn.isliu.core.logging.FsLogger; import cn.isliu.core.pojo.ApiResponse; +import cn.isliu.core.pojo.RootFolderMetaResponse; import cn.isliu.core.service.*; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import com.lark.oapi.service.drive.v1.model.BatchGetTmpDownloadUrlMediaReq; -import com.lark.oapi.service.drive.v1.model.BatchGetTmpDownloadUrlMediaResp; -import com.lark.oapi.service.drive.v1.model.DownloadMediaReq; -import com.lark.oapi.service.drive.v1.model.DownloadMediaResp; -import com.lark.oapi.service.sheets.v3.model.GetSpreadsheetReq; -import com.lark.oapi.service.sheets.v3.model.GetSpreadsheetResp; -import com.lark.oapi.service.sheets.v3.model.QuerySpreadsheetSheetReq; -import com.lark.oapi.service.sheets.v3.model.QuerySpreadsheetSheetResp; +import com.lark.oapi.service.drive.v1.model.*; +import com.lark.oapi.service.sheets.v3.model.*; import java.io.IOException; import java.util.List; @@ -160,6 +155,83 @@ public class FsApiUtil { } } + /** + * 获取根目录Token + * + * 调用飞书开放平台API获取当前租户的根目录token,用于后续的文件夹和文件操作 + * API接口: GET https://open.feishu.cn/open-apis/drive/v1/files/root_folder/meta + * + * @param client 飞书客户端 + * @return 根目录token,获取失败时抛出异常 + */ + public static String getRootFolderToken(FeishuClient client) { + try { + // 使用自定义文件服务获取根目录元数据 + RootFolderMetaResponse response = client.customFiles().getRootFolderMeta(); + + if (response.isSuccess() && response.hasValidData()) { + String rootFolderToken = response.getData().getToken(); + FsLogger.info("【飞书表格】 获取根目录Token成功!Token: {}", rootFolderToken); + return rootFolderToken; + } else { + FsLogger.warn("【飞书表格】 获取根目录Token失败!错误码:{},错误信息:{}", + response.getCode(), response.getMsg()); + throw new FsHelperException("【飞书表格】 获取根目录Token失败!错误信息:" + response.getMsg()); + } + } catch (Exception e) { + FsLogger.warn("【飞书表格】 获取根目录Token异常!错误信息:{}", e.getMessage(), e); + throw new FsHelperException("【飞书表格】 获取根目录Token异常!"); + } + } + + public static CreateFolderFileRespBody createFolder(String folderName, String folderToken, FeishuClient client) { + try { + // 创建请求对象 + CreateFolderFileReq req = CreateFolderFileReq.newBuilder() + .createFolderFileReqBody(CreateFolderFileReqBody.newBuilder() + .name(folderName) + .folderToken(folderToken) + .build()) + .build(); + + // 发起请求 + CreateFolderFileResp resp = client.drive().v1().file().createFolder(req); + if (resp.success()) { + FsLogger.info("【飞书表格】 创建文件夹成功! {}", gson.toJson(resp)); + return resp.getData(); + } else { + FsLogger.warn("【飞书表格】 创建文件夹失败!参数:{},错误信息:{}", String.format("folderName: %s, folderToken: %s", folderName, folderToken), resp.getMsg()); + throw new FsHelperException("【飞书表格】 创建文件夹失败!"); + } + } catch (Exception e) { + FsLogger.warn("【飞书表格】 创建文件夹异常!参数:{},错误信息:{}", String.format("folderName: %s, folderToken: %s", folderName, folderToken), e.getMessage(), e); + throw new FsHelperException("【飞书表格】 创建文件夹异常!"); + } + } + + public static CreateSpreadsheetRespBody createTable(String tableName, String folderToken, FeishuClient client) { + try { + CreateSpreadsheetReq req = CreateSpreadsheetReq.newBuilder() + .spreadsheet(Spreadsheet.newBuilder() + .title(tableName) + .folderToken(folderToken) + .build()) + .build(); + + CreateSpreadsheetResp resp = client.sheets().v3().spreadsheet().create(req); + if (resp.success()) { + FsLogger.info("【飞书表格】 创建表格成功! {}", gson.toJson(resp)); + return resp.getData(); + } else { + FsLogger.warn("【飞书表格】 创建表格失败!错误信息:{}", gson.toJson(resp)); + throw new FsHelperException("【飞书表格】 创建表格异常!"); + } + } catch (Exception e) { + FsLogger.warn("【飞书表格】 创建表格异常!参数:{},错误信息:{}", String.format("tableName:%s, folderToken:%s", tableName, folderToken), e.getMessage(), e); + throw new FsHelperException("【飞书表格】 创建表格异常!"); + } + } + public static String createSheet(String title, FeishuClient client, String spreadsheetToken) { String sheetId = null; try {