# 物流链接自动填充 - 完整逻辑说明

## 📋 功能概述

自动读取腾讯文档表格中的订单信息，根据**第三方单号**（thirdPartyOrderNo）在数据库中查询对应的物流链接，并自动填充到表格的"物流单号"列。

---

## 🔄 完整执行流程

### 第一步：读取表头（识别列位置）

```
输入参数：
- fileId: 文件ID
- sheetId: 工作表ID  
- headerRow: 表头所在行号（默认：2）

执行逻辑：
1. 构建 range：A{headerRow}:Z{headerRow}（例如：A2:Z2）
2. 调用 API 读取表头数据
3. 解析表头，识别关键列的位置：
   - "单号" 列（orderNoColumn）→ 这是第三方单号列
   - "物流单号" 列（logisticsLinkColumn）→ 要写入物流信息的列
```

**示例日志**：
```
读取表头 - 行号: 2, range: A2:Z2
解析后的数据行数: 1
第 1 行（26列）: ["日期","公司","单号","型号","数量",...,"物流单号","","标记",...]
识别到关键列：
  - "单号" 列位置: 第 3 列（索引2）
  - "物流单号" 列位置: 第 13 列（索引12）
```

---

### 第二步：读取数据行（批量处理）

```
输入参数：
- startRow: 起始行号（表头行 + 1，默认：3）
- endRow: 结束行号（startRow + 49，每次读取50行）

执行逻辑：
1. 构建 range：A{startRow}:Z{endRow}（例如：A3:Z52）
2. 调用 API 读取数据行
3. 如果读取失败（range超出实际范围），返回"没有更多数据"
4. 如果成功，继续第三步
```

**示例日志**：
```
开始读取数据行 - 行号: 3 ~ 52, range: A3:Z52
解析后的数据行数: 50
```

**⚠️ 重要限制**：
- A1 表示法的 range **不能超出表格实际数据区域**
- 如果表格只有 100 行，`A3:Z200` 会报错：`invalid param error: 'range' invalid`
- 因此每次只读取 **50 行**，避免超出范围

---

### 第三步：逐行处理（匹配+填充）

```
对于每一行数据：

1. 提取第三方单号
   - 从 orderNoColumn（"单号"列）取值
   - 例如：row[2] = "JY202506181808"

2. 数据库查询
   - 调用：jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo)
   - 查询条件：third_party_order_no = "JY202506181808"
   - 返回：JDOrder 对象（包含 logisticsLink 字段）

3. 检查物流链接
   - 如果 order == null：跳过，errorCount++
   - 如果 logisticsLink 为空：跳过，errorCount++
   - 如果 logisticsLink 不为空：继续第4步

4. 写入物流链接
   - 目标单元格：第 {当前行号} 行，第 {logisticsLinkColumn} 列
   - 例如：第 3 行，第 13 列（M3）
   - 调用 batchUpdate API 写入物流链接
   - 成功：filledCount++
   - 失败：errorCount++
```

**示例日志**：
```
正在处理订单 - 单号: JY202506181808, 行号: 3, 列: 3
查询到订单物流链接: https://m.kuaidi100.com/result.jsp?nu=6649902864
准备写入 - 目标单元格: 第 3 行, 第 13 列（索引12）
写入成功 - 单号: JY202506181808
```

---

### 第四步：更新进度（Redis缓存）

```
执行逻辑：
1. 记录本次处理的最大行号：lastMaxRow = endRow
2. 保存到 Redis：key = "tencent:doc:lastMaxRow:{fileId}:{sheetId}"
3. 下次执行时，从 lastMaxRow 开始继续处理
```

**进度管理**：
- 首次执行：startRow = 3, endRow = 52
- 第二次执行：startRow = 52, endRow = 101
- 第三次执行：startRow = 101, endRow = 150
- ...以此类推，直到所有数据处理完毕

---

## 📊 关键数据结构

### 1. 表格结构

| 列号 | 列名 | 数据示例 | 用途 |
|------|------|----------|------|
| A (0) | 日期 | 3月10日 | - |
| B (1) | 公司 | XX公司 | - |
| **C (2)** | **单号** | **JY202506181808** | **用于查询数据库** |
| D (3) | 型号 | iPhone 14 | - |
| E (4) | 数量 | 1 | - |
| ... | ... | ... | - |
| **M (12)** | **物流单号** | **6649902864** | **要填充的目标列** |

### 2. 数据库表结构（jd_order）

| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| **third_party_order_no** | varchar | **第三方单号（唯一索引）** | **JY202506181808** |
| **logistics_link** | varchar | **物流链接** | **https://m.kuaidi100.com/...** |
| order_no | varchar | 京东单号 | JD123456789 |
| order_status | int | 订单状态 | 1 |
| ... | ... | ... | ... |

### 3. API 请求示例

**读取表头**：
```http
GET https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/A2:Z2
Headers:
  Access-Token: {ACCESS_TOKEN}
  Client-Id: {CLIENT_ID}
  Open-Id: {OPEN_ID}
```

**读取数据行**：
```http
GET https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/BB08J2/A3:Z52
Headers:
  Access-Token: {ACCESS_TOKEN}
  Client-Id: {CLIENT_ID}
  Open-Id: {OPEN_ID}
```

**写入物流链接**（单个单元格）：
```http
POST https://docs.qq.com/openapi/spreadsheet/v3/files/DUW50RUprWXh2TGJK/batchUpdate
Headers:
  Access-Token: {ACCESS_TOKEN}
  Client-Id: {CLIENT_ID}
  Open-Id: {OPEN_ID}
  Content-Type: application/json

Body:
{
  "requests": [
    {
      "updateCells": {
        "range": {
          "sheetId": "BB08J2",
          "startRowIndex": 2,      // 第3行（索引2）
          "endRowIndex": 3,        // 不包含
          "startColumnIndex": 12,  // 第13列（索引12）
          "endColumnIndex": 13     // 不包含
        },
        "rows": [
          {
            "values": [
              {
                "cellValue": {
                  "text": "6649902864"
                }
              }
            ]
          }
        ]
      }
    }
  ]
}
```

---

## 🎯 核心逻辑图

```
┌─────────────────────────────────────────────────────────┐
│  1. 读取表头（A2:Z2）                                      │
│     → 识别"单号"列位置（orderNoColumn）                     │
│     → 识别"物流单号"列位置（logisticsLinkColumn）            │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  2. 读取数据行（A3:Z52，每次50行）                          │
│     → 解析为二维数组：[[row1], [row2], ...]                │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  3. 逐行处理                                              │
│     FOR EACH row IN dataRows:                           │
│       a. 提取单号：orderNo = row[orderNoColumn]          │
│       b. 查询数据库：                                      │
│          order = DB.query(third_party_order_no=orderNo) │
│       c. 检查物流链接：                                    │
│          IF order != null AND logistics_link != null:   │
│            → 写入表格：                                   │
│              cell[rowIndex][logisticsLinkColumn]        │
│              = order.logistics_link                     │
│            → filledCount++                              │
│          ELSE:                                          │
│            → errorCount++                               │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│  4. 更新进度                                              │
│     → Redis.set("lastMaxRow", endRow)                   │
│     → 返回统计信息：filledCount, errorCount, skippedCount │
└─────────────────────────────────────────────────────────┘
```

---

## ⚙️ 配置参数

### 接口请求参数

```json
{
  "accessToken": "用户访问令牌（必填）",
  "fileId": "文件ID（必填）",
  "sheetId": "工作表ID（必填）",
  "headerRow": 2,              // 表头行号（可选，默认2）
  "orderNoColumn": 2,          // 单号列位置（可选，自动识别）
  "logisticsLinkColumn": 12    // 物流列位置（可选，自动识别）
}
```

### 自动识别列位置

如果不传 `orderNoColumn` 和 `logisticsLinkColumn`，系统会自动识别：

```java
// 在表头行中查找匹配的列名
for (int i = 0; i < headerRow.size(); i++) {
    String cellValue = headerRow.getString(i);
    if ("单号".equals(cellValue)) {
        orderNoColumn = i;  // 找到"单号"列
    }
    if ("物流单号".equals(cellValue)) {
        logisticsLinkColumn = i;  // 找到"物流单号"列
    }
}
```

---

## 📈 返回结果示例

### 成功响应

```json
{
  "msg": "物流链接填充成功",
  "code": 200,
  "data": {
    "startRow": 3,
    "endRow": 52,
    "lastMaxRow": 52,
    "filledCount": 45,      // 成功填充45个
    "skippedCount": 3,      // 跳过3个（已有物流信息）
    "errorCount": 2,        // 失败2个（未找到订单）
    "message": "成功填充45个物流链接，跳过3个，失败2个"
  }
}
```

### 没有更多数据

```json
{
  "msg": "没有需要处理的数据",
  "code": 200,
  "data": {
    "startRow": 103,
    "endRow": 152,
    "lastMaxRow": 100,       // 上次处理到第100行
    "filledCount": 0,
    "skippedCount": 0,
    "errorCount": 0,
    "message": "指定范围内没有数据"
  }
}
```

### 错误响应

```json
{
  "msg": "读取数据行失败: invalid param error: 'range' invalid",
  "code": 500
}
```

---

## 🔧 关键代码片段

### 1. 根据第三方单号查询订单

```java
// Controller层
String orderNo = row.getString(orderNoColumn);  // 从"单号"列取值
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo);

// Service层
@Override
public JDOrder selectJDOrderByThirdPartyOrderNo(String thirdPartyOrderNo) {
    return jdOrderMapper.selectJDOrderByThirdPartyOrderNo(thirdPartyOrderNo);
}

// Mapper.xml
<select id="selectJDOrderByThirdPartyOrderNo" parameterType="string" resultMap="JDOrderResult">
    SELECT * FROM jd_order 
    WHERE third_party_order_no = #{thirdPartyOrderNo}
    LIMIT 1
</select>
```

### 2. 写入物流链接到指定单元格

```java
// 构建更新请求
JSONObject updateRequest = new JSONObject();
JSONObject updateCells = new JSONObject();

// 指定目标单元格范围（第excelRow行，第logisticsLinkColumn列）
JSONObject range = new JSONObject();
range.put("sheetId", sheetId);
range.put("startRowIndex", excelRow - 1);           // Excel行号转索引
range.put("endRowIndex", excelRow);                 // 不包含
range.put("startColumnIndex", logisticsLinkColumn); // 列索引
range.put("endColumnIndex", logisticsLinkColumn + 1); // 不包含

// 设置单元格值
JSONArray rows = new JSONArray();
JSONObject rowData = new JSONObject();
JSONArray values = new JSONArray();
JSONObject cellData = new JSONObject();
JSONObject cellValue = new JSONObject();
cellValue.put("text", logisticsLink);  // 物流链接文本
cellData.put("cellValue", cellValue);
values.add(cellData);
rowData.put("values", values);
rows.add(rowData);

updateCells.put("range", range);
updateCells.put("rows", rows);
updateRequest.put("updateCells", updateCells);

// 调用 batchUpdate API
tencentDocService.batchUpdate(accessToken, fileId, updateRequest);
```

---

## ⚠️ 注意事项

### 1. Range 格式限制

✅ **正确格式**（A1 表示法）：
- `A2:Z2` - 表头行
- `A3:Z52` - 数据行（50行）
- `M3` - 单个单元格

❌ **错误格式**：
- `1,0,1,25` - 索引格式（已废弃）
- `A3:Z203` - 超出实际数据范围（会报错）

### 2. API 限制

根据官方文档：
- 查询范围行数 ≤ 1000
- 查询范围列数 ≤ 200
- 范围内总单元格数 ≤ 10000

### 3. 批处理策略

- **每次读取 50 行**：避免超出表格实际范围
- **分批处理**：通过 Redis 记录进度，支持多次执行
- **错误重试**：如果某一行写入失败，记录错误但继续处理下一行

### 4. 数据库字段映射

⚠️ **关键对应关系**：
- 表格中的"单号" → 数据库中的 `third_party_order_no`
- **不是** `order_no`（京东单号）
- **不是** `remark`（备注）

### 5. 空值处理

- 如果单元格为空，`row.getString(index)` 返回空字符串 `""`
- 如果行不存在，`values.size()` 会小于预期列数
- 需要做好空值判断和边界检查

---

## 🧪 测试验证

### 测试请求

```bash
curl -X POST 'http://localhost:30313/jarvis/tencentDoc/fillLogisticsByOrderNo' \
  -H 'Content-Type: application/json' \
  -d '{
    "accessToken": "YOUR_ACCESS_TOKEN",
    "fileId": "DUW50RUprWXh2TGJK",
    "sheetId": "BB08J2",
    "headerRow": 2
  }'
```

### 预期日志

```
读取表头 - 行号: 2, range: A2:Z2
解析后的数据行数: 1
第 1 行（26列）: ["日期","公司","单号",...,"物流单号","","标记",...]

开始读取数据行 - 行号: 3 ~ 52, range: A3:Z52
解析后的数据行数: 50

正在处理订单 - 单号: JY202506181808, 行号: 3
查询到订单物流链接: https://m.kuaidi100.com/result.jsp?nu=6649902864
写入成功 - 单号: JY202506181808

正在处理订单 - 单号: JY202506181809, 行号: 4
未找到订单 - 单号: JY202506181809

...

填充完成 - 成功: 45, 跳过: 3, 失败: 2
```

---

## 📚 相关文档

- [腾讯文档官方 API 文档](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/batchupdate/update.html)

---

**文档版本**：1.0  
**创建时间**：2025-11-05  
**状态**：✅ 已完成

