feat(condition): SERVICE 判断收口——flw_router 增 direct_host/reg_app_key 两列(登记即持有,跨应用覆盖拒绝);新端点 POST /oapi/flow/router/reg(应用签名即归属);RouteJudgeClient 对 direct_host 行跳过服务目录解析直连(TargetSpec.fromUrl),存量手工行走目录解析零变化;契约测试与 SQL 留档
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.yunhai.flowcenter.controller.oapi;
|
||||
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.service.FlowRouterAdminService;
|
||||
import com.yunhai.flowcenter.service.OapiFlowService;
|
||||
import com.yunhai.platform.utils.model.BasePageResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -27,6 +28,7 @@ import java.util.List;
|
||||
* - GET /oapi/flow/diagram/url?instanceId=|flowGuid=&mode=&schId= → data={imgUrl,expiresIn}
|
||||
* - GET /oapi/flow/embed/url?instanceId=|bizType=&bizId=&mode=&schId= → data={pageUrl,expiresIn}
|
||||
* - POST /oapi/flow/callback/reg?schId= → data={secret}
|
||||
* - POST /oapi/flow/router/reg → data=null(判断服务自注册,归属=appKey)
|
||||
* 应用内审批(2026-09 第三步,原"刻意不提供审批动作"立场由该方案修订:登录身份由调用方
|
||||
* 应用按其 CAS 会话保证并以 operatorId 传入,本端按任务办理人逐字复核,审计链不断):
|
||||
* - GET /oapi/flow/tasks/todo?schId=&assigneeId=&bizType=&page=&size= → data=分页待办
|
||||
@@ -40,6 +42,7 @@ import java.util.List;
|
||||
public class OapiFlowController {
|
||||
|
||||
private final OapiFlowService flowService;
|
||||
private final FlowRouterAdminService routerAdminService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public List<OapiModels.FlowSummaryVo> list(@RequestParam("schId") Long schId) {
|
||||
@@ -124,6 +127,16 @@ public class OapiFlowController {
|
||||
return flowService.registerCallback(schId, appKey, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断服务登记(2026-10-12 SERVICE 收口):由 {@code yh-flow-callback-spring-boot-starter} 启动自注册调用。
|
||||
* 无 schId——{@code flw_router} 是全局表;归属即验签后的 appKey(登记即持有,跨应用覆盖拒绝)。
|
||||
*/
|
||||
@PostMapping("/router/reg")
|
||||
public void registerRouter(@RequestAttribute(value = "yh.app.auth", required = false) String appKey,
|
||||
@RequestBody OapiModels.RouterRegBody body) {
|
||||
routerAdminService.registerByApp(appKey, body);
|
||||
}
|
||||
|
||||
// ===== 应用内审批(第三步):查询 + 办理 =====
|
||||
|
||||
/** 我的待办(oapi):assigneeId 维度分页,bizType 可选过滤 */
|
||||
|
||||
@@ -16,7 +16,8 @@ import java.util.Map;
|
||||
* 身份即本应用 appKey(100019)——签名拦截器由 {@link YschOpenCall} 装配链注入,与回调推送同构。
|
||||
* 寻址:service_key 走服务目录三级解析(targets > services > 枚举兜底);
|
||||
* flw_router.base_path 非空时覆盖目录条目的 context-path(网关形态目录无前缀、实际服务有的场景),
|
||||
* 漏填必 401(规范 §4.2 同款坑)。信封解析复用 SDK({code,msg,data})。
|
||||
* 漏填必 401(规范 §4.2 同款坑)。<b>direct_host 非空的自注册行跳过目录解析直连</b>(2026-10-12 收口)。
|
||||
* 信封解析复用 SDK({code,msg,data})。
|
||||
*
|
||||
* <p>重试纪律:同步 3s×3 次退避重试(500ms/1500ms),业务方义务=幂等纯读轻实现。
|
||||
* 全部失败抛 {@link JudgeUnavailableException}(由调用方落 outlet=NULL 留痕并挂起)。
|
||||
@@ -121,14 +122,29 @@ public class RouteJudgeClient {
|
||||
throw new JudgeUnavailableException("判断服务调用失败(已重试 " + MAX_ATTEMPTS + " 次)", last);
|
||||
}
|
||||
|
||||
/** 目标解析:目录三级 + flw_router.base_path 覆盖(空=沿用目录条目前缀),判断专用 3s 超时 */
|
||||
/**
|
||||
* 目标解析:
|
||||
* <ul>
|
||||
* <li>{@code direct_host} 非空(2026-10-12 starter 自注册行)→ {@link TargetSpec#fromUrl(String)}
|
||||
* 拆基址直连,<b>跳过服务目录三级解析</b>(service_key/base_path 手工核对坑消失);</li>
|
||||
* <li>否则(存量手工行):目录三级 + flw_router.base_path 覆盖(空=沿用目录条目前缀),行为逐字不变。</li>
|
||||
* </ul>
|
||||
* 两分支同设判断专用 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();
|
||||
}
|
||||
TargetSpec spec = TargetSpec.of(resolved.getBaseHost(), basePath);
|
||||
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;
|
||||
|
||||
@@ -48,4 +48,18 @@ public class FlwRouter extends BaseEntity {
|
||||
/** 1 启用 / 0 停用(停用后发布校验拦截引用,存量在途办件重跑会失败挂起) */
|
||||
@Column(name = "status", nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
/**
|
||||
* 自注册直连基址 {@code scheme://authority[/ctx]}(yh-flow-callback starter 启动登记)。
|
||||
* 非空时 {@code RouteJudgeClient} 跳过服务目录三级解析直连——service_key/base_path 手工核对两坑消失;
|
||||
* 空=存量手工行,继续按目录寻址(老行为零改动)。
|
||||
*/
|
||||
@Column(name = "direct_host", length = 255)
|
||||
private String directHost;
|
||||
|
||||
/**
|
||||
* 登记应用 appKey(自注册归属:upsert 时校验,防跨应用覆盖);管理端手工行为空=无归属。
|
||||
*/
|
||||
@Column(name = "reg_app_key", length = 64)
|
||||
private String regAppKey;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,22 @@ public final class OapiModels {
|
||||
private String secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断服务登记请求体(POST /oapi/flow/router/reg,2026-10-12 SERVICE 收口):
|
||||
* 由 yh-flow-callback-spring-boot-starter 启动时自动提交,字段与 SDK
|
||||
* {@code com.yunhai.platform.openapi.flow.model.FlowRouterRegBody} 逐字对齐。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class RouterRegBody {
|
||||
private String routerKey;
|
||||
private String name;
|
||||
/** 判断端点路径(如 /oapi/flow/route/judge) */
|
||||
private String judgePath;
|
||||
/** 直连基址 scheme://authority[/ctx] */
|
||||
private String directHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* 办件进度(结构化轨迹,2026-09-16 用户拍板:SDK 对外提供图3/图4时间线数据源)。
|
||||
* steps 按 nodeId 任务到达顺序聚合(同人多任务行保留原始顺序),发起申请固定为第 0 步。
|
||||
|
||||
@@ -7,12 +7,14 @@ import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwRouter;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -23,6 +25,7 @@ import java.util.List;
|
||||
* 条件路由管理(2026-09-24 §4.2/§4.4):flw_router 注册 CRUD(全局表,管理员维护)
|
||||
* + 挂起办件的重跑路由入口。发布校验引用本表(routerKey 白名单),停用注册会拦住新发布。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FlowRouterAdminService {
|
||||
@@ -69,6 +72,75 @@ public class FlowRouterAdminService {
|
||||
return routerRepository.save(router);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自注册登记(2026-10-12 SERVICE 收口,由 yh-flow-callback starter 启动时调用)——<b>登记即持有</b>:
|
||||
* <ul>
|
||||
* <li>新键:建行,{@code path=judgePath}、{@code directHost} 非空(对端据此跳过服务目录三级解析
|
||||
* 直连),{@code serviceKey} 填归属 appKey(NOT NULL 列,管理端"来源"展示用),status=1;</li>
|
||||
* <li>已有行:{@code regAppKey} 非空且 ≠ 调用方 → 业务错拒绝(防跨应用覆盖);为空(管理端手工行)
|
||||
* → 首次自注册即认领。<b>status 不动</b>:停用状态不被自注册翻正,管理员保持控制权;</li>
|
||||
* <li>幂等:同 appKey 重复登记=更新地址,无 secret 轮换风险(与 callback 注册的差异)。</li>
|
||||
* </ul>
|
||||
* 管理端既有 CRUD({@link #save}/{@link #setStatus})不受影响——管理员仍可改一切。
|
||||
*/
|
||||
@Transactional
|
||||
public FlwRouter registerByApp(String appKey, OapiModels.RouterRegBody body) {
|
||||
if (isBlank(appKey)) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED, "判断服务登记需应用签名(appKey 为空)");
|
||||
}
|
||||
if (body == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "登记体不能为空");
|
||||
}
|
||||
String routerKey = requireLength(body.getRouterKey(), 64, "routerKey");
|
||||
String name = requireLength(body.getName(), 128, "name");
|
||||
String judgePath = body.getJudgePath() == null ? "" : body.getJudgePath().trim();
|
||||
if (!judgePath.startsWith("/")) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"judgePath 必须以 / 开头(如 /oapi/flow/route/judge)");
|
||||
}
|
||||
String directHost = body.getDirectHost() == null ? "" : body.getDirectHost().trim();
|
||||
if (!directHost.startsWith("http://") && !directHost.startsWith("https://")) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"directHost 必须是 http:// 或 https:// 开头的基址:" + directHost);
|
||||
}
|
||||
FlwRouter router = routerRepository.findByRouterKey(routerKey).orElse(null);
|
||||
if (router == null) {
|
||||
FlwRouter created = new FlwRouter();
|
||||
created.setRouterKey(routerKey);
|
||||
created.setName(name);
|
||||
created.setServiceKey(appKey);
|
||||
created.setPath(judgePath);
|
||||
created.setDirectHost(directHost);
|
||||
created.setRegAppKey(appKey);
|
||||
created.setStatus(1);
|
||||
log.info("[flow-center] 判断服务自注册新增:routerKey={} appKey={} host={} path={}",
|
||||
routerKey, appKey, directHost, judgePath);
|
||||
return routerRepository.save(created);
|
||||
}
|
||||
if (!isBlank(router.getRegAppKey()) && !router.getRegAppKey().equals(appKey)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN,
|
||||
"routerKey 已被应用 " + router.getRegAppKey() + " 登记:" + routerKey);
|
||||
}
|
||||
router.setName(name);
|
||||
router.setPath(judgePath);
|
||||
router.setDirectHost(directHost);
|
||||
router.setRegAppKey(appKey);
|
||||
log.info("[flow-center] 判断服务自注册更新:routerKey={} appKey={} host={} path={} status 保持不变={}",
|
||||
routerKey, appKey, directHost, judgePath, router.getStatus());
|
||||
return routerRepository.save(router);
|
||||
}
|
||||
|
||||
private static String requireLength(String raw, int max, String field) {
|
||||
String value = raw == null ? "" : raw.trim();
|
||||
if (value.isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, field + " 必填");
|
||||
}
|
||||
if (value.length() > max) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, field + " 长度超 " + max + ":" + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 启用/停用(停用拦新发布引用;存量在途办件重跑会失败挂起,注意时机) */
|
||||
@Transactional
|
||||
public FlwRouter setStatus(Long id, boolean enabled) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 2026-10-12 条件路由 SERVICE 收口:starter 启动自注册所需两列
|
||||
-- ddl-auto=update 会自动加列;本脚本为生产备案(生产建议停自动 DDL 后手工执行)
|
||||
-- direct_host 非空的行由 RouteJudgeClient 直连(跳过服务目录三级解析);存量手工行留 NULL 走原路
|
||||
ALTER TABLE `flw_router`
|
||||
ADD COLUMN `direct_host` VARCHAR(255) NULL COMMENT '自注册直连基址 scheme://authority[/ctx];NULL=按服务目录寻址(存量手工行)' AFTER `base_path`,
|
||||
ADD COLUMN `reg_app_key` VARCHAR(64) NULL COMMENT '登记应用 appKey(自注册归属,upsert 防跨应用覆盖);NULL=管理端手工行' AFTER `direct_host`;
|
||||
@@ -19,14 +19,15 @@ import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 路由判断客户端单测(2026-09-24 §4.3):重试纪律(3s×3 退避重试,Mock 出网)、
|
||||
* flw_router.base_path 对目录前缀的覆盖、信封 data.outlet 解析。
|
||||
* 引擎行为(选路/挂起/重跑)由 FlowEngineIT 覆盖。
|
||||
* flw_router.base_path 对目录前缀的覆盖、direct_host 自注册行直连(跳过目录解析,2026-10-12 收口)、
|
||||
* 信封 data.outlet 解析。引擎行为(选路/挂起/重跑)由 FlowEngineIT 覆盖。
|
||||
*/
|
||||
class RouteJudgeClientTest {
|
||||
|
||||
@@ -88,6 +89,55 @@ class RouteJudgeClientTest {
|
||||
assertEquals("/dirctx", spec.getValue().getBasePath(), "base_path 空=沿用目录条目 context-path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void directHost_非空时拆基址直连_不查服务目录() {
|
||||
FlwRouter r = router("");
|
||||
r.setDirectHost("http://open-gateway-service:10013/work-leave");
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("outlet", "甲线");
|
||||
when(openCall.post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(Map.class)))
|
||||
.thenReturn(data);
|
||||
|
||||
RouteJudgeClient.JudgeResult result = client.call(r, body());
|
||||
|
||||
assertEquals("甲线", result.outlet);
|
||||
ArgumentCaptor<TargetSpec> spec = ArgumentCaptor.forClass(TargetSpec.class);
|
||||
verify(openCall).post(spec.capture(), anyString(), anyMap(), any(), eq(Map.class));
|
||||
assertEquals("http://open-gateway-service:10013", spec.getValue().getBaseHost(), "direct_host 拆 authority");
|
||||
assertEquals("/work-leave", spec.getValue().getBasePath(), "direct_host 的 path 段即 context-path(签名剥离面)");
|
||||
assertEquals((Long) 3000L, spec.getValue().getReadTimeoutMs(), "直连仍用判断专用 3s 超时");
|
||||
verify(openCall, never()).resolveTarget(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void directHost_无path段_basePath为空() {
|
||||
FlwRouter r = router("");
|
||||
r.setDirectHost("http://work-leave:8080");
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("outlet", "甲线");
|
||||
when(openCall.post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(Map.class)))
|
||||
.thenReturn(data);
|
||||
|
||||
client.call(r, body());
|
||||
|
||||
ArgumentCaptor<TargetSpec> spec = ArgumentCaptor.forClass(TargetSpec.class);
|
||||
verify(openCall).post(spec.capture(), anyString(), anyMap(), any(), eq(Map.class));
|
||||
assertEquals("http://work-leave:8080", spec.getValue().getBaseHost());
|
||||
assertEquals("", spec.getValue().getBasePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void directHost为空_存量行仍走目录解析() {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("outlet", "甲线");
|
||||
when(openCall.post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(Map.class)))
|
||||
.thenReturn(data);
|
||||
|
||||
client.call(router(""), body());
|
||||
|
||||
verify(openCall).resolveTarget("notice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void transportFailure_retriesThreeTimes_thenThrows() {
|
||||
when(openCall.post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(Map.class)))
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.yunhai.flowcenter.oapi;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwRouter;
|
||||
import com.yunhai.flowcenter.service.DiagramTokenService;
|
||||
import com.yunhai.flowcenter.service.FlowRouterAdminService;
|
||||
import com.yunhai.flowcenter.service.OapiFlowService;
|
||||
import com.yunhai.flowcenter.support.FlowTestSupport;
|
||||
import com.yunhai.flowcenter.support.OapiSignedClient;
|
||||
@@ -50,6 +52,9 @@ class OapiFlowContractIT extends FlowTestSupport {
|
||||
@Autowired
|
||||
private OapiFlowService flowService;
|
||||
|
||||
@Autowired
|
||||
private FlowRouterAdminService routerAdminService;
|
||||
|
||||
@Autowired
|
||||
private DiagramTokenService tokenService;
|
||||
|
||||
@@ -507,6 +512,78 @@ class OapiFlowContractIT extends FlowTestSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断服务自注册契约(2026-10-12 SERVICE 收口,yh-flow-callback starter 的落点):未验签 401;
|
||||
* 登记即持有(新增行带 directHost、regAppKey/serviceKey=签名 appKey);同键重复登记幂等更新
|
||||
* (status 不被翻正);校验项业务码拒绝。跨应用覆盖拒绝见 FlowRouterAdminServiceRegisterTest。
|
||||
*/
|
||||
@Test
|
||||
void routerReg_selfRegistration_contract() {
|
||||
String reg = "{\"routerKey\":\"svc-hall.demo-judge\",\"name\":\"演示判断\","
|
||||
+ "\"judgePath\":\"/oapi/flow/route/judge\","
|
||||
+ "\"directHost\":\"http://open-gateway-service:10013/svc-hall\"}";
|
||||
|
||||
// 未验签(不带签名头)→ 401,不落行(本路径同受 /oapi/* 强制验签保护)
|
||||
assertEquals(401, client.getRaw("/oapi/flow/router/reg").getStatusCodeValue());
|
||||
assertFalse(routerRepository.findByRouterKey("svc-hall.demo-judge").isPresent());
|
||||
|
||||
// 登记 → 行落库,归属=签名 appKey
|
||||
assertEquals(0, client.post("/oapi/flow/router/reg", reg, JsonNode.class)
|
||||
.getBody().get("code").asInt(), "自注册应成功");
|
||||
FlwRouter row = routerRepository.findByRouterKey("svc-hall.demo-judge").get();
|
||||
assertEquals(APP_KEY, row.getRegAppKey());
|
||||
assertEquals(APP_KEY, row.getServiceKey(), "serviceKey 填归属 appKey(管理端『来源』展示用)");
|
||||
assertEquals("http://open-gateway-service:10013/svc-hall", row.getDirectHost());
|
||||
assertEquals("/oapi/flow/route/judge", row.getPath());
|
||||
assertEquals(1, row.getStatus().intValue());
|
||||
Long id = row.getId();
|
||||
|
||||
// 管理员停用后自注册 → 仍成功但不翻正 status(控制权在管理员)
|
||||
routerAdminService.setStatus(id, false);
|
||||
assertEquals(0, client.post("/oapi/flow/router/reg", reg, JsonNode.class).getBody().get("code").asInt());
|
||||
assertEquals(0, routerRepository.findByRouterKey("svc-hall.demo-judge").get().getStatus().intValue(),
|
||||
"停用状态不被自注册翻正");
|
||||
|
||||
// 重复登记(改名+换址)→ 同一行更新,不新增
|
||||
String again = "{\"routerKey\":\"svc-hall.demo-judge\",\"name\":\"演示判断2\","
|
||||
+ "\"judgePath\":\"/oapi/flow/route/judge2\",\"directHost\":\"http://new-host:9000/svc-hall\"}";
|
||||
assertEquals(0, client.post("/oapi/flow/router/reg", again, JsonNode.class)
|
||||
.getBody().get("code").asInt());
|
||||
FlwRouter updated = routerRepository.findByRouterKey("svc-hall.demo-judge").get();
|
||||
assertEquals(id, updated.getId(), "幂等 upsert,不应产生第二行");
|
||||
assertEquals("演示判断2", updated.getName());
|
||||
assertEquals("http://new-host:9000/svc-hall", updated.getDirectHost());
|
||||
assertEquals("/oapi/flow/route/judge2", updated.getPath());
|
||||
|
||||
// 校验项拒绝
|
||||
assertNotEquals(0, client.post("/oapi/flow/router/reg",
|
||||
"{\"routerKey\":\"a.b\",\"name\":\"n\",\"judgePath\":\"oapi/x\",\"directHost\":\"http://h:1\"}",
|
||||
JsonNode.class).getBody().get("code").asInt(), "judgePath 缺前导 / 应拒绝");
|
||||
assertNotEquals(0, client.post("/oapi/flow/router/reg",
|
||||
"{\"routerKey\":\"a.b\",\"name\":\"n\",\"judgePath\":\"/oapi/x\",\"directHost\":\"h:1\"}",
|
||||
JsonNode.class).getBody().get("code").asInt(), "directHost 非 http(s) 基址应拒绝");
|
||||
assertNotEquals(0, client.post("/oapi/flow/router/reg",
|
||||
"{\"name\":\"n\",\"judgePath\":\"/oapi/x\",\"directHost\":\"http://h:1\"}",
|
||||
JsonNode.class).getBody().get("code").asInt(), "routerKey 缺失应拒绝");
|
||||
}
|
||||
|
||||
/**
|
||||
* 自注册行天然满足 SERVICE 发布校验(2026-10-12 验收项):DefValidator 只认 routerKey
|
||||
* 已注册且启用,与 serviceKey 形态无关(自注册行 serviceKey 填的是数值 appKey)。
|
||||
*/
|
||||
@Test
|
||||
void routerReg_selfRegisteredRow_passesServicePublishValidation() {
|
||||
mapDept("km", 101, "甲");
|
||||
mapDept("kb", 102, "乙");
|
||||
String reg = "{\"routerKey\":\"svc-hall.cond-judge\",\"name\":\"条件判断\","
|
||||
+ "\"judgePath\":\"/oapi/flow/route/judge\",\"directHost\":\"http://h:1/svc-hall\"}";
|
||||
assertEquals(0, client.post("/oapi/flow/router/reg", reg, JsonNode.class)
|
||||
.getBody().get("code").asInt());
|
||||
|
||||
FlwDef def = saveAndPublishDef("t17", "自注册引用", conditionDef("svc-hall.cond-judge", "km", "kb"));
|
||||
assertEquals(1, def.getVersion().intValue(), "引用自注册行的 SERVICE 定义应发布成功");
|
||||
}
|
||||
|
||||
/** 轮询等异步回调推送(AFTER_COMMIT + 推送池),最长约 5s */
|
||||
private boolean awaitPayloadContains(List<String> payloads, String needle) throws InterruptedException {
|
||||
for (int i = 0; i < 25; i++) {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.yunhai.flowcenter.entity.FlwRouter;
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 判断服务自注册登记(2026-10-12 SERVICE 收口,/oapi/flow/router/reg 的服务层):
|
||||
* 登记即持有(新增行 serviceKey=归属 appKey、directHost 非空)、同 appKey 幂等更新且 status 不动、
|
||||
* 手工行首次自注册认领、跨应用覆盖拒绝、校验项与未验签(appKey 空)拒绝。
|
||||
*/
|
||||
class FlowRouterAdminServiceRegisterTest {
|
||||
|
||||
private static final String APP = "100021";
|
||||
|
||||
private FlwRouterRepository routerRepository;
|
||||
private FlowRouterAdminService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
routerRepository = mock(FlwRouterRepository.class);
|
||||
service = new FlowRouterAdminService(routerRepository,
|
||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwDefRepository.class),
|
||||
mock(com.yunhai.flowcenter.engine.FlowEngine.class),
|
||||
new com.fasterxml.jackson.databind.ObjectMapper());
|
||||
when(routerRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
}
|
||||
|
||||
private OapiModels.RouterRegBody body() {
|
||||
OapiModels.RouterRegBody b = new OapiModels.RouterRegBody();
|
||||
b.setRouterKey("work-leave.days-judge");
|
||||
b.setName("请假天数分级");
|
||||
b.setJudgePath("/oapi/flow/route/judge");
|
||||
b.setDirectHost("http://open-gateway-service:10013/work-leave");
|
||||
return b;
|
||||
}
|
||||
|
||||
private FlwRouter existing(String regAppKey, Integer status) {
|
||||
FlwRouter r = new FlwRouter();
|
||||
r.setId(7L);
|
||||
r.setRouterKey("work-leave.days-judge");
|
||||
r.setName("旧名");
|
||||
r.setServiceKey("work-leave");
|
||||
r.setPath("/oapi/legacy/judge");
|
||||
r.setBasePath("/work-leave");
|
||||
r.setRegAppKey(regAppKey);
|
||||
r.setStatus(status);
|
||||
return r;
|
||||
}
|
||||
|
||||
@Test
|
||||
void 新键_建行填归属并带直连基址() {
|
||||
when(routerRepository.findByRouterKey(anyString())).thenReturn(Optional.empty());
|
||||
|
||||
FlwRouter saved = service.registerByApp(APP, body());
|
||||
|
||||
ArgumentCaptor<FlwRouter> captor = ArgumentCaptor.forClass(FlwRouter.class);
|
||||
verify(routerRepository).save(captor.capture());
|
||||
FlwRouter row = captor.getValue();
|
||||
assertEquals("work-leave.days-judge", row.getRouterKey());
|
||||
assertEquals("请假天数分级", row.getName());
|
||||
assertEquals("/oapi/flow/route/judge", row.getPath(), "path=judgePath");
|
||||
assertEquals("http://open-gateway-service:10013/work-leave", row.getDirectHost());
|
||||
assertEquals(APP, row.getServiceKey(), "NOT NULL 列填归属 appKey(展示来源用)");
|
||||
assertEquals(APP, row.getRegAppKey());
|
||||
assertEquals(1, row.getStatus().intValue());
|
||||
assertNull(row.getBasePath(), "自注册行不填 base_path(直连不再需要目录前缀)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 同appKey重复登记_幂等更新地址_status不动() {
|
||||
when(routerRepository.findByRouterKey("work-leave.days-judge"))
|
||||
.thenReturn(Optional.of(existing(APP, 0)));
|
||||
OapiModels.RouterRegBody b = body();
|
||||
b.setName("改名后的判断");
|
||||
b.setDirectHost("http://new-host:9000/work-leave");
|
||||
|
||||
service.registerByApp(APP, b);
|
||||
|
||||
ArgumentCaptor<FlwRouter> captor = ArgumentCaptor.forClass(FlwRouter.class);
|
||||
verify(routerRepository).save(captor.capture());
|
||||
FlwRouter row = captor.getValue();
|
||||
assertEquals("改名后的判断", row.getName());
|
||||
assertEquals("http://new-host:9000/work-leave", row.getDirectHost());
|
||||
assertEquals("/oapi/flow/route/judge", row.getPath());
|
||||
assertEquals(0, row.getStatus().intValue(), "停用状态不被自注册翻正");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 管理端手工行_首次自注册即认领() {
|
||||
when(routerRepository.findByRouterKey("work-leave.days-judge"))
|
||||
.thenReturn(Optional.of(existing(null, 1)));
|
||||
|
||||
service.registerByApp(APP, body());
|
||||
|
||||
ArgumentCaptor<FlwRouter> captor = ArgumentCaptor.forClass(FlwRouter.class);
|
||||
verify(routerRepository).save(captor.capture());
|
||||
assertEquals(APP, captor.getValue().getRegAppKey());
|
||||
assertEquals("http://open-gateway-service:10013/work-leave", captor.getValue().getDirectHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 跨应用覆盖_拒绝并保留原行() {
|
||||
when(routerRepository.findByRouterKey("work-leave.days-judge"))
|
||||
.thenReturn(Optional.of(existing("100099", 1)));
|
||||
|
||||
BizException e = assertThrows(BizException.class, () -> service.registerByApp(APP, body()));
|
||||
|
||||
assertEquals(ErrorCode.FORBIDDEN, e.getCode());
|
||||
assertTrue(e.getMessage().contains("100099"), "错误信息要能定位占用方:" + e.getMessage());
|
||||
verify(routerRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 未验签调用_appKey空_拒绝() {
|
||||
assertEquals(ErrorCode.UNAUTHORIZED,
|
||||
assertThrows(BizException.class, () -> service.registerByApp(null, body())).getCode());
|
||||
assertEquals(ErrorCode.UNAUTHORIZED,
|
||||
assertThrows(BizException.class, () -> service.registerByApp(" ", body())).getCode());
|
||||
verify(routerRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 校验项逐个拒绝() {
|
||||
assertParam(reg -> reg.setRouterKey(" "));
|
||||
assertParam(reg -> reg.setRouterKey(repeat("k", 65)));
|
||||
assertParam(reg -> reg.setName(" "));
|
||||
assertParam(reg -> reg.setName(repeat("名", 129)));
|
||||
assertParam(reg -> reg.setJudgePath("oapi/flow/route/judge"));
|
||||
assertParam(reg -> reg.setDirectHost("open-gateway-service:10013/work-leave"));
|
||||
assertParam(reg -> reg.setDirectHost("ftp://host/work-leave"));
|
||||
// 校验全过:一条正向兜底,防"全部断言都靠拒绝"漏掉正常路径
|
||||
when(routerRepository.findByRouterKey(anyString())).thenReturn(Optional.empty());
|
||||
service.registerByApp(APP, body());
|
||||
verify(routerRepository).save(any());
|
||||
}
|
||||
|
||||
private void assertParam(java.util.function.Consumer<OapiModels.RouterRegBody> mutate) {
|
||||
OapiModels.RouterRegBody b = body();
|
||||
mutate.accept(b);
|
||||
BizException e = assertThrows(BizException.class, () -> service.registerByApp(APP, b));
|
||||
assertEquals(ErrorCode.PARAM_ERROR, e.getCode());
|
||||
}
|
||||
|
||||
private static String repeat(String s, int times) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < times; i++) {
|
||||
sb.append(s);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user