feat(msgcenter): jumpUrl 回落链——jumpApp 优先/bizType 回落/绝对 URL 直通(自指地址 Phase 2 接收侧)+ 本地补 flow.cc/flow.returned/svc_item 三键
This commit is contained in:
@@ -30,6 +30,11 @@ public class MsgCenterProperties {
|
||||
/** appKey → 应用显示名(消息行/弹层标签补全,如 100617 → 通知公告);未配置回退 msgType 文案 */
|
||||
private Map<String, String> appNames = new LinkedHashMap<>();
|
||||
|
||||
/** bizType → 应用 front 基址(jumpUrl = 基址 + "/#" + jumpPath);生产同主域用相对段如 /flow-center */
|
||||
/**
|
||||
* 基址表:key 兼容两种查询维度——「页面归属应用」(jumpApp,发送方代发时声明,优先)与
|
||||
* 「bizType」(发送方自己的消息分类,回落)。业务方应用名键(如 notice)与其主 bizType 键
|
||||
* 通常重合;细分分类(notice.expedite)由发送方配 jumpApp=notice 命中应用键,无需逐分类配键。
|
||||
* jumpUrl = 基址 + "/#" + jumpPath;生产同主域用相对段如 /flow-center。
|
||||
*/
|
||||
private Map<String, String> jumpBases = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ public class MsgCenterProxyService {
|
||||
|
||||
// ===== 出参改写 =====
|
||||
|
||||
/** MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且配置了 bizType 基址时)+ 应用名补全 */
|
||||
/** MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且查到基址时)+ 应用名补全 */
|
||||
private Map<String, Object> rewrite(JsonNode row) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("msgId", row.has("msgId") ? row.get("msgId").asLong() : null);
|
||||
@@ -191,6 +191,7 @@ public class MsgCenterProxyService {
|
||||
out.put("msgType", row.has("msgType") && row.get("msgType").isNumber() ? row.get("msgType").asInt() : null);
|
||||
out.put("jumpType", row.has("jumpType") && row.get("jumpType").isNumber() ? row.get("jumpType").asInt() : null);
|
||||
out.put("jumpPath", text(row, "jumpPath"));
|
||||
out.put("jumpApp", text(row, "jumpApp"));
|
||||
out.put("appKey", text(row, "appKey"));
|
||||
String appKey = text(row, "appKey");
|
||||
out.put("appName", appKey == null ? null : properties.getAppNames().get(appKey));
|
||||
@@ -202,16 +203,29 @@ public class MsgCenterProxyService {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 基址 + "/#" + jumpPath;未配置基址/非应用内跳转/无路径 → null(前端不渲染跳转) */
|
||||
/**
|
||||
* 基址 + "/#" + jumpPath。基址查询优先「页面归属应用」(jumpApp,代发场景页面属业务方时由
|
||||
* 发送方声明),缺省回落 bizType(=发送方自己的消息分类);都查不到返回 null(前端不渲染跳转)。
|
||||
* jumpPath 已是绝对 URL(http/https)时直接透传——兼容存量已落库的绝对地址消息,且不再
|
||||
* 重复拼基址产出双前缀畸形链接。非应用内跳转(jumpType≠1)/无路径 → null。
|
||||
*/
|
||||
private String jumpUrl(JsonNode row) {
|
||||
Integer jumpType = row.has("jumpType") && row.get("jumpType").isNumber() ? row.get("jumpType").asInt() : null;
|
||||
String jumpPath = text(row, "jumpPath");
|
||||
String jumpApp = text(row, "jumpApp");
|
||||
String bizType = text(row, "bizType");
|
||||
if (jumpType == null || jumpType != 1 || !StringUtils.hasText(jumpPath)
|
||||
|| !StringUtils.hasText(bizType)) {
|
||||
if (jumpType == null || jumpType != 1 || !StringUtils.hasText(jumpPath)) {
|
||||
return null;
|
||||
}
|
||||
String base = properties.getJumpBases().get(bizType);
|
||||
String path = jumpPath.trim();
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
String base = StringUtils.hasText(jumpApp)
|
||||
? properties.getJumpBases().get(jumpApp.trim()) : null;
|
||||
if (!StringUtils.hasText(base) && StringUtils.hasText(bizType)) {
|
||||
base = properties.getJumpBases().get(bizType);
|
||||
}
|
||||
if (!StringUtils.hasText(base)) {
|
||||
return null;
|
||||
}
|
||||
@@ -219,8 +233,8 @@ public class MsgCenterProxyService {
|
||||
while (b.endsWith("/")) {
|
||||
b = b.substring(0, b.length() - 1);
|
||||
}
|
||||
String path = jumpPath.startsWith("/") ? jumpPath : "/" + jumpPath;
|
||||
return b + "/#" + path;
|
||||
String p = path.startsWith("/") ? path : "/" + path;
|
||||
return b + "/#" + p;
|
||||
}
|
||||
|
||||
private static Map<String, Object> emptyPage() {
|
||||
|
||||
@@ -96,15 +96,21 @@ userhome:
|
||||
100617: 通知公告
|
||||
100019: 流程中心
|
||||
100001: 用户主页
|
||||
# bizType → 应用 front 基址:jumpPath 是裸相对路径(如 /task/123)且落地页是 hash 路由,
|
||||
# 代理拼 jumpUrl = 基址 + "/#" + jumpPath。生产(Nacos userhome.yaml 覆盖)同主域相对段:
|
||||
# jump-bases: { flow: /flow-center, flow.task: /flow-center, flow.result: /flow-center, flow.urge: /flow-center }
|
||||
# 基址表:key 兼容「页面归属应用」(jumpApp,发送方声明,优先)与「bizType」(回落)两个维度。
|
||||
# 跳转路径是裸相对路径(如 /task/123)且落地页是 hash 路由,代理拼 jumpUrl = 基址 + "/#" + jumpPath;
|
||||
# 已是绝对 URL 的存量消息直接透传(不二次拼基址)。生产(Nacos userhome.yaml 覆盖)同主域相对段:
|
||||
# jump-bases: { flow: /flow-center, flow.task: /flow-center, flow.result: /flow-center,
|
||||
# flow.urge: /flow-center, flow.cc: /flow-center, flow.returned: /flow-center,
|
||||
# notice: /notice, svc_item: /svc-hall }
|
||||
jump-bases:
|
||||
flow: http://localhost:8088
|
||||
flow.task: http://localhost:8088
|
||||
flow.result: http://localhost:8088
|
||||
flow.urge: http://localhost:8088
|
||||
flow.cc: http://localhost:8088
|
||||
flow.returned: http://localhost:8088
|
||||
notice: http://localhost:8077
|
||||
svc_item: http://localhost:8087
|
||||
|
||||
# [LOADTEST-METRICS-BEGIN] 临时埋点:健康探活 + prometheus 指标端点(方案 §3.3)。
|
||||
# Dockerfile 一直在探 /actuator/health,本段同时修复了探活 404 隐患;
|
||||
|
||||
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user