XuqmGroup-Server/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java

210 行
8.8 KiB
Java

2026-04-21 22:07:29 +08:00
package com.xuqm.push.service;
import com.xuqm.push.entity.DeviceTokenEntity;
import com.xuqm.push.entity.DeviceLoginLogEntity;
import com.xuqm.push.repository.DeviceLoginLogRepository;
2026-04-21 22:07:29 +08:00
import com.xuqm.push.repository.DeviceTokenRepository;
import com.xuqm.push.service.provider.PushProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
2026-04-21 22:07:29 +08:00
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class PushDispatcher {
private static final Logger log = LoggerFactory.getLogger(PushDispatcher.class);
private final DeviceTokenRepository tokenRepository;
private final DeviceLoginLogRepository logRepository;
private final TenantImConfigClient imConfigClient;
2026-04-21 22:07:29 +08:00
private final Map<String, PushProvider> providers;
public PushDispatcher(DeviceTokenRepository tokenRepository,
DeviceLoginLogRepository logRepository,
TenantImConfigClient imConfigClient,
List<PushProvider> providerList) {
2026-04-21 22:07:29 +08:00
this.tokenRepository = tokenRepository;
this.logRepository = logRepository;
this.imConfigClient = imConfigClient;
2026-04-21 22:07:29 +08:00
this.providers = providerList.stream()
.collect(Collectors.toMap(PushProvider::vendorName, p -> p));
}
public void pushToUser(String appId, String userId, String title, String body, String payload) {
List<DeviceTokenEntity> targets = selectTargets(appId, userId);
if (targets.isEmpty()) {
log.info("Skip push to {}@{}: no receive-enabled device", userId, appId);
return;
}
for (DeviceTokenEntity t : targets) {
2026-04-21 22:07:29 +08:00
PushProvider provider = providers.get(t.getVendor().name());
if (provider != null) {
boolean ok = provider.send(appId, t.getToken(), title, body, payload);
log.info("Push to {}@{} via {} deviceId={}: {}", userId, appId, t.getVendor(), t.getDeviceId(), ok ? "OK" : "FAIL");
2026-04-21 22:07:29 +08:00
}
}
}
public List<DeviceTokenEntity> selectedPushTargets(String appId, String userId) {
return selectTargets(appId, userId);
}
public void pushToUsers(String appId, List<String> userIds, String title, String body, String payload) {
if (userIds == null || userIds.isEmpty()) {
return;
}
for (String userId : userIds) {
pushToUser(appId, userId, title, body, payload);
}
}
private List<DeviceTokenEntity> selectTargets(String appId, String userId) {
List<DeviceTokenEntity> devices = tokenRepository
.findByAppIdAndUserIdAndReceivePushTrueOrderByLastLoginAtDescUpdatedAtDesc(appId, userId);
if (devices.isEmpty()) {
return List.of();
}
if (!imConfigClient.allowMultiDeviceLogin(appId)) {
return List.of(devices.get(0));
}
return devices.stream()
.collect(Collectors.toMap(
DeviceTokenEntity::getVendor,
device -> device,
(first, ignored) -> first,
java.util.LinkedHashMap::new))
.values()
.stream()
.toList();
}
public void registerToken(String appId,
String userId,
DeviceTokenEntity.Vendor vendor,
String token,
String platform,
String deviceId,
String brand,
String model,
String osVersion,
String appVersion) {
String resolvedDeviceId = normalizeDeviceId(deviceId, vendor, token);
Optional<DeviceTokenEntity> existing = tokenRepository.findByAppIdAndUserIdAndDeviceId(appId, userId, resolvedDeviceId);
if (existing.isEmpty() && (deviceId == null || deviceId.isBlank())) {
existing = tokenRepository.findByAppIdAndUserIdAndVendor(appId, userId, vendor);
}
LocalDateTime now = LocalDateTime.now();
2026-04-21 22:07:29 +08:00
DeviceTokenEntity entity = existing.orElseGet(() -> {
DeviceTokenEntity e = new DeviceTokenEntity();
e.setId(UUID.randomUUID().toString());
e.setAppId(appId);
e.setUserId(userId);
e.setVendor(vendor);
e.setCreatedAt(now);
2026-04-21 22:07:29 +08:00
return e;
});
entity.setVendor(vendor);
2026-04-21 22:07:29 +08:00
entity.setToken(token);
entity.setPlatform(blankToNull(platform));
entity.setDeviceId(resolvedDeviceId);
entity.setBrand(blankToNull(brand));
entity.setModel(blankToNull(model));
entity.setOsVersion(blankToNull(osVersion));
entity.setAppVersion(blankToNull(appVersion));
entity.setReceivePush(true);
entity.setLastLoginAt(now);
entity.setUpdatedAt(now);
DeviceTokenEntity saved = tokenRepository.save(entity);
recordLog(saved, DeviceLoginLogEntity.EventType.REGISTER);
2026-04-21 22:07:29 +08:00
}
public void setReceivePush(String appId, String userId, String deviceId, boolean enabled) {
List<DeviceTokenEntity> tokens = deviceId == null || deviceId.isBlank()
? tokenRepository.findByAppIdAndUserId(appId, userId)
: tokenRepository.findByAppIdAndUserIdAndDeviceId(appId, userId, deviceId).stream().toList();
for (DeviceTokenEntity token : tokens) {
token.setReceivePush(enabled);
token.setUpdatedAt(LocalDateTime.now());
recordLog(token, DeviceLoginLogEntity.EventType.RECEIVE_PUSH_UPDATE);
}
tokenRepository.saveAll(tokens);
}
public void unregisterToken(String appId, String userId, DeviceTokenEntity.Vendor vendor, String deviceId) {
if (deviceId != null && !deviceId.isBlank()) {
tokenRepository.findByAppIdAndUserIdAndDeviceId(appId, userId, deviceId)
.ifPresent(entity -> recordLog(entity, DeviceLoginLogEntity.EventType.UNREGISTER));
tokenRepository.deleteByAppIdAndUserIdAndDeviceId(appId, userId, deviceId);
return;
}
tokenRepository.findByAppIdAndUserIdAndVendor(appId, userId, vendor)
.ifPresent(entity -> recordLog(entity, DeviceLoginLogEntity.EventType.UNREGISTER));
tokenRepository.deleteByAppIdAndUserIdAndVendor(appId, userId, vendor);
}
private void recordLog(DeviceTokenEntity token, DeviceLoginLogEntity.EventType eventType) {
DeviceLoginLogEntity entity = new DeviceLoginLogEntity();
entity.setId(UUID.randomUUID().toString());
entity.setAppId(token.getAppId());
entity.setUserId(token.getUserId());
entity.setVendor(token.getVendor());
entity.setTokenHash(hashToken(token.getToken()));
entity.setTokenPreview(previewToken(token.getToken()));
entity.setPlatform(token.getPlatform());
entity.setDeviceId(token.getDeviceId());
entity.setBrand(token.getBrand());
entity.setModel(token.getModel());
entity.setOsVersion(token.getOsVersion());
entity.setAppVersion(token.getAppVersion());
entity.setReceivePush(token.isReceivePush());
entity.setEventType(eventType);
entity.setCreatedAt(LocalDateTime.now());
logRepository.save(entity);
}
private String normalizeDeviceId(String deviceId, DeviceTokenEntity.Vendor vendor, String token) {
if (deviceId != null && !deviceId.isBlank()) {
return deviceId.trim();
}
return vendor.name() + ":" + hashToken(token).substring(0, 24);
}
private String blankToNull(String value) {
return value == null || value.isBlank() ? null : value.trim();
}
private String hashToken(String token) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] bytes = digest.digest(token.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
public static String previewToken(String token) {
if (token == null || token.isBlank()) {
return "";
}
if (token.length() <= 16) {
return token;
}
return token.substring(0, 8) + "..." + token.substring(token.length() - 8);
}
2026-04-21 22:07:29 +08:00
}