目前有个需求是把泛微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");
    }
}

最后修改:2025 年 10 月 21 日
给我一点小钱钱也很高兴啦!o(* ̄▽ ̄*)ブ