refactor(sdk): 出站全端点收敛到服务目录命名目标 + 零专属地址配置
build / build-push (push) Successful in 3m20s

- FlowCenterClient:apply/getInstanceByBiz/registerCallback 由 ad-hoc TargetSpec
  改走 YschFlowOpenApi 门面(ApplyBody/CallbackRegBody/ApplyResultVo/CallbackRegVo
  继承 SDK DTO);instance 走 raw() 预绑视图保留 Map 全字段透传。
- CmsSiteDirectoryImpl:DIRECTORY_KEY 包可见;目录未配且 svcchall.cms.base-url
  缺失时构造期 fail-fast(原 @Value 无默认值会直接炸启动,无明确提示)。
- FlowCenterClient:svchall.flow.base-url 默认值去掉 + 同款 fail-fast(enabled=false 不校验)。
- application.yml:新增 yh.open.services.{flowcenter,cms},删 svchall.cms.base-url /
  svchall.flow.base-url —— 地址唯一来源=服务目录。
- 测试:新增 FlowCenterClientFacadeTest(6 例,冻结门面/归一目标/回落/fail-fast);
  CmsSiteDirectoryImplTest 加目录命名目标 + fail-fast 用例。112 测试全绿。
This commit is contained in:
chenxing
2026-09-16 21:04:28 +08:00
parent 47beb4eaa4
commit e462b71bef
6 changed files with 336 additions and 46 deletions
@@ -471,11 +471,35 @@ public final class SvcModels {
private Integer pageSize = 10;
}
// ===== 流程(管理端回显) =====
// ===== 流程(管理端回显 + flow-center 门面出入参) =====
/** 字段单一事实源=SDK 门面 DTO(可直接作 YschFlowOpenApi 泛型出入参) */
@Data
@lombok.EqualsAndHashCode(callSuper = true)
public static class FlowSummaryVo extends com.yunhai.platform.openapi.flow.model.FlowSummary {
}
/** 代发起请求体(门面 {@code YschFlowOpenApi.apply} 的 B 绑定;字段继承 SDK FlowApplyBody) */
@Data
@lombok.EqualsAndHashCode(callSuper = true)
public static class ApplyBody extends com.yunhai.platform.openapi.flow.model.FlowApplyBody {
}
/** 代发起响应(门面 R 绑定;{@code duplicated=true} 表示命中 biz uk 幂等返回既有办件) */
@Data
@lombok.EqualsAndHashCode(callSuper = true)
public static class ApplyResultVo extends com.yunhai.platform.openapi.flow.model.FlowApplyResult {
}
/** 回调注册请求体(门面 {@code YschFlowOpenApi.registerCallback} 的 B 绑定) */
@Data
@lombok.EqualsAndHashCode(callSuper = true)
public static class CallbackRegBody extends com.yunhai.platform.openapi.flow.model.FlowCallbackRegBody {
}
/** 回调注册响应(下发的 secret 用于验 X-Flw-Signature) */
@Data
@lombok.EqualsAndHashCode(callSuper = true)
public static class CallbackRegVo extends com.yunhai.platform.openapi.flow.model.FlowCallbackRegResult {
}
}
@@ -43,21 +43,28 @@ public class CmsSiteDirectoryImpl implements SiteDirectory {
.build();
/** 服务目录命名目标(cms 未晋升枚举,按名直调);目录未配置时为 null=走 legacy ad-hoc spec */
private static final String DIRECTORY_KEY = "cms";
static final String DIRECTORY_KEY = "cms";
private static final String LEGACY_KEY = "svchall.cms.base-url";
private final YschOpenCall openCall;
private final TargetSpec cmsSpec;
private final boolean directoryResolved;
public CmsSiteDirectoryImpl(YschOpenCall openCall,
@Value("${svchall.cms.base-url}") String cmsBaseUrl) {
@Value("${svchall.cms.base-url:}") String cmsBaseUrl) {
this.openCall = openCall;
this.directoryResolved = openCall.hasTarget(DIRECTORY_KEY);
this.directoryResolved = openCall != null && openCall.hasTarget(DIRECTORY_KEY);
if (directoryResolved) {
this.cmsSpec = null;
return;
}
String base = trimTrailing(cmsBaseUrl == null ? "" : cmsBaseUrl.trim());
if (base.isEmpty()) {
// 目录未配且 legacy 键也空:构造期 fail-fast——别把"地址没配"伪装成"站点服务不可达"
throw new IllegalStateException("CMS 站点目录调用地址未配置:请配置服务目录 yh.open.services."
+ DIRECTORY_KEY + "(推荐,容器直连完整 URL)或回落键 " + LEGACY_KEY);
}
URI uri = URI.create(base);
String host = uri.getScheme() + "://" + uri.getRawAuthority();
String path = uri.getRawPath() == null ? "" : uri.getRawPath();
@@ -42,20 +42,27 @@ public class FlowCenterClient implements FlowClient {
public FlowCenterClient(YschOpenCall openCall, ObjectMapper objectMapper,
@Value("${svchall.flow.enabled:false}") boolean enabled,
@Value("${svchall.flow.base-url:http://localhost:8085}") String baseUrl) {
@Value("${svchall.flow.base-url:}") String baseUrl) {
this.openCall = openCall;
this.objectMapper = objectMapper;
this.enabled = enabled;
String base = trimTrailing(baseUrl == null ? "" : baseUrl.trim());
URI uri = URI.create(base);
String host = uri.getScheme() + "://" + uri.getRawAuthority();
String path = uri.getRawPath() == null ? "" : uri.getRawPath();
this.flowSpec = TargetSpec.of(host, path);
this.directoryResolved = openCall.hasTarget(
this.directoryResolved = openCall != null && openCall.hasTarget(
com.yunhai.platform.openapi.core.PlatformService.FLOWCENTER.key());
if (directoryResolved) {
com.yunhai.platform.openapi.flow.YschFlowOpenApi.setCall(openCall);
}
if (enabled && base.isEmpty() && !directoryResolved) {
// 目录未配且 legacy 键也空:构造期 fail-fast——别把"地址没配"伪装成"流程中心不可达"。
// enabled=false(显式未接入)时不校验——地址本就不需要。
throw new IllegalStateException("流程中心调用地址未配置:请配置服务目录 yh.open.services."
+ com.yunhai.platform.openapi.core.PlatformService.FLOWCENTER.key()
+ "(推荐,容器直连完整 URL)或回落键 svchall.flow.base-url");
}
URI uri = URI.create(base);
String host = uri.getScheme() + "://" + uri.getRawAuthority();
String path = uri.getRawPath() == null ? "" : uri.getRawPath();
this.flowSpec = TargetSpec.of(host, path);
}
@Override
@@ -97,26 +104,30 @@ public class FlowCenterClient implements FlowClient {
String bizUrl, String bizSnapshot, Integer submitterId, String submitterName,
String formDataJson) {
requireEnabled();
Map<String, Object> body = new HashMap<>();
body.put("flowGuid", flowGuid);
body.put("bizType", bizType);
body.put("bizId", bizId);
body.put("bizTitle", bizTitle);
if (bizUrl != null) {
body.put("bizUrl", bizUrl);
}
if (bizSnapshot != null) {
body.put("bizSnapshot", bizSnapshot);
}
body.put("submitterId", submitterId);
body.put("submitterName", submitterName);
SvcModels.ApplyBody body = new SvcModels.ApplyBody();
body.setFlowGuid(flowGuid);
body.setBizType(bizType);
body.setBizId(bizId);
body.setBizTitle(bizTitle);
body.setBizUrl(bizUrl);
body.setBizSnapshot(bizSnapshot);
body.setSubmitterId(submitterId);
body.setSubmitterName(submitterName);
if (formDataJson != null && !formDataJson.isEmpty()) {
try {
body.put("formData", objectMapper.readValue(formDataJson, Map.class));
body.setFormData(objectMapper.readValue(formDataJson, Map.class));
} catch (Exception e) {
throw new BizException(ErrorCode.PARAM_ERROR, "表单数据不是合法 JSON 对象");
}
}
if (directoryResolved) {
SvcModels.ApplyResultVo result = com.yunhai.platform.openapi.flow.YschFlowOpenApi.apply(
schId, body, SvcModels.ApplyResultVo.class);
if (result == null || result.getInstanceId() == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "流程中心代发起返回异常(缺少 instanceId)");
}
return new ApplyResult(result.getInstanceId(), result.isDuplicated());
}
JsonNode data = openCall.post(flowSpec, "/oapi/flow/apply",
Collections.singletonMap("schId", String.valueOf(schId)), body, JsonNode.class);
if (data == null || data.get("instanceId") == null) {
@@ -133,24 +144,38 @@ public class FlowCenterClient implements FlowClient {
query.put("schId", String.valueOf(schId));
query.put("bizType", bizType);
query.put("bizId", bizId);
if (directoryResolved) {
// 门面 instance(...) 只反序列化 FlowInstance 的字段子集;本方法契约是 Map 全字段透传
// (对账/查看守卫要 status/version/assigneeIds/nodeSummary…),故走预绑 flowcenter 的
// 泛化视图 raw()——目标名由门面承载,不再回落应用专属地址。
return com.yunhai.platform.openapi.flow.YschFlowOpenApi.raw()
.get("/oapi/flow/instance", query, Map.class);
}
return openCall.get(flowSpec, "/oapi/flow/instance", query, Map.class);
}
@Override
public String registerCallback(Long schId, String flowGuid, String callbackUrl, String eventTypes) {
requireEnabled();
Map<String, Object> body = new HashMap<>();
body.put("flowGuid", flowGuid);
body.put("callbackUrl", callbackUrl);
if (eventTypes != null && !eventTypes.isEmpty()) {
body.put("eventTypes", eventTypes);
}
SvcModels.CallbackRegBody body = new SvcModels.CallbackRegBody();
body.setFlowGuid(flowGuid);
body.setCallbackUrl(callbackUrl);
body.setEventTypes(eventTypes);
// 申报本服务 context-path(从回调 URL path 段推导,去掉尾部 /callback/flow):
// 推送方按它剥离后再签应用签名,与本服务 AppAuthFilter/FlowCallbackVerifyFilter 剥
// contextPath 后的 target 对齐——本地裸地址(无 ctx)推导为空串,行为不变。
// contextPath 后的 target 对齐——本地裸地址(无 ctx)推导为空串,行为不变(不下发空串)。
String basePath = deriveCallbackBasePath(callbackUrl);
if (basePath != null && !basePath.isEmpty()) {
body.put("basePath", basePath);
body.setBasePath(basePath);
}
if (directoryResolved) {
SvcModels.CallbackRegVo result = com.yunhai.platform.openapi.flow.YschFlowOpenApi
.registerCallback(schId, body, SvcModels.CallbackRegVo.class);
Object secret = result == null ? null : result.getSecret();
if (secret == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "流程中心回调注册返回异常(缺少 secret)");
}
return String.valueOf(secret);
}
Map<?, ?> data = openCall.post(flowSpec, "/oapi/flow/callback/reg",
Collections.singletonMap("schId", String.valueOf(schId)), body, Map.class);
+16 -14
View File
@@ -59,13 +59,17 @@ yh:
trusted-apps:
100005: 2c7885de594742cc840b8762910b2d2e
open:
enabled: true
# 身份密钥唯一来源 = yh.app-auth.app-key/app-secret(本文件 app-auth 段),open.* 副本已删
base-host: http://base.yunhaixingtong.com
base-path: /userbase
app-key: 100018
# 联调态密钥(与 flow-center trusted-apps["100018"] 逐字一致);
# 注意旧占位"裸 32 个 0"会被 YAML 按八进制整数解析成 "0",签名时 SignUtil 校验直接失败,必须带字母+引号
app-secret: "svc-hall-dev-secret-100018"
# 服务目录(子应用配置优化方案 §4.2):跨服务出站唯一地址来源,命名目标走 SDK 门面 /
# YschOpenCall。cms 未晋升 PlatformService 枚举(按名直调);flowcenter 已入枚举,此处
# 显式覆盖为本地端口。完整 URL 拆 host+ctx,per-target base-path 与提供方 context-path
# 对齐 → 签名天然一致。生产改容器直连:flowcenter=http://flow-center-service:10008/flow-center、
# cms=http://yschcms-service:10005/yschcms。
services:
flowcenter: http://localhost:8085
cms: http://base.yunhaixingtong.com:10005/yschcms
# ===== 办事大厅专属配置 =====
svchall:
@@ -78,17 +82,15 @@ svchall:
# 服务部门"容器组"类型(UserBase GroupTypeEnum,默认 7=教师组;口径承袭 CMS cms.svc.item.dept-group-type)
dept-group-type: 7
cms:
# CMS 基址(站点目录 /oapi/site/list 数据源)。必须指向**后端**且带 context-path:
# 内网 CMS 挂在 base 域下 /yschcms,但 Caddy 只反代 /yschcms/{front,api,portal},
# 没有 /oapi 路由——用 http://base.yunhaixingtong.com/yschcms 打 /oapi/* 会拿到 Caddy 404,
# 因此走容器直连端口 10005(实测 /yschcms/oapi/site/list 能到应用)。
# 本机起 CMS(Cms/local-debug/start-cms.bat)时改用 http://localhost:8072/cms;
# 生产由 projman 注入覆盖。
base-url: http://base.yunhaixingtong.com:10005/yschcms
# CMS 基址已迁至服务目录 yh.open.services.cms(上方 open 段)。
# 务必指向**后端**且带 context-path:内网 CMS 挂在 base 域下 /yschcms,但 Caddy 只反代
# /yschcms/{front,api,portal},没有 /oapi 路由——用 http://base.yunhaixingtong.com/yschcms
# 打 /oapi/* 会拿到 Caddy 404,因此必须是容器直连端口 10005
# (实测 /yschcms/oapi/site/list 能到应用)。
flow:
# 流程中心 flow-center 基址;2026-09-12 联调置 true(本地 8085 已部署 flow-center M1)
# 流程中心 flow-center 基址已迁至服务目录 yh.open.services.flowcenter(上方 open 段)。
# enabled=false(本地未部署 flow-center)时全部方法抛业务异常,消费方按需降级。
enabled: true
base-url: http://localhost:8085
# 对账 Job 应急闸(契约冻结"消费方免每日对账":flow-center 持续重试+死信+挂起告警为主链路)。
# 默认 false 停用;仅大面积漂移事故应急时置 true 开闸(每日 04:00 跑)
reconcile-enabled: false