feat(msg): 消息归属换源页面属主——rewrite 出参 appId/appName 按 jumpApp by-code 解析(缓存+N+1 消除,查无/不可达回落发送方);countByApp 双桶按注册表 appId 合并;类目筛选翻译 app_key+app_code 双值(SDK appKeys -70);任务书:消息归属子应用收口-20261013
build / build-push (push) Successful in 1m22s
build / build-push (push) Successful in 1m22s
This commit is contained in:
@@ -101,6 +101,102 @@ public class MsgCenterProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* by-code 解析条目(UserBase /oapi/app/by-code/{appCode},值域=消息 jump_app)。
|
||||
* appId=注册表主键 id(2026-10-13 160 实测与 /oapi/app/list 同体系:notice 主键=100001、
|
||||
* 流程中心主键=100011/app_key=100009,无混体系风险)。
|
||||
*/
|
||||
private static final class ByCodeEntry {
|
||||
final int appId;
|
||||
final String appKey;
|
||||
final String name;
|
||||
final String pcUrl;
|
||||
final String code;
|
||||
|
||||
ByCodeEntry(int appId, String appKey, String name, String pcUrl, String code) {
|
||||
this.appId = appId;
|
||||
this.appKey = appKey;
|
||||
this.name = name;
|
||||
this.pcUrl = pcUrl;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查无哨兵(ConcurrentHashMap 不允许 null 值):脏 code 不重复出站 */
|
||||
private static final ByCodeEntry CODE_MISS = new ByCodeEntry(-1, null, null, null, null);
|
||||
|
||||
/** by-code 缓存本体与时间戳(TTL 对齐 {@link #APP_DICT_TTL_MS}) */
|
||||
private final java.util.concurrent.ConcurrentHashMap<String, ByCodeEntry> byCodeCache = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.concurrent.ConcurrentHashMap<String, Long> byCodeAt = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* by-code 应用缓存(TTL 对齐 {@link #APP_DICT_TTL_MS}):一次调用同时取 name/appId/pcUrl,
|
||||
* 消掉逐行 by-code 查址的 N+1(旧实现每行现查)。UserBase 不可达复用
|
||||
* {@link #userbaseDownUntil} 短路窗口;返回 null=不可达(调用方降级),返回
|
||||
* {@link #CODE_MISS}=查无(回落发送方口径)。
|
||||
*/
|
||||
private ByCodeEntry lookupByCode(String appCode) {
|
||||
if (appCode == null || appCode.isEmpty()) {
|
||||
return CODE_MISS;
|
||||
}
|
||||
Long at = byCodeAt.get(appCode);
|
||||
boolean fresh = at != null && System.currentTimeMillis() - at < APP_DICT_TTL_MS;
|
||||
if (fresh) {
|
||||
ByCodeEntry cached = byCodeCache.get(appCode);
|
||||
return cached == null ? CODE_MISS : cached;
|
||||
}
|
||||
if (userbaseDown()) {
|
||||
ByCodeEntry stale = byCodeCache.get(appCode);
|
||||
return stale == null ? CODE_MISS : stale;
|
||||
}
|
||||
try {
|
||||
JsonNode app = openCall.get(TARGET_USERBASE, "/oapi/app/by-code/" + appCode, null, JsonNode.class);
|
||||
userbaseDownUntil = 0L;
|
||||
ByCodeEntry entry = app != null && app.hasNonNull("id")
|
||||
? new ByCodeEntry(app.get("id").asInt(),
|
||||
app.hasNonNull("appKey") ? String.valueOf(app.get("appKey").asInt()) : null,
|
||||
text(app, "name"), text(app, "pcUrl"), text(app, "appCode"))
|
||||
: CODE_MISS;
|
||||
byCodeCache.put(appCode, entry);
|
||||
byCodeAt.put(appCode, System.currentTimeMillis());
|
||||
return entry;
|
||||
} catch (Exception e) {
|
||||
userbaseDownUntil = System.currentTimeMillis() + USERBASE_RETRY_MS;
|
||||
log.warn("[msgcenter] UserBase by-code 解析不可达(降级回落发送方口径)code={}:{}", appCode, e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** appKey→app_code 反查缓存(list 的 appId 双值翻译用;空串值=查无,退化为单值过滤) */
|
||||
private final java.util.concurrent.ConcurrentHashMap<String, String> appCodeCache = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.concurrent.ConcurrentHashMap<String, Long> appCodeAt = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** appKey→app_code 反查(/oapi/app/by-key,VO 含 appCode):首次查询后缓存,查无/不可达返回 null */
|
||||
private String appCodeByKey(String appKey) {
|
||||
if (appKey == null || appKey.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Long at = appCodeAt.get(appKey);
|
||||
if (at != null && System.currentTimeMillis() - at < APP_DICT_TTL_MS) {
|
||||
return appCodeCache.get(appKey);
|
||||
}
|
||||
if (appCodeCache.containsKey(appKey) && userbaseDown()) {
|
||||
return appCodeCache.get(appKey);
|
||||
}
|
||||
try {
|
||||
JsonNode app = openCall.get(TARGET_USERBASE, "/oapi/app/by-key/" + appKey, null, JsonNode.class);
|
||||
userbaseDownUntil = 0L;
|
||||
String code = app == null || app.isNull() ? null : text(app, "appCode");
|
||||
appCodeCache.put(appKey, code == null ? "" : code);
|
||||
appCodeAt.put(appKey, System.currentTimeMillis());
|
||||
return StringUtils.hasText(code) ? code : null;
|
||||
} catch (Exception e) {
|
||||
userbaseDownUntil = System.currentTimeMillis() + USERBASE_RETRY_MS;
|
||||
log.warn("[msgcenter] UserBase by-key 反查 app_code 不可达(退化单值过滤)appKey={}:{}", appKey, e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用字典(UserBase /oapi/app/list 按用户授权全量,TTL {@link #APP_DICT_TTL_MS})。
|
||||
* 失败/短路窗口内沿用旧字典(无旧值用空);出参不含 appSecret,仅取 id/appKey/name。
|
||||
@@ -237,7 +333,14 @@ public class MsgCenterProxyService {
|
||||
if (entry == null) {
|
||||
return empty; // 字典查无此应用:视作该应用无消息
|
||||
}
|
||||
query.setAppKey(entry.appKey);
|
||||
// 归属口径双值过滤:该应用自发行桶值=app_key,流程中心代发行桶值=app_code
|
||||
List<String> appKeys = new ArrayList<>();
|
||||
appKeys.add(entry.appKey);
|
||||
String code = appCodeByKey(entry.appKey);
|
||||
if (StringUtils.hasText(code) && !code.equals(entry.appKey)) {
|
||||
appKeys.add(code);
|
||||
}
|
||||
query.setAppKeys(appKeys);
|
||||
}
|
||||
query.setIsRead(isRead);
|
||||
query.setKeyword(StringUtils.hasText(keyword) ? keyword.trim() : null);
|
||||
@@ -327,7 +430,9 @@ public class MsgCenterProxyService {
|
||||
/**
|
||||
* 按应用消息计数(UserMsg /oapi/msg/countByApp),条目富化注册表 appId/appName:
|
||||
* [{appId, appName, total, unread}]——消息中心「按应用」清单角标(总数口径,前端隐藏 0 行)。
|
||||
* appId 取应用字典(按用户授权列表),字典查无(如授权已回收的来源应用)时 appId=null,前端不展示。
|
||||
* UserMsg 桶值为归属口径 code-or-key 混合(代发=业务方 app_code、自发=app_key):
|
||||
* 逐桶先 byCode 解析、未命中回落字典 byKey(发送方),解析出应用后按 appId 合并 total/unread
|
||||
* (同一应用的自发桶与代发货桶并成一类目)。双查无的桶跳过(与旧「字典查无即丢」一致)。
|
||||
* schId=当前身份学校口径(null=全部)。停机降级空列表。
|
||||
*/
|
||||
public List<Map<String, Object>> countByApp(Integer userId, Integer schId) {
|
||||
@@ -341,20 +446,49 @@ public class MsgCenterProxyService {
|
||||
return empty;
|
||||
}
|
||||
AppDict dict = appDict(userId);
|
||||
List<Map<String, Object>> out = new ArrayList<>();
|
||||
Map<Object, Map<String, Object>> merged = new LinkedHashMap<>();
|
||||
for (AppMsgCount a : counts) {
|
||||
String appKey = a == null ? null : a.getAppKey();
|
||||
if (appKey == null) {
|
||||
String bucket = a == null ? null : a.getAppKey();
|
||||
if (bucket == null || bucket.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
AppDictEntry entry = dict.byKey.get(appKey);
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("appId", entry == null ? null : entry.appId);
|
||||
row.put("appName", entry == null ? properties.getAppNames().get(appKey) : entry.name);
|
||||
row.put("total", a.getTotal());
|
||||
row.put("unread", a.getUnread());
|
||||
out.add(row);
|
||||
AppDictEntry entry = dict.byKey.get(bucket);
|
||||
// 纯数字桶值不可能是 app_code,免一次注定查无的出站;
|
||||
// 数字脏桶(字典查无)维持现状——行保留、appId=null 由前端隐藏
|
||||
ByCodeEntry codeEntry = entry == null && !bucket.matches("\\d+")
|
||||
? lookupByCode(bucket) : null;
|
||||
Integer appId;
|
||||
String name;
|
||||
if (entry != null) {
|
||||
appId = entry.appId;
|
||||
name = entry.name;
|
||||
} else if (codeEntry != null && codeEntry != CODE_MISS) {
|
||||
appId = codeEntry.appId;
|
||||
name = codeEntry.name;
|
||||
} else if (codeEntry == CODE_MISS) {
|
||||
continue; // code 脏桶双查无:丢弃,与改造前一致
|
||||
} else {
|
||||
appId = null;
|
||||
name = properties.getAppNames().get(bucket);
|
||||
}
|
||||
// 合桶键=注册表主键 appId:自发桶(byKey)与代发桶(byCode)同应用并成一类目
|
||||
Map<String, Object> row = merged.get(appId);
|
||||
if (row == null) {
|
||||
row = new LinkedHashMap<>();
|
||||
row.put("appId", appId);
|
||||
row.put("appName", name);
|
||||
row.put("total", 0L);
|
||||
row.put("unread", 0L);
|
||||
merged.put(appId, row);
|
||||
}
|
||||
row.put("total", (Long) row.get("total") + a.getTotal());
|
||||
row.put("unread", (Long) row.get("unread") + a.getUnread());
|
||||
}
|
||||
List<Map<String, Object>> out = new ArrayList<>(merged.values());
|
||||
out.sort((x, y) -> {
|
||||
int c = Long.compare((Long) y.get("total"), (Long) x.get("total"));
|
||||
return c != 0 ? c : Long.compare((Long) y.get("unread"), (Long) x.get("unread"));
|
||||
});
|
||||
return out;
|
||||
} catch (Exception e) {
|
||||
log.warn("[msgcenter] UserMsg 按应用计数不可达(降级空)userId={}:{}", userId, e.toString());
|
||||
@@ -451,8 +585,9 @@ public class MsgCenterProxyService {
|
||||
|
||||
/**
|
||||
* MsgListVo 逐字段透传 + jumpUrl 改写(jumpType=1 且查到基址时)+ showJump 判权 + 应用信息补全。
|
||||
* 应用标识对前端只出 appId/appName(应用字典富化):appKey 属后端签名标识不下发;
|
||||
* 字典查无(授权已回收等)appId/appName 为 null,前端按类型标签降级。
|
||||
* 应用标识对前端只出 appId/appName:归属换源(页面属主优先)——jumpApp(值域=app_code)可解析时
|
||||
* 出参指向业务方应用,为空/查无/不可达回落发送方 appKey(字典 byKey,再落手工名称表)。
|
||||
* appKey 属后端签名标识不下发;两口径均查无时 appId/appName 为 null,前端按类型标签降级。
|
||||
*/
|
||||
private Map<String, Object> rewrite(MsgListItem row, PerRequestAuthz authz, AppDict dict) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
@@ -463,10 +598,33 @@ public class MsgCenterProxyService {
|
||||
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);
|
||||
// 归属换源(页面属主优先):appId/appName 出参 jumpApp(值域=app_code)非空且 byCode 查到 → 用业务方应用;
|
||||
// jumpApp 为空/查无(脏值)/UserBase 不可达 → 回落发送方 byKey(宁可显示流程中心,不抛错)。
|
||||
// 其余出参(jumpApp/jumpUrl/showJump/bizType…)不动,前端已依赖。
|
||||
String jumpApp = row.getJumpApp();
|
||||
String ownerCode = jumpApp == null ? null : jumpApp.trim();
|
||||
AppDictEntry entry = null;
|
||||
Integer appId = null;
|
||||
String appName = null;
|
||||
if (StringUtils.hasText(ownerCode)) {
|
||||
ByCodeEntry codeEntry = lookupByCode(ownerCode);
|
||||
if (codeEntry != null && codeEntry != CODE_MISS) {
|
||||
appId = codeEntry.appId;
|
||||
appName = codeEntry.name;
|
||||
}
|
||||
}
|
||||
if (appId == null) {
|
||||
String appKey = row.getAppKey();
|
||||
entry = appKey == null ? null : dict.byKey.get(appKey);
|
||||
if (entry != null) {
|
||||
appId = entry.appId;
|
||||
appName = entry.name;
|
||||
} else {
|
||||
appName = properties.getAppNames().get(appKey);
|
||||
}
|
||||
}
|
||||
out.put("appId", appId);
|
||||
out.put("appName", appName);
|
||||
out.put("bizType", row.getBizType());
|
||||
out.put("bizId", row.getBizId());
|
||||
out.put("sendTime", row.getSendTime());
|
||||
@@ -475,9 +633,8 @@ public class MsgCenterProxyService {
|
||||
// showJump(C1):jumpApp 有值才判权;无权 → false 且 jumpUrl 一并置 null(双保险防前端漏改);
|
||||
// jumpApp 为空 / UserBase 不可达(判权结果 null)→ 不判权放行
|
||||
Boolean allowed = null;
|
||||
String jumpApp = row.getJumpApp();
|
||||
if (jumpUrl != null && StringUtils.hasText(jumpApp)) {
|
||||
allowed = authz.authorized(jumpApp.trim());
|
||||
if (jumpUrl != null && StringUtils.hasText(ownerCode)) {
|
||||
allowed = authz.authorized(ownerCode);
|
||||
}
|
||||
boolean showJump = jumpUrl != null && !Boolean.FALSE.equals(allowed);
|
||||
if (Boolean.FALSE.equals(allowed)) {
|
||||
@@ -531,9 +688,14 @@ public class MsgCenterProxyService {
|
||||
return base + "#" + p;
|
||||
}
|
||||
|
||||
/** 注册表 pc_url 查询(by-code 主查)。UserBase 不可达/查无 → null,失败进短路窗口 */
|
||||
/**
|
||||
* 注册表 pc_url 查询(by-code 主查):走 by-code 缓存,一次调用同取 name 与 pcUrl
|
||||
* (消掉旧逐行现查的 N+1)。不可达短路窗口内沿用已缓存值(旧 pcUrl 拼接语义不变,
|
||||
* 查不到/无缓存才隐藏跳转入口)。
|
||||
*/
|
||||
private String pcUrlByCode(String appCode) {
|
||||
return lookupPcUrl("/oapi/app/by-code/" + appCode);
|
||||
ByCodeEntry entry = lookupByCode(appCode.trim());
|
||||
return entry == null || entry == CODE_MISS ? null : entry.pcUrl;
|
||||
}
|
||||
|
||||
/** 注册表 pc_url 查询(发送方 appKey 回落,appKey 是 app_key 列非主键 id) */
|
||||
|
||||
@@ -124,6 +124,12 @@ class MsgCenterProxyServiceTest {
|
||||
+ "\",\"name\":\"" + name + "\"}]}]"));
|
||||
}
|
||||
|
||||
/** 应用字典 stub(多应用形态):合桶/双值翻译验证两桶同归一应用 */
|
||||
private void stubAppDictRaw(String groupsJson) throws Exception {
|
||||
when(openCall.get(eq("userbase"), eq("/oapi/app/list"), anyMap(), eq(JsonNode.class)))
|
||||
.thenReturn(json(groupsJson));
|
||||
}
|
||||
|
||||
// ===== R2:读取口径 schId 必须来自当前激活身份(严禁会话主学校) =====
|
||||
|
||||
@Test
|
||||
@@ -482,7 +488,8 @@ class MsgCenterProxyServiceTest {
|
||||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.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("100019", query.getValue().get("appKeys"), "appId 去空白经字典翻译为归属口径 appKeys 下行");
|
||||
assertFalse(query.getValue().containsKey("appKey"), "归属口径取代发送方 appKey 口径");
|
||||
assertEquals("0", query.getValue().get("isRead"));
|
||||
assertEquals("请假", query.getValue().get("keyword"));
|
||||
|
||||
@@ -649,6 +656,88 @@ class MsgCenterProxyServiceTest {
|
||||
return (Map<?, ?>) ((java.util.List<?>) out.get("list")).get(0);
|
||||
}
|
||||
|
||||
// ===== 消息归属子应用收口(2026-10-13):出参归属换源、countByApp 合桶、list 双值翻译 =====
|
||||
|
||||
private void stubMsgList(String contentJson) throws Exception {
|
||||
when(openCall.get(any(TargetSpec.class), eq("/oapi/msg/list"), anyMap(), eq(MsgListPage.class)))
|
||||
.thenReturn(dto("{\"total\":1,\"content\":" + contentJson + "}", MsgListPage.class));
|
||||
}
|
||||
|
||||
/** 换源三态①:jumpApp 可解析 → appId/appName 指向业务方应用(页面属主优先于发送方) */
|
||||
@Test
|
||||
void list_jumpAppResolvable_ownershipRewrittenToBusinessApp() throws Exception {
|
||||
stubAppDict(100011, "100009", "流程中心");
|
||||
stubUserbaseGet("/oapi/app/by-code/notice",
|
||||
"{\"id\":100001,\"appKey\":100001,\"name\":\"通知公告\",\"appCode\":\"notice\",\"pcUrl\":\"http://localhost:8077\"}");
|
||||
stubMsgList("[{\"msgId\":1,\"jumpType\":1,\"jumpPath\":\"/list/detail?serid=59\","
|
||||
+ "\"appKey\":\"100009\",\"jumpApp\":\"notice\",\"bizType\":\"flow.task\",\"isRead\":0}]");
|
||||
Map<?, ?> first = firstRow();
|
||||
assertEquals(100001, first.get("appId"), "归属出参跟页面属主,不跟发送方");
|
||||
assertEquals("通知公告", first.get("appName"));
|
||||
}
|
||||
|
||||
/** 换源三态②:jumpApp 脏值查无 → 回落发送方字典口径(现状对齐,appId/appName 走 byKey) */
|
||||
@Test
|
||||
void list_jumpAppDirty_fallsBackToSenderDictEntry() throws Exception {
|
||||
stubAppDict(100011, "100009", "流程中心");
|
||||
stubUserbaseGet("/oapi/app/by-code/svc_item", null);
|
||||
stubMsgList("[{\"msgId\":1,\"appKey\":\"100009\",\"jumpApp\":\"svc_item\",\"bizType\":\"flow.task\",\"isRead\":0}]");
|
||||
Map<?, ?> first = firstRow();
|
||||
assertEquals(100011, first.get("appId"), "脏 jumpApp 回落发送方 appKey 字典");
|
||||
assertEquals("流程中心", first.get("appName"));
|
||||
}
|
||||
|
||||
/** 换源三态③:jumpApp 为空(办结/催办按拍板留流程中心)→ 不调 by-code,直接发送方口径 */
|
||||
@Test
|
||||
void list_jumpAppAbsent_noByCodeCall_senderOwnership() throws Exception {
|
||||
stubAppDict(100011, "100009", "流程中心");
|
||||
stubMsgList("[{\"msgId\":1,\"appKey\":\"100009\",\"jumpApp\":null,\"bizType\":\"flow.result\",\"isRead\":0}]");
|
||||
Map<?, ?> first = firstRow();
|
||||
assertEquals(100011, first.get("appId"));
|
||||
assertEquals("流程中心", first.get("appName"));
|
||||
verify(openCall, never()).get(eq("userbase"), contains("/oapi/app/by-code/"), isNull(), eq(JsonNode.class));
|
||||
}
|
||||
|
||||
/** countByApp 合桶:notice 代发桶 + 100001 自发桶解析到同一注册表主键 → 并一行、计数相加 */
|
||||
@Test
|
||||
void countByApp_mergesCodeAndKeyBucketsIntoOneApp() throws Exception {
|
||||
stubAppDict(100001, "100001", "通知公告");
|
||||
stubUserbaseGet("/oapi/app/by-code/notice",
|
||||
"{\"id\":100001,\"appKey\":100001,\"name\":\"通知公告\",\"appCode\":\"notice\"}");
|
||||
when(openCall.getList(any(TargetSpec.class), eq("/oapi/msg/countByApp"), anyMap(), eq(AppMsgCount.class)))
|
||||
.thenReturn(listDto("[{\"appKey\":\"notice\",\"total\":2,\"unread\":2},"
|
||||
+ "{\"appKey\":\"100001\",\"total\":1,\"unread\":0},"
|
||||
+ "{\"appKey\":\"100009\",\"total\":3,\"unread\":1}]", AppMsgCount.class));
|
||||
|
||||
java.util.List<Map<String, Object>> rows = service.countByApp(201, null);
|
||||
|
||||
assertEquals(2, rows.size(), "notice 与 100001 两桶合并为通知公告一类目");
|
||||
Map<String, Object> notice = rows.stream().filter(r -> Integer.valueOf(100001).equals(r.get("appId")))
|
||||
.findFirst().orElseThrow(IllegalStateException::new);
|
||||
assertEquals("通知公告", notice.get("appName"));
|
||||
assertEquals(3L, notice.get("total"));
|
||||
assertEquals(2L, notice.get("unread"));
|
||||
assertEquals(3L, rows.get(0).get("total"), "合并后仍按总数降序");
|
||||
}
|
||||
|
||||
/** list 的 appId 筛选翻译改双值:app_key + app_code 下行 appKeys,不再下行 appKey */
|
||||
@Test
|
||||
void list_appIdFilter_translatesToDualOwnerValues() throws Exception {
|
||||
stubAppDictRaw("[{\"apps\":[{\"id\":100001,\"appKey\":\"100001\",\"name\":\"通知公告\"},"
|
||||
+ "{\"id\":100011,\"appKey\":\"100009\",\"name\":\"流程中心\"}]}]");
|
||||
stubUserbaseGet("/oapi/app/by-key/100001", "{\"id\":100001,\"appKey\":100001,\"appCode\":\"notice\"}");
|
||||
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, null, "100001", null, null, null);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, String>> query = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).get(any(TargetSpec.class), eq("/oapi/msg/list"), query.capture(), eq(MsgListPage.class));
|
||||
assertEquals("100001,notice", query.getValue().get("appKeys"), "自发桶+代发桶一次筛全");
|
||||
assertFalse(query.getValue().containsKey("appKey"), "归属口径取代发送方口径");
|
||||
}
|
||||
|
||||
/** 注册表命中:pc_url 含 #/route fragment → 丢 fragment、保 index.html、拼 #,无双 hash(§3.2) */
|
||||
@Test
|
||||
void list_registryPcUrlWithFragment_normalizedSingleHash() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user