1
This commit is contained in:
428
doc/腾讯文档API关键修复_根据官方文档.md
Normal file
428
doc/腾讯文档API关键修复_根据官方文档.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# 腾讯文档 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
|
||||
**修复依据**:腾讯文档开放平台官方文档
|
||||
**验证状态**:✅ 已通过编译验证
|
||||
**测试状态**:⏳ 待进行集成测试
|
||||
|
||||
Reference in New Issue
Block a user