From 570b59329336901361261180ba221410b99d1c93 Mon Sep 17 00:00:00 2001 From: chenxing Date: Fri, 9 Oct 2026 12:23:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(msgcenter):=20=E6=B6=88=E6=81=AF=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E6=94=B9=E8=B5=B0=20SDK=20msg=20=E8=AF=BB=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=B9=B6=E5=B8=A6=E5=BD=93=E5=89=8D=E6=BF=80=E6=B4=BB?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E5=AD=A6=E6=A0=A1=E5=8F=A3=E5=BE=84=EF=BC=88?= =?UTF-8?q?resolveReadSchId=E2=86=92/oapi/identity/current=EF=BC=8C0/?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E9=99=8D=E7=BA=A7=20null=EF=BC=89=EF=BC=9Bto?= =?UTF-8?q?do=20=E8=A7=92=E6=A0=87=E6=B2=BF=E7=94=A8=E4=B8=BB=E5=AD=A6?= =?UTF-8?q?=E6=A0=A1=E5=8F=A3=E5=BE=84=E4=B8=8D=E5=8F=98=EF=BC=9B=E7=9B=B4?= =?UTF-8?q?=E6=89=B9=E6=A0=87=E8=AF=BB=E8=81=94=E5=8A=A8=E5=B8=A6=E5=8F=A3?= =?UTF-8?q?=E5=BE=84=EF=BC=88=E6=B6=88=E6=81=AF=E6=8C=89=E5=AD=A6=E6=A0=A1?= =?UTF-8?q?=E9=9A=94=E7=A6=BB=20=E6=89=B9=E6=AC=A13=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/MessageCenterController.java | 28 +- .../service/MsgApprovalProxyService.java | 3 +- .../service/MsgCenterProxyService.java | 211 +++++----- .../service/MsgApprovalProxyServiceTest.java | 4 +- .../service/MsgCenterProxyServiceTest.java | 367 ++++++++++++------ 5 files changed, 402 insertions(+), 211 deletions(-) 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 84edd69..fb1e360 100644 --- a/src/main/java/com/yunhai/userhome/controller/api/MessageCenterController.java +++ b/src/main/java/com/yunhai/userhome/controller/api/MessageCenterController.java @@ -29,7 +29,11 @@ public class MessageCenterController { private final MsgCenterProxyService proxy; private final MsgApprovalProxyService approvalProxy; - /** schId=0 时推断:主学校 → 主归属组(bureau)→ 0(对齐 MessageController 旧口径) */ + /** + * 会话主学校(旧口径):只供待办角标(todo)与卡片直批(flow-center 办理人学校)使用—— + * 这两条链路口径不变(方案 §8)。消息读取一律不得用它(R2:跨校切换会错), + * 必须用 {@link MsgCenterProxyService#resolveReadSchId}(当前激活身份学校)。 + */ private Integer resolveSchId(BoaUser user) { if (user.getSchool() != null) { return user.getSchool().getId(); @@ -40,6 +44,11 @@ public class MessageCenterController { return 0; } + /** 消息读取口径 schId:当前激活身份所属学校(0/平台/超管与身份链不可达 → null=看全部,D5) */ + private Integer msgSchId(BoaUser authUserWithDept) { + return proxy.resolveReadSchId(authUserWithDept.getId()); + } + /** 消息分页列表:{total, list[]}(元素含改写后的绝对 jumpUrl;UserMsg 不可达降级空)。 * 可选服务端筛选:msgType 0/1/2、appId(注册表应用 id,代理翻译下行)、isRead 0/1、keyword 标题/内容模糊(不传与现状一致)。 */ @GetMapping("msgCenter/list") @@ -51,19 +60,20 @@ public class MessageCenterController { @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, appId, isRead, keyword); + return proxy.list(authUserWithDept.getId(), page, count, onlyUnread, msgType, appId, isRead, keyword, + msgSchId(authUserWithDept)); } - /** 铃铛摘要:未读数 + 各应用待办角标 */ + /** 铃铛摘要:未读数 + 各应用待办角标(未读/分类计数按当前身份学校口径;待办角标沿用主学校口径不变) */ @GetMapping("msgCenter/summary") public Map summary(BoaUser authUserWithDept) { - return proxy.summary(authUserWithDept.getId(), resolveSchId(authUserWithDept)); + return proxy.summary(authUserWithDept.getId(), msgSchId(authUserWithDept), resolveSchId(authUserWithDept)); } /** 按来源应用消息计数(消息中心「按应用」清单角标):[{appId, appName, total, unread}],总数含已读 */ @GetMapping("msgCenter/countByApp") public java.util.List> countByApp(BoaUser authUserWithDept) { - return proxy.countByApp(authUserWithDept.getId()); + return proxy.countByApp(authUserWithDept.getId(), msgSchId(authUserWithDept)); } /** @@ -77,16 +87,16 @@ public class MessageCenterController { return out; } - /** 标记单条已读(按登录人代理,UserMsg 侧带归属人防伪) */ + /** 标记单条已读(按登录人代理,UserMsg 侧带归属人防伪;schId 只用于缓存失效键与 readState 口径) */ @PostMapping("msgCenter/read") public void read(BoaUser authUserWithDept, @RequestParam("msgId") Long msgId) { - proxy.read(authUserWithDept.getId(), Collections.singletonList(msgId)); + proxy.read(authUserWithDept.getId(), Collections.singletonList(msgId), msgSchId(authUserWithDept)); } - /** 全部已读 */ + /** 全部已读(按当前身份学校限定:只标本校+平台未读,他校未读不受影响,D6) */ @PostMapping("msgCenter/readAll") public void readAll(BoaUser authUserWithDept) { - proxy.readAll(authUserWithDept.getId()); + proxy.readAll(authUserWithDept.getId(), msgSchId(authUserWithDept)); } /** diff --git a/src/main/java/com/yunhai/userhome/service/MsgApprovalProxyService.java b/src/main/java/com/yunhai/userhome/service/MsgApprovalProxyService.java index 9a0a7b5..2bb653e 100644 --- a/src/main/java/com/yunhai/userhome/service/MsgApprovalProxyService.java +++ b/src/main/java/com/yunhai/userhome/service/MsgApprovalProxyService.java @@ -107,7 +107,8 @@ public class MsgApprovalProxyService { return fail(out, "流程中心暂不可用,请稍后重试"); } - msgProxy.read(userId, java.util.Collections.singletonList(msgId)); + // 直批后标读:schId 用消息读取口径(当前激活身份学校),保证 UserMsg 侧失效的是本校缓存键 + msgProxy.read(userId, java.util.Collections.singletonList(msgId), msgProxy.resolveReadSchId(userId)); out.put("ok", true); out.put("message", "办理成功"); return out; diff --git a/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java b/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java index 5aa57c7..a46e953 100644 --- a/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java +++ b/src/main/java/com/yunhai/userhome/service/MsgCenterProxyService.java @@ -3,7 +3,15 @@ package com.yunhai.userhome.service; import com.fasterxml.jackson.databind.JsonNode; import com.yunhai.platform.openapi.core.TargetSpec; import com.yunhai.platform.openapi.core.YschOpenCall; +import com.yunhai.platform.openapi.msg.OpenApiMsgClient; +import com.yunhai.platform.openapi.msg.impl.DefaultOpenApiMsgClient; +import com.yunhai.platform.openapi.msg.model.AppMsgCount; +import com.yunhai.platform.openapi.msg.model.MsgListItem; +import com.yunhai.platform.openapi.msg.model.MsgListPage; +import com.yunhai.platform.openapi.msg.model.MsgListQuery; +import com.yunhai.platform.openapi.msg.model.UserMsgSummary; import com.yunhai.userhome.config.MsgCenterProperties; +import com.yunhai.userhome.entity.UserCurrentRole; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -21,8 +29,11 @@ import java.util.Set; * (Tier 0,per-target base-path 剥离——客户端签名 path ≡ UserMsg requestURI 减 context-path), * UserHome 前端不直连 UserMsg。 *
    - *
  • 列表/角标:/oapi/msg/list、/oapi/msg/unreadCount、/oapi/msg/todo/list;
  • - *
  • 已读:/oapi/msg/read、/oapi/msg/readAll(按当前登录人代理,userId 服务端取);
  • + *
  • 消息读取(列表/摘要/未读数/按应用计数/全部已读)走 SDK msg 域类型化读方法 + * ({@link OpenApiMsgClient}),并带「当前激活身份」所属学校口径 schId + * ({@link #resolveReadSchId})——按学校隔离读取侧;
  • + *
  • 待办角标 todo/list(仍按主学校 schId,链路口径不变)、单条已读 read、WS 票据 + * 继续走 {@link YschOpenCall} 泛化直调;
  • *
  • jumpUrl 改写:绝对 URL 直通;相对路径按应用注册表取 pc_url——jumpApp(值域=app_code) * 主查,jumpApp 为空才按发送方 appKey 回落(严格二选一,pc_url 空就是空不猜测, * 手工基址表 jump-bases 已退役),归一化后拼 "#" + jumpPath;
  • @@ -33,7 +44,8 @@ import java.util.Set; * base-path(/userbase),目标 URL 带 /usermsg ctx 时签名路径多一层前缀 → UserMsg 验签 401, * 列表/角标/票据全部静默降级为空。改 YschOpenCall 后 {@code usermsg-base-url} 解析为 * {@link TargetSpec}(host + /usermsg ctx),签名按目标 ctx 剥离,与 AppAuthFilter 对齐。 - * 目标地址来源:usermsg-base-url 显式配置优先,其次服务目录 usermsg(yh.open.services / 枚举兜底)。 + * 目标地址来源:usermsg-base-url 显式配置优先(ad-hoc TargetSpec,泛化直调与类型化读方法同一寻址), + * 其次服务目录 usermsg(yh.open.services / 枚举兜底)。 * UserMsg 不可达时降级空结果(铃铛不 500),恢复后自愈。旧 user_message 本地表链路不动。 */ @Slf4j @@ -55,6 +67,7 @@ public class MsgCenterProxyService { private final YschOpenCall openCall; private final MsgCenterProperties properties; + private final IUserCurrentRoleService userCurrentRoleService; /** 0=正常;>0=UserBase 查址失败后的自愈时间戳(System.currentTimeMillis 基准) */ private volatile long userbaseDownUntil; @@ -141,15 +154,46 @@ public class MsgCenterProxyService { } } - public MsgCenterProxyService(YschOpenCall openCall, MsgCenterProperties properties) { + public MsgCenterProxyService(YschOpenCall openCall, MsgCenterProperties properties, + IUserCurrentRoleService userCurrentRoleService) { this.openCall = openCall; this.properties = properties; + this.userCurrentRoleService = userCurrentRoleService; } private boolean enabled() { return StringUtils.hasText(properties.getUsermsgBaseUrl()) || openCall.hasTarget(TARGET_USERMSG); } + /** + * 消息读取口径 schId(按学校隔离读取侧,R2):取当前激活身份所属学校 + * ({@code userCurrentRoleService.findById} → UserBase /oapi/identity/current), + * 严禁用会话主学校(跨校切换会错)。解析为 0(平台/超管)或身份链不可达时返回 null + * =不下发过滤(看全部,D5,且与改造前行为一致)。 + */ + public Integer resolveReadSchId(Integer userId) { + if (userId == null) { + return null; + } + try { + return userCurrentRoleService.findById(userId) + .map(UserCurrentRole::getSchId) + .filter(schId -> schId != null && schId > 0) + .orElse(null); + } catch (Exception e) { + log.warn("[msgcenter] 当前身份学校解析不可达(降级不过滤=全量口径)userId={}:{}", userId, e.toString()); + return null; + } + } + + /** 显式 base-url → ad-hoc TargetSpec(path 段即目标 ctx);否则类型化读方法按服务目录 usermsg 寻址 */ + private OpenApiMsgClient msgClient() { + String base = properties.getUsermsgBaseUrl(); + return StringUtils.hasText(base) + ? new DefaultOpenApiMsgClient(openCall, TargetSpec.fromUrl(base.trim())) + : new DefaultOpenApiMsgClient(openCall); + } + /** 显式 base-url → ad-hoc TargetSpec(path 段即目标 ctx,per-target 签名剥离);否则按名走服务目录 */ private JsonNode get(String path, Map query) { String base = properties.getUsermsgBaseUrl(); @@ -171,9 +215,10 @@ public class MsgCenterProxyService { * 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的绝对 jumpUrl 与 showJump 判权出参。 * 可选筛选(null/空白不透传,旧 UserMsg 收到未知参数会忽略——不传时行为与历史版本一致): * msgType 0/1/2、appId(注册表应用 id,代理翻译为发送方 appKey 后下行)、isRead 0/1、keyword 标题/内容模糊。 + * schId=当前身份学校口径({@link #resolveReadSchId}),null=不过滤(平台/超管视角,看全部)。 */ public Map list(Integer userId, Integer pageIndex, Integer pageSize, Boolean onlyUnread, - Integer msgType, String appId, Integer isRead, String keyword) { + Integer msgType, String appId, Integer isRead, String keyword, Integer schId) { Map empty = emptyPage(); if (!enabled()) { return empty; @@ -181,34 +226,31 @@ public class MsgCenterProxyService { PerRequestAuthz authz = new PerRequestAuthz(userId); AppDict dict = appDict(userId); try { - Map query = new LinkedHashMap<>(); - query.put("userId", String.valueOf(userId)); - query.put("pageIndex", String.valueOf(pageIndex == null || pageIndex < 1 ? 1 : pageIndex)); - query.put("pageSize", String.valueOf(pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100))); - query.put("onlyUnread", String.valueOf(Boolean.TRUE.equals(onlyUnread))); - if (msgType != null) { - query.put("msgType", String.valueOf(msgType)); - } + MsgListQuery query = new MsgListQuery(); + query.setUserId(userId); + query.setPageIndex(pageIndex == null || pageIndex < 1 ? 1 : pageIndex); + query.setPageSize(pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100)); + query.setOnlyUnread(Boolean.TRUE.equals(onlyUnread)); + query.setMsgType(msgType); if (StringUtils.hasText(appId)) { AppDictEntry entry = dict.byId.get(Integer.valueOf(appId.trim())); if (entry == null) { return empty; // 字典查无此应用:视作该应用无消息 } - query.put("appKey", entry.appKey); + query.setAppKey(entry.appKey); } - if (isRead != null) { - query.put("isRead", String.valueOf(isRead)); - } - putIfText(query, "keyword", keyword); - JsonNode data = get("/oapi/msg/list", query); + query.setIsRead(isRead); + query.setKeyword(StringUtils.hasText(keyword) ? keyword.trim() : null); + query.setSchId(schId); + MsgListPage page = msgClient().list(query); List> rows = new ArrayList<>(); - if (data != null && data.path("content").isArray()) { - for (JsonNode row : data.get("content")) { - rows.add(rewrite(row, authz, dict)); + if (page != null && page.getContent() != null) { + for (MsgListItem item : page.getContent()) { + rows.add(rewrite(item, authz, dict)); } } Map out = new HashMap<>(); - out.put("total", data == null || !data.has("total") ? 0 : data.get("total").asInt()); + out.put("total", page == null ? 0 : page.getTotal()); out.put("list", rows); return out; } catch (Exception e) { @@ -220,9 +262,12 @@ public class MsgCenterProxyService { /** * 铃铛摘要:未读数 + 各应用待办角标 + 分类计数(停机各自降级 0/空)。 * 分类计数(sysUnread/bizUnread/approvalUnread/total/readCount)来自 UserMsg /oapi/msg/summary - * (消息中心左栏一次喂饱);旧版 UserMsg 无该端点 → 字段缺省不出现,前端按降级处理。 + * (消息中心左栏一次喂饱),按 msgSchId 学校口径取数;UserMsg 不可达/字段缺省时降级 unreadCount 单查。 + * + * @param msgSchId 消息读取口径(当前激活身份学校,null=全部) + * @param todoSchId 待办角标口径(沿用会话主学校,链路口径不变,见类注释) */ - public Map summary(Integer userId, Integer schId) { + public Map summary(Integer userId, Integer msgSchId, Integer todoSchId) { Map out = new HashMap<>(); out.put("unreadCount", 0L); out.put("todos", new ArrayList<>()); @@ -231,20 +276,18 @@ public class MsgCenterProxyService { } boolean summaryFetched = false; try { - Map q = new LinkedHashMap<>(); - q.put("userId", String.valueOf(userId)); - JsonNode vo = get("/oapi/msg/summary", q); - if (vo != null && vo.has("unreadCount")) { - out.put("unreadCount", vo.get("unreadCount").asLong()); - out.put("sysUnread", vo.path("sysUnread").asLong(0)); - out.put("bizUnread", vo.path("bizUnread").asLong(0)); - 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)); + UserMsgSummary vo = msgClient().summary(userId, msgSchId); + if (vo != null) { + out.put("unreadCount", vo.getUnreadCount()); + out.put("sysUnread", vo.getSysUnread()); + out.put("bizUnread", vo.getBizUnread()); + out.put("approvalUnread", vo.getApprovalUnread()); + out.put("total", vo.getTotal()); + out.put("readCount", vo.getReadCount()); + // 分类总数(含已读)——左栏计数与列表内容对齐的浏览口径 + out.put("sysTotal", vo.getSysTotal()); + out.put("bizTotal", vo.getBizTotal()); + out.put("approvalTotal", vo.getApprovalTotal()); summaryFetched = true; } } catch (Exception e) { @@ -253,10 +296,7 @@ public class MsgCenterProxyService { } if (!summaryFetched) { try { - Map q = new LinkedHashMap<>(); - q.put("userId", String.valueOf(userId)); - JsonNode unread = get("/oapi/msg/unreadCount", q); - out.put("unreadCount", unread == null || !unread.isNumber() ? 0L : unread.asLong()); + out.put("unreadCount", msgClient().unreadCount(userId, msgSchId)); } catch (Exception e) { log.warn("[msgcenter] UserMsg 未读数不可达(降级 0)userId={}:{}", userId, e.toString()); } @@ -264,8 +304,8 @@ public class MsgCenterProxyService { try { Map q = new LinkedHashMap<>(); q.put("userId", String.valueOf(userId)); - if (schId != null) { - q.put("schId", String.valueOf(schId)); + if (todoSchId != null) { + q.put("schId", String.valueOf(todoSchId)); } JsonNode todos = get("/oapi/msg/todo/list", q); List> rows = new ArrayList<>(); @@ -288,24 +328,22 @@ public class MsgCenterProxyService { * 按应用消息计数(UserMsg /oapi/msg/countByApp),条目富化注册表 appId/appName: * [{appId, appName, total, unread}]——消息中心「按应用」清单角标(总数口径,前端隐藏 0 行)。 * appId 取应用字典(按用户授权列表),字典查无(如授权已回收的来源应用)时 appId=null,前端不展示。 - * 停机降级空列表。 + * schId=当前身份学校口径(null=全部)。停机降级空列表。 */ - public List> countByApp(Integer userId) { + public List> countByApp(Integer userId, Integer schId) { List> empty = new ArrayList<>(); if (!enabled()) { return empty; } try { - Map q = new LinkedHashMap<>(); - q.put("userId", String.valueOf(userId)); - JsonNode arr = get("/oapi/msg/countByApp", q); - if (arr == null || !arr.isArray()) { + List counts = msgClient().countByApp(userId, schId); + if (counts == null || counts.isEmpty()) { return empty; } AppDict dict = appDict(userId); List> out = new ArrayList<>(); - for (JsonNode a : arr) { - String appKey = text(a, "appKey"); + for (AppMsgCount a : counts) { + String appKey = a == null ? null : a.getAppKey(); if (appKey == null) { continue; } @@ -313,8 +351,8 @@ public class MsgCenterProxyService { 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)); + row.put("total", a.getTotal()); + row.put("unread", a.getUnread()); out.add(row); } return out; @@ -324,25 +362,29 @@ public class MsgCenterProxyService { } } - /** 标记已读(代理按登录人发送,UserMsg 侧 UPDATE 带归属人防伪) */ - public void read(Integer userId, List msgIds) { + /** + * 标记已读(代理按登录人发送,UserMsg 侧 UPDATE 带归属人防伪)。 + * schId 仅用于 UserMsg 侧缓存键(含学校口径,R1)与 readState 帧口径对齐,不参与 UPDATE 归属判定。 + */ + public void read(Integer userId, List msgIds, Integer schId) { if (!enabled() || msgIds == null || msgIds.isEmpty()) { return; } Map body = new HashMap<>(); body.put("msgIds", msgIds); body.put("userId", userId); + if (schId != null) { + body.put("schId", schId); + } post("/oapi/msg/read", new HashMap<>(), body); } - /** 全部已读 */ - public void readAll(Integer userId) { + /** 全部已读(按 schId 学校口径限定,D6:只标本校+平台未读,他校未读不受影响;schId null=全部) */ + public void readAll(Integer userId, Integer schId) { if (!enabled()) { return; } - Map body = new HashMap<>(); - body.put("userId", userId); - post("/oapi/msg/readAll", new HashMap<>(), body); + msgClient().readAll(userId, schId); } /** @@ -412,28 +454,28 @@ public class MsgCenterProxyService { * 应用标识对前端只出 appId/appName(应用字典富化):appKey 属后端签名标识不下发; * 字典查无(授权已回收等)appId/appName 为 null,前端按类型标签降级。 */ - private Map rewrite(JsonNode row, PerRequestAuthz authz, AppDict dict) { + private Map rewrite(MsgListItem 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")); - 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("jumpApp", text(row, "jumpApp")); - String appKey = text(row, "appKey"); + out.put("msgId", row.getMsgId()); + out.put("title", row.getTitle()); + out.put("content", row.getContent()); + out.put("msgType", row.getMsgType()); + out.put("jumpType", row.getJumpType()); + out.put("jumpPath", row.getJumpPath()); + out.put("jumpApp", row.getJumpApp()); + String appKey = row.getAppKey(); 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")); - out.put("isRead", row.has("isRead") && row.get("isRead").isNumber() ? row.get("isRead").asInt() : 0); + out.put("bizType", row.getBizType()); + out.put("bizId", row.getBizId()); + out.put("sendTime", row.getSendTime()); + out.put("isRead", row.getIsRead() == null ? 0 : row.getIsRead()); String jumpUrl = jumpUrl(row); // showJump(C1):jumpApp 有值才判权;无权 → false 且 jumpUrl 一并置 null(双保险防前端漏改); // jumpApp 为空 / UserBase 不可达(判权结果 null)→ 不判权放行 Boolean allowed = null; - String jumpApp = text(row, "jumpApp"); + String jumpApp = row.getJumpApp(); if (jumpUrl != null && StringUtils.hasText(jumpApp)) { allowed = authz.authorized(jumpApp.trim()); } @@ -457,10 +499,10 @@ public class MsgCenterProxyService { * * 注册表是多端地址(pc/app/小程序/钉钉)唯一来源;jump-bases 手工基址表已退役(C2)。 */ - 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 jumpApp = text(row, "jumpApp"); + private String jumpUrl(MsgListItem row) { + Integer jumpType = row.getJumpType(); + String jumpPath = row.getJumpPath(); + String jumpApp = row.getJumpApp(); if (jumpType == null || jumpType != 1 || !StringUtils.hasText(jumpPath)) { return null; } @@ -472,7 +514,7 @@ public class MsgCenterProxyService { return null; } String p = path.startsWith("/") ? path : "/" + path; - String appKey = text(row, "appKey"); + String appKey = row.getAppKey(); String pcUrl = StringUtils.hasText(jumpApp) ? pcUrlByCode(jumpApp.trim()) : (StringUtils.hasText(appKey) ? pcUrlByKey(appKey.trim()) : null); if (!StringUtils.hasText(pcUrl)) { @@ -578,13 +620,6 @@ public class MsgCenterProxyService { return out; } - /** 有值(非空白)才进 query——空串不透传,保持与不传一致 */ - private static void putIfText(Map query, String key, String value) { - if (StringUtils.hasText(value)) { - query.put(key, value.trim()); - } - } - private static String text(JsonNode node, String field) { return node != null && node.has(field) && !node.get(field).isNull() ? node.get(field).asText() : null; } diff --git a/src/test/java/com/yunhai/userhome/service/MsgApprovalProxyServiceTest.java b/src/test/java/com/yunhai/userhome/service/MsgApprovalProxyServiceTest.java index 6ab7dc3..52bde41 100644 --- a/src/test/java/com/yunhai/userhome/service/MsgApprovalProxyServiceTest.java +++ b/src/test/java/com/yunhai/userhome/service/MsgApprovalProxyServiceTest.java @@ -136,7 +136,7 @@ class MsgApprovalProxyServiceTest { assertEquals("张三", postQuery.getValue().get("operatorName")); ArgumentCaptor> readIds = ArgumentCaptor.forClass(List.class); - verify(msgProxy).read(eq(201), readIds.capture()); + verify(msgProxy).read(eq(201), readIds.capture(), any()); assertTrue(readIds.getValue().contains(5L), "办理成功后标记已读"); } @@ -180,6 +180,6 @@ class MsgApprovalProxyServiceTest { assertFalse((Boolean) out.get("ok")); assertEquals("当前用户不是该任务办理人", out.get("message"), "业务拒绝原文透出(flow-center 逐字复核)"); - verify(msgProxy, never()).read(any(), any()); + verify(msgProxy, never()).read(any(), any(), any()); } } diff --git a/src/test/java/com/yunhai/userhome/service/MsgCenterProxyServiceTest.java b/src/test/java/com/yunhai/userhome/service/MsgCenterProxyServiceTest.java index c191739..1619619 100644 --- a/src/test/java/com/yunhai/userhome/service/MsgCenterProxyServiceTest.java +++ b/src/test/java/com/yunhai/userhome/service/MsgCenterProxyServiceTest.java @@ -5,13 +5,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yunhai.platform.openapi.core.OpenApiException; import com.yunhai.platform.openapi.core.TargetSpec; import com.yunhai.platform.openapi.core.YschOpenCall; +import com.yunhai.platform.openapi.msg.model.AppMsgCount; +import com.yunhai.platform.openapi.msg.model.MsgListPage; +import com.yunhai.platform.openapi.msg.model.ReadAllMsgRequest; +import com.yunhai.platform.openapi.msg.model.UserMsgSummary; import com.yunhai.userhome.config.MsgCenterProperties; +import com.yunhai.userhome.entity.UserCurrentRole; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import java.util.Map; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -45,27 +51,56 @@ import static org.mockito.Mockito.when; * 160 事故根因 = IDHttp 共享签名拦截器只剥本应用 base-path,目标带 ctx(/usermsg)时 * 签名路径多一层前缀 → UserMsg AppAuthFilter 401。回归断言:TargetSpec 从 * usermsg-base-url 解析出 basePath=/usermsg(per-target 剥离),调用 path 为 ctx 裸路径。 + *

    + * 按学校隔离(2026-10-09 批次3):消息读取(list/summary/unreadCount/countByApp/readAll)改走 + * SDK msg 域类型化读方法(Tier 1,仍委托同一个 {@link YschOpenCall},故断言靶心从 JsonNode + * 换成 DTO class:{@code MsgListPage/UserMsgSummary/Long/AppMsgCount/Void}),并带 + * 当前激活身份学校口径({@link MsgCenterProxyService#resolveReadSchId},R2); + * 待办角标 todo/list 仍按主学校口径(§8 不动角标链路),两者在 summary 上是两个独立入参。 */ -@DisplayName("MsgCenterProxyService - YschOpenCall 代理、jumpUrl 改写与 showJump 判权") +@DisplayName("MsgCenterProxyService - SDK 类型化读取、学校口径下行与 jumpUrl/showJump 改写") class MsgCenterProxyServiceTest { /** 160 生产形态的 UserMsg 直连地址(含目标 context-path) */ private static final String PROD_BASE_URL = "http://usermsg-service:10009/usermsg"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + private YschOpenCall openCall; + private IUserCurrentRoleService userCurrentRoleService; private MsgCenterProperties properties; private MsgCenterProxyService service; @BeforeEach void setUp() { openCall = mock(YschOpenCall.class); + userCurrentRoleService = mock(IUserCurrentRoleService.class); properties = new MsgCenterProperties(); properties.setUsermsgBaseUrl(PROD_BASE_URL); - service = new MsgCenterProxyService(openCall, properties); + service = new MsgCenterProxyService(openCall, properties, userCurrentRoleService); } private static JsonNode json(String s) throws Exception { - return new ObjectMapper().readTree(s); + return MAPPER.readTree(s); + } + + /** SDK 类型化读方法的响应靶心:同一份 JSON 夹具直接绑成 SDK DTO(契约字段与 UserMsg 出参对齐) */ + private static T dto(String s, Class type) throws Exception { + return MAPPER.readValue(s, type); + } + + private static java.util.List listDto(String s, Class elementType) throws Exception { + return MAPPER.readValue(s, MAPPER.getTypeFactory() + .constructCollectionType(java.util.List.class, elementType)); + } + + /** 当前身份 stub(R2 数据源):findById(userId) 返回给定 schId 的激活身份 */ + private void stubCurrentIdentity(Integer schId) { + UserCurrentRole role = new UserCurrentRole(); + role.setUserId(201); + role.setRoleId(9); + role.setSchId(schId); + when(userCurrentRoleService.findById(201)).thenReturn(Optional.of(role)); } /** 判权 stub:/oapi/app/authorized-codes 返回给定 app_code 集合 */ @@ -89,16 +124,118 @@ class MsgCenterProxyServiceTest { + "\",\"name\":\"" + name + "\"}]}]")); } + // ===== R2:读取口径 schId 必须来自当前激活身份(严禁会话主学校) ===== + + @Test + void resolveReadSchId_usesCurrentIdentitySchool() { + stubCurrentIdentity(7); + assertEquals(Integer.valueOf(7), service.resolveReadSchId(201), "当前身份 schId>0 → 原值下行(本校+平台可见)"); + } + + @Test + void resolveReadSchId_platformIdentityZeroMeansSeeAll() { + stubCurrentIdentity(0); + assertNull(service.resolveReadSchId(201), "schId=0(平台/超管)→ null=看全部(D5)"); + stubCurrentIdentity(null); + assertNull(service.resolveReadSchId(201), "身份无 schId 同样按看全部处理"); + } + + @Test + void resolveReadSchId_identityChainDown_seesAllWithoutThrowing() { + when(userCurrentRoleService.findById(201)).thenThrow(new OpenApiException("userbase down")); + assertNull(service.resolveReadSchId(201), "身份链不可达降级 null=改造前全量口径,铃铛不 500"); + assertNull(service.resolveReadSchId(null), "无登录人不出站"); + verify(userCurrentRoleService, never()).findById(null); + } + + // ===== 批次3:学校口径随读端点下行(R1 的下游半程) ===== + + @Test + void list_andSummary_passSchIdDownstream() throws Exception { + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":0,\"content\":[]}", MsgListPage.class)); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/summary"), anyMap(), eq(UserMsgSummary.class))) + .thenReturn(dto("{\"unreadCount\":1}", UserMsgSummary.class)); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class))) + .thenReturn(json("[]")); + + service.list(201, 1, 10, null, null, null, null, null, 7); + service.summary(201, 7, 3); + + @SuppressWarnings("unchecked") + ArgumentCaptor> listQuery = ArgumentCaptor.forClass(Map.class); + verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), listQuery.capture(), eq(MsgListPage.class)); + assertEquals("7", listQuery.getValue().get("schId")); + + @SuppressWarnings("unchecked") + ArgumentCaptor> summaryQuery = ArgumentCaptor.forClass(Map.class); + verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/summary"), summaryQuery.capture(), + eq(UserMsgSummary.class)); + assertEquals("7", summaryQuery.getValue().get("schId"), "摘要未读/分类计数按当前身份学校口径"); + + @SuppressWarnings("unchecked") + ArgumentCaptor> todoQuery = ArgumentCaptor.forClass(Map.class); + verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), todoQuery.capture(), eq(JsonNode.class)); + assertEquals("3", todoQuery.getValue().get("schId"), "待办角标沿用主学校口径(§8 链路不变)"); + } + + @Test + void countByApp_passesSchIdAndEnriches() throws Exception { + stubAppDict(100011, "100019", "流程中心"); + when(openCall.getList(any(TargetSpec.class), eq("/oapi/msg/countByApp"), anyMap(), eq(AppMsgCount.class))) + .thenReturn(listDto("[{\"appKey\":\"100019\",\"total\":4,\"unread\":1}]", AppMsgCount.class)); + + java.util.List> rows = service.countByApp(201, 7); + + @SuppressWarnings("unchecked") + ArgumentCaptor> query = ArgumentCaptor.forClass(Map.class); + verify(openCall).getList(any(TargetSpec.class), eq("/oapi/msg/countByApp"), query.capture(), + eq(AppMsgCount.class)); + assertEquals("7", query.getValue().get("schId")); + assertEquals(1, rows.size()); + assertEquals(100011, rows.get(0).get("appId")); + assertEquals(4L, rows.get(0).get("total")); + } + + @Test + void read_schIdGoesIntoBodyOnlyWhenPresent() { + ArgumentCaptor> body = ArgumentCaptor.forClass(Map.class); + + service.read(201, java.util.Collections.singletonList(7L), 7); + verify(openCall).post(any(TargetSpec.class), eq("/oapi/msg/read"), anyMap(), body.capture(), + eq(JsonNode.class)); + assertEquals(7, body.getValue().get("schId"), "schId 供 UserMsg 侧缓存失效键与 readState 口径(D8)"); + assertEquals(201, body.getValue().get("userId")); + + ArgumentCaptor> body2 = ArgumentCaptor.forClass(Map.class); + service.read(201, java.util.Collections.singletonList(8L), null); + verify(openCall, org.mockito.Mockito.times(2)) + .post(any(TargetSpec.class), eq("/oapi/msg/read"), anyMap(), body2.capture(), eq(JsonNode.class)); + assertFalse(body2.getAllValues().get(1).containsKey("schId"), "null 不下发=平台视角(与历史调用一致)"); + } + + @Test + void readAll_goesThroughSdkWithSchId() { + ArgumentCaptor body = ArgumentCaptor.forClass(ReadAllMsgRequest.class); + service.readAll(201, 7); + verify(openCall).post(any(TargetSpec.class), eq("/oapi/msg/readAll"), anyMap(), body.capture(), + eq(Void.class)); + assertEquals(Integer.valueOf(201), body.getValue().getUserId()); + assertEquals(Integer.valueOf(7), body.getValue().getSchId(), "全部已读按当前身份学校限定(D6)"); + } + + // ===== 寻址回归(160 事故)与 list 改写 ===== + @Test void list_targetSpecStripsUsermsgCtx_forPerTargetSign() { // 160 回归:签名剥离的载体是 TargetSpec.basePath,必须等于 /usermsg - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) .thenReturn(null); - service.list(201, 1, 10, null, null, null, null, null); + service.list(201, 1, 10, null, null, null, null, null, null); ArgumentCaptor spec = ArgumentCaptor.forClass(TargetSpec.class); - verify(openCall).get(spec.capture(), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)); + verify(openCall).get(spec.capture(), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class)); assertEquals("http://usermsg-service:10009", spec.getValue().getBaseHost()); assertEquals("/usermsg", spec.getValue().getBasePath(), "basePath=目标 ctx,AppSignInterceptor 据此剥离后签名"); } @@ -109,29 +246,31 @@ class MsgCenterProxyServiceTest { // 自发消息无 jumpApp → 按发送方 appKey 走 by-key 注册表(严格二选一) stubUserbaseGet("/oapi/app/by-key/100019", "{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":2,\"content\":[" + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"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}]}")); + + "\"jumpType\":0,\"jumpPath\":null,\"bizType\":null,\"isRead\":1}]}", MsgListPage.class)); - Map out = service.list(201, 2, 20, Boolean.TRUE, null, null, null, null); + Map out = service.list(201, 2, 20, Boolean.TRUE, null, null, null, null, null); @SuppressWarnings("unchecked") ArgumentCaptor> query = ArgumentCaptor.forClass(Map.class); - verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), query.capture(), eq(JsonNode.class)); + verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), query.capture(), eq(MsgListPage.class)); assertEquals("201", query.getValue().get("userId")); assertEquals("2", query.getValue().get("pageIndex")); assertEquals("20", query.getValue().get("pageSize")); assertEquals("true", query.getValue().get("onlyUnread")); + assertFalse(query.getValue().containsKey("schId"), "schId=null 不下发(向后兼容,验收 7)"); 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"), "注册表 pc_url + # + 裸路径"); assertEquals("flow.task", first.get("bizType")); + assertEquals("2026-09-13 10:00:00", first.get("sendTime"), "sendTime 逐字段透传(SDK 侧 String 口径)"); assertEquals(Boolean.TRUE, first.get("showJump"), "无 jumpApp 不判权放行"); assertNull(first.get("appName"), "未配置映射的 appKey 不补应用名"); Map second = (Map) rows.get(1); @@ -141,12 +280,12 @@ class MsgCenterProxyServiceTest { @Test void list_defaultsPageParams() { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) .thenReturn(null); - service.list(201, 0, 999, null, null, null, null, null); + service.list(201, 0, 999, null, null, null, null, null, null); @SuppressWarnings("unchecked") ArgumentCaptor> query = ArgumentCaptor.forClass(Map.class); - verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), query.capture(), eq(JsonNode.class)); + verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), query.capture(), eq(MsgListPage.class)); assertEquals("1", query.getValue().get("pageIndex"), "非法页码回退 1"); assertEquals("100", query.getValue().get("pageSize"), "页长上限 100"); assertEquals("false", query.getValue().get("onlyUnread")); @@ -155,10 +294,11 @@ class MsgCenterProxyServiceTest { @Test void list_appNameMappedFromAppKey() throws Exception { properties.getAppNames().put("100617", "通知公告"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"title\":\"通知公告\"," - + "\"content\":\"您有一条通知\",\"msgType\":0,\"appKey\":\"100617\",\"isRead\":0}]}")); - Map out = service.list(201, 1, 10, null, null, null, null, null); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"title\":\"通知公告\"," + + "\"content\":\"您有一条通知\",\"msgType\":0,\"appKey\":\"100617\",\"isRead\":0}]}", + MsgListPage.class)); + Map out = service.list(201, 1, 10, null, null, null, null, null, null); Map first = (Map) ((java.util.List) out.get("list")).get(0); assertEquals("通知公告", first.get("appName"), "appKey 命中映射补应用名"); } @@ -167,9 +307,9 @@ class MsgCenterProxyServiceTest { * 应用字典(appId 翻译/行富化)2026-10-08 起每次 list 固定取一次,与查址无关。 */ @Test void list_noRegistrySource_jumpUrlNull() throws Exception { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," - + "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"isRead\":0}]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + + "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"isRead\":0}]}", MsgListPage.class)); Map first = firstRow(); assertNull(first.get("jumpUrl"), "无注册表查询键 → 不改写"); assertEquals(Boolean.FALSE, first.get("showJump")); @@ -188,10 +328,10 @@ class MsgCenterProxyServiceTest { "{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\"," + "\"pcUrl\":\"http://localhost:8077\"}"); stubAuthorizedCodes("notice", "flow-center"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + "\"jumpPath\":\"/list/detail?serid=1\",\"bizType\":\"flow.task\",\"jumpApp\":\"notice\"," - + "\"isRead\":0}]}")); + + "\"isRead\":0}]}", MsgListPage.class)); Map first = firstRow(); assertEquals("http://localhost:8077#/list/detail?serid=1", first.get("jumpUrl"), "归属应用 jumpApp 主查 by-code"); @@ -202,10 +342,10 @@ class MsgCenterProxyServiceTest { /** 存量兼容:改造前落库的绝对 URL 直接透传,不再拼基址(B5;双前缀缺陷即源于二次拼接) */ @Test void list_absoluteJumpPath_passedThrough() throws Exception { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + "\"jumpPath\":\"http://base.yunhaixingtong.com/notice/#/list/detail?serid=1\"," - + "\"bizType\":\"flow.task\",\"isRead\":0}]}")); + + "\"bizType\":\"flow.task\",\"isRead\":0}]}", MsgListPage.class)); Map first = firstRow(); assertEquals("http://base.yunhaixingtong.com/notice/#/list/detail?serid=1", first.get("jumpUrl"), "绝对 URL 原样透传"); @@ -217,9 +357,10 @@ class MsgCenterProxyServiceTest { void list_jumpAppAbsent_fallsBackToSendersAppKey() throws Exception { stubUserbaseGet("/oapi/app/by-key/100019", "{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," - + "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"appKey\":\"100019\",\"isRead\":0}]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + + "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"appKey\":\"100019\",\"isRead\":0}]}", + MsgListPage.class)); Map first = firstRow(); assertEquals("http://localhost:8088#/task/9", first.get("jumpUrl"), "无归属声明回落发送方 appKey"); assertEquals(Boolean.TRUE, first.get("showJump")); @@ -229,10 +370,10 @@ class MsgCenterProxyServiceTest { @Test void list_jumpAppMiss_noAppKeyFallback_jumpUrlNull() throws Exception { stubUserbaseGet("/oapi/app/by-code/unknown-app", null); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"jumpApp\":\"unknown-app\"," - + "\"isRead\":0}]}")); + + "\"isRead\":0}]}", MsgListPage.class)); Map first = firstRow(); assertNull(first.get("jumpUrl")); assertEquals(Boolean.FALSE, first.get("showJump")); @@ -244,9 +385,10 @@ class MsgCenterProxyServiceTest { stubUserbaseGet("/oapi/app/by-code/notice", null); stubUserbaseGet("/oapi/app/by-key/100006", "{\"id\":100006,\"appKey\":100006,\"name\":\"成绩管理\",\"pcUrl\":\"http://localhost:8090\"}"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," - + "\"jumpPath\":\"/score/9\",\"appKey\":\"100006\",\"jumpApp\":\"notice\",\"isRead\":0}]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + + "\"jumpPath\":\"/score/9\",\"appKey\":\"100006\",\"jumpApp\":\"notice\",\"isRead\":0}]}", + MsgListPage.class)); Map first = firstRow(); assertNull(first.get("jumpUrl"), "jumpApp 命中即只认注册表结果,空就是空"); verify(openCall, never()).get(eq("userbase"), eq("/oapi/app/by-key/100006"), isNull(), eq(JsonNode.class)); @@ -258,9 +400,10 @@ class MsgCenterProxyServiceTest { stubUserbaseGet("/oapi/app/by-code/notice", "{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077/\"}"); stubAuthorizedCodes("notice"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," - + "\"jumpPath\":\"list/detail?serid=1\",\"jumpApp\":\"notice\",\"isRead\":0}]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + + "\"jumpPath\":\"list/detail?serid=1\",\"jumpApp\":\"notice\",\"isRead\":0}]}", + MsgListPage.class)); Map first = firstRow(); assertEquals("http://localhost:8077#/list/detail?serid=1", first.get("jumpUrl"), "尾斜杠归一 + 无前导斜杠路径补斜杠"); @@ -268,9 +411,9 @@ class MsgCenterProxyServiceTest { @Test void list_userMsgDown_degradesToEmpty() { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) .thenThrow(new OpenApiException("OpenAPI request failed: httpStatus=401")); - Map out = service.list(201, 1, 10, null, null, null, null, null); + Map out = service.list(201, 1, 10, null, null, null, null, null, null); assertEquals(0, out.get("total")); assertEquals(0, ((java.util.List) out.get("list")).size()); } @@ -303,12 +446,12 @@ class MsgCenterProxyServiceTest { @Test void summary_combinesUnreadAndTodos_withPerCallDegrade() throws Exception { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("3")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(Long.class))) + .thenReturn(3L); when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class))) .thenReturn(json("[{\"appKey\":\"100019\",\"count\":2}]")); - Map out = service.summary(201, 1); + Map out = service.summary(201, null, 1); assertEquals(3L, out.get("unreadCount")); assertEquals(1, ((java.util.List) out.get("todos")).size()); // appKey 属后端签名标识不下发(0e57022 口径收尾):待办行只出 count @@ -318,9 +461,9 @@ class MsgCenterProxyServiceTest { assertEquals(2, todoRow.get("count")); // 未读数不可达单独降级为 0,待办仍可用 - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(JsonNode.class))) + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(Long.class))) .thenThrow(new OpenApiException("down")); - Map out2 = service.summary(201, 1); + Map out2 = service.summary(201, null, 1); assertEquals(0L, out2.get("unreadCount")); assertEquals(1, ((java.util.List) out2.get("todos")).size()); } @@ -330,25 +473,25 @@ class MsgCenterProxyServiceTest { @Test void list_filtersPassedThrough() throws Exception { stubAppDict(100019, "100019", "流程中心"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":0,\"content\":[]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":0,\"content\":[]}", MsgListPage.class)); - service.list(201, 1, 10, null, 2, " 100019 ", 0, "请假"); + service.list(201, 1, 10, null, 2, " 100019 ", 0, "请假", null); @SuppressWarnings("unchecked") ArgumentCaptor> query = ArgumentCaptor.forClass(Map.class); - verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), query.capture(), eq(JsonNode.class)); + verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), query.capture(), eq(MsgListPage.class)); assertEquals("2", query.getValue().get("msgType")); assertEquals("100019", query.getValue().get("appKey"), "appId 去空白经字典翻译为 appKey 下行"); assertEquals("0", query.getValue().get("isRead")); assertEquals("请假", query.getValue().get("keyword")); // 全空筛选不出现新键(与历史调用逐字一致) - service.list(201, 1, 10, null, null, " ", null, ""); + service.list(201, 1, 10, null, null, " ", null, "", null); @SuppressWarnings("unchecked") ArgumentCaptor> query2 = ArgumentCaptor.forClass(Map.class); verify(openCall, org.mockito.Mockito.times(2)) - .get(any(TargetSpec.class), eq("/oapi/msg/list"), query2.capture(), eq(JsonNode.class)); + .get(any(TargetSpec.class), eq("/oapi/msg/list"), query2.capture(), eq(MsgListPage.class)); assertFalse(query2.getAllValues().get(1).containsKey("msgType")); assertFalse(query2.getAllValues().get(1).containsKey("appKey")); assertFalse(query2.getAllValues().get(1).containsKey("isRead")); @@ -359,12 +502,12 @@ class MsgCenterProxyServiceTest { @Test void list_appIdUnknownInDict_emptyPageNoUserMsgOutbound() throws Exception { // 字典未 stub → /oapi/app/list 返回 null → EMPTY - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":0,\"content\":[]}")); - Map out = service.list(201, 1, 10, null, null, "999999", null, null); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":0,\"content\":[]}", MsgListPage.class)); + Map out = service.list(201, 1, 10, null, null, "999999", null, null, null); assertEquals(0, out.get("total")); assertEquals(0, ((java.util.List) out.get("list")).size()); - verify(openCall, never()).get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class)); + verify(openCall, never()).get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class)); } /** 按应用计数(0e57022):UserMsg countByApp 条目经应用字典富化出 appId/appName,总数口径; @@ -372,11 +515,11 @@ class MsgCenterProxyServiceTest { @Test void countByApp_enrichesWithRegistryDict() throws Exception { stubAppDict(100011, "100019", "流程中心"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/countByApp"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("[{\"appKey\":\"100019\",\"total\":12,\"unread\":3}," + when(openCall.getList(any(TargetSpec.class), eq("/oapi/msg/countByApp"), anyMap(), eq(AppMsgCount.class))) + .thenReturn(listDto("[{\"appKey\":\"100019\",\"total\":12,\"unread\":3}," + "{\"appKey\":\"999999\",\"total\":5,\"unread\":1}," - + "{\"total\":2,\"unread\":0}]")); - java.util.List> rows = service.countByApp(201); + + "{\"total\":2,\"unread\":0}]", AppMsgCount.class)); + java.util.List> rows = service.countByApp(201, null); assertEquals(2, rows.size(), "无 appKey 行跳过"); Map first = rows.get(0); assertEquals(100011, first.get("appId")); @@ -393,61 +536,60 @@ class MsgCenterProxyServiceTest { void countByApp_disabledOrDown_degradesEmpty() { properties.setUsermsgBaseUrl(""); when(openCall.hasTarget("usermsg")).thenReturn(false); - assertTrue(service.countByApp(201).isEmpty()); + assertTrue(service.countByApp(201, null).isEmpty()); properties.setUsermsgBaseUrl(PROD_BASE_URL); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/countByApp"), anyMap(), eq(JsonNode.class))) + when(openCall.getList(any(TargetSpec.class), eq("/oapi/msg/countByApp"), anyMap(), eq(AppMsgCount.class))) .thenThrow(new OpenApiException("down")); - assertTrue(service.countByApp(201).isEmpty(), "UserMsg 不可达降级空,不抛出"); + assertTrue(service.countByApp(201, null).isEmpty(), "UserMsg 不可达降级空,不抛出"); } /** 摘要优先走 /oapi/msg/summary(分类计数一次喂饱),且不再单查 unreadCount */ @Test void summary_prefersSummaryEndpoint() throws Exception { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/summary"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"unreadCount\":5,\"sysUnread\":2,\"bizUnread\":1,\"approvalUnread\":2," - + "\"total\":9,\"readCount\":4}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/summary"), anyMap(), eq(UserMsgSummary.class))) + .thenReturn(dto("{\"unreadCount\":5,\"sysUnread\":2,\"bizUnread\":1,\"approvalUnread\":2," + + "\"total\":9,\"readCount\":4,\"sysTotal\":3,\"bizTotal\":2,\"approvalTotal\":4}", + UserMsgSummary.class)); when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class))) .thenReturn(json("[]")); - Map out = service.summary(201, 1); + Map out = service.summary(201, null, 1); assertEquals(5L, out.get("unreadCount")); assertEquals(2L, out.get("sysUnread")); assertEquals(1L, out.get("bizUnread")); assertEquals(2L, out.get("approvalUnread")); assertEquals(9L, out.get("total")); assertEquals(4L, out.get("readCount")); - verify(openCall, never()).get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(JsonNode.class)); + assertEquals(3L, out.get("sysTotal")); + verify(openCall, never()).get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(Long.class)); } /** 旧版 UserMsg 无 summary 端点(404/异常)→ 降级 unreadCount 单查,分类字段缺省 */ @Test void summary_legacyUserMsg_fallsBackToUnreadCount() throws Exception { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/summary"), anyMap(), eq(JsonNode.class))) + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/summary"), anyMap(), eq(UserMsgSummary.class))) .thenThrow(new OpenApiException("OpenAPI request failed: httpStatus=404")); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("7")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(Long.class))) + .thenReturn(7L); when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class))) .thenReturn(json("[]")); - Map out = service.summary(201, 1); + Map out = service.summary(201, 7, 1); assertEquals(7L, out.get("unreadCount")); assertFalse(out.containsKey("sysUnread"), "旧端点无分类计数,字段缺省由前端降级"); - } - - @Test - void read_sendsOwnerScopedPayload() { - service.read(201, java.util.Collections.singletonList(7L)); - verify(openCall).post(any(TargetSpec.class), eq("/oapi/msg/read"), anyMap(), any(Map.class), - eq(JsonNode.class)); + @SuppressWarnings("unchecked") + ArgumentCaptor> query = ArgumentCaptor.forClass(Map.class); + verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), query.capture(), eq(Long.class)); + assertEquals("7", query.getValue().get("schId"), "降级单查同样带当前身份学校口径"); } @Test void disabled_degradesWithoutOutbound() { properties.setUsermsgBaseUrl(""); when(openCall.hasTarget("usermsg")).thenReturn(false); - assertEquals(0, service.list(201, 1, 10, null, null, null, null, null).get("total")); - service.read(201, java.util.Collections.singletonList(7L)); + assertEquals(0, service.list(201, 1, 10, null, null, null, null, null, null).get("total")); + service.read(201, java.util.Collections.singletonList(7L), null); verify(openCall, never()).get(any(TargetSpec.class), anyString(), anyMap(), any()); verify(openCall, never()).post(any(TargetSpec.class), anyString(), anyMap(), any(), any()); } @@ -457,9 +599,9 @@ class MsgCenterProxyServiceTest { // base-url 未配但服务目录有 usermsg(平台公共配置形态):按名调用仍可用 properties.setUsermsgBaseUrl(""); when(openCall.hasTarget("usermsg")).thenReturn(true); - when(openCall.get(eq("usermsg"), eq("/oapi/msg/unreadCount"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("5")); - Map out = service.summary(201, null); + when(openCall.get(eq("usermsg"), eq("/oapi/msg/unreadCount"), anyMap(), eq(Long.class))) + .thenReturn(5L); + Map out = service.summary(201, null, null); assertEquals(5L, out.get("unreadCount")); } @@ -467,19 +609,19 @@ class MsgCenterProxyServiceTest { void summary_disabled_degradesToZero() { properties.setUsermsgBaseUrl(""); when(openCall.hasTarget("usermsg")).thenReturn(false); - Map out = service.summary(201, 1); + Map out = service.summary(201, null, 1); assertEquals(0L, out.get("unreadCount")); assertEquals(0, ((java.util.List) out.get("todos")).size()); } @Test void summary_todoQueryIncludesSchIdOnly_whenNotNull() throws Exception { - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("0")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), anyMap(), eq(Long.class))) + .thenReturn(0L); when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class))) .thenReturn(json("[]")); - service.summary(201, null); - service.summary(201, 3); + service.summary(201, null, null); + service.summary(201, null, 3); @SuppressWarnings("unchecked") ArgumentCaptor> query = ArgumentCaptor.forClass(Map.class); verify(openCall, org.mockito.Mockito.times(2)) @@ -491,7 +633,7 @@ class MsgCenterProxyServiceTest { @Test void malformedBaseUrl_disabledNotCrash() { properties.setUsermsgBaseUrl("::::not-a-url"); - Map out = service.summary(201, 1); + Map out = service.summary(201, null, 1); assertEquals(0L, out.get("unreadCount"), "非法 base-url 降级而非抛出"); } @@ -503,7 +645,7 @@ class MsgCenterProxyServiceTest { } private Map firstRow() throws Exception { - Map out = service.list(201, 1, 10, null, null, null, null, null); + Map out = service.list(201, 1, 10, null, null, null, null, null, null); return (Map) ((java.util.List) out.get("list")).get(0); } @@ -514,9 +656,10 @@ class MsgCenterProxyServiceTest { "{\"id\":100011,\"appKey\":100009,\"name\":\"流程中心\"," + "\"pcUrl\":\"http://base.yunhaixingtong.com/flow-center/front/index.html#/my\"}"); stubAuthorizedCodes("flow-center"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," - + "\"jumpPath\":\"/task/123\",\"appKey\":\"100009\",\"jumpApp\":\"flow-center\",\"isRead\":0}]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + + "\"jumpPath\":\"/task/123\",\"appKey\":\"100009\",\"jumpApp\":\"flow-center\",\"isRead\":0}]}", + MsgListPage.class)); Map first = firstRow(); assertEquals("http://base.yunhaixingtong.com/flow-center/front/index.html#/task/123", first.get("jumpUrl"), "丢 fragment + # 拼接,不产第二个 #"); @@ -531,9 +674,9 @@ class MsgCenterProxyServiceTest { stubUserbaseGet("/oapi/app/by-code/notice", "{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077\"}"); stubAuthorizedCodes("flow-center"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," - + "\"jumpPath\":\"/list/1\",\"jumpApp\":\"notice\",\"isRead\":0}]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + + "\"jumpPath\":\"/list/1\",\"jumpApp\":\"notice\",\"isRead\":0}]}", MsgListPage.class)); Map first = firstRow(); assertEquals(Boolean.FALSE, first.get("showJump"), "无该应用权限 → 隐藏跳转入口"); assertNull(first.get("jumpUrl"), "无权时 jumpUrl 同步置 null(双保险)"); @@ -544,9 +687,9 @@ class MsgCenterProxyServiceTest { void list_jumpAppBlank_noAuthzOutbound() throws Exception { stubUserbaseGet("/oapi/app/by-key/100019", "{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," - + "\"jumpPath\":\"/task/9\",\"appKey\":\"100019\",\"isRead\":0}]}")); + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + + "\"jumpPath\":\"/task/9\",\"appKey\":\"100019\",\"isRead\":0}]}", MsgListPage.class)); Map first = firstRow(); assertEquals(Boolean.TRUE, first.get("showJump")); verify(openCall, never()).get(eq("userbase"), eq("/oapi/app/authorized-codes"), anyMap(), eq(JsonNode.class)); @@ -557,10 +700,10 @@ class MsgCenterProxyServiceTest { void list_authzUnreachable_absoluteUrlStillPasses() throws Exception { when(openCall.get(eq("userbase"), eq("/oapi/app/authorized-codes"), anyMap(), eq(JsonNode.class))) .thenThrow(new RuntimeException("connect timeout")); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1," + "\"jumpPath\":\"http://base.yunhaixingtong.com/notice/#/list/1\",\"jumpApp\":\"notice\"," - + "\"isRead\":0}]}")); + + "\"isRead\":0}]}", MsgListPage.class)); Map first = firstRow(); assertEquals(Boolean.TRUE, first.get("showJump"), "判权通道故障 → 放行"); assertEquals("http://base.yunhaixingtong.com/notice/#/list/1", first.get("jumpUrl")); @@ -572,11 +715,12 @@ class MsgCenterProxyServiceTest { stubUserbaseGet("/oapi/app/by-code/notice", "{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077\"}"); stubAuthorizedCodes("notice"); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":2,\"content\":[" + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":2,\"content\":[" + "{\"msgId\":1,\"jumpType\":1,\"jumpPath\":\"/list/1\",\"jumpApp\":\"notice\",\"isRead\":0}," - + "{\"msgId\":2,\"jumpType\":1,\"jumpPath\":\"/list/2\",\"jumpApp\":\"notice\",\"isRead\":0}]}")); - Map out = service.list(201, 1, 10, null, null, null, null, null); + + "{\"msgId\":2,\"jumpType\":1,\"jumpPath\":\"/list/2\",\"jumpApp\":\"notice\",\"isRead\":0}]}", + MsgListPage.class)); + Map out = service.list(201, 1, 10, null, null, null, null, null, null); java.util.List rows = (java.util.List) out.get("list"); assertEquals(Boolean.TRUE, ((Map) rows.get(0)).get("showJump")); assertEquals(Boolean.TRUE, ((Map) rows.get(1)).get("showJump")); @@ -589,11 +733,12 @@ class MsgCenterProxyServiceTest { void list_userbaseDown_shortCircuit_relativeJumpNull() throws Exception { when(openCall.get(eq("userbase"), anyString(), isNull(), eq(JsonNode.class))) .thenThrow(new RuntimeException("connect timeout")); - when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(JsonNode.class))) - .thenReturn(json("{\"total\":2,\"content\":[" + when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class))) + .thenReturn(dto("{\"total\":2,\"content\":[" + "{\"msgId\":1,\"jumpType\":1,\"jumpPath\":\"/task/1\",\"appKey\":\"100009\",\"bizType\":\"flow.task\",\"isRead\":0}," - + "{\"msgId\":2,\"jumpType\":1,\"jumpPath\":\"/task/2\",\"appKey\":\"100009\",\"bizType\":\"flow.task\",\"isRead\":0}]}")); - Map out = service.list(201, 1, 10, null, null, null, null, null); + + "{\"msgId\":2,\"jumpType\":1,\"jumpPath\":\"/task/2\",\"appKey\":\"100009\",\"bizType\":\"flow.task\",\"isRead\":0}]}", + MsgListPage.class)); + Map out = service.list(201, 1, 10, null, null, null, null, null, null); java.util.List rows = (java.util.List) out.get("list"); assertNull(((Map) rows.get(0)).get("jumpUrl"), "注册表不可达无兜底 → null"); assertNull(((Map) rows.get(1)).get("jumpUrl"));