diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/JDOrderController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/JDOrderController.java index ad5c8fb..3dd804c 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/JDOrderController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/jarvis/JDOrderController.java @@ -10,6 +10,7 @@ import com.ruoyi.common.utils.http.HttpUtils; import com.ruoyi.jarvis.domain.JDOrder; import com.ruoyi.jarvis.domain.OrderRows; import com.ruoyi.jarvis.service.IJDOrderService; +import com.ruoyi.jarvis.service.ILogisticsService; import com.ruoyi.jarvis.service.IOrderRowsService; import com.ruoyi.jarvis.service.IGiftCouponService; import com.ruoyi.jarvis.domain.GiftCoupon; @@ -41,6 +42,7 @@ public class JDOrderController extends BaseController { private final IOrderRowsService orderRowsService; private final IGiftCouponService giftCouponService; private final ISysConfigService sysConfigService; + private final ILogisticsService logisticsService; private static final String CONFIG_KEY_PREFIX = "logistics.push.touser."; private static final java.util.regex.Pattern URL_DETECT_PATTERN = java.util.regex.Pattern.compile( "(https?://[^\\s]+)|(u\\.jd\\.com/[^\\s]+)", @@ -53,11 +55,13 @@ public class JDOrderController extends BaseController { java.util.regex.Pattern.CASE_INSENSITIVE); public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, - IGiftCouponService giftCouponService, ISysConfigService sysConfigService) { + IGiftCouponService giftCouponService, ISysConfigService sysConfigService, + ILogisticsService logisticsService) { this.jdOrderService = jdOrderService; this.orderRowsService = orderRowsService; this.giftCouponService = giftCouponService; this.sysConfigService = sysConfigService; + this.logisticsService = logisticsService; } private final static String skey = "2192057370ef8140c201079969c956a3"; @@ -68,12 +72,6 @@ public class JDOrderController extends BaseController { @Value("${jarvis.server.jarvis-java.jd-api-path:/jd}") private String jdApiPath; - @Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}") - private String logisticsBaseUrl; - - @Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}") - private String logisticsFetchPath; - /** * 获取JD接口请求URL */ @@ -947,9 +945,7 @@ public class JDOrderController extends BaseController { logger.info("手动获取物流信息 - 订单ID: {}, 订单号: {}, 分销标识: {}, 物流链接: {}", orderId, order.getOrderId(), distributionMark, logisticsLink); - // 构建外部接口URL - String externalUrl = logisticsBaseUrl + logisticsFetchPath + "?tracking_url=" + - java.net.URLEncoder.encode(logisticsLink, "UTF-8"); + String externalUrl = logisticsService.buildFetchLogisticsRequestUrl(logisticsLink); logger.info("准备调用外部接口 - URL: {}", externalUrl); diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 621d0a0..7ee6b5b 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -200,6 +200,8 @@ jarvis: # 物流接口服务地址 logistics: base-url: http://192.168.8.88:5001 + # 同机多进程多端口时配置逗号分隔列表;非空时仅按下列地址轮询,不再使用 base-url + base-urls: http://192.168.8.88:5001,http://192.168.8.88:5002,http://192.168.8.88:5003 fetch-path: /fetch_logistics health-path: /health # 每次定时任务最多处理多少条企微分享链待队列(RPUSH 入队、LPOP 出队) diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 59d076c..49e5257 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -200,6 +200,8 @@ jarvis: # 物流接口服务地址 logistics: base-url: http://127.0.0.1:5001 + # 同机多进程多端口时配置逗号分隔列表;非空时仅按下列地址轮询,不再使用 base-url + base-urls: http://127.0.0.1:5001,http://127.0.0.1:5002,http://127.0.0.1:5003 fetch-path: /fetch_logistics health-path: /health adhoc-pending-batch-size: 50 diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java index 16f7758..2bcde4c 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/ILogisticsService.java @@ -68,6 +68,14 @@ public interface ILogisticsService { * @return 健康状态信息,包含是否健康、状态描述等 */ HealthCheckResult checkHealth(); + + /** + * 构造调用物流解析服务的完整 GET URL(路径与编码与 {@link #fetchLogisticsAndPush} 一致)。 + * 配置多个 {@code jarvis.server.logistics.base-urls} 时按轮询选取 base,便于内网多实例并行。 + * + * @param logisticsLink 原始物流追踪链接(未编码) + */ + String buildFetchLogisticsRequestUrl(String logisticsLink); /** * 健康检测结果 diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java index 58fd41f..6b92485 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/LogisticsServiceImpl.java @@ -21,13 +21,16 @@ import javax.annotation.Resource; import javax.annotation.PostConstruct; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.springframework.util.DigestUtils; @@ -58,6 +61,10 @@ public class LogisticsServiceImpl implements ILogisticsService { @Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}") private String logisticsBaseUrl; + + /** 逗号分隔的多个解析服务 base,非空时优先于此列表轮询;例如 http://10.0.0.1:5001,http://10.0.0.2:5001 */ + @Value("${jarvis.server.logistics.base-urls:}") + private String logisticsBaseUrlsRaw; @Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}") private String logisticsFetchPath; @@ -71,8 +78,10 @@ public class LogisticsServiceImpl implements ILogisticsService { @Resource private WeComShareLinkLogisticsJobMapper weComShareLinkLogisticsJobMapper; - private String externalApiUrlTemplate; - private String healthCheckUrl; + /** 已规范化(无尾部斜杠)的物流解析服务 base 列表,至少一项 */ + private List logisticsServiceBases = Collections.emptyList(); + private String logisticsServiceBasesSummary = ""; + private final AtomicInteger logisticsBaseRoundRobin = new AtomicInteger(0); @Resource private StringRedisTemplate stringRedisTemplate; @@ -88,10 +97,79 @@ public class LogisticsServiceImpl implements ILogisticsService { @PostConstruct public void init() { - externalApiUrlTemplate = logisticsBaseUrl + logisticsFetchPath + "?tracking_url="; - healthCheckUrl = logisticsBaseUrl + logisticsHealthPath; - logger.info("物流服务地址已初始化: {}", externalApiUrlTemplate); - logger.info("物流服务健康检查地址已初始化: {}", healthCheckUrl); + List list = new ArrayList<>(); + if (StringUtils.hasText(logisticsBaseUrlsRaw)) { + for (String part : logisticsBaseUrlsRaw.split(",")) { + String n = normalizeLogisticsBaseUrl(part); + if (StringUtils.hasText(n)) { + list.add(n); + } + } + } + if (list.isEmpty()) { + list.add(normalizeLogisticsBaseUrl(logisticsBaseUrl)); + } + logisticsServiceBases = Collections.unmodifiableList(list); + logisticsServiceBasesSummary = String.join(", ", logisticsServiceBases); + logger.info("物流服务实例 {} 个(轮询): {}", logisticsServiceBases.size(), logisticsServiceBasesSummary); + } + + private static String normalizeLogisticsBaseUrl(String base) { + if (!StringUtils.hasText(base)) { + return ""; + } + String t = base.trim(); + while (t.endsWith("/")) { + t = t.substring(0, t.length() - 1); + } + return t; + } + + private String pickLogisticsBaseUrl() { + int n = logisticsServiceBases.size(); + if (n == 0) { + return normalizeLogisticsBaseUrl(logisticsBaseUrl); + } + if (n == 1) { + return logisticsServiceBases.get(0); + } + int idx = Math.floorMod(logisticsBaseRoundRobin.getAndIncrement(), n); + return logisticsServiceBases.get(idx); + } + + @Override + public String buildFetchLogisticsRequestUrl(String logisticsLink) { + String base = pickLogisticsBaseUrl(); + try { + return base + logisticsFetchPath + "?tracking_url=" + URLEncoder.encode(logisticsLink, "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + /** + * 与原先单 URL 健康检查语义一致:JSON 字段命中则通过;非 JSON 时仅当正文含 ok/healthy/success 子串则通过。 + */ + private boolean isHealthResponseBodyHealthy(String healthResult) { + if (healthResult == null || healthResult.trim().isEmpty()) { + return false; + } + try { + JSONObject response = JSON.parseObject(healthResult); + if (response != null) { + String status = response.getString("status"); + Boolean healthy = response.getBoolean("healthy"); + Integer code = response.getInteger("code"); + if ("ok".equalsIgnoreCase(status) || "healthy".equalsIgnoreCase(status) + || Boolean.TRUE.equals(healthy) || (code != null && code == 200)) { + return true; + } + } + return false; + } catch (Exception e) { + String lowerResult = healthResult.toLowerCase(); + return lowerResult.contains("ok") || lowerResult.contains("healthy") || lowerResult.contains("success"); + } } @Override @@ -105,46 +183,26 @@ public class LogisticsServiceImpl implements ILogisticsService { @Override public ILogisticsService.HealthCheckResult checkHealth() { - try { - logger.debug("开始检查物流服务健康状态 - URL: {}", healthCheckUrl); - String healthResult = HttpUtils.sendGet(healthCheckUrl); - - if (healthResult == null || healthResult.trim().isEmpty()) { - logger.warn("物流服务健康检查返回空结果"); - return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查返回空结果", healthCheckUrl); - } - - // 尝试解析JSON响应 + List errors = new ArrayList<>(); + for (String base : logisticsServiceBases) { + String url = base + logisticsHealthPath; try { - JSONObject response = JSON.parseObject(healthResult); - if (response != null) { - // 检查常见的健康状态字段 - String status = response.getString("status"); - Boolean healthy = response.getBoolean("healthy"); - Integer code = response.getInteger("code"); - - if ("ok".equalsIgnoreCase(status) || "healthy".equalsIgnoreCase(status) || - Boolean.TRUE.equals(healthy) || (code != null && code == 200)) { - logger.debug("物流服务健康检查通过"); - return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常", healthCheckUrl); - } + logger.debug("开始检查物流服务健康状态 - URL: {}", url); + String healthResult = HttpUtils.sendGet(url); + if (isHealthResponseBodyHealthy(healthResult)) { + logger.debug("物流服务健康检查通过 - {}", url); + return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常: " + url, + logisticsServiceBasesSummary); } + logger.warn("物流服务健康检查失败 - URL: {} 响应: {}", url, healthResult); + errors.add(url + " -> " + (healthResult == null || healthResult.isEmpty() ? "空响应" : "状态异常")); } catch (Exception e) { - // 如果不是JSON格式,检查是否包含成功标识 - String lowerResult = healthResult.toLowerCase(); - if (lowerResult.contains("ok") || lowerResult.contains("healthy") || lowerResult.contains("success")) { - logger.debug("物流服务健康检查通过(非JSON格式)"); - return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常", healthCheckUrl); - } + logger.error("物流服务健康检查异常 - URL: {}, 错误: {}", url, e.getMessage(), e); + errors.add(url + " -> " + e.getMessage()); } - - logger.warn("物流服务健康检查失败 - 响应: {}", healthResult); - return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查返回异常状态: " + healthResult, healthCheckUrl); - - } catch (Exception e) { - logger.error("物流服务健康检查异常 - URL: {}, 错误: {}", healthCheckUrl, e.getMessage(), e); - return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查异常: " + e.getMessage(), healthCheckUrl); } + String msg = errors.isEmpty() ? "未配置物流解析实例" : String.join("; ", errors); + return new ILogisticsService.HealthCheckResult(false, "异常", msg, logisticsServiceBasesSummary); } /** @@ -183,7 +241,7 @@ public class LogisticsServiceImpl implements ILogisticsService { // 构建推送消息 StringBuilder pushContent = new StringBuilder(); pushContent.append("【物流服务异常提醒】\n"); - pushContent.append("服务地址:").append(healthCheckUrl).append("\n"); + pushContent.append("服务实例:").append(logisticsServiceBasesSummary).append("\n"); pushContent.append("失败原因:").append(reason).append("\n"); pushContent.append("时间:").append(new Date()).append("\n"); pushContent.append("请及时检查服务状态!"); @@ -277,8 +335,7 @@ public class LogisticsServiceImpl implements ILogisticsService { return false; } - // 构建外部接口URL - String externalUrl = externalApiUrlTemplate + URLEncoder.encode(logisticsLink, "UTF-8"); + String externalUrl = buildFetchLogisticsRequestUrl(logisticsLink); logger.info("调用外部接口获取物流信息 - 订单ID: {}, URL: {}", orderId, externalUrl); // 在服务端执行HTTP请求 @@ -700,7 +757,7 @@ public class LogisticsServiceImpl implements ILogisticsService { if (debug != null) { debug.put("healthOk", true); } - String externalUrl = externalApiUrlTemplate + URLEncoder.encode(url, "UTF-8"); + String externalUrl = buildFetchLogisticsRequestUrl(url); if (debug != null) { debug.put("requestUrl", externalUrl); }