- 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:
@@ -41,17 +41,17 @@ public class MessageCenterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 消息分页列表:{total, list[]}(元素含改写后的绝对 jumpUrl;UserMsg 不可达降级空)。
|
/** 消息分页列表:{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")
|
@GetMapping("msgCenter/list")
|
||||||
public Map<String, Object> list(BoaUser authUserWithDept,
|
public Map<String, Object> list(BoaUser authUserWithDept,
|
||||||
@RequestParam(value = "page", required = false) Integer page,
|
@RequestParam(value = "page", required = false) Integer page,
|
||||||
@RequestParam(value = "count", required = false) Integer count,
|
@RequestParam(value = "count", required = false) Integer count,
|
||||||
@RequestParam(value = "onlyUnread", required = false) Boolean onlyUnread,
|
@RequestParam(value = "onlyUnread", required = false) Boolean onlyUnread,
|
||||||
@RequestParam(value = "msgType", required = false) Integer msgType,
|
@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 = "isRead", required = false) Integer isRead,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
@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));
|
return proxy.summary(authUserWithDept.getId(), resolveSchId(authUserWithDept));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按来源应用未读数映射(消息中心「按应用」清单角标):{appKey: 未读数},无未读的 appKey 不出现 */
|
/** 按来源应用消息计数(消息中心「按应用」清单角标):[{appId, appName, total, unread}],总数含已读 */
|
||||||
@GetMapping("msgCenter/unreadCountByApp")
|
@GetMapping("msgCenter/countByApp")
|
||||||
public Map<String, Long> unreadCountByApp(BoaUser authUserWithDept) {
|
public java.util.List<java.util.Map<String, Object>> countByApp(BoaUser authUserWithDept) {
|
||||||
return proxy.unreadCountByApp(authUserWithDept.getId());
|
return proxy.countByApp(authUserWithDept.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ public class MsgCenterProxyService {
|
|||||||
/** UserBase 查址失败短路窗口:列表逐行查址,下游停摆时避免每行都吃一次连接超时 */
|
/** UserBase 查址失败短路窗口:列表逐行查址,下游停摆时避免每行都吃一次连接超时 */
|
||||||
static final long USERBASE_RETRY_MS = 30_000L;
|
static final long USERBASE_RETRY_MS = 30_000L;
|
||||||
|
|
||||||
|
/** 应用字典 TTL:appKey↔appId 映射按用户缓存,到期或失败即重拉/沿用旧值 */
|
||||||
|
static final long APP_DICT_TTL_MS = 300_000L;
|
||||||
|
|
||||||
/** 消息定位扫描上限:5 页 × 100 条(UserMsg 无按 id 直查端点,逐页扫描兜底) */
|
/** 消息定位扫描上限:5 页 × 100 条(UserMsg 无按 id 直查端点,逐页扫描兜底) */
|
||||||
static final int MSG_SCAN_PAGES = 5;
|
static final int MSG_SCAN_PAGES = 5;
|
||||||
static final int MSG_SCAN_PAGE_SIZE = 100;
|
static final int MSG_SCAN_PAGE_SIZE = 100;
|
||||||
@@ -56,6 +59,88 @@ public class MsgCenterProxyService {
|
|||||||
/** 0=正常;>0=UserBase 查址失败后的自愈时间戳(System.currentTimeMillis 基准) */
|
/** 0=正常;>0=UserBase 查址失败后的自愈时间戳(System.currentTimeMillis 基准) */
|
||||||
private volatile long userbaseDownUntil;
|
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) {
|
public MsgCenterProxyService(YschOpenCall openCall, MsgCenterProperties properties) {
|
||||||
this.openCall = openCall;
|
this.openCall = openCall;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
@@ -83,17 +168,18 @@ public class MsgCenterProxyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的 jumpUrl 与 showJump 判权出参。
|
* 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的绝对 jumpUrl 与 showJump 判权出参。
|
||||||
* 可选筛选(null/空白不透传,旧 UserMsg 收到未知参数会忽略——不传时行为与历史版本一致):
|
* 可选筛选(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,
|
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();
|
Map<String, Object> empty = emptyPage();
|
||||||
if (!enabled()) {
|
if (!enabled()) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
PerRequestAuthz authz = new PerRequestAuthz(userId);
|
PerRequestAuthz authz = new PerRequestAuthz(userId);
|
||||||
|
AppDict dict = appDict(userId);
|
||||||
try {
|
try {
|
||||||
Map<String, String> query = new LinkedHashMap<>();
|
Map<String, String> query = new LinkedHashMap<>();
|
||||||
query.put("userId", String.valueOf(userId));
|
query.put("userId", String.valueOf(userId));
|
||||||
@@ -103,7 +189,13 @@ public class MsgCenterProxyService {
|
|||||||
if (msgType != null) {
|
if (msgType != null) {
|
||||||
query.put("msgType", String.valueOf(msgType));
|
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) {
|
if (isRead != null) {
|
||||||
query.put("isRead", String.valueOf(isRead));
|
query.put("isRead", String.valueOf(isRead));
|
||||||
}
|
}
|
||||||
@@ -112,7 +204,7 @@ public class MsgCenterProxyService {
|
|||||||
List<Map<String, Object>> rows = new ArrayList<>();
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
if (data != null && data.path("content").isArray()) {
|
if (data != null && data.path("content").isArray()) {
|
||||||
for (JsonNode row : data.get("content")) {
|
for (JsonNode row : data.get("content")) {
|
||||||
rows.add(rewrite(row, authz));
|
rows.add(rewrite(row, authz, dict));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map<String, Object> out = new HashMap<>();
|
Map<String, Object> out = new HashMap<>();
|
||||||
@@ -149,6 +241,10 @@ public class MsgCenterProxyService {
|
|||||||
out.put("approvalUnread", vo.path("approvalUnread").asLong(0));
|
out.put("approvalUnread", vo.path("approvalUnread").asLong(0));
|
||||||
out.put("total", vo.path("total").asLong(0));
|
out.put("total", vo.path("total").asLong(0));
|
||||||
out.put("readCount", vo.path("readCount").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;
|
summaryFetched = true;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -189,26 +285,42 @@ public class MsgCenterProxyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按来源应用未读数映射(UserMsg /oapi/msg/unreadCountByApp,{appKey: 未读数}):
|
* 按应用消息计数(UserMsg /oapi/msg/countByApp),条目富化注册表 appId/appName:
|
||||||
* 消息中心「按应用」清单角标。口径同 unreadCount(过滤删除与过期),无未读的 appKey 不出现。
|
* [{appId, appName, total, unread}]——消息中心「按应用」清单角标(总数口径,前端隐藏 0 行)。
|
||||||
* 停机降级空 Map(铃铛不 500)。
|
* 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()) {
|
if (!enabled()) {
|
||||||
return new LinkedHashMap<>();
|
return empty;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Map<String, String> q = new LinkedHashMap<>();
|
Map<String, String> q = new LinkedHashMap<>();
|
||||||
q.put("userId", String.valueOf(userId));
|
q.put("userId", String.valueOf(userId));
|
||||||
JsonNode data = get("/oapi/msg/unreadCountByApp", q);
|
JsonNode arr = get("/oapi/msg/countByApp", q);
|
||||||
Map<String, Long> out = new LinkedHashMap<>();
|
if (arr == null || !arr.isArray()) {
|
||||||
if (data != null && data.isObject()) {
|
return empty;
|
||||||
data.fields().forEachRemaining(e -> out.put(e.getKey(), e.getValue().asLong(0L)));
|
}
|
||||||
|
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;
|
return out;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[msgcenter] UserMsg 按应用未读数不可达(降级空)userId={}:{}", userId, e.toString());
|
log.warn("[msgcenter] UserMsg 按应用计数不可达(降级空)userId={}:{}", userId, e.toString());
|
||||||
return new LinkedHashMap<>();
|
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<>();
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
out.put("msgId", row.has("msgId") ? row.get("msgId").asLong() : null);
|
out.put("msgId", row.has("msgId") ? row.get("msgId").asLong() : null);
|
||||||
out.put("title", text(row, "title"));
|
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("jumpType", row.has("jumpType") && row.get("jumpType").isNumber() ? row.get("jumpType").asInt() : null);
|
||||||
out.put("jumpPath", text(row, "jumpPath"));
|
out.put("jumpPath", text(row, "jumpPath"));
|
||||||
out.put("jumpApp", text(row, "jumpApp"));
|
out.put("jumpApp", text(row, "jumpApp"));
|
||||||
out.put("appKey", text(row, "appKey"));
|
|
||||||
String 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("bizType", text(row, "bizType"));
|
||||||
out.put("bizId", text(row, "bizId"));
|
out.put("bizId", text(row, "bizId"));
|
||||||
out.put("sendTime", text(row, "sendTime"));
|
out.put("sendTime", text(row, "sendTime"));
|
||||||
|
|||||||
Reference in New Issue
Block a user