- summary 透传 sysTotal/bizTotal/approvalTotal 分类总数(含已读,UserMsg 新字段)
- 新代理端点 GET /msgCenter/countByApp([{appId, appName, total, unread}])替代 unreadCountByApp:UserMsg /oapi/msg/countByApp + 应用字典富化
- 新增应用字典(UserBase /oapi/app/list 按用户 5min TTL 缓存):appKey↔appId/name 映射,失败沿用旧值并进 30s 短路
- list 筛选参数 appKey→appId(代理翻译为 appKey 下行,字典查无返回空页);行出参 appKey 移除,替换为 appId/appName——appKey 属后端签名标识不再暴露前端
This commit is contained in:
@@ -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<Integer, AppDict> appDictCache = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.concurrent.ConcurrentHashMap<Integer, Long> 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<String, AppDictEntry> byKey;
|
||||
final Map<Integer, AppDictEntry> byId;
|
||||
|
||||
AppDict(Map<String, AppDictEntry> byKey, Map<Integer, AppDictEntry> 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<String, String> q = new LinkedHashMap<>();
|
||||
q.put("userId", String.valueOf(userId));
|
||||
JsonNode groups = openCall.get(TARGET_USERBASE, "/oapi/app/list", q, JsonNode.class);
|
||||
userbaseDownUntil = 0L;
|
||||
Map<String, AppDictEntry> byKey = new LinkedHashMap<>();
|
||||
Map<Integer, AppDictEntry> 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<String, Object> 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<String, Object> empty = emptyPage();
|
||||
if (!enabled()) {
|
||||
return empty;
|
||||
}
|
||||
PerRequestAuthz authz = new PerRequestAuthz(userId);
|
||||
AppDict dict = appDict(userId);
|
||||
try {
|
||||
Map<String, String> 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<Map<String, Object>> 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<String, Object> 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<String, Long> unreadCountByApp(Integer userId) {
|
||||
public List<Map<String, Object>> countByApp(Integer userId) {
|
||||
List<Map<String, Object>> empty = new ArrayList<>();
|
||||
if (!enabled()) {
|
||||
return new LinkedHashMap<>();
|
||||
return empty;
|
||||
}
|
||||
try {
|
||||
Map<String, String> q = new LinkedHashMap<>();
|
||||
q.put("userId", String.valueOf(userId));
|
||||
JsonNode data = get("/oapi/msg/unreadCountByApp", q);
|
||||
Map<String, Long> 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<Map<String, Object>> out = new ArrayList<>();
|
||||
for (JsonNode a : arr) {
|
||||
String appKey = text(a, "appKey");
|
||||
if (appKey == null) {
|
||||
continue;
|
||||
}
|
||||
AppDictEntry entry = dict.byKey.get(appKey);
|
||||
Map<String, Object> 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<String, Object> rewrite(JsonNode row, PerRequestAuthz authz) {
|
||||
/**
|
||||
* MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且查到基址时)+ showJump 判权 + 应用信息补全。
|
||||
* 应用标识对前端只出 appId/appName(应用字典富化):appKey 属后端签名标识不下发;
|
||||
* 字典查无(授权已回收等)appId/appName 为 null,前端按类型标签降级。
|
||||
*/
|
||||
private Map<String, Object> rewrite(JsonNode row, PerRequestAuthz authz, AppDict dict) {
|
||||
Map<String, Object> 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"));
|
||||
|
||||
Reference in New Issue
Block a user