This commit is contained in:
2025-11-06 11:12:21 +08:00
parent 7860df5c2e
commit a7f581bdbe
5 changed files with 474 additions and 23 deletions

View File

@@ -0,0 +1,426 @@
# 腾讯文档 API 官方格式修复
## 修复日期
2025-11-05
## 问题来源
根据[腾讯文档官方 API 文档](https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html),发现之前对 Range 格式的理解有误。
---
## ✅ 官方规范
### 1. Range 格式A1 表示法
根据官方文档range 参数使用 **A1 表示法**Excel 格式),**不是**索引格式。
**官方示例**
```bash
curl 'https://docs.qq.com/openapi/spreadsheet/v3/files/ABCDE123abcde/BB08J2/A10:D11' \
--header 'Access-Token: {ACCESS_TOKEN}' \
--header 'Open-Id: {OPEN_ID}' \
--header 'Client-Id: {CLIENT_ID}'
```
**正确格式**
-`A10:D11` - Excel 格式A1 表示法)
-`A2:Z2` - 表头行
-`A3:Z203` - 数据行
-`1,0,1,25` - 索引格式(错误)
---
### 2. 响应结构data.gridData
根据官方文档,成功响应的结构为:
```json
{
"ret": 0,
"msg": "Succeed",
"data": {
"gridData": {
"columnMetadata": [],
"rowMetadata": [],
"rows": [
{
"values": [
{
"cellFormat": null,
"cellValue": {
"text": "单元格内容"
},
"dataType": "DATA_TYPE_UNSPECIFIED"
}
]
}
],
"startColumn": 0,
"startRow": 9
}
}
}
```
**关键要点**
- ✅ 数据在 `data.gridData` 下(有两层包装)
- ✅ 成功时 `ret: 0`
- ✅ 错误时 `code != 0`
---
### 3. API 限制
根据官方文档,查询范围有以下限制:
| 限制项 | 最大值 |
|-------|-------|
| 查询范围行数 | ≤ 1000 |
| 查询范围列数 | ≤ 200 |
| 范围内总单元格数 | ≤ 10000 |
**我们的范围**
- `A3:Z203`201行 × 26列 = 5226单元格 ✅ 符合限制
- `A2:Z2`1行 × 26列 = 26单元格 ✅ 符合限制
---
## 🔧 修复内容
### 修复 1Range 格式(回到 A1 表示法)
#### TencentDocApiUtil.java
**修改前**
```java
// range格式startRow,startColumn,endRow,endColumn从0开始的索引
```
**修改后**
```java
/**
* @param range 范围,使用 A1 表示法(如:"A10:D11", "A1:Z100"
* 根据官方文档https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html
*/
```
#### TencentDocController.java
**修改前**(错误):
```java
int headerRowIndex = headerRow - 1;
String headerRange = String.format("%d,0,%d,25", headerRowIndex, headerRowIndex);
// 结果:"1,0,1,25"
```
**修改后**(正确):
```java
String headerRange = String.format("A%d:Z%d", headerRow, headerRow);
// 结果:"A2:Z2"
```
---
### 修复 2响应结构解析支持 data.gridData
#### TencentDocDataParser.java
**新增支持**
```java
// 方式1检查是否有 data.gridData 字段官方V3 API格式
JSONObject data = apiResponse.getJSONObject("data");
if (data != null) {
JSONObject gridData = data.getJSONObject("gridData");
if (gridData != null) {
return parseGridData(gridData);
}
}
// 方式2检查是否有 gridData 字段(直接格式)
JSONObject gridData = apiResponse.getJSONObject("gridData");
if (gridData != null) {
return parseGridData(gridData);
}
// 方式3检查是否有 values 字段(简单格式)
JSONArray values = apiResponse.getJSONArray("values");
if (values != null) {
return values;
}
```
**兼容性**:支持三种响应格式
1. 官方格式:`{ret, msg, data: {gridData}}`
2. 简化格式:`{gridData}`
3. 自定义格式:`{values}`
---
### 修复 3错误响应检查
#### TencentDocServiceImpl.java
**新增检查**
```java
// 检查错误码code字段
if (result.containsKey("code")) {
Integer code = result.getInteger("code");
if (code != null && code != 0) {
String message = result.getString("message");
throw new RuntimeException("腾讯文档API错误: " + message + " (code: " + code + ")");
}
}
// 检查业务返回码ret字段
if (result.containsKey("ret")) {
Integer ret = result.getInteger("ret");
if (ret != null && ret != 0) {
String msg = result.getString("msg");
throw new RuntimeException("腾讯文档API业务错误: " + msg + " (ret: " + ret + ")");
}
}
```
**两种错误格式**
- 错误响应:`{code: 400001, message: "..."}`
- 业务错误:`{ret: 1, msg: "..."}`虽然官方成功是ret=0但可能存在业务错误
---
## 📊 修改对比表
| 项目 | 修改前 | 修改后 |
|------|--------|--------|
| Range格式 | `1,0,1,25` | `A2:Z2` ✅ |
| 表头range | `1,0,1,25` | `A2:Z2` ✅ |
| 数据range | `2,0,202,25` | `A3:Z203` ✅ |
| 响应解析 | 只支持 `gridData` | 支持 `data.gridData` ✅ |
| 错误检查 | 只检查 `code` | 同时检查 `code``ret` ✅ |
---
## 🎯 API 调用示例
### 完整的 API 请求
**读取表头第2行**
```
GET https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/A2:Z2
Headers:
Access-Token: {YOUR_ACCESS_TOKEN}
Client-Id: {YOUR_CLIENT_ID}
Open-Id: {YOUR_OPEN_ID}
```
**读取数据第3-203行**
```
GET https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/A3:Z203
Headers:
Access-Token: {YOUR_ACCESS_TOKEN}
Client-Id: {YOUR_CLIENT_ID}
Open-Id: {YOUR_OPEN_ID}
```
---
### 成功响应示例
```json
{
"ret": 0,
"msg": "Succeed",
"data": {
"gridData": {
"startRow": 1,
"startColumn": 0,
"rows": [
{
"values": [
{
"cellValue": {"text": "日期"},
"dataType": "DATA_TYPE_UNSPECIFIED"
},
{
"cellValue": {"text": "公司"},
"dataType": "DATA_TYPE_UNSPECIFIED"
},
{
"cellValue": {"text": "草号"},
"dataType": "DATA_TYPE_UNSPECIFIED"
}
]
}
]
}
}
}
```
---
### 错误响应示例
```json
{
"code": 400001,
"message": "Req Parameters Range Validate error",
"details": {
"DebugInfo": {
"traceId": "b92e6e2a1c1e4810bf8cfc70eabf7351"
}
},
"internalCode": 0
}
```
---
## 📝 修改文件清单
### 1. TencentDocApiUtil.java
- ✅ 更新 `readSheetData` 方法注释
- ✅ 说明 range 使用 A1 表示法
- ✅ 添加官方文档链接
### 2. TencentDocController.java
- ✅ 将 headerRange 改为 A1 格式
- ✅ 将 dataRange 改为 A1 格式
- ✅ 简化日志输出
### 3. TencentDocDataParser.java
- ✅ 支持 `data.gridData` 格式(官方格式)
- ✅ 保持对 `gridData``values` 的兼容
- ✅ 添加详细的调试日志
### 4. TencentDocServiceImpl.java
- ✅ 同时检查 `code``ret` 错误码
- ✅ 分别处理 API 错误和业务错误
- ✅ 添加官方文档链接
### 5. 新增文档
-`腾讯文档API_官方格式修复.md` - 本文档
---
## 🚀 测试验证
### 请求参数
```json
{
"accessToken": "YOUR_ACCESS_TOKEN",
"fileId": "DUW50RUprWXh2TGJK",
"sheetId": "BB08J2",
"headerRow": 2,
"orderNoColumn": 2,
"logisticsLinkColumn": 12
}
```
### 预期日志输出
```
读取表头 - 行号: 2, range: A2:Z2
读取数据行 - 行号: 3 ~ 203, range: A3:Z203
使用 data.gridData 格式解析
解析后的数据行数: 98
数据结构(共 98 行,显示前 3 行):
第 1 行15列: ["日期","公司","草号",...,"物流单号","标记"]
第 2 行15列: ["3月10日","","JY20251032904",...,"",""]
第 3 行15列: ["3月10日","","JY20250309184",...,"6649902864",""]
成功读取 98 行数据,开始处理...
```
### 预期结果
```json
{
"msg": "物流链接填充成功",
"code": 200,
"data": {
"startRow": 3,
"endRow": 203,
"filledCount": 10,
"skippedCount": 85,
"errorCount": 3,
"message": "成功填充10个物流链接"
}
}
```
---
## ⚠️ 重要提醒
### 1. Range 格式必须是 A1 表示法
**正确示例**
-`A1`
-`A1:Z1`
-`A2:Z2`
-`A3:Z203`
-`M3` (单个单元格)
**错误示例**
-`0,0,0,0` (索引格式)
-`1,0,1,25` (索引格式)
-`a1:z1` (小写,应该大写)
### 2. 响应格式有两种
**成功响应**
```json
{
"ret": 0,
"msg": "Succeed",
"data": { ... }
}
```
**错误响应**
```json
{
"code": 400001,
"message": "...",
"details": { ... }
}
```
### 3. API 限制
- 单次查询行数 ≤ 1000
- 单次查询列数 ≤ 200
- 总单元格数 ≤ 10000
如果超过限制,需要分批查询。
---
## 📚 官方文档链接
- [获取范围内的表格信息](https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html) ⭐⭐⭐
- [A1 表示法说明](https://docs.qq.com/open/document/app/openapi/v3/sheet/model/a1_notation.html)
- [在线表格资源描述](https://docs.qq.com/open/document/app/openapi/v3/sheet/model/spreadsheet.html)
- [批量更新接口](https://docs.qq.com/open/document/app/openapi/v3/sheet/batchupdate/update.html)
---
## ✅ 总结
### 关键修复点
1.**Range 格式**:从索引格式改回 A1 表示法Excel 格式)
2.**响应解析**:支持官方的 `data.gridData` 结构
3.**错误检查**:同时检查 `code``ret` 两种错误格式
4.**文档引用**:所有修改都基于官方文档
### 修改影响
- ✅ 完全符合官方 API 规范
- ✅ 向后兼容(支持多种响应格式)
- ✅ 更好的错误提示
- ✅ 详细的日志记录
---
**文档版本**1.0
**创建时间**2025-11-05
**依据**:腾讯文档开放平台官方 API 文档
**状态**:✅ 已修复并验证

View File

@@ -509,11 +509,10 @@ public class TencentDocController extends BaseController {
fileId, sheetId, startRow, endRow, lastMaxRow);
// 读取表格数据(先读取表头行用于识别列位置)
// 腾讯文档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);
// 根据官方文档,使用 A1 表示法Excel格式
// 参考https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html
String headerRange = String.format("A%d:Z%d", headerRow, headerRow); // 例如A2:Z2
log.info("读取表头 - 行号: {}, range: {}", headerRow, headerRange);
JSONObject headerData = null;
try {
@@ -566,11 +565,9 @@ public class TencentDocController extends BaseController {
}
// 读取数据行
// 构建数据行的range从startRow到endRowA列到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);
// 使用 A1 表示法Excel格式
String range = String.format("A%d:Z%d", startRow, endRow); // 例如A3:Z203
log.info("开始读取数据行 - 行号: {} ~ {}, range: {}", startRow, endRow, range);
JSONObject sheetData = null;
try {

View File

@@ -264,12 +264,26 @@ 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 + ")");
// 根据官方文档,成功响应包含 ret=0错误响应包含 code!=0
// 参考https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html
if (result != null) {
// 检查错误码code字段
if (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 + ")");
}
}
// 检查业务返回码ret字段
if (result.containsKey("ret")) {
Integer ret = result.getInteger("ret");
if (ret != null && ret != 0) {
String msg = result.getString("msg");
log.error("腾讯文档API业务错误 - ret: {}, msg: {}", ret, msg);
throw new RuntimeException("腾讯文档API业务错误: " + msg + " (ret: " + ret + ")");
}
}
}

View File

@@ -330,19 +330,20 @@ public class TencentDocApiUtil {
/**
* 读取表格数据 - V3 API
* 根据官方文档https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html
*
* @param accessToken 访问令牌
* @param appId 应用ID
* @param openId 开放平台用户ID
* @param fileId 文件ID在线表格的唯一标识
* @param sheetId 工作表ID可从表格链接中获取如 ?tab=BB08J2 中的 BB08J2
* @param range 范围,格式startRow,startColumn,endRow,endColumn从0开始例如"0,0,10,26"表示A1Z11
* @param range 范围,使用 A1 表示法(如:"A10:D11", "A1:Z100"
* @param apiBaseUrl API基础地址默认https://docs.qq.com/openapi/spreadsheet/v3
* @return 表格数据JSON格式包含gridData
* @return 表格数据JSON格式包含 data.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开始的索引
// range格式A1表示法Excel格式如 A10:D11
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);

View File

@@ -17,8 +17,9 @@ public class TencentDocDataParser {
/**
* 解析腾讯文档 V3 API 返回的数据为简单的二维数组格式
* 根据官方文档,响应格式为:{ "ret": 0, "msg": "Succeed", "data": { "gridData": {...} } }
*
* @param apiResponse API响应可能包含 gridData 或 values 字段)
* @param apiResponse API响应可能包含 data.gridData、gridData 或 values 字段)
* @return 二维数组格式的数据每行是一个JSONArray
*/
public static JSONArray parseToSimpleArray(JSONObject apiResponse) {
@@ -26,20 +27,32 @@ public class TencentDocDataParser {
return new JSONArray();
}
// 方式1检查是否有 gridData 字段V3 API格式)
// 方式1检查是否有 data.gridData 字段(官方V3 API格式
JSONObject data = apiResponse.getJSONObject("data");
if (data != null) {
JSONObject gridData = data.getJSONObject("gridData");
if (gridData != null) {
log.debug("使用 data.gridData 格式解析");
return parseGridData(gridData);
}
}
// 方式2检查是否有 gridData 字段(直接格式)
JSONObject gridData = apiResponse.getJSONObject("gridData");
if (gridData != null) {
log.debug("使用 gridData 格式解析");
return parseGridData(gridData);
}
// 方式2:检查是否有 values 字段(简单格式)
// 方式3:检查是否有 values 字段(简单格式)
JSONArray values = apiResponse.getJSONArray("values");
if (values != null) {
log.debug("使用 values 格式解析");
return values;
}
// 如果都没有,返回空数组
log.warn("API响应中既没有 gridData 也没有 values 字段,返回空数组");
log.warn("API响应中既没有 data.gridData、gridData 也没有 values 字段,返回空数组");
return new JSONArray();
}