Przeglądaj źródła

feat:华为发送通知成功

刘云鑫 11 godzin temu
rodzic
commit
3fda7bc329

+ 188 - 46
alien-store/src/main/java/shop/alien/store/service/channel/CommonPushVendorHttpClient.java

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