# 腾讯文档 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` 为 259200(3天) --- ### 第 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. 如果返回 401:Access 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 fullTest( @RequestParam String accessToken, @RequestParam String fileId, @RequestParam String sheetId ) { Map 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": "用户昵称", # ... # } # } ``` 如果看到正确的响应结构,说明关键修复已生效! ✅