From a830c75bf172e8cd8477f77b80350b5412fc5ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8D=92?= Date: Thu, 6 Nov 2025 10:39:04 +0800 Subject: [PATCH] 1 --- doc/腾讯文档API读取失败诊断指南.md | 396 ++++++++++++++++++ .../jarvis/TencentDocController.java | 19 +- .../service/impl/TencentDocServiceImpl.java | 37 +- 3 files changed, 441 insertions(+), 11 deletions(-) create mode 100644 doc/腾讯文档API读取失败诊断指南.md diff --git a/doc/腾讯文档API读取失败诊断指南.md b/doc/腾讯文档API读取失败诊断指南.md new file mode 100644 index 0000000..0413ec9 --- /dev/null +++ b/doc/腾讯文档API读取失败诊断指南.md @@ -0,0 +1,396 @@ +# 腾讯文档 API 读取失败诊断指南 + +## 问题描述 +当调用腾讯文档读取接口时,返回错误: +```json +{ + "msg": "无法读取表头,请检查headerRow参数", + "code": 500 +} +``` + +请求参数: +```json +{ + "fileId": "DUW50RUprWXh2TGJK", + "sheetId": "BB08J2", + "headerRow": 1, + "orderNoColumn": 3, + "logisticsLinkColumn": 13 +} +``` + +--- + +## 已添加的调试功能 + +我已经在代码中添加了详细的日志记录,现在会输出以下信息: + +### 1. Service 层日志 +- 开始读取表格数据的参数 +- 获取用户信息的响应 +- Open ID 提取结果 +- API 调用参数 +- API 返回结果 + +### 2. Controller 层日志 +- 读取表头的范围 +- 表头数据的完整响应 +- values 数组是否为空 + +--- + +## 诊断步骤 + +### 步骤 1:查看应用日志 + +启用 DEBUG 级别日志: + +**application-dev.yml**: +```yaml +logging: + level: + com.ruoyi.jarvis.service.impl.TencentDocServiceImpl: DEBUG + com.ruoyi.jarvis.util.TencentDocApiUtil: DEBUG + com.ruoyi.web.controller.jarvis.TencentDocController: DEBUG +``` + +重启应用后,再次调用 API,查看日志输出。 + +### 步骤 2:分析日志信息 + +#### 2.1 检查用户信息获取 +查找日志: +``` +正在获取用户信息... +用户信息响应: {"ret":0,"msg":"Succeed","data":{...}} +``` + +**可能的问题**: +- ❌ 如果看到 `401 Unauthorized`:Access Token 无效或过期 +- ❌ 如果看到 `ret != 0`:业务逻辑错误 +- ❌ 如果 `data` 为 null:响应格式不正确 + +**解决方案**: +1. 检查 Access Token 是否有效 +2. 使用 Refresh Token 刷新 Access Token +3. 重新进行 OAuth2 授权 + +#### 2.2 检查 Open ID 获取 +查找日志: +``` +成功获取 Open ID: bcb50c8a4b724d86bbcf6fc64c5e2b22 +``` + +**可能的问题**: +- ❌ 如果看到 `openID 字段不存在`:响应结构解析错误 +- ❌ 如果 openID 为空:用户信息不完整 + +**解决方案**: +1. 检查用户信息响应的完整内容 +2. 确认响应格式是否为:`{"ret":0,"msg":"Succeed","data":{"openID":"xxx",...}}` +3. 注意字段名是 `openID`(大写 ID) + +#### 2.3 检查 API 调用 +查找日志: +``` +读取表格数据 - fileId: DUW50RUprWXh2TGJK, sheetId: BB08J2, range: A1:Z1, apiUrl: https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/A1:Z1 +``` + +**可能的问题**: +- ❌ 如果看到 `404 Not Found`:文件 ID 或工作表 ID 错误 +- ❌ 如果看到 `403 Forbidden`:没有访问权限 +- ❌ 如果看到 `400 Bad Request`:请求参数格式错误 + +**解决方案**: +1. **验证 File ID**: + - 打开腾讯文档,从 URL 中获取正确的 File ID + - URL 格式:`https://docs.qq.com/sheet/DUW50RUprWXh2TGJK?tab=BB08J2` + - File ID 是 `sheet/` 后面到 `?` 之前的部分 + +2. **验证 Sheet ID**: + - Sheet ID 是 URL 中 `tab=` 后面的部分 + - 例如:`BB08J2` + +3. **检查文档权限**: + - 确认授权用户有权限访问该文档 + - 在腾讯文档中检查分享设置 + +#### 2.4 检查 API 响应 +查找日志: +``` +表头数据响应: {"values":[["列1","列2","列3"]]} +``` +或 +``` +表头数据中values数组为空,完整响应: {...} +``` + +**可能的问题**: + +##### 问题 A:API 返回成功但 values 为空 +```json +{ + "values": [] +} +``` +或 +```json +{} +``` + +**原因**: +1. 指定的行数据确实为空 +2. Range 格式不正确 +3. 权限不足,只能看到空数据 + +**解决方案**: +1. 手动在腾讯文档中检查第 1 行是否有数据 +2. 尝试不同的 range: + - `A1:A1`(单个单元格) + - `A1:E1`(前 5 列) + - `A1`(从 A1 开始的所有数据) + +##### 问题 B:API 返回错误 +可能的错误响应: +```json +{ + "error": "invalid_token", + "error_description": "Invalid access token" +} +``` + +**原因**: +- Access Token 无效或过期 +- Open ID 不正确 +- Client ID(App ID)不正确 + +**解决方案**: +1. 刷新 Access Token +2. 重新获取 Open ID +3. 检查配置文件中的 App ID + +--- + +## 常见问题和解决方案 + +### 问题 1:Access Token 过期 + +**症状**: +``` +getUserInfo 返回 401 Unauthorized +``` + +**解决方案**: +```java +// 使用 Refresh Token 刷新 Access Token +JSONObject newTokens = tencentDocService.refreshAccessToken(refreshToken); +String newAccessToken = newTokens.getString("access_token"); +String newRefreshToken = newTokens.getString("refresh_token"); + +// 保存新的 tokens +// ... +``` + +### 问题 2:文档权限不足 + +**症状**: +``` +调用腾讯文档API失败: HTTP 403 Forbidden +``` + +**解决方案**: +1. 在腾讯文档中打开该文档 +2. 点击右上角"分享"按钮 +3. 确认授权用户的微信/QQ 账号有访问权限 +4. 如果是企业文档,需要确认企业权限设置 + +### 问题 3:File ID 或 Sheet ID 错误 + +**症状**: +``` +调用腾讯文档API失败: HTTP 404 Not Found +``` + +**解决方案**: +1. 重新从浏览器地址栏复制完整 URL +2. 正确提取 File ID 和 Sheet ID: + +``` +URL: https://docs.qq.com/sheet/DUW50RUprWXh2TGJK?tab=BB08J2 + ↑ ↑ + File ID Sheet ID + (18个字符) (6个字符) +``` + +3. File ID 通常以 `D` 开头,长度约 18 个字符 +4. Sheet ID 通常是 6 个大写字母和数字的组合 + +### 问题 4:Range 格式错误 + +**症状**: +``` +values 数组为空,但手动检查文档有数据 +``` + +**可能的原因**: +- Range 格式不符合腾讯文档 API 规范 +- 行号从 0 开始而不是从 1 开始 + +**测试不同的 Range 格式**: +```bash +# 测试 1:单个单元格 +curl "http://localhost:8080/api/test/read?fileId=XXX&sheetId=YYY&range=A1" + +# 测试 2:单行范围 +curl "http://localhost:8080/api/test/read?fileId=XXX&sheetId=YYY&range=A1:Z1" + +# 测试 3:多行范围 +curl "http://localhost:8080/api/test/read?fileId=XXX&sheetId=YYY&range=A1:Z10" + +# 测试 4:使用行号 0(如果API是从0开始) +curl "http://localhost:8080/api/test/read?fileId=XXX&sheetId=YYY&range=A0:Z0" +``` + +### 问题 5:鉴权头设置错误 + +**症状**: +``` +调用腾讯文档API失败: HTTP 401 Unauthorized +``` + +**检查**: +确认代码中使用了正确的鉴权方式: +```java +conn.setRequestProperty("Access-Token", accessToken); +conn.setRequestProperty("Client-Id", clientId); +conn.setRequestProperty("Open-Id", openId); +``` + +而不是: +```java +conn.setRequestProperty("Authorization", "Bearer " + accessToken); // ❌ 错误 +``` + +--- + +## 快速诊断脚本 + +创建一个测试接口来诊断问题: + +```java +@GetMapping("/test/diagnose") +public Map diagnose(@RequestParam String accessToken) { + Map result = new LinkedHashMap<>(); + + try { + // 1. 测试获取用户信息 + System.out.println("\n=== 步骤1:获取用户信息 ==="); + JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken); + result.put("1_userInfo", userInfo); + System.out.println("✓ 用户信息: " + userInfo.toJSONString()); + + // 2. 提取 Open ID + System.out.println("\n=== 步骤2:提取 Open ID ==="); + JSONObject data = userInfo.getJSONObject("data"); + String openID = data != null ? data.getString("openID") : null; + result.put("2_openID", openID); + System.out.println("✓ Open ID: " + openID); + + // 3. 测试读取文档 + String testFileId = "DUW50RUprWXh2TGJK"; + String testSheetId = "BB08J2"; + String testRange = "A1:Z1"; + + System.out.println("\n=== 步骤3:读取表格数据 ==="); + System.out.println("File ID: " + testFileId); + System.out.println("Sheet ID: " + testSheetId); + System.out.println("Range: " + testRange); + + JSONObject readResult = tencentDocService.readSheetData( + accessToken, testFileId, testSheetId, testRange + ); + result.put("3_readResult", readResult); + System.out.println("✓ 读取结果: " + readResult.toJSONString()); + + // 4. 检查 values 数组 + System.out.println("\n=== 步骤4:检查 values 数组 ==="); + JSONArray values = readResult != null ? readResult.getJSONArray("values") : null; + result.put("4_values", values); + result.put("4_valuesCount", values != null ? values.size() : 0); + System.out.println("✓ Values 数组大小: " + (values != null ? values.size() : 0)); + if (values != null && !values.isEmpty()) { + System.out.println("✓ 第一行数据: " + values.getJSONArray(0).toJSONString()); + } + + result.put("status", "success"); + result.put("message", "所有测试通过"); + + } catch (Exception e) { + result.put("status", "error"); + result.put("error", e.getMessage()); + result.put("stackTrace", Arrays.toString(e.getStackTrace())); + System.err.println("✗ 诊断失败: " + e.getMessage()); + e.printStackTrace(); + } + + return result; +} +``` + +**使用方法**: +```bash +curl "http://localhost:8080/test/diagnose?accessToken=YOUR_ACCESS_TOKEN" +``` + +--- + +## 检查清单 + +执行以下检查清单,确保所有配置正确: + +### 配置检查 +- [ ] `application-dev.yml` 中 `app-id` 配置正确 +- [ ] `application-dev.yml` 中 `app-secret` 配置正确 +- [ ] `application-dev.yml` 中 `api-base-url` 为 `https://docs.qq.com/openapi/spreadsheet/v3` +- [ ] 日志级别设置为 DEBUG + +### 授权检查 +- [ ] Access Token 未过期(有效期3天) +- [ ] 授权用户有访问该文档的权限 +- [ ] 文档没有被删除或移动 + +### 参数检查 +- [ ] File ID 正确(从 URL 中复制) +- [ ] Sheet ID 正确(从 URL 的 tab 参数中复制) +- [ ] headerRow 参数正确(通常为 1) +- [ ] Range 格式正确(如 `A1:Z1`) + +### 代码检查 +- [ ] 使用查询参数方式调用 `/oauth/v2/userinfo?access_token=xxx` +- [ ] 正确解析用户信息:`userInfo.getJSONObject("data").getString("openID")` +- [ ] 使用三个鉴权头:`Access-Token`, `Client-Id`, `Open-Id` + +--- + +## 联系支持 + +如果以上步骤都无法解决问题,请提供以下信息: + +1. **完整的日志输出**(DEBUG 级别) +2. **请求参数**: + - File ID + - Sheet ID + - Header Row + - Range +3. **腾讯文档 URL**(用于验证 ID 是否正确) +4. **错误信息**(完整的堆栈跟踪) +5. **用户信息响应**(脱敏后的 JSON) +6. **API 调用响应**(完整的 JSON) + +--- + +**诊断指南版本**:1.0 +**创建时间**:2025-11-05 +**适用场景**:腾讯文档 API 读取失败问题排查 + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java index e99e02a..53c8a8f 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java @@ -513,11 +513,26 @@ public class TencentDocController extends BaseController { // 读取表格数据(先读取表头行用于识别列位置) String headerRange = String.format("%s%d:%s%d", startColumn, headerRow, endColumn, headerRow); - JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange); + log.info("读取表头 - 范围: {}", headerRange); + + JSONObject headerData = null; + try { + headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange); + log.info("表头数据响应: {}", headerData != null ? headerData.toJSONString() : "null"); + } catch (Exception e) { + log.error("读取表头失败", e); + return AjaxResult.error("读取表头失败: " + e.getMessage()); + } + + if (headerData == null) { + return AjaxResult.error("读取表头返回null,请检查Access Token是否有效或文档权限"); + } + JSONArray headerValues = headerData.getJSONArray("values"); if (headerValues == null || headerValues.isEmpty()) { - return AjaxResult.error("无法读取表头,请检查headerRow参数"); + log.error("表头数据中values数组为空,完整响应: {}", headerData.toJSONString()); + return AjaxResult.error("无法读取表头,请检查headerRow参数。API响应: " + headerData.toJSONString()); } // 自动识别列位置(如果未指定) diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java index 5f4b956..56218c6 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java @@ -223,19 +223,34 @@ public class TencentDocServiceImpl implements ITencentDocService { @Override public JSONObject readSheetData(String accessToken, String fileId, String sheetId, String range) { try { + log.info("Service层 - 开始读取表格数据: fileId={}, sheetId={}, range={}", fileId, sheetId, range); + // 获取用户信息(包含Open-Id) // 官方响应格式:{ "ret": 0, "msg": "Succeed", "data": { "openID": "xxx", ... } } + log.debug("正在获取用户信息..."); 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是否有效"); + log.debug("用户信息响应: {}", userInfo != null ? userInfo.toJSONString() : "null"); + + if (userInfo == null) { + throw new RuntimeException("getUserInfo 返回 null,Access Token 可能无效"); } - return TencentDocApiUtil.readSheetData( + JSONObject data = userInfo.getJSONObject("data"); + if (data == null) { + log.error("用户信息响应中没有 data 字段,完整响应: {}", userInfo.toJSONString()); + throw new RuntimeException("无法获取用户数据,请检查Access Token是否有效。响应: " + userInfo.toJSONString()); + } + + String openId = data.getString("openID"); // 注意:官方返回的字段名是 openID(大写ID) + if (openId == null || openId.isEmpty()) { + log.error("data 对象中没有 openID 字段,data内容: {}", data.toJSONString()); + throw new RuntimeException("无法获取Open-Id,请检查Access Token是否有效。data: " + data.toJSONString()); + } + + log.info("成功获取 Open ID: {}", openId); + log.info("准备调用API - appId: {}, apiBaseUrl: {}", tencentDocConfig.getAppId(), tencentDocConfig.getApiBaseUrl()); + + JSONObject result = TencentDocApiUtil.readSheetData( accessToken, tencentDocConfig.getAppId(), openId, @@ -244,8 +259,12 @@ public class TencentDocServiceImpl implements ITencentDocService { range, tencentDocConfig.getApiBaseUrl() ); + + log.info("API调用成功,返回结果: {}", result != null ? result.toJSONString() : "null"); + return result; + } catch (Exception e) { - log.error("读取表格数据失败", e); + log.error("读取表格数据失败 - fileId: {}, sheetId: {}, range: {}", fileId, sheetId, range, e); throw new RuntimeException("读取表格数据失败: " + e.getMessage(), e); } }