refactor(push): 测试推送改为手动触发(移除请求预览)
build / build-push (push) Successful in 1m0s

- PushDispatcher 删除 previewManual(预览体构建),保留 dispatchManual 手动投递
- PushSubscriptionController /push/test 不再返回 preview,改为「手动触发一次推送」语义(前端推送测试页已下线)
- PushTestRequest 注释同步;同文件的「推送角色」过滤改走身份行(findUserIdsWithAnyRole),与 roleList 同源
This commit is contained in:
chenxing
2026-09-18 11:13:05 +08:00
parent 712e94803f
commit 6cc1bc23f7
3 changed files with 17 additions and 82 deletions
@@ -27,7 +27,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
/**
* 推送订阅控制器:测试推送 + 推送日志记录 / 重试。
* 推送订阅控制器:手动触发推送(组/用户全量同步)+ 推送日志记录 / 重试。
*
* @author chenx
* @date 2026/8/14
@@ -45,17 +45,14 @@ public class PushSubscriptionController {
@Autowired
private AccessScopeService accessScopeService;
/** 测试推送:手动触发一次推送(投递 MQ),并返回请求预览(URL/头/体,密钥不外露)。 */
/** 手动触发一次推送(投递 MQ),供应用推送设置的组/用户全量同步等场景调用。 */
@PostMapping("Test")
public Map<String, Object> test(@RequestBody PushTestRequest request) {
accessScopeService.requireSuper(); // 日志/推送管理仅超管
pushDispatcher.dispatchManual(request.getAppId(), request.getInterfacePath(), request.getTargetIds());
Map<String, Object> preview = pushDispatcher.previewManual(
request.getAppId(), request.getInterfacePath(), request.getTargetIds());
Map<String, Object> result = new LinkedHashMap<>();
result.put("sent", true);
result.put("message", "已投递推送队列,可在「推送日志」查看实际响应与耗时");
result.put("preview", preview);
return result;
}
@@ -5,7 +5,7 @@ import lombok.Data;
import java.util.List;
/**
* 测试推送请求(测试推送页调用)。
* 手动触发推送请求(应用推送设置的组/用户全量同步调用)。
*
* @author chenx
* @date 2026/8/14
@@ -7,6 +7,7 @@ import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.enums.StatusEnum;
import com.yunhai.userbase.event.DataSyncEvent;
import com.yunhai.userbase.model.push.PushInterfaceType;
import com.yunhai.userbase.model.push.PushTaskMessage;
@@ -14,6 +15,7 @@ import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.repository.SysUserRoleRelRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,7 +30,7 @@ import java.util.stream.Collectors;
/**
* 推送分发器(生产方):<br>
* ① 自动触发:监听 {@link DataSyncEvent}(AFTER_COMMIT),遍历所有开启推送的订阅方,按配置过滤后投递 MQ;<br>
* ② 手动测试:{@link #dispatchManual} 供测试推送页调用。<br><br>
* ② 手动触发:{@link #dispatchManual} 供应用推送设置的「组/用户全量同步」调用。<br><br>
* 签名在此处由基础平台计算一次(HMAC-SHA256(业务JSON+t, secret)),消息携带 t/sign,消费方原样 POST。
*
* @author chenx
@@ -47,6 +49,8 @@ public class PushDispatcher {
@Autowired
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Autowired
private SysUserRoleRelRepository sysUserRoleRelRepository;
@Autowired
private GroupScopeHelper groupScopeHelper;
@Autowired
private PushPayloadAssembler pushPayloadAssembler;
@@ -132,68 +136,6 @@ public class PushDispatcher {
dispatchToOne(app, type, groups, users, "manual", force);
}
/**
* 生成一次推送请求的预览(URL / 头 / 体),供测试推送页展示。<br>
* 签名在服务端用 secret 计算,前端不持有密钥;实际投递仍走 {@link #dispatchManual} → MQ。
*/
public Map<String, Object> previewManual(Integer appId, String interfacePath, List<Integer> targetIds) {
ApplicationInfo app = applicationInfoRepository.findById(appId).orElse(null);
if (app == null) {
throw new DException("应用不存在");
}
PushInterfaceType type = PushInterfaceType.getByPath(interfacePath);
if (type == null) {
throw new DException("不支持的接口路径:" + interfacePath);
}
List<SysGroup> groups = Collections.emptyList();
List<SysUser> users = Collections.emptyList();
if (type == PushInterfaceType.SYNC_GROUP) {
groups = sysGroupRepository.findAll();
} else if (type == PushInterfaceType.SYNC_USER) {
users = sysUserRepository.findAll();
} else if (type.isGroup()) {
groups = (targetIds == null || targetIds.isEmpty())
? sysGroupRepository.findAll() : sysGroupRepository.findAllById(targetIds);
} else {
users = (targetIds == null || targetIds.isEmpty())
? sysUserRepository.findAll() : sysUserRepository.findAllById(targetIds);
}
List<Object> bodies = buildBodies(type, groups, users);
if (bodies.isEmpty()) {
Map<String, Object> empty = new LinkedHashMap<>();
empty.put("url", joinUrl(app.getPushBaseUrl(), type.getPath()));
empty.put("method", "POST");
empty.put("headers", buildHeaders("", ""));
empty.put("body", "{}");
empty.put("batch", 0);
return empty;
}
try {
String body = objectMapper.writeValueAsString(bodies.get(0));
String t = String.valueOf(System.currentTimeMillis());
String secret = (app.getPushSecret() != null && !app.getPushSecret().isEmpty())
? app.getPushSecret() : app.getAppSecret();
String sign = PushSigner.sign(body, secret, t);
Map<String, Object> preview = new LinkedHashMap<>();
preview.put("url", joinUrl(app.getPushBaseUrl(), type.getPath()));
preview.put("method", "POST");
preview.put("headers", buildHeaders(t, sign));
preview.put("body", body);
preview.put("batch", bodies.size());
return preview;
} catch (Exception e) {
throw new DException("预览生成失败:" + e.getMessage());
}
}
private static Map<String, String> buildHeaders(String t, String sign) {
Map<String, String> headers = new LinkedHashMap<>();
headers.put("Content-Type", "application/json");
headers.put("t", t);
headers.put("sign", sign);
return headers;
}
// ===================== 核心 =====================
/** 删除类事件专用投递:跳过 DB 重载与范围过滤,直接用事件携带的 id 构建删除体。 */
@@ -331,6 +273,13 @@ public class PushDispatcher {
if (schoolSet.isEmpty() && roleSet.isEmpty()) {
return users;
}
// 角色过滤一次性批量取「命中推送角色的 userId」:事实源=身份行 sys_user_role_rel(与 roleList 同源),
// 此前读 sys_user.roles 快照列——该列已停写,新建/改过身份的用户恒为空,会被静默过滤掉不推送
Set<Integer> roleMatchedUserIds = (roleSet.isEmpty() || users.isEmpty())
? null
: new HashSet<>(sysUserRoleRelRepository.findUserIdsWithAnyRole(
users.stream().map(SysUser::getId).collect(Collectors.toList()),
roleSet, StatusEnum.NORMAL));
return users.stream().filter(u -> {
if (!schoolSet.isEmpty()) {
List<SysUserGroupRel> rels = sysUserGroupRelRepository.findByUserId(u.getId());
@@ -342,11 +291,9 @@ public class PushDispatcher {
return false;
}
}
if (!roleSet.isEmpty()) {
if (u.getRoles() == null || u.getRoles().stream().noneMatch(roleSet::contains)) {
if (roleMatchedUserIds != null && !roleMatchedUserIds.contains(u.getId())) {
return false;
}
}
return true;
}).collect(Collectors.toList());
}
@@ -382,13 +329,4 @@ public class PushDispatcher {
}
return set;
}
private static String joinUrl(String base, String path) {
if (base == null) {
return path;
}
String b = base.endsWith("/") ? base : base + "/";
String p = path.startsWith("/") ? path.substring(1) : path;
return b + p;
}
}