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; } }