|
|
@@ -23,8 +23,13 @@ import java.net.URLEncoder;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.security.MessageDigest;
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
|
|
|
/**
|
|
|
* 各厂商推送 HTTP 客户端,凭证从配置表 credential_json 读取。
|
|
|
@@ -38,10 +43,191 @@ public class CommonPushVendorHttpClient {
|
|
|
/** OPPO call_back_parameter 最大 100 字符 */
|
|
|
private static final int OPPO_CALLBACK_PARAM_MAX_LEN = 100;
|
|
|
|
|
|
+ private static final int HUAWEI_BATCH_TOKEN_LIMIT = 1000;
|
|
|
+ private static final String HUAWEI_PUSH_BASE_URL = "https://push-api.cloud.huawei.com";
|
|
|
+ private static final String HONOR_PUSH_BASE_URL = "https://push-api.cloud.honor.com";
|
|
|
+ private static final String HONOR_AUTH_URL =
|
|
|
+ "https://auth.honor.com/auth/realms/developer/protocol/openid-connect/token";
|
|
|
+
|
|
|
private final CommonPushProperties commonPushProperties;
|
|
|
+ private final ApnsPushClient apnsPushClient;
|
|
|
|
|
|
private static final RestTemplate restTemplate = new RestTemplate();
|
|
|
|
|
|
+ /**
|
|
|
+ * OPPO 全量广播推送(target_type=1),用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public boolean sendOppoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainOppoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String messageId = saveOppoMessageContent(authToken, task, null);
|
|
|
+ if (StringUtils.isBlank(messageId)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return broadcastOppoMessage(authToken, messageId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OPPO 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小米全量广播推送,用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public boolean sendXiaomiBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isBlank(appSecret)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ MultiValueMap<String, String> form = buildXiaomiMessageForm(credential, task, null);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "key=" + appSecret);
|
|
|
+ String resp = postForm("https://api.xmpush.xiaomi.com/v3/message/all", form, headers);
|
|
|
+ return isXiaomiSendSuccess(resp, task.getTaskNo(), "全量广播");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("小米广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * vivo 全量广播推送,用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public boolean sendVivoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainVivoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject sendBody = buildVivoNotificationBody(credential, task, null);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("authToken", authToken);
|
|
|
+ String sendResp = postJson("https://api-push.vivo.com.cn/message/all", sendBody.toJSONString(), headers);
|
|
|
+ return isVivoSendSuccess(sendResp, task.getTaskNo(), "全量广播");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("vivo 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 华为全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
+ */
|
|
|
+ public boolean sendHuaweiBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ List<String> deviceIds = extractDeviceIds(targets);
|
|
|
+ if (deviceIds.isEmpty()) {
|
|
|
+ log.warn("华为全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int successBatches = 0;
|
|
|
+ int totalBatches = 0;
|
|
|
+ for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
+ totalBatches++;
|
|
|
+ if (sendHuaweiLike(credential, task, batch)) {
|
|
|
+ successBatches++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("华为全量批量推送完成, taskNo={}, devices={}, batches={}/{}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), successBatches, totalBatches);
|
|
|
+ return successBatches > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("华为全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 荣耀全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
+ */
|
|
|
+ public boolean sendHonorBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ List<String> deviceIds = extractDeviceIds(targets);
|
|
|
+ if (deviceIds.isEmpty()) {
|
|
|
+ log.warn("荣耀全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int successBatches = 0;
|
|
|
+ int totalBatches = 0;
|
|
|
+ for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
+ totalBatches++;
|
|
|
+ if (sendHonorLike(credential, task, batch)) {
|
|
|
+ successBatches++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("荣耀全量批量推送完成, taskNo={}, devices={}, batches={}/{}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), successBatches, totalBatches);
|
|
|
+ return successBatches > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("荣耀全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * APNs 全量推送:按 iOS deviceToken 逐条下发(Apple 无 /all 接口)。
|
|
|
+ */
|
|
|
+ public boolean sendApnsBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null || !apnsPushClient.validateCredential(credential)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ List<CommonPushTargetDto> apnsTargets = ApnsPushClient.filterApnsTargets(targets, targets);
|
|
|
+ if (apnsTargets.isEmpty()) {
|
|
|
+ log.warn("APNs 全量推送无 iOS 设备 token, taskNo={}", task.getTaskNo());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return apnsPushClient.sendBroadcast(credential, task, apnsTargets);
|
|
|
+ }
|
|
|
+
|
|
|
public boolean send(CommonPushChannelConfig config, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
if (config == null || task == null || target == null || StringUtils.isBlank(target.getDeviceId())) {
|
|
|
return false;
|
|
|
@@ -80,11 +266,27 @@ public class CommonPushVendorHttpClient {
|
|
|
|
|
|
private boolean sendVivo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
String deviceId = target.getDeviceId();
|
|
|
+ if (StringUtils.isBlank(deviceId)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String authToken = obtainVivoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject sendBody = buildVivoNotificationBody(credential, task, target);
|
|
|
+ sendBody.put("regId", deviceId);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("authToken", authToken);
|
|
|
+ String sendResp = postJson("https://api-push.vivo.com.cn/message/send", sendBody.toJSONString(), headers);
|
|
|
+ return isVivoSendSuccess(sendResp, task.getTaskNo(), "单推");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainVivoAuthToken(JSONObject credential) {
|
|
|
String appId = credential.getString("appId");
|
|
|
String appKey = credential.getString("appKey");
|
|
|
String appSecret = credential.getString("appSecret");
|
|
|
if (StringUtils.isAnyBlank(appId, appKey, appSecret)) {
|
|
|
- return false;
|
|
|
+ return null;
|
|
|
}
|
|
|
long timestamp = System.currentTimeMillis();
|
|
|
String sign = md5Hex(appId + appKey + timestamp + appSecret);
|
|
|
@@ -96,99 +298,188 @@ public class CommonPushVendorHttpClient {
|
|
|
String authResp = postJson("https://api-push.vivo.com.cn/message/auth", authBody.toJSONString(), null);
|
|
|
JSONObject authJson = JSONObject.parseObject(authResp);
|
|
|
if (authJson == null || authJson.getIntValue("result") != 0) {
|
|
|
- return false;
|
|
|
+ log.warn("vivo 鉴权失败: {}", authResp);
|
|
|
+ return null;
|
|
|
}
|
|
|
- String authToken = authJson.getString("authToken");
|
|
|
+ return authJson.getString("authToken");
|
|
|
+ }
|
|
|
|
|
|
+ private JSONObject buildVivoNotificationBody(JSONObject credential, CommonPushTask task,
|
|
|
+ CommonPushTargetDto target) {
|
|
|
JSONObject sendBody = new JSONObject();
|
|
|
- sendBody.put("appId", appId);
|
|
|
- sendBody.put("regId", deviceId);
|
|
|
+ sendBody.put("appId", credential.getString("appId"));
|
|
|
sendBody.put("notifyType", 1);
|
|
|
sendBody.put("title", task.getTitle());
|
|
|
sendBody.put("content", task.getContent());
|
|
|
- sendBody.put("skipType", 1);
|
|
|
if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ sendBody.put("skipType", 2);
|
|
|
sendBody.put("skipContent", task.getJumpUrl());
|
|
|
+ } else {
|
|
|
+ sendBody.put("skipType", 1);
|
|
|
}
|
|
|
- Map<String, String> headers = new HashMap<>();
|
|
|
- headers.put("authToken", authToken);
|
|
|
- String sendResp = postJson("https://api-push.vivo.com.cn/message/send", sendBody.toJSONString(), headers);
|
|
|
- JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
- return sendJson != null && sendJson.getIntValue("result") == 0;
|
|
|
+ sendBody.put("requestId", buildVivoRequestId(task));
|
|
|
+ JSONObject extra = new JSONObject();
|
|
|
+ extra.put("callback", resolveCallbackUrl());
|
|
|
+ extra.put("callback.param", buildVivoCallbackParameter(task, target));
|
|
|
+ sendBody.put("extra", extra);
|
|
|
+ return sendBody;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildVivoRequestId(CommonPushTask task) {
|
|
|
+ String base = StringUtils.defaultIfBlank(task.getTaskNo(), "TASK");
|
|
|
+ String requestId = base + "_" + System.currentTimeMillis();
|
|
|
+ return requestId.length() > 64 ? requestId.substring(0, 64) : requestId;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildVivoCallbackParameter(CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ JSONObject param = new JSONObject(true);
|
|
|
+ if (task.getId() != null) {
|
|
|
+ param.put("p", task.getId());
|
|
|
+ } else if (StringUtils.isNotBlank(task.getTaskNo())) {
|
|
|
+ param.put("t", task.getTaskNo().trim());
|
|
|
+ }
|
|
|
+ if (target != null && target.getUserId() != null) {
|
|
|
+ param.put("u", target.getUserId());
|
|
|
+ }
|
|
|
+ param.put("c", "vivo");
|
|
|
+ return param.toJSONString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isVivoSendSuccess(String resp, String taskNo, String scene) {
|
|
|
+ log.info("vivo{}响应: {}", scene, resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json != null && json.getIntValue("result") == 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (json != null) {
|
|
|
+ log.error("vivo{}失败, taskNo={}, result={}, desc={}",
|
|
|
+ scene, taskNo, json.getIntValue("result"), json.getString("desc"));
|
|
|
+ }
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
private boolean sendXiaomi(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
String deviceId = target.getDeviceId();
|
|
|
String appSecret = credential.getString("appSecret");
|
|
|
- if (StringUtils.isBlank(appSecret)) {
|
|
|
+ if (StringUtils.isAnyBlank(appSecret, deviceId)) {
|
|
|
return false;
|
|
|
}
|
|
|
- MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ MultiValueMap<String, String> form = buildXiaomiMessageForm(credential, task, target);
|
|
|
form.add("registration_id", deviceId);
|
|
|
- form.add("title", task.getTitle());
|
|
|
- form.add("description", task.getContent());
|
|
|
- form.add("payload", buildPayload(task));
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
|
headers.put("Authorization", "key=" + appSecret);
|
|
|
String resp = postForm("https://api.xmpush.xiaomi.com/v3/message/regid", form, headers);
|
|
|
+ return isXiaomiSendSuccess(resp, task.getTaskNo(), "单推");
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean validateXiaomiCredential(JSONObject credential, String taskNo) {
|
|
|
+ String channelId = resolveXiaomiChannelId(credential);
|
|
|
+ if (StringUtils.isBlank(channelId)) {
|
|
|
+ log.error("小米推送缺少 channelId, taskNo={}。小米运营平台已创建 channel 后,还需将 channel_id 写入本系统 common_push_channel_config.credentialJson.channelId",
|
|
|
+ taskNo);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(credential.getString("packageName"))) {
|
|
|
+ log.warn("小米推送未配置 packageName, taskNo={}, channelId={},请确认与小米应用信息中的包名一致",
|
|
|
+ taskNo, channelId);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveXiaomiChannelId(JSONObject credential) {
|
|
|
+ if (credential == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return StringUtils.defaultIfBlank(
|
|
|
+ StringUtils.trimToNull(credential.getString("channelId")),
|
|
|
+ StringUtils.trimToNull(credential.getString("channel_id")));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isXiaomiSendSuccess(String resp, String taskNo, String scene) {
|
|
|
+ log.info("小米{}响应: {}", scene, resp);
|
|
|
JSONObject json = JSONObject.parseObject(resp);
|
|
|
- return json != null && "ok".equalsIgnoreCase(json.getString("result"));
|
|
|
+ if (json != null && "ok".equalsIgnoreCase(json.getString("result"))) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (json != null) {
|
|
|
+ log.error("小米{}失败, taskNo={}, code={}, reason={}, description={}",
|
|
|
+ scene, taskNo, json.getString("code"), json.getString("reason"), json.getString("description"));
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private MultiValueMap<String, String> buildXiaomiMessageForm(JSONObject credential, CommonPushTask task,
|
|
|
+ CommonPushTargetDto target) {
|
|
|
+ MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ form.add("title", task.getTitle());
|
|
|
+ form.add("description", task.getContent());
|
|
|
+ form.add("payload", buildPayload(task));
|
|
|
+ String packageName = credential.getString("packageName");
|
|
|
+ if (StringUtils.isNotBlank(packageName)) {
|
|
|
+ form.add("restricted_package_name", packageName.trim());
|
|
|
+ } else {
|
|
|
+ log.warn("小米推送未配置 packageName,多包名应用可能返回 22022 或 27001");
|
|
|
+ }
|
|
|
+ appendXiaomiExtraParams(form, credential, task, target);
|
|
|
+ return form;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小米 HTTP API 要求以 extra.xxx 形式传参(非 extra JSON 字符串)。
|
|
|
+ */
|
|
|
+ private void appendXiaomiExtraParams(MultiValueMap<String, String> form, JSONObject credential,
|
|
|
+ CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String channelId = resolveXiaomiChannelId(credential);
|
|
|
+ if (StringUtils.isNotBlank(channelId)) {
|
|
|
+ form.add("extra.channel_id", channelId);
|
|
|
+ }
|
|
|
+ form.add("extra.callback", resolveCallbackUrl());
|
|
|
+ form.add("extra.callback.param", buildXiaomiCallbackParameter(task, target));
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ form.add("extra.notify_effect", "3");
|
|
|
+ form.add("extra.web_uri", task.getJumpUrl());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildXiaomiCallbackParameter(CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ JSONObject param = new JSONObject(true);
|
|
|
+ if (task.getId() != null) {
|
|
|
+ param.put("p", task.getId());
|
|
|
+ } else if (StringUtils.isNotBlank(task.getTaskNo())) {
|
|
|
+ param.put("t", task.getTaskNo().trim());
|
|
|
+ }
|
|
|
+ if (target != null && target.getUserId() != null) {
|
|
|
+ param.put("u", target.getUserId());
|
|
|
+ }
|
|
|
+ param.put("c", "xiaomi");
|
|
|
+ return param.toJSONString();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* OPPO 厂商直连:使用 RegistrationID(非 UniPush CID),并设置送达回执回调。
|
|
|
*/
|
|
|
- private boolean sendOppo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) throws NoSuchAlgorithmException {
|
|
|
+ private boolean sendOppo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
String deviceId = target.getDeviceId();
|
|
|
- String appKey = credential.getString("appKey");
|
|
|
- String masterSecret = credential.getString("appSecret");
|
|
|
- if (StringUtils.isAnyBlank(appKey, masterSecret)) {
|
|
|
+ String authToken = obtainOppoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
return false;
|
|
|
}
|
|
|
- long timestamp = System.currentTimeMillis();
|
|
|
-// String sign = md5Hex(appKey + timestamp + masterSecret);
|
|
|
- String sign = cn.hutool.crypto.SecureUtil.sha256(appKey + timestamp + masterSecret);
|
|
|
- // 正确的签名生成
|
|
|
-// String signStr = appKey + timestamp + masterSecret;
|
|
|
-// MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
|
-// byte[] hash = digest.digest(signStr.getBytes(StandardCharsets.UTF_8));
|
|
|
-// // 转小写十六进制字符串
|
|
|
-// String sign = Hex.encodeHexString(hash);
|
|
|
- MultiValueMap<String, String> authForm = new LinkedMultiValueMap<>();
|
|
|
- authForm.add("app_key", appKey);
|
|
|
- authForm.add("timestamp", String.valueOf(timestamp));
|
|
|
- authForm.add("sign", sign);
|
|
|
- String authResp = postForm("https://api.push.oppomobile.com/server/v1/auth", authForm, null);
|
|
|
- JSONObject authJson = JSONObject.parseObject(authResp);
|
|
|
- if (authJson == null || authJson.getJSONObject("data") == null) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- String authToken = authJson.getJSONObject("data").getString("auth_token");
|
|
|
|
|
|
- JSONObject notification = new JSONObject();
|
|
|
- notification.put("title", task.getTitle());
|
|
|
- notification.put("content", task.getContent());
|
|
|
- notification.put("call_back_url", resolveCallbackUrl());
|
|
|
- notification.put("call_back_parameter", buildCallbackParameter(task, target, "oppo"));
|
|
|
- if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
- notification.put("click_action_url", task.getJumpUrl());
|
|
|
- }
|
|
|
-// 2. 构造最外层message完整JSON(官方要求顶层结构)
|
|
|
+ JSONObject notification = buildOppoNotification(task, target);
|
|
|
JSONObject messageRoot = new JSONObject();
|
|
|
-// messageRoot.put("auth_token", authToken);
|
|
|
- messageRoot.put("target_type", 2); // 固定2=registration_id单设备推送
|
|
|
+ messageRoot.put("target_type", 2);
|
|
|
messageRoot.put("target_value", deviceId);
|
|
|
messageRoot.put("notification", notification);
|
|
|
|
|
|
- // 3. 表单只放一个key:message,值为完整JSON字符串
|
|
|
MultiValueMap<String, String> sendForm = new LinkedMultiValueMap<>();
|
|
|
- sendForm.add("auth_token", authToken); // 鉴权令牌放在表单顶层
|
|
|
+ sendForm.add("auth_token", authToken);
|
|
|
sendForm.add("message", messageRoot.toJSONString());
|
|
|
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
-// // 显式指定UTF-8编码,避免中文乱码导致JSON解析失败
|
|
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
|
|
-//// headers.setContentType(MediaType.parseMediaType("application/x-www-form-urlencoded;charset=UTF-8"));
|
|
|
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(sendForm, headers);
|
|
|
try {
|
|
|
ResponseEntity<String> response = restTemplate.postForEntity(
|
|
|
@@ -197,17 +488,82 @@ public class CommonPushVendorHttpClient {
|
|
|
String.class
|
|
|
);
|
|
|
String body = response.getBody();
|
|
|
- log.info("OPPO 推送响应: {}", body);
|
|
|
+ log.info("OPPO 单推响应: {}", body);
|
|
|
JSONObject resp = JSONObject.parseObject(body);
|
|
|
return resp != null && resp.getIntValue("code") == 0;
|
|
|
} catch (Exception e) {
|
|
|
- log.error("OPPO 推送请求异常: {}", e.getMessage(), e);
|
|
|
+ log.error("OPPO 单推请求异常: {}", e.getMessage(), e);
|
|
|
return false;
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainOppoAuthToken(JSONObject credential) {
|
|
|
+ String appKey = credential.getString("appKey");
|
|
|
+ String masterSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appKey, masterSecret)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ long timestamp = System.currentTimeMillis();
|
|
|
+ String sign = cn.hutool.crypto.SecureUtil.sha256(appKey + timestamp + masterSecret);
|
|
|
+ MultiValueMap<String, String> authForm = new LinkedMultiValueMap<>();
|
|
|
+ authForm.add("app_key", appKey);
|
|
|
+ authForm.add("timestamp", String.valueOf(timestamp));
|
|
|
+ authForm.add("sign", sign);
|
|
|
+ String authResp = postForm("https://api.push.oppomobile.com/server/v1/auth", authForm, null);
|
|
|
+ JSONObject authJson = JSONObject.parseObject(authResp);
|
|
|
+ if (authJson == null || authJson.getJSONObject("data") == null) {
|
|
|
+ log.warn("OPPO 鉴权失败: {}", authResp);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return authJson.getJSONObject("data").getString("auth_token");
|
|
|
+ }
|
|
|
|
|
|
-// String sendResp = postForm("https://api.push.oppomobile.com/server/v1/message/notification/unicast", sendForm, null);
|
|
|
-// JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
-// return sendJson != null && sendJson.getIntValue("code") == 0;
|
|
|
+ private JSONObject buildOppoNotification(CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ JSONObject notification = new JSONObject();
|
|
|
+ notification.put("title", task.getTitle());
|
|
|
+ notification.put("content", task.getContent());
|
|
|
+ notification.put("call_back_url", resolveCallbackUrl());
|
|
|
+ notification.put("call_back_parameter", buildCallbackParameter(task, target));
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ notification.put("click_action_url", task.getJumpUrl());
|
|
|
+ }
|
|
|
+ return notification;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存 OPPO 广播消息体,返回 message_id。
|
|
|
+ */
|
|
|
+ private String saveOppoMessageContent(String authToken, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ form.add("auth_token", authToken);
|
|
|
+ form.add("title", task.getTitle());
|
|
|
+ form.add("content", task.getContent());
|
|
|
+ form.add("call_back_url", resolveCallbackUrl());
|
|
|
+ form.add("call_back_parameter", buildCallbackParameter(task, target));
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ form.add("click_action_url", task.getJumpUrl());
|
|
|
+ }
|
|
|
+ String resp = postForm("https://api.push.oppomobile.com/server/v1/message/notification/save_message_content", form, null);
|
|
|
+ log.info("OPPO 保存广播消息体响应: {}", resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json == null || json.getJSONObject("data") == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return json.getJSONObject("data").getString("message_id");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OPPO 广播下发,target_type=1 表示全量用户。
|
|
|
+ */
|
|
|
+ private boolean broadcastOppoMessage(String authToken, String messageId) {
|
|
|
+ MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ form.add("auth_token", authToken);
|
|
|
+ form.add("message_id", messageId);
|
|
|
+ form.add("target_type", "1");
|
|
|
+ String resp = postForm("https://api.push.oppomobile.com/server/v1/message/notification/broadcast", form, null);
|
|
|
+ log.info("OPPO 广播推送响应: {}", resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ return json != null && json.getIntValue("code") == 0;
|
|
|
}
|
|
|
|
|
|
private String resolveCallbackUrl() {
|
|
|
@@ -218,14 +574,14 @@ public class CommonPushVendorHttpClient {
|
|
|
/**
|
|
|
* OPPO call_back_parameter 限 100 字符,仅传 pushTaskId + userId 短键,deviceId 由回执 registrationIds 带回。
|
|
|
*/
|
|
|
- private String buildCallbackParameter(CommonPushTask task, CommonPushTargetDto target, String channelCode) {
|
|
|
+ private String buildCallbackParameter(CommonPushTask task, CommonPushTargetDto target) {
|
|
|
JSONObject param = new JSONObject(true);
|
|
|
if (task.getId() != null) {
|
|
|
param.put("p", task.getId());
|
|
|
} else if (StringUtils.isNotBlank(task.getTaskNo())) {
|
|
|
param.put("t", task.getTaskNo().trim());
|
|
|
}
|
|
|
- if (target.getUserId() != null) {
|
|
|
+ if (target != null && target.getUserId() != null) {
|
|
|
param.put("u", target.getUserId());
|
|
|
}
|
|
|
String json = param.toJSONString();
|
|
|
@@ -335,30 +691,102 @@ public class CommonPushVendorHttpClient {
|
|
|
}
|
|
|
|
|
|
private boolean sendHuawei(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
- return sendHuaweiLike(credential, task, deviceId, "https://push-api.cloud.huawei.com");
|
|
|
+ return sendHuaweiLike(credential, task, Collections.singletonList(deviceId));
|
|
|
}
|
|
|
|
|
|
private boolean sendHonor(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
- return sendHuaweiLike(credential, task, deviceId, "https://push-api.cloud.huawei.com");
|
|
|
+ return sendHonorLike(credential, task, Collections.singletonList(deviceId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean sendHuaweiLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
+ if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String accessToken = obtainHuaweiAccessToken(credential);
|
|
|
+ if (StringUtils.isBlank(accessToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject body = buildHuaweiMessageBody(task, deviceIds);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "Bearer " + accessToken);
|
|
|
+ String sendResp = postJson(HUAWEI_PUSH_BASE_URL + "/v1/" + appId + "/messages:send",
|
|
|
+ body.toJSONString(), headers);
|
|
|
+ JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
+ boolean ok = sendJson != null && StringUtils.isNotBlank(sendJson.getString("requestId"));
|
|
|
+ if (!ok) {
|
|
|
+ log.error("华为推送失败, taskNo={}, deviceCount={}, resp={}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), sendResp);
|
|
|
+ }
|
|
|
+ return ok;
|
|
|
}
|
|
|
|
|
|
- private boolean sendHuaweiLike(JSONObject credential, CommonPushTask task, String deviceId, String baseUrl) {
|
|
|
+ private boolean sendHonorLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
+ if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
String appId = credential.getString("appId");
|
|
|
String appSecret = credential.getString("appSecret");
|
|
|
if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
return false;
|
|
|
}
|
|
|
+ String accessToken = obtainHonorAccessToken(credential);
|
|
|
+ if (StringUtils.isBlank(accessToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject body = buildHuaweiMessageBody(task, deviceIds);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "Bearer " + accessToken);
|
|
|
+ String sendResp = postJson(HONOR_PUSH_BASE_URL + "/v1/" + appId + "/messages:send",
|
|
|
+ body.toJSONString(), headers);
|
|
|
+ JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
+ boolean ok = sendJson != null && StringUtils.isNotBlank(sendJson.getString("requestId"));
|
|
|
+ if (!ok) {
|
|
|
+ log.error("荣耀推送失败, taskNo={}, deviceCount={}, resp={}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), sendResp);
|
|
|
+ }
|
|
|
+ return ok;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainHuaweiAccessToken(JSONObject credential) {
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
MultiValueMap<String, String> tokenForm = new LinkedMultiValueMap<>();
|
|
|
tokenForm.add("grant_type", "client_credentials");
|
|
|
tokenForm.add("client_id", appId);
|
|
|
tokenForm.add("client_secret", appSecret);
|
|
|
- String tokenResp = postForm(baseUrl + "/oauth2/v2/token", tokenForm, null);
|
|
|
+ String tokenResp = postForm(HUAWEI_PUSH_BASE_URL + "/oauth2/v2/token", tokenForm, null);
|
|
|
JSONObject tokenJson = JSONObject.parseObject(tokenResp);
|
|
|
if (tokenJson == null || StringUtils.isBlank(tokenJson.getString("access_token"))) {
|
|
|
- return false;
|
|
|
+ log.warn("华为鉴权失败: {}", tokenResp);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return tokenJson.getString("access_token");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainHonorAccessToken(JSONObject credential) {
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ MultiValueMap<String, String> tokenForm = new LinkedMultiValueMap<>();
|
|
|
+ tokenForm.add("grant_type", "client_credentials");
|
|
|
+ tokenForm.add("client_id", appId);
|
|
|
+ tokenForm.add("client_secret", appSecret);
|
|
|
+ String tokenResp = postForm(HONOR_AUTH_URL, tokenForm, null);
|
|
|
+ JSONObject tokenJson = JSONObject.parseObject(tokenResp);
|
|
|
+ if (tokenJson == null || StringUtils.isBlank(tokenJson.getString("access_token"))) {
|
|
|
+ log.warn("荣耀鉴权失败: {}", tokenResp);
|
|
|
+ return null;
|
|
|
}
|
|
|
- String accessToken = tokenJson.getString("access_token");
|
|
|
+ return tokenJson.getString("access_token");
|
|
|
+ }
|
|
|
|
|
|
+ private JSONObject buildHuaweiMessageBody(CommonPushTask task, List<String> deviceIds) {
|
|
|
JSONObject body = new JSONObject();
|
|
|
JSONObject message = new JSONObject();
|
|
|
JSONObject android = new JSONObject();
|
|
|
@@ -368,29 +796,45 @@ public class CommonPushVendorHttpClient {
|
|
|
android.put("notification", notification);
|
|
|
message.put("android", android);
|
|
|
body.put("message", message);
|
|
|
- body.put("token", new String[]{deviceId});
|
|
|
+ body.put("token", deviceIds.toArray(new String[0]));
|
|
|
+ return body;
|
|
|
+ }
|
|
|
|
|
|
- Map<String, String> headers = new HashMap<>();
|
|
|
- headers.put("Authorization", "Bearer " + accessToken);
|
|
|
- String sendResp = postJson(baseUrl + "/v1/" + appId + "/messages:send", body.toJSONString(), headers);
|
|
|
- JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
- return sendJson != null && StringUtils.isNotBlank(sendJson.getString("requestId"));
|
|
|
+ private List<String> extractDeviceIds(List<CommonPushTargetDto> targets) {
|
|
|
+ if (targets == null || targets.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<String> deviceIds = new ArrayList<>();
|
|
|
+ Set<String> exists = new HashSet<>();
|
|
|
+ for (CommonPushTargetDto target : targets) {
|
|
|
+ if (target == null || StringUtils.isBlank(target.getDeviceId())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String deviceId = target.getDeviceId().trim();
|
|
|
+ if (exists.add(deviceId)) {
|
|
|
+ deviceIds.add(deviceId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return deviceIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<List<String>> partitionList(List<String> list, int batchSize) {
|
|
|
+ List<List<String>> partitions = new ArrayList<>();
|
|
|
+ if (list == null || list.isEmpty() || batchSize <= 0) {
|
|
|
+ return partitions;
|
|
|
+ }
|
|
|
+ for (int i = 0; i < list.size(); i += batchSize) {
|
|
|
+ partitions.add(list.subList(i, Math.min(i + batchSize, list.size())));
|
|
|
+ }
|
|
|
+ return partitions;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * APNs 需 HTTP/2 + ES256 JWT,当前环境未接入专用客户端,仅校验凭证字段完整性。
|
|
|
- */
|
|
|
private boolean sendApns(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
- String bundleId = credential.getString("bundleId");
|
|
|
- String teamId = credential.getString("teamId");
|
|
|
- String keyId = credential.getString("keyId");
|
|
|
- String privateKey = credential.getString("privateKey");
|
|
|
- if (StringUtils.isAnyBlank(bundleId, teamId, keyId, privateKey)) {
|
|
|
+ if (!apnsPushClient.validateCredential(credential)) {
|
|
|
log.warn("APNs 凭证不完整, deviceId={}", deviceId);
|
|
|
return false;
|
|
|
}
|
|
|
- log.warn("APNs 推送需 HTTP/2 客户端支持,当前跳过实际下发, deviceId={}, title={}", deviceId, task.getTitle());
|
|
|
- return false;
|
|
|
+ return apnsPushClient.send(credential, task, deviceId);
|
|
|
}
|
|
|
|
|
|
private JSONObject parseCredential(String credentialJson) {
|