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 8b0f909..c1ef083 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 @@ -2105,6 +2105,437 @@ public class TencentDocController extends BaseController { } } + /** + * 从腾讯文档拉取物流信息并对比更新本地订单 + * 读取腾讯文档中的单号和物流链接,与本地订单进行对比,如果不一样则覆盖写入本地 + * + * @param params 包含 fileId, sheetId, startRow(起始行,默认850), endRow(结束行,默认2500) + * @return 同步结果 + */ + @PostMapping("/syncLogisticsFromTencentDoc") + public AjaxResult syncLogisticsFromTencentDoc(@RequestBody Map params) { + String batchId = java.util.UUID.randomUUID().toString().replace("-", ""); + + try { + // 获取访问令牌 + String accessToken; + try { + accessToken = tencentDocTokenService.refreshAccessToken(); + log.info("成功刷新访问令牌"); + } catch (Exception e) { + log.error("刷新访问令牌失败", e); + try { + accessToken = tencentDocTokenService.getValidAccessToken(); + } catch (Exception e2) { + return AjaxResult.error("访问令牌无效,请先完成授权。获取授权URL: GET /jarvis/tendoc/authUrl"); + } + } + + // 从参数或配置中获取文档信息 + 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"); + if (fileId == null || fileId.isEmpty()) { + fileId = tencentDocConfig.getFileId(); + } + } + if (sheetId == null || sheetId.isEmpty()) { + sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId"); + if (sheetId == null || sheetId.isEmpty()) { + 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.valueOf(params.get("startRow").toString()) : 850; + + // 结束行,默认到2500行 + Integer endRow = params.get("endRow") != null ? + Integer.valueOf(params.get("endRow").toString()) : 2500; + + if (accessToken == null || fileId == null || sheetId == null) { + return AjaxResult.error("文档配置不完整,请先配置 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 updatedCount = 0; // 更新数量 + int skippedCount = 0; // 跳过数量(物流链接相同或为空) + int errorCount = 0; // 错误数量 + java.util.List updatedOrderNos = new java.util.ArrayList<>(); // 更新的单号列表 + + // 分批读取数据,每批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); + } catch (Exception e) { + log.error("读取第 {} 批数据失败({} - {} 行)", currentBatch, currentStartRow, currentEndRow, e); + errorCount += (currentEndRow - currentStartRow + 1); + // 继续处理下一批 + 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); + if (row == null || row.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) { + skippedCount++; + continue; + } + + int actualRow = currentStartRow + rowIndex; + // 确保不超过结束行 + if (actualRow > endRow) { + break; + } + + String orderNoFromDoc = row.getString(orderNoColumn); + String logisticsLinkFromDoc = row.getString(logisticsLinkColumn); + + // 跳过单号为空的行 + if (orderNoFromDoc == null || orderNoFromDoc.trim().isEmpty()) { + log.debug("跳过第 {} 行:单号为空", actualRow); + skippedCount++; + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", null, actualRow, null, + "SKIPPED", "单号为空"); + continue; + } + + // 跳过物流链接为空的行 + if (logisticsLinkFromDoc == null || logisticsLinkFromDoc.trim().isEmpty()) { + log.debug("跳过第 {} 行:物流链接为空", actualRow); + skippedCount++; + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", orderNoFromDoc, actualRow, null, + "SKIPPED", "物流链接为空"); + continue; + } + + // 清理物流链接(去除空格、换行符、中文等) + String cleanedLogisticsLink = cleanLogisticsLink(logisticsLinkFromDoc); + + try { + // 通过第三方单号查找本地订单 + JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNoFromDoc.trim()); + + if (order == null) { + // 如果通过第三方单号找不到,尝试通过内部单号(remark)查找 + order = jdOrderService.selectJDOrderByRemark(orderNoFromDoc.trim()); + } + + if (order == null) { + log.warn("未找到匹配的订单 - 行: {}, 单号: {}", actualRow, orderNoFromDoc); + errorCount++; + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", orderNoFromDoc, actualRow, cleanedLogisticsLink, + "FAILED", "未找到匹配的订单"); + continue; + } + + // 对比物流链接 + String localLogisticsLink = order.getLogisticsLink(); + if (localLogisticsLink != null) { + localLogisticsLink = localLogisticsLink.trim(); + } + + // 如果本地物流链接为空,直接更新 + if (localLogisticsLink == null || localLogisticsLink.isEmpty()) { + // 本地物流链接为空,直接更新 + order.setLogisticsLink(cleanedLogisticsLink); + int updateResult = jdOrderService.updateJDOrder(order); + + if (updateResult <= 0) { + log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}", + actualRow, order.getId(), order.getRemark()); + errorCount++; + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink, + "FAILED", "更新订单失败"); + continue; + } + + log.info("✓ 更新订单物流链接成功(本地为空) - 行: {}, 订单: {}, 新物流: {}", + actualRow, order.getRemark(), cleanedLogisticsLink); + + updatedCount++; + updatedOrderNos.add(order.getRemark() != null ? order.getRemark() : orderNoFromDoc); + + // 记录成功日志 + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink, + "SUCCESS", String.format("已更新物流链接:空 -> %s", cleanedLogisticsLink)); + continue; + } + + // 如果物流链接相同,跳过 + if (cleanedLogisticsLink.equals(localLogisticsLink)) { + log.debug("跳过第 {} 行:物流链接相同 - 单号: {}, 物流: {}", actualRow, orderNoFromDoc, cleanedLogisticsLink); + skippedCount++; + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink, + "SKIPPED", "物流链接相同"); + continue; + } + + // 物流链接不同,更新本地订单 + String oldLogisticsLink = localLogisticsLink != null ? localLogisticsLink : "空"; + order.setLogisticsLink(cleanedLogisticsLink); + int updateResult = jdOrderService.updateJDOrder(order); + + if (updateResult <= 0) { + log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}", + actualRow, order.getId(), order.getRemark()); + errorCount++; + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink, + "FAILED", "更新订单失败"); + continue; + } + + log.info("✓ 更新订单物流链接成功 - 行: {}, 订单: {}, 旧物流: {}, 新物流: {}", + actualRow, order.getRemark(), oldLogisticsLink, cleanedLogisticsLink); + + updatedCount++; + updatedOrderNos.add(order.getRemark() != null ? order.getRemark() : orderNoFromDoc); + + // 记录成功日志 + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", order.getRemark(), actualRow, cleanedLogisticsLink, + "SUCCESS", String.format("已更新物流链接:%s -> %s", oldLogisticsLink, cleanedLogisticsLink)); + + } catch (Exception e) { + log.error("处理第 {} 行失败", actualRow, e); + errorCount++; + logOperation(batchId, fileId, sheetId, "SYNC_LOGISTICS", orderNoFromDoc, actualRow, cleanedLogisticsLink, + "FAILED", "处理异常: " + e.getMessage()); + } + + // 添加延迟,避免API调用频率过高 + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + log.info("第 {}/{} 批处理完成,当前统计 - 更新: {}, 跳过: {}, 错误: {}", + currentBatch, totalBatches, updatedCount, skippedCount, errorCount); + + // 移动到下一批 + currentStartRow = currentEndRow + 1; + + // 批次之间的延迟,避免API调用频率过高 + if (currentStartRow <= endRow) { + try { + Thread.sleep(200); // 批次之间延迟200ms + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + JSONObject result = new JSONObject(); + result.put("batchId", batchId); + result.put("startRow", startRow); + result.put("endRow", endRow); + result.put("updatedCount", updatedCount); + result.put("skippedCount", skippedCount); + result.put("errorCount", errorCount); + result.put("updatedOrderNos", updatedOrderNos); + + String message = String.format( + "✓ 物流信息同步完成:更新 %d 条,跳过 %d 条,错误 %d 条\n" + + " 处理范围:第 %d-%d 行\n" + + " 批次ID:%s", + updatedCount, skippedCount, errorCount, startRow, endRow, batchId); + result.put("message", message); + + log.info("从腾讯文档同步物流信息完成 - {}", message); + + // 如果有更新的订单,发送微信推送通知 + if (updatedCount > 0) { + try { + sendLogisticsSyncNotification(updatedOrderNos, updatedCount, skippedCount, errorCount, batchId, fileId, sheetId); + } catch (Exception e) { + log.error("发送微信推送通知失败", e); + } + } + + return AjaxResult.success("物流信息同步完成", result); + + } catch (Exception e) { + log.error("从腾讯文档同步物流信息失败", e); + return AjaxResult.error("同步失败: " + e.getMessage()); + } + } + + /** + * 发送物流信息同步的微信推送通知 + * + * @param updatedOrderNos 更新的单号列表 + * @param updatedCount 更新数量 + * @param skippedCount 跳过数量 + * @param errorCount 错误数量 + * @param batchId 批次ID + * @param fileId 文档ID + * @param sheetId 工作表ID + */ + private void sendLogisticsSyncNotification(java.util.List updatedOrderNos, int updatedCount, int skippedCount, int errorCount, String batchId, String fileId, String sheetId) { + try { + log.info("========== 开始发送物流信息同步微信推送通知 =========="); + log.info("更新: {} 条, 跳过: {} 条, 错误: {} 条", updatedCount, skippedCount, errorCount); + log.info("批次ID: {}, 文档ID: {}, 工作表ID: {}", batchId, fileId, sheetId); + + // 微信推送服务配置 + String wxSendBaseUrl = "https://wxts.van333.cn"; + String pushToken = "super_token_b62190c26"; + String pushUrl = wxSendBaseUrl + "/wx/send/ty"; + + // 构建推送内容 + StringBuilder content = new StringBuilder(); + content.append("【腾讯文档物流信息同步成功】\n\n"); + content.append(String.format("✓ 更新数量: %d 条\n", updatedCount)); + if (skippedCount > 0) { + content.append(String.format("⊘ 跳过: %d 条\n", skippedCount)); + } + if (errorCount > 0) { + content.append(String.format("✗ 错误: %d 条\n", errorCount)); + } + content.append("\n"); + + if (!updatedOrderNos.isEmpty()) { + content.append("【更新的单号列表】\n"); + // 最多显示30条单号,避免消息过长 + int maxDisplay = Math.min(30, updatedOrderNos.size()); + for (int i = 0; i < maxDisplay; i++) { + content.append(String.format("%d. %s\n", i + 1, updatedOrderNos.get(i))); + } + + if (updatedOrderNos.size() > maxDisplay) { + content.append(String.format("\n... 还有 %d 个单号未显示", updatedOrderNos.size() - maxDisplay)); + } + } + + // 构建请求体 + JSONObject requestBody = new JSONObject(); + requestBody.put("title", "腾讯文档物流信息同步成功"); + requestBody.put("text", content.toString()); + requestBody.put("vanToken", pushToken); + requestBody.put("messageType", "TY"); + requestBody.put("touser", "LinPingFan,Hong"); + + 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("物流信息同步微信推送成功 - 更新: %d条, 跳过: %d条, 错误: %d条", + updatedCount, skippedCount, errorCount); + logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null, + "SUCCESS", pushLogMessage); + + } catch (Exception e) { + log.error("========== 发送物流信息同步微信推送失败 ==========", e); + + // 记录微信推送失败日志 + String pushLogMessage = String.format("物流信息同步微信推送失败 - 错误: %s", e.getMessage()); + logOperation(batchId, fileId, sheetId, "WECHAT_PUSH", null, null, null, + "FAILED", pushLogMessage); + + // 不抛出异常,避免影响主流程 + log.warn("微信推送失败,但不影响主流程,继续执行"); + } + } + /** * 发送微信推送通知(同步成功日志) *