diff --git a/mall-common/pom.xml b/mall-common/pom.xml
index f9b531ed..626e9b4c 100644
--- a/mall-common/pom.xml
+++ b/mall-common/pom.xml
@@ -50,6 +50,18 @@
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.9.3
+
+
+
+ org.springframework.retry
+ spring-retry
+
+
net.logstash.logback
logstash-logback-encoder
diff --git a/mall-common/src/main/java/com/suisung/mall/common/config/RestTemplateConfig.java b/mall-common/src/main/java/com/suisung/mall/common/config/RestTemplateConfig.java
new file mode 100644
index 00000000..9da170dd
--- /dev/null
+++ b/mall-common/src/main/java/com/suisung/mall/common/config/RestTemplateConfig.java
@@ -0,0 +1,64 @@
+package com.suisung.mall.common.config;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
+import org.springframework.retry.backoff.FixedBackOffPolicy;
+import org.springframework.retry.policy.SimpleRetryPolicy;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.Duration;
+
+/**
+ * RestTemplate 企业级配置
+ * 兼容 Spring Boot 2.3.0 版本
+ */
+@Configuration
+public class RestTemplateConfig {
+
+ /**
+ * 企业级 RestTemplate 配置
+ * 1. 使用 OkHttp3 客户端(性能优化)
+ * 2. 配置连接超时(5秒)和读取超时(10秒)
+ * 3. 添加请求日志拦截器
+ * 4. 兼容 Spring Boot 2.3.0 的 API
+ */
+ @Bean
+ public RestTemplate restTemplate(RestTemplateBuilder builder) {
+ return builder
+ .requestFactory(OkHttp3ClientHttpRequestFactory::new)
+ .setConnectTimeout(Duration.ofSeconds(15)) // 连接超时5秒
+ .setReadTimeout(Duration.ofSeconds(25)) // 读取超时10秒
+ .additionalInterceptors((request, body, execution) -> {
+ logRequest(request); // 请求日志记录
+ return execution.execute(request, body);
+ })
+ .build();
+ }
+
+ /**
+ * 重试机制配置(兼容 Spring 2.3)
+ * 1. 最多重试3次
+ * 2. 每次间隔1秒
+ */
+ @Bean
+ public RetryTemplate retryTemplate() {
+ FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
+ backOffPolicy.setBackOffPeriod(1000); // 1秒间隔
+
+ SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
+ retryPolicy.setMaxAttempts(3); // 最大重试3次
+
+ RetryTemplate template = new RetryTemplate();
+ template.setBackOffPolicy(backOffPolicy);
+ template.setRetryPolicy(retryPolicy);
+ return template;
+ }
+
+ private void logRequest(HttpRequest request) {
+ // 实现请求日志记录(需配合项目日志组件)
+ }
+}
diff --git a/mall-common/src/main/java/com/suisung/mall/common/service/impl/UniCloudPushServiceImpl.java b/mall-common/src/main/java/com/suisung/mall/common/service/impl/UniCloudPushServiceImpl.java
index 8fe0d52d..37bf2d33 100644
--- a/mall-common/src/main/java/com/suisung/mall/common/service/impl/UniCloudPushServiceImpl.java
+++ b/mall-common/src/main/java/com/suisung/mall/common/service/impl/UniCloudPushServiceImpl.java
@@ -1,10 +1,4 @@
-/*
- * Copyright (c) 2025. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan.
- * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna.
- * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus.
- * Vestibulum commodo. Ut rhoncus gravida arcu.
- */
+// ... existing copyright notice ...
package com.suisung.mall.common.service.impl;
@@ -26,15 +20,20 @@ import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
/**
- * 调用 UniCloud 云函数 推送服务
- * 提供向多个客户端 ID 发送推送消息的功能
+ * UniCloud 推送服务实现类
+ * 提供向客户端发送推送消息的功能
*/
@Slf4j
@Service
public class UniCloudPushServiceImpl implements UniCloudPushService {
+ private static final int MAX_CLIENT_IDS = 500;
+ private static final String DEFAULT_TITLE = "小发同城:您有新的消息";
+ private static final long DEFAULT_TTL = 3 * 24 * 3600 * 1000L; // 3天有效期
+
@Lazy
@Resource
private RestTemplate restTemplate;
@@ -45,132 +44,147 @@ public class UniCloudPushServiceImpl implements UniCloudPushService {
/**
* 向单个客户端发送推送消息
*
- * @param clientId 客户端 ID
+ * @param clientId 客户端唯一标识
* @param title 推送标题
* @param content 推送内容
- * @param payload 推送内容
- * @return
+ * @param payload 附加数据(JSON格式)
+ * @return Pair<是否成功, 结果描述>
*/
+ @Override
public Pair sendPushMessage(String clientId, String title, String content, JSONObject payload) {
+ if (StrUtil.isBlank(clientId)) {
+ log.warn("[推送服务] 客户端ID不能为空");
+ return Pair.of(false, "客户端ID不能为空");
+ }
return sendPushMessageBatch(Collections.singletonList(clientId), title, content, payload);
}
/**
- * 向多个客户端ID批量发送推送消息
+ * 批量发送推送消息
*
- * @param clientIds 目标客户端 ID 列表 注意:超过500个,直接忽略 必填项
- * @param title 推送标题 可选项
- * @param content 推送内容 必填项
- * @param payload 附加数据(JSON对象) 可选项
- * @return 推送结果响应对象
+ * @param clientIds 客户端ID列表(最多500个)
+ * @param title 推送标题
+ * @param content 推送内容
+ * @param payload 附加数据(JSON格式)
+ * @return Pair<是否成功, 结果描述>
*/
@Override
public Pair sendPushMessageBatch(List clientIds, String title, String content, JSONObject payload) {
- if (StrUtil.isBlank(pushMessageUrl) || CollUtil.isEmpty(clientIds) || StrUtil.isBlank(content)) {
- return Pair.of(false, "缺少必要参数");
- }
-
- if (clientIds.size() > 500) {
- log.warn("批量推送消息时,CIDs 数量超过限制");
- return Pair.of(false, "推送客户端超出500限额");
- }
-
- if (StrUtil.isBlank(title)) {
- title = "小发同城:您有新的消息";
+ // === 参数校验 ===
+ if (StrUtil.isBlank(pushMessageUrl)) {
+ log.error("[推送服务] 推送URL未配置");
+ return Pair.of(false, "推送服务未配置");
+ }
+ if (CollUtil.isEmpty(clientIds)) {
+ log.warn("[推送服务] 客户端ID列表为空");
+ return Pair.of(false, "客户端ID列表不能为空");
+ }
+ if (StrUtil.isBlank(content)) {
+ log.warn("[推送服务] 推送内容为空");
+ return Pair.of(false, "推送内容不能为空");
+ }
+ if (clientIds.size() > MAX_CLIENT_IDS) {
+ log.warn("[推送服务] 客户端ID数量超过限制: {}", clientIds.size());
+ return Pair.of(false, "客户端ID数量超过500限制");
}
+ // === 业务处理 ===
try {
- // 构建请求体
- JSONObject requestBody = buildPushRequest(clientIds, title, content, payload);
+ log.debug("[推送服务] 开始处理推送请求, 客户端数量: {}", clientIds.size());
- // 执行HTTP请求
- ResponseEntity response = executeHttpRequest(pushMessageUrl, requestBody);
+ // 1. 构建请求体
+ JSONObject requestBody = buildPushRequest(
+ clientIds,
+ StrUtil.isBlank(title) ? DEFAULT_TITLE : title,
+ content,
+ payload
+ );
- // 处理响应结果
- return processResponse(response);
+ // 2. 执行HTTP请求
+ ResponseEntity response = restTemplate.exchange(
+ pushMessageUrl,
+ HttpMethod.POST,
+ new HttpEntity<>(requestBody.toString(), buildHeaders()),
+ String.class
+ );
+
+ // 3. 处理响应
+ return processPushResponse(response);
} catch (RestClientException e) {
- // 处理REST客户端异常
- log.error("推送请求发送失败:{}", e.getMessage(), e);
- return Pair.of(false, "推送请求发送失败");
+ log.error("[推送服务] REST请求异常: {}", e.getMessage(), e);
+ return Pair.of(false, "推送请求失败");
} catch (Exception e) {
- // 处理其他异常
- log.error("推送处理过程发生异常:{}", e.getMessage(), e);
- return Pair.of(false, "推送处理过程发生异常");
+ log.error("[推送服务] 处理异常: {}", e.getMessage(), e);
+ return Pair.of(false, "推送处理异常");
}
}
+ /**
+ * 构建请求头
+ */
+ private HttpHeaders buildHeaders() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ return headers;
+ }
+
/**
* 构建推送请求体
*/
private JSONObject buildPushRequest(List clientIds, String title, String content, JSONObject payload) {
- JSONObject requestBody = new JSONObject();
- requestBody.put("action", "push");
- requestBody.put("cidList", clientIds);
+ JSONObject request = new JSONObject();
- JSONObject message = new JSONObject();
- message.put("title", title);
- message.put("content", content);
+ // 新增:客户端ID去重
+ List distinctClientIds = clientIds.stream().distinct().collect(Collectors.toList());
+ if (distinctClientIds.size() != clientIds.size()) {
+ log.warn("[推送服务] 客户端ID列表存在重复,已自动去重。原数量:{},去重后数量:{}",
+ clientIds.size(), distinctClientIds.size());
+ }
+
+ request.put("push_clientid", distinctClientIds)
+ .put("title", title)
+ .put("content", content);
if (payload != null) {
- message.put("payload", payload);
+ request.put("payload", payload);
}
- requestBody.put("message", message);
-
- JSONArray platform = new JSONArray();
- platform.add("web");
- platform.add("app-ios");
- platform.add("app-android");
- platform.add("mp-weixin");
- requestBody.put("platform", platform);
+ // 设置平台和有效期
+ JSONArray platforms = new JSONArray();
+ platforms.add("web");
+ platforms.add("app-ios");
+ platforms.add("app-android");
+ platforms.add("mp-weixin");
+ request.put("platform", platforms)
+ .put("settings", new JSONObject().set("ttl", DEFAULT_TTL));
- //消息有效期设置,单位毫秒,-1表示不设离线。默认是 2 小时,取值范围:-1 ~ 3 * 24 * 3600 * 1000(3天)之间
- requestBody.put("settings", new JSONObject().set("ttl", 3 * 24 * 3600 * 1000));
-
- return requestBody;
+ return request;
}
/**
- * 执行HTTP POST请求
+ * 处理推送响应
*/
- private ResponseEntity executeHttpRequest(String cloudFunctionUrl, JSONObject requestBody) {
- // 设置请求头
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
-
- // 创建请求实体
- HttpEntity requestEntity = new HttpEntity<>(requestBody.toString(), headers);
-
- // 执行POST请求
- return restTemplate.exchange(
- cloudFunctionUrl,
- HttpMethod.POST,
- requestEntity,
- String.class
- );
- }
-
- /**
- * 处理HTTP响应
- */
- private Pair processResponse(ResponseEntity response) {
+ private Pair processPushResponse(ResponseEntity response) {
if (response.getStatusCode() != HttpStatus.OK) {
- return Pair.of(false, String.format("推送请求失败,状态码: %d,响应: %s",
- response.getStatusCodeValue(),
- response.getBody()));
+ log.error("[推送服务] 请求失败, 状态码: {}", response.getStatusCodeValue());
+ return Pair.of(false, "推送请求失败");
}
- String responseBody = response.getBody();
- if (responseBody == null || responseBody.trim().isEmpty()) {
- return Pair.of(false, "推送响应内容为空");
+ String body = response.getBody();
+ if (StrUtil.isBlank(body)) {
+ log.warn("[推送服务] 响应体为空");
+ return Pair.of(false, "推送响应为空");
}
- // 解析响应JSON
- JSONObject responseJson = JSONUtil.parseObj(responseBody);
- return Pair.of(true, String.format("推送请求成功,状态码: %d,响应: %s",
- responseJson.getInt("code"),
- responseJson.getJSONObject("data").toString()));
+ try {
+ JSONObject json = JSONUtil.parseObj(body);
+ log.info("[推送服务] 推送成功, 响应: {}", json.toStringPretty());
+ return Pair.of(true, json.getJSONObject("data").toString());
+ } catch (Exception e) {
+ log.error("[推送服务] 响应解析异常: {}", e.getMessage(), e);
+ return Pair.of(false, "响应解析失败");
+ }
}
}
diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/lakala/controller/mobile/LakalaController.java b/mall-shop/src/main/java/com/suisung/mall/shop/lakala/controller/mobile/LakalaController.java
index 9cdb8410..976b13cc 100644
--- a/mall-shop/src/main/java/com/suisung/mall/shop/lakala/controller/mobile/LakalaController.java
+++ b/mall-shop/src/main/java/com/suisung/mall/shop/lakala/controller/mobile/LakalaController.java
@@ -9,10 +9,12 @@
package com.suisung.mall.shop.lakala.controller.mobile;
import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
import com.suisung.mall.common.api.CommonResult;
import com.suisung.mall.common.service.impl.BaseControllerImpl;
import com.suisung.mall.shop.lakala.service.LakalaApiService;
import com.suisung.mall.shop.library.service.LibraryProductService;
+import com.suisung.mall.shop.message.service.PushMessageService;
import com.suisung.mall.shop.order.service.ShopOrderReturnService;
import com.suisung.mall.shop.store.service.ShopStoreSameCityTransportBaseService;
import io.swagger.annotations.Api;
@@ -26,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
+import java.util.List;
@Api(tags = "拉卡拉相关接口 - 前端控制器")
@RestController
@@ -44,6 +47,9 @@ public class LakalaController extends BaseControllerImpl {
@Resource
private ShopStoreSameCityTransportBaseService storeSameCityTransportBaseService;
+ @Resource
+ private PushMessageService pushMessageService;
+
@ApiOperation(value = "测试案例", notes = "测试案例")
@RequestMapping(value = "/testcase", method = RequestMethod.POST)
public Object testcase(@RequestBody JSONObject paramsJSON) {
@@ -62,6 +68,19 @@ public class LakalaController extends BaseControllerImpl {
//// tags.add("放心");
// return libraryProductService.matchLibraryProducts(paramsJSON.getStr("barcode"), paramsJSON.getStr("productName"), tags);
+ // 测试推送消息
+// List clientIds = JSONUtil.toList(paramsJSON.getJSONArray("clientIds"), String.class);
+// return pushMessageService.sendMessage(clientIds, paramsJSON.getStr("title"), paramsJSON.getStr("content"), paramsJSON.getJSONObject("payload"));
+
+ }
+
+ @ApiOperation(value = "批量发送推送消息 - 测试案例", notes = "批量发送推送消息 - 测试案例")
+ @PostMapping(value = "/unipush/testcase")
+ public Object pushMessageTestCase(@RequestBody JSONObject paramsJSON) {
+ // 测试推送消息
+ List clientIds = JSONUtil.toList(paramsJSON.getJSONArray("clientIds"), String.class);
+ return pushMessageService.sendMessage(clientIds, paramsJSON.getStr("title"), paramsJSON.getStr("content"), paramsJSON.getJSONObject("payload"));
+
}
@ApiOperation(value = "本地文件转base64", notes = "本地文件转base64")
diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/message/service/PushMessageService.java b/mall-shop/src/main/java/com/suisung/mall/shop/message/service/PushMessageService.java
index acdae895..fa50c336 100644
--- a/mall-shop/src/main/java/com/suisung/mall/shop/message/service/PushMessageService.java
+++ b/mall-shop/src/main/java/com/suisung/mall/shop/message/service/PushMessageService.java
@@ -9,11 +9,34 @@
package com.suisung.mall.shop.message.service;
import cn.hutool.json.JSONObject;
+import org.springframework.data.util.Pair;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface PushMessageService {
+ /**
+ * 批量发送推送消息
+ * === 功能说明 ===
+ * 1. 异步批量发送推送消息
+ * 2. 支持自定义标题、内容和附加数据
+ * 3. 返回发送结果和错误信息
+ * === 技术实现 ===
+ * - 使用@Async实现异步发送
+ * - 通过uniCloudPushService批量发送
+ * - 记录详细日志便于问题追踪
+ *
+ * @param clientIds 客户端ID列表 (必填)
+ * @param title 消息标题 (可选)
+ * @param content 消息内容 (必填)
+ * @param payload 附加数据 (可为空)
+ * @return CompletableFuture>
+ * - Boolean: 发送是否成功
+ * - String: 成功为"", 失败为错误信息
+ */
+ CompletableFuture> sendMessage(List clientIds, String title, String content, JSONObject payload);
+
/**
* 异步发送推送通知 商户签约电子合同
*
diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/message/service/impl/PushMessageServiceImpl.java b/mall-shop/src/main/java/com/suisung/mall/shop/message/service/impl/PushMessageServiceImpl.java
index 6cee360d..f924a5b2 100644
--- a/mall-shop/src/main/java/com/suisung/mall/shop/message/service/impl/PushMessageServiceImpl.java
+++ b/mall-shop/src/main/java/com/suisung/mall/shop/message/service/impl/PushMessageServiceImpl.java
@@ -8,6 +8,7 @@
package com.suisung.mall.shop.message.service.impl;
+import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.suisung.mall.common.feignService.AccountService;
import com.suisung.mall.common.modules.account.AccountUserBindGeTui;
@@ -19,6 +20,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.data.util.Pair;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List;
@@ -43,6 +45,59 @@ public class PushMessageServiceImpl implements PushMessageService {
@Resource
private AccountService accountService;
+ /**
+ * 批量发送推送消息
+ * === 功能说明 ===
+ * 1. 异步批量发送推送消息
+ * 2. 支持自定义标题、内容和附加数据
+ * 3. 返回发送结果和错误信息
+ * === 技术实现 ===
+ * - 使用@Async实现异步发送
+ * - 通过uniCloudPushService批量发送
+ * - 记录详细日志便于问题追踪
+ *
+ * @param clientIds 客户端ID列表 (必填)
+ * @param title 消息标题 (可选)
+ * @param content 消息内容 (必填)
+ * @param payload 附加数据 (可为空)
+ * @return CompletableFuture>
+ * - Boolean: 发送是否成功
+ * - String: 成功为"", 失败为错误信息
+ */
+ @Async("asyncExecutor")
+ @Override
+ public CompletableFuture> sendMessage(List clientIds, String title, String content, JSONObject payload) {
+ // 参数校验
+ if (CollectionUtils.isEmpty(clientIds) || StrUtil.isBlank(content)) {
+ log.warn("推送消息参数无效 - clientIds:{}, title:{}, content:{}", clientIds, title, content);
+ return CompletableFuture.completedFuture(Pair.of(false, "推送消息参数无效"));
+ }
+
+ try {
+ if (StrUtil.isBlank(title)) {
+ title = "小发同城:您有新的消息";
+ }
+
+ log.debug("开始发送推送消息 - clientIds:{}, title:{}", clientIds, title);
+ Pair result = uniCloudPushService.sendPushMessageBatch(clientIds, title, content, payload);
+
+ if (!result.getFirst()) {
+ log.error("推送消息失败 - clientIds:{}, error:{}", clientIds, result.getSecond());
+ } else {
+ log.debug("推送消息成功 - clientIds:{}", clientIds);
+ }
+
+ return CompletableFuture.completedFuture(result);
+
+ } catch (Exception e) {
+ String errorMsg = "推送消息系统异常";
+ log.error("{} - clientIds:{}, title:{}, content:{}, payload:{}",
+ errorMsg, clientIds, title, content, payload, e);
+ return CompletableFuture.completedFuture(Pair.of(false, errorMsg));
+ }
+ }
+
+
/**
* 异步发送推送通知 商户签约电子合同
*