|
|
@@ -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;
|
|
|
@@ -24,14 +24,7 @@ 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 读取。
|
|
|
@@ -46,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;
|
|
|
@@ -756,29 +755,36 @@ public class CommonPushVendorHttpClient {
|
|
|
if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
return CommonPushVendorSendResult.fail();
|
|
|
}
|
|
|
- String appId = credential.getString("appId");
|
|
|
- String appSecret = credential.getString("appSecret");
|
|
|
- if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
+ 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 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);
|
|
|
- String requestId = sendJson != null ? sendJson.getString("requestId") : null;
|
|
|
- if (StringUtils.isBlank(requestId)) {
|
|
|
- log.error("华为推送失败, taskNo={}, deviceCount={}, resp={}",
|
|
|
- task.getTaskNo(), deviceIds.size(), sendResp);
|
|
|
- return CommonPushVendorSendResult.fail();
|
|
|
- }
|
|
|
- return CommonPushVendorSendResult.ok(requestId);
|
|
|
+ // 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);
|
|
|
+ return parseHuaweiSendResult(sendResp, task.getTaskNo(), pushTokens.size());
|
|
|
}
|
|
|
|
|
|
private CommonPushVendorSendResult sendHonorLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
@@ -790,40 +796,98 @@ public class CommonPushVendorHttpClient {
|
|
|
if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
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 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 + "/messages:send",
|
|
|
body.toJSONString(), headers);
|
|
|
- JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
- String requestId = sendJson != null ? sendJson.getString("requestId") : null;
|
|
|
- if (StringUtils.isBlank(requestId)) {
|
|
|
- log.error("荣耀推送失败, taskNo={}, deviceCount={}, resp={}",
|
|
|
- task.getTaskNo(), deviceIds.size(), sendResp);
|
|
|
- return CommonPushVendorSendResult.fail();
|
|
|
+ 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 CommonPushVendorSendResult.ok(requestId);
|
|
|
+ return StringUtils.trimToNull(credential.getString("appSecret"));
|
|
|
}
|
|
|
|
|
|
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");
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|
|
|
@@ -842,20 +906,98 @@ public class CommonPushVendorHttpClient {
|
|
|
return tokenJson.getString("access_token");
|
|
|
}
|
|
|
|
|
|
- private JSONObject buildHuaweiMessageBody(CommonPushTask task, List<String> deviceIds) {
|
|
|
- JSONObject body = new JSONObject();
|
|
|
+ /**
|
|
|
+ * 华为/荣耀下行消息体:token 必须与 android 同级,放在 message 对象内。
|
|
|
+ *
|
|
|
+ * @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";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * HUAWEI_CN_IQAAA... → IQAAA...
|
|
|
+ */
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 过滤空白、去重后的华为 Push Token,单批 1~1000 个。 */
|
|
|
+ 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();
|