From e865220a50ba73153f3b431fee15ea73fc4a1657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8D=92?= Date: Thu, 6 Nov 2025 20:18:15 +0800 Subject: [PATCH] 1 --- doc/智能状态同步机制说明.md | 274 ++++++++++++ doc/腾讯文档防重复写入-完整解决方案.md | 395 ++++++++++++++++++ doc/订单表添加腾讯文档推送标记.sql | 8 + .../jarvis/TencentDocController.java | 116 ++++- .../java/com/ruoyi/jarvis/domain/JDOrder.java | 6 + .../resources/mapper/jarvis/JDOrderMapper.xml | 6 + 6 files changed, 798 insertions(+), 7 deletions(-) create mode 100644 doc/智能状态同步机制说明.md create mode 100644 doc/腾讯文档防重复写入-完整解决方案.md create mode 100644 doc/订单表添加腾讯文档推送标记.sql diff --git a/doc/智能状态同步机制说明.md b/doc/智能状态同步机制说明.md new file mode 100644 index 0000000..6d09b92 --- /dev/null +++ b/doc/智能状态同步机制说明.md @@ -0,0 +1,274 @@ +# 智能状态同步机制 - 详细说明 + +## 📖 背景 + +在实际使用中,腾讯文档的物流链接可能通过多种方式填写: +1. **系统推送**:通过"推送物流"按钮自动填写 +2. **手动填写**:用户直接在文档中手动填写 +3. **外部导入**:从Excel等外部文件导入 +4. **协同编辑**:团队成员直接编辑文档 + +如果没有智能同步机制,会导致: +- ❌ 订单状态显示"未推送",但文档中已有值 +- ❌ 批量同步时重复查询这些订单 +- ❌ 增加数据库查询负担 +- ❌ 状态不一致,影响业务判断 + +## ✨ 智能同步机制 + +### 核心思路 + +**以腾讯文档的实际状态为准,自动同步到订单系统** + +``` +文档是最终展示层(实际填写状态) + ↓ +订单系统是管理层(推送状态记录) + ↓ +文档有值 + 订单未标记 = 状态不一致 + ↓ +智能同步:自动更新订单状态 +``` + +## 🔄 工作流程 + +### 场景1:系统推送(正常流程) + +``` +用户点击"推送物流" + ↓ +1. 检查订单状态:未推送 ✅ +2. 检查文档物流列:无值 ✅ + ↓ +写入物流链接到文档 + ↓ +更新订单状态为"已推送" + ↓ +记录操作日志:SUCCESS +``` + +**结果**:订单状态 ✅ 已推送 | 文档状态 ✅ 有值 + +--- + +### 场景2:手动填写后首次批量同步(智能同步触发) + +``` +某人手动在文档中填写物流链接 + ↓ +订单状态仍为"未推送"(因为是手动填写) + ↓ +批量同步开始 + ↓ +1. 读取文档数据 +2. 发现某行物流列已有值 +3. 查询该单号对应的订单 + ↓ +检测到状态不一致: + - 订单状态:未推送 ❌ + - 文档状态:有值 ✅ + ↓ +【智能同步触发】 + ↓ +自动更新订单状态为"已推送" + ↓ +记录同步日志:SKIPPED(文档中已有值,已同步订单状态) +``` + +**结果**:订单状态 ✅ 已推送 | 文档状态 ✅ 有值 + +--- + +### 场景3:手动填写后再次批量同步(无需同步) + +``` +批量同步开始 + ↓ +1. 读取文档数据 +2. 发现某行物流列已有值 +3. 查询该单号对应的订单 + ↓ +检测到状态一致: + - 订单状态:已推送 ✅(上次已同步) + - 文档状态:有值 ✅ + ↓ +直接跳过(无需同步) +``` + +**结果**:订单状态 ✅ 已推送 | 文档状态 ✅ 有值 + +--- + +### 场景4:用户尝试重复推送(拒绝) + +``` +用户点击"推送物流" + ↓ +1. 检查订单状态:已推送 ❌ + ↓ +拒绝推送 + ↓ +返回错误提示: +"该订单已推送到腾讯文档(推送时间:2025-11-06 12:30:00),请勿重复操作!" +``` + +**结果**:请求被拒绝,订单和文档状态保持不变 + +## 📊 状态同步矩阵 + +| 订单状态 | 文档物流列 | 用户操作 | 系统行为 | 最终状态 | +|---------|-----------|---------|---------|---------| +| 未推送 | 无值 | 单个推送 | ✅ 写入物流链接,更新订单状态 | 已推送 + 有值 | +| 未推送 | 无值 | 批量同步 | ✅ 写入物流链接,更新订单状态 | 已推送 + 有值 | +| 未推送 | **有值** | 单个推送 | ❌ 拒绝(文档已有值) | 未推送 + 有值 | +| 未推送 | **有值** | 批量同步 | ✅ **智能同步订单状态** | **已推送 + 有值** | +| 已推送 | 有值 | 单个推送 | ❌ 拒绝(订单已推送) | 已推送 + 有值 | +| 已推送 | 有值 | 批量同步 | ✅ 跳过(订单已推送) | 已推送 + 有值 | +| 已推送 | 无值 | 单个推送 | ❌ 拒绝(订单已推送) | 已推送 + 无值 | +| 已推送 | 无值 | 批量同步 | ✅ 跳过(订单已推送) | 已推送 + 无值 | + +**重点场景**:第4行 - 未推送 + 有值 + 批量同步 = **智能同步** + +## 🎯 核心优势 + +### 1. 自动化 +- ✅ 无需人工干预 +- ✅ 批量同步时自动检测 +- ✅ 自动修正状态不一致 + +### 2. 高效性 +- ✅ 同步后下次批量同步会跳过 +- ✅ 减少数据库查询 +- ✅ 减少不必要的状态检查 + +### 3. 可追溯 +- ✅ 记录同步操作日志 +- ✅ 标记同步原因:"文档中已有物流链接(可能手动填写)" +- ✅ 便于审计和问题排查 + +### 4. 兼容性 +- ✅ 兼容手动填写 +- ✅ 兼容外部导入 +- ✅ 兼容协同编辑 +- ✅ 兼容各种数据来源 + +## 📝 代码实现(简化版) + +```java +// 批量同步时,检查文档物流列 +String existingLogisticsLink = row.getString(logisticsLinkColumn); +if (existingLogisticsLink != null && !existingLogisticsLink.trim().isEmpty()) { + // 文档中已有物流链接,检查订单状态 + JDOrder existingOrder = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo); + + if (existingOrder != null && + (existingOrder.getTencentDocPushed() == null || + existingOrder.getTencentDocPushed() == 0)) { + + // 状态不一致,触发智能同步 + existingOrder.setTencentDocPushed(1); + existingOrder.setTencentDocPushTime(new Date()); + jdOrderService.updateJDOrder(existingOrder); + + log.info("✓ 同步订单状态 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)", + orderNo, excelRow); + + // 记录同步日志 + logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, + existingLogisticsLink, "SKIPPED", "文档中已有物流链接,已同步订单状态"); + } + + skippedCount++; // 跳过写入 + continue; +} +``` + +## 🔍 日志示例 + +### 智能同步触发 + +``` +2025-11-06 14:30:15 INFO - 批量同步开始 - 范围:第3-202行 +2025-11-06 14:30:16 INFO - 发现物流列已有值 - 单号: JY2025110329041, 行号: 123 +2025-11-06 14:30:16 INFO - 检测到状态不一致 - 订单状态: 未推送, 文档状态: 有值 +2025-11-06 14:30:16 INFO - ✓ 同步订单状态 - 单号: JY2025110329041, 行号: 123, 原因: 文档中已有物流链接(可能手动填写) +2025-11-06 14:30:16 INFO - 记录同步日志 - 操作类型: BATCH_SYNC, 状态: SKIPPED +``` + +### 操作日志表记录 + +```sql +INSERT INTO tencent_doc_operation_log ( + file_id, sheet_id, operation_type, order_no, target_row, + logistics_link, operation_status, error_message, operator, create_time +) VALUES ( + 'DUW50RUprWXh2TGJK', 'BB08J2', 'BATCH_SYNC', 'JY2025110329041', 123, + 'https://3.cn/2ume-Ak1', 'SKIPPED', '文档中已有物流链接,已同步订单状态', + 'admin', '2025-11-06 14:30:16' +); +``` + +## 🛠️ 排查与维护 + +### 查询智能同步记录 + +```sql +-- 查询所有智能同步操作 +SELECT order_no, target_row, logistics_link, create_time, operator +FROM tencent_doc_operation_log +WHERE operation_status = 'SKIPPED' +AND error_message LIKE '%文档中已有物流链接,已同步订单状态%' +ORDER BY create_time DESC; +``` + +### 查询状态不一致的订单(理论上应该为0) + +```sql +-- 如果有记录,说明智能同步未触发或失败 +SELECT o.third_party_order_no, o.tencent_doc_pushed, o.logistics_link +FROM jd_order o +WHERE o.logistics_link IS NOT NULL +AND o.logistics_link != '' +AND (o.tencent_doc_pushed IS NULL OR o.tencent_doc_pushed = 0); +``` + +### 手动修正状态不一致 + +```sql +-- 如果发现状态不一致,可以手动修正 +UPDATE jd_order +SET tencent_doc_pushed = 1, + tencent_doc_push_time = NOW() +WHERE logistics_link IS NOT NULL +AND logistics_link != '' +AND (tencent_doc_pushed IS NULL OR tencent_doc_pushed = 0); +``` + +## ✅ 最佳实践 + +1. **定期批量同步** + - 建议每天运行一次批量同步 + - 自动修正所有状态不一致 + +2. **监控同步日志** + - 定期检查 `SKIPPED` 状态的日志 + - 分析手动填写的频率和模式 + +3. **培训团队成员** + - 告知团队手动填写会被系统自动同步 + - 建议优先使用系统推送功能 + +4. **备份重要数据** + - 定期备份腾讯文档 + - 定期备份订单数据库 + +## 🎉 总结 + +智能状态同步机制确保了: +- ✅ **订单状态** 与 **文档实际状态** 始终保持一致 +- ✅ 兼容多种数据来源(系统推送、手动填写、外部导入) +- ✅ 减少重复查询,提高系统效率 +- ✅ 所有同步操作可追溯,便于审计 + +**这是一个真正智能的、自适应的状态管理机制!** 🚀 + diff --git a/doc/腾讯文档防重复写入-完整解决方案.md b/doc/腾讯文档防重复写入-完整解决方案.md new file mode 100644 index 0000000..0d08c3a --- /dev/null +++ b/doc/腾讯文档防重复写入-完整解决方案.md @@ -0,0 +1,395 @@ +# 腾讯文档防重复写入 - 完整解决方案 + +## 🎯 问题背景 + +**原问题**:物流链接被重复写入到腾讯文档,导致同一订单的物流信息出现多次。 + +**根本原因**: +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. **录单不再自动触发** + - 必须手动点击按钮 + - 人工确认,避免误操作 + +### 防护等级:⭐⭐⭐⭐⭐(最高) + +现在即使: +- ✅ 用户多次点击 → 拒绝重复推送 +- ✅ 并发请求 → 分布式锁防护 +- ✅ 误操作 → 已推送则拒绝 +- ✅ **别人手动填写文档** → **智能同步状态** +- ✅ 外部数据导入 → 自动检测并同步 + +**彻底解决所有重复写入场景!** 🎉 + diff --git a/doc/订单表添加腾讯文档推送标记.sql b/doc/订单表添加腾讯文档推送标记.sql new file mode 100644 index 0000000..ba00b3f --- /dev/null +++ b/doc/订单表添加腾讯文档推送标记.sql @@ -0,0 +1,8 @@ +-- 给订单表添加腾讯文档推送标记字段 +ALTER TABLE jd_order +ADD COLUMN `tencent_doc_pushed` tinyint(1) DEFAULT 0 COMMENT '是否已推送到腾讯文档(0-未推送,1-已推送)' AFTER `logistics_link`, +ADD COLUMN `tencent_doc_push_time` datetime DEFAULT NULL COMMENT '推送到腾讯文档的时间' AFTER `tencent_doc_pushed`; + +-- 添加索引,方便查询未推送的订单 +CREATE INDEX idx_tencent_doc_pushed ON jd_order(tencent_doc_pushed, distribution_mark); + 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 ddaf209..354bea4 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 @@ -467,6 +467,7 @@ public class TencentDocController extends BaseController { // 1. 获取参数 thirdPartyOrderNo = (String) params.get("thirdPartyOrderNo"); String logisticsLink = (String) params.get("logisticsLink"); + Boolean forceRePush = params.get("forceRePush") != null ? (Boolean) params.get("forceRePush") : false; if (thirdPartyOrderNo == null || thirdPartyOrderNo.isEmpty()) { return AjaxResult.error("第三方单号不能为空"); @@ -475,7 +476,24 @@ public class TencentDocController extends BaseController { return AjaxResult.error("物流链接不能为空"); } - // 2. 获取访问令牌 + // 2. 检查订单是否已推送(防止重复推送) + JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(thirdPartyOrderNo); + if (order == null) { + logOperation(null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + "FAILED", "订单不存在"); + return AjaxResult.error("订单不存在:" + thirdPartyOrderNo); + } + + // 如果已推送且不是强制重推,则拒绝 + if (order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 && !forceRePush) { + String pushTimeStr = order.getTencentDocPushTime() != null ? + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(order.getTencentDocPushTime()) : "未知"; + logOperation(null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + "SKIPPED", String.format("订单已推送,推送时间: %s", pushTimeStr)); + return AjaxResult.error(String.format("该订单已推送到腾讯文档(推送时间:%s),请勿重复操作!如需重新推送,请使用强制推送功能。", pushTimeStr)); + } + + // 3. 获取访问令牌 String accessToken; try { accessToken = tencentDocTokenService.getValidAccessToken(); @@ -710,11 +728,24 @@ public class TencentDocController extends BaseController { log.info("✓ 物流链接写入成功 - 单号: {}, 行: {}, 链接: {}", thirdPartyOrderNo, targetRow, logisticsLink); + // 13. 更新订单的推送状态(标记为已推送) + try { + order.setTencentDocPushed(1); + order.setTencentDocPushTime(new java.util.Date()); + jdOrderService.updateJDOrder(order); + log.info("✓ 订单推送状态已更新 - 单号: {}", thirdPartyOrderNo); + } catch (Exception e) { + log.error("更新订单推送状态失败,但写入腾讯文档已成功", e); + // 不影响主流程,继续返回成功 + } + JSONObject result = new JSONObject(); result.put("thirdPartyOrderNo", thirdPartyOrderNo); result.put("logisticsLink", logisticsLink); result.put("row", targetRow); result.put("column", logisticsColumn); + result.put("pushed", true); + result.put("pushTime", order.getTencentDocPushTime()); return AjaxResult.success("物流链接填充成功", result); @@ -817,6 +848,10 @@ public class TencentDocController extends BaseController { Boolean.valueOf(params.get("forceStart").toString()) : false; Integer forceStartRow = params.get("forceStartRow") != null ? Integer.valueOf(params.get("forceStartRow").toString()) : configStartRow; + + // 新增参数:是否跳过已推送的订单(默认true,防止重复推送) + Boolean skipPushedOrders = params.get("skipPushedOrders") != null ? + Boolean.valueOf(params.get("skipPushedOrders").toString()) : true; if (accessToken == null || fileId == null || sheetId == null) { return AjaxResult.error("文档配置不完整,请先配置 fileId 和 sheetId"); @@ -1056,10 +1091,30 @@ public class TencentDocController extends BaseController { orderNo = orderNo.trim(); - // 检查物流链接列是否已有值 + // 检查物流链接列是否已有值(可能是别人手动填写的) String existingLogisticsLink = row.getString(logisticsLinkColumn); if (existingLogisticsLink != null && !existingLogisticsLink.trim().isEmpty()) { - skippedCount++; // 已有物流链接,跳过 + // 文档中已有物流链接,同步更新订单的推送状态 + try { + JDOrder existingOrder = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo); + if (existingOrder != null && + (existingOrder.getTencentDocPushed() == null || existingOrder.getTencentDocPushed() == 0)) { + // 订单未标记为已推送,但文档中已有值,同步状态 + existingOrder.setTencentDocPushed(1); + existingOrder.setTencentDocPushTime(new java.util.Date()); + jdOrderService.updateJDOrder(existingOrder); + log.info("✓ 同步订单状态 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)", + orderNo, excelRow); + + // 记录同步日志 + logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink, + "SKIPPED", "文档中已有物流链接,已同步订单状态"); + } + } catch (Exception e) { + log.error("同步订单状态失败 - 单号: {}, 行号: {}", orderNo, excelRow, e); + } + + skippedCount++; // 已有物流链接,跳过写入 continue; } @@ -1067,22 +1122,42 @@ public class TencentDocController extends BaseController { // 根据第三方单号查询订单 JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo); - if (order != null && order.getLogisticsLink() != null && !order.getLogisticsLink().trim().isEmpty()) { + if (order == null) { + errorCount++; + log.warn("未找到订单 - 单号: {}, 行号: {}", orderNo, excelRow); + continue; + } + + // 检查订单是否已推送(防止重复推送) + if (skipPushedOrders && order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1) { + skippedCount++; + log.info("跳过已推送订单 - 单号: {}, 推送时间: {}, 行号: {}", + orderNo, + order.getTencentDocPushTime() != null ? + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(order.getTencentDocPushTime()) : "未知", + excelRow); + continue; + } + + 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); // 保存订单对象 updates.add(update); filledCount++; - log.info("找到订单物流链接 - 单号: {}, 物流链接: {}, 行号: {}", orderNo, logisticsLink, excelRow); + log.info("找到订单物流链接 - 单号: {}, 物流链接: {}, 行号: {}, 已推送: {}", + orderNo, logisticsLink, excelRow, + order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 ? "是" : "否"); } else { errorCount++; - log.warn("未找到订单或物流链接为空 - 单号: {}, 行号: {}", orderNo, excelRow); + log.warn("订单物流链接为空 - 单号: {}, 行号: {}", orderNo, excelRow); } } catch (Exception e) { errorCount++; @@ -1179,9 +1254,33 @@ public class TencentDocController extends BaseController { successUpdates++; log.info("✓ 写入成功 - 行: {}, 单号: {}, 物流链接: {}", row, expectedOrderNo, logisticsLink); + + // 更新订单的推送状态 + try { + JDOrder orderToUpdate = (JDOrder) update.get("order"); + if (orderToUpdate != null) { + orderToUpdate.setTencentDocPushed(1); + orderToUpdate.setTencentDocPushTime(new java.util.Date()); + jdOrderService.updateJDOrder(orderToUpdate); + log.info("✓ 订单推送状态已更新 - 单号: {}", expectedOrderNo); + } + } catch (Exception e) { + log.error("更新订单推送状态失败(但写入腾讯文档已成功) - 单号: {}", expectedOrderNo, e); + // 不影响主流程,继续执行 + } + + // 记录操作日志 + logOperation(fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, logisticsLink, + "SUCCESS", null); } catch (Exception e) { log.error("写入数据失败 - 行: {}", entry.getKey(), e); errorCount++; + + // 记录失败日志 + String orderNo = entry.getValue().getString("orderNo"); + String logisticsLink = entry.getValue().getString("logisticsLink"); + logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, entry.getKey(), logisticsLink, + "FAILED", "写入异常: " + e.getMessage()); } // 添加延迟,避免API调用频率过高 @@ -1245,16 +1344,19 @@ public class TencentDocController extends BaseController { result.put("errorCount", errorCount); result.put("orderNoColumn", orderNoColumn); result.put("logisticsLinkColumn", logisticsLinkColumn); + result.put("skipPushedOrders", skipPushedOrders); // 是否跳过已推送订单 // 构建详细的提示信息 String message = String.format( "✓ 同步完成:成功填充 %d 条,跳过 %d 条,错误 %d 条\n" + " 本次范围:第 %d-%d 行\n" + " 当前进度:第 %d 行\n" + + " 防重推送:%s\n" + " %s", filledCount, skippedCount, errorCount, startRow, currentMaxRow, currentMaxRow, + skipPushedOrders ? "已启用(跳过已推送订单)" : "已禁用", nextSyncHint ); result.put("message", message); diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java index 877863e..b0718f2 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java @@ -50,6 +50,12 @@ public class JDOrder extends BaseEntity { @Excel(name = "物流链接") private String logisticsLink; + /** 是否已推送到腾讯文档(0-未推送,1-已推送) */ + private Integer tencentDocPushed; + + /** 推送到腾讯文档的时间 */ + private Date tencentDocPushTime; + /** 订单号 */ @Excel(name = "订单号", cellType = ColumnType.STRING) private String orderId; diff --git a/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml b/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml index ff4a1a1..5de78d7 100644 --- a/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml @@ -12,6 +12,8 @@ + + @@ -97,10 +99,12 @@ insert into jd_order ( remark, distribution_mark, model_number, link, payment_amount, rebate_amount, address, logistics_link, + tencent_doc_pushed, tencent_doc_push_time, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price ) values ( #{remark}, #{distributionMark}, #{modelNumber}, #{link}, #{paymentAmount}, #{rebateAmount}, #{address}, #{logisticsLink}, + 0, null, #{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice} ) @@ -116,6 +120,8 @@ rebate_amount = #{rebateAmount}, address = #{address}, logistics_link = #{logisticsLink}, + tencent_doc_pushed = #{tencentDocPushed}, + tencent_doc_push_time = #{tencentDocPushTime}, order_id = #{orderId}, buyer = #{buyer}, order_time = #{orderTime},