目前有个需求是把泛微OA的需求台账同步到飞书多维表格里面,虽然飞书官方封装了对应的SDK,但是泛微OA只能通过本地jar包的形式导入,多有不便,并且SDK的文档也不全过于分散(可能是我没找到具体的点)。只能自己造一个轮子了,把常用的接口都封装了一下:
FeiShuHttpService.java
package weaver.interfaces.push.feishu;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* @Author zF.
* @ClassName FeiShuHttpService.java
* @ProjectName ecology
* @Description 飞书多维表格HTTP请求工具类 - 基于HTTP API实现多维表格的增删改查操作
*/
public class FeiShuHttpService {
// 飞书API基础URL
private static final String FEISHU_BASE_URL = "https://open.feishu.cn/open-apis";
// 缓存访问令牌
private static volatile String cachedAccessToken;
private static volatile long tokenExpireTime = 0;
/**
* 获取访问令牌
*
* @return 访问令牌
*/
public static String getAccessToken() {
// 检查缓存是否有效
if (StrUtil.isNotBlank(cachedAccessToken) && System.currentTimeMillis() < tokenExpireTime) {
return cachedAccessToken;
}
try {
Map<String, String> config = FeiShuConfig.getAppConfig();
String appId = config.get("app_id");
String appSecret = config.get("app_secret");
if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
throw new RuntimeException("飞书应用配置不完整");
}
String url = FEISHU_BASE_URL + "/auth/v3/tenant_access_token/internal";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("app_id", appId);
requestBody.put("app_secret", appSecret);
HttpResponse response = HttpRequest.post(url)
.header("Content-Type", "application/json; charset=utf-8")
.body(JSONObject.toJSONString(requestBody))
.timeout(30000)
.execute();
if (response.getStatus() == 200) {
JSONObject result = JSONObject.parseObject(response.body());
if (result.getIntValue("code") == 0) {
cachedAccessToken = result.getString("tenant_access_token");
// 设置过期时间(提前5分钟过期)
int expire = result.getIntValue("expire");
tokenExpireTime = System.currentTimeMillis() + (expire - 300) * 1000L;
System.out.println("飞书访问令牌获取成功");
return cachedAccessToken;
} else {
throw new RuntimeException("获取访问令牌失败: " + result.getString("msg"));
}
} else {
throw new RuntimeException("获取访问令牌HTTP请求失败: " + response.getStatus());
}
} catch (Exception e) {
System.out.println("获取飞书访问令牌异常: " + e.getMessage());
e.printStackTrace();
throw new RuntimeException("获取访问令牌异常: " + e.getMessage(), e);
}
}
/**
* 发送HTTP请求的通用方法
*
* @param method 请求方法
* @param url 请求URL
* @param requestBody 请求体
* @return 响应结果
*/
private static JSONObject sendHttpRequest(String method, String url, Object requestBody) {
try {
HttpRequest request;
if ("GET".equalsIgnoreCase(method)) {
request = HttpRequest.get(url);
} else if ("POST".equalsIgnoreCase(method)) {
request = HttpRequest.post(url);
} else if ("PUT".equalsIgnoreCase(method)) {
request = HttpRequest.put(url);
} else if ("DELETE".equalsIgnoreCase(method)) {
request = HttpRequest.delete(url);
} else if ("PATCH".equalsIgnoreCase(method)) {
request = HttpRequest.patch(url);
} else {
throw new IllegalArgumentException("不支持的HTTP方法: " + method);
}
// 添加请求头
request.header("Authorization", "Bearer " + getAccessToken())
.header("Content-Type", "application/json; charset=utf-8")
.timeout(30000);
// 添加请求体
if (requestBody != null) {
request.body(JSONObject.toJSONString(requestBody));
}
HttpResponse response = request.execute();
JSONObject result = new JSONObject();
result.put("httpStatus", response.getStatus());
result.put("body", response.body());
if (response.getStatus() == 200) {
JSONObject responseJson = JSONObject.parseObject(response.body());
result.put("code", responseJson.getIntValue("code"));
result.put("msg", responseJson.getString("msg"));
result.put("data", responseJson.get("data"));
result.put("success", responseJson.getIntValue("code") == 0);
} else {
result.put("success", false);
result.put("msg", "HTTP请求失败: " + response.getStatus());
}
return result;
} catch (Exception e) {
System.out.println("HTTP请求异常: " + e.getMessage());
e.printStackTrace();
JSONObject result = new JSONObject();
result.put("success", false);
result.put("msg", "请求异常: " + e.getMessage());
return result;
}
}
// ==================== 多维表格应用管理 ====================
/**
* 创建多维表格应用
*
* @param name 应用名称
* @return 创建结果
*/
public static JSONObject createApp(String name) {
try {
if (StrUtil.isBlank(name)) {
return createErrorResult(-1, "应用名称不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("name", name);
System.out.println("创建飞书多维表格应用: " + name);
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("创建飞书多维表格应用异常: " + e.getMessage());
return createErrorResult(-1, "创建应用异常: " + e.getMessage());
}
}
/**
* 复制多维表格应用
*
* @param appToken 源应用Token
* @param name 新应用名称
* @return 复制结果
*/
public static JSONObject copyApp(String appToken, String name) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(name)) {
return createErrorResult(-1, "应用Token和名称不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/copy";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("name", name);
System.out.println("复制飞书多维表格应用: " + appToken + " -> " + name);
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("复制飞书多维表格应用异常: " + e.getMessage());
return createErrorResult(-1, "复制应用异常: " + e.getMessage());
}
}
/**
* 获取多维表格应用信息
*
* @param appToken 应用Token
* @return 应用信息
*/
public static JSONObject getApp(String appToken) {
try {
if (StrUtil.isBlank(appToken)) {
return createErrorResult(-1, "应用Token不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken;
System.out.println("获取飞书多维表格应用信息: " + appToken);
return sendHttpRequest("GET", url, null);
} catch (Exception e) {
System.out.println("获取飞书多维表格应用信息异常: " + e.getMessage());
return createErrorResult(-1, "获取应用信息异常: " + e.getMessage());
}
}
/**
* 更新多维表格应用
*
* @param appToken 应用Token
* @param name 新名称
* @return 更新结果
*/
public static JSONObject updateApp(String appToken, String name) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(name)) {
return createErrorResult(-1, "应用Token和名称不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken;
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("name", name);
System.out.println("更新飞书多维表格应用: " + appToken + " -> " + name);
return sendHttpRequest("PUT", url, requestBody);
} catch (Exception e) {
System.out.println("更新飞书多维表格应用异常: " + e.getMessage());
return createErrorResult(-1, "更新应用异常: " + e.getMessage());
}
}
// ==================== 多维表格管理 ====================
/**
* 创建多维表格
*
* @param appToken 应用Token
* @param tableName 表格名称
* @return 创建结果
*/
public static JSONObject createTable(String appToken, String tableName) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableName)) {
return createErrorResult(-1, "应用Token和表格名称不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("table", new HashMap<String, Object>() {{
put("name", tableName);
}});
System.out.println("创建飞书多维表格: " + appToken + " -> " + tableName);
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("创建飞书多维表格异常: " + e.getMessage());
return createErrorResult(-1, "创建表格异常: " + e.getMessage());
}
}
/**
* 批量创建多维表格
*
* @param appToken 应用Token
* @param tables 表格列表
* @return 创建结果
*/
public static JSONObject batchCreateTables(String appToken, Object[] tables) {
try {
if (StrUtil.isBlank(appToken) || tables == null || tables.length == 0) {
return createErrorResult(-1, "应用Token和表格列表不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/batch_create";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("tables", tables);
System.out.println("批量创建飞书多维表格: " + appToken + " -> " + tables.length + "个表格");
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("批量创建飞书多维表格异常: " + e.getMessage());
return createErrorResult(-1, "批量创建表格异常: " + e.getMessage());
}
}
/**
* 更新多维表格
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param tableName 新表格名称
* @return 更新结果
*/
public static JSONObject updateTable(String appToken, String tableId, String tableName) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || StrUtil.isBlank(tableName)) {
return createErrorResult(-1, "应用Token、表格ID和名称不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId;
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("table", new HashMap<String, Object>() {{
put("name", tableName);
}});
System.out.println("更新飞书多维表格: " + appToken + " -> " + tableId + " -> " + tableName);
return sendHttpRequest("PATCH", url, requestBody);
} catch (Exception e) {
System.out.println("更新飞书多维表格异常: " + e.getMessage());
return createErrorResult(-1, "更新表格异常: " + e.getMessage());
}
}
/**
* 获取多维表格列表
*
* @param appToken 应用Token
* @param pageSize 每页大小
* @param pageToken 分页Token
* @return 表格列表
*/
public static JSONObject listTables(String appToken, Integer pageSize, String pageToken) {
try {
if (StrUtil.isBlank(appToken)) {
return createErrorResult(-1, "应用Token不能为空");
}
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables");
boolean hasParam = false;
if (pageSize != null && pageSize > 0) {
url.append(hasParam ? "&" : "?").append("page_size=").append(pageSize);
hasParam = true;
}
if (StrUtil.isNotBlank(pageToken)) {
url.append(hasParam ? "&" : "?").append("page_token=").append(pageToken);
}
System.out.println("获取飞书多维表格列表: " + appToken);
return sendHttpRequest("GET", url.toString(), null);
} catch (Exception e) {
System.out.println("获取飞书多维表格列表异常: " + e.getMessage());
return createErrorResult(-1, "获取表格列表异常: " + e.getMessage());
}
}
/**
* 删除多维表格
*
* @param appToken 应用Token
* @param tableId 表格ID
* @return 删除结果
*/
public static JSONObject deleteTable(String appToken, String tableId) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId)) {
return createErrorResult(-1, "应用Token和表格ID不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId;
System.out.println("删除飞书多维表格: " + appToken + " -> " + tableId);
return sendHttpRequest("DELETE", url, null);
} catch (Exception e) {
System.out.println("删除飞书多维表格异常: " + e.getMessage());
return createErrorResult(-1, "删除表格异常: " + e.getMessage());
}
}
/**
* 批量删除多维表格
*
* @param appToken 应用Token
* @param tableIds 表格ID列表
* @return 删除结果
*/
public static JSONObject batchDeleteTables(String appToken, String[] tableIds) {
try {
if (StrUtil.isBlank(appToken) || tableIds == null || tableIds.length == 0) {
return createErrorResult(-1, "应用Token和表格ID列表不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/batch_delete";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("table_ids", tableIds);
System.out.println("批量删除飞书多维表格: " + appToken + " -> " + tableIds.length + "个表格");
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("批量删除飞书多维表格异常: " + e.getMessage());
return createErrorResult(-1, "批量删除表格异常: " + e.getMessage());
}
}
// ==================== 记录管理 ====================
/**
* 创建记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param fields 字段数据
* @return 创建结果
*/
public static JSONObject createRecord(String appToken, String tableId, Map<String, Object> fields) {
return createRecord(appToken, tableId, fields, null);
}
/**
* 创建记录(带user_id_type参数)
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param fields 字段数据
* @param userIdType 用户ID类型
* @return 创建结果
*/
public static JSONObject createRecord(String appToken, String tableId, Map<String, Object> fields, String userIdType) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || fields == null) {
return createErrorResult(-1, "应用Token、表格ID和字段数据不能为空");
}
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records");
if (StrUtil.isNotBlank(userIdType)) {
url.append("?user_id_type=").append(userIdType);
}
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("fields", fields);
System.out.println("创建飞书多维表格记录: " + appToken + " -> " + tableId + (StrUtil.isNotBlank(userIdType) ? " -> user_id_type=" + userIdType : ""));
return sendHttpRequest("POST", url.toString(), requestBody);
} catch (Exception e) {
System.out.println("创建飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "创建记录异常: " + e.getMessage());
}
}
/**
* 更新记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param recordId 记录ID
* @param fields 字段数据
* @return 更新结果
*/
public static JSONObject updateRecord(String appToken, String tableId, String recordId, Map<String, Object> fields) {
return updateRecord(appToken, tableId, recordId, fields, null);
}
/**
* 更新记录(带user_id_type参数)
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param recordId 记录ID
* @param fields 字段数据
* @param userIdType 用户ID类型
* @return 更新结果
*/
public static JSONObject updateRecord(String appToken, String tableId, String recordId, Map<String, Object> fields, String userIdType) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || StrUtil.isBlank(recordId) || fields == null) {
return createErrorResult(-1, "应用Token、表格ID、记录ID和字段数据不能为空");
}
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/" + recordId);
if (StrUtil.isNotBlank(userIdType)) {
url.append("?user_id_type=").append(userIdType);
}
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("fields", fields);
System.out.println("更新飞书多维表格记录: " + appToken + " -> " + tableId + " -> " + recordId + (StrUtil.isNotBlank(userIdType) ? " -> user_id_type=" + userIdType : ""));
return sendHttpRequest("PUT", url.toString(), requestBody);
} catch (Exception e) {
System.out.println("更新飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "更新记录异常: " + e.getMessage());
}
}
/**
* 搜索记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param searchKey 搜索关键字(暂未使用,查询所有记录时传递null即可)
* @param pageSize 每页大小
* @param pageToken 分页Token
* @param userIdType 用户ID类型
* @return 搜索结果
*/
public static JSONObject searchRecords(String appToken, String tableId, String searchKey, Integer pageSize, String pageToken, String userIdType) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId)) {
return createErrorResult(-1, "应用Token和表格ID不能为空");
}
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/search");
boolean hasParam = false;
if (pageSize != null && pageSize > 0) {
url.append(hasParam ? "&" : "?").append("page_size=").append(pageSize);
hasParam = true;
}
if (StrUtil.isNotBlank(pageToken)) {
url.append(hasParam ? "&" : "?").append("page_token=").append(pageToken);
hasParam = true;
}
if (StrUtil.isNotBlank(userIdType)) {
url.append(hasParam ? "&" : "?").append("user_id_type=").append(userIdType);
}
// 构建请求体 - 根据飞书官方文档,查询所有记录时传递空对象
Map<String, Object> requestBody = new HashMap<>();
// 注意:飞书官方文档中没有search_key参数,查询所有记录时传递空请求体即可
System.out.println("搜索飞书多维表格记录: " + appToken + " -> " + tableId + " -> " + searchKey + (StrUtil.isNotBlank(userIdType) ? " -> user_id_type=" + userIdType : ""));
return sendHttpRequest("POST", url.toString(), requestBody);
} catch (Exception e) {
System.out.println("搜索飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "搜索记录异常: " + e.getMessage());
}
}
/**
* 获取记录详情
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param recordId 记录ID
* @return 记录详情
*/
public static JSONObject getRecord(String appToken, String tableId, String recordId) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || StrUtil.isBlank(recordId)) {
return createErrorResult(-1, "应用Token、表格ID和记录ID不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/" + recordId;
System.out.println("获取飞书多维表格记录详情: " + appToken + " -> " + tableId + " -> " + recordId);
return sendHttpRequest("GET", url, null);
} catch (Exception e) {
System.out.println("获取飞书多维表格记录详情异常: " + e.getMessage());
return createErrorResult(-1, "获取记录详情异常: " + e.getMessage());
}
}
/**
* 删除记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param recordId 记录ID
* @return 删除结果
*/
public static JSONObject deleteRecord(String appToken, String tableId, String recordId) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || StrUtil.isBlank(recordId)) {
return createErrorResult(-1, "应用Token、表格ID和记录ID不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/" + recordId;
System.out.println("删除飞书多维表格记录: " + appToken + " -> " + tableId + " -> " + recordId);
return sendHttpRequest("DELETE", url, null);
} catch (Exception e) {
System.out.println("删除飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "删除记录异常: " + e.getMessage());
}
}
/**
* 批量创建记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param records 记录列表
* @return 创建结果
*/
public static JSONObject batchCreateRecords(String appToken, String tableId, Object[] records) {
return batchCreateRecords(appToken, tableId, records, null);
}
/**
* 批量创建记录(带user_id_type参数)
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param records 记录列表
* @param userIdType 用户ID类型
* @return 创建结果
*/
public static JSONObject batchCreateRecords(String appToken, String tableId, Object[] records, String userIdType) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || records == null || records.length == 0) {
return createErrorResult(-1, "应用Token、表格ID和记录列表不能为空");
}
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/batch_create");
if (StrUtil.isNotBlank(userIdType)) {
url.append("?user_id_type=").append(userIdType);
}
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("records", records);
System.out.println("批量创建飞书多维表格记录: " + appToken + " -> " + tableId + " -> " + records.length + "条记录" + (StrUtil.isNotBlank(userIdType) ? " -> user_id_type=" + userIdType : ""));
return sendHttpRequest("POST", url.toString(), requestBody);
} catch (Exception e) {
System.out.println("批量创建飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "批量创建记录异常: " + e.getMessage());
}
}
/**
* 批量更新记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param records 记录列表
* @return 更新结果
*/
public static JSONObject batchUpdateRecords(String appToken, String tableId, Object[] records) {
return batchUpdateRecords(appToken, tableId, records, null);
}
/**
* 批量更新记录(带user_id_type参数)
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param records 记录列表
* @param userIdType 用户ID类型
* @return 更新结果
*/
public static JSONObject batchUpdateRecords(String appToken, String tableId, Object[] records, String userIdType) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || records == null || records.length == 0) {
return createErrorResult(-1, "应用Token、表格ID和记录列表不能为空");
}
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/batch_update");
if (StrUtil.isNotBlank(userIdType)) {
url.append("?user_id_type=").append(userIdType);
}
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("records", records);
System.out.println("批量更新飞书多维表格记录: " + appToken + " -> " + tableId + " -> " + records.length + "条记录" + (StrUtil.isNotBlank(userIdType) ? " -> user_id_type=" + userIdType : ""));
return sendHttpRequest("POST", url.toString(), requestBody);
} catch (Exception e) {
System.out.println("批量更新飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "批量更新记录异常: " + e.getMessage());
}
}
/**
* 批量获取记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param recordIds 记录ID列表
* @return 记录列表
*/
public static JSONObject batchGetRecords(String appToken, String tableId, String[] recordIds) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || recordIds == null || recordIds.length == 0) {
return createErrorResult(-1, "应用Token、表格ID和记录ID列表不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/batch_get";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("records", recordIds);
System.out.println("批量获取飞书多维表格记录: " + appToken + " -> " + tableId + " -> " + recordIds.length + "条记录");
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("批量获取飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "批量获取记录异常: " + e.getMessage());
}
}
/**
* 批量删除记录
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param recordIds 记录ID列表
* @return 删除结果
*/
public static JSONObject batchDeleteRecords(String appToken, String tableId, String[] recordIds) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || recordIds == null || recordIds.length == 0) {
return createErrorResult(-1, "应用Token、表格ID和记录ID列表不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/records/batch_delete";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("records", recordIds);
System.out.println("批量删除飞书多维表格记录: " + appToken + " -> " + tableId + " -> " + recordIds.length + "条记录");
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("批量删除飞书多维表格记录异常: " + e.getMessage());
return createErrorResult(-1, "批量删除记录异常: " + e.getMessage());
}
}
// ==================== 文件管理 ====================
/**
* 上传文件到飞书
*
* @param fileUrl 文件下载URL
* @param fileName 文件名
* @param accessToken 访问令牌
* @param appToken 应用令牌
* @return 上传结果
*/
public static JSONObject uploadFile(String fileUrl, String fileName, String accessToken, String appToken) {
try {
if (StrUtil.isBlank(fileUrl) || StrUtil.isBlank(fileName) || StrUtil.isBlank(accessToken)) {
return createErrorResult(-1, "文件URL、文件名和访问令牌不能为空");
}
// 1. 先下载文件
byte[] fileBytes = downloadFileFromUrl(fileUrl);
if (fileBytes == null || fileBytes.length == 0) {
return createErrorResult(-1, "文件下载失败: " + fileName);
}
// 2. 上传文件到飞书
String url = FEISHU_BASE_URL + "/drive/v1/medias/upload_all";
// 构建multipart/form-data请求
HttpRequest request = HttpRequest.post(url);
request.header("Authorization", "Bearer " + accessToken)
.timeout(60000); // 文件上传超时时间设置为60秒
// 添加文件参数 - 按照飞书官方curl示例格式
// 必需参数:file_name, file
// 可选参数:parent_type, size, extra, parent_node
request.form("file_name", fileName);
request.form("file", fileBytes, fileName);
// 根据官方示例添加可选参数
request.form("parent_type", "bitable_file"); // 用于多维表格
request.form("size", String.valueOf(fileBytes.length)); // 文件大小
// extra参数(可选,用于多维表格)
String extra = "{\"drive_route_token\":\"\"}";
request.form("extra", extra);
// parent_node参数:指定上传到哪个多维表格(使用app_token)
String parentNode = appToken;
if (StrUtil.isNotBlank(parentNode)) {
request.form("parent_node", parentNode);
}
System.out.println("上传文件到飞书: " + fileName + ", 大小: " + fileBytes.length + " 字节");
System.out.println("请求URL: " + url);
System.out.println("请求头: Authorization=Bearer " + accessToken.substring(0, Math.min(20, accessToken.length())) + "...");
System.out.println("Content-Type: multipart/form-data (自动设置)");
System.out.println("请求参数: file_name=" + fileName + ", file=字节数组, parent_type=bitable_file, size=" + fileBytes.length + ", parent_node=" + parentNode);
HttpResponse response = request.execute();
System.out.println("飞书文件上传响应状态: " + response.getStatus());
System.out.println("飞书文件上传响应体: " + response.body());
JSONObject result = new JSONObject();
result.put("httpStatus", response.getStatus());
result.put("body", response.body());
if (response.getStatus() == 200) {
try {
JSONObject responseBody = JSONObject.parseObject(response.body());
System.out.println("飞书API响应解析: " + responseBody);
// 检查飞书API的标准响应格式
if (responseBody.containsKey("code") && responseBody.getIntValue("code") == 0) {
// 飞书API成功响应
result.put("code", 0);
result.put("data", responseBody.get("data"));
result.put("msg", "文件上传成功");
} else {
// 飞书API错误响应
result.put("code", responseBody.getIntValue("code"));
result.put("data", responseBody.get("data"));
result.put("msg", responseBody.getString("msg"));
}
} catch (Exception e) {
System.out.println("解析飞书API响应异常: " + e.getMessage());
result.put("code", -1);
result.put("msg", "解析响应异常: " + e.getMessage());
}
} else {
result.put("code", -1);
result.put("msg", "文件上传失败: HTTP " + response.getStatus());
}
return result;
} catch (Exception e) {
System.out.println("上传文件到飞书异常: " + e.getMessage());
return createErrorResult(-1, "上传文件异常: " + e.getMessage());
}
}
/**
* 从URL下载文件
*
* @param fileUrl 文件URL
* @return 文件字节数组
*/
private static byte[] downloadFileFromUrl(String fileUrl) {
try {
HttpRequest request = HttpRequest.get(fileUrl);
request.timeout(30000); // 30秒超时
HttpResponse response = request.execute();
if (response.getStatus() == 200) {
return response.bodyBytes();
} else {
System.out.println("文件下载失败: HTTP " + response.getStatus() + ", URL: " + fileUrl);
return null;
}
} catch (Exception e) {
System.out.println("文件下载异常: " + e.getMessage() + ", URL: " + fileUrl);
return null;
}
}
// ==================== 字段管理 ====================
/**
* 创建字段
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param fieldName 字段名称
* @param fieldType 字段类型
* @return 创建结果
*/
public static JSONObject createField(String appToken, String tableId, String fieldName, String fieldType) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || StrUtil.isBlank(fieldName) || StrUtil.isBlank(fieldType)) {
return createErrorResult(-1, "应用Token、表格ID、字段名称和类型不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/fields";
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("field_name", fieldName);
requestBody.put("type", fieldType);
System.out.println("创建飞书多维表格字段: " + appToken + " -> " + tableId + " -> " + fieldName);
return sendHttpRequest("POST", url, requestBody);
} catch (Exception e) {
System.out.println("创建飞书多维表格字段异常: " + e.getMessage());
return createErrorResult(-1, "创建字段异常: " + e.getMessage());
}
}
/**
* 更新字段
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param fieldId 字段ID
* @param fieldName 新字段名称
* @return 更新结果
*/
public static JSONObject updateField(String appToken, String tableId, String fieldId, String fieldName) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || StrUtil.isBlank(fieldId) || StrUtil.isBlank(fieldName)) {
return createErrorResult(-1, "应用Token、表格ID、字段ID和名称不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/fields/" + fieldId;
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("field_name", fieldName);
System.out.println("更新飞书多维表格字段: " + appToken + " -> " + tableId + " -> " + fieldId + " -> " + fieldName);
return sendHttpRequest("PUT", url, requestBody);
} catch (Exception e) {
System.out.println("更新飞书多维表格字段异常: " + e.getMessage());
return createErrorResult(-1, "更新字段异常: " + e.getMessage());
}
}
/**
* 获取字段列表
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param pageSize 每页大小
* @param pageToken 分页Token
* @return 字段列表
*/
public static JSONObject listFields(String appToken, String tableId, Integer pageSize, String pageToken) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId)) {
return createErrorResult(-1, "应用Token和表格ID不能为空");
}
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/fields");
boolean hasParam = false;
if (pageSize != null && pageSize > 0) {
url.append(hasParam ? "&" : "?").append("page_size=").append(pageSize);
hasParam = true;
}
if (StrUtil.isNotBlank(pageToken)) {
url.append(hasParam ? "&" : "?").append("page_token=").append(pageToken);
}
System.out.println("获取飞书多维表格字段列表: " + appToken + " -> " + tableId);
return sendHttpRequest("GET", url.toString(), null);
} catch (Exception e) {
System.out.println("获取飞书多维表格字段列表异常: " + e.getMessage());
return createErrorResult(-1, "获取字段列表异常: " + e.getMessage());
}
}
/**
* 删除字段
*
* @param appToken 应用Token
* @param tableId 表格ID
* @param fieldId 字段ID
* @return 删除结果
*/
public static JSONObject deleteField(String appToken, String tableId, String fieldId) {
try {
if (StrUtil.isBlank(appToken) || StrUtil.isBlank(tableId) || StrUtil.isBlank(fieldId)) {
return createErrorResult(-1, "应用Token、表格ID和字段ID不能为空");
}
String url = FEISHU_BASE_URL + "/bitable/v1/apps/" + appToken + "/tables/" + tableId + "/fields/" + fieldId;
System.out.println("删除飞书多维表格字段: " + appToken + " -> " + tableId + " -> " + fieldId);
return sendHttpRequest("DELETE", url, null);
} catch (Exception e) {
System.out.println("删除飞书多维表格字段异常: " + e.getMessage());
return createErrorResult(-1, "删除字段异常: " + e.getMessage());
}
}
// ==================== 工具方法 ====================
/**
* 处理飞书API响应
*
* @param response 响应JSON
* @return 处理结果
*/
public static Map<String, Object> processFeiShuResponse(JSONObject response) {
Map<String, Object> result = new HashMap<>();
if (response == null) {
result.put("TYPE", "E");
result.put("MESSAGE", "飞书API响应为空");
return result;
}
int code = response.getIntValue("code");
String msg = response.getString("msg");
// 飞书API:code=0表示成功
if (code == 0) {
result.put("TYPE", "S");
result.put("MESSAGE", "操作成功");
result.put("DATA", response.get("data"));
} else {
result.put("TYPE", "E");
result.put("MESSAGE", "操作失败: " + msg);
result.put("ERROR_CODE", code);
}
return result;
}
/**
* 创建错误结果对象
*
* @param code 错误码
* @param message 错误信息
* @return 错误结果JSONObject
*/
private static JSONObject createErrorResult(int code, String message) {
JSONObject result = new JSONObject();
result.put("success", false);
result.put("code", code);
result.put("msg", message);
return result;
}
/**
* 清除访问令牌缓存
*/
public static void clearTokenCache() {
cachedAccessToken = null;
tokenExpireTime = 0;
System.out.println("飞书访问令牌缓存已清除");
}
// ==================== 用户管理 ====================
/**
* 批量获取用户信息
*
* @param userIds 用户ID列表
* @param departmentIdType 部门ID类型,可选值:department_id, open_department_id
* @param userIdType 用户ID类型,可选值:user_id, open_id, union_id
* @return 用户信息列表
*/
public static JSONObject batchGetUsers(String[] userIds, String departmentIdType, String userIdType) {
try {
if (userIds == null || userIds.length == 0) {
return createErrorResult(-1, "用户ID列表不能为空");
}
// 获取访问令牌
String accessToken = getAccessToken();
if (StrUtil.isBlank(accessToken)) {
return createErrorResult(-1, "获取访问令牌失败");
}
// 构建URL
StringBuilder url = new StringBuilder(FEISHU_BASE_URL + "/contact/v3/users/batch");
boolean hasParam = false;
// 添加department_id_type参数
if (StrUtil.isNotBlank(departmentIdType)) {
url.append(hasParam ? "&" : "?").append("department_id_type=").append(departmentIdType);
hasParam = true;
}
// 添加user_id_type参数
if (StrUtil.isNotBlank(userIdType)) {
url.append(hasParam ? "&" : "?").append("user_id_type=").append(userIdType);
hasParam = true;
}
// 添加user_ids参数(多个用户ID)
for (String userId : userIds) {
if (StrUtil.isNotBlank(userId)) {
url.append(hasParam ? "&" : "?").append("user_ids=").append(userId);
hasParam = true;
}
}
System.out.println("批量获取飞书用户信息: " + userIds.length + "个用户, department_id_type=" + departmentIdType + ", user_id_type=" + userIdType);
System.out.println("请求URL: " + url.toString());
// 发送GET请求
HttpRequest request = HttpRequest.get(url.toString());
request.header("Authorization", "Bearer " + accessToken)
.timeout(30000);
HttpResponse response = request.execute();
System.out.println("飞书用户信息查询响应状态: " + response.getStatus());
System.out.println("飞书用户信息查询响应体: " + response.body());
JSONObject result = new JSONObject();
result.put("httpStatus", response.getStatus());
result.put("body", response.body());
if (response.getStatus() == 200) {
try {
JSONObject responseBody = JSONObject.parseObject(response.body());
System.out.println("飞书用户信息API响应解析: " + responseBody);
if (responseBody.containsKey("code") && responseBody.getIntValue("code") == 0) {
result.put("code", 0);
result.put("data", responseBody.get("data"));
result.put("msg", "获取用户信息成功");
} else {
result.put("code", responseBody.getIntValue("code"));
result.put("data", responseBody.get("data"));
result.put("msg", responseBody.getString("msg"));
}
} catch (Exception e) {
System.out.println("解析飞书用户信息API响应异常: " + e.getMessage());
result.put("code", -1);
result.put("msg", "解析响应异常: " + e.getMessage());
}
} else {
result.put("code", -1);
result.put("msg", "获取用户信息失败: HTTP " + response.getStatus());
}
return result;
} catch (Exception e) {
System.out.println("批量获取飞书用户信息异常: " + e.getMessage());
return createErrorResult(-1, "批量获取用户信息异常: " + e.getMessage());
}
}
/**
* 批量获取用户信息(使用默认参数)
*
* @param userIds 用户ID列表
* @return 用户信息列表
*/
public static JSONObject batchGetUsers(String[] userIds) {
return batchGetUsers(userIds, "department_id", "user_id");
}
/**
* 批量获取用户信息(单个用户ID)
*
* @param userId 用户ID
* @param departmentIdType 部门ID类型
* @param userIdType 用户ID类型
* @return 用户信息
*/
public static JSONObject batchGetUsers(String userId, String departmentIdType, String userIdType) {
return batchGetUsers(new String[]{userId}, departmentIdType, userIdType);
}
/**
* 批量获取用户信息(单个用户ID,使用默认参数)
*
* @param userId 用户ID
* @return 用户信息
*/
public static JSONObject batchGetUsers(String userId) {
return batchGetUsers(new String[]{userId}, "department_id", "user_id");
}
}