一、业务层代码
public class CrmBindCustomerPushService {
private static final String BIZ_TYPE = "bind";
private static final String PRIVATE_KEY = "MIIEvQIBADANBgkqhkiG9w0BAQEFAA...."
@Resource
private CrmSyncLogMapper syncLogMapper;
@Resource
private CrmCustomerBindMapper bindMapper;
@Resource
private MessageRetryHandler messageRetryHandler;
public String syncBindPush() throws Exception {
// 1️ 获取同步记录
CrmSyncLogDO logRecord = syncLogMapper.selectByBizType(BIZ_TYPE);
if (logRecord == null) {
log.warn("未找到同步记录,将初始化日志记录。");
syncLogMapper.insertInit(BIZ_TYPE);
logRecord = syncLogMapper.selectByBizType(BIZ_TYPE);
}
// 2️ 处理首次同步(lastSyncTime 为空的情况)
LocalDateTime lastSyncTime = logRecord.getLastSyncTime();
if (lastSyncTime == null) {
lastSyncTime = LocalDateTime.of(2025, 11, 3, 9, 50, 50);
log.info("首次执行增量同步,将从 {} 开始查询。", lastSyncTime);
}
// 3 查询增量数据
List<CrmBindCustomerPush> dataList = posBindMapper.selectIncrementalData(lastSyncTime);
if (dataList.isEmpty()) {
log.info("没有需要推送的增量数据。");
return "无增量数据同步";
}
log.info("开始增量推送,共 {} 条记录(起点时间:{})", dataList.size(), lastSyncTime);
// 4️ 推送
boolean result = pushIncrementalWithSign(dataList);
// 5️ 更新同步日志
CrmSyncLogDO update = new CrmSyncLogDO();
update.setBizType(BIZ_TYPE);
update.setLastSyncTime(LocalDateTime.now());
update.setStatus(result ? "SUCCESS" : "FAIL");
update.setRemark("增量推送");
update.setRetryCount(logRecord.getRetryCount() + 1);
syncLogMapper.updateStatus(update);
log.info("增量推送结束,结果:{}", result ? "成功" : "失败");
return "同步结果:"+ result;
}
/**
* 带签名的增量推送逻辑(每条单独推送)
*/
private boolean pushIncrementalWithSign(List<CrmBindCustomerPush> dataList) throws Exception {
String url = "http://localhost:8080/api/getBindData";
boolean allSuccess = true;
for (CrmBindCustomerPush item : dataList) {
String json = JSON.toJSONString(item);
String sign = RsaSignVerifyDemo.sign(json, PRIVATE_KEY);
Map<String, Object> payload = new HashMap<>();
payload.put("data", item);
payload.put("sign", sign);
String jsonStr = JSON.toJSONString(payload);
try {
String response = HttpUtil.post(url, jsonStr);
log.info("机构响应: {}", response);
if (!response.contains("\"ret_code\":\"00\"")) {
log.warn("响应不正确,发送到重试队列: {}", json);
// 使用注入的 messageRetryHandler 添加待重试消息
String messageId = UUID.randomUUID().toString();
messageRetryHandler.addPendingMessage(messageId, url, jsonStr, item,"CRM_BIND_PUSH_RETRY_TOPIC");
allSuccess = false;
}
} catch (Exception e) {
log.warn("推送异常,发送到重试队列: {}", json, e);
// 异常情况也使用 messageRetryHandler 添加待重试消息
String messageId = UUID.randomUUID().toString();
messageRetryHandler.addPendingMessage(messageId, url, jsonStr, item);
allSuccess = false;
}
}
return allSuccess;
}
}
二、消息重试处理器
@Component
@Slf4j
public class MessageRetryHandler {
// 重发间隔配置(秒):1s/10s/1m/3m/5m/10m/20m/30m/1h/2h
private static final int[] RETRY_INTERVALS = {1, 10, 60, 180, 300, 600, 1200, 1800, 3600, 7200};
private static final int MAX_RETRY = 10;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private CrmPushFailedMessageMapper crmPushFailedMessageMapper;
/**
* 处理消息重试
*/
public boolean handleRetry(RetryMessage retryMsg) {
try {
String response = HttpUtil.post(retryMsg.getUrl(), retryMsg.getPayload());
if (isValidResponse(response)) {
log.info("消息重发成功,消息ID: {}", retryMsg.getMessageId());
return true;
}
} catch (Exception e) {
log.warn("消息重发失败,消息ID: {}, 重试次数: {}",
retryMsg.getMessageId(), retryMsg.getRetryCount(), e);
}
retryMsg.incrementRetryCount();
if (retryMsg.getRetryCount() >= MAX_RETRY) {
handleMaxRetriesExceeded(retryMsg);
return true;
}
// 发送到 RocketMQ 延迟队列进行重试
// 发送到延迟队列,使用消息中指定的topic
try {
rocketMQTemplate.syncSend(retryMsg.getTopic(),
MessageBuilder.withPayload(JSON.toJSONString(retryMsg)).build(),
3000,
getDelayLevel(retryMsg.getRetryCount()));
log.info("消息发送到延迟队列,消息ID: {}, Topic: {}, 延迟级别: {}", retryMsg.getMessageId(), retryMsg.getTopic(), getDelayLevel(retryMsg.getRetryCount()));
} catch (Exception e) {
log.error("发送消息到延迟队列失败,消息ID: {}, Topic: {}", retryMsg.getMessageId(), retryMsg.getTopic(), e);
// 如果发送到MQ失败,记录错误但继续重试
}
return false;
}
/**
* 根据重试次数获取 RocketMQ 延迟级别1s/10s/1m/3m/5m/10m/20m/30m/1h/2h
* RocketMQ 延迟级别: 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
*/
private int getDelayLevel(int retryCount) {
// 映射到 RocketMQ 的延迟级别,对应 1s/10s/1m/3m/5m/10m/20m/30m/1h/2h
switch (retryCount) {
case 0: return 1; // 1s
case 1: return 3; // 10s
case 2: return 5; // 1m
case 3: return 7; // 3m
case 4: return 9; // 5m
case 5: return 14; // 10m
case 6: return 15; // 20m
case 7: return 16; // 30m
case 8: return 17; // 1h
default: return 18; // 2h (默认最大)
}
}
/**
* 验证响应是否有效
*/
private boolean isValidResponse(String response) {
return response != null && response.contains("\"ret_code\":\"00\"");
}
/**
* 处理达到最大重试次数的消息
*/
private void handleMaxRetriesExceeded(RetryMessage retryMsg) {
log.error("消息重发达到最大次数{},停止重试。消息ID: {}, 最终状态: 失败",
MAX_RETRY, retryMsg.getMessageId());
// 保存到数据库
try {
CrmPushFailedMessageDO failedMessage = new CrmPushFailedMessageDO();
failedMessage.setMessageId(retryMsg.getMessageId());
failedMessage.setUrl(retryMsg.getUrl());
failedMessage.setPayload(retryMsg.getPayload());
failedMessage.setOriginalData(JSON.toJSONString(retryMsg.getOriginalData()));
failedMessage.setRetryCount(retryMsg.getRetryCount());
failedMessage.setFailReason("达到最大重试次数");
crmPushFailedMessageMapper.insert(failedMessage);
log.info("失败消息已记录到数据库,消息ID: {}", retryMsg.getMessageId());
} catch (Exception e) {
log.error("记录失败消息到数据库失败,消息ID: {}", retryMsg.getMessageId(), e);
}
}
/**
* 添加待重试消息(指定topic)并立即发送到延迟队列
*/
public void addPendingMessage(String messageId, String url, String payload, Object originalData, String topic) {
RetryMessage retryMsg = new RetryMessage(messageId, url, payload, 0,
LocalDateTime.now().plusSeconds(RETRY_INTERVALS[0]), originalData, topic);
// 直接发送到 RocketMQ 延迟队列,不保存在本地内存中
try {
rocketMQTemplate.syncSend(topic,
MessageBuilder.withPayload(JSON.toJSONString(retryMsg)).build(),
3000,
getDelayLevel(0)); // 第一次重试使用级别1(1s)
log.info("======未正确响应=====消息首次发送到延迟队列,消息ID: {}, Topic: {}, 延迟级别: {}", messageId, topic, getDelayLevel(0));
} catch (Exception e) {
log.error("======未正确响应=====首次发送消息到延迟队列失败,消息ID: {}, Topic: {}", messageId, topic, e);
}
log.info("添加待重试消息,消息ID: {}, Topic: {}", messageId, topic);
}
/**
* 添加待重试消息(使用默认topic)
*/
public void addPendingMessage(String messageId, String url, String payload, Object originalData) {
addPendingMessage(messageId, url, payload, originalData, "CRM_PUSH_EXCEPTION_RETRY_TOPIC");
}
}
三、消费者处理
@Component
@RocketMQMessageListener(topic = "CRM_BIND_PUSH_RETRY_TOPIC", consumerGroup = "crm-bind-retry-consumer-group")
@Slf4j
public class CrmBindRetryConsumer implements RocketMQListener<String> {
@Autowired
private MessageRetryHandler messageRetryHandler;
@Override
public void onMessage(String message) {
try {
RetryMessage retryMsg = JSON.parseObject(message, RetryMessage.class);
// 使用统一的重试处理器处理消息
messageRetryHandler.handleRetry(retryMsg);
} catch (Exception e) {
log.error("处理绑定消息失败: {}", message, e);
}
}
}
四、定时任务推送
@Component
@Slf4j
public class CrmBindCustomerJob implements JobHandler {
@Resource
private CrmBindCustomerPushService crmBindCustomerPushService;
@Override
@TenantJob
public String execute(String param) throws Exception {
log.info("开始执行绑定数据推送定时任务");
try {
String result = crmPosBindCustomerPushService.syncPosBindPush();
log.info("绑定数据推送定时任务执行完成,结果: {}", result);
return result;
} catch (Exception e) {
log.error("绑定数据推送定时任务执行失败", e);
throw e;
}
}
}
五、相关日志记录类
@TableName("crm_push_failed_message")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CrmPushFailedMessageDO extends BaseDO {
/**
* 主键
*/
private Long id;
/**
* 消息ID
*/
private String messageId;
/**
* 推送URL
*/
private String url;
/**
* 推送载荷
*/
private String payload;
/**
* 原始数据
*/
private String originalData;
/**
* 重试次数
*/
private Integer retryCount;
/**
* 失败原因
*/
private String failReason;
}
@Data
public class CrmSyncLogDO {
private Long id;
private String bizType;
private LocalDateTime lastSyncTime;
private Integer retryCount;
private String status;
private String remark;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RetryMessage {
private String messageId;
private String url;
private String payload;
private int retryCount = 0;
private LocalDateTime nextRetryTime;
private Object originalData;
private String topic;
public void incrementRetryCount() {
this.retryCount++;
}
}
172万+

被折叠的 条评论
为什么被折叠?



