package com.xuqm.push.service; import com.xuqm.common.security.JwtUtil; import com.xuqm.push.entity.DeviceLoginLogEntity; import com.xuqm.push.entity.DeviceTokenEntity; import com.xuqm.push.repository.DeviceLoginLogRepository; import com.xuqm.push.repository.DeviceTokenRepository; import io.jsonwebtoken.Claims; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Service public class PushDiagnosticsService { private final DeviceTokenRepository tokenRepository; private final DeviceLoginLogRepository logRepository; private final ImPresenceClient presenceClient; private final PushDispatcher pushDispatcher; private final JwtUtil jwtUtil; public PushDiagnosticsService(DeviceTokenRepository tokenRepository, DeviceLoginLogRepository logRepository, ImPresenceClient presenceClient, PushDispatcher pushDispatcher, JwtUtil jwtUtil) { this.tokenRepository = tokenRepository; this.logRepository = logRepository; this.presenceClient = presenceClient; this.pushDispatcher = pushDispatcher; this.jwtUtil = jwtUtil; } public PushTokenDiagnostics searchByToken(String token, String appIdHint) { Optional tokenMatch = tokenRepository.findFirstByToken(token); String appId = appIdHint; String userId = null; String tokenType = "UNKNOWN"; if (tokenMatch.isPresent()) { DeviceTokenEntity device = tokenMatch.get(); appId = device.getAppId(); userId = device.getUserId(); tokenType = "PUSH"; } else { Optional resolved = presenceClient.resolveToken(token); if (resolved.isPresent()) { appId = resolved.get().appId(); userId = resolved.get().userId(); tokenType = "IM"; } else { try { Claims claims = jwtUtil.parse(token); userId = claims.getSubject(); String claimAppId = claims.get("appId", String.class); appId = claimAppId == null || claimAppId.isBlank() ? appIdHint : claimAppId; tokenType = "IM"; } catch (Exception ignored) { // Keep UNKNOWN and return an empty diagnostic. } } } if (appId == null || appId.isBlank() || userId == null || userId.isBlank()) { return new PushTokenDiagnostics(tokenType, appId, userId, false, 0L, false, null, List.of(), List.of()); } ImPresenceClient.PresenceStatus presence = presenceClient.userStatus(appId, userId) .orElse(new ImPresenceClient.PresenceStatus(appId, userId, false, 0L)); List devices = tokenRepository.findByAppIdAndUserIdOrderByLastLoginAtDescUpdatedAtDesc(appId, userId); List deliverableDevices = pushDispatcher.selectedPushTargets(appId, userId) .stream() .map(DeviceInfo::from) .toList(); DeviceInfo deliverable = deliverableDevices.isEmpty() ? null : deliverableDevices.get(0); boolean canSendOffline = !presence.online() && !deliverableDevices.isEmpty(); return new PushTokenDiagnostics( tokenType, appId, userId, presence.online(), presence.lastSeenAt(), canSendOffline, deliverable, deliverableDevices, devices.stream().map(DeviceInfo::from).toList()); } public PushTokenDiagnostics searchByUserId(String appId, String userId) { ImPresenceClient.PresenceStatus presence = presenceClient.userStatus(appId, userId) .orElse(new ImPresenceClient.PresenceStatus(appId, userId, false, 0L)); List devices = tokenRepository.findByAppIdAndUserIdOrderByLastLoginAtDescUpdatedAtDesc(appId, userId); List deliverableDevices = pushDispatcher.selectedPushTargets(appId, userId) .stream() .map(DeviceInfo::from) .toList(); DeviceInfo deliverable = deliverableDevices.isEmpty() ? null : deliverableDevices.get(0); boolean canSendOffline = !presence.online() && !deliverableDevices.isEmpty(); return new PushTokenDiagnostics( "USER", appId, userId, presence.online(), presence.lastSeenAt(), canSendOffline, deliverable, deliverableDevices, devices.stream().map(DeviceInfo::from).toList()); } public TestPushResult sendTestOfflineMessage(String appId, String userId, String title, String body, String payload) { List targets = pushDispatcher.selectedPushTargets(appId, userId) .stream() .map(DeviceInfo::from) .toList(); if (!targets.isEmpty()) { pushDispatcher.pushToUser(appId, userId, title, body, payload); } return new TestPushResult(appId, userId, !targets.isEmpty(), targets.size(), targets); } public Page deviceLogs(String appId, String userId, int page, int size) { int safePage = Math.max(page, 0); int safeSize = Math.min(Math.max(size, 1), 200); return logRepository.findByAppIdAndUserIdOrderByCreatedAtDesc(appId, userId, PageRequest.of(safePage, safeSize)); } public record PushTokenDiagnostics( String tokenType, String appId, String userId, boolean online, long lastSeenAt, boolean canSendOfflineMessage, DeviceInfo deliverableDevice, List deliverableDevices, List devices) {} public record TestPushResult( String appId, String userId, boolean sent, int targetCount, List targets) {} public record DeviceInfo( String id, String vendor, String tokenPreview, String platform, String deviceId, String brand, String model, String osVersion, String appVersion, boolean receivePush, LocalDateTime lastLoginAt, LocalDateTime updatedAt) { static DeviceInfo from(DeviceTokenEntity entity) { return new DeviceInfo( entity.getId(), entity.getVendor().name(), PushDispatcher.previewToken(entity.getToken()), entity.getPlatform(), entity.getDeviceId(), entity.getBrand(), entity.getModel(), entity.getOsVersion(), entity.getAppVersion(), entity.isReceivePush(), entity.getLastLoginAt(), entity.getUpdatedAt()); } } }