diff --git a/src/main/java/com/yunhai/userhome/controller/api/MessageCenterController.java b/src/main/java/com/yunhai/userhome/controller/api/MessageCenterController.java index d6a0d09..84edd69 100644 --- a/src/main/java/com/yunhai/userhome/controller/api/MessageCenterController.java +++ b/src/main/java/com/yunhai/userhome/controller/api/MessageCenterController.java @@ -41,17 +41,17 @@ public class MessageCenterController { } /** 消息分页列表:{total, list[]}(元素含改写后的绝对 jumpUrl;UserMsg 不可达降级空)。 - * 可选服务端筛选:msgType 0/1/2、appKey、isRead 0/1、keyword 标题/内容模糊(不传与现状一致)。 */ + * 可选服务端筛选:msgType 0/1/2、appId(注册表应用 id,代理翻译下行)、isRead 0/1、keyword 标题/内容模糊(不传与现状一致)。 */ @GetMapping("msgCenter/list") public Map list(BoaUser authUserWithDept, @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "count", required = false) Integer count, @RequestParam(value = "onlyUnread", required = false) Boolean onlyUnread, @RequestParam(value = "msgType", required = false) Integer msgType, - @RequestParam(value = "appKey", required = false) String appKey, + @RequestParam(value = "appId", required = false) String appId, @RequestParam(value = "isRead", required = false) Integer isRead, @RequestParam(value = "keyword", required = false) String keyword) { - return proxy.list(authUserWithDept.getId(), page, count, onlyUnread, msgType, appKey, isRead, keyword); + return proxy.list(authUserWithDept.getId(), page, count, onlyUnread, msgType, appId, isRead, keyword); } /** 铃铛摘要:未读数 + 各应用待办角标 */ @@ -60,10 +60,10 @@ public class MessageCenterController { return proxy.summary(authUserWithDept.getId(), resolveSchId(authUserWithDept)); } - /** 按来源应用未读数映射(消息中心「按应用」清单角标):{appKey: 未读数},无未读的 appKey 不出现 */ - @GetMapping("msgCenter/unreadCountByApp") - public Map unreadCountByApp(BoaUser authUserWithDept) { - return proxy.unreadCountByApp(authUserWithDept.getId()); + /** 按来源应用消息计数(消息中心「按应用」清单角标):[{appId, appName, total, unread}],总数含已读 */ + @GetMapping("msgCenter/countByApp") + public java.util.List> countByApp(BoaUser authUserWithDept) { + return proxy.countByApp(authUserWithDept.getId()); } /** diff --git a/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java b/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java index 55c2da2..bc35c8c 100644 --- a/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java +++ b/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java @@ -46,6 +46,9 @@ public class MsgCenterProxyService { /** UserBase 查址失败短路窗口:列表逐行查址,下游停摆时避免每行都吃一次连接超时 */ static final long USERBASE_RETRY_MS = 30_000L; + /** 应用字典 TTL:appKey↔appId 映射按用户缓存,到期或失败即重拉/沿用旧值 */ + static final long APP_DICT_TTL_MS = 300_000L; + /** 消息定位扫描上限:5 页 × 100 条(UserMsg 无按 id 直查端点,逐页扫描兜底) */ static final int MSG_SCAN_PAGES = 5; static final int MSG_SCAN_PAGE_SIZE = 100; @@ -56,6 +59,88 @@ public class MsgCenterProxyService { /** 0=正常;>0=UserBase 查址失败后的自愈时间戳(System.currentTimeMillis 基准) */ private volatile long userbaseDownUntil; + /** 应用字典按用户缓存(volatile 整体替换,并发重拉无害) */ + private final java.util.concurrent.ConcurrentHashMap appDictCache = new java.util.concurrent.ConcurrentHashMap<>(); + private final java.util.concurrent.ConcurrentHashMap appDictAt = new java.util.concurrent.ConcurrentHashMap<>(); + + /** 应用字典条目:注册表 appId / appKey / 名称(UserBase BoaApp) */ + private static final class AppDictEntry { + final int appId; + final String appKey; + final String name; + + AppDictEntry(int appId, String appKey, String name) { + this.appId = appId; + this.appKey = appKey; + this.name = name; + } + } + + /** 应用字典:byKey 正查(行富化/出参)+ byId 反查(appId 筛选参数翻译) */ + private static final class AppDict { + static final AppDict EMPTY = new AppDict(java.util.Collections.emptyMap(), java.util.Collections.emptyMap()); + final Map byKey; + final Map byId; + + AppDict(Map byKey, Map byId) { + this.byKey = byKey; + this.byId = byId; + } + } + + /** + * 应用字典(UserBase /oapi/app/list 按用户授权全量,TTL {@link #APP_DICT_TTL_MS})。 + * 失败/短路窗口内沿用旧字典(无旧值用空);出参不含 appSecret,仅取 id/appKey/name。 + */ + private AppDict appDict(Integer userId) { + if (userId == null) { + return AppDict.EMPTY; + } + AppDict cached = appDictCache.get(userId); + Long at = appDictAt.get(userId); + long now = System.currentTimeMillis(); + if (cached != null && at != null && now - at < APP_DICT_TTL_MS) { + return cached; + } + if (userbaseDown()) { + return cached != null ? cached : AppDict.EMPTY; + } + try { + Map q = new LinkedHashMap<>(); + q.put("userId", String.valueOf(userId)); + JsonNode groups = openCall.get(TARGET_USERBASE, "/oapi/app/list", q, JsonNode.class); + userbaseDownUntil = 0L; + Map byKey = new LinkedHashMap<>(); + Map byId = new LinkedHashMap<>(); + if (groups != null && groups.isArray()) { + for (JsonNode group : groups) { + JsonNode apps = group.path("apps"); + if (!apps.isArray()) { + continue; + } + for (JsonNode app : apps) { + if (app == null || !app.hasNonNull("id") || !app.hasNonNull("appKey")) { + continue; + } + AppDictEntry entry = new AppDictEntry(app.get("id").asInt(), + app.get("appKey").asText(), text(app, "name")); + byKey.put(entry.appKey, entry); + byId.put(entry.appId, entry); + } + } + } + AppDict dict = new AppDict(byKey, byId); + appDictCache.put(userId, dict); + appDictAt.put(userId, now); + return dict; + } catch (Exception e) { + userbaseDownUntil = now + USERBASE_RETRY_MS; + log.warn("[msgcenter] UserBase 应用字典不可达({}ms 内沿用旧字典/空)userId={}:{}", + USERBASE_RETRY_MS, userId, e.toString()); + return cached != null ? cached : AppDict.EMPTY; + } + } + public MsgCenterProxyService(YschOpenCall openCall, MsgCenterProperties properties) { this.openCall = openCall; this.properties = properties; @@ -83,17 +168,18 @@ public class MsgCenterProxyService { } /** - * 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的 jumpUrl 与 showJump 判权出参。 + * 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的绝对 jumpUrl 与 showJump 判权出参。 * 可选筛选(null/空白不透传,旧 UserMsg 收到未知参数会忽略——不传时行为与历史版本一致): - * msgType 0/1/2、appKey 精确、isRead 0/1、keyword 标题/内容模糊。 + * msgType 0/1/2、appId(注册表应用 id,代理翻译为发送方 appKey 后下行)、isRead 0/1、keyword 标题/内容模糊。 */ public Map list(Integer userId, Integer pageIndex, Integer pageSize, Boolean onlyUnread, - Integer msgType, String appKey, Integer isRead, String keyword) { + Integer msgType, String appId, Integer isRead, String keyword) { Map empty = emptyPage(); if (!enabled()) { return empty; } PerRequestAuthz authz = new PerRequestAuthz(userId); + AppDict dict = appDict(userId); try { Map query = new LinkedHashMap<>(); query.put("userId", String.valueOf(userId)); @@ -103,7 +189,13 @@ public class MsgCenterProxyService { if (msgType != null) { query.put("msgType", String.valueOf(msgType)); } - putIfText(query, "appKey", appKey); + if (StringUtils.hasText(appId)) { + AppDictEntry entry = dict.byId.get(Integer.valueOf(appId.trim())); + if (entry == null) { + return empty; // 字典查无此应用:视作该应用无消息 + } + query.put("appKey", entry.appKey); + } if (isRead != null) { query.put("isRead", String.valueOf(isRead)); } @@ -112,7 +204,7 @@ public class MsgCenterProxyService { List> rows = new ArrayList<>(); if (data != null && data.path("content").isArray()) { for (JsonNode row : data.get("content")) { - rows.add(rewrite(row, authz)); + rows.add(rewrite(row, authz, dict)); } } Map out = new HashMap<>(); @@ -149,6 +241,10 @@ public class MsgCenterProxyService { out.put("approvalUnread", vo.path("approvalUnread").asLong(0)); out.put("total", vo.path("total").asLong(0)); out.put("readCount", vo.path("readCount").asLong(0)); + // 分类总数(含已读)——左栏计数与列表内容对齐的浏览口径;旧版 UserMsg 无该字段时缺省,前端按降级处理 + out.put("sysTotal", vo.path("sysTotal").asLong(0)); + out.put("bizTotal", vo.path("bizTotal").asLong(0)); + out.put("approvalTotal", vo.path("approvalTotal").asLong(0)); summaryFetched = true; } } catch (Exception e) { @@ -189,26 +285,42 @@ public class MsgCenterProxyService { } /** - * 按来源应用未读数映射(UserMsg /oapi/msg/unreadCountByApp,{appKey: 未读数}): - * 消息中心「按应用」清单角标。口径同 unreadCount(过滤删除与过期),无未读的 appKey 不出现。 - * 停机降级空 Map(铃铛不 500)。 + * 按应用消息计数(UserMsg /oapi/msg/countByApp),条目富化注册表 appId/appName: + * [{appId, appName, total, unread}]——消息中心「按应用」清单角标(总数口径,前端隐藏 0 行)。 + * appId 取应用字典(按用户授权列表),字典查无(如授权已回收的来源应用)时 appId=null,前端不展示。 + * 停机降级空列表。 */ - public Map unreadCountByApp(Integer userId) { + public List> countByApp(Integer userId) { + List> empty = new ArrayList<>(); if (!enabled()) { - return new LinkedHashMap<>(); + return empty; } try { Map q = new LinkedHashMap<>(); q.put("userId", String.valueOf(userId)); - JsonNode data = get("/oapi/msg/unreadCountByApp", q); - Map out = new LinkedHashMap<>(); - if (data != null && data.isObject()) { - data.fields().forEachRemaining(e -> out.put(e.getKey(), e.getValue().asLong(0L))); + JsonNode arr = get("/oapi/msg/countByApp", q); + if (arr == null || !arr.isArray()) { + return empty; + } + AppDict dict = appDict(userId); + List> out = new ArrayList<>(); + for (JsonNode a : arr) { + String appKey = text(a, "appKey"); + if (appKey == null) { + continue; + } + AppDictEntry entry = dict.byKey.get(appKey); + Map row = new LinkedHashMap<>(); + row.put("appId", entry == null ? null : entry.appId); + row.put("appName", entry == null ? properties.getAppNames().get(appKey) : entry.name); + row.put("total", a.path("total").asLong(0)); + row.put("unread", a.path("unread").asLong(0)); + out.add(row); } return out; } catch (Exception e) { - log.warn("[msgcenter] UserMsg 按应用未读数不可达(降级空)userId={}:{}", userId, e.toString()); - return new LinkedHashMap<>(); + log.warn("[msgcenter] UserMsg 按应用计数不可达(降级空)userId={}:{}", userId, e.toString()); + return empty; } } @@ -295,8 +407,12 @@ public class MsgCenterProxyService { // ===== 出参改写 ===== - /** MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且查到基址时)+ showJump 判权 + 应用名补全 */ - private Map rewrite(JsonNode row, PerRequestAuthz authz) { + /** + * MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且查到基址时)+ showJump 判权 + 应用信息补全。 + * 应用标识对前端只出 appId/appName(应用字典富化):appKey 属后端签名标识不下发; + * 字典查无(授权已回收等)appId/appName 为 null,前端按类型标签降级。 + */ + private Map rewrite(JsonNode row, PerRequestAuthz authz, AppDict dict) { Map out = new LinkedHashMap<>(); out.put("msgId", row.has("msgId") ? row.get("msgId").asLong() : null); out.put("title", text(row, "title")); @@ -305,9 +421,10 @@ public class MsgCenterProxyService { 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)); + AppDictEntry entry = appKey == null ? null : dict.byKey.get(appKey); + out.put("appId", entry == null ? null : entry.appId); + out.put("appName", entry == null ? properties.getAppNames().get(appKey) : entry.name); out.put("bizType", text(row, "bizType")); out.put("bizId", text(row, "bizId")); out.put("sendTime", text(row, "sendTime"));