1
This commit is contained in:
396
doc/腾讯文档API读取失败诊断指南.md
Normal file
396
doc/腾讯文档API读取失败诊断指南.md
Normal file
@@ -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<String, Object> diagnose(@RequestParam String accessToken) {
|
||||
Map<String, Object> 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 读取失败问题排查
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
// 自动识别列位置(如果未指定)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user