feat(push): 测试推送支持指定设备;改进小米/VIVO 错误日志
- TestOfflineRequest 新增 deviceId 字段,可直接推送到指定设备 - PushDispatcher 新增 pushToSpecificDevice(绕过 receivePush 过滤,适合测试) - sendTestOfflineMessage:指定设备时 targetCount=1(设备已找到),sent=false 表示厂商拒绝 - XiaomiPushProvider:21301 鉴权失败时打印 appKey + 前缀供排查 - VivoPushProvider:发送失败时打印 VIVO 返回的错误码和描述 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
c1a96af765
当前提交
68f6145c71
@ -156,11 +156,14 @@ public class PushManagementController {
|
|||||||
PushDiagnosticsService.TestPushResult result = diagnosticsService.sendTestOfflineMessage(
|
PushDiagnosticsService.TestPushResult result = diagnosticsService.sendTestOfflineMessage(
|
||||||
request.appKey(),
|
request.appKey(),
|
||||||
request.userId(),
|
request.userId(),
|
||||||
|
request.deviceId(),
|
||||||
request.title(),
|
request.title(),
|
||||||
request.body(),
|
request.body(),
|
||||||
request.payload());
|
request.payload());
|
||||||
opLogService.record(request.appKey(), "TEST_PUSH", "PUSH", request.userId(),
|
String target = request.deviceId() != null && !request.deviceId().isBlank()
|
||||||
"向 " + request.userId() + " 发送测试推送", null);
|
? "向 " + request.userId() + " 的指定设备发送测试推送"
|
||||||
|
: "向 " + request.userId() + " 发送测试推送";
|
||||||
|
opLogService.record(request.appKey(), "TEST_PUSH", "PUSH", request.userId(), target, null);
|
||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
return ResponseEntity.ok(ApiResponse.success(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,5 +182,6 @@ public class PushManagementController {
|
|||||||
public record ImportUserRequest(String appKey, String userId, String nickname,
|
public record ImportUserRequest(String appKey, String userId, String nickname,
|
||||||
String avatar, String gender, String status) {}
|
String avatar, String gender, String status) {}
|
||||||
public record TestOfflineRequest(String appKey, String userId,
|
public record TestOfflineRequest(String appKey, String userId,
|
||||||
|
String deviceId,
|
||||||
String title, String body, String payload) {}
|
String title, String body, String payload) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,19 @@ public class PushDiagnosticsService {
|
|||||||
devices.stream().map(DeviceInfo::from).toList());
|
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<DeviceTokenEntity> 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<DeviceInfo> targets = pushDispatcher.selectedPushTargets(appKey, userId)
|
List<DeviceInfo> targets = pushDispatcher.selectedPushTargets(appKey, userId)
|
||||||
.stream()
|
.stream()
|
||||||
.map(DeviceInfo::from)
|
.map(DeviceInfo::from)
|
||||||
|
|||||||
@ -276,6 +276,24 @@ public class PushDispatcher {
|
|||||||
recordLog(saved, DeviceLoginLogEntity.EventType.REGISTER);
|
recordLog(saved, DeviceLoginLogEntity.EventType.REGISTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean pushToSpecificDevice(String appKey, String userId, String deviceEntityId, String title, String body, String payload) {
|
||||||
|
Optional<DeviceTokenEntity> 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) {
|
public void setReceivePush(String appKey, String userId, String deviceId, boolean enabled) {
|
||||||
List<DeviceTokenEntity> tokens = deviceId == null || deviceId.isBlank()
|
List<DeviceTokenEntity> tokens = deviceId == null || deviceId.isBlank()
|
||||||
? tokenRepository.findByAppKeyAndUserId(appKey, userId)
|
? tokenRepository.findByAppKeyAndUserId(appKey, userId)
|
||||||
|
|||||||
@ -83,8 +83,13 @@ public class VivoPushProvider implements PushProvider {
|
|||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
JsonNode json = objectMapper.readTree(response.body());
|
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;
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Vivo push failed: {}", e.getMessage());
|
log.error("Vivo push failed: {}", e.getMessage());
|
||||||
|
|||||||
@ -48,9 +48,10 @@ public class XiaomiPushProvider implements PushProvider {
|
|||||||
appSecret = resolveVendorConfig(appKey, "appKey", envAppSecret);
|
appSecret = resolveVendorConfig(appKey, "appKey", envAppSecret);
|
||||||
}
|
}
|
||||||
if (appSecret.isBlank()) {
|
if (appSecret.isBlank()) {
|
||||||
log.warn("Xiaomi push not configured");
|
log.warn("Xiaomi push not configured: appSecret is blank for appKey={}", appKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
log.debug("Xiaomi push using appSecret={}...", appSecret.length() > 8 ? appSecret.substring(0, 8) : "[short]");
|
||||||
|
|
||||||
Map<String, String> platformInfo = configClient.fetchPlatformInfo(appKey);
|
Map<String, String> platformInfo = configClient.fetchPlatformInfo(appKey);
|
||||||
String packageName = platformInfo.getOrDefault("androidPackageName", "");
|
String packageName = platformInfo.getOrDefault("androidPackageName", "");
|
||||||
@ -95,8 +96,17 @@ public class XiaomiPushProvider implements PushProvider {
|
|||||||
JsonNode json = mapper.readTree(response.body());
|
JsonNode json = mapper.readTree(response.body());
|
||||||
String result = json.path("result").asText("");
|
String result = json.path("result").asText("");
|
||||||
if (!"ok".equalsIgnoreCase(result)) {
|
if (!"ok".equalsIgnoreCase(result)) {
|
||||||
log.warn("Xiaomi push rejected result={} description={} body={}",
|
int code = json.path("code").asInt(0);
|
||||||
result, json.path("description").asText(""), response.body());
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户