feat(msgcenter): 消息读取改走 SDK msg 读方法并带当前激活身份学校口径(resolveReadSchId→/oapi/identity/current,0/异常降级 null);todo 角标沿用主学校口径不变;直批标读联动带口径(消息按学校隔离 批次3)
This commit is contained in:
@@ -29,7 +29,11 @@ public class MessageCenterController {
|
|||||||
private final MsgCenterProxyService proxy;
|
private final MsgCenterProxyService proxy;
|
||||||
private final MsgApprovalProxyService approvalProxy;
|
private final MsgApprovalProxyService approvalProxy;
|
||||||
|
|
||||||
/** schId=0 时推断:主学校 → 主归属组(bureau)→ 0(对齐 MessageController 旧口径) */
|
/**
|
||||||
|
* 会话主学校(旧口径):<b>只</b>供待办角标(todo)与卡片直批(flow-center 办理人学校)使用——
|
||||||
|
* 这两条链路口径不变(方案 §8)。<b>消息读取一律不得用它</b>(R2:跨校切换会错),
|
||||||
|
* 必须用 {@link MsgCenterProxyService#resolveReadSchId}(当前激活身份学校)。
|
||||||
|
*/
|
||||||
private Integer resolveSchId(BoaUser user) {
|
private Integer resolveSchId(BoaUser user) {
|
||||||
if (user.getSchool() != null) {
|
if (user.getSchool() != null) {
|
||||||
return user.getSchool().getId();
|
return user.getSchool().getId();
|
||||||
@@ -40,6 +44,11 @@ public class MessageCenterController {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 消息读取口径 schId:当前激活身份所属学校(0/平台/超管与身份链不可达 → null=看全部,D5) */
|
||||||
|
private Integer msgSchId(BoaUser authUserWithDept) {
|
||||||
|
return proxy.resolveReadSchId(authUserWithDept.getId());
|
||||||
|
}
|
||||||
|
|
||||||
/** 消息分页列表:{total, list[]}(元素含改写后的绝对 jumpUrl;UserMsg 不可达降级空)。
|
/** 消息分页列表:{total, list[]}(元素含改写后的绝对 jumpUrl;UserMsg 不可达降级空)。
|
||||||
* 可选服务端筛选:msgType 0/1/2、appId(注册表应用 id,代理翻译下行)、isRead 0/1、keyword 标题/内容模糊(不传与现状一致)。 */
|
* 可选服务端筛选:msgType 0/1/2、appId(注册表应用 id,代理翻译下行)、isRead 0/1、keyword 标题/内容模糊(不传与现状一致)。 */
|
||||||
@GetMapping("msgCenter/list")
|
@GetMapping("msgCenter/list")
|
||||||
@@ -51,19 +60,20 @@ public class MessageCenterController {
|
|||||||
@RequestParam(value = "appId", required = false) String appId,
|
@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, appId, isRead, keyword);
|
return proxy.list(authUserWithDept.getId(), page, count, onlyUnread, msgType, appId, isRead, keyword,
|
||||||
|
msgSchId(authUserWithDept));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 铃铛摘要:未读数 + 各应用待办角标 */
|
/** 铃铛摘要:未读数 + 各应用待办角标(未读/分类计数按当前身份学校口径;待办角标沿用主学校口径不变) */
|
||||||
@GetMapping("msgCenter/summary")
|
@GetMapping("msgCenter/summary")
|
||||||
public Map<String, Object> summary(BoaUser authUserWithDept) {
|
public Map<String, Object> summary(BoaUser authUserWithDept) {
|
||||||
return proxy.summary(authUserWithDept.getId(), resolveSchId(authUserWithDept));
|
return proxy.summary(authUserWithDept.getId(), msgSchId(authUserWithDept), resolveSchId(authUserWithDept));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按来源应用消息计数(消息中心「按应用」清单角标):[{appId, appName, total, unread}],总数含已读 */
|
/** 按来源应用消息计数(消息中心「按应用」清单角标):[{appId, appName, total, unread}],总数含已读 */
|
||||||
@GetMapping("msgCenter/countByApp")
|
@GetMapping("msgCenter/countByApp")
|
||||||
public java.util.List<java.util.Map<String, Object>> countByApp(BoaUser authUserWithDept) {
|
public java.util.List<java.util.Map<String, Object>> countByApp(BoaUser authUserWithDept) {
|
||||||
return proxy.countByApp(authUserWithDept.getId());
|
return proxy.countByApp(authUserWithDept.getId(), msgSchId(authUserWithDept));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,16 +87,16 @@ public class MessageCenterController {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 标记单条已读(按登录人代理,UserMsg 侧带归属人防伪) */
|
/** 标记单条已读(按登录人代理,UserMsg 侧带归属人防伪;schId 只用于缓存失效键与 readState 口径) */
|
||||||
@PostMapping("msgCenter/read")
|
@PostMapping("msgCenter/read")
|
||||||
public void read(BoaUser authUserWithDept, @RequestParam("msgId") Long msgId) {
|
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")
|
@PostMapping("msgCenter/readAll")
|
||||||
public void readAll(BoaUser authUserWithDept) {
|
public void readAll(BoaUser authUserWithDept) {
|
||||||
proxy.readAll(authUserWithDept.getId());
|
proxy.readAll(authUserWithDept.getId(), msgSchId(authUserWithDept));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ public class MsgApprovalProxyService {
|
|||||||
return fail(out, "流程中心暂不可用,请稍后重试");
|
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("ok", true);
|
||||||
out.put("message", "办理成功");
|
out.put("message", "办理成功");
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ package com.yunhai.userhome.service;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.yunhai.platform.openapi.core.TargetSpec;
|
import com.yunhai.platform.openapi.core.TargetSpec;
|
||||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
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.config.MsgCenterProperties;
|
||||||
|
import com.yunhai.userhome.entity.UserCurrentRole;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
@@ -21,8 +29,11 @@ import java.util.Set;
|
|||||||
* (Tier 0,per-target base-path 剥离——客户端签名 path ≡ UserMsg requestURI 减 context-path),
|
* (Tier 0,per-target base-path 剥离——客户端签名 path ≡ UserMsg requestURI 减 context-path),
|
||||||
* UserHome 前端不直连 UserMsg。
|
* UserHome 前端不直连 UserMsg。
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>列表/角标:/oapi/msg/list、/oapi/msg/unreadCount、/oapi/msg/todo/list;</li>
|
* <li>消息读取(列表/摘要/未读数/按应用计数/全部已读)走 SDK msg 域类型化读方法
|
||||||
* <li>已读:/oapi/msg/read、/oapi/msg/readAll(按当前登录人代理,userId 服务端取);</li>
|
* ({@link OpenApiMsgClient}),并带「当前激活身份」所属学校口径 schId
|
||||||
|
* ({@link #resolveReadSchId})——按学校隔离读取侧;</li>
|
||||||
|
* <li>待办角标 todo/list(仍按主学校 schId,链路口径不变)、单条已读 read、WS 票据
|
||||||
|
* 继续走 {@link YschOpenCall} 泛化直调;</li>
|
||||||
* <li>jumpUrl 改写:绝对 URL 直通;相对路径按应用注册表取 pc_url——jumpApp(值域=app_code)
|
* <li>jumpUrl 改写:绝对 URL 直通;相对路径按应用注册表取 pc_url——jumpApp(值域=app_code)
|
||||||
* 主查,jumpApp 为空才按发送方 appKey 回落(严格二选一,pc_url 空就是空不猜测,
|
* 主查,jumpApp 为空才按发送方 appKey 回落(严格二选一,pc_url 空就是空不猜测,
|
||||||
* 手工基址表 jump-bases 已退役),归一化后拼 "#" + jumpPath;</li>
|
* 手工基址表 jump-bases 已退役),归一化后拼 "#" + jumpPath;</li>
|
||||||
@@ -33,7 +44,8 @@ import java.util.Set;
|
|||||||
* base-path(/userbase),目标 URL 带 /usermsg ctx 时签名路径多一层前缀 → UserMsg 验签 401,
|
* base-path(/userbase),目标 URL 带 /usermsg ctx 时签名路径多一层前缀 → UserMsg 验签 401,
|
||||||
* 列表/角标/票据全部静默降级为空。改 YschOpenCall 后 {@code usermsg-base-url} 解析为
|
* 列表/角标/票据全部静默降级为空。改 YschOpenCall 后 {@code usermsg-base-url} 解析为
|
||||||
* {@link TargetSpec}(host + /usermsg ctx),签名按目标 ctx 剥离,与 AppAuthFilter 对齐。
|
* {@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 本地表链路不动。
|
* UserMsg 不可达时降级空结果(铃铛不 500),恢复后自愈。旧 user_message 本地表链路不动。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -55,6 +67,7 @@ public class MsgCenterProxyService {
|
|||||||
|
|
||||||
private final YschOpenCall openCall;
|
private final YschOpenCall openCall;
|
||||||
private final MsgCenterProperties properties;
|
private final MsgCenterProperties properties;
|
||||||
|
private final IUserCurrentRoleService userCurrentRoleService;
|
||||||
|
|
||||||
/** 0=正常;>0=UserBase 查址失败后的自愈时间戳(System.currentTimeMillis 基准) */
|
/** 0=正常;>0=UserBase 查址失败后的自愈时间戳(System.currentTimeMillis 基准) */
|
||||||
private volatile long userbaseDownUntil;
|
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.openCall = openCall;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
|
this.userCurrentRoleService = userCurrentRoleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean enabled() {
|
private boolean enabled() {
|
||||||
return StringUtils.hasText(properties.getUsermsgBaseUrl()) || openCall.hasTarget(TARGET_USERMSG);
|
return StringUtils.hasText(properties.getUsermsgBaseUrl()) || openCall.hasTarget(TARGET_USERMSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息读取口径 schId(按学校隔离读取侧,R2):取<b>当前激活身份</b>所属学校
|
||||||
|
* ({@code userCurrentRoleService.findById} → UserBase /oapi/identity/current),
|
||||||
|
* <b>严禁</b>用会话主学校(跨校切换会错)。解析为 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 签名剥离);否则按名走服务目录 */
|
/** 显式 base-url → ad-hoc TargetSpec(path 段即目标 ctx,per-target 签名剥离);否则按名走服务目录 */
|
||||||
private JsonNode get(String path, Map<String, String> query) {
|
private JsonNode get(String path, Map<String, String> query) {
|
||||||
String base = properties.getUsermsgBaseUrl();
|
String base = properties.getUsermsgBaseUrl();
|
||||||
@@ -171,9 +215,10 @@ public class MsgCenterProxyService {
|
|||||||
* 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的绝对 jumpUrl 与 showJump 判权出参。
|
* 消息分页列表(已过滤删除/过期):{total, list[]},元素含改写后的绝对 jumpUrl 与 showJump 判权出参。
|
||||||
* 可选筛选(null/空白不透传,旧 UserMsg 收到未知参数会忽略——不传时行为与历史版本一致):
|
* 可选筛选(null/空白不透传,旧 UserMsg 收到未知参数会忽略——不传时行为与历史版本一致):
|
||||||
* msgType 0/1/2、appId(注册表应用 id,代理翻译为发送方 appKey 后下行)、isRead 0/1、keyword 标题/内容模糊。
|
* msgType 0/1/2、appId(注册表应用 id,代理翻译为发送方 appKey 后下行)、isRead 0/1、keyword 标题/内容模糊。
|
||||||
|
* schId=当前身份学校口径({@link #resolveReadSchId}),null=不过滤(平台/超管视角,看全部)。
|
||||||
*/
|
*/
|
||||||
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 appId, Integer isRead, String keyword) {
|
Integer msgType, String appId, Integer isRead, String keyword, Integer schId) {
|
||||||
Map<String, Object> empty = emptyPage();
|
Map<String, Object> empty = emptyPage();
|
||||||
if (!enabled()) {
|
if (!enabled()) {
|
||||||
return empty;
|
return empty;
|
||||||
@@ -181,34 +226,31 @@ public class MsgCenterProxyService {
|
|||||||
PerRequestAuthz authz = new PerRequestAuthz(userId);
|
PerRequestAuthz authz = new PerRequestAuthz(userId);
|
||||||
AppDict dict = appDict(userId);
|
AppDict dict = appDict(userId);
|
||||||
try {
|
try {
|
||||||
Map<String, String> query = new LinkedHashMap<>();
|
MsgListQuery query = new MsgListQuery();
|
||||||
query.put("userId", String.valueOf(userId));
|
query.setUserId(userId);
|
||||||
query.put("pageIndex", String.valueOf(pageIndex == null || pageIndex < 1 ? 1 : pageIndex));
|
query.setPageIndex(pageIndex == null || pageIndex < 1 ? 1 : pageIndex);
|
||||||
query.put("pageSize", String.valueOf(pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100)));
|
query.setPageSize(pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100));
|
||||||
query.put("onlyUnread", String.valueOf(Boolean.TRUE.equals(onlyUnread)));
|
query.setOnlyUnread(Boolean.TRUE.equals(onlyUnread));
|
||||||
if (msgType != null) {
|
query.setMsgType(msgType);
|
||||||
query.put("msgType", String.valueOf(msgType));
|
|
||||||
}
|
|
||||||
if (StringUtils.hasText(appId)) {
|
if (StringUtils.hasText(appId)) {
|
||||||
AppDictEntry entry = dict.byId.get(Integer.valueOf(appId.trim()));
|
AppDictEntry entry = dict.byId.get(Integer.valueOf(appId.trim()));
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
return empty; // 字典查无此应用:视作该应用无消息
|
return empty; // 字典查无此应用:视作该应用无消息
|
||||||
}
|
}
|
||||||
query.put("appKey", entry.appKey);
|
query.setAppKey(entry.appKey);
|
||||||
}
|
}
|
||||||
if (isRead != null) {
|
query.setIsRead(isRead);
|
||||||
query.put("isRead", String.valueOf(isRead));
|
query.setKeyword(StringUtils.hasText(keyword) ? keyword.trim() : null);
|
||||||
}
|
query.setSchId(schId);
|
||||||
putIfText(query, "keyword", keyword);
|
MsgListPage page = msgClient().list(query);
|
||||||
JsonNode data = get("/oapi/msg/list", query);
|
|
||||||
List<Map<String, Object>> rows = new ArrayList<>();
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
if (data != null && data.path("content").isArray()) {
|
if (page != null && page.getContent() != null) {
|
||||||
for (JsonNode row : data.get("content")) {
|
for (MsgListItem item : page.getContent()) {
|
||||||
rows.add(rewrite(row, authz, dict));
|
rows.add(rewrite(item, authz, dict));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map<String, Object> out = new HashMap<>();
|
Map<String, Object> 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);
|
out.put("list", rows);
|
||||||
return out;
|
return out;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -220,9 +262,12 @@ public class MsgCenterProxyService {
|
|||||||
/**
|
/**
|
||||||
* 铃铛摘要:未读数 + 各应用待办角标 + 分类计数(停机各自降级 0/空)。
|
* 铃铛摘要:未读数 + 各应用待办角标 + 分类计数(停机各自降级 0/空)。
|
||||||
* 分类计数(sysUnread/bizUnread/approvalUnread/total/readCount)来自 UserMsg /oapi/msg/summary
|
* 分类计数(sysUnread/bizUnread/approvalUnread/total/readCount)来自 UserMsg /oapi/msg/summary
|
||||||
* (消息中心左栏一次喂饱);旧版 UserMsg 无该端点 → 字段缺省不出现,前端按降级处理。
|
* (消息中心左栏一次喂饱),按 msgSchId 学校口径取数;UserMsg 不可达/字段缺省时降级 unreadCount 单查。
|
||||||
|
*
|
||||||
|
* @param msgSchId 消息读取口径(当前激活身份学校,null=全部)
|
||||||
|
* @param todoSchId 待办角标口径(沿用会话主学校,链路口径不变,见类注释)
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> summary(Integer userId, Integer schId) {
|
public Map<String, Object> summary(Integer userId, Integer msgSchId, Integer todoSchId) {
|
||||||
Map<String, Object> out = new HashMap<>();
|
Map<String, Object> out = new HashMap<>();
|
||||||
out.put("unreadCount", 0L);
|
out.put("unreadCount", 0L);
|
||||||
out.put("todos", new ArrayList<>());
|
out.put("todos", new ArrayList<>());
|
||||||
@@ -231,20 +276,18 @@ public class MsgCenterProxyService {
|
|||||||
}
|
}
|
||||||
boolean summaryFetched = false;
|
boolean summaryFetched = false;
|
||||||
try {
|
try {
|
||||||
Map<String, String> q = new LinkedHashMap<>();
|
UserMsgSummary vo = msgClient().summary(userId, msgSchId);
|
||||||
q.put("userId", String.valueOf(userId));
|
if (vo != null) {
|
||||||
JsonNode vo = get("/oapi/msg/summary", q);
|
out.put("unreadCount", vo.getUnreadCount());
|
||||||
if (vo != null && vo.has("unreadCount")) {
|
out.put("sysUnread", vo.getSysUnread());
|
||||||
out.put("unreadCount", vo.get("unreadCount").asLong());
|
out.put("bizUnread", vo.getBizUnread());
|
||||||
out.put("sysUnread", vo.path("sysUnread").asLong(0));
|
out.put("approvalUnread", vo.getApprovalUnread());
|
||||||
out.put("bizUnread", vo.path("bizUnread").asLong(0));
|
out.put("total", vo.getTotal());
|
||||||
out.put("approvalUnread", vo.path("approvalUnread").asLong(0));
|
out.put("readCount", vo.getReadCount());
|
||||||
out.put("total", vo.path("total").asLong(0));
|
// 分类总数(含已读)——左栏计数与列表内容对齐的浏览口径
|
||||||
out.put("readCount", vo.path("readCount").asLong(0));
|
out.put("sysTotal", vo.getSysTotal());
|
||||||
// 分类总数(含已读)——左栏计数与列表内容对齐的浏览口径;旧版 UserMsg 无该字段时缺省,前端按降级处理
|
out.put("bizTotal", vo.getBizTotal());
|
||||||
out.put("sysTotal", vo.path("sysTotal").asLong(0));
|
out.put("approvalTotal", vo.getApprovalTotal());
|
||||||
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) {
|
||||||
@@ -253,10 +296,7 @@ public class MsgCenterProxyService {
|
|||||||
}
|
}
|
||||||
if (!summaryFetched) {
|
if (!summaryFetched) {
|
||||||
try {
|
try {
|
||||||
Map<String, String> q = new LinkedHashMap<>();
|
out.put("unreadCount", msgClient().unreadCount(userId, msgSchId));
|
||||||
q.put("userId", String.valueOf(userId));
|
|
||||||
JsonNode unread = get("/oapi/msg/unreadCount", q);
|
|
||||||
out.put("unreadCount", unread == null || !unread.isNumber() ? 0L : unread.asLong());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[msgcenter] UserMsg 未读数不可达(降级 0)userId={}:{}", userId, e.toString());
|
log.warn("[msgcenter] UserMsg 未读数不可达(降级 0)userId={}:{}", userId, e.toString());
|
||||||
}
|
}
|
||||||
@@ -264,8 +304,8 @@ public class MsgCenterProxyService {
|
|||||||
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));
|
||||||
if (schId != null) {
|
if (todoSchId != null) {
|
||||||
q.put("schId", String.valueOf(schId));
|
q.put("schId", String.valueOf(todoSchId));
|
||||||
}
|
}
|
||||||
JsonNode todos = get("/oapi/msg/todo/list", q);
|
JsonNode todos = get("/oapi/msg/todo/list", q);
|
||||||
List<Map<String, Object>> rows = new ArrayList<>();
|
List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
@@ -288,24 +328,22 @@ public class MsgCenterProxyService {
|
|||||||
* 按应用消息计数(UserMsg /oapi/msg/countByApp),条目富化注册表 appId/appName:
|
* 按应用消息计数(UserMsg /oapi/msg/countByApp),条目富化注册表 appId/appName:
|
||||||
* [{appId, appName, total, unread}]——消息中心「按应用」清单角标(总数口径,前端隐藏 0 行)。
|
* [{appId, appName, total, unread}]——消息中心「按应用」清单角标(总数口径,前端隐藏 0 行)。
|
||||||
* appId 取应用字典(按用户授权列表),字典查无(如授权已回收的来源应用)时 appId=null,前端不展示。
|
* appId 取应用字典(按用户授权列表),字典查无(如授权已回收的来源应用)时 appId=null,前端不展示。
|
||||||
* 停机降级空列表。
|
* schId=当前身份学校口径(null=全部)。停机降级空列表。
|
||||||
*/
|
*/
|
||||||
public List<Map<String, Object>> countByApp(Integer userId) {
|
public List<Map<String, Object>> countByApp(Integer userId, Integer schId) {
|
||||||
List<Map<String, Object>> empty = new ArrayList<>();
|
List<Map<String, Object>> empty = new ArrayList<>();
|
||||||
if (!enabled()) {
|
if (!enabled()) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Map<String, String> q = new LinkedHashMap<>();
|
List<AppMsgCount> counts = msgClient().countByApp(userId, schId);
|
||||||
q.put("userId", String.valueOf(userId));
|
if (counts == null || counts.isEmpty()) {
|
||||||
JsonNode arr = get("/oapi/msg/countByApp", q);
|
|
||||||
if (arr == null || !arr.isArray()) {
|
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
AppDict dict = appDict(userId);
|
AppDict dict = appDict(userId);
|
||||||
List<Map<String, Object>> out = new ArrayList<>();
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
for (JsonNode a : arr) {
|
for (AppMsgCount a : counts) {
|
||||||
String appKey = text(a, "appKey");
|
String appKey = a == null ? null : a.getAppKey();
|
||||||
if (appKey == null) {
|
if (appKey == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -313,8 +351,8 @@ public class MsgCenterProxyService {
|
|||||||
Map<String, Object> row = new LinkedHashMap<>();
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
row.put("appId", entry == null ? null : entry.appId);
|
row.put("appId", entry == null ? null : entry.appId);
|
||||||
row.put("appName", entry == null ? properties.getAppNames().get(appKey) : entry.name);
|
row.put("appName", entry == null ? properties.getAppNames().get(appKey) : entry.name);
|
||||||
row.put("total", a.path("total").asLong(0));
|
row.put("total", a.getTotal());
|
||||||
row.put("unread", a.path("unread").asLong(0));
|
row.put("unread", a.getUnread());
|
||||||
out.add(row);
|
out.add(row);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -324,25 +362,29 @@ public class MsgCenterProxyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 标记已读(代理按登录人发送,UserMsg 侧 UPDATE 带归属人防伪) */
|
/**
|
||||||
public void read(Integer userId, List<Long> msgIds) {
|
* 标记已读(代理按登录人发送,UserMsg 侧 UPDATE 带归属人防伪)。
|
||||||
|
* schId 仅用于 UserMsg 侧缓存键(含学校口径,R1)与 readState 帧口径对齐,不参与 UPDATE 归属判定。
|
||||||
|
*/
|
||||||
|
public void read(Integer userId, List<Long> msgIds, Integer schId) {
|
||||||
if (!enabled() || msgIds == null || msgIds.isEmpty()) {
|
if (!enabled() || msgIds == null || msgIds.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Map<String, Object> body = new HashMap<>();
|
Map<String, Object> body = new HashMap<>();
|
||||||
body.put("msgIds", msgIds);
|
body.put("msgIds", msgIds);
|
||||||
body.put("userId", userId);
|
body.put("userId", userId);
|
||||||
|
if (schId != null) {
|
||||||
|
body.put("schId", schId);
|
||||||
|
}
|
||||||
post("/oapi/msg/read", new HashMap<>(), body);
|
post("/oapi/msg/read", new HashMap<>(), body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 全部已读 */
|
/** 全部已读(按 schId 学校口径限定,D6:只标本校+平台未读,他校未读不受影响;schId null=全部) */
|
||||||
public void readAll(Integer userId) {
|
public void readAll(Integer userId, Integer schId) {
|
||||||
if (!enabled()) {
|
if (!enabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Map<String, Object> body = new HashMap<>();
|
msgClient().readAll(userId, schId);
|
||||||
body.put("userId", userId);
|
|
||||||
post("/oapi/msg/readAll", new HashMap<>(), body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -412,28 +454,28 @@ public class MsgCenterProxyService {
|
|||||||
* 应用标识对前端只出 appId/appName(应用字典富化):appKey 属后端签名标识不下发;
|
* 应用标识对前端只出 appId/appName(应用字典富化):appKey 属后端签名标识不下发;
|
||||||
* 字典查无(授权已回收等)appId/appName 为 null,前端按类型标签降级。
|
* 字典查无(授权已回收等)appId/appName 为 null,前端按类型标签降级。
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> rewrite(JsonNode row, PerRequestAuthz authz, AppDict dict) {
|
private Map<String, Object> rewrite(MsgListItem 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.getMsgId());
|
||||||
out.put("title", text(row, "title"));
|
out.put("title", row.getTitle());
|
||||||
out.put("content", text(row, "content"));
|
out.put("content", row.getContent());
|
||||||
out.put("msgType", row.has("msgType") && row.get("msgType").isNumber() ? row.get("msgType").asInt() : null);
|
out.put("msgType", row.getMsgType());
|
||||||
out.put("jumpType", row.has("jumpType") && row.get("jumpType").isNumber() ? row.get("jumpType").asInt() : null);
|
out.put("jumpType", row.getJumpType());
|
||||||
out.put("jumpPath", text(row, "jumpPath"));
|
out.put("jumpPath", row.getJumpPath());
|
||||||
out.put("jumpApp", text(row, "jumpApp"));
|
out.put("jumpApp", row.getJumpApp());
|
||||||
String appKey = text(row, "appKey");
|
String appKey = row.getAppKey();
|
||||||
AppDictEntry entry = appKey == null ? null : dict.byKey.get(appKey);
|
AppDictEntry entry = appKey == null ? null : dict.byKey.get(appKey);
|
||||||
out.put("appId", entry == null ? null : entry.appId);
|
out.put("appId", entry == null ? null : entry.appId);
|
||||||
out.put("appName", entry == null ? properties.getAppNames().get(appKey) : entry.name);
|
out.put("appName", entry == null ? properties.getAppNames().get(appKey) : entry.name);
|
||||||
out.put("bizType", text(row, "bizType"));
|
out.put("bizType", row.getBizType());
|
||||||
out.put("bizId", text(row, "bizId"));
|
out.put("bizId", row.getBizId());
|
||||||
out.put("sendTime", text(row, "sendTime"));
|
out.put("sendTime", row.getSendTime());
|
||||||
out.put("isRead", row.has("isRead") && row.get("isRead").isNumber() ? row.get("isRead").asInt() : 0);
|
out.put("isRead", row.getIsRead() == null ? 0 : row.getIsRead());
|
||||||
String jumpUrl = jumpUrl(row);
|
String jumpUrl = jumpUrl(row);
|
||||||
// showJump(C1):jumpApp 有值才判权;无权 → false 且 jumpUrl 一并置 null(双保险防前端漏改);
|
// showJump(C1):jumpApp 有值才判权;无权 → false 且 jumpUrl 一并置 null(双保险防前端漏改);
|
||||||
// jumpApp 为空 / UserBase 不可达(判权结果 null)→ 不判权放行
|
// jumpApp 为空 / UserBase 不可达(判权结果 null)→ 不判权放行
|
||||||
Boolean allowed = null;
|
Boolean allowed = null;
|
||||||
String jumpApp = text(row, "jumpApp");
|
String jumpApp = row.getJumpApp();
|
||||||
if (jumpUrl != null && StringUtils.hasText(jumpApp)) {
|
if (jumpUrl != null && StringUtils.hasText(jumpApp)) {
|
||||||
allowed = authz.authorized(jumpApp.trim());
|
allowed = authz.authorized(jumpApp.trim());
|
||||||
}
|
}
|
||||||
@@ -457,10 +499,10 @@ public class MsgCenterProxyService {
|
|||||||
* </ol>
|
* </ol>
|
||||||
* 注册表是多端地址(pc/app/小程序/钉钉)唯一来源;jump-bases 手工基址表已退役(C2)。
|
* 注册表是多端地址(pc/app/小程序/钉钉)唯一来源;jump-bases 手工基址表已退役(C2)。
|
||||||
*/
|
*/
|
||||||
private String jumpUrl(JsonNode row) {
|
private String jumpUrl(MsgListItem row) {
|
||||||
Integer jumpType = row.has("jumpType") && row.get("jumpType").isNumber() ? row.get("jumpType").asInt() : null;
|
Integer jumpType = row.getJumpType();
|
||||||
String jumpPath = text(row, "jumpPath");
|
String jumpPath = row.getJumpPath();
|
||||||
String jumpApp = text(row, "jumpApp");
|
String jumpApp = row.getJumpApp();
|
||||||
if (jumpType == null || jumpType != 1 || !StringUtils.hasText(jumpPath)) {
|
if (jumpType == null || jumpType != 1 || !StringUtils.hasText(jumpPath)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -472,7 +514,7 @@ public class MsgCenterProxyService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String p = path.startsWith("/") ? path : "/" + path;
|
String p = path.startsWith("/") ? path : "/" + path;
|
||||||
String appKey = text(row, "appKey");
|
String appKey = row.getAppKey();
|
||||||
String pcUrl = StringUtils.hasText(jumpApp) ? pcUrlByCode(jumpApp.trim())
|
String pcUrl = StringUtils.hasText(jumpApp) ? pcUrlByCode(jumpApp.trim())
|
||||||
: (StringUtils.hasText(appKey) ? pcUrlByKey(appKey.trim()) : null);
|
: (StringUtils.hasText(appKey) ? pcUrlByKey(appKey.trim()) : null);
|
||||||
if (!StringUtils.hasText(pcUrl)) {
|
if (!StringUtils.hasText(pcUrl)) {
|
||||||
@@ -578,13 +620,6 @@ public class MsgCenterProxyService {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 有值(非空白)才进 query——空串不透传,保持与不传一致 */
|
|
||||||
private static void putIfText(Map<String, String> query, String key, String value) {
|
|
||||||
if (StringUtils.hasText(value)) {
|
|
||||||
query.put(key, value.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String text(JsonNode node, String field) {
|
private static String text(JsonNode node, String field) {
|
||||||
return node != null && node.has(field) && !node.get(field).isNull() ? node.get(field).asText() : null;
|
return node != null && node.has(field) && !node.get(field).isNull() ? node.get(field).asText() : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class MsgApprovalProxyServiceTest {
|
|||||||
assertEquals("张三", postQuery.getValue().get("operatorName"));
|
assertEquals("张三", postQuery.getValue().get("operatorName"));
|
||||||
|
|
||||||
ArgumentCaptor<List<Long>> readIds = ArgumentCaptor.forClass(List.class);
|
ArgumentCaptor<List<Long>> 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), "办理成功后标记已读");
|
assertTrue(readIds.getValue().contains(5L), "办理成功后标记已读");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +180,6 @@ class MsgApprovalProxyServiceTest {
|
|||||||
|
|
||||||
assertFalse((Boolean) out.get("ok"));
|
assertFalse((Boolean) out.get("ok"));
|
||||||
assertEquals("当前用户不是该任务办理人", out.get("message"), "业务拒绝原文透出(flow-center 逐字复核)");
|
assertEquals("当前用户不是该任务办理人", out.get("message"), "业务拒绝原文透出(flow-center 逐字复核)");
|
||||||
verify(msgProxy, never()).read(any(), any());
|
verify(msgProxy, never()).read(any(), any(), any());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.yunhai.platform.openapi.core.OpenApiException;
|
import com.yunhai.platform.openapi.core.OpenApiException;
|
||||||
import com.yunhai.platform.openapi.core.TargetSpec;
|
import com.yunhai.platform.openapi.core.TargetSpec;
|
||||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
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.config.MsgCenterProperties;
|
||||||
|
import com.yunhai.userhome.entity.UserCurrentRole;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
@@ -45,27 +51,56 @@ import static org.mockito.Mockito.when;
|
|||||||
* 160 事故根因 = IDHttp 共享签名拦截器只剥本应用 base-path,目标带 ctx(/usermsg)时
|
* 160 事故根因 = IDHttp 共享签名拦截器只剥本应用 base-path,目标带 ctx(/usermsg)时
|
||||||
* 签名路径多一层前缀 → UserMsg AppAuthFilter 401。回归断言:TargetSpec 从
|
* 签名路径多一层前缀 → UserMsg AppAuthFilter 401。回归断言:TargetSpec 从
|
||||||
* usermsg-base-url 解析出 basePath=/usermsg(per-target 剥离),调用 path 为 ctx 裸路径。
|
* usermsg-base-url 解析出 basePath=/usermsg(per-target 剥离),调用 path 为 ctx 裸路径。
|
||||||
|
* <p>
|
||||||
|
* 按学校隔离(2026-10-09 批次3):消息读取(list/summary/unreadCount/countByApp/readAll)改走
|
||||||
|
* SDK msg 域类型化读方法(Tier 1,仍委托同一个 {@link YschOpenCall},故断言靶心从 JsonNode
|
||||||
|
* 换成 DTO class:{@code MsgListPage/UserMsgSummary/Long/AppMsgCount/Void}),并带
|
||||||
|
* <b>当前激活身份</b>学校口径({@link MsgCenterProxyService#resolveReadSchId},R2);
|
||||||
|
* 待办角标 todo/list 仍按主学校口径(§8 不动角标链路),两者在 summary 上是两个独立入参。
|
||||||
*/
|
*/
|
||||||
@DisplayName("MsgCenterProxyService - YschOpenCall 代理、jumpUrl 改写与 showJump 判权")
|
@DisplayName("MsgCenterProxyService - SDK 类型化读取、学校口径下行与 jumpUrl/showJump 改写")
|
||||||
class MsgCenterProxyServiceTest {
|
class MsgCenterProxyServiceTest {
|
||||||
|
|
||||||
/** 160 生产形态的 UserMsg 直连地址(含目标 context-path) */
|
/** 160 生产形态的 UserMsg 直连地址(含目标 context-path) */
|
||||||
private static final String PROD_BASE_URL = "http://usermsg-service:10009/usermsg";
|
private static final String PROD_BASE_URL = "http://usermsg-service:10009/usermsg";
|
||||||
|
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
private YschOpenCall openCall;
|
private YschOpenCall openCall;
|
||||||
|
private IUserCurrentRoleService userCurrentRoleService;
|
||||||
private MsgCenterProperties properties;
|
private MsgCenterProperties properties;
|
||||||
private MsgCenterProxyService service;
|
private MsgCenterProxyService service;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
openCall = mock(YschOpenCall.class);
|
openCall = mock(YschOpenCall.class);
|
||||||
|
userCurrentRoleService = mock(IUserCurrentRoleService.class);
|
||||||
properties = new MsgCenterProperties();
|
properties = new MsgCenterProperties();
|
||||||
properties.setUsermsgBaseUrl(PROD_BASE_URL);
|
properties.setUsermsgBaseUrl(PROD_BASE_URL);
|
||||||
service = new MsgCenterProxyService(openCall, properties);
|
service = new MsgCenterProxyService(openCall, properties, userCurrentRoleService);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonNode json(String s) throws Exception {
|
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> T dto(String s, Class<T> type) throws Exception {
|
||||||
|
return MAPPER.readValue(s, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <E> java.util.List<E> listDto(String s, Class<E> 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 集合 */
|
/** 判权 stub:/oapi/app/authorized-codes 返回给定 app_code 集合 */
|
||||||
@@ -89,16 +124,118 @@ class MsgCenterProxyServiceTest {
|
|||||||
+ "\",\"name\":\"" + name + "\"}]}]"));
|
+ "\",\"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<Map<String, String>> 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<Map<String, String>> 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<Map<String, String>> 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<Map<String, Object>> rows = service.countByApp(201, 7);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<Map<String, String>> 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<Map<String, Object>> 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<Map<String, Object>> 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<ReadAllMsgRequest> 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
|
@Test
|
||||||
void list_targetSpecStripsUsermsgCtx_forPerTargetSign() {
|
void list_targetSpecStripsUsermsgCtx_forPerTargetSign() {
|
||||||
// 160 回归:签名剥离的载体是 TargetSpec.basePath,必须等于 /usermsg
|
// 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);
|
.thenReturn(null);
|
||||||
|
|
||||||
service.list(201, 1, 10, null, null, null, null, null);
|
service.list(201, 1, 10, null, null, null, null, null, null);
|
||||||
|
|
||||||
ArgumentCaptor<TargetSpec> spec = ArgumentCaptor.forClass(TargetSpec.class);
|
ArgumentCaptor<TargetSpec> 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("http://usermsg-service:10009", spec.getValue().getBaseHost());
|
||||||
assertEquals("/usermsg", spec.getValue().getBasePath(), "basePath=目标 ctx,AppSignInterceptor 据此剥离后签名");
|
assertEquals("/usermsg", spec.getValue().getBasePath(), "basePath=目标 ctx,AppSignInterceptor 据此剥离后签名");
|
||||||
}
|
}
|
||||||
@@ -109,29 +246,31 @@ class MsgCenterProxyServiceTest {
|
|||||||
// 自发消息无 jumpApp → 按发送方 appKey 走 by-key 注册表(严格二选一)
|
// 自发消息无 jumpApp → 按发送方 appKey 走 by-key 注册表(严格二选一)
|
||||||
stubUserbaseGet("/oapi/app/by-key/100019",
|
stubUserbaseGet("/oapi/app/by-key/100019",
|
||||||
"{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}");
|
"{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}");
|
||||||
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(json("{\"total\":2,\"content\":["
|
.thenReturn(dto("{\"total\":2,\"content\":["
|
||||||
+ "{\"msgId\":1,\"title\":\"新待办\",\"content\":\"您有新的审批任务\",\"msgType\":2,"
|
+ "{\"msgId\":1,\"title\":\"新待办\",\"content\":\"您有新的审批任务\",\"msgType\":2,"
|
||||||
+ "\"jumpType\":1,\"jumpPath\":\"/task/123\",\"appKey\":\"100019\",\"bizType\":\"flow.task\","
|
+ "\"jumpType\":1,\"jumpPath\":\"/task/123\",\"appKey\":\"100019\",\"bizType\":\"flow.task\","
|
||||||
+ "\"bizId\":\"123\",\"sendTime\":\"2026-09-13 10:00:00\",\"isRead\":0},"
|
+ "\"bizId\":\"123\",\"sendTime\":\"2026-09-13 10:00:00\",\"isRead\":0},"
|
||||||
+ "{\"msgId\":2,\"title\":\"无跳转\",\"content\":\"系统通知\",\"msgType\":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<String, Object> out = service.list(201, 2, 20, Boolean.TRUE, null, null, null, null);
|
Map<String, Object> out = service.list(201, 2, 20, Boolean.TRUE, null, null, null, null, null);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor<Map<String, String>> 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("201", query.getValue().get("userId"));
|
||||||
assertEquals("2", query.getValue().get("pageIndex"));
|
assertEquals("2", query.getValue().get("pageIndex"));
|
||||||
assertEquals("20", query.getValue().get("pageSize"));
|
assertEquals("20", query.getValue().get("pageSize"));
|
||||||
assertEquals("true", query.getValue().get("onlyUnread"));
|
assertEquals("true", query.getValue().get("onlyUnread"));
|
||||||
|
assertFalse(query.getValue().containsKey("schId"), "schId=null 不下发(向后兼容,验收 7)");
|
||||||
|
|
||||||
assertEquals(2, out.get("total"));
|
assertEquals(2, out.get("total"));
|
||||||
java.util.List<?> rows = (java.util.List<?>) out.get("list");
|
java.util.List<?> rows = (java.util.List<?>) out.get("list");
|
||||||
Map<?, ?> first = (Map<?, ?>) rows.get(0);
|
Map<?, ?> first = (Map<?, ?>) rows.get(0);
|
||||||
assertEquals("http://localhost:8088#/task/123", first.get("jumpUrl"), "注册表 pc_url + # + 裸路径");
|
assertEquals("http://localhost:8088#/task/123", first.get("jumpUrl"), "注册表 pc_url + # + 裸路径");
|
||||||
assertEquals("flow.task", first.get("bizType"));
|
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 不判权放行");
|
assertEquals(Boolean.TRUE, first.get("showJump"), "无 jumpApp 不判权放行");
|
||||||
assertNull(first.get("appName"), "未配置映射的 appKey 不补应用名");
|
assertNull(first.get("appName"), "未配置映射的 appKey 不补应用名");
|
||||||
Map<?, ?> second = (Map<?, ?>) rows.get(1);
|
Map<?, ?> second = (Map<?, ?>) rows.get(1);
|
||||||
@@ -141,12 +280,12 @@ class MsgCenterProxyServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void list_defaultsPageParams() {
|
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);
|
.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")
|
@SuppressWarnings("unchecked")
|
||||||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor<Map<String, String>> 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("1", query.getValue().get("pageIndex"), "非法页码回退 1");
|
||||||
assertEquals("100", query.getValue().get("pageSize"), "页长上限 100");
|
assertEquals("100", query.getValue().get("pageSize"), "页长上限 100");
|
||||||
assertEquals("false", query.getValue().get("onlyUnread"));
|
assertEquals("false", query.getValue().get("onlyUnread"));
|
||||||
@@ -155,10 +294,11 @@ class MsgCenterProxyServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void list_appNameMappedFromAppKey() throws Exception {
|
void list_appNameMappedFromAppKey() throws Exception {
|
||||||
properties.getAppNames().put("100617", "通知公告");
|
properties.getAppNames().put("100617", "通知公告");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"title\":\"通知公告\","
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"title\":\"通知公告\","
|
||||||
+ "\"content\":\"您有一条通知\",\"msgType\":0,\"appKey\":\"100617\",\"isRead\":0}]}"));
|
+ "\"content\":\"您有一条通知\",\"msgType\":0,\"appKey\":\"100617\",\"isRead\":0}]}",
|
||||||
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null);
|
MsgListPage.class));
|
||||||
|
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null, null);
|
||||||
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
|
Map<?, ?> first = (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
|
||||||
assertEquals("通知公告", first.get("appName"), "appKey 命中映射补应用名");
|
assertEquals("通知公告", first.get("appName"), "appKey 命中映射补应用名");
|
||||||
}
|
}
|
||||||
@@ -167,9 +307,9 @@ class MsgCenterProxyServiceTest {
|
|||||||
* 应用字典(appId 翻译/行富化)2026-10-08 起每次 list 固定取一次,与查址无关。 */
|
* 应用字典(appId 翻译/行富化)2026-10-08 起每次 list 固定取一次,与查址无关。 */
|
||||||
@Test
|
@Test
|
||||||
void list_noRegistrySource_jumpUrlNull() throws Exception {
|
void list_noRegistrySource_jumpUrlNull() throws Exception {
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"isRead\":0}]}"));
|
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"isRead\":0}]}", MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertNull(first.get("jumpUrl"), "无注册表查询键 → 不改写");
|
assertNull(first.get("jumpUrl"), "无注册表查询键 → 不改写");
|
||||||
assertEquals(Boolean.FALSE, first.get("showJump"));
|
assertEquals(Boolean.FALSE, first.get("showJump"));
|
||||||
@@ -188,10 +328,10 @@ class MsgCenterProxyServiceTest {
|
|||||||
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\","
|
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\","
|
||||||
+ "\"pcUrl\":\"http://localhost:8077\"}");
|
+ "\"pcUrl\":\"http://localhost:8077\"}");
|
||||||
stubAuthorizedCodes("notice", "flow-center");
|
stubAuthorizedCodes("notice", "flow-center");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/list/detail?serid=1\",\"bizType\":\"flow.task\",\"jumpApp\":\"notice\","
|
+ "\"jumpPath\":\"/list/detail?serid=1\",\"bizType\":\"flow.task\",\"jumpApp\":\"notice\","
|
||||||
+ "\"isRead\":0}]}"));
|
+ "\"isRead\":0}]}", MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals("http://localhost:8077#/list/detail?serid=1", first.get("jumpUrl"),
|
assertEquals("http://localhost:8077#/list/detail?serid=1", first.get("jumpUrl"),
|
||||||
"归属应用 jumpApp 主查 by-code");
|
"归属应用 jumpApp 主查 by-code");
|
||||||
@@ -202,10 +342,10 @@ class MsgCenterProxyServiceTest {
|
|||||||
/** 存量兼容:改造前落库的绝对 URL 直接透传,不再拼基址(B5;双前缀缺陷即源于二次拼接) */
|
/** 存量兼容:改造前落库的绝对 URL 直接透传,不再拼基址(B5;双前缀缺陷即源于二次拼接) */
|
||||||
@Test
|
@Test
|
||||||
void list_absoluteJumpPath_passedThrough() throws Exception {
|
void list_absoluteJumpPath_passedThrough() throws Exception {
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"http://base.yunhaixingtong.com/notice/#/list/detail?serid=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();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals("http://base.yunhaixingtong.com/notice/#/list/detail?serid=1", first.get("jumpUrl"),
|
assertEquals("http://base.yunhaixingtong.com/notice/#/list/detail?serid=1", first.get("jumpUrl"),
|
||||||
"绝对 URL 原样透传");
|
"绝对 URL 原样透传");
|
||||||
@@ -217,9 +357,10 @@ class MsgCenterProxyServiceTest {
|
|||||||
void list_jumpAppAbsent_fallsBackToSendersAppKey() throws Exception {
|
void list_jumpAppAbsent_fallsBackToSendersAppKey() throws Exception {
|
||||||
stubUserbaseGet("/oapi/app/by-key/100019",
|
stubUserbaseGet("/oapi/app/by-key/100019",
|
||||||
"{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}");
|
"{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"appKey\":\"100019\",\"isRead\":0}]}"));
|
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"appKey\":\"100019\",\"isRead\":0}]}",
|
||||||
|
MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals("http://localhost:8088#/task/9", first.get("jumpUrl"), "无归属声明回落发送方 appKey");
|
assertEquals("http://localhost:8088#/task/9", first.get("jumpUrl"), "无归属声明回落发送方 appKey");
|
||||||
assertEquals(Boolean.TRUE, first.get("showJump"));
|
assertEquals(Boolean.TRUE, first.get("showJump"));
|
||||||
@@ -229,10 +370,10 @@ class MsgCenterProxyServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void list_jumpAppMiss_noAppKeyFallback_jumpUrlNull() throws Exception {
|
void list_jumpAppMiss_noAppKeyFallback_jumpUrlNull() throws Exception {
|
||||||
stubUserbaseGet("/oapi/app/by-code/unknown-app", null);
|
stubUserbaseGet("/oapi/app/by-code/unknown-app", null);
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"jumpApp\":\"unknown-app\","
|
+ "\"jumpPath\":\"/task/9\",\"bizType\":\"flow.task\",\"jumpApp\":\"unknown-app\","
|
||||||
+ "\"isRead\":0}]}"));
|
+ "\"isRead\":0}]}", MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertNull(first.get("jumpUrl"));
|
assertNull(first.get("jumpUrl"));
|
||||||
assertEquals(Boolean.FALSE, first.get("showJump"));
|
assertEquals(Boolean.FALSE, first.get("showJump"));
|
||||||
@@ -244,9 +385,10 @@ class MsgCenterProxyServiceTest {
|
|||||||
stubUserbaseGet("/oapi/app/by-code/notice", null);
|
stubUserbaseGet("/oapi/app/by-code/notice", null);
|
||||||
stubUserbaseGet("/oapi/app/by-key/100006",
|
stubUserbaseGet("/oapi/app/by-key/100006",
|
||||||
"{\"id\":100006,\"appKey\":100006,\"name\":\"成绩管理\",\"pcUrl\":\"http://localhost:8090\"}");
|
"{\"id\":100006,\"appKey\":100006,\"name\":\"成绩管理\",\"pcUrl\":\"http://localhost:8090\"}");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/score/9\",\"appKey\":\"100006\",\"jumpApp\":\"notice\",\"isRead\":0}]}"));
|
+ "\"jumpPath\":\"/score/9\",\"appKey\":\"100006\",\"jumpApp\":\"notice\",\"isRead\":0}]}",
|
||||||
|
MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertNull(first.get("jumpUrl"), "jumpApp 命中即只认注册表结果,空就是空");
|
assertNull(first.get("jumpUrl"), "jumpApp 命中即只认注册表结果,空就是空");
|
||||||
verify(openCall, never()).get(eq("userbase"), eq("/oapi/app/by-key/100006"), isNull(), eq(JsonNode.class));
|
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",
|
stubUserbaseGet("/oapi/app/by-code/notice",
|
||||||
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077/\"}");
|
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077/\"}");
|
||||||
stubAuthorizedCodes("notice");
|
stubAuthorizedCodes("notice");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"list/detail?serid=1\",\"jumpApp\":\"notice\",\"isRead\":0}]}"));
|
+ "\"jumpPath\":\"list/detail?serid=1\",\"jumpApp\":\"notice\",\"isRead\":0}]}",
|
||||||
|
MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals("http://localhost:8077#/list/detail?serid=1", first.get("jumpUrl"),
|
assertEquals("http://localhost:8077#/list/detail?serid=1", first.get("jumpUrl"),
|
||||||
"尾斜杠归一 + 无前导斜杠路径补斜杠");
|
"尾斜杠归一 + 无前导斜杠路径补斜杠");
|
||||||
@@ -268,9 +411,9 @@ class MsgCenterProxyServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void list_userMsgDown_degradesToEmpty() {
|
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"));
|
.thenThrow(new OpenApiException("OpenAPI request failed: httpStatus=401"));
|
||||||
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null);
|
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null, null);
|
||||||
assertEquals(0, out.get("total"));
|
assertEquals(0, out.get("total"));
|
||||||
assertEquals(0, ((java.util.List<?>) out.get("list")).size());
|
assertEquals(0, ((java.util.List<?>) out.get("list")).size());
|
||||||
}
|
}
|
||||||
@@ -303,12 +446,12 @@ class MsgCenterProxyServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void summary_combinesUnreadAndTodos_withPerCallDegrade() throws Exception {
|
void summary_combinesUnreadAndTodos_withPerCallDegrade() throws Exception {
|
||||||
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)))
|
||||||
.thenReturn(json("3"));
|
.thenReturn(3L);
|
||||||
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class)))
|
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class)))
|
||||||
.thenReturn(json("[{\"appKey\":\"100019\",\"count\":2}]"));
|
.thenReturn(json("[{\"appKey\":\"100019\",\"count\":2}]"));
|
||||||
|
|
||||||
Map<String, Object> out = service.summary(201, 1);
|
Map<String, Object> out = service.summary(201, null, 1);
|
||||||
assertEquals(3L, out.get("unreadCount"));
|
assertEquals(3L, out.get("unreadCount"));
|
||||||
assertEquals(1, ((java.util.List<?>) out.get("todos")).size());
|
assertEquals(1, ((java.util.List<?>) out.get("todos")).size());
|
||||||
// appKey 属后端签名标识不下发(0e57022 口径收尾):待办行只出 count
|
// appKey 属后端签名标识不下发(0e57022 口径收尾):待办行只出 count
|
||||||
@@ -318,9 +461,9 @@ class MsgCenterProxyServiceTest {
|
|||||||
assertEquals(2, todoRow.get("count"));
|
assertEquals(2, todoRow.get("count"));
|
||||||
|
|
||||||
// 未读数不可达单独降级为 0,待办仍可用
|
// 未读数不可达单独降级为 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"));
|
.thenThrow(new OpenApiException("down"));
|
||||||
Map<String, Object> out2 = service.summary(201, 1);
|
Map<String, Object> out2 = service.summary(201, null, 1);
|
||||||
assertEquals(0L, out2.get("unreadCount"));
|
assertEquals(0L, out2.get("unreadCount"));
|
||||||
assertEquals(1, ((java.util.List<?>) out2.get("todos")).size());
|
assertEquals(1, ((java.util.List<?>) out2.get("todos")).size());
|
||||||
}
|
}
|
||||||
@@ -330,25 +473,25 @@ class MsgCenterProxyServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void list_filtersPassedThrough() throws Exception {
|
void list_filtersPassedThrough() throws Exception {
|
||||||
stubAppDict(100019, "100019", "流程中心");
|
stubAppDict(100019, "100019", "流程中心");
|
||||||
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(json("{\"total\":0,\"content\":[]}"));
|
.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")
|
@SuppressWarnings("unchecked")
|
||||||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor<Map<String, String>> 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("2", query.getValue().get("msgType"));
|
||||||
assertEquals("100019", query.getValue().get("appKey"), "appId 去空白经字典翻译为 appKey 下行");
|
assertEquals("100019", query.getValue().get("appKey"), "appId 去空白经字典翻译为 appKey 下行");
|
||||||
assertEquals("0", query.getValue().get("isRead"));
|
assertEquals("0", query.getValue().get("isRead"));
|
||||||
assertEquals("请假", query.getValue().get("keyword"));
|
assertEquals("请假", query.getValue().get("keyword"));
|
||||||
|
|
||||||
// 全空筛选不出现新键(与历史调用逐字一致)
|
// 全空筛选不出现新键(与历史调用逐字一致)
|
||||||
service.list(201, 1, 10, null, null, " ", null, "");
|
service.list(201, 1, 10, null, null, " ", null, "", null);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ArgumentCaptor<Map<String, String>> query2 = ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor<Map<String, String>> query2 = ArgumentCaptor.forClass(Map.class);
|
||||||
verify(openCall, org.mockito.Mockito.times(2))
|
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("msgType"));
|
||||||
assertFalse(query2.getAllValues().get(1).containsKey("appKey"));
|
assertFalse(query2.getAllValues().get(1).containsKey("appKey"));
|
||||||
assertFalse(query2.getAllValues().get(1).containsKey("isRead"));
|
assertFalse(query2.getAllValues().get(1).containsKey("isRead"));
|
||||||
@@ -359,12 +502,12 @@ class MsgCenterProxyServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void list_appIdUnknownInDict_emptyPageNoUserMsgOutbound() throws Exception {
|
void list_appIdUnknownInDict_emptyPageNoUserMsgOutbound() throws Exception {
|
||||||
// 字典未 stub → /oapi/app/list 返回 null → EMPTY
|
// 字典未 stub → /oapi/app/list 返回 null → EMPTY
|
||||||
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(json("{\"total\":0,\"content\":[]}"));
|
.thenReturn(dto("{\"total\":0,\"content\":[]}", MsgListPage.class));
|
||||||
Map<String, Object> out = service.list(201, 1, 10, null, null, "999999", null, null);
|
Map<String, Object> out = service.list(201, 1, 10, null, null, "999999", null, null, null);
|
||||||
assertEquals(0, out.get("total"));
|
assertEquals(0, out.get("total"));
|
||||||
assertEquals(0, ((java.util.List<?>) out.get("list")).size());
|
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,总数口径;
|
/** 按应用计数(0e57022):UserMsg countByApp 条目经应用字典富化出 appId/appName,总数口径;
|
||||||
@@ -372,11 +515,11 @@ class MsgCenterProxyServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void countByApp_enrichesWithRegistryDict() throws Exception {
|
void countByApp_enrichesWithRegistryDict() throws Exception {
|
||||||
stubAppDict(100011, "100019", "流程中心");
|
stubAppDict(100011, "100019", "流程中心");
|
||||||
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)))
|
||||||
.thenReturn(json("[{\"appKey\":\"100019\",\"total\":12,\"unread\":3},"
|
.thenReturn(listDto("[{\"appKey\":\"100019\",\"total\":12,\"unread\":3},"
|
||||||
+ "{\"appKey\":\"999999\",\"total\":5,\"unread\":1},"
|
+ "{\"appKey\":\"999999\",\"total\":5,\"unread\":1},"
|
||||||
+ "{\"total\":2,\"unread\":0}]"));
|
+ "{\"total\":2,\"unread\":0}]", AppMsgCount.class));
|
||||||
java.util.List<Map<String, Object>> rows = service.countByApp(201);
|
java.util.List<Map<String, Object>> rows = service.countByApp(201, null);
|
||||||
assertEquals(2, rows.size(), "无 appKey 行跳过");
|
assertEquals(2, rows.size(), "无 appKey 行跳过");
|
||||||
Map<String, Object> first = rows.get(0);
|
Map<String, Object> first = rows.get(0);
|
||||||
assertEquals(100011, first.get("appId"));
|
assertEquals(100011, first.get("appId"));
|
||||||
@@ -393,61 +536,60 @@ class MsgCenterProxyServiceTest {
|
|||||||
void countByApp_disabledOrDown_degradesEmpty() {
|
void countByApp_disabledOrDown_degradesEmpty() {
|
||||||
properties.setUsermsgBaseUrl("");
|
properties.setUsermsgBaseUrl("");
|
||||||
when(openCall.hasTarget("usermsg")).thenReturn(false);
|
when(openCall.hasTarget("usermsg")).thenReturn(false);
|
||||||
assertTrue(service.countByApp(201).isEmpty());
|
assertTrue(service.countByApp(201, null).isEmpty());
|
||||||
|
|
||||||
properties.setUsermsgBaseUrl(PROD_BASE_URL);
|
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"));
|
.thenThrow(new OpenApiException("down"));
|
||||||
assertTrue(service.countByApp(201).isEmpty(), "UserMsg 不可达降级空,不抛出");
|
assertTrue(service.countByApp(201, null).isEmpty(), "UserMsg 不可达降级空,不抛出");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 摘要优先走 /oapi/msg/summary(分类计数一次喂饱),且不再单查 unreadCount */
|
/** 摘要优先走 /oapi/msg/summary(分类计数一次喂饱),且不再单查 unreadCount */
|
||||||
@Test
|
@Test
|
||||||
void summary_prefersSummaryEndpoint() throws Exception {
|
void summary_prefersSummaryEndpoint() 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)))
|
||||||
.thenReturn(json("{\"unreadCount\":5,\"sysUnread\":2,\"bizUnread\":1,\"approvalUnread\":2,"
|
.thenReturn(dto("{\"unreadCount\":5,\"sysUnread\":2,\"bizUnread\":1,\"approvalUnread\":2,"
|
||||||
+ "\"total\":9,\"readCount\":4}"));
|
+ "\"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)))
|
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class)))
|
||||||
.thenReturn(json("[]"));
|
.thenReturn(json("[]"));
|
||||||
|
|
||||||
Map<String, Object> out = service.summary(201, 1);
|
Map<String, Object> out = service.summary(201, null, 1);
|
||||||
assertEquals(5L, out.get("unreadCount"));
|
assertEquals(5L, out.get("unreadCount"));
|
||||||
assertEquals(2L, out.get("sysUnread"));
|
assertEquals(2L, out.get("sysUnread"));
|
||||||
assertEquals(1L, out.get("bizUnread"));
|
assertEquals(1L, out.get("bizUnread"));
|
||||||
assertEquals(2L, out.get("approvalUnread"));
|
assertEquals(2L, out.get("approvalUnread"));
|
||||||
assertEquals(9L, out.get("total"));
|
assertEquals(9L, out.get("total"));
|
||||||
assertEquals(4L, out.get("readCount"));
|
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 单查,分类字段缺省 */
|
/** 旧版 UserMsg 无 summary 端点(404/异常)→ 降级 unreadCount 单查,分类字段缺省 */
|
||||||
@Test
|
@Test
|
||||||
void summary_legacyUserMsg_fallsBackToUnreadCount() throws Exception {
|
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"));
|
.thenThrow(new OpenApiException("OpenAPI request failed: httpStatus=404"));
|
||||||
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)))
|
||||||
.thenReturn(json("7"));
|
.thenReturn(7L);
|
||||||
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class)))
|
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class)))
|
||||||
.thenReturn(json("[]"));
|
.thenReturn(json("[]"));
|
||||||
|
|
||||||
Map<String, Object> out = service.summary(201, 1);
|
Map<String, Object> out = service.summary(201, 7, 1);
|
||||||
assertEquals(7L, out.get("unreadCount"));
|
assertEquals(7L, out.get("unreadCount"));
|
||||||
assertFalse(out.containsKey("sysUnread"), "旧端点无分类计数,字段缺省由前端降级");
|
assertFalse(out.containsKey("sysUnread"), "旧端点无分类计数,字段缺省由前端降级");
|
||||||
}
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||||||
@Test
|
verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/unreadCount"), query.capture(), eq(Long.class));
|
||||||
void read_sendsOwnerScopedPayload() {
|
assertEquals("7", query.getValue().get("schId"), "降级单查同样带当前身份学校口径");
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void disabled_degradesWithoutOutbound() {
|
void disabled_degradesWithoutOutbound() {
|
||||||
properties.setUsermsgBaseUrl("");
|
properties.setUsermsgBaseUrl("");
|
||||||
when(openCall.hasTarget("usermsg")).thenReturn(false);
|
when(openCall.hasTarget("usermsg")).thenReturn(false);
|
||||||
assertEquals(0, service.list(201, 1, 10, null, null, null, null, null).get("total"));
|
assertEquals(0, service.list(201, 1, 10, null, null, null, null, null, null).get("total"));
|
||||||
service.read(201, java.util.Collections.singletonList(7L));
|
service.read(201, java.util.Collections.singletonList(7L), null);
|
||||||
verify(openCall, never()).get(any(TargetSpec.class), anyString(), anyMap(), any());
|
verify(openCall, never()).get(any(TargetSpec.class), anyString(), anyMap(), any());
|
||||||
verify(openCall, never()).post(any(TargetSpec.class), anyString(), anyMap(), any(), any());
|
verify(openCall, never()).post(any(TargetSpec.class), anyString(), anyMap(), any(), any());
|
||||||
}
|
}
|
||||||
@@ -457,9 +599,9 @@ class MsgCenterProxyServiceTest {
|
|||||||
// base-url 未配但服务目录有 usermsg(平台公共配置形态):按名调用仍可用
|
// base-url 未配但服务目录有 usermsg(平台公共配置形态):按名调用仍可用
|
||||||
properties.setUsermsgBaseUrl("");
|
properties.setUsermsgBaseUrl("");
|
||||||
when(openCall.hasTarget("usermsg")).thenReturn(true);
|
when(openCall.hasTarget("usermsg")).thenReturn(true);
|
||||||
when(openCall.get(eq("usermsg"), eq("/oapi/msg/unreadCount"), anyMap(), eq(JsonNode.class)))
|
when(openCall.get(eq("usermsg"), eq("/oapi/msg/unreadCount"), anyMap(), eq(Long.class)))
|
||||||
.thenReturn(json("5"));
|
.thenReturn(5L);
|
||||||
Map<String, Object> out = service.summary(201, null);
|
Map<String, Object> out = service.summary(201, null, null);
|
||||||
assertEquals(5L, out.get("unreadCount"));
|
assertEquals(5L, out.get("unreadCount"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,19 +609,19 @@ class MsgCenterProxyServiceTest {
|
|||||||
void summary_disabled_degradesToZero() {
|
void summary_disabled_degradesToZero() {
|
||||||
properties.setUsermsgBaseUrl("");
|
properties.setUsermsgBaseUrl("");
|
||||||
when(openCall.hasTarget("usermsg")).thenReturn(false);
|
when(openCall.hasTarget("usermsg")).thenReturn(false);
|
||||||
Map<String, Object> out = service.summary(201, 1);
|
Map<String, Object> out = service.summary(201, null, 1);
|
||||||
assertEquals(0L, out.get("unreadCount"));
|
assertEquals(0L, out.get("unreadCount"));
|
||||||
assertEquals(0, ((java.util.List<?>) out.get("todos")).size());
|
assertEquals(0, ((java.util.List<?>) out.get("todos")).size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void summary_todoQueryIncludesSchIdOnly_whenNotNull() throws Exception {
|
void summary_todoQueryIncludesSchIdOnly_whenNotNull() throws Exception {
|
||||||
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)))
|
||||||
.thenReturn(json("0"));
|
.thenReturn(0L);
|
||||||
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class)))
|
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/todo/list"), anyMap(), eq(JsonNode.class)))
|
||||||
.thenReturn(json("[]"));
|
.thenReturn(json("[]"));
|
||||||
service.summary(201, null);
|
service.summary(201, null, null);
|
||||||
service.summary(201, 3);
|
service.summary(201, null, 3);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||||||
verify(openCall, org.mockito.Mockito.times(2))
|
verify(openCall, org.mockito.Mockito.times(2))
|
||||||
@@ -491,7 +633,7 @@ class MsgCenterProxyServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void malformedBaseUrl_disabledNotCrash() {
|
void malformedBaseUrl_disabledNotCrash() {
|
||||||
properties.setUsermsgBaseUrl("::::not-a-url");
|
properties.setUsermsgBaseUrl("::::not-a-url");
|
||||||
Map<String, Object> out = service.summary(201, 1);
|
Map<String, Object> out = service.summary(201, null, 1);
|
||||||
assertEquals(0L, out.get("unreadCount"), "非法 base-url 降级而非抛出");
|
assertEquals(0L, out.get("unreadCount"), "非法 base-url 降级而非抛出");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +645,7 @@ class MsgCenterProxyServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map<?, ?> firstRow() throws Exception {
|
private Map<?, ?> firstRow() throws Exception {
|
||||||
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null);
|
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null, null);
|
||||||
return (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
|
return (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,9 +656,10 @@ class MsgCenterProxyServiceTest {
|
|||||||
"{\"id\":100011,\"appKey\":100009,\"name\":\"流程中心\","
|
"{\"id\":100011,\"appKey\":100009,\"name\":\"流程中心\","
|
||||||
+ "\"pcUrl\":\"http://base.yunhaixingtong.com/flow-center/front/index.html#/my\"}");
|
+ "\"pcUrl\":\"http://base.yunhaixingtong.com/flow-center/front/index.html#/my\"}");
|
||||||
stubAuthorizedCodes("flow-center");
|
stubAuthorizedCodes("flow-center");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/task/123\",\"appKey\":\"100009\",\"jumpApp\":\"flow-center\",\"isRead\":0}]}"));
|
+ "\"jumpPath\":\"/task/123\",\"appKey\":\"100009\",\"jumpApp\":\"flow-center\",\"isRead\":0}]}",
|
||||||
|
MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals("http://base.yunhaixingtong.com/flow-center/front/index.html#/task/123", first.get("jumpUrl"),
|
assertEquals("http://base.yunhaixingtong.com/flow-center/front/index.html#/task/123", first.get("jumpUrl"),
|
||||||
"丢 fragment + # 拼接,不产第二个 #");
|
"丢 fragment + # 拼接,不产第二个 #");
|
||||||
@@ -531,9 +674,9 @@ class MsgCenterProxyServiceTest {
|
|||||||
stubUserbaseGet("/oapi/app/by-code/notice",
|
stubUserbaseGet("/oapi/app/by-code/notice",
|
||||||
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077\"}");
|
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077\"}");
|
||||||
stubAuthorizedCodes("flow-center");
|
stubAuthorizedCodes("flow-center");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/list/1\",\"jumpApp\":\"notice\",\"isRead\":0}]}"));
|
+ "\"jumpPath\":\"/list/1\",\"jumpApp\":\"notice\",\"isRead\":0}]}", MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals(Boolean.FALSE, first.get("showJump"), "无该应用权限 → 隐藏跳转入口");
|
assertEquals(Boolean.FALSE, first.get("showJump"), "无该应用权限 → 隐藏跳转入口");
|
||||||
assertNull(first.get("jumpUrl"), "无权时 jumpUrl 同步置 null(双保险)");
|
assertNull(first.get("jumpUrl"), "无权时 jumpUrl 同步置 null(双保险)");
|
||||||
@@ -544,9 +687,9 @@ class MsgCenterProxyServiceTest {
|
|||||||
void list_jumpAppBlank_noAuthzOutbound() throws Exception {
|
void list_jumpAppBlank_noAuthzOutbound() throws Exception {
|
||||||
stubUserbaseGet("/oapi/app/by-key/100019",
|
stubUserbaseGet("/oapi/app/by-key/100019",
|
||||||
"{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}");
|
"{\"id\":100011,\"appKey\":100019,\"name\":\"流程中心\",\"pcUrl\":\"http://localhost:8088\"}");
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"/task/9\",\"appKey\":\"100019\",\"isRead\":0}]}"));
|
+ "\"jumpPath\":\"/task/9\",\"appKey\":\"100019\",\"isRead\":0}]}", MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals(Boolean.TRUE, first.get("showJump"));
|
assertEquals(Boolean.TRUE, first.get("showJump"));
|
||||||
verify(openCall, never()).get(eq("userbase"), eq("/oapi/app/authorized-codes"), anyMap(), eq(JsonNode.class));
|
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 {
|
void list_authzUnreachable_absoluteUrlStillPasses() throws Exception {
|
||||||
when(openCall.get(eq("userbase"), eq("/oapi/app/authorized-codes"), anyMap(), eq(JsonNode.class)))
|
when(openCall.get(eq("userbase"), eq("/oapi/app/authorized-codes"), anyMap(), eq(JsonNode.class)))
|
||||||
.thenThrow(new RuntimeException("connect timeout"));
|
.thenThrow(new RuntimeException("connect timeout"));
|
||||||
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(json("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
.thenReturn(dto("{\"total\":1,\"content\":[{\"msgId\":1,\"jumpType\":1,"
|
||||||
+ "\"jumpPath\":\"http://base.yunhaixingtong.com/notice/#/list/1\",\"jumpApp\":\"notice\","
|
+ "\"jumpPath\":\"http://base.yunhaixingtong.com/notice/#/list/1\",\"jumpApp\":\"notice\","
|
||||||
+ "\"isRead\":0}]}"));
|
+ "\"isRead\":0}]}", MsgListPage.class));
|
||||||
Map<?, ?> first = firstRow();
|
Map<?, ?> first = firstRow();
|
||||||
assertEquals(Boolean.TRUE, first.get("showJump"), "判权通道故障 → 放行");
|
assertEquals(Boolean.TRUE, first.get("showJump"), "判权通道故障 → 放行");
|
||||||
assertEquals("http://base.yunhaixingtong.com/notice/#/list/1", first.get("jumpUrl"));
|
assertEquals("http://base.yunhaixingtong.com/notice/#/list/1", first.get("jumpUrl"));
|
||||||
@@ -572,11 +715,12 @@ class MsgCenterProxyServiceTest {
|
|||||||
stubUserbaseGet("/oapi/app/by-code/notice",
|
stubUserbaseGet("/oapi/app/by-code/notice",
|
||||||
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077\"}");
|
"{\"id\":100013,\"appKey\":100001,\"name\":\"通知公告\",\"pcUrl\":\"http://localhost:8077\"}");
|
||||||
stubAuthorizedCodes("notice");
|
stubAuthorizedCodes("notice");
|
||||||
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(json("{\"total\":2,\"content\":["
|
.thenReturn(dto("{\"total\":2,\"content\":["
|
||||||
+ "{\"msgId\":1,\"jumpType\":1,\"jumpPath\":\"/list/1\",\"jumpApp\":\"notice\",\"isRead\":0},"
|
+ "{\"msgId\":1,\"jumpType\":1,\"jumpPath\":\"/list/1\",\"jumpApp\":\"notice\",\"isRead\":0},"
|
||||||
+ "{\"msgId\":2,\"jumpType\":1,\"jumpPath\":\"/list/2\",\"jumpApp\":\"notice\",\"isRead\":0}]}"));
|
+ "{\"msgId\":2,\"jumpType\":1,\"jumpPath\":\"/list/2\",\"jumpApp\":\"notice\",\"isRead\":0}]}",
|
||||||
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null);
|
MsgListPage.class));
|
||||||
|
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null, null);
|
||||||
java.util.List<?> rows = (java.util.List<?>) out.get("list");
|
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(0)).get("showJump"));
|
||||||
assertEquals(Boolean.TRUE, ((Map<?, ?>) rows.get(1)).get("showJump"));
|
assertEquals(Boolean.TRUE, ((Map<?, ?>) rows.get(1)).get("showJump"));
|
||||||
@@ -589,11 +733,12 @@ class MsgCenterProxyServiceTest {
|
|||||||
void list_userbaseDown_shortCircuit_relativeJumpNull() throws Exception {
|
void list_userbaseDown_shortCircuit_relativeJumpNull() throws Exception {
|
||||||
when(openCall.get(eq("userbase"), anyString(), isNull(), eq(JsonNode.class)))
|
when(openCall.get(eq("userbase"), anyString(), isNull(), eq(JsonNode.class)))
|
||||||
.thenThrow(new RuntimeException("connect timeout"));
|
.thenThrow(new RuntimeException("connect timeout"));
|
||||||
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(json("{\"total\":2,\"content\":["
|
.thenReturn(dto("{\"total\":2,\"content\":["
|
||||||
+ "{\"msgId\":1,\"jumpType\":1,\"jumpPath\":\"/task/1\",\"appKey\":\"100009\",\"bizType\":\"flow.task\",\"isRead\":0},"
|
+ "{\"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}]}"));
|
+ "{\"msgId\":2,\"jumpType\":1,\"jumpPath\":\"/task/2\",\"appKey\":\"100009\",\"bizType\":\"flow.task\",\"isRead\":0}]}",
|
||||||
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null);
|
MsgListPage.class));
|
||||||
|
Map<String, Object> out = service.list(201, 1, 10, null, null, null, null, null, null);
|
||||||
java.util.List<?> rows = (java.util.List<?>) out.get("list");
|
java.util.List<?> rows = (java.util.List<?>) out.get("list");
|
||||||
assertNull(((Map<?, ?>) rows.get(0)).get("jumpUrl"), "注册表不可达无兜底 → null");
|
assertNull(((Map<?, ?>) rows.get(0)).get("jumpUrl"), "注册表不可达无兜底 → null");
|
||||||
assertNull(((Map<?, ?>) rows.get(1)).get("jumpUrl"));
|
assertNull(((Map<?, ?>) rows.get(1)).get("jumpUrl"));
|
||||||
|
|||||||
Reference in New Issue
Block a user