# 腾讯文档防重复写入 - 完整解决方案

## 🎯 问题背景

**原问题**：物流链接被重复写入到腾讯文档，导致同一订单的物流信息出现多次。

**根本原因**：
1. ❌ 没有持久化的推送状态标记
2. ❌ 用户可以多次点击推送按钮
3. ❌ 锁释放后仍可再次推送
4. ❌ 录单时自动推送，无人工确认

## ✅ 完整解决方案

### 核心机制（五重防护）

#### 🛡️ 第一重：订单表状态标记（持久化）
- 在 `jd_order` 表添加两个字段：
  - `tencent_doc_pushed`（0-未推送，1-已推送）
  - `tencent_doc_push_time`（推送时间）
- 推送成功后立即更新订单状态
- 再次推送前先检查状态，已推送则拒绝

#### 🔄 第五重：智能状态同步（新增）
- **批量同步时检测文档已有值**
- 如果文档中已有物流链接（可能手动填写）
- 但订单状态为"未推送"
- **自动同步订单状态为"已推送"**
- 保持订单状态与文档状态一致

#### 🔒 第二重：Redis分布式锁（防并发）
- 锁的粒度：`文档ID:工作表ID:订单单号`
- 30秒超时自动释放
- 同一订单同一时刻只能有一个请求处理

#### ✅ 第三重：写入前验证（防覆盖）
每次写入前都会：
1. 再次读取目标行
2. 验证单号是否匹配
3. 检查物流列是否为空
4. 任何一项不通过都拒绝写入

#### 📊 第四重：操作日志记录（可追溯）
- 所有操作记录到 `tencent_doc_operation_log` 表
- 记录成功/失败/跳过状态
- 记录操作人和时间
- 可以查询历史操作

### 录单行为变更

**旧行为**（已禁用）：
```java
// 录单时如果是H-TF，自动写入腾讯文档
if ("H-TF".equals(order.getDistributionMark())) {
    asyncWriteToTencentDoc(order);
}
```

**新行为**：
- ✅ 录单时**不再自动推送**
- ✅ 必须在订单列表**手动点击按钮**推送
- ✅ 推送前人工确认，避免误操作

## 🔄 智能状态同步机制

### 为什么需要智能同步？

在实际使用中，可能出现以下情况：
1. **手动填写**：有人直接在腾讯文档中手动填写了物流链接
2. **外部导入**：从其他系统导入数据到腾讯文档
3. **状态不一致**：订单状态显示"未推送"，但文档中已有值

### 智能同步的工作流程

```
批量同步读取腾讯文档
    ↓
发现某行的物流列已有值
    ↓
查询该订单的推送状态
    ↓
如果订单状态为"未推送"
    ↓
自动更新为"已推送"
    ↓
记录同步日志
    ↓
下次批量同步时就会跳过这个订单
```

### 同步效果

| 场景 | 订单状态 | 文档状态 | 系统行为 |
|------|---------|---------|---------|
| 正常推送 | 未推送 | 无值 | ✅ 写入物流链接，更新状态 |
| 手动填写后首次同步 | 未推送 | 有值 | ✅ **自动同步状态**，跳过写入 |
| 手动填写后再次同步 | 已推送 | 有值 | ✅ 跳过（订单状态已同步） |
| 重复推送尝试 | 已推送 | 有值 | ✅ 拒绝（订单已推送） |

### 日志示例

```
INFO  - ✓ 同步订单状态 - 单号: JY2025110329041, 行号: 123, 原因: 文档中已有物流链接（可能手动填写）
INFO  - 记录同步日志 - 操作类型: BATCH_SYNC, 状态: SKIPPED, 错误信息: 文档中已有物流链接，已同步订单状态
```

## 📋 使用流程

### 1. 首次推送
1. 在订单列表找到目标订单
2. 点击"推送物流"按钮
3. 系统检查：
   - ✅ 订单未推送过 → 执行推送
   - ✅ 推送成功 → 更新订单状态为"已推送"
   - ✅ 返回成功提示

### 2. 再次推送（默认拒绝）
1. 再次点击"推送物流"按钮
2. 系统检查：
   - ❌ 订单已推送 → 拒绝推送
   - 📝 提示："该订单已推送到腾讯文档（推送时间：2025-11-06 12:30:00），请勿重复操作！"

### 3. 强制重新推送（特殊情况）
如果需要重新推送（例如腾讯文档被误删），可以：
- 前端传递参数：`forceRePush: true`
- 系统会忽略"已推送"状态，重新执行推送

## 🔧 部署步骤

### Step 1: 执行SQL脚本（必须）

```bash
# 1. 添加订单表字段
mysql -u root -p your_database < doc/订单表添加腾讯文档推送标记.sql

# 2. 创建操作日志表
mysql -u root -p your_database < doc/腾讯文档操作日志表.sql
```

### Step 2: 重新编译部署

```bash
cd d:\code\RuoYi-Vue-master\ruoyi-java
mvn clean package -DskipTests
```

### Step 3: 重启服务

```bash
# 停止旧服务，启动新服务
```

## 🛡️ 安全保障

### 防止重复推送
| 机制 | 说明 | 效果 |
|------|------|------|
| 订单状态标记 | 持久化到数据库 | ✅ 永久防止重复（除非强制） |
| 智能状态同步 | 自动同步文档状态到订单 | ✅ 处理手动填写场景 |
| 分布式锁 | Redis锁，30秒超时 | ✅ 防止并发冲突 |
| 写入前验证 | 验证单号和物流列 | ✅ 防止写错行或覆盖 |
| 操作日志 | 记录所有操作 | ✅ 可追溯，可审计 |

### 防止覆盖已有数据
- ✅ 验证物流列是否为空
- ✅ 如果已有值，拒绝写入
- ✅ 返回错误提示："该订单物流链接已存在：xxx"

### 防止并发冲突
- ✅ Redis分布式锁
- ✅ 同一订单同时只能有一个请求处理
- ✅ 锁冲突时返回："该订单正在处理中，请稍后再试"

## 📊 数据库表结构

### 订单表新增字段

```sql
ALTER TABLE jd_order 
ADD COLUMN `tencent_doc_pushed` tinyint(1) DEFAULT 0 COMMENT '是否已推送到腾讯文档（0-未推送，1-已推送）',
ADD COLUMN `tencent_doc_push_time` datetime DEFAULT NULL COMMENT '推送到腾讯文档的时间';

CREATE INDEX idx_tencent_doc_pushed ON jd_order(tencent_doc_pushed, distribution_mark);
```

### 操作日志表

```sql
CREATE TABLE `tencent_doc_operation_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `file_id` varchar(100) DEFAULT NULL,
  `sheet_id` varchar(100) DEFAULT NULL,
  `operation_type` varchar(50) DEFAULT NULL COMMENT 'WRITE_SINGLE / BATCH_SYNC',
  `order_no` varchar(100) DEFAULT NULL,
  `target_row` int(11) DEFAULT NULL,
  `logistics_link` varchar(500) DEFAULT NULL,
  `operation_status` varchar(20) DEFAULT NULL COMMENT 'SUCCESS / FAILED / SKIPPED',
  `error_message` text,
  `operator` varchar(100) DEFAULT NULL,
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `remark` varchar(500) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_create_time` (`create_time`),
  KEY `idx_file_sheet` (`file_id`, `sheet_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

## 🔍 常见问题排查

### Q1: 订单明明没推送过，为什么提示"已推送"？

**排查方法**：
```sql
-- 查询订单的推送状态
SELECT third_party_order_no, tencent_doc_pushed, tencent_doc_push_time
FROM jd_order
WHERE third_party_order_no = 'JY2025110329041';
```

**解决方法**：
```sql
-- 如果确认是误标记，可以手动重置
UPDATE jd_order 
SET tencent_doc_pushed = 0, tencent_doc_push_time = NULL
WHERE third_party_order_no = 'JY2025110329041';
```

### Q2: 如何查看某个订单的推送历史？

```sql
-- 查询操作日志
SELECT * FROM tencent_doc_operation_log 
WHERE order_no = 'JY2025110329041' 
ORDER BY create_time DESC;
```

### Q3: 如何批量重置推送状态？

```sql
-- 谨慎操作！只在确认需要重新推送时使用
UPDATE jd_order 
SET tencent_doc_pushed = 0, tencent_doc_push_time = NULL
WHERE distribution_mark = 'H-TF' 
AND tencent_doc_pushed = 1;
```

### Q4: 如何查看最近失败的推送？

```sql
SELECT order_no, error_message, create_time, operator
FROM tencent_doc_operation_log 
WHERE operation_status = 'FAILED' 
AND create_time > DATE_SUB(NOW(), INTERVAL 1 DAY)
ORDER BY create_time DESC;
```

## 📞 前端对接说明

### API参数

```javascript
// 基本推送（默认，如果已推送则拒绝）
{
  "thirdPartyOrderNo": "JY2025110329041",
  "logisticsLink": "https://3.cn/2ume-Ak1"
}

// 强制推送（忽略已推送状态）
{
  "thirdPartyOrderNo": "JY2025110329041",
  "logisticsLink": "https://3.cn/2ume-Ak1",
  "forceRePush": true  // 特殊情况使用
}
```

### 返回结果

```javascript
// 成功
{
  "code": 200,
  "msg": "物流链接填充成功",
  "data": {
    "thirdPartyOrderNo": "JY2025110329041",
    "logisticsLink": "https://3.cn/2ume-Ak1",
    "row": 123,
    "column": 12,
    "pushed": true,
    "pushTime": "2025-11-06 12:30:00"
  }
}

// 失败（已推送）
{
  "code": 500,
  "msg": "该订单已推送到腾讯文档（推送时间：2025-11-06 12:30:00），请勿重复操作！如需重新推送，请使用强制推送功能。"
}
```

### 前端按钮建议

```javascript
// 推送按钮应该：
1. 根据订单的 tencentDocPushed 状态显示不同文本
   - 未推送：显示"推送物流"
   - 已推送：显示"已推送"（置灰或隐藏）

2. 提供"强制推送"选项（需二次确认）
   this.$confirm('该订单已推送，确定要重新推送吗？', '警告', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
     type: 'warning'
   }).then(() => {
     // 调用API，forceRePush: true
   });

3. 防止快速重复点击（前端防抖）
   methods: {
     handlePush: _.debounce(function() {
       // 调用API
     }, 1000, { leading: true, trailing: false })
   }
```

## ✅ 验证测试

### 测试场景 1：首次推送
1. 选择一个未推送的订单（`tencent_doc_pushed = 0`）
2. 点击"推送物流"
3. **预期**：推送成功，订单状态更新为"已推送"

### 测试场景 2：重复推送（默认拒绝）
1. 选择一个已推送的订单（`tencent_doc_pushed = 1`）
2. 点击"推送物流"
3. **预期**：拒绝推送，提示"已推送"

### 测试场景 3：强制推送
1. 选择一个已推送的订单
2. 勾选"强制推送"选项
3. 点击"推送物流"
4. **预期**：推送成功，更新推送时间

### 测试场景 4：并发推送
1. 同一订单，同时点击两次"推送物流"按钮
2. **预期**：只有一个请求成功，另一个提示"正在处理中"

### 测试场景 5：物流列非空
1. 手动在腾讯文档中填写物流链接
2. 点击"推送物流"
3. **预期**：拒绝推送，提示"物流链接已存在"

## 🎯 总结

### 彻底解决重复写入的核心

1. **持久化状态**（最关键）
   - 订单表增加 `tencent_doc_pushed` 字段
   - 推送成功后立即更新
   - 再次推送前先检查状态

2. **智能状态同步**（新增核心功能）
   - 批量同步时检测文档已有值
   - 自动同步订单状态为"已推送"
   - 处理手动填写、外部导入等场景
   - 保持订单状态与文档状态一致

3. **分布式锁**
   - 防止并发冲突
   - 同一订单同时只能一个请求处理

4. **写入前验证**
   - 验证单号匹配
   - 验证物流列为空
   - 防止写错行或覆盖

5. **操作日志**
   - 所有操作可追溯
   - 便于问题排查和审计

6. **录单不再自动触发**
   - 必须手动点击按钮
   - 人工确认，避免误操作

### 防护等级：⭐⭐⭐⭐⭐（最高）

现在即使：
- ✅ 用户多次点击 → 拒绝重复推送
- ✅ 并发请求 → 分布式锁防护
- ✅ 误操作 → 已推送则拒绝
- ✅ **别人手动填写文档** → **智能同步状态**
- ✅ 外部数据导入 → 自动检测并同步

**彻底解决所有重复写入场景！** 🎉

