From b3c532616395236837e762a66a16119eeb16aa2f Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 16 Sep 2026 21:06:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(flow):=20=E5=9B=9E=E8=B0=83=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E8=BF=87=E6=9C=9F=E5=80=BC=E4=BF=AE=E6=AD=A3=20+=20?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E7=BC=BA=E5=A4=B1=20fail-fast=20+=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=9B=AE=E5=BD=95=E6=98=BE=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - application.yml:notice.flow.callback-path 由过期的 /callback/flow 改回 /oapi/notice/flow/callback(/callback/* 已随 X-Flw-* 过滤器下线,旧值会让本地注册的 回调地址 404);新增 yh.open.services.flowcenter,删 notice.flow.base-url。 - NoticeFlowProperties.baseUrl 默认值去掉(localhost:8085 静默回落是隐患); FlowCenterClient 在 enabled=true 且「目录与 legacy 键都缺」时构造期 fail-fast (enabled=false 不校验——显式未接入本就不需要地址)。 - 测试:新增 NoticeFlowCallbackConfigContractTest(反射拼控制器映射,冻结 callback-path/callback-base-path 与控制器/Runtime ctx 的咬合);FlowCenterClientTest 补 fail-fast 与 disabled 两态。68 测试全绿。 --- .../notice/service/flow/FlowCenterClient.java | 9 ++ .../service/flow/NoticeFlowProperties.java | 9 +- src/main/resources/application.yml | 13 ++- .../service/flow/FlowCenterClientTest.java | 29 +++++- .../NoticeFlowCallbackConfigContractTest.java | 88 +++++++++++++++++++ 5 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/yunhai/notice/service/flow/NoticeFlowCallbackConfigContractTest.java diff --git a/src/main/java/com/yunhai/notice/service/flow/FlowCenterClient.java b/src/main/java/com/yunhai/notice/service/flow/FlowCenterClient.java index fa05251..705c99f 100644 --- a/src/main/java/com/yunhai/notice/service/flow/FlowCenterClient.java +++ b/src/main/java/com/yunhai/notice/service/flow/FlowCenterClient.java @@ -58,6 +58,15 @@ public class FlowCenterClient implements FlowClient { if (directoryResolved) { com.yunhai.platform.openapi.flow.YschFlowOpenApi.setCall(call); } + String legacy = properties.getBaseUrl() == null ? "" : properties.getBaseUrl().trim(); + if (properties.isEnabled() && !directoryResolved && legacy.isEmpty()) { + // 目录未配且 legacy 键也空:构造期 fail-fast(本键已无默认值), + // 避免静默打 localhost 把"地址没配"伪装成"流程中心不可达"。 + // enabled=false(显式未接入)时不校验——地址本就不需要。 + throw new IllegalStateException("流程中心调用地址未配置:请配置服务目录 yh.open.services." + + com.yunhai.platform.openapi.core.PlatformService.FLOWCENTER.key() + + "(推荐,容器直连完整 URL)或回落键 notice.flow.base-url"); + } } @Override diff --git a/src/main/java/com/yunhai/notice/service/flow/NoticeFlowProperties.java b/src/main/java/com/yunhai/notice/service/flow/NoticeFlowProperties.java index 25c3e0e..0e7d2d3 100644 --- a/src/main/java/com/yunhai/notice/service/flow/NoticeFlowProperties.java +++ b/src/main/java/com/yunhai/notice/service/flow/NoticeFlowProperties.java @@ -15,8 +15,13 @@ public class NoticeFlowProperties { /** 是否接入流程中心(false=编辑页选择器隐藏、FlowClient 全部抛业务异常降级) */ private boolean enabled = true; - /** flow-center 基址(Tier 0 目标;本地 http://localhost:8085,生产由 projman 注入) */ - private String baseUrl = "http://localhost:8085"; + /** + * flow-center 基址的 legacy 回落(Tier 0 ad-hoc TargetSpec 路径)。 + * 主路径是服务目录 {@code yh.open.services.flowcenter} → {@code YschFlowOpenApi} 门面; + * 本键仅迁移期兼容,不再给默认值——两者都空时 {@code FlowCenterClient} 构造期 + * fail-fast,避免静默打 localhost 把"地址没配"伪装成"流程中心不可达"。 + */ + private String baseUrl = ""; /** * 本应用对外可达基址(回调注册用):flow-center 探活与事件推送都打这里。 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 79485f8..c3394c0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -54,11 +54,14 @@ sms: notice: flow: enabled: true - # flow-center 基址(Tier 0 目标,裸地址直连 base-path 空) - base-url: http://localhost:8085 + # flow-center 地址不再单独配置:见上方 yh.open.services.flowcenter(SDK 门面 YschFlowOpenApi)。 + # legacy 回落键 notice.flow.base-url 已删,且代码侧不再给默认值——两者都缺时启动即失败 + # (构造期 fail-fast),不静默打 localhost。 # 本应用对外可达基址(flow-center 回调探活/事件推送打这里;本地=后端直连端口) callback-base: http://localhost:8101 - callback-path: /callback/flow + # 回调端点:必须与 OapiFlowCallbackController 映射一致(/callback/* 已随 X-Flw-* 过滤器下线, + # 旧值会让本地注册的回调地址 404)。不变式由 NoticeFlowCallbackConfigContractTest 冻结。 + callback-path: /oapi/notice/flow/callback # Notice 前端基址(apply bizUrl 拼接,审批人点击跳回通知详情) front-base-url: http://localhost:8077 # 短信到点补发调度(§3.2 修 BP-R5-2):fixedDelay 5min,扫"已发布+要短信+未发过+已到点" @@ -95,6 +98,10 @@ yh: open: # 四域服务目录零配置:userbase/userclass/usermsg/flowcenter 枚举兜底 = base-host + 默认 ctx base-host: http://base.yunhaixingtong.com + # flowcenter 显式指向本地流程中心(默认枚举兜底会打 base 域网关,网关无 /oapi 路由); + # 生产改容器直连 http://flow-center-service:10008/flow-center(projman 注入)。 + services: + flowcenter: http://localhost:8085 # env-config 前端运行时配置(免登录 GET /api/env-config?target=front,数据源见 # config/PropertyEnvConfigSource:聚合 starter 自动装配,有 bean 即注册端点)。 # 本地无需配值(前端 dev 走 .env.dev);线上值经环境变量 YH_ENV_CONFIG_SOURCE_FRONT diff --git a/src/test/java/com/yunhai/notice/service/flow/FlowCenterClientTest.java b/src/test/java/com/yunhai/notice/service/flow/FlowCenterClientTest.java index 4f877bf..d55d445 100644 --- a/src/test/java/com/yunhai/notice/service/flow/FlowCenterClientTest.java +++ b/src/test/java/com/yunhai/notice/service/flow/FlowCenterClientTest.java @@ -11,6 +11,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -141,6 +142,30 @@ class FlowCenterClientTest { return new FlowCenterClient(props, new FixedProvider<>(openCall), null, objectMapper); } + @Test + @DisplayName("目录与 legacy 键皆未配且 enabled=true:构造期 fail-fast(不静默打 localhost)") + void misconfiguredFailsFast() { + NoticeFlowProperties props = new NoticeFlowProperties(); // baseUrl 已无默认值 + YschOpenCall openCall = new YschOpenCall(new YschCallProperties(), objectMapper, + (host, basePath) -> java.util.Collections.emptyList()); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new FlowCenterClient(props, new FixedProvider<>(openCall), null, objectMapper)); + assertTrue(e.getMessage().contains("yh.open.services.flowcenter"), e.getMessage()); + assertTrue(e.getMessage().contains("notice.flow.base-url"), e.getMessage()); + } + + @Test + @DisplayName("enabled=false 且地址全缺:不 fail-fast(显式未接入,地址本就不需要)") + void disabledWithoutAddressIsAllowed() { + NoticeFlowProperties props = new NoticeFlowProperties(); + props.setEnabled(false); + YschOpenCall openCall = new YschOpenCall(new YschCallProperties(), objectMapper, + (host, basePath) -> java.util.Collections.emptyList()); + FlowCenterClient client = new FlowCenterClient(props, new FixedProvider<>(openCall), null, objectMapper); + BizException ex = assertThrows(BizException.class, () -> client.listFlows(1)); + assertTrue(ex.getMessage().contains("流程中心未接入"), ex.getMessage()); + } + private FlowCenterClient newClient() { return newClient(true); } @@ -198,13 +223,13 @@ class FlowCenterClientTest { void registerCallback_contractFields() { FlowModels.CallbackRegBody body = new FlowModels.CallbackRegBody(); body.setFlowGuid("fg-1"); - body.setCallbackUrl("http://localhost:8101/callback/flow"); + body.setCallbackUrl("http://localhost:8101/oapi/notice/flow/callback"); FlowModels.CallbackRegResultVo result = newClient().registerCallback(1, body); assertEquals("reg-secret-0123456789abcdef", result.getSecret()); Captured c = captured.get(0); assertEquals("POST", c.method); assertEquals("/oapi/flow/callback/reg", c.path); - assertEquals("http://localhost:8101/callback/flow", c.body.get("callbackUrl").asText()); + assertEquals("http://localhost:8101/oapi/notice/flow/callback", c.body.get("callbackUrl").asText()); } @Test diff --git a/src/test/java/com/yunhai/notice/service/flow/NoticeFlowCallbackConfigContractTest.java b/src/test/java/com/yunhai/notice/service/flow/NoticeFlowCallbackConfigContractTest.java new file mode 100644 index 0000000..af23159 --- /dev/null +++ b/src/test/java/com/yunhai/notice/service/flow/NoticeFlowCallbackConfigContractTest.java @@ -0,0 +1,88 @@ +package com.yunhai.notice.service.flow; + +import com.yunhai.notice.controller.oapi.OapiFlowCallbackController; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.yaml.snakeyaml.Yaml; + +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * 回调路径配置契约测试(防"过期值"回归)。

+ * 不变式:{@code notice.flow.callback-path}(注册给 flow-center 的回调地址尾段)必须与 + * {@link OapiFlowCallbackController} 的真实映射逐字一致——不一致时 flow-center 探活/推送全部 + * 404,而本地单测因为走伪服务端不会发现。
+ * 背景:{@code /callback/*} 自 X-Flw-* 自定义过滤器下线后已不存在,但本地 {@code application.yml} + * 一度仍写着 {@code /callback/flow}(生产 yml 与代码默认值都是 {@code /oapi/notice/flow/callback}), + * 导致本地起链时注册到已下线的路径。 + */ +@DisplayName("Notice 回调路径配置 - 与控制器映射咬合") +class NoticeFlowCallbackConfigContractTest { + + /** 控制器真实映射(反射拼类级 @RequestMapping + 方法级 @PostMapping) */ + private static String controllerMapping() { + RequestMapping classLevel = OapiFlowCallbackController.class.getAnnotation(RequestMapping.class); + assertNotNull(classLevel, "控制器必须有类级 @RequestMapping"); + String base = classLevel.value()[0]; + for (Method m : OapiFlowCallbackController.class.getDeclaredMethods()) { + PostMapping post = m.getAnnotation(PostMapping.class); + if (post != null) { + String sub = post.value().length > 0 ? post.value()[0] : ""; + return base + sub; + } + } + throw new IllegalStateException("控制器缺少 @PostMapping 回调方法"); + } + + @SuppressWarnings("unchecked") + private static Object yamlValue(String path) throws Exception { + try (InputStream in = NoticeFlowCallbackConfigContractTest.class.getClassLoader() + .getResourceAsStream("application.yml")) { + assertNotNull(in, "application.yml 必须在测试类路径上"); + Map root = new Yaml().load(in); + Object cur = root; + for (String seg : path.split("\\.")) { + if (!(cur instanceof Map)) { + return null; + } + cur = ((Map) cur).get(seg); + if (cur == null) { + return null; + } + } + return cur; + } + } + + @Test + @DisplayName("application.yml 的 notice.flow.callback-path 与控制器映射一致(缺省=代码默认值)") + void callbackPathMatchesControllerMapping() throws Exception { + String expected = controllerMapping(); + assertEquals("/oapi/notice/flow/callback", expected, "控制器路径变了,请同步生产配置与注册契约"); + + Object configured = yamlValue("notice.flow.callback-path"); + String actual = configured == null + ? new NoticeFlowProperties().getCallbackPath() // 未配置 → 代码默认值 + : String.valueOf(configured); + assertEquals(expected, actual, + "notice.flow.callback-path 与控制器映射不一致:flow-center 派发会 404"); + } + + @Test + @DisplayName("application.yml 的 notice.flow.callback-base-path 与部署 context-path 一致(缺省=空)") + void callbackBasePathMatchesContextPath() throws Exception { + Object configured = yamlValue("notice.flow.callback-base-path"); + String actual = configured == null ? "" : String.valueOf(configured); + // 本地进程无 context-path(server.servlet.context-path 未设)→ 申报空串; + // 生产由 projman 注入 /notice(见 deploy/notice.yaml)。配错的表现是回调 401。 + Object contextPath = yamlValue("server.servlet.context-path"); + assertEquals(contextPath == null ? "" : String.valueOf(contextPath), actual); + } +}