1
This commit is contained in:
468
doc/腾讯文档API数据格式解析说明.md
Normal file
468
doc/腾讯文档API数据格式解析说明.md
Normal file
@@ -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 数据格式解析
|
||||||
|
|
||||||
Reference in New Issue
Block a user