From 7860df5c2ee6518dd752fc79b3675a725e6f4808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8D=92?= Date: Thu, 6 Nov 2025 11:05:22 +0800 Subject: [PATCH] 1 --- doc/腾讯文档API_Range格式说明.md | 351 ++++++++++++++++++ .../jarvis/TencentDocController.java | 17 +- .../service/impl/TencentDocServiceImpl.java | 10 + .../ruoyi/jarvis/util/TencentDocApiUtil.java | 5 +- 4 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 doc/腾讯文档API_Range格式说明.md diff --git a/doc/腾讯文档API_Range格式说明.md b/doc/腾讯文档API_Range格式说明.md new file mode 100644 index 0000000..6e8cd71 --- /dev/null +++ b/doc/腾讯文档API_Range格式说明.md @@ -0,0 +1,351 @@ +# 腾讯文档 API Range 格式说明 + +## 问题发现 + +在调用腾讯文档 V3 API 时,使用 Excel 格式的 range(如 `A3:Z203`)会返回错误: + +```json +{ + "code": 400001, + "message": "invalid param error: 'range' invalid" +} +``` + +## ✅ 正确的 Range 格式 + +腾讯文档 V3 API 使用**索引格式**,不是 Excel 的字母+数字格式。 + +### 格式规范 + +``` +startRow,startColumn,endRow,endColumn +``` + +**说明**: +- 所有索引**从 0 开始** +- 用逗号分隔四个值 +- `startRow`:起始行索引 +- `startColumn`:起始列索引 +- `endRow`:结束行索引 +- `endColumn`:结束列索引 + +--- + +## 📋 格式转换示例 + +### 示例 1:读取表头(第2行,A到Z列) + +**Excel 格式**(错误): +``` +A2:Z2 +``` + +**索引格式**(正确): +``` +1,0,1,25 +``` + +**解释**: +- 第2行 → 索引 1(行从0开始) +- A列 → 索引 0 +- 第2行 → 索引 1 +- Z列 → 索引 25(A=0, B=1, ..., Z=25) + +--- + +### 示例 2:读取数据行(第3行到第203行,A到Z列) + +**Excel 格式**(错误): +``` +A3:Z203 +``` + +**索引格式**(正确): +``` +2,0,202,25 +``` + +**解释**: +- 第3行 → 索引 2 +- A列 → 索引 0 +- 第203行 → 索引 202 +- Z列 → 索引 25 + +--- + +### 示例 3:读取单个单元格(M3) + +**Excel 格式**(错误): +``` +M3 +``` + +**索引格式**(正确): +``` +2,12,2,12 +``` + +**解释**: +- 第3行 → 索引 2 +- M列 → 索引 12(A=0, ..., M=12) +- 结束行也是第3行 → 索引 2 +- 结束列也是M列 → 索引 12 + +--- + +## 🔢 行列索引对照表 + +### 行索引(Excel行号 → 索引) + +| Excel 行号 | 索引 | +|-----------|------| +| 1 | 0 | +| 2 | 1 | +| 3 | 2 | +| ... | ... | +| 100 | 99 | +| 203 | 202 | + +**转换公式**:`索引 = Excel行号 - 1` + +### 列索引(列字母 → 索引) + +| 列字母 | 索引 | +|-------|------| +| A | 0 | +| B | 1 | +| C | 2 | +| D | 3 | +| ... | ... | +| M | 12 | +| ... | ... | +| Z | 25 | +| AA | 26 | +| AB | 27 | + +**转换公式**: +- 单字母:`索引 = 字母 - 'A'`(A=0, B=1, ...) +- 多字母:`索引 = (第一字母 - 'A' + 1) * 26 + (第二字母 - 'A')` + +--- + +## 💻 代码实现 + +### Java 转换方法 + +```java +/** + * 将Excel行号转换为API索引 + * @param excelRow Excel行号(从1开始) + * @return API索引(从0开始) + */ +public static int excelRowToIndex(int excelRow) { + return excelRow - 1; +} + +/** + * 将列字母转换为索引 + * @param column 列字母(A, B, C, ..., Z, AA, AB, ...) + * @return 列索引(从0开始) + */ +public static int columnLetterToIndex(String column) { + column = column.toUpperCase(); + int index = 0; + for (int i = 0; i < column.length(); i++) { + index = index * 26 + (column.charAt(i) - 'A' + 1); + } + return index - 1; +} + +/** + * 将Excel范围转换为API range格式 + * @param excelRange Excel范围(如 "A3:Z203") + * @return API range格式(如 "2,0,202,25") + */ +public static String excelRangeToApiRange(String excelRange) { + // 解析 A3:Z203 + String[] parts = excelRange.split(":"); + String start = parts[0]; // A3 + String end = parts[1]; // Z203 + + // 提取起始列和行 + int startCol = columnLetterToIndex(start.replaceAll("\\d", "")); // A -> 0 + int startRow = Integer.parseInt(start.replaceAll("[A-Z]", "")) - 1; // 3 -> 2 + + // 提取结束列和行 + int endCol = columnLetterToIndex(end.replaceAll("\\d", "")); // Z -> 25 + int endRow = Integer.parseInt(end.replaceAll("[A-Z]", "")) - 1; // 203 -> 202 + + return String.format("%d,%d,%d,%d", startRow, startCol, endRow, endCol); +} +``` + +### 使用示例 + +```java +// 读取表头(第2行) +int headerRowIndex = headerRow - 1; // 2 -> 1 +String headerRange = String.format("%d,0,%d,25", headerRowIndex, headerRowIndex); +// 结果:"1,0,1,25" + +// 读取数据行(第3行到第203行) +int startRowIndex = startRow - 1; // 3 -> 2 +int endRowIndex = endRow - 1; // 203 -> 202 +String dataRange = String.format("%d,0,%d,25", startRowIndex, endRowIndex); +// 结果:"2,0,202,25" + +// 读取单个单元格(M3) +int rowIndex = 2; // 第3行 -> 索引2 +int colIndex = 12; // M列 -> 索引12 +String cellRange = String.format("%d,%d,%d,%d", rowIndex, colIndex, rowIndex, colIndex); +// 结果:"2,12,2,12" +``` + +--- + +## 🔍 API 调用示例 + +### 完整的 API URL + +``` +https://docs.qq.com/openapi/spreadsheet/v3/files/{fileId}/{sheetId}/{range} +``` + +**示例**: + +``` +# 读取表头(第2行) +https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/1,0,1,25 + +# 读取数据行(第3行到第203行) +https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/2,0,202,25 + +# 读取单个单元格(M3) +https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/2,12,2,12 +``` + +--- + +## ⚠️ 常见错误 + +### 错误 1:使用 Excel 格式 + +``` +❌ https://.../ files/xxx/yyy/A3:Z203 +✅ https://.../files/xxx/yyy/2,0,202,25 +``` + +### 错误 2:索引从 1 开始 + +``` +❌ 第1行 → 索引 1 +✅ 第1行 → 索引 0 + +❌ A列 → 索引 1 +✅ A列 → 索引 0 +``` + +### 错误 3:行列顺序错误 + +``` +❌ startColumn,startRow,endColumn,endRow +✅ startRow,startColumn,endRow,endColumn +``` + +正确顺序:**行在前,列在后** + +--- + +## 📊 快速参考表 + +| Excel 表示 | 索引格式 | 说明 | +|-----------|---------|------| +| A1:Z1 | 0,0,0,25 | 第1行,A到Z列 | +| A2:Z2 | 1,0,1,25 | 第2行,A到Z列(表头) | +| A3:Z203 | 2,0,202,25 | 第3行到第203行,A到Z列 | +| A1:A100 | 0,0,99,0 | A列,前100行 | +| M3 | 2,12,2,12 | M列第3行(单个单元格) | +| A1 | 0,0,0,0 | A1单元格 | +| AA1:AZ100 | 0,26,99,51 | AA到AZ列,前100行 | + +--- + +## 🔧 修改记录 + +### 修改文件 + +1. ✅ `TencentDocApiUtil.java` + - 更新 `readSheetData` 方法的注释 + - 说明 range 格式为索引格式 + +2. ✅ `TencentDocController.java` + - 将 range 构建从 Excel 格式改为索引格式 + - 添加详细的转换日志 + +3. ✅ `TencentDocServiceImpl.java` + - 添加 API 错误响应检查 + - 当 code != 0 时抛出异常 + +--- + +## 🎯 测试验证 + +### 测试参数 + +```json +{ + "accessToken": "YOUR_ACCESS_TOKEN", + "fileId": "DUW50RUprWXh2TGJK", + "sheetId": "BB08J2", + "headerRow": 2, + "orderNoColumn": 2, + "logisticsLinkColumn": 12 +} +``` + +### 预期日志输出 + +``` +读取表头 - Excel行号: 2, 索引行号: 1, range: 1,0,1,25 +读取数据行 - Excel行号: 3 ~ 203, 索引: 2 ~ 202, range: 2,0,202,25 +``` + +### 成功响应 + +```json +{ + "gridData": { + "startRow": 1, + "startColumn": 0, + "rows": [ + { + "values": [ + {"cellValue": {"text": "日期"}}, + {"cellValue": {"text": "公司"}}, + {"cellValue": {"text": "草号"}}, + ... + ] + } + ] + } +} +``` + +--- + +## 📚 参考文档 + +根据实际API测试结果和错误提示总结的格式规范。 + +**关键要点**: +1. ✅ Range 使用索引格式:`startRow,startColumn,endRow,endColumn` +2. ✅ 所有索引从 0 开始 +3. ✅ 顺序:行在前,列在后 +4. ✅ Excel行号需要减1转换为索引 + +--- + +**文档版本**:1.0 +**创建时间**:2025-11-05 +**修改原因**:修复 "invalid param error: 'range' invalid" 错误 + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java index 57547c9..5a8c0ff 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java @@ -504,16 +504,16 @@ public class TencentDocController extends BaseController { // 计算读取范围:从起始行开始,读取足够多的行(假设每次最多处理200行) int endRow = startRow + 200; // 每次最多读取200行 - String startColumn = "A"; - String endColumn = "Z"; - String range = String.format("%s%d:%s%d", startColumn, startRow, endColumn, endRow); log.info("开始填充物流链接 - 文件ID: {}, 工作表ID: {}, 起始行: {}, 结束行: {}, 上次最大行: {}", fileId, sheetId, startRow, endRow, lastMaxRow); // 读取表格数据(先读取表头行用于识别列位置) - String headerRange = String.format("%s%d:%s%d", startColumn, headerRow, endColumn, headerRow); - log.info("读取表头 - 范围: {}", headerRange); + // 腾讯文档V3 API的range格式:startRow,startColumn,endRow,endColumn(从0开始的索引) + // 例如:第2行A列到Z列 = "1,0,1,25"(行索引从0开始,所以第2行是索引1) + int headerRowIndex = headerRow - 1; // Excel行号转索引(第2行 = 索引1) + String headerRange = String.format("%d,0,%d,25", headerRowIndex, headerRowIndex); // 读取A到Z列(0-25) + log.info("读取表头 - Excel行号: {}, 索引行号: {}, range: {}", headerRow, headerRowIndex, headerRange); JSONObject headerData = null; try { @@ -566,7 +566,12 @@ public class TencentDocController extends BaseController { } // 读取数据行 - log.info("开始读取数据行 - 范围: {}", range); + // 构建数据行的range(从startRow到endRow,A列到Z列) + int startRowIndex = startRow - 1; // Excel行号转索引 + int endRowIndex = endRow - 1; // Excel行号转索引 + String range = String.format("%d,0,%d,25", startRowIndex, endRowIndex); // 读取A到Z列(0-25) + log.info("开始读取数据行 - Excel行号: {} ~ {}, 索引: {} ~ {}, range: {}", startRow, endRow, startRowIndex, endRowIndex, range); + JSONObject sheetData = null; try { sheetData = tencentDocService.readSheetData(accessToken, fileId, sheetId, range); diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java index 07ce9e0..8354476 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/TencentDocServiceImpl.java @@ -263,6 +263,16 @@ public class TencentDocServiceImpl implements ITencentDocService { log.info("API调用成功,原始返回结果: {}", result != null ? result.toJSONString() : "null"); + // 检查API响应中的错误码 + if (result != null && result.containsKey("code")) { + Integer code = result.getInteger("code"); + if (code != null && code != 0) { + String message = result.getString("message"); + log.error("腾讯文档API返回错误 - code: {}, message: {}", code, message); + throw new RuntimeException("腾讯文档API错误: " + message + " (code: " + code + ")"); + } + } + // 解析数据为统一的简单格式 JSONArray parsedValues = TencentDocDataParser.parseToSimpleArray(result); log.info("解析后的数据行数: {}", parsedValues != null ? parsedValues.size() : 0); diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocApiUtil.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocApiUtil.java index 274d0f9..e521e16 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocApiUtil.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocApiUtil.java @@ -336,12 +336,13 @@ public class TencentDocApiUtil { * @param openId 开放平台用户ID * @param fileId 文件ID(在线表格的唯一标识) * @param sheetId 工作表ID(可从表格链接中获取,如 ?tab=BB08J2 中的 BB08J2) - * @param range 范围,例如 "A1:Z100"(行列从0开始,遵循左闭右开原则) + * @param range 范围,格式:startRow,startColumn,endRow,endColumn(从0开始,例如:"0,0,10,26"表示A1到Z11) * @param apiBaseUrl API基础地址(默认:https://docs.qq.com/openapi/spreadsheet/v3) - * @return 表格数据(JSON格式,包含values数组) + * @return 表格数据(JSON格式,包含gridData) */ public static JSONObject readSheetData(String accessToken, String appId, String openId, String fileId, String sheetId, String range, String apiBaseUrl) { // V3版本API路径格式:/openapi/spreadsheet/v3/files/{fileId}/{sheetId}/{range} + // range格式:startRow,startColumn,endRow,endColumn(从0开始的索引) String apiUrl = String.format("%s/files/%s/%s/%s", apiBaseUrl, fileId, sheetId, range); log.info("读取表格数据 - fileId: {}, sheetId: {}, range: {}, apiUrl: {}", fileId, sheetId, range, apiUrl); return callApi(accessToken, appId, openId, apiUrl, "GET", null);