Files
ruoyi-java/doc/腾讯文档API测试验证指南.md
2025-11-06 10:26:40 +08:00

647 lines
18 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 测试验证指南
## 测试目的
验证根据官方文档修复后的 API 实现是否正确工作。
## 前置条件
### 1. 获取测试凭证
访问 [腾讯文档开放平台](https://docs.qq.com/open/),创建应用并获取:
- ✅ Client ID应用ID
- ✅ Client Secret应用密钥
- ✅ Redirect URI已配置的回调地址
### 2. 配置测试环境
`application-dev.yml` 中配置:
```yaml
tencent:
doc:
app-id: YOUR_CLIENT_ID
app-secret: YOUR_CLIENT_SECRET
redirect-uri: YOUR_REDIRECT_URI
api-base-url: https://docs.qq.com/openapi/spreadsheet/v3
```
### 3. 准备测试文档
在腾讯文档中创建一个测试表格,获取:
- ✅ File ID从 URL 中获取)
- ✅ Sheet ID从 URL 参数 `tab` 中获取)
示例 URL
```
https://docs.qq.com/sheet/DQXxxxxxxxxxxxxxx?tab=BB08J2
↑ ↑
File ID Sheet ID
```
---
## 测试流程
### 第 1 步OAuth2 授权测试
#### 1.1 获取授权 URL
```java
@RestController
@RequestMapping("/test/tencent-doc")
public class TencentDocTestController {
@Autowired
private ITencentDocService tencentDocService;
@GetMapping("/auth-url")
public String getAuthUrl() {
String authUrl = tencentDocService.getAuthUrl();
System.out.println("授权 URL: " + authUrl);
return authUrl;
}
}
```
**访问测试**
```
GET http://localhost:8080/test/tencent-doc/auth-url
```
**预期响应**
```
https://docs.qq.com/oauth/v2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=all&state=xxxxx
```
**手动测试**
1. 复制授权 URL 到浏览器
2. 扫码或微信授权
3. 授权成功后会重定向到回调地址,并带上 `code` 参数
#### 1.2 获取 Access Token
```java
@GetMapping("/callback")
public JSONObject callback(@RequestParam String code) {
System.out.println("收到授权码: " + code);
JSONObject tokenResponse = tencentDocService.getAccessTokenByCode(code);
System.out.println("Token 响应: " + tokenResponse);
String accessToken = tokenResponse.getString("access_token");
String refreshToken = tokenResponse.getString("refresh_token");
Integer expiresIn = tokenResponse.getInteger("expires_in");
String userId = tokenResponse.getString("user_id");
System.out.println("Access Token: " + accessToken);
System.out.println("Refresh Token: " + refreshToken);
System.out.println("过期时间: " + expiresIn + " 秒");
System.out.println("User ID (Open ID): " + userId);
return tokenResponse;
}
```
**预期响应**(根据官方文档):
```json
{
"access_token": "ACCESSTOKENEXAMPLE",
"token_type": "Bearer",
"refresh_token": "REFRESHTOKENEXAMPLE",
"expires_in": 259200,
"scope": "scope.file.editable,scope.folder.creatable",
"user_id": "bcb50c8a4b724d86bbcf6fc64c5e2b22"
}
```
**验证要点**
- ✅ 响应包含 `access_token`
- ✅ 响应包含 `refresh_token`
- ✅ 响应包含 `user_id`(即 Open ID
-`expires_in` 为 2592003天
---
### 第 2 步:获取用户信息测试(关键修复点)
```java
@GetMapping("/user-info")
public JSONObject getUserInfo(@RequestParam String accessToken) {
System.out.println("测试获取用户信息Access Token: " + accessToken);
JSONObject result = TencentDocApiUtil.getUserInfo(accessToken);
System.out.println("完整响应: " + result.toJSONString());
// 验证响应结构
Integer ret = result.getInteger("ret");
String msg = result.getString("msg");
JSONObject data = result.getJSONObject("data");
System.out.println("业务返回码 (ret): " + ret);
System.out.println("业务返回信息 (msg): " + msg);
if (ret == 0 && data != null) {
String openID = data.getString("openID");
String nick = data.getString("nick");
String avatar = data.getString("avatar");
String source = data.getString("source");
String unionID = data.getString("unionID");
System.out.println("✓ Open ID: " + openID);
System.out.println("✓ 昵称: " + nick);
System.out.println("✓ 头像: " + avatar);
System.out.println("✓ 来源: " + source);
System.out.println("✓ Union ID: " + unionID);
} else {
System.err.println("✗ 获取用户信息失败: " + msg);
}
return result;
}
```
**访问测试**
```
GET http://localhost:8080/test/tencent-doc/user-info?accessToken=YOUR_ACCESS_TOKEN
```
**预期响应**(根据官方文档):
```json
{
"ret": 0,
"msg": "Succeed",
"data": {
"openID": "bcb50c8a4b724d86bbcf6fc64c5e2b22",
"nick": "用户昵称",
"avatar": "https://thirdwx.qlogo.cn/mmopen/xxx",
"source": "wx",
"unionID": "xxxxxx"
}
}
```
**验证要点**(关键!):
- ✅ HTTP 状态码为 200
-`ret` 字段为 0表示成功
-`msg` 字段为 "Succeed"
-`data` 对象存在且包含 `openID` 字段(注意大写 ID
-`openID` 字段不为空
- ✅ 其他字段nick、avatar 等)正常返回
**常见错误**
1. 如果返回 401Access Token 无效或过期
2. 如果返回 `ret != 0`:业务逻辑错误,查看 `msg` 信息
3. 如果 `data` 为 null响应解析错误
---
### 第 3 步:获取文件信息测试
```java
@GetMapping("/file-info")
public JSONObject getFileInfo(
@RequestParam String accessToken,
@RequestParam String fileId
) {
System.out.println("测试获取文件信息");
System.out.println("File ID: " + fileId);
JSONObject result = tencentDocService.getFileInfo(accessToken, fileId);
System.out.println("文件信息: " + result.toJSONString());
// 解析文件信息
String fileIdResp = result.getString("fileId");
JSONObject metadata = result.getJSONObject("metadata");
JSONArray sheets = result.getJSONArray("sheets");
System.out.println("✓ 文件 ID: " + fileIdResp);
System.out.println("✓ 元数据: " + metadata);
System.out.println("✓ 工作表数量: " + (sheets != null ? sheets.size() : 0));
if (sheets != null) {
for (int i = 0; i < sheets.size(); i++) {
JSONObject sheet = sheets.getJSONObject(i);
JSONObject properties = sheet.getJSONObject("properties");
if (properties != null) {
String sheetId = properties.getString("sheetId");
String title = properties.getString("title");
Integer rowCount = properties.getInteger("rowCount");
Integer columnCount = properties.getInteger("columnCount");
System.out.println(" 工作表 " + (i + 1) + ": " + title);
System.out.println(" - Sheet ID: " + sheetId);
System.out.println(" - 行数: " + rowCount);
System.out.println(" - 列数: " + columnCount);
}
}
}
return result;
}
```
**访问测试**
```
GET http://localhost:8080/test/tencent-doc/file-info?accessToken=YOUR_ACCESS_TOKEN&fileId=YOUR_FILE_ID
```
**验证要点**
- ✅ HTTP 状态码为 200
- ✅ 返回文件 ID
- ✅ 返回工作表列表
- ✅ 每个工作表包含 properties 信息
---
### 第 4 步:读取表格数据测试
```java
@GetMapping("/read-data")
public JSONObject readData(
@RequestParam String accessToken,
@RequestParam String fileId,
@RequestParam String sheetId,
@RequestParam(defaultValue = "A1:Z10") String range
) {
System.out.println("测试读取表格数据");
System.out.println("File ID: " + fileId);
System.out.println("Sheet ID: " + sheetId);
System.out.println("Range: " + range);
JSONObject result = tencentDocService.readSheetData(
accessToken, fileId, sheetId, range
);
System.out.println("读取结果: " + result.toJSONString());
// 解析数据
JSONArray values = result.getJSONArray("values");
if (values != null && values.size() > 0) {
System.out.println("✓ 读取到 " + values.size() + " 行数据");
// 打印前 5 行
for (int i = 0; i < Math.min(5, values.size()); i++) {
JSONArray row = values.getJSONArray(i);
System.out.println(" 行 " + (i + 1) + ": " + row.toJSONString());
}
if (values.size() > 5) {
System.out.println(" ... 还有 " + (values.size() - 5) + " 行");
}
} else {
System.out.println("✓ 指定范围内没有数据");
}
return result;
}
```
**访问测试**
```
GET http://localhost:8080/test/tencent-doc/read-data?accessToken=YOUR_ACCESS_TOKEN&fileId=YOUR_FILE_ID&sheetId=YOUR_SHEET_ID&range=A1:Z10
```
**预期响应**
```json
{
"values": [
["列1", "列2", "列3"],
["数据1", "数据2", "数据3"],
["数据4", "数据5", "数据6"]
]
}
```
**验证要点**
- ✅ HTTP 状态码为 200
- ✅ 返回 `values` 数组
- ✅ 数据结构为二维数组
- ✅ 数据内容正确
---
### 第 5 步:写入表格数据测试
```java
@PostMapping("/write-data")
public JSONObject writeData(
@RequestParam String accessToken,
@RequestParam String fileId,
@RequestParam String sheetId,
@RequestParam(defaultValue = "A1") String range
) {
System.out.println("测试写入表格数据");
// 构造测试数据
Object[][] values = {
{"测试标题1", "测试标题2", "测试标题3"},
{"测试数据1", "测试数据2", "测试数据3"},
{"测试数据4", "测试数据5", "测试数据6"}
};
System.out.println("写入数据到 " + range);
JSONObject result = tencentDocService.writeSheetData(
accessToken, fileId, sheetId, range, values
);
System.out.println("写入结果: " + result.toJSONString());
return result;
}
```
**访问测试**
```
POST http://localhost:8080/test/tencent-doc/write-data?accessToken=YOUR_ACCESS_TOKEN&fileId=YOUR_FILE_ID&sheetId=YOUR_SHEET_ID&range=A1
```
**验证要点**
- ✅ HTTP 状态码为 200
- ✅ 写入成功
- ✅ 在腾讯文档中手动验证数据已写入
---
### 第 6 步:追加数据测试
```java
@PostMapping("/append-data")
public JSONObject appendData(
@RequestParam String accessToken,
@RequestParam String fileId,
@RequestParam String sheetId
) {
System.out.println("测试追加表格数据");
// 构造测试数据
Object[][] values = {
{"追加行1-列1", "追加行1-列2", "追加行1-列3"},
{"追加行2-列1", "追加行2-列2", "追加行2-列3"}
};
System.out.println("追加 " + values.length + " 行数据");
// 注意appendSheetData 内部会自动查找最后一行并追加
// 这里需要使用 TencentDocApiUtil 直接调用
JSONObject result = TencentDocApiUtil.appendSheetData(
accessToken,
tencentDocConfig.getAppId(),
getOpenID(accessToken), // 辅助方法
fileId,
sheetId,
values,
tencentDocConfig.getApiBaseUrl()
);
System.out.println("追加结果: " + result.toJSONString());
return result;
}
// 辅助方法:获取 Open ID
private String getOpenID(String accessToken) {
JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken);
JSONObject data = userInfo.getJSONObject("data");
return data.getString("openID");
}
```
**验证要点**
- ✅ HTTP 状态码为 200
- ✅ 数据追加到表格末尾
- ✅ 在腾讯文档中手动验证数据位置正确
---
## 完整测试流程示例
```java
@RestController
@RequestMapping("/test/tencent-doc")
public class TencentDocTestController {
@Autowired
private ITencentDocService tencentDocService;
@Autowired
private TencentDocConfig tencentDocConfig;
/**
* 完整流程测试
*/
@GetMapping("/full-test")
public Map<String, Object> fullTest(
@RequestParam String accessToken,
@RequestParam String fileId,
@RequestParam String sheetId
) {
Map<String, Object> results = new LinkedHashMap<>();
try {
// 1. 获取用户信息
System.out.println("\n=== 第1步获取用户信息 ===");
JSONObject userInfo = TencentDocApiUtil.getUserInfo(accessToken);
JSONObject data = userInfo.getJSONObject("data");
String openID = data.getString("openID");
results.put("1_userInfo", Map.of(
"status", "success",
"openID", openID,
"nick", data.getString("nick")
));
System.out.println("✓ 用户信息获取成功Open ID: " + openID);
// 2. 获取文件信息
System.out.println("\n=== 第2步获取文件信息 ===");
JSONObject fileInfo = tencentDocService.getFileInfo(accessToken, fileId);
results.put("2_fileInfo", Map.of(
"status", "success",
"fileId", fileInfo.getString("fileId"),
"sheetCount", fileInfo.getJSONArray("sheets").size()
));
System.out.println("✓ 文件信息获取成功");
// 3. 读取表格数据
System.out.println("\n=== 第3步读取表格数据 ===");
JSONObject readResult = tencentDocService.readSheetData(
accessToken, fileId, sheetId, "A1:Z10"
);
JSONArray values = readResult.getJSONArray("values");
results.put("3_readData", Map.of(
"status", "success",
"rowCount", values != null ? values.size() : 0
));
System.out.println("✓ 读取数据成功,共 " + (values != null ? values.size() : 0) + " 行");
// 4. 写入测试数据
System.out.println("\n=== 第4步写入测试数据 ===");
Object[][] testData = {
{"测试时间", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())},
{"测试状态", "成功"}
};
JSONObject writeResult = tencentDocService.writeSheetData(
accessToken, fileId, sheetId, "A100", testData
);
results.put("4_writeData", Map.of(
"status", "success"
));
System.out.println("✓ 写入数据成功");
// 5. 总结
results.put("summary", Map.of(
"totalTests", 4,
"passedTests", 4,
"failedTests", 0,
"status", "✓ 所有测试通过"
));
System.out.println("\n=== 测试完成 ===");
System.out.println("✓ 所有测试通过!");
} catch (Exception e) {
results.put("error", Map.of(
"status", "failed",
"message", e.getMessage(),
"type", e.getClass().getName()
));
System.err.println("\n=== 测试失败 ===");
System.err.println("✗ 错误: " + e.getMessage());
e.printStackTrace();
}
return results;
}
}
```
**访问测试**
```
GET http://localhost:8080/test/tencent-doc/full-test?accessToken=YOUR_ACCESS_TOKEN&fileId=YOUR_FILE_ID&sheetId=YOUR_SHEET_ID
```
---
## 常见问题排查
### 问题 1获取用户信息返回 401
**原因**Access Token 无效或过期
**解决方案**
1. 检查 Access Token 是否正确
2. 使用 Refresh Token 刷新 Access Token
3. 重新进行 OAuth2 授权
### 问题 2获取用户信息返回 `ret != 0`
**原因**:业务逻辑错误
**解决方案**
1. 查看 `msg` 字段的具体错误信息
2. 确认 Access Token 是否有效
3. 检查网络连接
### 问题 3无法获取 Open ID返回 null
**原因**:响应解析错误
**解决方案**
1. 打印完整响应内容,检查结构
2. 确认使用 `data.getString("openID")`(大写 ID
3. 确认响应中包含 `data` 对象
### 问题 4表格操作返回 404
**原因**File ID 或 Sheet ID 错误
**解决方案**
1. 从浏览器地址栏重新获取 File ID 和 Sheet ID
2. 确认用户有权限访问该文档
3. 检查 API 路径是否正确
### 问题 5表格操作返回 403
**原因**:权限不足
**解决方案**
1. 确认授权时选择了正确的权限范围
2. 在腾讯文档中检查文档的分享设置
3. 确认 Access Token 对应的用户有编辑权限
---
## 测试检查清单
### OAuth2 授权 ✅
- [ ] 成功生成授权 URL
- [ ] 用户可以扫码或微信授权
- [ ] 成功获取 Access Token
- [ ] 成功获取 Refresh Token
- [ ] Access Token 有效期正确3天
### 用户信息 API ✅ (关键修复点)
- [ ] HTTP 请求使用查询参数 `access_token`
- [ ] 响应包含 `ret``msg``data` 字段
- [ ] `ret` 为 0 表示成功
- [ ] `data.openID` 字段存在且不为空(注意大写 ID
- [ ] 其他用户信息nick、avatar 等)正常
### 文件操作 API ✅
- [ ] 成功获取文件信息
- [ ] 成功获取工作表列表
- [ ] 工作表信息完整sheetId、title、rowCount 等)
### 表格数据操作 API ✅
- [ ] 成功读取表格数据
- [ ] 数据格式为二维数组
- [ ] 成功写入表格数据
- [ ] 写入的数据在腾讯文档中可见
- [ ] 成功追加表格数据
- [ ] 追加的数据位置正确(在最后一行之后)
---
## 性能测试建议
### 1. 并发测试
测试多个用户同时调用 API 的性能表现。
### 2. 大数据量测试
测试读取和写入大量数据(如 1000 行)的性能。
### 3. API 限流测试
测试 API 调用频率限制,避免被限流。
---
**测试指南版本**1.0
**最后更新**2025-11-05
**适用修复版本**:根据官方文档的关键修复
---
## 快速开始
如果您想快速验证修复是否成功,执行以下最小测试:
```bash
# 1. 启动应用
cd ruoyi-java
mvn spring-boot:run
# 2. 获取授权(浏览器访问)
http://localhost:8080/test/tencent-doc/auth-url
# 3. 扫码授权后,从回调 URL 中获取 code
# 4. 测试用户信息接口(最关键)
curl "http://localhost:8080/test/tencent-doc/user-info?accessToken=YOUR_ACCESS_TOKEN"
# 预期看到:
# {
# "ret": 0,
# "msg": "Succeed",
# "data": {
# "openID": "xxx...",
# "nick": "用户昵称",
# ...
# }
# }
```
如果看到正确的响应结构,说明关键修复已生效! ✅