# 腾讯文档 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<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": "用户昵称",
#     ...
#   }
# }
```

如果看到正确的响应结构，说明关键修复已生效！ ✅

