diff --git a/doc/紧急修复-重复写入问题.md b/doc/紧急修复-重复写入问题.md new file mode 100644 index 0000000..4bd95c9 --- /dev/null +++ b/doc/紧急修复-重复写入问题.md @@ -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` 等其他类型 + +#### 辅助修复(建议) +✅ 重新查询订单再更新状态 +✅ 检查更新结果 +✅ 详细日志 + +### 重要性 + +这是一个**数据完整性严重问题**,必须立即修复! + +**如果不修复**: +- ❌ 每次批量同步都会重复写入 +- ❌ 文档中数据越来越多 +- ❌ 用户无法使用批量同步功能 +- ❌ 手动填写的数据也会被重复写入 + +**修复后**: +- ✅ 正确识别物流列已有值 +- ✅ 跳过已有数据的行 +- ✅ 不再重复写入 +- ✅ 文档数据保持唯一 + +--- + +**修复完成后,请按照验证步骤仔细测试!特别要测试超链接类型的单元格!** + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java index 354bea4..8ca24ad 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/TencentDocController.java @@ -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); // 不影响主流程,继续执行 } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java index 815fba6..d90aa51 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/util/TencentDocDataParser.java @@ -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 ""; } /**