432 lines
11 KiB
Markdown
432 lines
11 KiB
Markdown
# 紧急修复:重复写入问题
|
||
|
||
## ❌ 严重问题
|
||
|
||
用户反馈:**完全重复写入了**
|
||
|
||
## 🔍 根本原因分析
|
||
|
||
### 问题1:数据解析器无法识别超链接类型(最严重!)
|
||
|
||
**错误代码** (`TencentDocDataParser.java` 第120-122行):
|
||
```java
|
||
private static String extractCellText(JSONObject cell) {
|
||
// ...
|
||
// ❌ 只提取 text 字段,link类型提取不到!
|
||
String text = cellValue.getString("text");
|
||
return text != null ? text : "";
|
||
}
|
||
```
|
||
|
||
**单元格类型**:
|
||
- 普通文本:`cellValue = {"text": "xxx"}` ✅ 能提取
|
||
- **超链接**:`cellValue = {"link": {"url": "xxx", "text": "xxx"}}` ❌ **提取失败!**
|
||
|
||
**后果**:
|
||
1. 读取腾讯文档时,物流列(超链接类型)被解析为空字符串 `""`
|
||
2. 系统检查:`existingLogisticsLink == ""` → 认为物流列为空
|
||
3. 系统认为可以写入
|
||
4. **再次写入同一订单** → **重复写入!**
|
||
5. 文档中出现两行相同数据!
|
||
|
||
**验证**:
|
||
```
|
||
第一次写入:
|
||
- 物流列为空 ✅
|
||
- 写入物流链接(超链接类型)✅
|
||
- 文档中有1行 ✅
|
||
|
||
第二次批量同步:
|
||
- 读取数据,物流列被解析为 "" ❌
|
||
- 系统认为物流列为空 ❌
|
||
- 又写入了一次 ❌
|
||
- 文档中有2行!❌
|
||
```
|
||
|
||
### 问题2:订单状态更新使用旧对象
|
||
|
||
**错误代码**:
|
||
```java
|
||
// 批量同步中,在数据收集阶段保存order对象
|
||
update.put("order", order); // ❌ 保存的是旧对象
|
||
|
||
// 写入成功后,使用旧对象更新状态
|
||
JDOrder orderToUpdate = (JDOrder) update.get("order"); // ❌ 使用旧对象
|
||
orderToUpdate.setTencentDocPushed(1);
|
||
jdOrderService.updateJDOrder(orderToUpdate); // ❌ 可能失败或覆盖其他字段
|
||
```
|
||
|
||
**问题**:
|
||
1. 从数据收集到写入成功,可能间隔数秒甚至数十秒
|
||
2. 这期间订单可能被其他操作修改(如状态变更、金额更新等)
|
||
3. 使用旧对象更新会:
|
||
- ❌ 覆盖其他字段的最新值
|
||
- ❌ 可能因为乐观锁或版本号失败
|
||
- ❌ 导致 `tencentDocPushed` 字段更新失败
|
||
|
||
**后果**:
|
||
- 订单状态未更新为"已推送"
|
||
- 下次批量同步时,系统认为订单未推送
|
||
- **再次写入同一订单** → **重复写入!**
|
||
|
||
### 问题2:没有检查更新结果
|
||
|
||
**错误代码**:
|
||
```java
|
||
jdOrderService.updateJDOrder(orderToUpdate); // ❌ 没有检查返回值
|
||
log.info("✓ 订单推送状态已更新"); // ❌ 假设成功
|
||
```
|
||
|
||
**问题**:
|
||
- `updateJDOrder` 返回受影响的行数
|
||
- 如果返回0,说明更新失败
|
||
- 但代码没有检查,误以为更新成功
|
||
- 订单状态实际未更新 → **下次重复写入**
|
||
|
||
### 问题3:智能同步也存在同样问题
|
||
|
||
智能同步虽然查询是实时的,但也没有检查更新结果。
|
||
|
||
## ✅ 修复方案
|
||
|
||
### 修复1:增强数据解析器支持超链接类型(最关键!)
|
||
|
||
**新代码** (`TencentDocDataParser.java`):
|
||
```java
|
||
private static String extractCellText(JSONObject cell) {
|
||
// ...
|
||
JSONObject cellValue = cell.getJSONObject("cellValue");
|
||
|
||
// ✅ 优先级1:检查link字段(超链接类型)
|
||
JSONObject link = cellValue.getJSONObject("link");
|
||
if (link != null) {
|
||
String linkText = link.getString("text");
|
||
if (linkText != null && !linkText.isEmpty()) {
|
||
return linkText; // ✅ 能正确提取超链接文本
|
||
}
|
||
String linkUrl = link.getString("url");
|
||
if (linkUrl != null && !linkUrl.isEmpty()) {
|
||
return linkUrl; // ✅ 或返回url
|
||
}
|
||
}
|
||
|
||
// ✅ 优先级2:检查text字段(普通文本)
|
||
String text = cellValue.getString("text");
|
||
if (text != null) {
|
||
return text;
|
||
}
|
||
|
||
// ✅ 优先级3:支持number、bool等其他类型
|
||
// ...
|
||
|
||
return "";
|
||
}
|
||
```
|
||
|
||
**修复效果**:
|
||
```
|
||
第二次批量同步(修复后):
|
||
- 读取数据,物流列被解析为 "https://3.cn/xxx" ✅
|
||
- 系统检查:existingLogisticsLink != "" ✅
|
||
- 跳过写入 ✅
|
||
- 文档仍然只有1行 ✅
|
||
```
|
||
|
||
### 修复2:重新查询订单(关键)
|
||
|
||
```java
|
||
// ✅ 写入成功后,重新查询订单,确保数据最新
|
||
JDOrder orderToUpdate = jdOrderService.selectJDOrderByThirdPartyOrderNo(expectedOrderNo);
|
||
if (orderToUpdate != null) {
|
||
orderToUpdate.setTencentDocPushed(1);
|
||
orderToUpdate.setTencentDocPushTime(new Date());
|
||
int updateResult = jdOrderService.updateJDOrder(orderToUpdate);
|
||
|
||
// ✅ 检查更新结果
|
||
if (updateResult > 0) {
|
||
log.info("✓ 订单推送状态已更新");
|
||
} else {
|
||
log.warn("⚠️ 订单推送状态更新返回0,可能未更新");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 修复2:移除不必要的order对象保存
|
||
|
||
```java
|
||
// ❌ 旧代码
|
||
update.put("order", order); // 不再需要
|
||
|
||
// ✅ 新代码
|
||
// 不保存order对象,写入成功后重新查询
|
||
```
|
||
|
||
### 修复3:增强日志
|
||
|
||
```java
|
||
// ✅ 详细日志,便于排查
|
||
log.info("✓ 订单推送状态已更新 - 单号: {}, updateResult: {}", orderNo, updateResult);
|
||
log.warn("⚠️ 订单推送状态更新返回0 - 单号: {}, 可能未更新", orderNo);
|
||
log.error("❌ 更新订单推送状态失败 - 单号: {}", orderNo, e);
|
||
```
|
||
|
||
## 📊 修复前后对比
|
||
|
||
### 场景:批量同步100个订单
|
||
|
||
#### 修复前(有bug)
|
||
|
||
```
|
||
1. 读取100行数据
|
||
2. 收集100个订单对象(保存到updates)
|
||
3. 开始写入(10秒后)
|
||
4. 写入第1个订单成功
|
||
5. 使用10秒前的旧对象更新状态
|
||
6. 更新失败(对象已过期)或覆盖其他字段
|
||
7. 订单状态仍为"未推送"
|
||
8. 写入第2个订单...
|
||
...
|
||
|
||
下次批量同步:
|
||
1. 读取数据,发现第1个订单"未推送"
|
||
2. 再次写入第1个订单 ❌ 重复写入!
|
||
3. 再次写入第2个订单 ❌ 重复写入!
|
||
...
|
||
```
|
||
|
||
#### 修复后(正确)
|
||
|
||
```
|
||
1. 读取100行数据
|
||
2. 收集100个订单号(只保存必要信息)
|
||
3. 开始写入
|
||
4. 写入第1个订单成功
|
||
5. 重新查询第1个订单(最新数据)
|
||
6. 更新状态成功 ✅
|
||
7. 检查updateResult > 0 ✅
|
||
8. 订单状态更新为"已推送"
|
||
9. 写入第2个订单...
|
||
...
|
||
|
||
下次批量同步:
|
||
1. 读取数据,发现第1个订单"已推送"
|
||
2. 跳过第1个订单 ✅ 不重复!
|
||
3. 跳过第2个订单 ✅ 不重复!
|
||
...
|
||
```
|
||
|
||
## 🔧 修复的文件
|
||
|
||
### 核心修复(最重要)
|
||
|
||
1. **`TencentDocDataParser.java`** ⭐⭐⭐⭐⭐
|
||
- 行110-160:`extractCellText` 方法
|
||
- **增加超链接类型支持**
|
||
- 修复数据解析bug,彻底解决重复写入
|
||
|
||
### 次要修复
|
||
|
||
2. **`TencentDocController.java`**
|
||
- 行1258-1276:批量同步中的订单状态更新逻辑
|
||
- 行1098-1120:智能同步中的状态更新逻辑
|
||
- 行1150-1157:移除不必要的order对象保存
|
||
|
||
## ✅ 验证步骤
|
||
|
||
### Step 1: 清空测试数据
|
||
|
||
```sql
|
||
-- 重置所有订单的推送状态
|
||
UPDATE jd_order
|
||
SET tencent_doc_pushed = 0,
|
||
tencent_doc_push_time = NULL
|
||
WHERE distribution_mark = 'H-TF';
|
||
|
||
-- 清空操作日志
|
||
TRUNCATE TABLE tencent_doc_operation_log;
|
||
```
|
||
|
||
### Step 2: 第一次批量同步
|
||
|
||
```bash
|
||
# 预期:写入10个订单,所有订单状态更新为"已推送"
|
||
# 检查日志:
|
||
grep "✓ 订单推送状态已更新" application.log | wc -l # 应该是10
|
||
grep "⚠️ 订单推送状态更新返回0" application.log | wc -l # 应该是0
|
||
```
|
||
|
||
### Step 3: 检查订单状态
|
||
|
||
```sql
|
||
-- 应该有10个订单已推送
|
||
SELECT COUNT(*) FROM jd_order
|
||
WHERE distribution_mark = 'H-TF'
|
||
AND tencent_doc_pushed = 1; -- 应该返回10
|
||
```
|
||
|
||
### Step 4: 第二次批量同步
|
||
|
||
```bash
|
||
# 预期:跳过所有已推送的订单,不重复写入
|
||
# 检查日志:
|
||
grep "跳过已推送订单" application.log | wc -l # 应该是10
|
||
```
|
||
|
||
### Step 5: 检查腾讯文档
|
||
|
||
- 每个订单应该只出现一次
|
||
- **不应该有重复的物流链接**
|
||
|
||
### Step 6: 检查操作日志
|
||
|
||
```sql
|
||
-- 每个订单应该只有1条SUCCESS记录
|
||
SELECT order_no, COUNT(*) as count
|
||
FROM tencent_doc_operation_log
|
||
WHERE operation_status = 'SUCCESS'
|
||
GROUP BY order_no
|
||
HAVING COUNT(*) > 1; -- 应该返回0行
|
||
```
|
||
|
||
## 🚨 紧急部署
|
||
|
||
### 1. 先执行SQL(必须)
|
||
|
||
```bash
|
||
mysql -u root -p your_database < doc/订单表添加腾讯文档推送标记.sql
|
||
mysql -u root -p your_database < doc/腾讯文档操作日志表.sql
|
||
```
|
||
|
||
### 2. 重新编译
|
||
|
||
```bash
|
||
cd d:\code\RuoYi-Vue-master\ruoyi-java
|
||
mvn clean package -DskipTests
|
||
```
|
||
|
||
### 3. 立即重启服务
|
||
|
||
```bash
|
||
# 停止旧服务
|
||
# 部署新war/jar
|
||
# 启动新服务
|
||
```
|
||
|
||
### 4. 观察日志
|
||
|
||
```bash
|
||
tail -f application.log | grep -E "(✓|⚠️|❌)"
|
||
```
|
||
|
||
## 📝 监控要点
|
||
|
||
### 正常日志(修复后)
|
||
|
||
```
|
||
✓ 写入成功 - 行: 123, 单号: JY2025110329041, 物流链接: xxx
|
||
✓ 订单推送状态已更新 - 单号: JY2025110329041, updateResult: 1
|
||
```
|
||
|
||
### 异常日志(需要关注)
|
||
|
||
```
|
||
⚠️ 订单推送状态更新返回0 - 单号: JY2025110329041, 可能未更新
|
||
→ 检查数据库连接、订单是否存在
|
||
|
||
❌ 更新订单推送状态失败 - 单号: JY2025110329041
|
||
→ 检查异常堆栈,可能是数据库锁、约束等问题
|
||
```
|
||
|
||
## 💡 预防措施
|
||
|
||
### 1. 数据库层面
|
||
|
||
```sql
|
||
-- 添加唯一索引,防止重复单号(如果适用)
|
||
CREATE UNIQUE INDEX uk_third_party_order_no
|
||
ON jd_order(third_party_order_no);
|
||
|
||
-- 添加检查约束
|
||
ALTER TABLE jd_order
|
||
ADD CONSTRAINT ck_tencent_doc_pushed
|
||
CHECK (tencent_doc_pushed IN (0, 1));
|
||
```
|
||
|
||
### 2. 应用层面
|
||
|
||
- ✅ 始终重新查询订单再更新
|
||
- ✅ 检查更新结果
|
||
- ✅ 记录详细日志
|
||
- ✅ 定期检查操作日志表
|
||
|
||
### 3. 监控告警
|
||
|
||
```sql
|
||
-- 每小时检查是否有订单被重复写入
|
||
SELECT order_no, COUNT(*) as write_count
|
||
FROM tencent_doc_operation_log
|
||
WHERE operation_status = 'SUCCESS'
|
||
AND create_time > DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||
GROUP BY order_no
|
||
HAVING COUNT(*) > 1;
|
||
|
||
-- 如果有结果,发送告警
|
||
```
|
||
|
||
## 🎯 总结
|
||
|
||
### 根本原因(按重要性排序)
|
||
|
||
#### 1️⃣ 数据解析器无法识别超链接(最严重!)⭐⭐⭐⭐⭐
|
||
|
||
**问题**:`TencentDocDataParser.extractCellText()` 只提取 `cellValue.text`,对于超链接类型 `cellValue.link` 提取失败
|
||
|
||
**后果**:
|
||
- 读取数据时,物流列(超链接)被解析为空字符串
|
||
- 系统误认为物流列为空
|
||
- **重复写入同一订单!**
|
||
- **文档中出现多行相同数据!**
|
||
|
||
#### 2️⃣ 使用旧订单对象更新状态(严重)
|
||
|
||
**问题**:批量同步时保存旧订单对象,写入成功后使用旧对象更新状态
|
||
|
||
**后果**:
|
||
- 状态更新可能失败
|
||
- 订单状态未更新为"已推送"
|
||
- 下次批量同步时重复处理
|
||
|
||
### 解决方案
|
||
|
||
#### 核心修复(必须)
|
||
✅ **增强数据解析器支持超链接类型**
|
||
- 优先检查 `link` 字段
|
||
- 再检查 `text` 字段
|
||
- 支持 `number`、`bool` 等其他类型
|
||
|
||
#### 辅助修复(建议)
|
||
✅ 重新查询订单再更新状态
|
||
✅ 检查更新结果
|
||
✅ 详细日志
|
||
|
||
### 重要性
|
||
|
||
这是一个**数据完整性严重问题**,必须立即修复!
|
||
|
||
**如果不修复**:
|
||
- ❌ 每次批量同步都会重复写入
|
||
- ❌ 文档中数据越来越多
|
||
- ❌ 用户无法使用批量同步功能
|
||
- ❌ 手动填写的数据也会被重复写入
|
||
|
||
**修复后**:
|
||
- ✅ 正确识别物流列已有值
|
||
- ✅ 跳过已有数据的行
|
||
- ✅ 不再重复写入
|
||
- ✅ 文档数据保持唯一
|
||
|
||
---
|
||
|
||
**修复完成后,请按照验证步骤仔细测试!特别要测试超链接类型的单元格!**
|
||
|