|
|
@@ -0,0 +1,904 @@
|
|
|
+package shop.alien.store.service.channel;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+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;
|
|
|
+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.CommonPushTargetDto;
|
|
|
+
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 各厂商推送 HTTP 客户端,凭证从配置表 credential_json 读取。
|
|
|
+ */
|
|
|
+@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;
|
|
|
+ private static final String HUAWEI_PUSH_BASE_URL = "https://push-api.cloud.huawei.com";
|
|
|
+ private static final String HONOR_PUSH_BASE_URL = "https://push-api.cloud.honor.com";
|
|
|
+ private static final String HONOR_AUTH_URL =
|
|
|
+ "https://auth.honor.com/auth/realms/developer/protocol/openid-connect/token";
|
|
|
+
|
|
|
+ private final CommonPushProperties commonPushProperties;
|
|
|
+ private final ApnsPushClient apnsPushClient;
|
|
|
+
|
|
|
+ private static final RestTemplate restTemplate = new RestTemplate();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OPPO 全量广播推送(target_type=1),用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public boolean sendOppoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainOppoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String messageId = saveOppoMessageContent(authToken, task, null);
|
|
|
+ if (StringUtils.isBlank(messageId)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return broadcastOppoMessage(authToken, messageId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OPPO 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小米全量广播推送,用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public boolean sendXiaomiBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isBlank(appSecret)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ MultiValueMap<String, String> form = buildXiaomiMessageForm(credential, task, null);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "key=" + appSecret);
|
|
|
+ String resp = postForm("https://api.xmpush.xiaomi.com/v3/message/all", form, headers);
|
|
|
+ return isXiaomiSendSuccess(resp, task.getTaskNo(), "全量广播");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("小米广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * vivo 全量广播推送,用于 targetType=1 全量场景。
|
|
|
+ */
|
|
|
+ public boolean sendVivoBroadcast(CommonPushChannelConfig config, CommonPushTask task) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String authToken = obtainVivoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject sendBody = buildVivoNotificationBody(credential, task, null);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("authToken", authToken);
|
|
|
+ String sendResp = postJson("https://api-push.vivo.com.cn/message/all", sendBody.toJSONString(), headers);
|
|
|
+ return isVivoSendSuccess(sendResp, task.getTaskNo(), "全量广播");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("vivo 广播推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 华为全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
+ */
|
|
|
+ public boolean sendHuaweiBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ List<String> deviceIds = extractDeviceIds(targets);
|
|
|
+ if (deviceIds.isEmpty()) {
|
|
|
+ log.warn("华为全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int successBatches = 0;
|
|
|
+ int totalBatches = 0;
|
|
|
+ for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
+ totalBatches++;
|
|
|
+ if (sendHuaweiLike(credential, task, batch)) {
|
|
|
+ successBatches++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("华为全量批量推送完成, taskNo={}, devices={}, batches={}/{}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), successBatches, totalBatches);
|
|
|
+ return successBatches > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("华为全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 荣耀全量推送:官方无 /all 接口,按 token 批量下发(每批最多 1000 个)。
|
|
|
+ */
|
|
|
+ public boolean sendHonorBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ List<String> deviceIds = extractDeviceIds(targets);
|
|
|
+ if (deviceIds.isEmpty()) {
|
|
|
+ log.warn("荣耀全量推送无可用 deviceId, taskNo={}", task.getTaskNo());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ int successBatches = 0;
|
|
|
+ int totalBatches = 0;
|
|
|
+ for (List<String> batch : partitionList(deviceIds, HUAWEI_BATCH_TOKEN_LIMIT)) {
|
|
|
+ totalBatches++;
|
|
|
+ if (sendHonorLike(credential, task, batch)) {
|
|
|
+ successBatches++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("荣耀全量批量推送完成, taskNo={}, devices={}, batches={}/{}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), successBatches, totalBatches);
|
|
|
+ return successBatches > 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("荣耀全量推送失败, taskNo={}, err={}", task.getTaskNo(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * APNs 全量推送:按 iOS deviceToken 逐条下发(Apple 无 /all 接口)。
|
|
|
+ */
|
|
|
+ public boolean sendApnsBroadcast(CommonPushChannelConfig config, CommonPushTask task,
|
|
|
+ List<CommonPushTargetDto> targets) {
|
|
|
+ if (config == null || task == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null || !apnsPushClient.validateCredential(credential)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ List<CommonPushTargetDto> apnsTargets = ApnsPushClient.filterApnsTargets(targets, targets);
|
|
|
+ if (apnsTargets.isEmpty()) {
|
|
|
+ log.warn("APNs 全量推送无 iOS 设备 token, taskNo={}", task.getTaskNo());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return apnsPushClient.sendBroadcast(credential, task, apnsTargets);
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean send(CommonPushChannelConfig config, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ if (config == null || task == null || target == null || StringUtils.isBlank(target.getDeviceId())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ JSONObject credential = parseCredential(config.getCredentialJson());
|
|
|
+ if (credential == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ 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);
|
|
|
+ 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 false;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("渠道推送失败, channelCode={}, deviceId={}, err={}", channelCode, deviceId, e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean sendVivo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ if (StringUtils.isBlank(deviceId)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String authToken = obtainVivoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ JSONObject sendBody = buildVivoNotificationBody(credential, task, target);
|
|
|
+ sendBody.put("regId", deviceId);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("authToken", authToken);
|
|
|
+ String sendResp = postJson("https://api-push.vivo.com.cn/message/send", sendBody.toJSONString(), headers);
|
|
|
+ return isVivoSendSuccess(sendResp, task.getTaskNo(), "单推");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainVivoAuthToken(JSONObject credential) {
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appKey = credential.getString("appKey");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appId, appKey, appSecret)) {
|
|
|
+ return 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 boolean isVivoSendSuccess(String resp, String taskNo, String scene) {
|
|
|
+ log.info("vivo{}响应: {}", scene, resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json != null && json.getIntValue("result") == 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (json != null) {
|
|
|
+ log.error("vivo{}失败, taskNo={}, result={}, desc={}",
|
|
|
+ scene, taskNo, json.getIntValue("result"), json.getString("desc"));
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean sendXiaomi(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appSecret, deviceId)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!validateXiaomiCredential(credential, task.getTaskNo())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ 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(), "单推");
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean validateXiaomiCredential(JSONObject credential, String taskNo) {
|
|
|
+ String channelId = resolveXiaomiChannelId(credential);
|
|
|
+ if (StringUtils.isBlank(channelId)) {
|
|
|
+ log.error("小米推送缺少 channelId, taskNo={}。小米运营平台已创建 channel 后,还需将 channel_id 写入本系统 common_push_channel_config.credentialJson.channelId",
|
|
|
+ taskNo);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(credential.getString("packageName"))) {
|
|
|
+ log.warn("小米推送未配置 packageName, taskNo={}, channelId={},请确认与小米应用信息中的包名一致",
|
|
|
+ taskNo, channelId);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveXiaomiChannelId(JSONObject credential) {
|
|
|
+ if (credential == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return StringUtils.defaultIfBlank(
|
|
|
+ StringUtils.trimToNull(credential.getString("channelId")),
|
|
|
+ StringUtils.trimToNull(credential.getString("channel_id")));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isXiaomiSendSuccess(String resp, String taskNo, String scene) {
|
|
|
+ log.info("小米{}响应: {}", scene, resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json != null && "ok".equalsIgnoreCase(json.getString("result"))) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (json != null) {
|
|
|
+ log.error("小米{}失败, taskNo={}, code={}, reason={}, description={}",
|
|
|
+ scene, taskNo, json.getString("code"), json.getString("reason"), json.getString("description"));
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private MultiValueMap<String, String> buildXiaomiMessageForm(JSONObject credential, CommonPushTask task,
|
|
|
+ CommonPushTargetDto target) {
|
|
|
+ MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ form.add("title", task.getTitle());
|
|
|
+ form.add("description", task.getContent());
|
|
|
+ form.add("payload", buildPayload(task));
|
|
|
+ String packageName = credential.getString("packageName");
|
|
|
+ if (StringUtils.isNotBlank(packageName)) {
|
|
|
+ form.add("restricted_package_name", packageName.trim());
|
|
|
+ } else {
|
|
|
+ log.warn("小米推送未配置 packageName,多包名应用可能返回 22022 或 27001");
|
|
|
+ }
|
|
|
+ appendXiaomiExtraParams(form, credential, task, target);
|
|
|
+ return form;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小米 HTTP API 要求以 extra.xxx 形式传参(非 extra JSON 字符串)。
|
|
|
+ */
|
|
|
+ private void appendXiaomiExtraParams(MultiValueMap<String, String> form, JSONObject credential,
|
|
|
+ CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String channelId = resolveXiaomiChannelId(credential);
|
|
|
+ if (StringUtils.isNotBlank(channelId)) {
|
|
|
+ form.add("extra.channel_id", channelId);
|
|
|
+ }
|
|
|
+ form.add("extra.callback", resolveCallbackUrl());
|
|
|
+ form.add("extra.callback.param", buildXiaomiCallbackParameter(task, target));
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ form.add("extra.notify_effect", "3");
|
|
|
+ form.add("extra.web_uri", task.getJumpUrl());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildXiaomiCallbackParameter(CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ JSONObject param = new JSONObject(true);
|
|
|
+ if (task.getId() != null) {
|
|
|
+ param.put("p", task.getId());
|
|
|
+ } else if (StringUtils.isNotBlank(task.getTaskNo())) {
|
|
|
+ param.put("t", task.getTaskNo().trim());
|
|
|
+ }
|
|
|
+ if (target != null && target.getUserId() != null) {
|
|
|
+ param.put("u", target.getUserId());
|
|
|
+ }
|
|
|
+ param.put("c", "xiaomi");
|
|
|
+ return param.toJSONString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OPPO 厂商直连:使用 RegistrationID(非 UniPush CID),并设置送达回执回调。
|
|
|
+ */
|
|
|
+ private boolean sendOppo(JSONObject credential, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ String deviceId = target.getDeviceId();
|
|
|
+ String authToken = obtainOppoAuthToken(credential);
|
|
|
+ if (StringUtils.isBlank(authToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ JSONObject resp = JSONObject.parseObject(body);
|
|
|
+ return resp != null && resp.getIntValue("code") == 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OPPO 单推请求异常: {}", e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainOppoAuthToken(JSONObject credential) {
|
|
|
+ String appKey = credential.getString("appKey");
|
|
|
+ String masterSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appKey, masterSecret)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ long timestamp = System.currentTimeMillis();
|
|
|
+ String sign = cn.hutool.crypto.SecureUtil.sha256(appKey + timestamp + masterSecret);
|
|
|
+ MultiValueMap<String, String> authForm = new LinkedMultiValueMap<>();
|
|
|
+ authForm.add("app_key", appKey);
|
|
|
+ authForm.add("timestamp", String.valueOf(timestamp));
|
|
|
+ authForm.add("sign", sign);
|
|
|
+ String authResp = postForm("https://api.push.oppomobile.com/server/v1/auth", authForm, null);
|
|
|
+ JSONObject authJson = JSONObject.parseObject(authResp);
|
|
|
+ if (authJson == null || authJson.getJSONObject("data") == null) {
|
|
|
+ log.warn("OPPO 鉴权失败: {}", authResp);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return authJson.getJSONObject("data").getString("auth_token");
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject buildOppoNotification(CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ JSONObject notification = new JSONObject();
|
|
|
+ notification.put("title", task.getTitle());
|
|
|
+ notification.put("content", task.getContent());
|
|
|
+ notification.put("call_back_url", resolveCallbackUrl());
|
|
|
+ notification.put("call_back_parameter", buildCallbackParameter(task, target));
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ notification.put("click_action_url", task.getJumpUrl());
|
|
|
+ }
|
|
|
+ return notification;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存 OPPO 广播消息体,返回 message_id。
|
|
|
+ */
|
|
|
+ private String saveOppoMessageContent(String authToken, CommonPushTask task, CommonPushTargetDto target) {
|
|
|
+ MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ form.add("auth_token", authToken);
|
|
|
+ form.add("title", task.getTitle());
|
|
|
+ form.add("content", task.getContent());
|
|
|
+ form.add("call_back_url", resolveCallbackUrl());
|
|
|
+ form.add("call_back_parameter", buildCallbackParameter(task, target));
|
|
|
+ if (StringUtils.isNotBlank(task.getJumpUrl())) {
|
|
|
+ form.add("click_action_url", task.getJumpUrl());
|
|
|
+ }
|
|
|
+ String resp = postForm("https://api.push.oppomobile.com/server/v1/message/notification/save_message_content", form, null);
|
|
|
+ log.info("OPPO 保存广播消息体响应: {}", resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ if (json == null || json.getJSONObject("data") == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return json.getJSONObject("data").getString("message_id");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OPPO 广播下发,target_type=1 表示全量用户。
|
|
|
+ */
|
|
|
+ private boolean broadcastOppoMessage(String authToken, String messageId) {
|
|
|
+ MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
|
|
+ form.add("auth_token", authToken);
|
|
|
+ form.add("message_id", messageId);
|
|
|
+ form.add("target_type", "1");
|
|
|
+ String resp = postForm("https://api.push.oppomobile.com/server/v1/message/notification/broadcast", form, null);
|
|
|
+ log.info("OPPO 广播推送响应: {}", resp);
|
|
|
+ JSONObject json = JSONObject.parseObject(resp);
|
|
|
+ return json != null && json.getIntValue("code") == 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveCallbackUrl() {
|
|
|
+ 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 boolean sendHuawei(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ return sendHuaweiLike(credential, task, Collections.singletonList(deviceId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean sendHonor(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ return sendHonorLike(credential, task, Collections.singletonList(deviceId));
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean sendHuaweiLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
+ if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String accessToken = obtainHuaweiAccessToken(credential);
|
|
|
+ if (StringUtils.isBlank(accessToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject body = buildHuaweiMessageBody(task, deviceIds);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "Bearer " + accessToken);
|
|
|
+ String sendResp = postJson(HUAWEI_PUSH_BASE_URL + "/v1/" + appId + "/messages:send",
|
|
|
+ body.toJSONString(), headers);
|
|
|
+ JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
+ boolean ok = sendJson != null && StringUtils.isNotBlank(sendJson.getString("requestId"));
|
|
|
+ if (!ok) {
|
|
|
+ log.error("华为推送失败, taskNo={}, deviceCount={}, resp={}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), sendResp);
|
|
|
+ }
|
|
|
+ return ok;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean sendHonorLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
|
|
|
+ if (deviceIds == null || deviceIds.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ if (StringUtils.isAnyBlank(appId, appSecret)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String accessToken = obtainHonorAccessToken(credential);
|
|
|
+ if (StringUtils.isBlank(accessToken)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject body = buildHuaweiMessageBody(task, deviceIds);
|
|
|
+ Map<String, String> headers = new HashMap<>();
|
|
|
+ headers.put("Authorization", "Bearer " + accessToken);
|
|
|
+ String sendResp = postJson(HONOR_PUSH_BASE_URL + "/v1/" + appId + "/messages:send",
|
|
|
+ body.toJSONString(), headers);
|
|
|
+ JSONObject sendJson = JSONObject.parseObject(sendResp);
|
|
|
+ boolean ok = sendJson != null && StringUtils.isNotBlank(sendJson.getString("requestId"));
|
|
|
+ if (!ok) {
|
|
|
+ log.error("荣耀推送失败, taskNo={}, deviceCount={}, resp={}",
|
|
|
+ task.getTaskNo(), deviceIds.size(), sendResp);
|
|
|
+ }
|
|
|
+ return ok;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainHuaweiAccessToken(JSONObject credential) {
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ MultiValueMap<String, String> tokenForm = new LinkedMultiValueMap<>();
|
|
|
+ tokenForm.add("grant_type", "client_credentials");
|
|
|
+ tokenForm.add("client_id", appId);
|
|
|
+ tokenForm.add("client_secret", appSecret);
|
|
|
+ String tokenResp = postForm(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);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return tokenJson.getString("access_token");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String obtainHonorAccessToken(JSONObject credential) {
|
|
|
+ String appId = credential.getString("appId");
|
|
|
+ String appSecret = credential.getString("appSecret");
|
|
|
+ MultiValueMap<String, String> tokenForm = new LinkedMultiValueMap<>();
|
|
|
+ tokenForm.add("grant_type", "client_credentials");
|
|
|
+ tokenForm.add("client_id", appId);
|
|
|
+ tokenForm.add("client_secret", appSecret);
|
|
|
+ String tokenResp = postForm(HONOR_AUTH_URL, tokenForm, null);
|
|
|
+ JSONObject tokenJson = JSONObject.parseObject(tokenResp);
|
|
|
+ if (tokenJson == null || StringUtils.isBlank(tokenJson.getString("access_token"))) {
|
|
|
+ log.warn("荣耀鉴权失败: {}", tokenResp);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return tokenJson.getString("access_token");
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject buildHuaweiMessageBody(CommonPushTask task, List<String> deviceIds) {
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
+ JSONObject message = new JSONObject();
|
|
|
+ JSONObject android = new JSONObject();
|
|
|
+ JSONObject notification = new JSONObject();
|
|
|
+ notification.put("title", task.getTitle());
|
|
|
+ notification.put("body", task.getContent());
|
|
|
+ android.put("notification", notification);
|
|
|
+ message.put("android", android);
|
|
|
+ body.put("message", message);
|
|
|
+ body.put("token", deviceIds.toArray(new String[0]));
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 boolean sendApns(JSONObject credential, CommonPushTask task, String deviceId) {
|
|
|
+ if (!apnsPushClient.validateCredential(credential)) {
|
|
|
+ log.warn("APNs 凭证不完整, deviceId={}", deviceId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return apnsPushClient.send(credential, task, deviceId);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|