package com.yunhai.userbase.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.yunhai.userbase.model.push.PushTaskMessage;
import java.util.HashMap;
import java.util.Map;
/**
* 推送订阅 MQ 配置。
* 拓扑:
*
* push.topic (topic, durable)
* └─ push.main (durable, concurrency=1, x-dead-letter-exchange=push.dlx) ← 串行消费,可观测
* push.dlx (fanout, durable)
* └─ push.dlq (durable) ← 死信归档(重试耗尽)
*
* 连接信息(host/port/user/pwd/vhost)由 Nacos {@code application-infra.yaml} 的 {@code spring.rabbitmq.*}
* 公共配置注入,本类不写死(也不改 bootstrap.yml / application.yml)。
* 可靠性:消费端手动 ack,失败由 {@code PushConsumer} 应用层自管重试(共 3 次),耗尽进死信队列。
*
* @author chenx
* @date 2026/8/14
*/
@Configuration
public class PushMqConfig {
public static final String EXCHANGE_TOPIC = "push.topic";
public static final String EXCHANGE_DLX = "push.dlx";
public static final String QUEUE_MAIN = "push.main";
public static final String QUEUE_DLQ = "push.dlq";
/** 统一 JSON 消息转换器(固定反序列化为 PushTaskMessage,绕过 trusted-packages 版本差异)。 */
@Bean
public MessageConverter pushMessageConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
converter.setClassMapper(new PushTaskClassMapper(PushTaskMessage.class));
return converter;
}
@Bean
public TopicExchange pushTopicExchange() {
return new TopicExchange(EXCHANGE_TOPIC, true, false);
}
@Bean
public FanoutExchange pushDlxExchange() {
return new FanoutExchange(EXCHANGE_DLX, true, false);
}
/** 主队列:单消费者串行 + 死信路由到 push.dlx。 */
@Bean
public Queue pushMainQueue() {
Map args = new HashMap<>();
args.put("x-dead-letter-exchange", EXCHANGE_DLX);
return new Queue(QUEUE_MAIN, true, false, false, args);
}
/** 死信队列:归档重试耗尽的任务。 */
@Bean
public Queue pushDlqQueue() {
return new Queue(QUEUE_DLQ, true, false, false);
}
@Bean
public Binding pushMainBinding() {
return BindingBuilder.bind(pushMainQueue()).to(pushTopicExchange()).with("#");
}
@Bean
public Binding pushDlqBinding() {
return BindingBuilder.bind(pushDlqQueue()).to(pushDlxExchange());
}
@Bean
public RabbitTemplate pushRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(pushMessageConverter());
return template;
}
/** 消费者容器:手动 ack + 单并发 + prefetch=1,保证串行可观测。 */
@Bean(name = "pushListenerContainerFactory")
public SimpleRabbitListenerContainerFactory pushListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(pushMessageConverter());
factory.setAcknowledgeMode(org.springframework.amqp.core.AcknowledgeMode.MANUAL);
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(1);
factory.setPrefetchCount(1);
return factory;
}
}