This commit is contained in:
2025-11-06 20:32:16 +08:00
parent e865220a50
commit 3bf02de147
3 changed files with 492 additions and 13 deletions

View File

@@ -0,0 +1,431 @@
# 紧急修复:重复写入问题
## ❌ 严重问题
用户反馈:**完全重复写入了**
## 🔍 根本原因分析
### 问题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` 等其他类型
#### 辅助修复(建议)
✅ 重新查询订单再更新状态
✅ 检查更新结果
✅ 详细日志
### 重要性
这是一个**数据完整性严重问题**,必须立即修复!
**如果不修复**
- ❌ 每次批量同步都会重复写入
- ❌ 文档中数据越来越多
- ❌ 用户无法使用批量同步功能
- ❌ 手动填写的数据也会被重复写入
**修复后**
- ✅ 正确识别物流列已有值
- ✅ 跳过已有数据的行
- ✅ 不再重复写入
- ✅ 文档数据保持唯一
---
**修复完成后,请按照验证步骤仔细测试!特别要测试超链接类型的单元格!**

View File

@@ -1102,16 +1102,21 @@ public class TencentDocController extends BaseController {
// 订单未标记为已推送,但文档中已有值,同步状态 // 订单未标记为已推送,但文档中已有值,同步状态
existingOrder.setTencentDocPushed(1); existingOrder.setTencentDocPushed(1);
existingOrder.setTencentDocPushTime(new java.util.Date()); existingOrder.setTencentDocPushTime(new java.util.Date());
jdOrderService.updateJDOrder(existingOrder); int syncResult = jdOrderService.updateJDOrder(existingOrder);
log.info("✓ 同步订单状态 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)", if (syncResult > 0) {
log.info("✓ 同步订单状态成功 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)",
orderNo, excelRow); orderNo, excelRow);
} else {
log.warn("⚠️ 同步订单状态返回0 - 单号: {}, 行号: {}, 可能未更新",
orderNo, excelRow);
}
// 记录同步日志 // 记录同步日志
logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink, logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink,
"SKIPPED", "文档中已有物流链接,已同步订单状态"); "SKIPPED", "文档中已有物流链接,已同步订单状态");
} }
} catch (Exception e) { } catch (Exception e) {
log.error("同步订单状态失败 - 单号: {}, 行号: {}", orderNo, excelRow, e); log.error("同步订单状态失败 - 单号: {}, 行号: {}", orderNo, excelRow, e);
} }
skippedCount++; // 已有物流链接,跳过写入 skippedCount++; // 已有物流链接,跳过写入
@@ -1142,13 +1147,13 @@ public class TencentDocController extends BaseController {
if (order.getLogisticsLink() != null && !order.getLogisticsLink().trim().isEmpty()) { if (order.getLogisticsLink() != null && !order.getLogisticsLink().trim().isEmpty()) {
String logisticsLink = order.getLogisticsLink().trim(); String logisticsLink = order.getLogisticsLink().trim();
// 构建更新请求,同时保存订单对象用于后续更新推送状态 // 构建更新请求
JSONObject update = new JSONObject(); JSONObject update = new JSONObject();
update.put("row", excelRow); update.put("row", excelRow);
update.put("column", logisticsLinkColumn); update.put("column", logisticsLinkColumn);
update.put("orderNo", orderNo); update.put("orderNo", orderNo);
update.put("logisticsLink", logisticsLink); update.put("logisticsLink", logisticsLink);
update.put("order", order); // 保存订单对象 // 注意不再保存order对象写入成功后会重新查询以确保数据最新
updates.add(update); updates.add(update);
filledCount++; filledCount++;
@@ -1255,17 +1260,23 @@ public class TencentDocController extends BaseController {
log.info("✓ 写入成功 - 行: {}, 单号: {}, 物流链接: {}", row, expectedOrderNo, logisticsLink); log.info("✓ 写入成功 - 行: {}, 单号: {}, 物流链接: {}", row, expectedOrderNo, logisticsLink);
// 更新订单的推送状态 // 更新订单的推送状态(重新查询订单,避免使用旧对象)
try { try {
JDOrder orderToUpdate = (JDOrder) update.get("order"); JDOrder orderToUpdate = jdOrderService.selectJDOrderByThirdPartyOrderNo(expectedOrderNo);
if (orderToUpdate != null) { if (orderToUpdate != null) {
orderToUpdate.setTencentDocPushed(1); orderToUpdate.setTencentDocPushed(1);
orderToUpdate.setTencentDocPushTime(new java.util.Date()); orderToUpdate.setTencentDocPushTime(new java.util.Date());
jdOrderService.updateJDOrder(orderToUpdate); int updateResult = jdOrderService.updateJDOrder(orderToUpdate);
log.info("✓ 订单推送状态已更新 - 单号: {}", expectedOrderNo); if (updateResult > 0) {
log.info("✓ 订单推送状态已更新 - 单号: {}, updateResult: {}", expectedOrderNo, updateResult);
} else {
log.warn("⚠️ 订单推送状态更新返回0 - 单号: {}, 可能未更新", expectedOrderNo);
}
} else {
log.warn("⚠️ 重新查询订单失败 - 单号: {}", expectedOrderNo);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("更新订单推送状态失败(但写入腾讯文档已成功) - 单号: {}", expectedOrderNo, e); log.error("更新订单推送状态失败(但写入腾讯文档已成功) - 单号: {}", expectedOrderNo, e);
// 不影响主流程,继续执行 // 不影响主流程,继续执行
} }

View File

@@ -102,6 +102,7 @@ public class TencentDocDataParser {
/** /**
* 从单元格对象中提取文本内容 * 从单元格对象中提取文本内容
* 支持多种单元格类型text普通文本、link超链接
* *
* @param cell 单元格对象 * @param cell 单元格对象
* @return 文本内容(如果没有则返回空字符串) * @return 文本内容(如果没有则返回空字符串)
@@ -117,9 +118,45 @@ public class TencentDocDataParser {
return ""; return "";
} }
// 获取 text 字段 // 优先级1检查是否有 link 字段(超链接类型)
// 格式:{"cellValue": {"link": {"url": "xxx", "text": "xxx"}}}
JSONObject link = cellValue.getJSONObject("link");
if (link != null) {
// 优先返回 text如果没有则返回 url
String linkText = link.getString("text");
if (linkText != null && !linkText.isEmpty()) {
log.debug("提取link类型单元格text: {}", linkText);
return linkText;
}
String linkUrl = link.getString("url");
if (linkUrl != null && !linkUrl.isEmpty()) {
log.debug("提取link类型单元格url: {}", linkUrl);
return linkUrl;
}
}
// 优先级2检查是否有 text 字段(普通文本类型)
// 格式:{"cellValue": {"text": "xxx"}}
String text = cellValue.getString("text"); String text = cellValue.getString("text");
return text != null ? text : ""; if (text != null) {
return text;
}
// 优先级3检查其他可能的字段
// 数字类型
if (cellValue.containsKey("number")) {
Object number = cellValue.get("number");
return number != null ? number.toString() : "";
}
// 布尔类型
if (cellValue.containsKey("bool")) {
Object bool = cellValue.get("bool");
return bool != null ? bool.toString() : "";
}
// 如果都没有,返回空字符串
return "";
} }
/** /**