From 00f0c3867226484d3590a6292ecd73ac7b5c3b92 Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 6 Jan 2026 22:52:24 +0800 Subject: [PATCH] 1 --- .../jarvis/TencentDocController.java | 530 +++++++++--------- 1 file changed, 265 insertions(+), 265 deletions(-) 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 77ac620..af7a3a0 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 @@ -329,7 +329,7 @@ public class TencentDocController extends BaseController { return AjaxResult.error("上传物流信息失败: " + e.getMessage()); } } - + /** * 将单个订单的物流信息追加到表格 @@ -458,7 +458,7 @@ public class TencentDocController extends BaseController { /** * 直接填充单个订单的物流链接 - * + * * @param params 包含 thirdPartyOrderNo(第三方单号)和 logisticsLink(物流链接) * @return 填充结果 */ @@ -468,54 +468,54 @@ public class TencentDocController extends BaseController { String fileId = null; String sheetId = null; String lockKey = null; - + try { // 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("第三方单号不能为空"); } if (logisticsLink == null || logisticsLink.isEmpty()) { return AjaxResult.error("物流链接不能为空"); } - + // 2. 检查订单是否已推送(防止重复推送) JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(thirdPartyOrderNo); if (order == null) { - logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, 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 ? + String pushTimeStr = order.getTencentDocPushTime() != null ? new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(order.getTencentDocPushTime()) : "未知"; - logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, 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(); } catch (Exception e) { - logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "访问令牌无效: " + e.getMessage()); return AjaxResult.error("访问令牌无效,请先完成授权"); } - + // 3. 从配置中读取文档信息 final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:"; fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId"); sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId"); Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow"); Integer configStartRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "startRow"); - + if (fileId == null || fileId.isEmpty()) { fileId = tencentDocConfig.getFileId(); } @@ -528,56 +528,56 @@ public class TencentDocController extends BaseController { if (configStartRow == null) { configStartRow = tencentDocConfig.getStartRow(); } - + if (fileId == null || fileId.isEmpty() || sheetId == null || sheetId.isEmpty()) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "文档配置不完整"); return AjaxResult.error("文档配置不完整,请先完成配置"); } - + // 4. 获取分布式锁(针对该订单单号+文档+工作表) lockKey = TENCENT_DOC_LOCK_KEY + fileId + ":" + sheetId + ":" + thirdPartyOrderNo; - + // 检查锁是否已存在 String existingLock = redisCache.getCacheObject(lockKey); boolean lockAcquired = (existingLock == null); - + if (!lockAcquired) { log.warn("获取锁失败,该订单正在被其他请求处理:{}", thirdPartyOrderNo); - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "获取分布式锁失败,该订单正在处理中"); return AjaxResult.error("该订单正在处理中,请稍后再试"); } - + // 设置锁 redisCache.setCacheObject(lockKey, "locked", 30, TimeUnit.SECONDS); log.info("✓ 获取分布式锁成功 - 单号: {}, lockKey: {}", thirdPartyOrderNo, lockKey); - + // 5. 读取表头(从配置的 headerRow 读取,与 startRow 独立) String headerRange = String.format("A%d:Z%d", headerRow, headerRow); - + JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange); if (headerData == null || !headerData.containsKey("gridData")) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "无法读取表头数据"); return AjaxResult.error("无法读取表头数据"); } - + // 6. 解析表头,找到"单号"和"物流单号"列 JSONObject gridData = headerData.getJSONObject("gridData"); JSONArray rows = gridData.getJSONArray("rows"); if (rows == null || rows.isEmpty()) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "表头数据为空"); return AjaxResult.error("表头数据为空"); } - + JSONObject headerRowData = rows.getJSONObject(0); JSONArray headerCells = headerRowData.getJSONArray("values"); - + int orderNoColumn = -1; int logisticsColumn = -1; - + for (int i = 0; i < headerCells.size(); i++) { JSONObject cell = headerCells.getJSONObject(i); if (cell.containsKey("cellValue")) { @@ -591,40 +591,40 @@ public class TencentDocController extends BaseController { } } } - + if (orderNoColumn == -1 || logisticsColumn == -1) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "未找到'单号'或'物流'列"); return AjaxResult.error("未找到'单号'或'物流'列,请检查表头配置"); } - + log.info("表头解析完成 - 单号列: {}, 物流列: {}", orderNoColumn, logisticsColumn); - + // 7. 读取数据区域,查找指定单号 String dataRange = String.format("A%d:Z%d", configStartRow, configStartRow + 999); JSONObject data = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange); - + if (data == null || !data.containsKey("gridData")) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "无法读取数据区域"); return AjaxResult.error("无法读取数据区域"); } - + JSONObject dataGridData = data.getJSONObject("gridData"); JSONArray dataRows = dataGridData.getJSONArray("rows"); - + if (dataRows == null || dataRows.isEmpty()) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "数据区域为空"); return AjaxResult.error("数据区域为空"); } - + // 8. 查找匹配的单号 int targetRow = -1; for (int i = 0; i < dataRows.size(); i++) { JSONObject row = dataRows.getJSONObject(i); JSONArray cells = row.getJSONArray("values"); - + if (cells != null && cells.size() > orderNoColumn) { JSONObject orderNoCell = cells.getJSONObject(orderNoColumn); if (orderNoCell.containsKey("cellValue")) { @@ -636,86 +636,86 @@ public class TencentDocController extends BaseController { } } } - + if (targetRow == -1) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink, "FAILED", "未找到单号"); return AjaxResult.error("未找到单号: " + thirdPartyOrderNo); } - + log.info("找到单号 {} 在第 {} 行", thirdPartyOrderNo, targetRow); - + // 9. 再次读取该行数据,验证单号和物流链接列是否为空(防止并发覆盖) String verifyRange = String.format("A%d:Z%d", targetRow, targetRow); JSONObject verifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, verifyRange); - + if (verifyData == null || !verifyData.containsKey("gridData")) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "FAILED", "验证读取失败"); return AjaxResult.error("验证读取失败"); } - + JSONObject verifyGridData = verifyData.getJSONObject("gridData"); JSONArray verifyRows = verifyGridData.getJSONArray("rows"); - + if (verifyRows == null || verifyRows.isEmpty()) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "FAILED", "验证行数据为空"); return AjaxResult.error("验证行数据为空"); } - + JSONObject verifyRowData = verifyRows.getJSONObject(0); JSONArray verifyCells = verifyRowData.getJSONArray("values"); - + // 验证单号是否仍然匹配 if (verifyCells == null || verifyCells.size() <= orderNoColumn) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "SKIPPED", "验证时单号列为空,行已变化"); return AjaxResult.error("验证失败:行数据已变化,单号列为空"); } - + JSONObject verifyOrderNoCell = verifyCells.getJSONObject(orderNoColumn); String verifyOrderNo = null; if (verifyOrderNoCell.containsKey("cellValue")) { verifyOrderNo = verifyOrderNoCell.getJSONObject("cellValue").getString("text"); } - + if (!thirdPartyOrderNo.equals(verifyOrderNo)) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "SKIPPED", String.format("验证失败:单号不匹配,期望 %s,实际 %s", thirdPartyOrderNo, verifyOrderNo)); return AjaxResult.error("验证失败:单号不匹配,行数据已变化"); } - + // 验证物流链接列是否为空 if (verifyCells.size() > logisticsColumn) { JSONObject logisticsCell = verifyCells.getJSONObject(logisticsColumn); if (logisticsCell.containsKey("cellValue")) { String existingLogistics = logisticsCell.getJSONObject("cellValue").getString("text"); if (existingLogistics != null && !existingLogistics.trim().isEmpty()) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "SKIPPED", String.format("物流链接列已有值:%s", existingLogistics)); return AjaxResult.error(String.format("该订单物流链接已存在:%s", existingLogistics)); } } } - + log.info("✓ 验证通过 - 单号匹配且物流列为空,可以写入"); - + // 10. 获取用户信息(获取 openId) JSONObject userInfo = com.ruoyi.jarvis.util.TencentDocApiUtil.getUserInfo(accessToken); JSONObject userData = userInfo.getJSONObject("data"); if (userData == null) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "FAILED", "无法获取用户数据"); return AjaxResult.error("无法获取用户数据"); } String openId = userData.getString("openID"); if (openId == null || openId.isEmpty()) { - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "FAILED", "无法获取Open-Id"); return AjaxResult.error("无法获取Open-Id"); } - + // 11. 写入物流链接 com.ruoyi.jarvis.util.TencentDocApiUtil.writeSheetData( accessToken, @@ -727,13 +727,13 @@ public class TencentDocController extends BaseController { logisticsLink, tencentDocConfig.getApiBaseUrl() ); - + // 12. 记录成功日志 - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink, "SUCCESS", null); - + log.info("✓ 物流链接写入成功 - 单号: {}, 行: {}, 链接: {}", thirdPartyOrderNo, targetRow, logisticsLink); - + // 13. 更新订单的推送状态(标记为已推送) try { order.setTencentDocPushed(1); @@ -744,24 +744,24 @@ public class TencentDocController extends BaseController { log.error("更新订单推送状态失败,但写入腾讯文档已成功", e); // 不影响主流程,继续返回成功 } - + // 14. 发送微信推送通知(手动执行) try { Map successLog = new java.util.HashMap<>(); successLog.put("orderNo", thirdPartyOrderNo); successLog.put("row", targetRow); successLog.put("logisticsLink", logisticsLink); - + List> successLogs = new java.util.ArrayList<>(); successLogs.add(successLog); - + // 手动执行时,batchId为null sendWeChatNotification(successLogs, 1, 0, 0, null, fileId, sheetId, true); } catch (Exception e) { log.error("发送微信推送失败", e); // 不影响主流程,继续返回成功 } - + JSONObject result = new JSONObject(); result.put("thirdPartyOrderNo", thirdPartyOrderNo); result.put("logisticsLink", logisticsLink); @@ -769,12 +769,12 @@ public class TencentDocController extends BaseController { result.put("column", logisticsColumn); result.put("pushed", true); result.put("pushTime", order.getTencentDocPushTime()); - + return AjaxResult.success("物流链接填充成功", result); - + } catch (Exception e) { log.error("填充物流链接失败", e); - logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, null, + logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, null, "FAILED", "异常: " + e.getMessage()); return AjaxResult.error("填充物流链接失败: " + e.getMessage()); } finally { @@ -789,12 +789,12 @@ public class TencentDocController extends BaseController { } } } - + /** * 记录操作日志到数据库 */ - private void logOperation(String batchId, String fileId, String sheetId, String operationType, - String orderNo, Integer targetRow, String logisticsLink, + private void logOperation(String batchId, String fileId, String sheetId, String operationType, + String orderNo, Integer targetRow, String logisticsLink, String status, String errorMessage) { try { com.ruoyi.jarvis.domain.TencentDocOperationLog log = new com.ruoyi.jarvis.domain.TencentDocOperationLog(); @@ -832,7 +832,7 @@ public class TencentDocController extends BaseController { * 查询最近的操作日志 */ @GetMapping("/recentLogs") - public AjaxResult getRecentLogs(@RequestParam(required = false) String fileId, + public AjaxResult getRecentLogs(@RequestParam(required = false) String fileId, @RequestParam(defaultValue = "50") int limit) { try { List list = operationLogMapper.selectRecentLogs(fileId, limit); @@ -846,7 +846,7 @@ public class TencentDocController extends BaseController { /** * 从文本中提取手机号码 * 支持11位手机号码,可能包含空格、横线等分隔符 - * + * * @param text 文本信息(可以是备注、状态等字段) * @return 提取到的手机号码,如果没有则返回null */ @@ -857,7 +857,7 @@ public class TencentDocController extends BaseController { } log.debug("原始文本: [{}]", text); - + // 移除所有空格、横线、括号等分隔符 String cleanedText = text.replaceAll("[\\s\\-\\(\\)\\[\\]()\\【\\】]", ""); log.debug("清理后文本: [{}]", cleanedText); @@ -875,7 +875,7 @@ public class TencentDocController extends BaseController { log.debug("未找到匹配的手机号码,文本: [{}]", text); return null; } - + /** * 批量同步物流链接 - 读取表格数据,根据单号查询订单系统中的物流链接,并填充到表格 * 优化:记录上次处理的最大行数,每次从最大行数-100开始读取,避免重复处理历史数据 @@ -902,11 +902,11 @@ public class TencentDocController extends BaseController { // 从参数获取批次ID(如果是批量调用会传入) String batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null; - + // 从参数或配置中获取文档信息 String fileId = (String) params.get("fileId"); String sheetId = (String) params.get("sheetId"); - + // 如果前端没有传fileId/sheetId,则从配置中读取 final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:"; if (fileId == null || fileId.isEmpty()) { @@ -921,11 +921,11 @@ public class TencentDocController extends BaseController { sheetId = tencentDocConfig.getSheetId(); } } - + // 从配置中读取表头行和数据起始行 Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow"); Integer configStartRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "startRow"); - + if (headerRow == null) { headerRow = tencentDocConfig.getHeaderRow(); } @@ -938,7 +938,7 @@ 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; @@ -946,25 +946,25 @@ public class TencentDocController extends BaseController { if (accessToken == null || fileId == null || sheetId == null) { return AjaxResult.error("文档配置不完整,请先配置 fileId 和 sheetId"); } - + // 如果batchId为空,创建一个新的批次ID和批量推送记录(用于日志记录) if (batchId == null || batchId.trim().isEmpty()) { // 计算起始行和结束行(用于创建批量推送记录) int estimatedStartRow = configStartRow != null ? configStartRow : (headerRow + 1); int estimatedEndRow = estimatedStartRow + 199; - + batchId = batchPushService.createBatchPushRecord( - fileId, - sheetId, + fileId, + sheetId, "MANUAL", // 手动触发 "MANUAL_TRIGGER", // 手动触发来源 - estimatedStartRow, + estimatedStartRow, estimatedEndRow ); log.info("未提供batchId,自动创建新的批次ID和批量推送记录: {}", batchId); } - - log.info("同步物流配置 - fileId: {}, sheetId: {}, batchId: {}, 配置起始行: {}, 表头行: {}", + + log.info("同步物流配置 - fileId: {}, sheetId: {}, batchId: {}, 配置起始行: {}, 表头行: {}", fileId, sheetId, batchId, configStartRow, headerRow); // 生成Redis key,用于存储该文件的工作表的动态处理进度 @@ -995,10 +995,10 @@ public class TencentDocController extends BaseController { // 计算本次同步的起始行和结束行(带回溯机制) // 确保 configStartRow 不为 null int effectiveStartRow = configStartRow != null ? configStartRow : (headerRow + 1); - + int startRow; int endRow; - + if (forceStartRow != null) { // 强制指定行(手动指定) startRow = forceStartRow; @@ -1007,12 +1007,12 @@ public class TencentDocController extends BaseController { } else if (lastMaxRow != null && lastMaxRow >= effectiveStartRow) { // 计算回溯阈值:配置起始行 + 100 int threshold = effectiveStartRow + 100; - + if (lastMaxRow <= (effectiveStartRow + 199)) { // 进度较小(一批数据内),重新从配置起始行开始 startRow = effectiveStartRow; endRow = effectiveStartRow + 199; // 读取200行 - log.info("上次进度 {} 较小(≤{}),重新从第 {} 行开始,到第 {} 行", + log.info("上次进度 {} 较小(≤{}),重新从第 {} 行开始,到第 {} 行", lastMaxRow, effectiveStartRow + 199, startRow, endRow); } else if (lastMaxRow > threshold) { // 进度较大(超过阈值) @@ -1020,13 +1020,13 @@ public class TencentDocController extends BaseController { startRow = Math.max(effectiveStartRow, lastMaxRow - 100); // 结束行:从上次位置往前读100行 endRow = lastMaxRow + 100; - log.info("上次进度 {} 较大(>{}),回溯100行从第 {} 行开始,往前读100行到第 {} 行(范围:{} 行)", + log.info("上次进度 {} 较大(>{}),回溯100行从第 {} 行开始,往前读100行到第 {} 行(范围:{} 行)", lastMaxRow, threshold, startRow, endRow, endRow - startRow + 1); } else { // 进度在阈值范围内,重新从配置起始行开始 startRow = effectiveStartRow; endRow = effectiveStartRow + 199; // 读取200行 - log.info("上次进度 {} 在阈值范围内({}-{}),重新从第 {} 行开始,到第 {} 行", + log.info("上次进度 {} 在阈值范围内({}-{}),重新从第 {} 行开始,到第 {} 行", lastMaxRow, effectiveStartRow + 200, threshold, startRow, endRow); } } else { @@ -1202,7 +1202,7 @@ public class TencentDocController extends BaseController { int errorCount = 0; int successUpdates = 0; // 实际成功写入的数量 int maxSuccessRow = 0; // 记录实际成功写入的最大行号 - + // 收集同步成功的详细日志(用于微信推送) List> successLogs = new java.util.ArrayList<>(); @@ -1232,28 +1232,28 @@ public class TencentDocController extends BaseController { // 文档中已有物流链接,同步更新订单的推送状态 try { JDOrder existingOrder = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo); - if (existingOrder != null && + if (existingOrder != null && (existingOrder.getTencentDocPushed() == null || existingOrder.getTencentDocPushed() == 0)) { // 订单未标记为已推送,但文档中已有值,同步状态 existingOrder.setTencentDocPushed(1); existingOrder.setTencentDocPushTime(new java.util.Date()); int syncResult = jdOrderService.updateJDOrder(existingOrder); if (syncResult > 0) { - log.info("✓ 同步订单状态成功 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)", + log.info("✓ 同步订单状态成功 - 单号: {}, 行号: {}, 原因: 文档中已有物流链接(可能手动填写)", orderNo, excelRow); } else { - log.warn("⚠️ 同步订单状态返回0 - 单号: {}, 行号: {}, 可能未更新", + log.warn("⚠️ 同步订单状态返回0 - 单号: {}, 行号: {}, 可能未更新", orderNo, excelRow); } - + // 记录同步日志 - logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink, + logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink, "SKIPPED", "文档中已有物流链接,已同步订单状态"); } } catch (Exception e) { log.error("❌ 同步订单状态失败 - 单号: {}, 行号: {}", orderNo, excelRow, e); } - + skippedCount++; // 已有物流链接,跳过写入 continue; } @@ -1267,18 +1267,18 @@ public class TencentDocController extends BaseController { log.warn("未找到订单 - 单号: {}, 行号: {}", orderNo, excelRow); continue; } - + // 检查订单是否已推送(防止重复推送) if (skipPushedOrders && order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1) { skippedCount++; - log.info("跳过已推送订单 - 单号: {}, 推送时间: {}, 行号: {}", - orderNo, - order.getTencentDocPushTime() != null ? + 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(); @@ -1303,19 +1303,19 @@ public class TencentDocController extends BaseController { update.put("column", logisticsLinkColumn); update.put("orderNo", orderNo); update.put("logisticsLink", logisticsLink); - + // 如果找到手机号码,也添加到更新中 if (phone != null && phoneColumn != null) { update.put("phone", phone); update.put("phoneColumn", phoneColumn); } - + // 注意:不再保存order对象,写入成功后会重新查询以确保数据最新 updates.add(update); filledCount++; - log.info("找到订单物流链接 - 单号: {}, 物流链接: {}, 手机号: {}, 行号: {}, 已推送: {}", - orderNo, logisticsLink, phone != null ? phone : "无", excelRow, + log.info("找到订单物流链接 - 单号: {}, 物流链接: {}, 手机号: {}, 行号: {}, 已推送: {}", + orderNo, logisticsLink, phone != null ? phone : "无", excelRow, order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 ? "是" : "否"); } else { errorCount++; @@ -1355,27 +1355,27 @@ public class TencentDocController extends BaseController { // 重新读取该行数据,验证单号和物流链接列 String verifyRange = String.format("A%d:Z%d", row, row); JSONObject verifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, verifyRange); - + if (verifyData == null || !verifyData.containsKey("values")) { log.warn("验证失败 - 无法读取行 {}", row); errorCount++; continue; } - + JSONArray verifyRows = verifyData.getJSONArray("values"); if (verifyRows == null || verifyRows.isEmpty()) { log.warn("验证失败 - 行 {} 数据为空", row); errorCount++; continue; } - + JSONArray verifyRow = verifyRows.getJSONArray(0); if (verifyRow == null || verifyRow.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) { log.warn("验证失败 - 行 {} 列数不足", row); errorCount++; continue; } - + // 验证1:单号是否匹配 String currentOrderNo = verifyRow.getString(orderNoColumn); if (currentOrderNo == null || !currentOrderNo.trim().equals(expectedOrderNo)) { @@ -1383,7 +1383,7 @@ public class TencentDocController extends BaseController { errorCount++; continue; } - + // 验证2:物流链接列是否为空 String currentLogisticsLink = verifyRow.getString(logisticsLinkColumn); if (currentLogisticsLink != null && !currentLogisticsLink.trim().isEmpty()) { @@ -1391,7 +1391,7 @@ public class TencentDocController extends BaseController { skippedCount++; continue; } - + // 验证通过,执行写入 // 使用 batchUpdate 一次性更新多个字段 JSONArray requests = new JSONArray(); @@ -1441,7 +1441,7 @@ public class TencentDocController extends BaseController { logMsg += String.format(", 手机号: %s", phone); } log.info(logMsg); - + // 更新订单的推送状态(重新查询订单,避免使用旧对象) try { JDOrder orderToUpdate = jdOrderService.selectJDOrderByThirdPartyOrderNo(expectedOrderNo); @@ -1461,11 +1461,11 @@ public class TencentDocController extends BaseController { log.error("❌ 更新订单推送状态失败(但写入腾讯文档已成功) - 单号: {}", expectedOrderNo, e); // 不影响主流程,继续执行 } - + // 记录操作日志 - logOperation(batchId, fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, logisticsLink, + logOperation(batchId, fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, logisticsLink, "SUCCESS", null); - + // 收集成功日志信息(用于微信推送) Map successLog = new java.util.HashMap<>(); successLog.put("orderNo", expectedOrderNo); @@ -1478,11 +1478,11 @@ public class TencentDocController extends BaseController { } catch (Exception e) { log.error("写入数据失败 - 行: {}", entry.getKey(), e); errorCount++; - + // 记录失败日志 String orderNo = entry.getValue().getString("orderNo"); String logisticsLink = entry.getValue().getString("logisticsLink"); - logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, entry.getKey(), logisticsLink, + logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, entry.getKey(), logisticsLink, "FAILED", "写入异常: " + e.getMessage()); } @@ -1497,8 +1497,8 @@ public class TencentDocController extends BaseController { // 更新全局成功计数 successUpdates = currentBatchSuccessUpdates; maxSuccessRow = currentBatchMaxSuccessRow; - - log.info("批量填充物流链接完成 - 成功: {}, 跳过: {}, 错误: {}, 最大成功行号: {}", + + log.info("批量填充物流链接完成 - 成功: {}, 跳过: {}, 错误: {}, 最大成功行号: {}", successUpdates, skippedCount, errorCount, maxSuccessRow > 0 ? maxSuccessRow : "无"); } @@ -1507,19 +1507,19 @@ public class TencentDocController extends BaseController { int currentMaxRow = (maxSuccessRow > 0) ? maxSuccessRow : endRow; int nextStartRow; String nextSyncHint; - + // 使用 successUpdates(实际成功写入数量)而不是 filledCount(准备写入数量) if (successUpdates > 0) { // 有数据实际写入成功,说明当前范围有有效数据,继续往前推进 // 下次从 currentMaxRow - 100 开始(回溯100行,防止遗漏) nextStartRow = Math.max(effectiveStartRow, currentMaxRow - 100); - nextSyncHint = String.format("下次将从第 %d 行开始(本次实际写入成功 %d 条,最大行号: %d,回溯100行防止遗漏)", + nextSyncHint = String.format("下次将从第 %d 行开始(本次实际写入成功 %d 条,最大行号: %d,回溯100行防止遗漏)", nextStartRow, successUpdates, currentMaxRow); - + // 记录进度到实际成功写入的最大行号,持续往前 redisCache.setCacheObject(redisKey, currentMaxRow, 30, TimeUnit.DAYS); log.info("========== 更新Redis进度 =========="); - log.info("本次实际写入成功 {} 条,最大成功行号: {},更新进度到第 {} 行,下次从第 {} 行开始", + log.info("本次实际写入成功 {} 条,最大成功行号: {},更新进度到第 {} 行,下次从第 {} 行开始", successUpdates, maxSuccessRow, currentMaxRow, nextStartRow); log.info("Redis Key: {}", redisKey); } else { @@ -1527,27 +1527,27 @@ public class TencentDocController extends BaseController { if (lastMaxRow != null && currentMaxRow > (lastMaxRow + 300)) { // 如果跳跃太大(>300行),可能已经超出数据区域,回到配置起始行重新扫描 nextStartRow = effectiveStartRow; - nextSyncHint = String.format("下次将从第 %d 行重新开始(本次范围 %d-%d 无实际写入,重新扫描)", + nextSyncHint = String.format("下次将从第 %d 行重新开始(本次范围 %d-%d 无实际写入,重新扫描)", nextStartRow, startRow, endRow); - + // 不更新 Redis 进度,保持原有进度 - log.info("本次无实际写入,且跳跃过大({} > {} + 300),下次将从配置起始行 {} 重新开始", + log.info("本次无实际写入,且跳跃过大({} > {} + 300),下次将从配置起始行 {} 重新开始", currentMaxRow, lastMaxRow, effectiveStartRow); } else { // 跳跃不大,继续往前推进 nextStartRow = Math.max(effectiveStartRow, currentMaxRow - 100); - nextSyncHint = String.format("下次将从第 %d 行开始(本次范围 %d-%d 无实际写入,继续往前)", + nextSyncHint = String.format("下次将从第 %d 行开始(本次范围 %d-%d 无实际写入,继续往前)", nextStartRow, startRow, endRow); - + // 记录进度到 endRow(即使没有写入,也要记录扫描进度) redisCache.setCacheObject(redisKey, currentMaxRow, 30, TimeUnit.DAYS); log.info("========== 更新Redis进度 =========="); - log.info("本次无实际写入,更新扫描进度到第 {} 行,下次从第 {} 行继续", + log.info("本次无实际写入,更新扫描进度到第 {} 行,下次从第 {} 行继续", currentMaxRow, nextStartRow); log.info("Redis Key: {}", redisKey); } } - + JSONObject result = new JSONObject(); result.put("startRow", startRow); result.put("endRow", endRow); @@ -1560,7 +1560,7 @@ public class TencentDocController extends BaseController { result.put("orderNoColumn", orderNoColumn); result.put("logisticsLinkColumn", logisticsLinkColumn); result.put("skipPushedOrders", skipPushedOrders); // 是否跳过已推送订单 - + // 构建详细的提示信息 String message = String.format( "✓ 同步完成:成功填充 %d 条,跳过 %d 条,错误 %d 条\n" + @@ -1575,7 +1575,7 @@ public class TencentDocController extends BaseController { nextSyncHint ); result.put("message", message); - + // 更新批量推送记录状态(batchId 应该总是存在,因为如果没有会创建) if (batchId != null && !batchId.trim().isEmpty()) { try { @@ -1586,9 +1586,9 @@ public class TencentDocController extends BaseController { } else if (errorCount > 0) { status = "PARTIAL_SUCCESS"; } - batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount, + batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount, message, null); - log.info("✓ 批量推送记录已更新 - batchId: {}, status: {}, 实际成功: {}, 跳过: {}, 错误: {}", + log.info("✓ 批量推送记录已更新 - batchId: {}, status: {}, 实际成功: {}, 跳过: {}, 错误: {}", batchId, status, successUpdates, skippedCount, errorCount); } catch (Exception e) { log.error("更新批量推送记录失败", e); @@ -1597,7 +1597,7 @@ public class TencentDocController extends BaseController { } else { log.warn("⚠️ batchId 为空,无法更新批量推送记录"); } - + // 如果有成功记录,发送微信推送 if (!successLogs.isEmpty()) { try { @@ -1628,7 +1628,7 @@ public class TencentDocController extends BaseController { private JSONObject buildUpdateCellRequest(String sheetId, int rowIndex, int columnIndex, String value) { return buildUpdateCellRequest(sheetId, rowIndex, columnIndex, value, false); } - + /** * 构建单个单元格的更新请求(用于 batchUpdate)- 支持超链接 * @@ -1657,7 +1657,7 @@ public class TencentDocController extends BaseController { // 构建单元格数据 JSONObject cellData = new JSONObject(); JSONObject cellValue = new JSONObject(); - + if (isLink) { // 写入超链接类型(url 和 text 都设置为相同的值) JSONObject link = new JSONObject(); @@ -1668,7 +1668,7 @@ public class TencentDocController extends BaseController { // 写入普通文本 cellValue.put("text", value); } - + cellData.put("cellValue", cellValue); cellValues.add(cellData); @@ -1714,10 +1714,10 @@ public class TencentDocController extends BaseController { if (sheetId == null || sheetId.trim().isEmpty()) { sheetId = tencentDocConfig.getSheetId(); } - - List records = + + List records = batchPushService.getBatchPushRecordListWithLogs(fileId, sheetId, limit); - + return AjaxResult.success(records); } catch (Exception e) { log.error("查询批量推送记录失败", e); @@ -1731,13 +1731,13 @@ public class TencentDocController extends BaseController { @GetMapping("/batchPushRecord/{batchId}") public AjaxResult getBatchPushRecordDetail(@PathVariable String batchId) { try { - com.ruoyi.jarvis.domain.TencentDocBatchPushRecord record = + com.ruoyi.jarvis.domain.TencentDocBatchPushRecord record = batchPushService.getBatchPushRecord(batchId); - + if (record == null) { return AjaxResult.error("未找到批次记录"); } - + return AjaxResult.success(record); } catch (Exception e) { log.error("查询批量推送记录详情失败", e); @@ -1752,17 +1752,17 @@ public class TencentDocController extends BaseController { public AjaxResult getPushStatus() { try { Map status = batchPushService.getPushStatusAndCountdown(); - + // 添加最后一次成功的推送记录信息 String fileId = tencentDocConfig.getFileId(); String sheetId = tencentDocConfig.getSheetId(); - + if (fileId != null && sheetId != null) { - com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess = + com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess = batchPushService.getLastSuccessRecord(fileId, sheetId); status.put("lastSuccessRecord", lastSuccess); } - + return AjaxResult.success(status); } catch (Exception e) { log.error("查询推送状态失败", e); @@ -1801,14 +1801,14 @@ public class TencentDocController extends BaseController { /** * 反向同步第三方单号 * 从腾讯文档的物流单号列读取链接,通过链接匹配本地订单,将腾讯文档的单号列值写入到订单的第三方单号字段 - * + * * @param params 包含 fileId, sheetId, startRow(起始行,默认850) * @return 同步结果 */ @PostMapping("/reverseSyncThirdPartyOrderNo") public AjaxResult reverseSyncThirdPartyOrderNo(@RequestBody Map params) { String batchId = java.util.UUID.randomUUID().toString().replace("-", ""); - + try { // 获取访问令牌 String accessToken; @@ -1827,7 +1827,7 @@ public class TencentDocController extends BaseController { // 从参数或配置中获取文档信息 String fileId = (String) params.get("fileId"); String sheetId = (String) params.get("sheetId"); - + final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:"; if (fileId == null || fileId.isEmpty()) { fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId"); @@ -1841,24 +1841,24 @@ public class TencentDocController extends BaseController { sheetId = tencentDocConfig.getSheetId(); } } - + // 从配置中读取表头行 Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow"); if (headerRow == null) { headerRow = tencentDocConfig.getHeaderRow(); } - + // 起始行,默认850 - Integer startRow = params.get("startRow") != null ? + Integer startRow = params.get("startRow") != null ? Integer.valueOf(params.get("startRow").toString()) : 850; - + // 结束行,默认到2500行 - Integer endRow = params.get("endRow") != null ? + Integer endRow = params.get("endRow") != null ? Integer.valueOf(params.get("endRow").toString()) : 2500; - + // 记录接收到的参数 log.info("接收到参数 - startRow: {}, endRow: {}, params: {}", startRow, endRow, params); - + // 如果 endRow 小于 startRow + 1000,可能是前端传错了,强制设置为 2500 if (endRow < startRow + 1000) { log.warn("检测到 endRow ({}) 可能不正确,强制设置为 2500", endRow); @@ -1868,76 +1868,76 @@ public class TencentDocController extends BaseController { if (accessToken == null || fileId == null || sheetId == null) { return AjaxResult.error("文档配置不完整,请先配置 fileId 和 sheetId"); } - - log.info("反向同步第三方单号开始 - fileId: {}, sheetId: {}, 起始行: {}, 结束行: {}", + + log.info("反向同步第三方单号开始 - fileId: {}, sheetId: {}, 起始行: {}, 结束行: {}", fileId, sheetId, startRow, endRow); // 读取表头,识别列位置 String headerRange = String.format("A%d:Z%d", headerRow, headerRow); JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange); - + if (headerData == null || !headerData.containsKey("values")) { return AjaxResult.error("读取表头失败"); } - + JSONArray headerValues = headerData.getJSONArray("values"); if (headerValues == null || headerValues.isEmpty()) { return AjaxResult.error("表头数据为空"); } - + JSONArray headerRowData = headerValues.getJSONArray(0); if (headerRowData == null || headerRowData.isEmpty()) { return AjaxResult.error("无法识别表头"); } - + // 识别列位置 Integer orderNoColumn = null; // "单号"列 Integer logisticsLinkColumn = null; // "物流单号"列 - + for (int i = 0; i < headerRowData.size(); i++) { String cellValue = headerRowData.getString(i); if (cellValue != null) { String cellValueTrim = cellValue.trim(); - + if (orderNoColumn == null && cellValueTrim.contains("单号") && !cellValueTrim.contains("物流")) { orderNoColumn = i; log.info("✓ 识别到 '单号' 列:第 {} 列(索引{})", i + 1, i); } - + if (logisticsLinkColumn == null && (cellValueTrim.contains("物流单号") || cellValueTrim.contains("物流链接"))) { logisticsLinkColumn = i; log.info("✓ 识别到 '物流单号' 列:第 {} 列(索引{})", i + 1, i); } } } - + if (orderNoColumn == null || logisticsLinkColumn == null) { return AjaxResult.error("无法识别表头列,请确保表头包含'单号'和'物流单号'列"); } - + // 统计结果 int successCount = 0; int skippedCount = 0; int errorCount = 0; - + // 分批读取数据,每批200行(避免单次读取过多数据导致API限制) final int BATCH_SIZE = 200; int currentStartRow = startRow; int totalBatches = (int) Math.ceil((double)(endRow - startRow + 1) / BATCH_SIZE); int currentBatch = 0; - + log.info("开始分批处理,共 {} 批,每批 {} 行", totalBatches, BATCH_SIZE); - + while (currentStartRow <= endRow) { currentBatch++; int currentEndRow = Math.min(currentStartRow + BATCH_SIZE - 1, endRow); - + log.info("正在处理第 {}/{} 批:第 {} 行到第 {} 行", currentBatch, totalBatches, currentStartRow, currentEndRow); - + // 读取当前批次的数据行 String dataRange = String.format("A%d:Z%d", currentStartRow, currentEndRow); log.info("读取数据范围: {}", dataRange); - + JSONObject dataResponse = null; try { dataResponse = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange); @@ -1948,22 +1948,22 @@ public class TencentDocController extends BaseController { currentStartRow = currentEndRow + 1; continue; } - + if (dataResponse == null || !dataResponse.containsKey("values")) { log.warn("第 {} 批数据读取返回空({} - {} 行),跳过", currentBatch, currentStartRow, currentEndRow); currentStartRow = currentEndRow + 1; continue; } - + JSONArray rows = dataResponse.getJSONArray("values"); if (rows == null || rows.isEmpty()) { log.info("第 {} 批数据为空({} - {} 行),跳过", currentBatch, currentStartRow, currentEndRow); currentStartRow = currentEndRow + 1; continue; } - + log.info("第 {} 批读取到 {} 行数据", currentBatch, rows.size()); - + // 处理当前批次的每一行 for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { JSONArray row = rows.getJSONArray(rowIndex); @@ -1971,91 +1971,91 @@ public class TencentDocController extends BaseController { skippedCount++; continue; } - + int actualRow = currentStartRow + rowIndex; // 确保不超过结束行 if (actualRow > endRow) { break; } - + String logisticsLink = row.getString(logisticsLinkColumn); String orderNoFromDoc = row.getString(orderNoColumn); - + // 跳过物流链接为空的行 if (logisticsLink == null || logisticsLink.trim().isEmpty()) { log.debug("跳过第 {} 行:物流链接为空", actualRow); skippedCount++; - logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, null, + logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, null, "SKIPPED", "物流链接为空"); continue; } - + // 跳过单号为空的行 if (orderNoFromDoc == null || orderNoFromDoc.trim().isEmpty()) { log.debug("跳过第 {} 行:单号为空", actualRow); skippedCount++; - logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink, + logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink, "SKIPPED", "单号为空"); continue; } - + // 清理物流链接(去除空格、换行符、中文等) logisticsLink = cleanLogisticsLink(logisticsLink); - + try { // 通过物流链接查找订单 JDOrder order = jdOrderService.selectJDOrderByLogisticsLink(logisticsLink); - + if (order == null) { log.warn("未找到匹配的订单 - 行: {}, 物流链接: {}", actualRow, logisticsLink); errorCount++; - logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink, + logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink, "FAILED", "未找到匹配的订单"); continue; } - + // 检查订单是否已有第三方单号(如果已有且与文档中的不同,跳过) if (order.getThirdPartyOrderNo() != null && !order.getThirdPartyOrderNo().trim().isEmpty()) { if (!order.getThirdPartyOrderNo().trim().equals(orderNoFromDoc.trim())) { - log.info("跳过第 {} 行:订单已有第三方单号且不同 - 现有: {}, 文档: {}", + log.info("跳过第 {} 行:订单已有第三方单号且不同 - 现有: {}, 文档: {}", actualRow, order.getThirdPartyOrderNo(), orderNoFromDoc); skippedCount++; - logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink, + logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink, "SKIPPED", "订单已有第三方单号且不同"); continue; } // 如果相同,继续执行(清除物流链接) } - + // 更新订单的第三方单号 order.setThirdPartyOrderNo(orderNoFromDoc.trim()); int updateResult = jdOrderService.updateJDOrder(order); - + if (updateResult <= 0) { - log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}", + log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}", actualRow, order.getId(), order.getRemark()); errorCount++; - logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink, + logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink, "FAILED", "更新订单失败"); continue; } - - log.info("✓ 更新订单成功 - 行: {}, 订单: {}, 第三方单号: {}", + + log.info("✓ 更新订单成功 - 行: {}, 订单: {}, 第三方单号: {}", actualRow, order.getRemark(), orderNoFromDoc); - + successCount++; - + // 记录成功日志 - logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink, + logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink, "SUCCESS", String.format("已将单号 %s 写入订单的第三方单号字段", orderNoFromDoc)); - + } catch (Exception e) { log.error("处理第 {} 行失败", actualRow, e); errorCount++; - logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink, + logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink, "FAILED", "处理异常: " + e.getMessage()); } - + // 添加延迟,避免API调用频率过高 try { Thread.sleep(100); @@ -2063,13 +2063,13 @@ public class TencentDocController extends BaseController { Thread.currentThread().interrupt(); } } - - log.info("第 {}/{} 批处理完成,当前统计 - 成功: {}, 跳过: {}, 错误: {}", + + log.info("第 {}/{} 批处理完成,当前统计 - 成功: {}, 跳过: {}, 错误: {}", currentBatch, totalBatches, successCount, skippedCount, errorCount); - + // 移动到下一批 currentStartRow = currentEndRow + 1; - + // 批次之间的延迟,避免API调用频率过高 if (currentStartRow <= endRow) { try { @@ -2079,7 +2079,7 @@ public class TencentDocController extends BaseController { } } } - + JSONObject result = new JSONObject(); result.put("batchId", batchId); result.put("startRow", startRow); @@ -2087,18 +2087,18 @@ public class TencentDocController extends BaseController { result.put("successCount", successCount); result.put("skippedCount", skippedCount); result.put("errorCount", errorCount); - + String message = String.format( "✓ 反向同步完成:成功 %d 条,跳过 %d 条,错误 %d 条\n" + " 处理范围:第 %d-%d 行\n" + " 批次ID:%s", successCount, skippedCount, errorCount, startRow, endRow, batchId); result.put("message", message); - + log.info("反向同步第三方单号完成 - {}", message); - + return AjaxResult.success("反向同步完成", result); - + } catch (Exception e) { log.error("反向同步第三方单号失败", e); return AjaxResult.error("反向同步失败: " + e.getMessage()); @@ -2107,7 +2107,7 @@ public class TencentDocController extends BaseController { /** * 发送微信推送通知(同步成功日志) - * + * * @param successLogs 成功日志列表 * @param filledCount 成功填充数量 * @param skippedCount 跳过数量 @@ -2116,10 +2116,10 @@ public class TencentDocController extends BaseController { private void sendWeChatNotification(List> successLogs, int filledCount, int skippedCount, int errorCount) { sendWeChatNotification(successLogs, filledCount, skippedCount, errorCount, true); } - + /** * 发送微信推送通知(同步成功日志)- 带batchId参数 - * + * * @param successLogs 成功日志列表 * @param filledCount 成功填充数量 * @param skippedCount 跳过数量 @@ -2132,10 +2132,10 @@ public class TencentDocController extends BaseController { private void sendWeChatNotificationWithBatchId(List> successLogs, int filledCount, int skippedCount, int errorCount, String batchId, String fileId, String sheetId, boolean isManual) { sendWeChatNotification(successLogs, filledCount, skippedCount, errorCount, batchId, fileId, sheetId, isManual); } - + /** * 发送微信推送通知(同步成功日志) - * + * * @param successLogs 成功日志列表 * @param filledCount 成功填充数量 * @param skippedCount 跳过数量 @@ -2155,10 +2155,10 @@ public class TencentDocController extends BaseController { } sendWeChatNotification(successLogs, filledCount, skippedCount, errorCount, null, fileId, sheetId, isManual); } - + /** * 发送微信推送通知(同步成功日志) - * + * * @param successLogs 成功日志列表 * @param filledCount 成功填充数量 * @param skippedCount 跳过数量 @@ -2171,15 +2171,15 @@ public class TencentDocController extends BaseController { private void sendWeChatNotification(List> successLogs, int filledCount, int skippedCount, int errorCount, String batchId, String fileId, String sheetId, boolean isManual) { try { log.info("========== 开始发送微信推送通知 =========="); - log.info("推送类型: {}, 成功: {} 条, 跳过: {} 条, 错误: {} 条", + log.info("推送类型: {}, 成功: {} 条, 跳过: {} 条, 错误: {} 条", isManual ? "手动执行" : "批量同步", filledCount, skippedCount, errorCount); log.info("批次ID: {}, 文档ID: {}, 工作表ID: {}", batchId, fileId, sheetId); - + // 微信推送服务配置 String wxSendBaseUrl = "https://wxts.van333.cn"; String pushToken = "super_token_b62190c26"; - String pushUrl = wxSendBaseUrl + "/dc/send/ty"; - + String pushUrl = wxSendBaseUrl + "/wx/send/ty"; + // 构建推送内容 StringBuilder content = new StringBuilder(); if (isManual) { @@ -2195,7 +2195,7 @@ public class TencentDocController extends BaseController { content.append(String.format("✗ 错误: %d 条\n", errorCount)); } content.append("\n"); - + if (!successLogs.isEmpty()) { content.append("【成功详情】\n"); // 最多显示20条详细记录,避免消息过长 @@ -2206,7 +2206,7 @@ public class TencentDocController extends BaseController { Integer row = (Integer) log.get("row"); String logisticsLink = (String) log.get("logisticsLink"); String phone = (String) log.get("phone"); - + content.append(String.format("%d. 单号: %s\n", i + 1, orderNo)); content.append(String.format(" 行号: %d\n", row)); if (phone != null && !phone.isEmpty()) { @@ -2217,12 +2217,12 @@ public class TencentDocController extends BaseController { content.append("\n"); } } - + if (successLogs.size() > maxDisplay) { content.append(String.format("\n... 还有 %d 条记录未显示", successLogs.size() - maxDisplay)); } } - + // 构建请求体 JSONObject requestBody = new JSONObject(); requestBody.put("title", "腾讯文档同步成功"); @@ -2230,42 +2230,42 @@ public class TencentDocController extends BaseController { requestBody.put("vanToken", pushToken); requestBody.put("messageType", "TY"); // touser 可以为空,使用默认接收人 - + String jsonBody = requestBody.toJSONString(); - + log.info("微信推送请求 - URL: {}", pushUrl); log.info("微信推送请求体: {}", jsonBody); - + // 发送POST请求 String result = sendPostRequest(pushUrl, jsonBody, pushToken); - + log.info("========== 微信推送发送完成 =========="); log.info("推送URL: {}", pushUrl); log.info("推送响应: {}", result); - + // 记录微信推送操作日志 - String pushLogMessage = String.format("微信推送成功 - 类型: %s, 成功: %d条, 跳过: %d条, 错误: %d条", + String pushLogMessage = String.format("微信推送成功 - 类型: %s, 成功: %d条, 跳过: %d条, 错误: %d条", isManual ? "手动执行" : "批量同步", filledCount, skippedCount, errorCount); - logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null, + logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null, "SUCCESS", pushLogMessage); - + } catch (Exception e) { log.error("========== 发送微信推送失败 ==========", e); - + // 记录微信推送失败日志 - String pushLogMessage = String.format("微信推送失败 - 类型: %s, 错误: %s", + String pushLogMessage = String.format("微信推送失败 - 类型: %s, 错误: %s", isManual ? "手动执行" : "批量同步", e.getMessage()); - logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null, + logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null, "FAILED", pushLogMessage); - + // 不抛出异常,避免影响主流程 log.warn("微信推送失败,但不影响主流程,继续执行"); } } - + /** * 发送POST请求到微信推送服务 - * + * * @param url 请求URL * @param jsonBody JSON请求体 * @param token 认证token @@ -2279,10 +2279,10 @@ public class TencentDocController extends BaseController { log.info(">>> 发送HTTP POST请求到微信推送服务"); log.info(">>> 请求URL: {}", url); log.info(">>> 请求体长度: {} 字节", jsonBody.length()); - + java.net.URL realUrl = new java.net.URL(url); java.net.URLConnection conn = realUrl.openConnection(); - + // 设置请求头 conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); @@ -2290,23 +2290,23 @@ public class TencentDocController extends BaseController { conn.setRequestProperty("Accept-Charset", "utf-8"); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("vanToken", token); - + log.info(">>> 请求头已设置 - vanToken: {}...", token != null && token.length() > 10 ? token.substring(0, 10) : token); - + conn.setDoOutput(true); conn.setDoInput(true); - + // 设置超时时间 conn.setConnectTimeout(10000); // 10秒连接超时 conn.setReadTimeout(10000); // 10秒读取超时 - + log.info(">>> 开始发送请求体..."); // 发送请求体 out = new java.io.PrintWriter(conn.getOutputStream()); out.print(jsonBody); out.flush(); log.info(">>> 请求体已发送"); - + log.info(">>> 开始读取响应..."); // 读取响应 in = new java.io.BufferedReader(new java.io.InputStreamReader(conn.getInputStream(), java.nio.charset.StandardCharsets.UTF_8)); @@ -2314,7 +2314,7 @@ public class TencentDocController extends BaseController { while ((line = in.readLine()) != null) { result.append(line); } - + log.info(">>> HTTP请求成功 - URL: {}, 响应: {}", url, result.toString()); } catch (java.net.ConnectException e) { log.error(">>> HTTP请求连接失败 - URL: {}", url, e); @@ -2346,7 +2346,7 @@ public class TencentDocController extends BaseController { /** * 清理物流链接 * 去除空格、换行符、制表符、中文等特殊字符,只保留URL有效字符 - * + * * @param logisticsLink 原始物流链接 * @return 清理后的物流链接 */ @@ -2354,21 +2354,21 @@ public class TencentDocController extends BaseController { if (logisticsLink == null) { return null; } - + // 去除首尾空格 String cleaned = logisticsLink.trim(); - + // 去除换行符、制表符等空白字符 cleaned = cleaned.replaceAll("[\\r\\n\\t]", ""); - + // 去除所有空格 cleaned = cleaned.replaceAll("\\s+", ""); - + // 提取URL(如果包含中文或其他非URL字符,尝试提取URL部分) // 匹配 http:// 或 https:// 开头的URL java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile("https?://[^\\s\\u4e00-\\u9fa5]+"); java.util.regex.Matcher matcher = urlPattern.matcher(cleaned); - + if (matcher.find()) { cleaned = matcher.group(); } else { @@ -2379,7 +2379,7 @@ public class TencentDocController extends BaseController { cleaned = shortMatcher.group(); } } - + return cleaned.trim(); } }