feat(msgcenter): jumpUrl 回落链——jumpApp 优先/bizType 回落/绝对 URL 直通(自指地址 Phase 2 接收侧)+ 本地补 flow.cc/flow.returned/svc_item 三键

This commit is contained in:
chenxing
2026-09-28 11:54:35 +08:00
parent 8492fba06b
commit 7924b2d654
4 changed files with 116 additions and 12 deletions
@@ -28,7 +28,8 @@ import static org.mockito.Mockito.when;
/**
* 消息中心代理单测(§3.5,BP-R3-1):冻结 UserMsg oapi 契约
* (list 信封 {total,content[]} / unreadCount 裸数字 / todo list 数组)与
* jumpUrl 改写规则(jumpType=1 + bizType 命中基址 → 基址 + "/#" + jumpPath)。
* jumpUrl 改写规则(jumpType=1 时:绝对 URL 直接透传;相对路径按「页面归属应用 jumpApp →
* bizType 回落」查基址 → 基址 + "/#" + jumpPath)。
* UserMsg 不可达降级空结果(铃铛不 500)。
* <p>
* 出站通道 2026-09-20 起由 IDHttp 换 YschOpenCall(泛化签名直调,Tier 0):
@@ -138,6 +139,84 @@ class MsgCenterProxyServiceTest {
assertNull(first.get("jumpUrl"), "bizType 未配基址 → 不改写(前端不渲染跳转)");
}
// ===== Phase 2(自指地址优化):归属优先 + 绝对 URL 透传 =====
/** 代发待办:bizType=flow.task 但页面属业务方(jumpApp=notice)→ 按 notice 基址拼(B1) */
@Test
void list_jumpAppPreferredOverBizType() throws Exception {
properties.getJumpBases().put("notice", "http://localhost:8077");
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)))
.thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
+ "\"jumpPath\":\"/list/detail?serid=1\",\"bizType\":\"flow.task\",\"jumpApp\":\"notice\","
+ "\"isRead\":0}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
assertEquals("http://localhost:8077/#/list/detail?serid=1", first.get("jumpUrl"),
"归属应用优先于 bizType 查基址");
assertEquals("notice", first.get("jumpApp"), "jumpApp 逐字段透传");
}
/** 存量兼容:改造前落库的绝对 URL 直接透传,不再拼基址(B5;双前缀缺陷即源于二次拼接) */
@Test
void list_absoluteJumpPath_passedThrough() throws Exception {
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)))
.thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
+ "\"jumpPath\":\"http://base.yunhaixingtong.com/notice/#/list/detail?serid=1\","
+ "\"bizType\":\"flow.task\",\"isRead\":0}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
assertEquals("http://base.yunhaixingtong.com/notice/#/list/detail?serid=1", first.get("jumpUrl"),
"绝对 URL 原样透传");
}
/** 灰度兼容:旧发送端未升级(无 jumpApp)→ 回落按 bizType 查(= 改造前行为) */
@Test
void list_jumpAppAbsent_fallsBackToBizType() throws Exception {
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)))
.thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"isRead\":0}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
assertEquals("http://localhost:8088/#/task/9", first.get("jumpUrl"), "无归属声明回落 bizType");
}
/** 归属键未配基址(如催办 notice.expedite 场景 jumpApp 未升级)→ 仍回落 bizType */
@Test
void list_jumpAppMiss_fallsBackToBizType() throws Exception {
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)))
.thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"jumpApp\":\"unknown-app\","
+ "\"isRead\":0}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
assertEquals("http://localhost:8088/#/task/9", first.get("jumpUrl"));
}
/** 归属与 bizType 都查不到基址 → null(不渲染跳转) */
@Test
void list_jumpAppAndBizTypeBothMiss_null() throws Exception {
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)))
.thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
+ "\"jumpPath\":\"/x\",\"bizType\":\"flow.nowhere\",\"jumpApp\":\"nowhere\","
+ "\"isRead\":0}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
assertNull(first.get("jumpUrl"));
}
/** 基址尾部斜杠归一保留:jumpApp 命中带尾斜杠的基址也不产出双斜杠 */
@Test
void list_jumpAppBaseTrailingSlash_normalized() throws Exception {
properties.getJumpBases().put("notice", "http://localhost:8077/");
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)))
.thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
+ "\"jumpPath\":\"list/detail?serid=1\",\"jumpApp\":\"notice\",\"isRead\":0}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
assertEquals("http://localhost:8077/#/list/detail?serid=1", first.get("jumpUrl"),
"基址尾斜杠归一 + 无前导斜杠路径补斜杠");
}
@Test
void list_userMsgDown_degradesToEmpty() {
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)))