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);
+ }
+}