|
|
@@ -0,0 +1,1284 @@
|
|
|
+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.lang3.StringUtils;
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.util.LinkedMultiValueMap;
|
|
|
+import org.springframework.util.MultiValueMap;
|
|
|
+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.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 各厂商推送 HTTP 客户端,凭证从配置表 credential_json 读取。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class CommonPushVendorHttpClient {
|
|
|
+
|
|
|
+ private static final int TIMEOUT_MS = 8000;
|
|
|
+ /** OPPO call_back_parameter 最大 100 字符 */
|
|
|
+ private static final int OPPO_CALLBACK_PARAM_MAX_LEN = 100;
|
|
|
+
|
|
|
+ private static final int HUAWEI_BATCH_TOKEN_LIMIT = 1000;
|
|
|
+ /** 华为 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;
|
|
|
+
|
|
|
+ private static final RestTemplate restTemplate = new RestTemplate();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OPPO 全量广播推送(target_type=1),用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public CommonPushVendorSendResult sendOppoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainOppoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String messageId = saveOppoMessageContent(authToken, task, null);
|
|
|
+ if (StringUtils.isBlank(messageId)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String taskId = broadcastOppoMessage(authToken, messageId);
|
|
|
+ if (StringUtils.isBlank(taskId)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ return CommonPushVendorSendResult.ok(taskId, messageId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OPPO 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小米全量广播推送,用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public CommonPushVendorSendResult sendXiaomiBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isBlank(appSecret)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
+ 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 parseXiaomiSendResult(resp, task.getTaskNo(), "全量广播");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("小米广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * vivo 全量广播推送,用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public CommonPushVendorSendResult sendVivoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainVivoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ 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 parseVivoSendResult(sendResp, task.getTaskNo(), "全量广播");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("vivo 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 华为全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
+ */
|
|
|
+ public CommonPushVendorSendResult sendHuaweiBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ List<String> deviceIds = extractDeviceIds(targets);
|
|
|
+ if (deviceIds.isEmpty()) {
|
|
|
+ log.warn("华为全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int successBatches = 0;
|
|
|
+ int totalBatches = 0;
|
|
|
+ String requestId = null;
|
|
|
+ for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
+ totalBatches++;
|
|
|
+ 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 ? CommonPushVendorSendResult.ok(requestId) : CommonPushVendorSendResult.fail();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("华为全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 荣耀全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
+ */
|
|
|
+ public CommonPushVendorSendResult sendHonorBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ List<String> deviceIds = extractDeviceIds(targets);
|
|
|
+ if (deviceIds.isEmpty()) {
|
|
|
+ log.warn("荣耀全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int successBatches = 0;
|
|
|
+ int totalBatches = 0;
|
|
|
+ String requestId = null;
|
|
|
+ for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
+ totalBatches++;
|
|
|
+ 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 ? CommonPushVendorSendResult.ok(requestId) : CommonPushVendorSendResult.fail();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("荣耀全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * APNs 全量推送:按 iOS deviceToken 逐条下发(Apple 无 /all 接口)。
|
|
|
+ */
|
|
|
+ public CommonPushVendorSendResult sendApnsBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null || !apnsPushClient.validateCredential(credential)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ List<CommonPushTargetDto> apnsTargets = ApnsPushClient.filterApnsTargets(targets, targets);
|
|
|
+ if (apnsTargets.isEmpty()) {
|
|
|
+ log.warn("APNs 全量推送无 iOS 设备 token, taskNo={}", task.getTaskNo());
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ boolean ok = apnsPushClient.sendBroadcast(credential, task, apnsTargets);
|
|
|
+ return ok ? CommonPushVendorSendResult.ok(task.getTaskNo()) : CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+
|
|
|
+ public CommonPushVendorSendResult send(CommonPushChannelConfig config, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ if (config == null || task == null || target == null || StringUtils.isBlank(target.getDeviceId())) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String channelCode = StringUtils.lowerCase(StringUtils.trim(config.getChannelCode()));
|
|
|
+ try {
|
|
|
+ switch (channelCode) {
|
|
|
+ case "vivo":
|
|
|
+ return sendVivo(credential, task, target);
|
|
|
+ case "xiaomi":
|
|
|
+ return sendXiaomi(credential, task, target);
|
|
|
+ case "oppo":
|
|
|
+ return sendOppo(credential, task, target);
|
|
|
+ case "samsung":
|
|
|
+ return sendSamsung(credential, task, target) ? CommonPushVendorSendResult.ok(task.getTaskNo())
|
|
|
+ : CommonPushVendorSendResult.fail();
|
|
|
+ case "huawei":
|
|
|
+ return sendHuawei(credential, task, deviceId);
|
|
|
+ case "honor":
|
|
|
+ return sendHonor(credential, task, deviceId);
|
|
|
+ case "apns":
|
|
|
+ return sendApns(credential, task, deviceId);
|
|
|
+ default:
|
|
|
+ log.warn("不支持的推送渠道: {}", channelCode);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("渠道推送失败, channelCode={}, deviceId={}, err={}", channelCode, deviceId, e.getMessage(), e);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private CommonPushVendorSendResult sendVivo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ if (StringUtils.isBlank(deviceId)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String authToken = obtainVivoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ 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 parseVivoSendResult(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 null;
|
|
|
+ }
|
|
|
+ long timestamp = System.currentTimeMillis();
|
|
|
+ String sign = md5Hex(appId + appKey + timestamp + appSecret);
|
|
|
+ JSONObject authBody = new JSONObject();
|
|
|
+ authBody.put("appId", appId);
|
|
|
+ authBody.put("appKey", appKey);
|
|
|
+ authBody.put("timestamp", timestamp);
|
|
|
+ authBody.put("sign", sign);
|
|
|
+ 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) {
|
|
|
+ log.warn("vivo 鉴权失败: {}", authResp);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return authJson.getString("authToken");
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject buildVivoNotificationBody(JSONObject credential, CommonPushTask task,
|
|
|
+ CommonPushTargetDto target) {
|
|
|
+ JSONObject sendBody = new JSONObject();
|
|
|
+ sendBody.put("appId", credential.getString("appId"));
|
|
|
+ sendBody.put("notifyType", 1);
|
|
|
+ sendBody.put("title", task.getTitle());
|
|
|
+ sendBody.put("content", task.getContent());
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ sendBody.put("skipType", 2);
|
|
|
+ sendBody.put("skipContent", task.getJumpUrl());
|
|
|
+ } else {
|
|
|
+ sendBody.put("skipType", 1);
|
|
|
+ }
|
|
|
+ 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 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) {
|
|
|
+ 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 CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+
|
|
|
+ private CommonPushVendorSendResult sendXiaomi(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appSecret, deviceId)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
+ 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 parseXiaomiSendResult(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 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"))) {
|
|
|
+ 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 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,
|
|
|
+ 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 CommonPushVendorSendResult sendOppo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ String authToken = obtainOppoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject notification = buildOppoNotification(task, target);
|
|
|
+ JSONObject messageRoot = new JSONObject();
|
|
|
+ messageRoot.put("target_type", 2);
|
|
|
+ messageRoot.put("target_value", deviceId);
|
|
|
+ messageRoot.put("notification", notification);
|
|
|
+
|
|
|
+ MultiValueMap<String, String> sendForm = new LinkedMultiValueMap<>();
|
|
|
+ sendForm.add("auth_token", authToken);
|
|
|
+ sendForm.add("message", messageRoot.toJSONString());
|
|
|
+
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
|
|
+ HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(sendForm, headers);
|
|
|
+ try {
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(
|
|
|
+ "https://api.push.oppomobile.com/server/v1/message/notification/unicast",
|
|
|
+ entity,
|
|
|
+ String.class
|
|
|
+ );
|
|
|
+ String body = response.getBody();
|
|
|
+ log.info("OPPO 单推响应: {}", body);
|
|
|
+ return parseOppoSendResult(body);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OPPO 单推请求异常: {}", e.getMessage(), e);
|
|
|
+ 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) {
|
|
|
+ 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");
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 表示全量用户,返回 task_id。
|
|
|
+ */
|
|
|
+ private String 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);
|
|
|
+ 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() {
|
|
|
+ return StringUtils.defaultIfBlank(commonPushProperties.getCallbackUrl(),
|
|
|
+ "https://frp-off.com:40279/alienStore/commonPushTaskUser/callback");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OPPO call_back_parameter 限 100 字符,仅传 pushTaskId + userId 短键,deviceId 由回执 registrationIds 带回。
|
|
|
+ */
|
|
|
+ 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 != null && target.getUserId() != null) {
|
|
|
+ param.put("u", target.getUserId());
|
|
|
+ }
|
|
|
+ String json = param.toJSONString();
|
|
|
+ if (json.length() > OPPO_CALLBACK_PARAM_MAX_LEN) {
|
|
|
+ log.warn("OPPO call_back_parameter 超长({}), 降级为仅 pushTaskId/taskNo", json.length());
|
|
|
+ JSONObject minimal = new JSONObject(true);
|
|
|
+ if (task.getId() != null) {
|
|
|
+ minimal.put("p", task.getId());
|
|
|
+ } else if (StringUtils.isNotBlank(task.getTaskNo())) {
|
|
|
+ String taskNo = task.getTaskNo().trim();
|
|
|
+ int maxTaskNoLen = OPPO_CALLBACK_PARAM_MAX_LEN - 7;
|
|
|
+ if (taskNo.length() > maxTaskNoLen) {
|
|
|
+ taskNo = taskNo.substring(0, maxTaskNoLen);
|
|
|
+ }
|
|
|
+ minimal.put("t", taskNo);
|
|
|
+ }
|
|
|
+ json = minimal.toJSONString();
|
|
|
+ }
|
|
|
+ if (json.length() > OPPO_CALLBACK_PARAM_MAX_LEN) {
|
|
|
+ log.error("OPPO call_back_parameter 仍超长({}): {}", json.length(), json);
|
|
|
+ }
|
|
|
+ return json;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 三星推送(Samsung Push Platform / SPP),根据 regID 前缀选择区域 RQM 节点。
|
|
|
+ */
|
|
|
+ private boolean sendSamsung(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ String appId = StringUtils.defaultIfBlank(credential.getString("appId"), credential.getString("appID"));
|
|
|
+ String appSecret = StringUtils.defaultIfBlank(credential.getString("appSecret"),
|
|
|
+ credential.getString("app_secret"));
|
|
|
+ if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
+ log.warn("三星推送凭证不完整, deviceId={}", deviceId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
+ body.put("regID", deviceId);
|
|
|
+ body.put("requestID", StringUtils.defaultIfBlank(task.getTaskNo(), "REQ" + System.currentTimeMillis()));
|
|
|
+ body.put("message", buildSamsungMessage(task));
|
|
|
+
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("appID", appId);
|
|
|
+ headers.put("appSecret", appSecret);
|
|
|
+
|
|
|
+ String url = resolveSamsungPushUrl(deviceId);
|
|
|
+ String resp = postJson(url, body.toJSONString(), headers);
|
|
|
+ log.info("三星推送响应: url={}, body={}", url, resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ return json != null && json.getIntValue("statusCode") == 1000;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildSamsungMessage(CommonPushTask task) {
|
|
|
+ StringBuilder sb = new StringBuilder("action=ALERT");
|
|
|
+ if (StringUtils.isNotBlank(task.getTitle())) {
|
|
|
+ sb.append("&alertTitle=").append(urlEncode(task.getTitle()));
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(task.getContent())) {
|
|
|
+ sb.append("&alertMessage=").append(urlEncode(task.getContent()));
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ sb.append("&uri=").append(urlEncode(task.getJumpUrl()));
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveSamsungPushUrl(String regId) {
|
|
|
+ if (StringUtils.isBlank(regId) || regId.length() < 2) {
|
|
|
+ return "https://apchina.push.samsungosp.com.cn:8090/spp/pns/api/push";
|
|
|
+ }
|
|
|
+ switch (regId.substring(0, 2)) {
|
|
|
+ case "00":
|
|
|
+ return "https://useast.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "02":
|
|
|
+ return "https://apsoutheast.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "03":
|
|
|
+ return "https://euwest.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "04":
|
|
|
+ return "https://apnortheast.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "05":
|
|
|
+ return "https://apkorea.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "06":
|
|
|
+ return "https://apchina.push.samsungosp.com.cn:8090/spp/pns/api/push";
|
|
|
+ case "50":
|
|
|
+ return "https://useast.gateway.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "52":
|
|
|
+ return "https://apsoutheast.gateway.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "53":
|
|
|
+ return "https://euwest.gateway.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "54":
|
|
|
+ return "https://apnortheast.gateway.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "55":
|
|
|
+ return "https://apkorea.gateway.push.samsungosp.com:8090/spp/pns/api/push";
|
|
|
+ case "56":
|
|
|
+ return "https://apchina.gateway.push.samsungosp.com.cn:8090/spp/pns/api/push";
|
|
|
+ default:
|
|
|
+ return "https://apchina.push.samsungosp.com.cn:8090/spp/pns/api/push";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String urlEncode(String value) {
|
|
|
+ try {
|
|
|
+ return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
|
|
|
+ } catch (Exception e) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private CommonPushVendorSendResult sendHuawei(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ return sendHuaweiLike(credential, task, Collections.singletonList(deviceId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private CommonPushVendorSendResult sendHonor(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ return sendHonorLike(credential, task, Collections.singletonList(deviceId));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 华为 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 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 CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject body = buildHuaweiMessageBody(task, pushTokens);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "Bearer " + accessToken);
|
|
|
+ // 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 CommonPushVendorSendResult sendHonorLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
+ if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ 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, pushTokens);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "Bearer " + accessToken);
|
|
|
+ String sendResp = postJson(HONOR_PUSH_API_BASE + "/v1/" + appId + "/sendMessage",
|
|
|
+ body.toJSONString(), headers);
|
|
|
+ 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 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 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", 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析华为 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) {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ return tokenJson.getString("access_token");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 华为/荣耀下行消息体: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);
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CommonPushVendorSendResult sendApns(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ if (!apnsPushClient.validateCredential(credential)) {
|
|
|
+ log.warn("APNs 凭证不完整, deviceId={}", deviceId);
|
|
|
+ return CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+ boolean ok = apnsPushClient.send(credential, task, deviceId);
|
|
|
+ return ok ? CommonPushVendorSendResult.ok(task.getTaskNo()) : CommonPushVendorSendResult.fail();
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject parseCredential(String credentialJson) {
|
|
|
+ if (StringUtils.isBlank(credentialJson)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return JSONObject.parseObject(credentialJson);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("credential_json 解析失败: {}", e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildPayload(CommonPushTask task) {
|
|
|
+ JSONObject payload = new JSONObject();
|
|
|
+ payload.put("title", task.getTitle());
|
|
|
+ payload.put("content", task.getContent());
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ payload.put("jumpUrl", task.getJumpUrl());
|
|
|
+ }
|
|
|
+ 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();
|
|
|
+ httpHeaders.setContentType(MediaType.APPLICATION_JSON);
|
|
|
+ if (headers != null) {
|
|
|
+ headers.forEach(httpHeaders::set);
|
|
|
+ }
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, new HttpEntity<>(body, httpHeaders), String.class);
|
|
|
+ return response.getBody();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String postForm(String url, MultiValueMap<String, String> form, Map<String, String> headers) {
|
|
|
+ RestTemplate restTemplate = buildRestTemplate();
|
|
|
+ HttpHeaders httpHeaders = new HttpHeaders();
|
|
|
+ httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
|
|
+ if (headers != null) {
|
|
|
+ headers.forEach(httpHeaders::set);
|
|
|
+ }
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, new HttpEntity<>(form, httpHeaders), String.class);
|
|
|
+ return response.getBody();
|
|
|
+ }
|
|
|
+
|
|
|
+ private RestTemplate buildRestTemplate() {
|
|
|
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
|
|
+ factory.setConnectTimeout(TIMEOUT_MS);
|
|
|
+ factory.setReadTimeout(TIMEOUT_MS);
|
|
|
+ return new RestTemplate(factory);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String md5Hex(String input) {
|
|
|
+ try {
|
|
|
+ MessageDigest md = MessageDigest.getInstance("MD5");
|
|
|
+ byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (byte b : digest) {
|
|
|
+ sb.append(String.format("%02x", b));
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new IllegalStateException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|