diff --git a/push-service/src/main/java/com/xuqm/push/controller/PushManagementController.java b/push-service/src/main/java/com/xuqm/push/controller/PushManagementController.java index c1cfa50..b40407d 100644 --- a/push-service/src/main/java/com/xuqm/push/controller/PushManagementController.java +++ b/push-service/src/main/java/com/xuqm/push/controller/PushManagementController.java @@ -156,11 +156,14 @@ public class PushManagementController { PushDiagnosticsService.TestPushResult result = diagnosticsService.sendTestOfflineMessage( request.appKey(), request.userId(), + request.deviceId(), request.title(), request.body(), request.payload()); - opLogService.record(request.appKey(), "TEST_PUSH", "PUSH", request.userId(), - "向 " + request.userId() + " 发送测试推送", null); + String target = request.deviceId() != null && !request.deviceId().isBlank() + ? "向 " + request.userId() + " 的指定设备发送测试推送" + : "向 " + request.userId() + " 发送测试推送"; + opLogService.record(request.appKey(), "TEST_PUSH", "PUSH", request.userId(), target, null); return ResponseEntity.ok(ApiResponse.success(result)); } @@ -179,5 +182,6 @@ public class PushManagementController { public record ImportUserRequest(String appKey, String userId, String nickname, String avatar, String gender, String status) {} public record TestOfflineRequest(String appKey, String userId, + String deviceId, String title, String body, String payload) {} } diff --git a/push-service/src/main/java/com/xuqm/push/service/PushDiagnosticsService.java b/push-service/src/main/java/com/xuqm/push/service/PushDiagnosticsService.java index 3a4d61a..57635ad 100644 --- a/push-service/src/main/java/com/xuqm/push/service/PushDiagnosticsService.java +++ b/push-service/src/main/java/com/xuqm/push/service/PushDiagnosticsService.java @@ -112,7 +112,19 @@ public class PushDiagnosticsService { devices.stream().map(DeviceInfo::from).toList()); } - public TestPushResult sendTestOfflineMessage(String appKey, String userId, String title, String body, String payload) { + public TestPushResult sendTestOfflineMessage(String appKey, String userId, String deviceId, String title, String body, String payload) { + if (deviceId != null && !deviceId.isBlank()) { + Optional device = tokenRepository.findById(deviceId) + .filter(e -> appKey.equals(e.getAppKey()) && userId.equals(e.getUserId())); + if (device.isEmpty()) { + return new TestPushResult(appKey, userId, false, 0, List.of()); + } + // Device found — attempt regardless of receivePush flag (this is a manual test) + DeviceInfo target = DeviceInfo.from(device.get()); + boolean ok = pushDispatcher.pushToSpecificDevice(appKey, userId, deviceId, title, body, payload); + // Always include the target so frontend can distinguish "vendor rejected" from "device not found" + return new TestPushResult(appKey, userId, ok, 1, List.of(target)); + } List targets = pushDispatcher.selectedPushTargets(appKey, userId) .stream() .map(DeviceInfo::from) diff --git a/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java b/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java index 15e6d8a..50463d4 100644 --- a/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java +++ b/push-service/src/main/java/com/xuqm/push/service/PushDispatcher.java @@ -276,6 +276,24 @@ public class PushDispatcher { recordLog(saved, DeviceLoginLogEntity.EventType.REGISTER); } + public boolean pushToSpecificDevice(String appKey, String userId, String deviceEntityId, String title, String body, String payload) { + Optional device = tokenRepository.findById(deviceEntityId) + .filter(e -> appKey.equals(e.getAppKey()) && userId.equals(e.getUserId())); + if (device.isEmpty()) { + log.warn("pushToSpecificDevice: device not found id={} userId={} appKey={}", deviceEntityId, userId, appKey); + return false; + } + DeviceTokenEntity t = device.get(); + PushProvider provider = providers.get(t.getVendor().name()); + if (provider == null) { + log.warn("pushToSpecificDevice: no provider for vendor={}", t.getVendor()); + return false; + } + boolean ok = provider.send(appKey, t.getToken(), title, body, payload, resolveOptions(appKey, payload, t.getVendor())); + log.info("Test push to {}@{} via {} deviceId={}: {}", userId, appKey, t.getVendor(), t.getDeviceId(), ok ? "OK" : "FAIL"); + return ok; + } + public void setReceivePush(String appKey, String userId, String deviceId, boolean enabled) { List tokens = deviceId == null || deviceId.isBlank() ? tokenRepository.findByAppKeyAndUserId(appKey, userId) diff --git a/push-service/src/main/java/com/xuqm/push/service/provider/VivoPushProvider.java b/push-service/src/main/java/com/xuqm/push/service/provider/VivoPushProvider.java index f4aae6f..eae1c8b 100644 --- a/push-service/src/main/java/com/xuqm/push/service/provider/VivoPushProvider.java +++ b/push-service/src/main/java/com/xuqm/push/service/provider/VivoPushProvider.java @@ -83,8 +83,13 @@ public class VivoPushProvider implements PushProvider { HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { JsonNode json = objectMapper.readTree(response.body()); - return json.path("result").asInt(-1) == 0; + int result = json.path("result").asInt(-1); + if (result != 0) { + log.warn("Vivo push rejected result={} desc={} body={}", result, json.path("desc").asText(""), response.body()); + } + return result == 0; } + log.warn("Vivo push HTTP error status={} body={}", response.statusCode(), response.body()); return false; } catch (Exception e) { log.error("Vivo push failed: {}", e.getMessage()); diff --git a/push-service/src/main/java/com/xuqm/push/service/provider/XiaomiPushProvider.java b/push-service/src/main/java/com/xuqm/push/service/provider/XiaomiPushProvider.java index d4a4882..39aa9c4 100644 --- a/push-service/src/main/java/com/xuqm/push/service/provider/XiaomiPushProvider.java +++ b/push-service/src/main/java/com/xuqm/push/service/provider/XiaomiPushProvider.java @@ -48,9 +48,10 @@ public class XiaomiPushProvider implements PushProvider { appSecret = resolveVendorConfig(appKey, "appKey", envAppSecret); } if (appSecret.isBlank()) { - log.warn("Xiaomi push not configured"); + log.warn("Xiaomi push not configured: appSecret is blank for appKey={}", appKey); return false; } + log.debug("Xiaomi push using appSecret={}...", appSecret.length() > 8 ? appSecret.substring(0, 8) : "[short]"); Map platformInfo = configClient.fetchPlatformInfo(appKey); String packageName = platformInfo.getOrDefault("androidPackageName", ""); @@ -95,8 +96,17 @@ public class XiaomiPushProvider implements PushProvider { JsonNode json = mapper.readTree(response.body()); String result = json.path("result").asText(""); if (!"ok".equalsIgnoreCase(result)) { - log.warn("Xiaomi push rejected result={} description={} body={}", - result, json.path("description").asText(""), response.body()); + int code = json.path("code").asInt(0); + String reason = json.path("reason").asText(json.path("description").asText("")); + if (code == 21301) { + log.error("Xiaomi push auth failed (21301): AppSecret is invalid for appKey={}. " + + "Check vendor config → 小米 → AppSecret. " + + "Used appSecret prefix={}...", + appKey, + appSecret.length() > 8 ? appSecret.substring(0, 8) : "[short]"); + } else { + log.warn("Xiaomi push rejected code={} reason={} body={}", code, reason, response.body()); + } return false; } } catch (Exception e) {