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)); + } + } + + /** * 异步发送推送通知 商户签约电子合同 *