# 腾讯文档 API 关键修复 - 根据官方文档 ## 修复日期 2025-11-05 ## 问题来源 根据用户提供的腾讯文档官方文档链接,发现之前的实现存在重大错误,与官方文档规范不符。 ## 官方文档参考 - [发起授权](https://docs.qq.com/open/document/app/oauth2/authorize.html) - [获取 Access Token](https://docs.qq.com/open/document/app/oauth2/access_token.html) - [获取用户信息](https://docs.qq.com/open/document/app/oauth2/user_info.html) - [刷新 Token](https://docs.qq.com/open/document/app/oauth2/refresh_token.html) - [批量更新表格](https://docs.qq.com/open/document/app/openapi/v3/sheet/batchupdate/update.html) - [获取表格信息](https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_sheet.html) --- ## 🚨 关键问题修复 ### 问题 1:获取用户信息接口的鉴权方式错误 #### ❌ 错误实现 ```java // 使用 Authorization: Bearer 请求头 conn.setRequestProperty("Authorization", "Bearer " + accessToken); ``` #### ✅ 正确实现(根据官方文档) 根据[官方文档](https://docs.qq.com/open/document/app/oauth2/user_info.html),应该使用**查询参数**传递 `access_token`: ```java // 使用查询参数传递 access_token String apiUrl = "https://docs.qq.com/oauth/v2/userinfo?access_token=" + accessToken; ``` **官方文档说明**: - 接口名:`/oauth/v2/userinfo` - 请求方式:`GET` - 请求参数:`access_token`(string,必选,访问令牌) --- ### 问题 2:用户信息响应字段解析错误 #### ❌ 错误实现 ```java // 直接从根对象获取 openId String openId = userInfo.getString("openId"); ``` #### ✅ 正确实现(根据官方文档) 根据[官方文档](https://docs.qq.com/open/document/app/oauth2/user_info.html),响应结构为: ```json { "ret": 0, "msg": "Succeed", "data": { "openID": "bcb50c8a4b724d86bbcf6fc64c5e2b22", "nick": "用户昵称", "avatar": "https://example.com/avatar.jpg", "source": "wx", "unionID": "xxxxxx" } } ``` **正确的解析方式**: ```java 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 #### 修改1:getUserInfo 方法完全重写 ```java /** * 获取用户信息(包含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 方法 ```java 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 的逻辑 **修改前**(错误): ```java JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken); String openId = userInfo.getString("openId"); if (openId == null || openId.isEmpty()) { throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效"); } ``` **修改后**(正确): ```java // 获取用户信息(包含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 Accept:application/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. 响应结构理解错误 腾讯文档的响应采用统一的业务响应格式: ```json { "ret": 0, // 业务返回码,0表示成功 "msg": "Succeed", // 业务返回信息 "data": { // 实际数据在这里 "openID": "xxx" } } ``` 之前的实现忽略了外层的 `ret` 和 `msg` 字段,也没有从 `data` 对象中获取数据。 ### 3. 字段命名不一致 官方文档明确使用 `openID`(大写 ID),而之前使用了 `openId`(小写 id),导致解析失败。 --- ## 测试验证 ### 测试用例 1:获取用户信息 ```java @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:完整的表格操作流程 ```java @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 变更历史 - 定期检查官方文档更新 --- ## 编译验证 ✅ **编译状态**:无错误,无警告 ```bash 文件:TencentDocApiUtil.java 状态:✓ 编译通过 文件:TencentDocServiceImpl.java 状态:✓ 编译通过 ``` --- ## 修改统计 - 修改文件数:2 个 - 新增代码行:约 60 行 - 删除代码行:约 80 行 - 净减少代码行:约 20 行(代码更简洁) --- **修复完成时间**:2025-11-05 **修复依据**:腾讯文档开放平台官方文档 **验证状态**:✅ 已通过编译验证 **测试状态**:⏳ 待进行集成测试