This commit is contained in:
Leo
2026-01-09 20:08:13 +08:00
parent 01e5312ccc
commit 01b19602b6

View File

@@ -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<String, Object> 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<String> 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<String> 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("微信推送失败,但不影响主流程,继续执行");
}
}
/**
* 发送微信推送通知(同步成功日志)
*