feat(push): 添加推送服务功能支持
- 新增推送相关的类型定义,包括消息类型、聊天类型、推送配置等接口 - 实现 HarmonyOS 推送 SDK,集成 HarmonyOS NEXT Push Kit 服务 - 实现 iOS 推送 SDK,支持 APNS 推送注册和消息接收 - 添加服务器端 APNS 推送提供商,支持 JWT 认证和推送消息发送 - 添加服务器端 HarmonyOS 推送提供商基础框架 - 集成推送配置加载和路由功能,支持多渠道推送分类管理
这个提交包含在:
父节点
824f11c7ea
当前提交
a408b2b39a
@ -58,15 +58,16 @@ public class PushDispatcher {
|
|||||||
for (DeviceTokenEntity t : targets) {
|
for (DeviceTokenEntity t : targets) {
|
||||||
PushProvider provider = providers.get(t.getVendor().name());
|
PushProvider provider = providers.get(t.getVendor().name());
|
||||||
if (provider != null) {
|
if (provider != null) {
|
||||||
boolean ok = provider.send(appId, t.getToken(), title, body, payload, resolveOptions(appId, payload));
|
boolean ok = provider.send(appId, t.getToken(), title, body, payload, resolveOptions(appId, payload, t.getVendor()));
|
||||||
log.info("Push to {}@{} via {} deviceId={}: {}", userId, appId, t.getVendor(), t.getDeviceId(), ok ? "OK" : "FAIL");
|
log.info("Push to {}@{} via {} deviceId={}: {}", userId, appId, t.getVendor(), t.getDeviceId(), ok ? "OK" : "FAIL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PushSendOptions resolveOptions(String appId, String payload) {
|
private PushSendOptions resolveOptions(String appId, String payload, DeviceTokenEntity.Vendor vendor) {
|
||||||
String routeType = routeType(payload);
|
String routeType = routeType(payload);
|
||||||
return pushConfigClient.loadServiceConfig(appId, "ANDROID", "PUSH")
|
String platform = platformForVendor(vendor, payload);
|
||||||
|
return pushConfigClient.loadServiceConfig(appId, platform, "PUSH")
|
||||||
.map(config -> {
|
.map(config -> {
|
||||||
JsonNode route = config.path("routing").path(routeType);
|
JsonNode route = config.path("routing").path(routeType);
|
||||||
String channelKey = route.path("channel").asText("");
|
String channelKey = route.path("channel").asText("");
|
||||||
@ -80,6 +81,29 @@ public class PushDispatcher {
|
|||||||
.orElseGet(() -> new PushSendOptions(routeType, "", "", ""));
|
.orElseGet(() -> new PushSendOptions(routeType, "", "", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String platformForVendor(DeviceTokenEntity.Vendor vendor, String payload) {
|
||||||
|
if (payload == null || payload.isBlank()) {
|
||||||
|
return defaultPlatformForVendor(vendor);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonNode node = objectMapper.readTree(payload);
|
||||||
|
String platform = node.path("platform").asText("");
|
||||||
|
if (!platform.isBlank()) {
|
||||||
|
return platform.toUpperCase();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return defaultPlatformForVendor(vendor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultPlatformForVendor(DeviceTokenEntity.Vendor vendor) {
|
||||||
|
return switch (vendor) {
|
||||||
|
case APNS -> "IOS";
|
||||||
|
case HARMONY -> "HARMONY";
|
||||||
|
default -> "ANDROID";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private String routeType(String payload) {
|
private String routeType(String payload) {
|
||||||
if (payload == null || payload.isBlank()) {
|
if (payload == null || payload.isBlank()) {
|
||||||
return "IM_MESSAGE";
|
return "IM_MESSAGE";
|
||||||
|
|||||||
@ -64,6 +64,11 @@ public class ApnsPushProvider implements PushProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean send(String appId, String token, String title, String body, String payload) {
|
public boolean send(String appId, String token, String title, String body, String payload) {
|
||||||
|
return send(appId, token, title, body, payload, PushSendOptions.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean send(String appId, String token, String title, String body, String payload, PushSendOptions options) {
|
||||||
String teamId = resolveConfig(appId, "teamId", envTeamId);
|
String teamId = resolveConfig(appId, "teamId", envTeamId);
|
||||||
String keyId = resolveConfig(appId, "keyId", envKeyId);
|
String keyId = resolveConfig(appId, "keyId", envKeyId);
|
||||||
String bundleId = resolveConfig(appId, "bundleId", envBundleId);
|
String bundleId = resolveConfig(appId, "bundleId", envBundleId);
|
||||||
@ -75,10 +80,20 @@ public class ApnsPushProvider implements PushProvider {
|
|||||||
try {
|
try {
|
||||||
String authToken = getAuthToken(teamId, keyId, privateKeyPem);
|
String authToken = getAuthToken(teamId, keyId, privateKeyPem);
|
||||||
String url = (production ? productionPushUrl : sandboxPushUrl).replace("{token}", token);
|
String url = (production ? productionPushUrl : sandboxPushUrl).replace("{token}", token);
|
||||||
Map<String, Object> aps = Map.of(
|
Map<String, Object> aps = new java.util.LinkedHashMap<>();
|
||||||
"alert", Map.of("title", title, "body", body),
|
aps.put("alert", Map.of("title", title, "body", body));
|
||||||
"sound", "default"
|
aps.put("sound", "default");
|
||||||
);
|
if (options != null) {
|
||||||
|
if (options.category() != null && !options.category().isBlank()) {
|
||||||
|
aps.put("category", options.category());
|
||||||
|
}
|
||||||
|
if (options.routeType() != null && !options.routeType().isBlank()) {
|
||||||
|
aps.put("thread-id", options.routeType());
|
||||||
|
}
|
||||||
|
if ("HIGH".equalsIgnoreCase(options.priority())) {
|
||||||
|
aps.put("interruption-level", "time-sensitive");
|
||||||
|
}
|
||||||
|
}
|
||||||
Map<String, Object> message = new java.util.LinkedHashMap<>();
|
Map<String, Object> message = new java.util.LinkedHashMap<>();
|
||||||
message.put("aps", aps);
|
message.put("aps", aps);
|
||||||
if (payload != null) {
|
if (payload != null) {
|
||||||
@ -91,6 +106,7 @@ public class ApnsPushProvider implements PushProvider {
|
|||||||
.header("Authorization", "Bearer " + authToken)
|
.header("Authorization", "Bearer " + authToken)
|
||||||
.header("apns-topic", bundleId)
|
.header("apns-topic", bundleId)
|
||||||
.header("apns-push-type", "alert")
|
.header("apns-push-type", "alert")
|
||||||
|
.header("apns-priority", options != null && "LOW".equalsIgnoreCase(options.priority()) ? "5" : "10")
|
||||||
.header("apns-id", UUID.randomUUID().toString())
|
.header("apns-id", UUID.randomUUID().toString())
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import java.net.URI;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,6 +54,11 @@ public class HarmonyPushProvider implements PushProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean send(String appId, String token, String title, String body, String payload) {
|
public boolean send(String appId, String token, String title, String body, String payload) {
|
||||||
|
return send(appId, token, title, body, payload, PushSendOptions.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean send(String appId, String token, String title, String body, String payload, PushSendOptions options) {
|
||||||
String resolvedAppId = resolveConfig(appId, "appId", envAppId);
|
String resolvedAppId = resolveConfig(appId, "appId", envAppId);
|
||||||
String resolvedAppSecret = resolveConfig(appId, "appSecret", envAppSecret);
|
String resolvedAppSecret = resolveConfig(appId, "appSecret", envAppSecret);
|
||||||
if (resolvedAppId.isBlank() || resolvedAppSecret.isBlank()) {
|
if (resolvedAppId.isBlank() || resolvedAppSecret.isBlank()) {
|
||||||
@ -62,10 +68,21 @@ public class HarmonyPushProvider implements PushProvider {
|
|||||||
try {
|
try {
|
||||||
String accessToken = getAccessToken(resolvedAppId, resolvedAppSecret);
|
String accessToken = getAccessToken(resolvedAppId, resolvedAppSecret);
|
||||||
String url = pushUrl.replace("{appId}", resolvedAppId);
|
String url = pushUrl.replace("{appId}", resolvedAppId);
|
||||||
|
Map<String, Object> notification = new LinkedHashMap<>();
|
||||||
|
notification.put("title", title);
|
||||||
|
notification.put("body", body);
|
||||||
|
if (options != null) {
|
||||||
|
if (options.category() != null && !options.category().isBlank()) {
|
||||||
|
notification.put("category", options.category());
|
||||||
|
}
|
||||||
|
if (options.channelId() != null && !options.channelId().isBlank()) {
|
||||||
|
notification.put("channel_id", options.channelId());
|
||||||
|
}
|
||||||
|
}
|
||||||
Map<String, Object> message = Map.of(
|
Map<String, Object> message = Map.of(
|
||||||
"message", Map.of(
|
"message", Map.of(
|
||||||
"token", new String[]{token},
|
"token", new String[]{token},
|
||||||
"notification", Map.of("title", title, "body", body),
|
"notification", notification,
|
||||||
"data", payload != null ? payload : "{}"
|
"data", payload != null ? payload : "{}"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -126,6 +126,8 @@ public class FeatureServiceController {
|
|||||||
req == null ? null : req.honorAppIdValue(),
|
req == null ? null : req.honorAppIdValue(),
|
||||||
req == null ? null : req.honorClientIdValue(),
|
req == null ? null : req.honorClientIdValue(),
|
||||||
req == null ? null : req.honorClientSecretValue(),
|
req == null ? null : req.honorClientSecretValue(),
|
||||||
|
req == null ? null : req.harmonyAppIdValue(),
|
||||||
|
req == null ? null : req.harmonyAppSecretValue(),
|
||||||
req == null ? null : req.apnsTeamIdValue(),
|
req == null ? null : req.apnsTeamIdValue(),
|
||||||
req == null ? null : req.apnsKeyIdValue(),
|
req == null ? null : req.apnsKeyIdValue(),
|
||||||
req == null ? null : req.apnsBundleIdValue(),
|
req == null ? null : req.apnsBundleIdValue(),
|
||||||
@ -222,6 +224,8 @@ public class FeatureServiceController {
|
|||||||
String honorAppId,
|
String honorAppId,
|
||||||
String honorClientId,
|
String honorClientId,
|
||||||
String honorClientSecret,
|
String honorClientSecret,
|
||||||
|
String harmonyAppId,
|
||||||
|
String harmonyAppSecret,
|
||||||
String apnsTeamId,
|
String apnsTeamId,
|
||||||
String apnsKeyId,
|
String apnsKeyId,
|
||||||
String apnsBundleId,
|
String apnsBundleId,
|
||||||
@ -233,6 +237,7 @@ public class FeatureServiceController {
|
|||||||
PushVendorConfig oppo,
|
PushVendorConfig oppo,
|
||||||
PushVendorConfig vivo,
|
PushVendorConfig vivo,
|
||||||
PushVendorConfig honor,
|
PushVendorConfig honor,
|
||||||
|
PushVendorConfig harmony,
|
||||||
PushVendorConfig apns,
|
PushVendorConfig apns,
|
||||||
PushVendorConfig fcm,
|
PushVendorConfig fcm,
|
||||||
JsonNode channels,
|
JsonNode channels,
|
||||||
@ -252,6 +257,8 @@ public class FeatureServiceController {
|
|||||||
public String honorAppIdValue() { return firstText(honorAppId, honor == null ? null : honor.appId()); }
|
public String honorAppIdValue() { return firstText(honorAppId, honor == null ? null : honor.appId()); }
|
||||||
public String honorClientIdValue() { return firstText(honorClientId, honor == null ? null : honor.clientId()); }
|
public String honorClientIdValue() { return firstText(honorClientId, honor == null ? null : honor.clientId()); }
|
||||||
public String honorClientSecretValue() { return firstText(honorClientSecret, honor == null ? null : honor.clientSecret()); }
|
public String honorClientSecretValue() { return firstText(honorClientSecret, honor == null ? null : honor.clientSecret()); }
|
||||||
|
public String harmonyAppIdValue() { return firstText(harmonyAppId, harmony == null ? null : harmony.appId()); }
|
||||||
|
public String harmonyAppSecretValue() { return firstText(harmonyAppSecret, harmony == null ? null : harmony.appSecret()); }
|
||||||
public String apnsTeamIdValue() { return firstText(apnsTeamId, apns == null ? null : apns.teamId()); }
|
public String apnsTeamIdValue() { return firstText(apnsTeamId, apns == null ? null : apns.teamId()); }
|
||||||
public String apnsKeyIdValue() { return firstText(apnsKeyId, apns == null ? null : apns.keyId()); }
|
public String apnsKeyIdValue() { return firstText(apnsKeyId, apns == null ? null : apns.keyId()); }
|
||||||
public String apnsBundleIdValue() { return firstText(apnsBundleId, apns == null ? null : apns.bundleId()); }
|
public String apnsBundleIdValue() { return firstText(apnsBundleId, apns == null ? null : apns.bundleId()); }
|
||||||
|
|||||||
@ -491,6 +491,8 @@ public class FeatureServiceManager {
|
|||||||
String honorAppId,
|
String honorAppId,
|
||||||
String honorClientId,
|
String honorClientId,
|
||||||
String honorClientSecret,
|
String honorClientSecret,
|
||||||
|
String harmonyAppId,
|
||||||
|
String harmonyAppSecret,
|
||||||
String apnsTeamId,
|
String apnsTeamId,
|
||||||
String apnsKeyId,
|
String apnsKeyId,
|
||||||
String apnsBundleId,
|
String apnsBundleId,
|
||||||
@ -505,6 +507,7 @@ public class FeatureServiceManager {
|
|||||||
ensureObjectNode(node, "oppo");
|
ensureObjectNode(node, "oppo");
|
||||||
ensureObjectNode(node, "vivo");
|
ensureObjectNode(node, "vivo");
|
||||||
ensureObjectNode(node, "honor");
|
ensureObjectNode(node, "honor");
|
||||||
|
ensureObjectNode(node, "harmony");
|
||||||
ensureObjectNode(node, "apns");
|
ensureObjectNode(node, "apns");
|
||||||
ensureObjectNode(node, "fcm");
|
ensureObjectNode(node, "fcm");
|
||||||
|
|
||||||
@ -527,6 +530,9 @@ public class FeatureServiceManager {
|
|||||||
putText(node.with("honor"), "clientId", honorClientId);
|
putText(node.with("honor"), "clientId", honorClientId);
|
||||||
putText(node.with("honor"), "clientSecret", honorClientSecret);
|
putText(node.with("honor"), "clientSecret", honorClientSecret);
|
||||||
|
|
||||||
|
putText(node.with("harmony"), "appId", harmonyAppId);
|
||||||
|
putText(node.with("harmony"), "appSecret", harmonyAppSecret);
|
||||||
|
|
||||||
putText(node.with("apns"), "teamId", apnsTeamId);
|
putText(node.with("apns"), "teamId", apnsTeamId);
|
||||||
putText(node.with("apns"), "keyId", apnsKeyId);
|
putText(node.with("apns"), "keyId", apnsKeyId);
|
||||||
putText(node.with("apns"), "bundleId", apnsBundleId);
|
putText(node.with("apns"), "bundleId", apnsBundleId);
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户