# 腾讯文档 API 数据格式解析说明

## 问题发现

在实际调用腾讯文档 V3 API 时，发现返回的数据格式与预期完全不同。

---

## 数据格式对比

### ❌ 我们最初预期的格式（简单格式）

```json
{
  "values": [
    ["单元格1", "单元格2", "单元格3"],
    ["数据1", "数据2", "数据3"]
  ]
}
```

### ✅ 实际返回的格式（gridData 格式）

```json
{
  "gridData": {
    "startRow": 0,
    "startColumn": 0,
    "rows": [
      {
        "values": [
          {
            "cellValue": {
              "text": "JY202506181808"
            },
            "cellFormat": {
              "textFormat": {
                "font": "Microsoft YaHei",
                "fontSize": 11,
                "bold": false,
                "italic": false,
                "strikethrough": false,
                "underline": false,
                "color": {
                  "red": 0,
                  "green": 0,
                  "blue": 0,
                  "alpha": 255
                }
              },
              "horizontalAlignment": "HORIZONTAL_ALIGNMENT_UNSPECIFIED",
              "verticalAlignment": "VERTICAL_ALIGNMENT_UNSPECIFIED"
            },
            "dataType": "DATA_TYPE_UNSPECIFIED"
          },
          {
            "cellValue": {
              "text": ""
            },
            ...
          }
        ]
      }
    ],
    "rowMetadata": [],
    "columnMetadata": []
  },
  "version": "0"
}
```

---

## 格式差异分析

### 数据层级

**简单格式**：
```
响应
└── values (数组)
    ├── 行1 (数组)
    │   ├── "单元格1"
    │   └── "单元格2"
    └── 行2 (数组)
        ├── "数据1"
        └── "数据2"
```

**gridData 格式**：
```
响应
└── gridData (对象)
    ├── startRow (数字)
    ├── startColumn (数字)
    ├── rows (数组)
    │   └── 行对象
    │       └── values (数组)
    │           └── 单元格对象
    │               ├── cellValue (对象)
    │               │   └── text (字符串) ← 实际文本内容在这里
    │               ├── cellFormat (对象)
    │               └── dataType (字符串)
    ├── rowMetadata (数组)
    └── columnMetadata (数组)
```

### 关键区别

| 项目 | 简单格式 | gridData 格式 |
|------|---------|--------------|
| 根字段 | `values` | `gridData` |
| 行数据 | 直接数组 | 在 `gridData.rows` 中 |
| 单元格数据 | 直接字符串 | 在 `cellValue.text` 中 |
| 格式信息 | 无 | 在 `cellFormat` 中 |
| 元数据 | 无 | 在 `rowMetadata`、`columnMetadata` 中 |

---

## 解决方案

### 1. 创建数据解析器

我们创建了 `TencentDocDataParser` 工具类来统一处理两种格式：

**位置**：`ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java`

**核心功能**：

#### (1) 解析为简单数组格式
```java
JSONArray parsedValues = TencentDocDataParser.parseToSimpleArray(apiResponse);
```

**输入**（gridData 格式）：
```json
{
  "gridData": {
    "rows": [
      {
        "values": [
          {"cellValue": {"text": "单元格1"}},
          {"cellValue": {"text": "单元格2"}}
        ]
      }
    ]
  }
}
```

**输出**（简单格式）：
```json
[
  ["单元格1", "单元格2"]
]
```

#### (2) 获取指定行数据
```java
JSONArray row = TencentDocDataParser.getRow(apiResponse, 0);  // 获取第1行
```

#### (3) 获取指定单元格文本
```java
String cellText = TencentDocDataParser.getCellText(apiResponse, 0, 2);  // 第1行第3列
```

#### (4) 打印数据结构（调试用）
```java
TencentDocDataParser.printDataStructure(apiResponse, 5);  // 打印前5行
```

### 2. 更新 Service 层

在 `TencentDocServiceImpl.java` 的 `readSheetData` 方法中：

```java
// API 调用
JSONObject result = TencentDocApiUtil.readSheetData(...);

// 解析数据为统一的简单格式
JSONArray parsedValues = TencentDocDataParser.parseToSimpleArray(result);

// 返回包含简化格式的响应
JSONObject response = new JSONObject();
response.put("values", parsedValues);  // 统一格式
response.put("_原始数据", result);     // 保留原始数据供调试

return response;
```

### 3. 向后兼容性

解析器会自动检测数据格式：
- 如果有 `gridData` 字段 → 解析为 gridData 格式
- 如果有 `values` 字段 → 直接返回（简单格式）
- 如果都没有 → 返回空数组

```java
public static JSONArray parseToSimpleArray(JSONObject apiResponse) {
    // 方式1：检查是否有 gridData 字段（V3 API 新格式）
    JSONObject gridData = apiResponse.getJSONObject("gridData");
    if (gridData != null) {
        return parseGridData(gridData);
    }
    
    // 方式2：检查是否有 values 字段（简单格式）
    JSONArray values = apiResponse.getJSONArray("values");
    if (values != null) {
        return values;
    }
    
    // 如果都没有，返回空数组
    return new JSONArray();
}
```

---

## 使用示例

### 示例 1：读取表头

```java
// 读取第2行（索引为1）作为表头
String headerRange = "A2:Z2";
JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);

// 获取简化后的数据
JSONArray headerValues = headerData.getJSONArray("values");
if (headerValues != null && !headerValues.isEmpty()) {
    JSONArray headerRow = headerValues.getJSONArray(0);  // 第一行数据
    
    // 遍历表头列
    for (int i = 0; i < headerRow.size(); i++) {
        String columnName = headerRow.getString(i);
        System.out.println("第 " + (i+1) + " 列: " + columnName);
    }
}
```

### 示例 2：查找特定列

```java
// 读取表头行
JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, "A2:Z2");
JSONArray headerValues = headerData.getJSONArray("values");
JSONArray headerRow = headerValues.getJSONArray(0);

// 查找"物流单号"列的索引
int logisticsColumn = -1;
for (int i = 0; i < headerRow.size(); i++) {
    String columnName = headerRow.getString(i);
    if ("物流单号".equals(columnName)) {
        logisticsColumn = i;
        break;
    }
}

System.out.println("物流单号列在第 " + (logisticsColumn + 1) + " 列（索引: " + logisticsColumn + "）");
```

### 示例 3：读取数据行

```java
// 读取数据行（第3行到第100行）
JSONObject sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, "A3:Z100");
JSONArray dataValues = sheetData.getJSONArray("values");

// 遍历每一行
for (int i = 0; i < dataValues.size(); i++) {
    JSONArray row = dataValues.getJSONArray(i);
    
    // 获取订单号（假设在第1列，索引0）
    String orderNo = row.getString(0);
    
    // 获取物流单号（假设在第13列，索引12）
    String logisticsNo = row.getString(12);
    
    System.out.println("订单号: " + orderNo + ", 物流单号: " + logisticsNo);
}
```

---

## 真实数据示例

根据用户提供的截图，表格结构：

```
第1行：合并单元格，包含链接（这是合并的标题行）
第2行：表头
  A列：日期
  B列：公司
  C列：草号
  D列：型号
  E列：数量
  F列：姓名
  G列：电话
  H列：地址
  I列：价格
  J列：备注
  K列：打聚戳图
  L列：是否安排
  M列：物流单号
  N列：标记

第3行及以后：数据行
  A列：3月10日
  B列：（空）
  C列：JY20251032904
  ...
  M列：（物流单号，可能为空）
```

### 处理代码示例

```java
// 1. 读取表头（第2行）
JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, "A2:Z2");
JSONArray headerValues = headerData.getJSONArray("values");
JSONArray headerRow = headerValues.getJSONArray(0);

// 2. 查找关键列的索引
int orderNoColumn = -1;      // 订单号列（草号）
int logisticsColumn = -1;    // 物流单号列

for (int i = 0; i < headerRow.size(); i++) {
    String columnName = headerRow.getString(i);
    if (columnName != null) {
        if (columnName.contains("草号")) {
            orderNoColumn = i;
        }
        if (columnName.contains("物流单号")) {
            logisticsColumn = i;
        }
    }
}

System.out.println("订单号列索引: " + orderNoColumn);        // 预期: 2（C列）
System.out.println("物流单号列索引: " + logisticsColumn);    // 预期: 12（M列）

// 3. 读取数据行（从第3行开始）
JSONObject sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, "A3:Z100");
JSONArray dataValues = sheetData.getJSONArray("values");

// 4. 处理每一行数据
for (int i = 0; i < dataValues.size(); i++) {
    JSONArray row = dataValues.getJSONArray(i);
    
    // 获取订单号
    String orderNo = orderNoColumn >= 0 && orderNoColumn < row.size() 
        ? row.getString(orderNoColumn) 
        : null;
    
    // 获取物流单号
    String logisticsNo = logisticsColumn >= 0 && logisticsColumn < row.size() 
        ? row.getString(logisticsColumn) 
        : null;
    
    if (orderNo != null && !orderNo.isEmpty()) {
        System.out.println("第 " + (i+3) + " 行 - 订单号: " + orderNo + ", 物流单号: " + logisticsNo);
        
        // 如果物流单号为空，可以填充
        if (logisticsNo == null || logisticsNo.isEmpty()) {
            System.out.println("  → 需要填充物流单号");
        }
    }
}
```

---

## 写入数据注意事项

### 写入接口的数据格式

根据腾讯文档 API 规范，写入数据时仍然使用简单格式：

```java
// 写入数据（简单格式）
Object[][] values = {
    {"数据1", "数据2", "数据3"}
};

tencentDocService.writeSheetData(accessToken, fileId, sheetId, "A10", values);
```

**不需要**转换为 gridData 格式，API 会自动处理。

---

## 调试技巧

### 1. 启用详细日志

```yaml
logging:
  level:
    com.ruoyi.jarvis.util.TencentDocDataParser: DEBUG
    com.ruoyi.jarvis.service.impl.TencentDocServiceImpl: DEBUG
```

### 2. 查看数据结构

当调用 `readSheetData` 时，会自动打印前3行数据结构：

```
数据结构（共 98 行，显示前 3 行）：
  第 1 行（15列）: ["日期","公司","草号",...,"物流单号","标记"]
  第 2 行（15列）: ["3月10日","","JY20251032904",...,"",""]
  第 3 行（15列）: ["3月10日","","JY20250309184",...,"6649902864",""]
```

### 3. 检查原始响应

Service 返回的数据中包含 `_原始数据` 字段，可以查看 API 的原始响应：

```java
JSONObject result = tencentDocService.readSheetData(...);
JSONObject originalData = result.getJSONObject("_原始数据");
System.out.println("原始响应: " + originalData.toJSONString());
```

---

## 常见问题

### Q1：为什么会有两种数据格式？

**A**：腾讯文档 V3 API 使用 gridData 格式以支持更丰富的格式信息（字体、颜色、对齐方式等）。但对于简单的数据读写，我们只需要文本内容，因此解析器会提取纯文本数据。

### Q2：读取的数据是否包含格式信息？

**A**：gridData 格式包含完整的格式信息（字体、颜色等），但我们的解析器只提取文本内容。如果需要格式信息，可以从 `_原始数据` 字段中获取。

### Q3：解析器会影响性能吗？

**A**：解析器只是简单的 JSON 遍历和文本提取，性能影响很小。对于大数据量（数千行），建议分批读取。

### Q4：是否兼容旧代码？

**A**：完全兼容。解析后的数据格式与旧代码期望的格式一致（`{"values": [[]]}`），无需修改现有代码。

---

## 总结

### 关键要点

1. ✅ **腾讯文档 V3 API 使用 gridData 格式**
2. ✅ **创建了 TencentDocDataParser 统一处理**
3. ✅ **Service 层自动解析为简单格式**
4. ✅ **完全向后兼容，无需修改上层代码**
5. ✅ **保留原始数据供调试使用**

### 文件清单

- ✅ `TencentDocDataParser.java` - 数据解析器（新增）
- ✅ `TencentDocServiceImpl.java` - Service 层（已更新）
- ✅ `腾讯文档API数据格式解析说明.md` - 本文档（新增）

---

**文档版本**：1.0  
**创建时间**：2025-11-05  
**适用场景**：腾讯文档 V3 API 数据格式解析

