|
|
@@ -1,9 +1,9 @@
|
|
|
package shop.alien.store.service.channel;
|
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.alibaba.fastjson2.JSONArray;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
-import org.apache.commons.codec.binary.Hex;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.springframework.http.HttpEntity;
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
@@ -17,19 +17,14 @@ import org.springframework.web.client.RestTemplate;
|
|
|
import shop.alien.entity.store.CommonPushChannelConfig;
|
|
|
import shop.alien.entity.store.CommonPushTask;
|
|
|
import shop.alien.store.config.CommonPushProperties;
|
|
|
+import shop.alien.store.dto.CommonPushChannelStatsDto;
|
|
|
import shop.alien.store.dto.CommonPushTargetDto;
|
|
|
+import shop.alien.store.dto.CommonPushVendorSendResult;
|
|
|
|
|
|
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;
|
|
|
+import java.util.*;
|
|
|
|
|
|
/**
|
|
|
* 各厂商推送 HTTP 客户端,凭证从配置表 credential_json 读取。
|
|
|
@@ -44,10 +39,16 @@ public class CommonPushVendorHttpClient {
|
|
|
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";
|
|
|
+ /** 华为 OAuth 换 token(与推送 API 域名不同) */
|
|
|
+ private static final String HUAWEI_OAUTH_URL = "https://oauth-login.cloud.huawei.com/oauth2/v3/token";
|
|
|
+ /** 华为 Push Kit 下行消息 API */
|
|
|
+ private static final String HUAWEI_PUSH_API_BASE = "https://push-api.cloud.huawei.com";
|
|
|
+ /** 荣耀 Push API */
|
|
|
+ private static final String HONOR_PUSH_API_BASE = "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 static final String HUAWEI_SUCCESS_CODE = "80000000";
|
|
|
|
|
|
private final CommonPushProperties commonPushProperties;
|
|
|
private final ApnsPushClient apnsPushClient;
|
|
|
@@ -57,185 +58,200 @@ public class CommonPushVendorHttpClient {
|
|
|
/**
|
|
|
* OPPO 全量广播推送(target_type=1),用于 targetType=1 全量场景。
|
|
|
*/
|
|
|
- public boolean sendOppoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ public CommonPushVendorSendResult sendOppoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
if (config == null || task == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
if (credential == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
try {
|
|
|
String authToken = obtainOppoAuthToken(credential);
|
|
|
if (StringUtils.isBlank(authToken)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
String messageId = saveOppoMessageContent(authToken, task, null);
|
|
|
if (StringUtils.isBlank(messageId)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String taskId = broadcastOppoMessage(authToken, messageId);
|
|
|
+ if (StringUtils.isBlank(taskId)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
- return broadcastOppoMessage(authToken, messageId);
|
|
|
+ return CommonPushVendorSendResult.ok(taskId, messageId);
|
|
|
} catch (Exception e) {
|
|
|
log.error("OPPO 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 小米全量广播推送,用于 targetType=1 全量场景。
|
|
|
*/
|
|
|
- public boolean sendXiaomiBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ public CommonPushVendorSendResult sendXiaomiBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
if (config == null || task == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
if (credential == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
String appSecret = credential.getString("appSecret");
|
|
|
if (StringUtils.isBlank(appSecret)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
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(), "全量广播");
|
|
|
+ return parseXiaomiSendResult(resp, task.getTaskNo(), "全量广播");
|
|
|
} catch (Exception e) {
|
|
|
log.error("小米广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* vivo 全量广播推送,用于 targetType=1 全量场景。
|
|
|
*/
|
|
|
- public boolean sendVivoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ public CommonPushVendorSendResult sendVivoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
if (config == null || task == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
if (credential == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
try {
|
|
|
String authToken = obtainVivoAuthToken(credential);
|
|
|
if (StringUtils.isBlank(authToken)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
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(), "全量广播");
|
|
|
+ return parseVivoSendResult(sendResp, task.getTaskNo(), "全量广播");
|
|
|
} catch (Exception e) {
|
|
|
log.error("vivo 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 华为全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
*/
|
|
|
- public boolean sendHuaweiBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ public CommonPushVendorSendResult sendHuaweiBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
List<CommonPushTargetDto> targets) {
|
|
|
if (config == null || task == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
if (credential == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
List<String> deviceIds = extractDeviceIds(targets);
|
|
|
if (deviceIds.isEmpty()) {
|
|
|
log.warn("华为全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
try {
|
|
|
int successBatches = 0;
|
|
|
int totalBatches = 0;
|
|
|
+ String requestId = null;
|
|
|
for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
totalBatches++;
|
|
|
- if (sendHuaweiLike(credential, task, batch)) {
|
|
|
+ CommonPushVendorSendResult batchResult = sendHuaweiLike(credential, task, batch);
|
|
|
+ if (batchResult.isSuccess()) {
|
|
|
successBatches++;
|
|
|
+ if (requestId == null) {
|
|
|
+ requestId = batchResult.getVendorTaskId();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
log.info("华为全量批量推送完成, taskNo={}, devices={}, batches={}/{}",
|
|
|
task.getTaskNo(), deviceIds.size(), successBatches, totalBatches);
|
|
|
- return successBatches > 0;
|
|
|
+ return successBatches > 0 ? CommonPushVendorSendResult.ok(requestId) : CommonPushVendorSendResult.fail();
|
|
|
} catch (Exception e) {
|
|
|
log.error("华为全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 荣耀全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
*/
|
|
|
- public boolean sendHonorBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ public CommonPushVendorSendResult sendHonorBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
List<CommonPushTargetDto> targets) {
|
|
|
if (config == null || task == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
if (credential == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
List<String> deviceIds = extractDeviceIds(targets);
|
|
|
if (deviceIds.isEmpty()) {
|
|
|
log.warn("荣耀全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
try {
|
|
|
int successBatches = 0;
|
|
|
int totalBatches = 0;
|
|
|
+ String requestId = null;
|
|
|
for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
totalBatches++;
|
|
|
- if (sendHonorLike(credential, task, batch)) {
|
|
|
+ CommonPushVendorSendResult batchResult = sendHonorLike(credential, task, batch);
|
|
|
+ if (batchResult.isSuccess()) {
|
|
|
successBatches++;
|
|
|
+ if (requestId == null) {
|
|
|
+ requestId = batchResult.getVendorTaskId();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
log.info("荣耀全量批量推送完成, taskNo={}, devices={}, batches={}/{}",
|
|
|
task.getTaskNo(), deviceIds.size(), successBatches, totalBatches);
|
|
|
- return successBatches > 0;
|
|
|
+ return successBatches > 0 ? CommonPushVendorSendResult.ok(requestId) : CommonPushVendorSendResult.fail();
|
|
|
} catch (Exception e) {
|
|
|
log.error("荣耀全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* APNs 全量推送:按 iOS deviceToken 逐条下发(Apple 无 /all 接口)。
|
|
|
*/
|
|
|
- public boolean sendApnsBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ public CommonPushVendorSendResult sendApnsBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
List<CommonPushTargetDto> targets) {
|
|
|
if (config == null || task == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
if (credential == null || !apnsPushClient.validateCredential(credential)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
List<CommonPushTargetDto> apnsTargets = ApnsPushClient.filterApnsTargets(targets, targets);
|
|
|
if (apnsTargets.isEmpty()) {
|
|
|
log.warn("APNs 全量推送无 iOS 设备 token, taskNo={}", task.getTaskNo());
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
- return apnsPushClient.sendBroadcast(credential, task, apnsTargets);
|
|
|
+ boolean ok = apnsPushClient.sendBroadcast(credential, task, apnsTargets);
|
|
|
+ return ok ? CommonPushVendorSendResult.ok(task.getTaskNo()) : CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
|
|
|
- public boolean send(CommonPushChannelConfig config, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ public CommonPushVendorSendResult send(CommonPushChannelConfig config, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
if (config == null || task == null || target == null || StringUtils.isBlank(target.getDeviceId())) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
String deviceId = target.getDeviceId();
|
|
|
JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
if (credential == null) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
String channelCode = StringUtils.lowerCase(StringUtils.trim(config.getChannelCode()));
|
|
|
try {
|
|
|
@@ -247,7 +263,8 @@ public class CommonPushVendorHttpClient {
|
|
|
case "oppo":
|
|
|
return sendOppo(credential, task, target);
|
|
|
case "samsung":
|
|
|
- return sendSamsung(credential, task, target);
|
|
|
+ return sendSamsung(credential, task, target) ? CommonPushVendorSendResult.ok(task.getTaskNo())
|
|
|
+ : CommonPushVendorSendResult.fail();
|
|
|
case "huawei":
|
|
|
return sendHuawei(credential, task, deviceId);
|
|
|
case "honor":
|
|
|
@@ -256,29 +273,29 @@ public class CommonPushVendorHttpClient {
|
|
|
return sendApns(credential, task, deviceId);
|
|
|
default:
|
|
|
log.warn("不支持的推送渠道: {}", channelCode);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
log.error("渠道推送失败, channelCode={}, deviceId={}, err={}", channelCode, deviceId, e.getMessage(), e);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private boolean sendVivo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ private CommonPushVendorSendResult sendVivo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
String deviceId = target.getDeviceId();
|
|
|
if (StringUtils.isBlank(deviceId)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
String authToken = obtainVivoAuthToken(credential);
|
|
|
if (StringUtils.isBlank(authToken)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
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(), "单推");
|
|
|
+ return parseVivoSendResult(sendResp, task.getTaskNo(), "单推");
|
|
|
}
|
|
|
|
|
|
private String obtainVivoAuthToken(JSONObject credential) {
|
|
|
@@ -345,34 +362,35 @@ public class CommonPushVendorHttpClient {
|
|
|
return param.toJSONString();
|
|
|
}
|
|
|
|
|
|
- private boolean isVivoSendSuccess(String resp, String taskNo, String scene) {
|
|
|
+ private CommonPushVendorSendResult parseVivoSendResult(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;
|
|
|
+ String taskId = StringUtils.defaultIfBlank(json.getString("taskId"), json.getString("task_id"));
|
|
|
+ return CommonPushVendorSendResult.ok(taskId);
|
|
|
}
|
|
|
if (json != null) {
|
|
|
log.error("vivo{}失败, taskNo={}, result={}, desc={}",
|
|
|
scene, taskNo, json.getIntValue("result"), json.getString("desc"));
|
|
|
}
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
|
|
|
- private boolean sendXiaomi(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ private CommonPushVendorSendResult sendXiaomi(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
String deviceId = target.getDeviceId();
|
|
|
String appSecret = credential.getString("appSecret");
|
|
|
if (StringUtils.isAnyBlank(appSecret, deviceId)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
MultiValueMap<String, String> form = buildXiaomiMessageForm(credential, task, target);
|
|
|
form.add("registration_id", deviceId);
|
|
|
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(), "单推");
|
|
|
+ return parseXiaomiSendResult(resp, task.getTaskNo(), "单推");
|
|
|
}
|
|
|
|
|
|
private boolean validateXiaomiCredential(JSONObject credential, String taskNo) {
|
|
|
@@ -398,17 +416,32 @@ public class CommonPushVendorHttpClient {
|
|
|
StringUtils.trimToNull(credential.getString("channel_id")));
|
|
|
}
|
|
|
|
|
|
- private boolean isXiaomiSendSuccess(String resp, String taskNo, String scene) {
|
|
|
+ private CommonPushVendorSendResult parseXiaomiSendResult(String resp, String taskNo, String scene) {
|
|
|
log.info("小米{}响应: {}", scene, resp);
|
|
|
JSONObject json = JSONObject.parseObject(resp);
|
|
|
if (json != null && "ok".equalsIgnoreCase(json.getString("result"))) {
|
|
|
- return true;
|
|
|
+ String msgId = extractXiaomiMessageId(json);
|
|
|
+ return CommonPushVendorSendResult.ok(msgId);
|
|
|
}
|
|
|
if (json != null) {
|
|
|
log.error("小米{}失败, taskNo={}, code={}, reason={}, description={}",
|
|
|
scene, taskNo, json.getString("code"), json.getString("reason"), json.getString("description"));
|
|
|
}
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String extractXiaomiMessageId(JSONObject json) {
|
|
|
+ if (json == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject data = json.getJSONObject("data");
|
|
|
+ if (data != null) {
|
|
|
+ String id = StringUtils.defaultIfBlank(data.getString("id"), data.getString("msg_id"));
|
|
|
+ if (StringUtils.isNotBlank(id)) {
|
|
|
+ return id;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return StringUtils.defaultIfBlank(json.getString("messageId"), json.getString("msg_id"));
|
|
|
}
|
|
|
|
|
|
private MultiValueMap<String, String> buildXiaomiMessageForm(JSONObject credential, CommonPushTask task,
|
|
|
@@ -461,11 +494,11 @@ public class CommonPushVendorHttpClient {
|
|
|
/**
|
|
|
* OPPO 厂商直连:使用 RegistrationID(非 UniPush CID),并设置送达回执回调。
|
|
|
*/
|
|
|
- private boolean sendOppo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ private CommonPushVendorSendResult sendOppo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
String deviceId = target.getDeviceId();
|
|
|
String authToken = obtainOppoAuthToken(credential);
|
|
|
if (StringUtils.isBlank(authToken)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
|
|
|
JSONObject notification = buildOppoNotification(task, target);
|
|
|
@@ -489,12 +522,25 @@ public class CommonPushVendorHttpClient {
|
|
|
);
|
|
|
String body = response.getBody();
|
|
|
log.info("OPPO 单推响应: {}", body);
|
|
|
- JSONObject resp = JSONObject.parseObject(body);
|
|
|
- return resp != null && resp.getIntValue("code") == 0;
|
|
|
+ return parseOppoSendResult(body);
|
|
|
} catch (Exception e) {
|
|
|
log.error("OPPO 单推请求异常: {}", e.getMessage(), e);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private CommonPushVendorSendResult parseOppoSendResult(String resp) {
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json == null || json.getIntValue("code") != 0) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
+ JSONObject data = json.getJSONObject("data");
|
|
|
+ if (data == null) {
|
|
|
+ return CommonPushVendorSendResult.ok(null);
|
|
|
+ }
|
|
|
+ String taskId = StringUtils.defaultIfBlank(data.getString("task_id"), data.getString("message_id"));
|
|
|
+ String messageId = StringUtils.defaultIfBlank(data.getString("message_id"), data.getString("messageId"));
|
|
|
+ return CommonPushVendorSendResult.ok(taskId, messageId);
|
|
|
}
|
|
|
|
|
|
private String obtainOppoAuthToken(JSONObject credential) {
|
|
|
@@ -553,9 +599,9 @@ public class CommonPushVendorHttpClient {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * OPPO 广播下发,target_type=1 表示全量用户。
|
|
|
+ * OPPO 广播下发,target_type=1 表示全量用户,返回 task_id。
|
|
|
*/
|
|
|
- private boolean broadcastOppoMessage(String authToken, String messageId) {
|
|
|
+ private String broadcastOppoMessage(String authToken, String messageId) {
|
|
|
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
form.add("auth_token", authToken);
|
|
|
form.add("message_id", messageId);
|
|
|
@@ -563,7 +609,14 @@ public class CommonPushVendorHttpClient {
|
|
|
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;
|
|
|
+ if (json == null || json.getIntValue("code") != 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject data = json.getJSONObject("data");
|
|
|
+ if (data == null) {
|
|
|
+ return messageId;
|
|
|
+ }
|
|
|
+ return StringUtils.defaultIfBlank(data.getString("task_id"), messageId);
|
|
|
}
|
|
|
|
|
|
private String resolveCallbackUrl() {
|
|
|
@@ -690,84 +743,177 @@ public class CommonPushVendorHttpClient {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private boolean sendHuawei(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ private CommonPushVendorSendResult sendHuawei(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
return sendHuaweiLike(credential, task, Collections.singletonList(deviceId));
|
|
|
}
|
|
|
|
|
|
- private boolean sendHonor(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ private CommonPushVendorSendResult sendHonor(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
return sendHonorLike(credential, task, Collections.singletonList(deviceId));
|
|
|
}
|
|
|
|
|
|
- private boolean sendHuaweiLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
+ /**
|
|
|
+ * 华为 Push Kit 批量/单条下发:OAuth 换 token → 组装 message body → POST messages:send。
|
|
|
+ * <p>格式转换 credential_json → 请求 URL:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 credential: { "clientId": "115095083", "clientSecret": "..." }
|
|
|
+ * 转换后 URL: https://push-api.cloud.huawei.com/v1/115095083/messages:send
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
+ private CommonPushVendorSendResult 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;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String pushClientId = resolveHuaweiOAuthClientId(credential);
|
|
|
+ if (StringUtils.isBlank(pushClientId)) {
|
|
|
+ log.warn("华为推送缺少 clientId/appId, taskNo={}", task.getTaskNo());
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ List<String> pushTokens = normalizeHuaweiPushTokens(deviceIds);
|
|
|
+ if (pushTokens.isEmpty()) {
|
|
|
+ log.warn("华为推送 token 为空, taskNo={}", task.getTaskNo());
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ for (String pushToken : pushTokens) {
|
|
|
+ if (!isLikelyHuaweiPushToken(pushToken)) {
|
|
|
+ log.warn("华为推送 token 格式可疑, taskNo={}, token={}, 请确认 life_user.device_id 存的是 HMS PushKit token 而非 uni-push cid 或其他厂商 regId",
|
|
|
+ task.getTaskNo(), maskPushToken(pushToken));
|
|
|
+ }
|
|
|
}
|
|
|
String accessToken = obtainHuaweiAccessToken(credential);
|
|
|
if (StringUtils.isBlank(accessToken)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
|
|
|
- JSONObject body = buildHuaweiMessageBody(task, deviceIds);
|
|
|
+ JSONObject body = buildHuaweiMessageBody(task, pushTokens);
|
|
|
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;
|
|
|
+ // v1 路径参数为 OAuth 2.0 client ID,与换 token 的 client_id 一致
|
|
|
+ String sendUrl = HUAWEI_PUSH_API_BASE + "/v1/" + pushClientId + "/messages:send";
|
|
|
+ log.info("华为推送请求, taskNo={}, tokenCount={}, sendUrl={}, sampleToken={}",
|
|
|
+ task.getTaskNo(), pushTokens.size(), sendUrl, maskPushToken(pushTokens.get(0)));
|
|
|
+ String sendResp = postJson(sendUrl, body.toJSONString(), headers);
|
|
|
+ log.info("推送参数和结果为,accessToken={}, sendResp={}", accessToken, sendResp);
|
|
|
+ return parseHuaweiSendResult(sendResp, task.getTaskNo(), pushTokens.size());
|
|
|
}
|
|
|
|
|
|
- private boolean sendHonorLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
+ private CommonPushVendorSendResult sendHonorLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
String appId = credential.getString("appId");
|
|
|
String appSecret = credential.getString("appSecret");
|
|
|
if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ List<String> pushTokens = normalizeHuaweiPushTokens(deviceIds);
|
|
|
+ if (pushTokens.isEmpty()) {
|
|
|
+ log.warn("荣耀推送 token 为空, taskNo={}", task.getTaskNo());
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
String accessToken = obtainHonorAccessToken(credential);
|
|
|
if (StringUtils.isBlank(accessToken)) {
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
|
|
|
- JSONObject body = buildHuaweiMessageBody(task, deviceIds);
|
|
|
+ JSONObject body = buildHuaweiMessageBody(task, pushTokens);
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
|
headers.put("Authorization", "Bearer " + accessToken);
|
|
|
- String sendResp = postJson(HONOR_PUSH_BASE_URL + "/v1/" + appId + "/messages:send",
|
|
|
+ String sendResp = postJson(HONOR_PUSH_API_BASE + "/v1/" + appId + "/sendMessage",
|
|
|
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 parseHuaweiSendResult(sendResp, task.getTaskNo(), pushTokens.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 华为推送 API URL 中的 App ID(AppGallery Connect 应用 ID)。
|
|
|
+ */
|
|
|
+ private String resolveHuaweiPushAppId(JSONObject credential) {
|
|
|
+ String appId = StringUtils.trimToNull(credential.getString("appId"));
|
|
|
+ if (appId != null) {
|
|
|
+ return appId;
|
|
|
+ }
|
|
|
+ return StringUtils.trimToNull(credential.getString("clientId"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OAuth client_id:优先 clientId,否则与 pushAppId 一致。
|
|
|
+ * 须与下发 URL 中的 appId 对应同一应用,否则返回 80200001。
|
|
|
+ */
|
|
|
+ private String resolveHuaweiOAuthClientId(JSONObject credential) {
|
|
|
+ String clientId = StringUtils.trimToNull(credential.getString("clientId"));
|
|
|
+ if (clientId != null) {
|
|
|
+ return clientId;
|
|
|
+ }
|
|
|
+ return resolveHuaweiPushAppId(credential);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveHuaweiOAuthClientSecret(JSONObject credential) {
|
|
|
+ String clientSecret = StringUtils.trimToNull(credential.getString("clientSecret"));
|
|
|
+ if (clientSecret != null) {
|
|
|
+ return clientSecret;
|
|
|
}
|
|
|
- return ok;
|
|
|
+ return StringUtils.trimToNull(credential.getString("appSecret"));
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 向华为 OAuth 服务换取 access_token(用于 Authorization: Bearer)。
|
|
|
+ * <p>格式转换 credential → form 参数:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 credential: { "clientId": "115095083", "clientSecret": "xxx" }
|
|
|
+ * 转换后 POST body: grant_type=client_credentials&client_id=115095083&client_secret=xxx
|
|
|
+ * 响应: { "access_token": "AT_xxx", "expires_in": 3600 }
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
private String obtainHuaweiAccessToken(JSONObject credential) {
|
|
|
- String appId = credential.getString("appId");
|
|
|
- String appSecret = credential.getString("appSecret");
|
|
|
+ String clientId = resolveHuaweiOAuthClientId(credential);
|
|
|
+ String clientSecret = resolveHuaweiOAuthClientSecret(credential);
|
|
|
+ String pushAppId = resolveHuaweiPushAppId(credential);
|
|
|
+ if (StringUtils.isAnyBlank(clientId, clientSecret)) {
|
|
|
+ log.warn("华为 OAuth 凭证不完整, 需配置 appId+appSecret 或 clientId+clientSecret");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (pushAppId != null && !StringUtils.equals(clientId, pushAppId)) {
|
|
|
+ log.warn("华为 OAuth clientId({}) 与 push appId({}) 不一致,若鉴权失败请改为同一应用 ID",
|
|
|
+ clientId, pushAppId);
|
|
|
+ }
|
|
|
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(HUAWEI_PUSH_BASE_URL + "/oauth2/v2/token", tokenForm, null);
|
|
|
- JSONObject tokenJson = JSONObject.parseObject(tokenResp);
|
|
|
- if (tokenJson == null || StringUtils.isBlank(tokenJson.getString("access_token"))) {
|
|
|
- log.warn("华为鉴权失败: {}", tokenResp);
|
|
|
+ tokenForm.add("client_id", clientId);
|
|
|
+ tokenForm.add("client_secret", clientSecret);
|
|
|
+ try {
|
|
|
+ String tokenResp = postForm(HUAWEI_OAUTH_URL, tokenForm, null);
|
|
|
+ JSONObject tokenJson = JSONObject.parseObject(tokenResp);
|
|
|
+ if (tokenJson == null || StringUtils.isBlank(tokenJson.getString("access_token"))) {
|
|
|
+ log.warn("华为 OAuth 鉴权失败, clientId={}, resp={}", clientId, tokenResp);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return tokenJson.getString("access_token");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("华为 OAuth 请求异常, clientId={}, err={}", clientId, e.getMessage(), e);
|
|
|
return null;
|
|
|
}
|
|
|
- return tokenJson.getString("access_token");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析华为 Push API 响应;成功码为 80000000。
|
|
|
+ * <p>格式转换:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 JSON: { "code":"80000000", "requestId":"178169515953238426090801" }
|
|
|
+ * 转换后 CommonPushVendorSendResult: ok, vendorTaskId=requestId
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
+ private CommonPushVendorSendResult parseHuaweiSendResult(String resp, String taskNo, int deviceCount) {
|
|
|
+ JSONObject sendJson = JSONObject.parseObject(resp);
|
|
|
+ if (sendJson != null && HUAWEI_SUCCESS_CODE.equals(sendJson.getString("code"))) {
|
|
|
+ String requestId = sendJson.getString("requestId");
|
|
|
+ log.info("华为推送成功, taskNo={}, deviceCount={}, requestId={}", taskNo, deviceCount, requestId);
|
|
|
+ return CommonPushVendorSendResult.ok(requestId);
|
|
|
+ }
|
|
|
+ if (sendJson != null && "80300007".equals(sendJson.getString("code"))) {
|
|
|
+ log.error("华为推送 token 无效(80300007), taskNo={}, deviceCount={}, requestId={}, 请检查 token 是否由当前华为应用上报、是否过期、是否误用其他厂商或测试 token",
|
|
|
+ taskNo, deviceCount, sendJson.getString("requestId"));
|
|
|
+ }
|
|
|
+ log.error("华为推送失败, taskNo={}, deviceCount={}, resp={}", taskNo, deviceCount, resp);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
|
|
|
private String obtainHonorAccessToken(JSONObject credential) {
|
|
|
@@ -786,20 +932,117 @@ public class CommonPushVendorHttpClient {
|
|
|
return tokenJson.getString("access_token");
|
|
|
}
|
|
|
|
|
|
- private JSONObject buildHuaweiMessageBody(CommonPushTask task, List<String> deviceIds) {
|
|
|
- JSONObject body = new JSONObject();
|
|
|
+ /**
|
|
|
+ * 华为/荣耀下行消息体:token 必须与 android 同级,放在 message 对象内。
|
|
|
+ * <p>格式转换 pushTokens → 华为 API JSON body:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前 List: ["HUAWEI_CN_IQAAAACy1la8..."]
|
|
|
+ * 转换后 body:
|
|
|
+ * {
|
|
|
+ * "message": {
|
|
|
+ * "android": { "notification": { "title":"...", "body":"...", "click_action": {"type":3} } },
|
|
|
+ * "token": ["IQAAAACy1la8..."]
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @see <a href="https://developer.huawei.com/consumer/en/doc/HMSCore-References/https-send-api-0000001050986197">Huawei Push send API</a>
|
|
|
+ */
|
|
|
+ private JSONObject buildHuaweiMessageBody(CommonPushTask task, List<String> pushTokens) {
|
|
|
JSONObject message = new JSONObject();
|
|
|
JSONObject android = new JSONObject();
|
|
|
JSONObject notification = new JSONObject();
|
|
|
+ JSONObject clickAction = new JSONObject();
|
|
|
notification.put("title", task.getTitle());
|
|
|
notification.put("body", task.getContent());
|
|
|
+ clickAction.put("type", 3);
|
|
|
+ notification.put("click_action", clickAction);
|
|
|
android.put("notification", notification);
|
|
|
message.put("android", android);
|
|
|
+ List<String> huaweiTokens = new ArrayList<>();
|
|
|
+ for (String pushToken : pushTokens) {
|
|
|
+ String extracted = extractHuaweiPushToken(pushToken);
|
|
|
+ if (StringUtils.isNotBlank(extracted)) {
|
|
|
+ huaweiTokens.add(extracted);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ message.put("token", new JSONArray(huaweiTokens));
|
|
|
+
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
body.put("message", message);
|
|
|
- body.put("token", deviceIds.toArray(new String[0]));
|
|
|
return body;
|
|
|
}
|
|
|
|
|
|
+ /** 华为 Push Kit token 以 IQAAA 开头;客户端可能带 HUAWEI_CN_ 前缀。 */
|
|
|
+ private static final String HUAWEI_PUSH_TOKEN_MARKER = "IQAAA";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 剥离客户端上报的厂商前缀,得到华为 Push Kit 可用 token。
|
|
|
+ * <p>格式转换:</p>
|
|
|
+ * <pre>
|
|
|
+ * 转换前: "HUAWEI_CN_IQAAAACy1la8AAC0PfgZ4PM9..."
|
|
|
+ * 转换后: "IQAAAACy1la8AAC0PfgZ4PM9..."
|
|
|
+ * </pre>
|
|
|
+ */
|
|
|
+ private String extractHuaweiPushToken(String rawToken) {
|
|
|
+ if (StringUtils.isBlank(rawToken)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String trimmed = rawToken.trim();
|
|
|
+ if (trimmed.startsWith(HUAWEI_PUSH_TOKEN_MARKER)) {
|
|
|
+ return trimmed;
|
|
|
+ }
|
|
|
+ int markerIndex = trimmed.indexOf(HUAWEI_PUSH_TOKEN_MARKER);
|
|
|
+ if (markerIndex >= 0) {
|
|
|
+ return trimmed.substring(markerIndex);
|
|
|
+ }
|
|
|
+ return trimmed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isLikelyHuaweiPushToken(String token) {
|
|
|
+ String extracted = extractHuaweiPushToken(token);
|
|
|
+ return StringUtils.isNotBlank(extracted) && extracted.startsWith(HUAWEI_PUSH_TOKEN_MARKER);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String maskPushToken(String token) {
|
|
|
+ if (StringUtils.isBlank(token)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ String t = token.trim();
|
|
|
+ if (t.length() <= 12) {
|
|
|
+ return t.substring(0, Math.min(4, t.length())) + "***";
|
|
|
+ }
|
|
|
+ return t.substring(0, 8) + "..." + t.substring(t.length() - 4);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 过滤空白、去重并剥离 HUAWEI_CN_ 前缀,单批最多 1000 个 token。
|
|
|
+ * <p>格式转换 life_user.device_id 列表 → 华为 API token 列表(见 {@link #extractHuaweiPushToken})。</p>
|
|
|
+ */
|
|
|
+ private List<String> normalizeHuaweiPushTokens(List<String> deviceIds) {
|
|
|
+ if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<String> tokens = new ArrayList<>();
|
|
|
+ Set<String> exists = new HashSet<>();
|
|
|
+ for (String deviceId : deviceIds) {
|
|
|
+ if (StringUtils.isBlank(deviceId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String token = extractHuaweiPushToken(deviceId);
|
|
|
+ if (StringUtils.isBlank(token)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (exists.add(token)) {
|
|
|
+ tokens.add(token);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (tokens.size() > HUAWEI_BATCH_TOKEN_LIMIT) {
|
|
|
+ return tokens.subList(0, HUAWEI_BATCH_TOKEN_LIMIT);
|
|
|
+ }
|
|
|
+ return tokens;
|
|
|
+ }
|
|
|
+
|
|
|
private List<String> extractDeviceIds(List<CommonPushTargetDto> targets) {
|
|
|
if (targets == null || targets.isEmpty()) {
|
|
|
return Collections.emptyList();
|
|
|
@@ -829,12 +1072,13 @@ public class CommonPushVendorHttpClient {
|
|
|
return partitions;
|
|
|
}
|
|
|
|
|
|
- private boolean sendApns(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ private CommonPushVendorSendResult sendApns(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
if (!apnsPushClient.validateCredential(credential)) {
|
|
|
log.warn("APNs 凭证不完整, deviceId={}", deviceId);
|
|
|
- return false;
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
- return apnsPushClient.send(credential, task, deviceId);
|
|
|
+ boolean ok = apnsPushClient.send(credential, task, deviceId);
|
|
|
+ return ok ? CommonPushVendorSendResult.ok(task.getTaskNo()) : CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
|
|
|
private JSONObject parseCredential(String credentialJson) {
|
|
|
@@ -859,6 +1103,142 @@ public class CommonPushVendorHttpClient {
|
|
|
return payload.toJSONString();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 查询 OPPO 广播推送统计(需 message_id + task_id)。
|
|
|
+ */
|
|
|
+ public CommonPushChannelStatsDto queryOppoStatistics(JSONObject credential, String messageId, String taskId) {
|
|
|
+ if (credential == null || StringUtils.isAnyBlank(messageId, taskId)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainOppoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ form.add("auth_token", authToken);
|
|
|
+ form.add("message_id", messageId);
|
|
|
+ form.add("task_id", taskId);
|
|
|
+ String resp = postForm("https://api.push.oppomobile.com/server/v1/message/statistics", form, null);
|
|
|
+ log.info("OPPO 统计查询响应: messageId={}, taskId={}, resp={}", messageId, taskId, resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json == null || json.getIntValue("code") != 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject data = json.getJSONObject("data");
|
|
|
+ if (data == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ CommonPushChannelStatsDto stats = new CommonPushChannelStatsDto();
|
|
|
+ stats.setRealSend(longToString(data.getLong("sendCount")));
|
|
|
+ stats.setRealDelivered(longToString(data.getLong("arriveCount")));
|
|
|
+ stats.setShowSum(longToString(data.getLong("showCount")));
|
|
|
+ stats.setClickSum(longToString(data.getLong("openCount")));
|
|
|
+ return stats;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OPPO 统计查询失败, messageId={}, taskId={}, err={}", messageId, taskId, e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询 vivo 推送统计。
|
|
|
+ */
|
|
|
+ public CommonPushChannelStatsDto queryVivoStatistics(JSONObject credential, String taskId) {
|
|
|
+ if (credential == null || StringUtils.isBlank(taskId)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainVivoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("authToken", authToken);
|
|
|
+ String url = "https://api-push.vivo.com.cn/report/getStatistics?taskIds=" + urlEncode(taskId);
|
|
|
+ String resp = getJson(url, headers);
|
|
|
+ log.info("vivo 统计查询响应: taskId={}, resp={}", taskId, resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json == null || json.getIntValue("result") != 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ com.alibaba.fastjson.JSONArray statistics = json.getJSONArray("statistics");
|
|
|
+ if (statistics == null || statistics.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject item = statistics.getJSONObject(0);
|
|
|
+ CommonPushChannelStatsDto stats = new CommonPushChannelStatsDto();
|
|
|
+ stats.setRealSend(longToString(item.getLong("send")));
|
|
|
+ stats.setRealDelivered(longToString(item.getLong("receive")));
|
|
|
+ stats.setShowSum(longToString(item.getLong("display")));
|
|
|
+ stats.setClickSum(longToString(item.getLong("click")));
|
|
|
+ return stats;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("vivo 统计查询失败, taskId={}, err={}", taskId, e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询小米推送统计(msg_id)。
|
|
|
+ */
|
|
|
+ public CommonPushChannelStatsDto queryXiaomiStatistics(JSONObject credential, String msgId) {
|
|
|
+ if (credential == null || StringUtils.isBlank(msgId)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isBlank(appSecret)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "key=" + appSecret);
|
|
|
+ String url = "https://api.xmpush.xiaomi.com/v1/trace/message/status?msg_id=" + urlEncode(msgId);
|
|
|
+ String resp = getJson(url, headers);
|
|
|
+ log.info("小米统计查询响应: msgId={}, resp={}", msgId, resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json == null || !"ok".equalsIgnoreCase(json.getString("result"))) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject data = json.getJSONObject("data");
|
|
|
+ if (data == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JSONObject status = data.getJSONObject(msgId);
|
|
|
+ if (status == null && !data.isEmpty()) {
|
|
|
+ status = data.getJSONObject(data.keySet().iterator().next());
|
|
|
+ }
|
|
|
+ if (status == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ CommonPushChannelStatsDto stats = new CommonPushChannelStatsDto();
|
|
|
+ stats.setExpectedSend(longToString(status.getLong("resolved")));
|
|
|
+ stats.setRealSend(longToString(status.getLong("msg_send")));
|
|
|
+ stats.setRealDelivered(longToString(status.getLong("delivered")));
|
|
|
+ stats.setShowSum(longToString(status.getLong("msg_display")));
|
|
|
+ stats.setClickSum(longToString(status.getLong("click")));
|
|
|
+ return stats;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("小米统计查询失败, msgId={}, err={}", msgId, e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String longToString(Long value) {
|
|
|
+ return value == null ? null : String.valueOf(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getJson(String url, Map<String, String> headers) {
|
|
|
+ RestTemplate client = buildRestTemplate();
|
|
|
+ HttpHeaders httpHeaders = new HttpHeaders();
|
|
|
+ if (headers != null) {
|
|
|
+ headers.forEach(httpHeaders::set);
|
|
|
+ }
|
|
|
+ ResponseEntity<String> response = client.exchange(url, org.springframework.http.HttpMethod.GET,
|
|
|
+ new HttpEntity<>(httpHeaders), String.class);
|
|
|
+ return response.getBody();
|
|
|
+ }
|
|
|
+
|
|
|
private String postJson(String url, String body, Map<String, String> headers) {
|
|
|
RestTemplate restTemplate = buildRestTemplate();
|
|
|
HttpHeaders httpHeaders = new HttpHeaders();
|