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

429 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 腾讯文档 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
#### 修改1getUserInfo 方法完全重写
```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
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. 响应结构理解错误
腾讯文档的响应采用统一的业务响应格式:
```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
**修复依据**:腾讯文档开放平台官方文档
**验证状态**:✅ 已通过编译验证
**测试状态**:⏳ 待进行集成测试