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,213 @@
package com.yunhai.userhome.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.platform.utils.http.IDHttp;
import com.yunhai.userhome.config.MsgCenterProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* UserMsg 消息中心签名代理(§3.5 / BP-R3-1):IDHttp 出站自动签名(yh.app-auth 身份
* appKey=100001,UserMsg trusted-apps 已含),UserHome 前端不直连 UserMsg。
* <ul>
* <li>列表/角标:/oapi/msg/list、/oapi/msg/unreadCount、/oapi/msg/todo/list;</li>
* <li>已读:/oapi/msg/read、/oapi/msg/readAll(按当前登录人代理,userId 服务端取);</li>
* <li>jumpUrl 改写:jumpType=1 且 jumpPath 非空且 bizType 配了基址 → 基址 + "/#" + jumpPath
* (jumpPath 是裸相对路径如 /task/123,各应用落地页是 hash 路由)。</li>
* </ul>
* UserMsg 不可达时降级空结果(铃铛不 500),恢复后自愈。旧 user_message 本地表链路不动。
*/
@Slf4j
@Service
public class MsgCenterProxyService {
private final IDHttp http;
private final MsgCenterProperties properties;
private final ObjectMapper objectMapper;
public MsgCenterProxyService(IDHttp http, MsgCenterProperties properties, ObjectMapper objectMapper) {
this.http = http;
this.properties = properties;
this.objectMapper = objectMapper;
}
private boolean enabled() {
return StringUtils.hasText(properties.getUsermsgBaseUrl());
}
private String url(String pathAndQuery) {
String base = properties.getUsermsgBaseUrl().trim();
while (base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
return base + pathAndQuery;
}
/** 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的 jumpUrl */
public Map<String, Object> list(Integer userId, Integer pageIndex, Integer pageSize, Boolean onlyUnread) {
Map<String, Object> empty = emptyPage();
if (!enabled()) {
return empty;
}
try {
JsonNode data = http.get(url("/oapi/msg/list?userId=" + userId
+ "&pageIndex=" + (pageIndex == null || pageIndex < 1 ? 1 : pageIndex)
+ "&pageSize=" + (pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100))
+ "&onlyUnread=" + Boolean.TRUE.equals(onlyUnread)), JsonNode.class);
List<Map<String, Object>> rows = new ArrayList<>();
if (data != null && data.path("content").isArray()) {
for (JsonNode row : data.get("content")) {
rows.add(rewrite(row));
}
}
Map<String, Object> out = new HashMap<>();
out.put("total", data == null || !data.has("total") ? 0 : data.get("total").asInt());
out.put("list", rows);
return out;
} catch (Exception e) {
log.warn("[msgcenter] UserMsg 列表不可达(降级空)userId={}:{}", userId, e.toString());
return empty;
}
}
/** 铃铛摘要:未读数 + 各应用待办角标(停机各自降级 0/空) */
public Map<String, Object> summary(Integer userId, Integer schId) {
Map<String, Object> out = new HashMap<>();
out.put("unreadCount", 0L);
out.put("todos", new ArrayList<>());
if (!enabled()) {
return out;
}
try {
JsonNode unread = http.get(url("/oapi/msg/unreadCount?userId=" + userId), JsonNode.class);
out.put("unreadCount", unread == null || !unread.isNumber() ? 0L : unread.asLong());
} catch (Exception e) {
log.warn("[msgcenter] UserMsg 未读数不可达(降级 0)userId={}:{}", userId, e.toString());
}
try {
JsonNode todos = http.get(url("/oapi/msg/todo/list?userId=" + userId
+ (schId == null ? "" : "&schId=" + schId)), JsonNode.class);
List<Map<String, Object>> rows = new ArrayList<>();
if (todos != null && todos.isArray()) {
for (JsonNode t : todos) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("appKey", text(t, "appKey"));
row.put("count", t.has("count") ? t.get("count").asInt() : 0);
rows.add(row);
}
}
out.put("todos", rows);
} catch (Exception e) {
log.warn("[msgcenter] UserMsg 待办角标不可达(降级空)userId={}:{}", userId, e.toString());
}
return out;
}
/** 标记已读(代理按登录人发送,UserMsg 侧 UPDATE 带归属人防伪) */
public void read(Integer userId, List<Long> msgIds) {
if (!enabled() || msgIds == null || msgIds.isEmpty()) {
return;
}
Map<String, Object> body = new HashMap<>();
body.put("msgIds", msgIds);
body.put("userId", userId);
http.post(url("/oapi/msg/read"), body, JsonNode.class);
}
/** 全部已读 */
public void readAll(Integer userId) {
if (!enabled()) {
return;
}
Map<String, Object> body = new HashMap<>();
body.put("userId", userId);
http.post(url("/oapi/msg/readAll"), body, JsonNode.class);
}
/**
* WS 握手票据:以应用身份为登录人换一次性票据(UserMsg /oapi/msg/ws/ticket),
* 拼上浏览器可达的 WS 端点返回。未配置 WS 地址 / UserMsg 不可达 → null(前端维持轮询兜底表现)。
*/
public String createWsTicket(Integer userId) {
if (!enabled() || !StringUtils.hasText(properties.getUsermsgWsUrl())) {
return null;
}
try {
Map<String, Object> body = new HashMap<>();
body.put("userId", userId);
JsonNode data = http.post(url("/oapi/msg/ws/ticket"), body, JsonNode.class);
String ticket = data == null || !data.hasNonNull("ticket") ? null : data.get("ticket").asText();
if (!StringUtils.hasText(ticket)) {
return null;
}
String wsUrl = properties.getUsermsgWsUrl().trim();
return wsUrl + (wsUrl.contains("?") ? "&" : "?") + "ticket=" + ticket;
} catch (Exception e) {
log.warn("[msgcenter] UserMsg WS 票据不可达(降级 null,前端不建立长连接)userId={}:{}",
userId, e.toString());
return null;
}
}
// ===== 出参改写 =====
/** MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且配置了 bizType 基址时)+ 应用名补全 */
private Map<String, Object> rewrite(JsonNode row) {
Map<String, Object> out = new LinkedHashMap<>();
out.put("msgId", row.has("msgId") ? row.get("msgId").asLong() : null);
out.put("title", text(row, "title"));
out.put("content", text(row, "content"));
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("appKey", text(row, "appKey"));
String appKey = text(row, "appKey");
out.put("appName", appKey == null ? null : properties.getAppNames().get(appKey));
out.put("bizType", text(row, "bizType"));
out.put("bizId", text(row, "bizId"));
out.put("sendTime", text(row, "sendTime"));
out.put("isRead", row.has("isRead") && row.get("isRead").isNumber() ? row.get("isRead").asInt() : 0);
out.put("jumpUrl", jumpUrl(row));
return out;
}
/** 基址 + "/#" + jumpPath;未配置基址/非应用内跳转/无路径 → 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 bizType = text(row, "bizType");
if (jumpType == null || jumpType != 1 || !StringUtils.hasText(jumpPath)
|| !StringUtils.hasText(bizType)) {
return null;
}
String base = properties.getJumpBases().get(bizType);
if (!StringUtils.hasText(base)) {
return null;
}
String b = base.trim();
while (b.endsWith("/")) {
b = b.substring(0, b.length() - 1);
}
String path = jumpPath.startsWith("/") ? jumpPath : "/" + jumpPath;
return b + "/#" + path;
}
private static Map<String, Object> emptyPage() {
Map<String, Object> out = new HashMap<>();
out.put("total", 0);
out.put("list", new ArrayList<>());
return out;
}
private static String text(JsonNode node, String field) {
return node != null && node.has(field) && !node.get(field).isNull() ? node.get(field).asText() : null;
}
}