fix restemplate bug

This commit is contained in:
Jack 2025-07-19 17:42:10 +08:00
parent 2b15682b0a
commit 341342c8b9
6 changed files with 280 additions and 93 deletions

View File

@ -50,6 +50,18 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>

View File

@ -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) {
// 实现请求日志记录需配合项目日志组件
}
}

View File

@ -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<Boolean, String> 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<Boolean, String> sendPushMessageBatch(List<String> 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<String> response = executeHttpRequest(pushMessageUrl, requestBody);
// 1. 构建请求体
JSONObject requestBody = buildPushRequest(
clientIds,
StrUtil.isBlank(title) ? DEFAULT_TITLE : title,
content,
payload
);
// 处理响应结果
return processResponse(response);
// 2. 执行HTTP请求
ResponseEntity<String> 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<String> 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<String> 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<String> executeHttpRequest(String cloudFunctionUrl, JSONObject requestBody) {
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 创建请求实体
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody.toString(), headers);
// 执行POST请求
return restTemplate.exchange(
cloudFunctionUrl,
HttpMethod.POST,
requestEntity,
String.class
);
}
/**
* 处理HTTP响应
*/
private Pair<Boolean, String> processResponse(ResponseEntity<String> response) {
private Pair<Boolean, String> processPushResponse(ResponseEntity<String> 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, "响应解析失败");
}
}
}

View File

@ -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<String> 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<String> 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")

View File

@ -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<Pair < Boolean, String>>
* - Boolean: 发送是否成功
* - String: 成功为"", 失败为错误信息
*/
CompletableFuture<Pair<Boolean, String>> sendMessage(List<String> clientIds, String title, String content, JSONObject payload);
/**
* 异步发送推送通知 商户签约电子合同
*

View File

@ -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<Pair < Boolean, String>>
* - Boolean: 发送是否成功
* - String: 成功为"", 失败为错误信息
*/
@Async("asyncExecutor")
@Override
public CompletableFuture<Pair<Boolean, String>> sendMessage(List<String> 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<Boolean, String> 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));
}
}
/**
* 异步发送推送通知 商户签约电子合同
*