# 腾讯文档 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 数据格式解析