feat(sdk+msgcenter): 对齐 SDK 重构包名(openapi.client→base)+ 消息中心代理后端(09-16/09-19 遗漏提交)
build / build-push (push) Successful in 1m34s

- import 全面切换 com.yunhai.platform.openapi.base.*(对齐 Nexus -57 SDK)
- MsgCenter 代理三件(Properties/Controller/ProxyService)+ 测试 11/11(09-19 消息中心二期本地完成态)
- Dockerfile 已在 563edb5 去掉写死 profile;本提交补齐源码侧遗漏
This commit is contained in:
chenxing
2026-09-19 13:08:23 +08:00
parent ed8bbcdf45
commit 5f5ac3db71
14 changed files with 566 additions and 30 deletions
@@ -0,0 +1,169 @@
package com.yunhai.userhome.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.platform.http.DHttpException;
import com.yunhai.platform.utils.http.IDHttp;
import com.yunhai.userhome.config.MsgCenterProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 消息中心代理单测(§3.5,BP-R3-1):冻结 UserMsg oapi 契约
* (list 信封 {total,content[]} / unreadCount 裸数字 / todo list 数组)与
* jumpUrl 改写规则(jumpType=1 + bizType 命中基址 → 基址 + "/#" + jumpPath)。
* UserMsg 不可达降级空结果(铃铛不 500)。
*/
@DisplayName("MsgCenterProxyService - UserMsg 代理与 jumpUrl 改写")
class MsgCenterProxyServiceTest {
private IDHttp http;
private MsgCenterProperties properties;
private MsgCenterProxyService service;
@BeforeEach
void setUp() {
http = mock(IDHttp.class);
properties = new MsgCenterProperties();
properties.setUsermsgBaseUrl("http://localhost:8080");
properties.getJumpBases().put("flow.task", "http://localhost:8088/");
service = new MsgCenterProxyService(http, properties, new ObjectMapper());
}
private static com.fasterxml.jackson.databind.JsonNode json(String s) throws Exception {
return new ObjectMapper().readTree(s);
}
@Test
void list_rewritesJumpUrlForFlowTask() throws Exception {
properties.getAppNames().put("100617", "通知公告");
when(http.get(contains("/oapi/msg/list?userId=201"), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenReturn(json("{\"total\":2,\"content\":["
+ "{\"msgId\":1,\"title\":\"新待办\",\"content\":\"您有新的审批任务\",\"msgType\":2,"
+ "\"jumpType\":1,\"jumpPath\":\"/task/123\",\"appKey\":\"100019\",\"bizType\":\"flow.task\","
+ "\"bizId\":\"123\",\"sendTime\":\"2026-09-13 10:00:00\",\"isRead\":0},"
+ "{\"msgId\":2,\"title\":\"无跳转\",\"content\":\"系统通知\",\"msgType\":0,"
+ "\"jumpType\":0,\"jumpPath\":null,\"bizType\":null,\"isRead\":1}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
assertEquals(2, out.get("total"));
java.util.List<?> rows = (java.util.List<?>) out.get("list");
Map<?, ?> first = (Map<?, ?>) rows.get(0);
assertEquals("http://localhost:8088/#/task/123", first.get("jumpUrl"), "基址+/#+裸路径");
assertEquals("flow.task", first.get("bizType"));
assertNull(first.get("appName"), "未配置映射的 appKey 不补应用名");
assertNull(((Map<?, ?>) rows.get(1)).get("jumpUrl"), "非应用内跳转不改写");
}
@Test
void list_appNameMappedFromAppKey() throws Exception {
properties.getAppNames().put("100617", "通知公告");
when(http.get(contains("/oapi/msg/list"), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"title\":\"通知公告\","
+ "\"content\":\"您有一条通知\",\"msgType\":0,\"appKey\":\"100617\",\"isRead\":0}]}"));
Map<String, Object> out = service.list(201, 1, 10, null);
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
assertEquals("通知公告", first.get("appName"), "appKey 命中映射补应用名");
}
@Test
void wsTicket_returnsUrlWithTicket() throws Exception {
properties.setUsermsgWsUrl("ws://localhost:8080/ws/msg");
when(http.post(contains("/oapi/msg/ws/ticket"), any(Map.class), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenReturn(json("{\"ticket\":\"abc123ticket\",\"expiresIn\":60}"));
String url = service.createWsTicket(201);
assertEquals("ws://localhost:8080/ws/msg?ticket=abc123ticket", url);
}
@Test
void wsTicket_wsUrlNotConfigured_nullWithoutOutbound() {
properties.setUsermsgWsUrl("");
assertNull(service.createWsTicket(201));
verify(http, never()).post(anyString(), any(), any());
}
@Test
void wsTicket_userMsgDown_degradesNull() {
properties.setUsermsgWsUrl("ws://localhost:8080/ws/msg");
when(http.post(contains("/oapi/msg/ws/ticket"), any(Map.class), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenThrow(new DHttpException("down", 0));
assertNull(service.createWsTicket(201), "票据失败降级 null,前端不建立长连接");
}
@Test
void list_noBaseConfigured_jumpUrlNull() throws Exception {
properties.getJumpBases().clear();
when(http.get(contains("/oapi/msg/list"), eq(com.fasterxml.jackson.databind.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);
assertNull(first.get("jumpUrl"), "bizType 未配基址 → 不改写(前端不渲染跳转)");
}
@Test
void list_userMsgDown_degradesToEmpty() {
when(http.get(contains("/oapi/msg/list"), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenThrow(new DHttpException("DHttp request failed", 0));
Map<String, Object> out = service.list(201, 1, 10, null);
assertEquals(0, out.get("total"));
assertEquals(0, ((java.util.List<?>) out.get("list")).size());
}
@Test
void summary_combinesUnreadAndTodos_withPerCallDegrade() throws Exception {
when(http.get(contains("/oapi/msg/unreadCount"), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenReturn(json("3"));
when(http.get(contains("/oapi/msg/todo/list"), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenReturn(json("[{\"appKey\":\"100019\",\"count\":2}]"));
Map<String, Object> out = service.summary(201, 1);
assertEquals(3L, out.get("unreadCount"));
assertEquals(1, ((java.util.List<?>) out.get("todos")).size());
// 未读数不可达单独降级为 0,待办仍可用
when(http.get(contains("/oapi/msg/unreadCount"), eq(com.fasterxml.jackson.databind.JsonNode.class)))
.thenThrow(new DHttpException("down", 0));
Map<String, Object> out2 = service.summary(201, 1);
assertEquals(0L, out2.get("unreadCount"));
assertEquals(1, ((java.util.List<?>) out2.get("todos")).size());
}
@Test
void read_sendsOwnerScopedPayload() {
service.read(201, java.util.Collections.singletonList(7L));
verify(http).post(contains("/oapi/msg/read"), any(Map.class), eq(com.fasterxml.jackson.databind.JsonNode.class));
}
@Test
void disabled_degradesWithoutOutbound() {
properties.setUsermsgBaseUrl("");
assertEquals(0, service.list(201, 1, 10, null).get("total"));
service.read(201, java.util.Collections.singletonList(7L));
verify(http, never()).get(anyString(), any());
verify(http, never()).post(anyString(), any(), any());
}
@Test
void summary_disabled_degradesToZero() {
properties.setUsermsgBaseUrl("");
Map<String, Object> out = service.summary(201, 1);
assertEquals(0L, out.get("unreadCount"));
assertEquals(0, ((java.util.List<?>) out.get("todos")).size());
}
}