feat(core): 新增飞书文件服务和表格构建器功能

- 新增 CustomFileService 类,用于获取根目录元数据等文件操作
- 新增 RootFolderMetaResponse 类,用于解析根目录元数据响应
- 新增 SheetBuilder 类,提供链式调用方式创建飞书表格
- 在 FeishuClient 中添加 customFiles 方法,返回 CustomFileService 实例
- 在 FsApiUtil 中添加创建文件夹和表格的方法
- 在 FsHelper 中添加创建表格构建器方法
This commit is contained in:
liushuang 2025-08-28 18:47:54 +08:00
parent a34df121bc
commit 3e25a91aed
6 changed files with 553 additions and 8 deletions

@ -5,6 +5,7 @@ import cn.isliu.core.FileData;
import cn.isliu.core.FsTableData; import cn.isliu.core.FsTableData;
import cn.isliu.core.Sheet; import cn.isliu.core.Sheet;
import cn.isliu.core.annotation.TableConf; import cn.isliu.core.annotation.TableConf;
import cn.isliu.core.builder.SheetBuilder;
import cn.isliu.core.client.FeishuClient; import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.client.FsClient; import cn.isliu.core.client.FsClient;
import cn.isliu.core.enums.ErrorCode; import cn.isliu.core.enums.ErrorCode;
@ -74,6 +75,22 @@ public class FsHelper {
return sheetId; return sheetId;
} }
/**
* 创建飞书表格构建器
*
* 返回一个表格构建器实例支持链式调用和高级配置选项
* 如字段过滤等功能
*
* @param sheetName 工作表名称
* @param spreadsheetToken 电子表格Token
* @param clazz 实体类Class对象用于解析表头和字段属性
* @param <T> 实体类泛型
* @return SheetBuilder实例支持链式调用
*/
public static <T> SheetBuilder<T> createBuilder(String sheetName, String spreadsheetToken, Class<T> clazz) {
return new SheetBuilder<>(sheetName, spreadsheetToken, clazz);
}
/** /**
* 从飞书表格中读取数据 * 从飞书表格中读取数据

@ -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<T> {
private final String sheetName;
private final String spreadsheetToken;
private final Class<T> clazz;
private List<String> includeFields;
/**
* 构造函数
*
* @param sheetName 工作表名称
* @param spreadsheetToken 电子表格Token
* @param clazz 实体类Class对象
*/
public SheetBuilder(String sheetName, String spreadsheetToken, Class<T> clazz) {
this.sheetName = sheetName;
this.spreadsheetToken = spreadsheetToken;
this.clazz = clazz;
}
/**
* 设置包含的字段列表
*
* 指定要包含在表格中的字段名称列表如果不设置则包含所有带有@TableProperty注解的字段
*
* @param fields 要包含的字段名称列表
* @return SheetBuilder实例支持链式调用
*/
public SheetBuilder<T> includeColumnField(List<String> fields) {
this.includeFields = new ArrayList<>(fields);
return this;
}
/**
* 构建表格并返回工作表ID
*
* 根据配置的参数创建飞书表格包括表头样式单元格格式和下拉选项等
*
* @return 创建成功返回工作表ID
*/
public String build() {
// 获取所有字段映射
Map<String, FieldProperty> allFieldsMap = PropertyUtil.getTablePropertyFieldsMap(clazz);
// 根据includeFields过滤字段映射
Map<String, FieldProperty> fieldsMap = filterFieldsMap(allFieldsMap);
// 生成表头
List<String> 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<CustomCellService.CellRequest> 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<String, FieldProperty> filterFieldsMap(Map<String, FieldProperty> 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));
}
}

@ -116,6 +116,16 @@ public class FeishuClient {
return serviceManager.getService(CustomProtectedDimensionService.class, () -> new CustomProtectedDimensionService(this)); return serviceManager.getService(CustomProtectedDimensionService.class, () -> new CustomProtectedDimensionService(this));
} }
/**
* 获取扩展文件服务
*
* @return 扩展文件服务
*/
public CustomFileService customFiles() {
return serviceManager.getService(CustomFileService.class, () -> new CustomFileService(this));
}
/** /**
* 获取官方客户端 * 获取官方客户端
* *

@ -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 +
'}';
}
}

@ -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);
}
}

@ -9,18 +9,13 @@ import cn.isliu.core.client.FeishuClient;
import cn.isliu.core.exception.FsHelperException; import cn.isliu.core.exception.FsHelperException;
import cn.isliu.core.logging.FsLogger; import cn.isliu.core.logging.FsLogger;
import cn.isliu.core.pojo.ApiResponse; import cn.isliu.core.pojo.ApiResponse;
import cn.isliu.core.pojo.RootFolderMetaResponse;
import cn.isliu.core.service.*; import cn.isliu.core.service.*;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.lark.oapi.service.drive.v1.model.BatchGetTmpDownloadUrlMediaReq; import com.lark.oapi.service.drive.v1.model.*;
import com.lark.oapi.service.drive.v1.model.BatchGetTmpDownloadUrlMediaResp; import com.lark.oapi.service.sheets.v3.model.*;
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 java.io.IOException; import java.io.IOException;
import java.util.List; 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) { public static String createSheet(String title, FeishuClient client, String spreadsheetToken) {
String sheetId = null; String sheetId = null;
try { try {