1
This commit is contained in:
351
doc/腾讯文档API_Range格式说明.md
Normal file
351
doc/腾讯文档API_Range格式说明.md
Normal file
@@ -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" 错误
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user