1
This commit is contained in:
431
doc/紧急修复-重复写入问题.md
Normal file
431
doc/紧急修复-重复写入问题.md
Normal 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` 等其他类型
|
||||
|
||||
#### 辅助修复(建议)
|
||||
✅ 重新查询订单再更新状态
|
||||
✅ 检查更新结果
|
||||
✅ 详细日志
|
||||
|
||||
### 重要性
|
||||
|
||||
这是一个**数据完整性严重问题**,必须立即修复!
|
||||
|
||||
**如果不修复**:
|
||||
- ❌ 每次批量同步都会重复写入
|
||||
- ❌ 文档中数据越来越多
|
||||
- ❌ 用户无法使用批量同步功能
|
||||
- ❌ 手动填写的数据也会被重复写入
|
||||
|
||||
**修复后**:
|
||||
- ✅ 正确识别物流列已有值
|
||||
- ✅ 跳过已有数据的行
|
||||
- ✅ 不再重复写入
|
||||
- ✅ 文档数据保持唯一
|
||||
|
||||
---
|
||||
|
||||
**修复完成后,请按照验证步骤仔细测试!特别要测试超链接类型的单元格!**
|
||||
|
||||
@@ -1102,16 +1102,21 @@ public class TencentDocController extends BaseController {
|
||||
// 订单未标记为已推送,但文档中已有值,同步状态
|
||||
existingOrder.setTencentDocPushed(1);
|
||||
existingOrder.setTencentDocPushTime(new java.util.Date());
|
||||
jdOrderService.updateJDOrder(existingOrder);
|
||||
log.info("✓ 同步订单状态 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)",
|
||||
orderNo, excelRow);
|
||||
int syncResult = jdOrderService.updateJDOrder(existingOrder);
|
||||
if (syncResult > 0) {
|
||||
log.info("✓ 同步订单状态成功 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)",
|
||||
orderNo, excelRow);
|
||||
} else {
|
||||
log.warn("⚠️ 同步订单状态返回0 - 单号: {}, 行号: {}, 可能未更新",
|
||||
orderNo, excelRow);
|
||||
}
|
||||
|
||||
// 记录同步日志
|
||||
logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink,
|
||||
"SKIPPED", "文档中已有物流链接,已同步订单状态");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("同步订单状态失败 - 单号: {}, 行号: {}", orderNo, excelRow, e);
|
||||
log.error("❌ 同步订单状态失败 - 单号: {}, 行号: {}", orderNo, excelRow, e);
|
||||
}
|
||||
|
||||
skippedCount++; // 已有物流链接,跳过写入
|
||||
@@ -1142,13 +1147,13 @@ public class TencentDocController extends BaseController {
|
||||
if (order.getLogisticsLink() != null && !order.getLogisticsLink().trim().isEmpty()) {
|
||||
String logisticsLink = order.getLogisticsLink().trim();
|
||||
|
||||
// 构建更新请求,同时保存订单对象用于后续更新推送状态
|
||||
// 构建更新请求
|
||||
JSONObject update = new JSONObject();
|
||||
update.put("row", excelRow);
|
||||
update.put("column", logisticsLinkColumn);
|
||||
update.put("orderNo", orderNo);
|
||||
update.put("logisticsLink", logisticsLink);
|
||||
update.put("order", order); // 保存订单对象
|
||||
// 注意:不再保存order对象,写入成功后会重新查询以确保数据最新
|
||||
updates.add(update);
|
||||
|
||||
filledCount++;
|
||||
@@ -1255,17 +1260,23 @@ public class TencentDocController extends BaseController {
|
||||
|
||||
log.info("✓ 写入成功 - 行: {}, 单号: {}, 物流链接: {}", row, expectedOrderNo, logisticsLink);
|
||||
|
||||
// 更新订单的推送状态
|
||||
// 更新订单的推送状态(重新查询订单,避免使用旧对象)
|
||||
try {
|
||||
JDOrder orderToUpdate = (JDOrder) update.get("order");
|
||||
JDOrder orderToUpdate = jdOrderService.selectJDOrderByThirdPartyOrderNo(expectedOrderNo);
|
||||
if (orderToUpdate != null) {
|
||||
orderToUpdate.setTencentDocPushed(1);
|
||||
orderToUpdate.setTencentDocPushTime(new java.util.Date());
|
||||
jdOrderService.updateJDOrder(orderToUpdate);
|
||||
log.info("✓ 订单推送状态已更新 - 单号: {}", expectedOrderNo);
|
||||
int updateResult = jdOrderService.updateJDOrder(orderToUpdate);
|
||||
if (updateResult > 0) {
|
||||
log.info("✓ 订单推送状态已更新 - 单号: {}, updateResult: {}", expectedOrderNo, updateResult);
|
||||
} else {
|
||||
log.warn("⚠️ 订单推送状态更新返回0 - 单号: {}, 可能未更新", expectedOrderNo);
|
||||
}
|
||||
} else {
|
||||
log.warn("⚠️ 重新查询订单失败 - 单号: {}", expectedOrderNo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新订单推送状态失败(但写入腾讯文档已成功) - 单号: {}", expectedOrderNo, e);
|
||||
log.error("❌ 更新订单推送状态失败(但写入腾讯文档已成功) - 单号: {}", expectedOrderNo, e);
|
||||
// 不影响主流程,继续执行
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ public class TencentDocDataParser {
|
||||
|
||||
/**
|
||||
* 从单元格对象中提取文本内容
|
||||
* 支持多种单元格类型:text(普通文本)、link(超链接)等
|
||||
*
|
||||
* @param cell 单元格对象
|
||||
* @return 文本内容(如果没有则返回空字符串)
|
||||
@@ -117,9 +118,45 @@ public class TencentDocDataParser {
|
||||
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");
|
||||
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 "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user