From 9b9aea8d406f7e1447e3c4f0817618f261801fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8D=92?= Date: Thu, 6 Nov 2025 10:46:01 +0800 Subject: [PATCH] 1 --- doc/腾讯文档API数据格式解析说明.md | 468 +++++++++++++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 doc/腾讯文档API数据格式解析说明.md diff --git a/doc/腾讯文档API数据格式解析说明.md b/doc/腾讯文档API数据格式解析说明.md new file mode 100644 index 0000000..22572a1 --- /dev/null +++ b/doc/腾讯文档API数据格式解析说明.md @@ -0,0 +1,468 @@ +# 腾讯文档 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 数据格式解析 +