feat(push): 添加推送服务功能支持

- 新增推送相关的类型定义,包括消息类型、聊天类型、推送配置等接口
- 实现 HarmonyOS 推送 SDK,集成 HarmonyOS NEXT Push Kit 服务
- 实现 iOS 推送 SDK,支持 APNS 推送注册和消息接收
- 添加服务器端 APNS 推送提供商,支持 JWT 认证和推送消息发送
- 添加服务器端 HarmonyOS 推送提供商基础框架
- 集成推送配置加载和路由功能,支持多渠道推送分类管理
这个提交包含在:
XuqmGroup 2026-05-05 22:26:32 +08:00
父节点 824f11c7ea
当前提交 a408b2b39a
共有 5 个文件被更改,包括 78 次插入8 次删除

查看文件

@ -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);