SpringBoot整合RabbitMq各种模式的使用

一.demo项目结构(两个服务(生产者消费者)共用一个公共配置)

在这里插入图片描述
在这里插入图片描述

1. spring-rabbitmq-config公共参数模块

/**
 * 交换机队列等参数
 */
public class RabbitConstant {

    /**
     * direct队列、交换机、路由键
     */
    public static final String DIRECT_QUEUE_NAME = "direct_queue";
    public static final String DIRECT_EXCHANGE_NAME = "direct_exchange";
    public static final String DIRECT_ROUTING_KEY = "direct_routing_key";

    /**
     * fanout队列、交换机、路由键
     */
    public static final String FANOUT_QUEUE_NAME1="fanout_queue1";
    public static final String FANOUT_QUEUE_NAME2="fanout_queue2";
    public static final String FANOUT_EXCHANGE_NAME="fanout_exchange";

    /**
     * topic队列、交换机、路由键
     */
    public static final String PRODUCT_TOPIC_QUEUE_NAME="product_topic_queue"; //对应routingKey=product.*
    public static final String ORDER_TOPIC_QUEUE_NAME="order_topic_queue"; //对应routingKey=order.*
    public static final String TOPIC_EXCHANGE_NAME="topic_exchange";

    /**
     * header队列、交换机、路由键
     */
    public final static String HEADER_EXCHANGE_NAME = "header_exchange";
    public final static String HEADER_NAME_QUEUE = "nameQueue";
    public final static String HEADER_AGE_QUEUE = "ageQueue";

    /**
     * 延迟队列、交换机、交换机类型
     */
    public static final String DELAY_QUEUE_NAME = "delay_queue";
    public static final String DELAY_EXCHANGE_NAME = "delay_exchange";
    public static final String DELAY_EXCHANGE_TYPE = "x-delayed-message";

    /**
     * 死信队列、交换机、路由键
     */
    public static final String DLX_QUEUE_NAME = "dlx_queue";
    public static final String DLX_EXCHANGE_NAME = "dlx_exchange";
    public static final String DLX_ROUTING_KEY = "dlx_routing_key";
}

2.spring-rabbitmq-producer生产者微服务(只做了单元测试代码)

2.1 application.yml
server:
  port: 8091
spring:
  rabbitmq:
    host: 192.168.89.100
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-returns: true
    #配置生产者消息发送确认机制
    publisher-confirm-type: correlated
    template:
      retry:
        #开启重试机制
        enabled: true
        #重试起始间隔时间
        initial-interval: 1000ms
        #最大重试次数
        max-attempts: 10
        #最大重试间隔时间
        max-interval: 10000ms
        #间隔时间乘数(这里配置间隔时间乘数为 2,则第一次间隔时间 1 秒,第二次重试间隔时间 2 秒,第三次 4 秒,以此类推)
        multiplier: 2
2.2 ProducerConfig.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import static com.zdb.common.constant.RabbitConstant.*;

/**
 * @author asdmin
 */
@Configuration
public class ProducerConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private static final Logger logger = LoggerFactory.getLogger(ProducerConfig.class);

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 配置消息发送的确认机制
     */
    @PostConstruct
    public void initRabbitTemplate() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String s) {
        if (ack) {
            logger.info("{}:消息成功到达交换器", correlationData.getId());
        } else {
            logger.error("{}:消息发送交换器失败", correlationData.getId());
        }
    }

    @Override
    public void returnedMessage(final Message message, final int replyCode, final String replyText,
                                final String exchange,
                                final String routingKey) {
        // 排除调延迟交换机,因为消息在延迟交换机中延迟,会因消息失效时间和延迟时间的差距触发此函数回调
        if (!DELAY_EXCHANGE_NAME.equals(exchange)) {
            logger.info("交换机返回消息的方法收到的消息:{} \n交换机回复的内容:{} \n交换机是:{} \n路由:key:{}",
                    new String(message.getBody()), replyText, exchange, routingKey);
        }
    }

    /**
     * 普通直连交换机 direct_exchange
     */
    @Bean("directExchange")
    DirectExchange grtDirectExchange() {
        return new DirectExchange(DIRECT_EXCHANGE_NAME, true, false);
    }

    /**
     * 声明广播交换机 fanout_exchange
     */
    @Bean
    public FanoutExchange getFanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE_NAME, true, false);
    }

    /**
     * 声明主题交换机 topic_exchange
     */
    @Bean
    public TopicExchange getTopicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE_NAME, true, false);
    }

    /**
     * header消息交换机
     */
    @Bean("headersExchange")
    HeadersExchange getHeadersExchange() {
        return new HeadersExchange(HEADER_EXCHANGE_NAME, true, false);
    }

    /**
     * 声明延迟义交换机 delay_exchange
     */
    @Bean
    public CustomExchange getDelayExchange() {
        return new CustomExchange(DELAY_EXCHANGE_NAME, DELAY_EXCHANGE_TYPE, true, false);
    }

    /**
     * 死信交换机 dlx_exchange
     */
    @Bean("dlxExchange")
    DirectExchange getDlxExchange() {
        return new DirectExchange(DLX_EXCHANGE_NAME, true, false);
    }
}

2.3 TestSendMessage.java
import com.zdb.common.constant.RabbitConstant;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.UUID;


@SpringBootTest
@RunWith(SpringRunner.class)
public class TestSendMessage {

    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 测试直连和死信队列(发送消息到一个未配置消费者的直连队列中,让消息超时成为死信)
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testSendDLXMessage() throws UnsupportedEncodingException {
        Message msg = MessageBuilder.withBody(("测试javaboy_queue队列的死信消息!" ).getBytes("UTF-8")).build();
        rabbitTemplate.convertAndSend(RabbitConstant.DIRECT_EXCHANGE_NAME, RabbitConstant.DIRECT_ROUTING_KEY, msg ,new CorrelationData(UUID.randomUUID().toString()));
    }

    /**
     * 测试广播消息
     */
    @Test
    public void testSendFanoutMessage() {
        //参数:交换机、路由键、消息体
        rabbitTemplate.convertAndSend(RabbitConstant.FANOUT_EXCHANGE_NAME,"","测试生产者产生Fanout消息!");
    }

    /**
     * 测试topic交换机的routingKey  路由模式
     */
    @Test
    public void testSendTopicMessage() {
        rabbitTemplate.convertAndSend(RabbitConstant.TOPIC_EXCHANGE_NAME,"product.test1","测试生产者产生productTopic消息!");
        rabbitTemplate.convertAndSend(RabbitConstant.TOPIC_EXCHANGE_NAME,"order.test1","测试生产者产生orderTopic消息!");
    }

    /**
     * 测试header交换机的header头条件
     */
    @Test
    public void testSendHeaderMessage() throws UnsupportedEncodingException {
        Message nameMsg = MessageBuilder
                .withBody("hello header! nameQueue".getBytes("UTF-8"))
                .setHeader("name", "zdb").build();
        Message ageMsg = MessageBuilder
                .withBody("hello header! ageQueue".getBytes())
                .setHeader("age", "99").build();
        rabbitTemplate.convertAndSend(RabbitConstant.HEADER_EXCHANGE_NAME,null,nameMsg,new CorrelationData(UUID.randomUUID().toString()));
        rabbitTemplate.convertAndSend(RabbitConstant.HEADER_EXCHANGE_NAME,null,ageMsg,new CorrelationData(UUID.randomUUID().toString()));
    }

    /**
     * 测试延迟消息
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testSendDelayMessage() throws UnsupportedEncodingException {
        Message msg = MessageBuilder.withBody(("hello 周定斌,这条10s延迟的消息发送时间为:" + new Date()).getBytes("UTF-8")).setHeader("x-delay", 10000).build();
        rabbitTemplate.convertAndSend(RabbitConstant.DELAY_EXCHANGE_NAME, RabbitConstant.DELAY_QUEUE_NAME, msg);
    }

    /**
     * 测试生产者发送消息到mq的确认和回调
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testProducerReturnAndConfirm() throws UnsupportedEncodingException {
        Message msg = MessageBuilder.withBody(("测试消息确认和回调!" ).getBytes("UTF-8")).build();
        rabbitTemplate.convertAndSend(RabbitConstant.DIRECT_EXCHANGE_NAME, "不存在的routingKey", msg ,new CorrelationData(UUID.randomUUID().toString()));
    }

}

3.spring-rabbitmq-consumer消费者微服务

3.1 applicatin.yml
server:
  port: 8090
spring:
  rabbitmq:
    host: 192.168.89.100
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    ##消费消息设置手动ACK,默认是自动
    listener:
      simple:
        acknowledge-mode: manual
3.2 ConsumerConfig.java
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

import static com.zdb.common.constant.RabbitConstant.*;

/**
 * @author asdmin
 */
@Configuration
public class ConsumerConfig {

    /**
     * direct直连型消息队列(配合死信队列使用)
     */
    @Bean("directQueue")
    Queue getDirectQueue() {
        Map<String, Object> args = new HashMap<>();
        //设置消息过期时间10s
        args.put("x-message-ttl", 1000 * 10);
        //设置死信交换机
        args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
        //设置死信 routing_key
        args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
        return new Queue(DIRECT_QUEUE_NAME, true, false, false, args);
    }

    @Bean("directExchange")
    DirectExchange getDirectExchange() {
        return new DirectExchange(DIRECT_EXCHANGE_NAME, true, false);
    }

    @Bean
    Binding getDirectBinding(@Qualifier("directExchange") DirectExchange directExchange, @Qualifier("directQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with(DIRECT_ROUTING_KEY);
    }


    /**
     * fanout模式的消息配置
     */
    @Bean
    public FanoutExchange getFanoutExchange() {
        return new FanoutExchange(FANOUT_EXCHANGE_NAME, true, false);
    }

    @Bean("fanoutQueue1")
    public Queue getFanoutQueue1() {
        return new Queue(FANOUT_QUEUE_NAME1, true, false, false);
    }

    @Bean("fanoutQueue2")
    public Queue getFanoutQueue2() {
        return new Queue(FANOUT_QUEUE_NAME2, true, false, false);
    }

    @Bean
    public Binding getFanoutBinding1(FanoutExchange fanoutExchange, @Qualifier("fanoutQueue1") Queue queue) {
        return BindingBuilder.bind(queue).to(fanoutExchange);
    }

    @Bean
    public Binding getFanoutBinding2(FanoutExchange fanoutExchange, @Qualifier("fanoutQueue2") Queue queue) {
        return BindingBuilder.bind(queue).to(fanoutExchange);
    }


    /**
     * topic模式的消息配置(product 和 order)
     */
    @Bean
    public TopicExchange getTopicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE_NAME, true, false);
    }

    @Bean("productTopicQueue")
    public Queue getProductTopicQueue() {
        return new Queue(PRODUCT_TOPIC_QUEUE_NAME, true, false, false);
    }

    @Bean("orderTopicQueue")
    public Queue getOrderTopicQueue() {
        return new Queue(ORDER_TOPIC_QUEUE_NAME, true, false, false);
    }

    @Bean
    public Binding getProductTopicBinding(TopicExchange topicExchange, @Qualifier("productTopicQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(topicExchange).with("product.*");
    }

    @Bean
    public Binding getOrderTopicBinding(TopicExchange topicExchange, @Qualifier("orderTopicQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(topicExchange).with("order.*");
    }


    /**
     * header消息队列
     */
    @Bean("headersExchange")
    HeadersExchange getHeadersExchange() {
        return new HeadersExchange(HEADER_EXCHANGE_NAME, true, false);
    }

    @Bean("nameQueue")
    Queue getNameQueue() {
        return new Queue(HEADER_NAME_QUEUE, true, false, false);
    }

    @Bean("ageQueue")
    Queue getAgeQueue() {
        return new Queue(HEADER_AGE_QUEUE, true, false, false);
    }

    @Bean
    Binding bindingName(@Qualifier("headersExchange") HeadersExchange headersExchange, @Qualifier("nameQueue") Queue queue) {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "zdb");
        return BindingBuilder.bind(queue)
                .to(headersExchange).whereAny(map).match();
    }

    @Bean
    Binding bindingAge(@Qualifier("headersExchange") HeadersExchange headersExchange, @Qualifier("ageQueue") Queue queue) {
        return BindingBuilder.bind(queue)
                .to(headersExchange).where("age").exists();
    }


    /**
     * 延迟队列配置
     */
    @Bean("delayedExchange")
    CustomExchange getDelayedExchange() {
        Map<String, Object> args = new HashMap<>(10);
        args.put("x-delayed-type", "direct");
        /**
         * 交换机名称。
         * 交换机类型,这个地方是固定的。 x-delayed-message
         * 交换机是否持久化。
         * 如果没有队列绑定到交换机,交换机是否删除。
         * 其他参数
         */
        return new CustomExchange(DELAY_EXCHANGE_NAME, DELAY_EXCHANGE_TYPE, true, false, args);
    }

    @Bean("delayQueue")
    Queue getDelayQueue() {
        return new Queue(DELAY_QUEUE_NAME, true, false, false);
    }

    @Bean
    Binding binding(@Qualifier("delayedExchange") CustomExchange customExchange, @Qualifier("delayQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(customExchange).with(DELAY_QUEUE_NAME).noargs();
    }


    /**
     * 死信队列(配合普通队列使用,普通队列消息过期后进入私信队列)
     */
    @Bean("dlxQueue")
    Queue dlxQueue() {
        return new Queue(DLX_QUEUE_NAME, true, false, false);
    }

    @Bean("dlxExchange")
    DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE_NAME, true, false);
    }

    @Bean
    Binding dlxBinding(@Qualifier("dlxExchange") DirectExchange directExchange, @Qualifier("dlxQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with(DLX_ROUTING_KEY);
    }

}

3.3 MessageListener.java
package com.zdb.consumer.listener;

import com.rabbitmq.client.Channel;
import com.zdb.common.constant.RabbitConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * @author asdmin
 * Attribute value must be constant
 */
@Service
public class MessageListener {

    private static final Logger logger = LoggerFactory.getLogger(MessageListener.class);

    /**
     * 监听direct直连型队列direct_queue,(测试时不开启接收,检验延迟队列的接收是否正常)
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitListener(queues = RabbitConstant.DIRECT_QUEUE_NAME)
    public void handle(Message message , Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.DIRECT_QUEUE_NAME+"收到的direct_queue队列消息:"+new String(messageBody));

            //模拟消息处理判断接收还是拒收
            boolean dealFlag = false;
            if(dealFlag){
                /**
                 * 执行完成无异常手动签收信息
                 * 入参:
                 * deliveryTag:确认消息的编号,这是每个消息被消费时都会分配一个递增唯一编号
                 * multiple:批量确认,true表示所有编号小于目前确认消息编号的待确认消息都会被确认,false则只确认当前消息
                 */
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                logger.info("业务判断成功,接收并清理mq中的消息:{}",new String(messageBody));
            } else {
                /**
                 * 业务异常拒绝签收
                 * 入参:
                 * deliveryTag:确认消息的编号,这是每个消息被消费时都会分配一个递增唯一编号
                 * multiple:批量确认,true表示所有编号小于目前确认消息编号的待确认消息都会被确认,false则只确认当前消息
                 * requeue:表示是否重新将消息放回队列(如果设置为true,则将消息重新放回队列,如果设置为false,进入死信队列或直接将消息丢弃)。
                 */
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
                logger.error("业务判断失败,拒收该消息");
            }
        } catch (Exception e) {
            e.printStackTrace();
            /**
             * 异常拒绝接收
             * 入参:
             * deliveryTag:确认消息的编号,这是每个消息被消费时都会分配一个递增唯一编号
             * requeue:表示是否重新将消息放回队列(如果设置为true,则将消息重新放回队列,如果设置为false,则直接将消息丢弃)。
             */
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

    /**
     * 监听fanout广播消息队列 my_boot_fanout_queue1 my_boot_fanout_queue2
     * @param message
     * @param message
     * @throws IOException
     */
    @RabbitListener(queues = {RabbitConstant.FANOUT_QUEUE_NAME1})
    public void dealFanoutMessage1(Message message, Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.FANOUT_QUEUE_NAME1+"收到的广播消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }

    }

    @RabbitListener(queues = {RabbitConstant.FANOUT_QUEUE_NAME2})
    public void dealFanoutMessage2(Message message, Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.FANOUT_QUEUE_NAME2+"收到的广播消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }

    }

    /**
     * 监听topic消息队列(产品和订单消息)
     * @param message
     * @param message
     * @throws IOException
     */
    @RabbitListener(queues = {RabbitConstant.PRODUCT_TOPIC_QUEUE_NAME})
    public void dealProductTopicMessage(Message message,Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.PRODUCT_TOPIC_QUEUE_NAME+"收到的topic消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

    @RabbitListener(queues = {RabbitConstant.ORDER_TOPIC_QUEUE_NAME})
    public void dealOrderTopicMessage(Message message,Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.ORDER_TOPIC_QUEUE_NAME+"收到的topic消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }


    /**
     * 监听header消息队列(name 和 age)
     * @param message
     * @param message
     * @throws IOException
     */
    @RabbitListener(queues = {RabbitConstant.HEADER_NAME_QUEUE})
    public void dealNameHeaderMessage(Message message,Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.HEADER_NAME_QUEUE+"收到的nameHeader消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

    @RabbitListener(queues = {RabbitConstant.HEADER_AGE_QUEUE})
    public void dealAgeHeaderMessage(Message message,Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.HEADER_AGE_QUEUE+"收到的ageHeader消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

    /**
     * 监听延迟消息队列my_boot_delay_queue
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitListener(queues = RabbitConstant.DELAY_QUEUE_NAME)
    public void dealDelayMessage(Message message , Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println(RabbitConstant.DELAY_QUEUE_NAME+"收到的延迟消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

    /**
     * 监听死信队列dlx_queue_name
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitListener(queues = RabbitConstant.DLX_QUEUE_NAME)
    public void dealDLXMessage(Message message , Channel channel) throws IOException {
        try {
            //处理消息
            byte[] messageBody = message.getBody();
            System.out.println("收到的死信队列消息:"+new String(messageBody));
            //执行完成无异常手动签收信息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            e.printStackTrace();
            //异常拒绝接收
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }


}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值