package com.yunhai.flowcenter.engine; import com.fasterxml.jackson.databind.ObjectMapper; import com.yunhai.platform.openapi.core.TargetSpec; import com.yunhai.platform.openapi.core.YschOpenCall; import com.yunhai.flowcenter.entity.FlwRouter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 条件路由判断调用客户端(2026-09-24 §4.3):POST {目录解析 host}{basePath}{path}?schId=, * 身份即本应用 appKey(100019)——签名拦截器由 {@link YschOpenCall} 装配链注入,与回调推送同构。 * 寻址:service_key 走服务目录三级解析(targets > services > 枚举兜底); * flw_router.base_path 非空时覆盖目录条目的 context-path(网关形态目录无前缀、实际服务有的场景), * 漏填必 401(规范 §4.2 同款坑)。direct_host 非空的自注册行跳过目录解析直连(2026-10-12 收口)。 * 信封解析复用 SDK({code,msg,data})。 * *

重试纪律:同步 3s×3 次退避重试(500ms/1500ms),业务方义务=幂等纯读轻实现。 * 全部失败抛 {@link JudgeUnavailableException}(由调用方落 outlet=NULL 留痕并挂起)。 */ @Slf4j @Service public class RouteJudgeClient { /** 判断接口单次超时(毫秒)与重试次数/退避(设计 §4.4:总预算 3s×3 退避重试) */ static final int TIMEOUT_MS = 3_000; static final int MAX_ATTEMPTS = 3; private static final long[] BACKOFF_MS = {500L, 1500L}; private final YschOpenCall openCall; private final ObjectMapper objectMapper; public RouteJudgeClient(YschOpenCall openCall, ObjectMapper objectMapper) { this.openCall = openCall; this.objectMapper = objectMapper; } /** 判断请求体(契约冻结:query 带 schId,业务方按 outlets 返回 {outlet};submitterId 2026-10-10 增补,老实现忽略即可) */ public static class JudgeBody { public final Long schId; public final Long instanceId; public final Integer instanceVersion; public final String bizType; public final String bizId; public final Integer submitterId; public final String nodeId; public final String nodeName; public final String routerKey; public final List outlets; public final Map variables; public JudgeBody(Long schId, Long instanceId, Integer instanceVersion, String bizType, String bizId, Integer submitterId, String nodeId, String nodeName, String routerKey, List outlets, Map variables) { this.schId = schId; this.instanceId = instanceId; this.instanceVersion = instanceVersion; this.bizType = bizType; this.bizId = bizId; this.submitterId = submitterId; this.nodeId = nodeId; this.nodeName = nodeName; this.routerKey = routerKey; this.outlets = outlets; this.variables = variables; } } /** 判断结果:出口名 + 原始返回(data 节点序列化,审计落 flw_route_log.raw_response) */ public static class JudgeResult { public final String outlet; public final String rawResponse; public JudgeResult(String outlet, String rawResponse) { this.outlet = outlet; this.rawResponse = rawResponse; } } /** 判断服务不可用(重试耗尽/网络/信封错误);outlet 未命中由调用方判定,不走本异常 */ public static class JudgeUnavailableException extends RuntimeException { public JudgeUnavailableException(String message, Throwable cause) { super(message, cause); } } @SuppressWarnings("unchecked") public JudgeResult call(FlwRouter router, JudgeBody body) { TargetSpec spec = resolveTarget(router); Map query = new HashMap<>(); query.put("schId", String.valueOf(body.schId)); RuntimeException last = null; for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { try { Map data = openCall.post(spec, router.getPath(), query, body, Map.class); String raw = objectMapper.writeValueAsString(data); Object outlet = data == null ? null : data.get("outlet"); if (outlet == null || String.valueOf(outlet).trim().isEmpty()) { throw new IllegalStateException("判断接口未返回 outlet:" + raw); } return new JudgeResult(String.valueOf(outlet).trim(), raw); } catch (RuntimeException e) { last = e; log.warn("[flow-center] 路由判断调用失败(第 {}/{} 次):router={} {}", attempt, MAX_ATTEMPTS, router.getRouterKey(), e.toString()); if (attempt < MAX_ATTEMPTS) { sleep(BACKOFF_MS[attempt - 1]); } } catch (Exception e) { last = new IllegalStateException(e.toString(), e); log.warn("[flow-center] 路由判断调用异常(第 {}/{} 次):router={} {}", attempt, MAX_ATTEMPTS, router.getRouterKey(), e.toString()); if (attempt < MAX_ATTEMPTS) { sleep(BACKOFF_MS[attempt - 1]); } } } throw new JudgeUnavailableException("判断服务调用失败(已重试 " + MAX_ATTEMPTS + " 次)", last); } /** * 目标解析: *

* 两分支同设判断专用 3s 超时。 */ private TargetSpec resolveTarget(FlwRouter router) { String directHost = router.getDirectHost(); if (directHost != null && !directHost.trim().isEmpty()) { return withJudgeTimeout(TargetSpec.fromUrl(directHost.trim())); } TargetSpec resolved = openCall.resolveTarget(router.getServiceKey()); String basePath = resolved.getBasePath(); if (router.getBasePath() != null && !router.getBasePath().trim().isEmpty()) { basePath = router.getBasePath().trim(); } return withJudgeTimeout(TargetSpec.of(resolved.getBaseHost(), basePath)); } private static TargetSpec withJudgeTimeout(TargetSpec spec) { spec.setConnectTimeoutMs((long) TIMEOUT_MS); spec.setReadTimeoutMs((long) TIMEOUT_MS); return spec; } private static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }