Files
ruoyi-java/doc/腾讯文档API关键修复_根据官方文档.md
2025-11-06 10:26:40 +08:00

13 KiB
Raw Blame History

腾讯文档 API 关键修复 - 根据官方文档

修复日期

2025-11-05

问题来源

根据用户提供的腾讯文档官方文档链接,发现之前的实现存在重大错误,与官方文档规范不符。

官方文档参考


🚨 关键问题修复

问题 1获取用户信息接口的鉴权方式错误

错误实现

// 使用 Authorization: Bearer 请求头
conn.setRequestProperty("Authorization", "Bearer " + accessToken);

正确实现(根据官方文档)

根据官方文档,应该使用查询参数传递 access_token

// 使用查询参数传递 access_token
String apiUrl = "https://docs.qq.com/oauth/v2/userinfo?access_token=" + accessToken;

官方文档说明

  • 接口名:/oauth/v2/userinfo
  • 请求方式:GET
  • 请求参数:access_tokenstring必选访问令牌

问题 2用户信息响应字段解析错误

错误实现

// 直接从根对象获取 openId
String openId = userInfo.getString("openId");

正确实现(根据官方文档)

根据官方文档,响应结构为:

{
  "ret": 0,
  "msg": "Succeed",
  "data": {
    "openID": "bcb50c8a4b724d86bbcf6fc64c5e2b22",
    "nick": "用户昵称",
    "avatar": "https://example.com/avatar.jpg",
    "source": "wx",
    "unionID": "xxxxxx"
  }
}

正确的解析方式

JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken);
JSONObject data = userInfo.getJSONObject("data");
String openId = data.getString("openID");  // 注意:是 openID大写 ID不是 openId

关键点

  1. 用户信息在 data 对象中,不是根对象
  2. 字段名是 openID(大写 ID不是 openId
  3. 需要检查 ret 是否为 0表示成功

修改的文件清单

1. TencentDocApiUtil.java

修改1getUserInfo 方法完全重写

/**
 * 获取用户信息包含Open-Id
 * 根据官方文档https://docs.qq.com/open/document/app/oauth2/user_info.html
 * 
 * @param accessToken 访问令牌
 * @return 用户信息
 *         响应格式:{ "ret": 0, "msg": "Succeed", "data": { "openID": "xxx", "nick": "xxx", "avatar": "xxx", "source": "wx", "unionID": "xxx" } }
 */
public static JSONObject getUserInfo(String accessToken) {
    try {
        // 官方文档要求使用查询参数传递 access_token而不是请求头
        String apiUrl = "https://docs.qq.com/oauth/v2/userinfo?access_token=" + accessToken;
        log.info("调用获取用户信息API: url={}", apiUrl);
        
        URL url = new URL(apiUrl);
        java.net.Proxy proxy = java.net.Proxy.NO_PROXY;
        HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy);
        
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Accept", "application/json");
        conn.setDoInput(true);
        conn.setConnectTimeout(10000);
        conn.setReadTimeout(30000);
        
        // 读取响应
        int responseCode = conn.getResponseCode();
        log.info("获取用户信息API响应状态码: {}", responseCode);
        
        java.io.InputStream inputStream = (responseCode >= 200 && responseCode < 300) 
            ? conn.getInputStream() 
            : conn.getErrorStream();
        
        StringBuilder response = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
        }
        
        String responseBody = response.toString();
        log.debug("获取用户信息API响应: {}", responseBody);
        
        if (responseCode >= 200 && responseCode < 300) {
            JSONObject result = JSONObject.parseObject(responseBody);
            // 检查业务返回码
            Integer ret = result.getInteger("ret");
            if (ret != null && ret == 0) {
                return result;
            } else {
                String msg = result.getString("msg");
                throw new RuntimeException("获取用户信息失败: " + msg);
            }
        } else {
            throw new RuntimeException("获取用户信息失败HTTP状态码: " + responseCode + ", 响应: " + responseBody);
        }
    } catch (Exception e) {
        log.error("获取用户信息失败", e);
        throw new RuntimeException("获取用户信息失败: " + e.getMessage(), e);
    }
}

修改2更新 callApiSimple 方法

public static JSONObject callApiSimple(String accessToken, String appId, String apiUrl, String method, String body) {
    // 获取用户信息以获得 openId
    // 官方响应格式:{ "ret": 0, "msg": "Succeed", "data": { "openID": "xxx", ... } }
    JSONObject userInfo = getUserInfo(accessToken);
    JSONObject data = userInfo.getJSONObject("data");
    if (data == null) {
        throw new RuntimeException("无法获取用户数据,请检查 Access Token 是否有效");
    }
    
    String openId = data.getString("openID");  // 注意:官方返回的字段名是 openID大写ID
    if (openId == null || openId.isEmpty()) {
        throw new RuntimeException("无法获取 Open-Id请检查 Access Token 是否有效");
    }
    
    return callApi(accessToken, appId, openId, apiUrl, method, body);
}

修改3删除不再使用的 callApiLegacy 方法

  • 删除了 callApiLegacy 方法约50行代码
  • 该方法使用 Authorization: Bearer 方式,与官方文档不符

2. TencentDocServiceImpl.java

修改:更新所有 Service 方法中获取 Open-Id 的逻辑

修改前(错误):

JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken);
String openId = userInfo.getString("openId");
if (openId == null || openId.isEmpty()) {
    throw new RuntimeException("无法获取Open-Id请检查Access Token是否有效");
}

修改后(正确):

// 获取用户信息包含Open-Id
// 官方响应格式:{ "ret": 0, "msg": "Succeed", "data": { "openID": "xxx", ... } }
JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken);
JSONObject data = userInfo.getJSONObject("data");
if (data == null) {
    throw new RuntimeException("无法获取用户数据请检查Access Token是否有效");
}
String openId = data.getString("openID");  // 注意:官方返回的字段名是 openID大写ID
if (openId == null || openId.isEmpty()) {
    throw new RuntimeException("无法获取Open-Id请检查Access Token是否有效");
}

影响的方法

  1. uploadLogisticsToSheet - 批量上传物流信息
  2. appendLogisticsToSheet - 追加物流信息
  3. readSheetData - 读取表格数据
  4. writeSheetData - 写入表格数据
  5. getFileInfo - 获取文件信息
  6. getSheetList - 获取工作表列表

官方文档对比

获取用户信息接口

官方文档规范

接口名:/oauth/v2/userinfo
请求方式GET
Acceptapplication/json

请求参数:
| 名称          | 类型     | 必选 | 备注   |
| ------------ | ------- | --- | ------ |
| access_token | string  | 是  | 访问令牌 |

响应体:
{
  "ret": 0,
  "msg": "Succeed",
  "data": {
    "openID": "bcb50c8a4b724d86bbcf6fc64c5e2b22",
    "nick": "用户昵称",
    "avatar": "https://example.com/avatar.jpg",
    "source": "wx",
    "unionID": "xxxxxx"
  }
}

之前的错误实现

✗ 使用 Authorization: Bearer 请求头(官方文档未要求)
✗ 直接从根对象获取 openId 字段(实际在 data 对象中)
✗ 字段名使用 openId官方是 openID大写 ID

现在的正确实现

✓ 使用查询参数 access_token符合官方文档
✓ 从 data 对象中获取用户信息(符合官方响应格式)
✓ 使用正确的字段名 openID大写 ID
✓ 检查 ret 返回码是否为 0符合官方业务逻辑

为什么之前的实现会失败

1. 鉴权方式错误

腾讯文档的 OAuth2 用户信息接口与标准的 OAuth2 规范略有不同:

  • 标准 OAuth2:使用 Authorization: Bearer {token} 请求头
  • 腾讯文档:使用查询参数 access_token={token}

这是腾讯文档平台的特殊设计,必须严格按照官方文档实现。

2. 响应结构理解错误

腾讯文档的响应采用统一的业务响应格式:

{
  "ret": 0,          // 业务返回码0表示成功
  "msg": "Succeed",  // 业务返回信息
  "data": {          // 实际数据在这里
    "openID": "xxx"
  }
}

之前的实现忽略了外层的 retmsg 字段,也没有从 data 对象中获取数据。

3. 字段命名不一致

官方文档明确使用 openID(大写 ID而之前使用了 openId(小写 id导致解析失败。


测试验证

测试用例 1获取用户信息

@Test
public void testGetUserInfo() {
    String accessToken = "your_access_token";
    
    JSONObject result = TencentDocApiUtil.getUserInfo(accessToken);
    
    // 验证响应结构
    assertNotNull(result);
    assertEquals(0, result.getInteger("ret").intValue());
    assertEquals("Succeed", result.getString("msg"));
    
    // 验证用户数据
    JSONObject data = result.getJSONObject("data");
    assertNotNull(data);
    
    String openID = data.getString("openID");
    assertNotNull(openID);
    assertFalse(openID.isEmpty());
    
    System.out.println("Open ID: " + openID);
    System.out.println("昵称: " + data.getString("nick"));
    System.out.println("头像: " + data.getString("avatar"));
}

测试用例 2完整的表格操作流程

@Test
public void testCompleteFlow() {
    String accessToken = "your_access_token";
    String fileId = "your_file_id";
    String sheetId = "your_sheet_id";
    
    // 1. 获取用户信息
    JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken);
    JSONObject data = userInfo.getJSONObject("data");
    String openID = data.getString("openID");
    
    System.out.println("获取到 Open ID: " + openID);
    
    // 2. 获取文件信息
    JSONObject fileInfo = tencentDocService.getFileInfo(accessToken, fileId);
    System.out.println("文件信息: " + fileInfo);
    
    // 3. 读取表格数据
    JSONObject readResult = tencentDocService.readSheetData(
        accessToken, fileId, sheetId, "A1:Z10"
    );
    System.out.println("读取结果: " + readResult);
}

影响范围

破坏性变更

  • ⚠️ getUserInfo 方法的返回值结构变化
  • ⚠️ 所有依赖 getUserInfo 的代码需要相应调整

兼容性

  • 对外暴露的 Service 接口签名未变化
  • 内部实现优化,不影响上层调用
  • 所有修改都基于官方文档,确保长期稳定

关键要点总结

1. 严格遵循官方文档

  • 不要假设或猜测 API 行为
  • 必须使用官方文档规定的鉴权方式
  • 必须使用官方文档规定的请求参数
  • 必须按照官方文档解析响应结构

2. 腾讯文档 API 的特殊性

  • OAuth2 用户信息接口使用查询参数,不是请求头
  • 响应采用统一的业务格式ret + msg + data
  • 字段命名严格区分大小写openID vs openId

3. 错误处理

  • 检查 HTTP 状态码200-299 为成功)
  • 检查业务返回码ret == 0 为成功)
  • 提供详细的错误信息便于排查

后续建议

1. 添加单元测试

为所有修改的方法添加单元测试,确保与官方文档规范一致。

2. 添加集成测试

使用真实的 Access Token 进行端到端测试,验证完整流程。

3. 监控和日志

  • 记录所有 API 调用的请求和响应
  • 统计 API 调用成功率
  • 及时发现和处理异常情况

4. 文档维护

  • 保持代码注释与官方文档同步
  • 记录所有 API 变更历史
  • 定期检查官方文档更新

编译验证

编译状态:无错误,无警告

文件TencentDocApiUtil.java
状态:✓ 编译通过

文件TencentDocServiceImpl.java
状态:✓ 编译通过

修改统计

  • 修改文件数2 个
  • 新增代码行:约 60 行
  • 删除代码行:约 80 行
  • 净减少代码行:约 20 行(代码更简洁)

修复完成时间2025-11-05 修复依据:腾讯文档开放平台官方文档 验证状态 已通过编译验证 测试状态 待进行集成测试