RabbitMQ的死信队列和延迟队列

文章目录

    • 死信队列
    • 如何配置死信队列
      • 死信队列的应用场景
      • Spring Boot实现RabbitMQ的死信队列
    • 延迟队列
      • 方案优劣:
      • 延迟队列的实现有两种方式:

死信队列

1)“死信”是RabbitMQ中的一种消息机制。
2)消息变成死信,可能是由于以下的原因
● 消息被拒绝
● 消息过期
● 队列达到最大长度
3)死信队列
当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是 DLX(Dead-Letter-Exchange ) ,绑定 DLX 的队列就称之为死信队列。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。

如何配置死信队列

  1. 配置业务队列,绑定到业务交换机上
  2. 为业务队列配置死信交换机和路由key
  3. 为死信交换机配置死信队列
    注意:并不是直接声明一个公共的死信队列,然后所以死信消息就自己跑到死信队列里去了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key。
    有了死信交换机和路由key后,接下来,就像配置业务队列一样,配置死信队列,然后绑定在死信交换机上。也就是说,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列。
    具体因为队列消息过期而被投递到死信队列的流程:
    在这里插入图片描述

死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。
死信消息的生命周期:
1)业务消息被投入业务队列
2)消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了nck或者reject操作
3)被nck或reject的消息由RabbitMQ投递到死信交换机中
4)死信交换机将消息投入相应的死信队列
5)死信队列的消费者消费死信消息
死信消息是RabbitMQ为我们做的一层保证,其实我们也可以不使用死信队列,而是在消息消费异常时,将消息主动投递到另一个交换机中,当你明白了这些之后,这些Exchange和Queue想怎样配合就能怎么配合。比如从死信队列拉取消息,然后发送邮件、短信、钉钉通知来通知开发人员关注。或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费

死信队列的应用场景

一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息。
通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。

Spring Boot实现RabbitMQ的死信队列

当您在使用Spring Boot实现RabbitMQ的死信队列时,您需要完成以下步骤:

  1. 添加Maven依赖
    确保您的pom.xml文件中包含以下Maven依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

这将为您的应用程序提供RabbitMQ的基本支持。
2. 配置RabbitMQ连接信息
在application.properties或application.yml文件中添加RabbitMQ的连接信息,包括主机地址、用户名、密码等。
3. 创建RabbitMQConfig类
创建一个配置类,用于定义交换机、队列以及它们之间的绑定关系。在这个类中,您需要定义普通队列、死信队列、死信交换机,并将它们进行绑定。
4. 创建消费者类
创建一个消费者类,使用@RabbitListener注解标记需要监听的死信队列,并在方法上使用@RabbitHandler注解来处理接收到的死信消息。
5. 发送消息到普通队列
在需要发送消息的地方,使用RabbitTemplate发送消息到普通队列中。当消息因为过期或被拒绝接收时,会被标记为死信消息,并根据参数设置转发到死信交换机中,然后路由到死信队列中。

下面是一个简单的示例代码,演示了如何在Spring Boot中实现RabbitMQ的死信队列,并消费死信队列的消息:

// RabbitMQConfig.java
@Configuration
public class RabbitMQConfig {

    @Bean
    public Queue myQueue() {
        return QueueBuilder.durable("my_queue")
                .withArgument("x-dead-letter-exchange", "dlx_exchange")
                .withArgument("x-dead-letter-routing-key", "dlq_queue")
                .build();
    }

    @Bean
    public Queue dlqQueue() {
        return QueueBuilder.durable("dlq_queue").build();
    }

    @Bean
    public Exchange dlxExchange() {
        return ExchangeBuilder.directExchange("dlx_exchange").durable(true).build();
    }

    @Bean
    public Binding binding(Queue myQueue, Exchange dlxExchange) {
        return BindingBuilder.bind(myQueue).to(dlxExchange).with("my_queue").noargs();
    }
}

// DeadLetterQueueConsumer.java
@Component
@RabbitListener(queues = "dlq_queue")
public class DeadLetterQueueConsumer {

    @RabbitHandler
    public void processDeadLetterMessage(String message) {
        System.out.println("Received message from dead letter queue: " + message);
        // 处理接收到的死信消息
    }
}

// RabbitMQService.java
@Service
public class RabbitMQService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessage() {
        rabbitTemplate.convertAndSend("my_queue", "Hello, RabbitMQ!");
    }
}

通过以上步骤,您就可以在Spring Boot项目中实现RabbitMQ的死信队列,并消费死信队列的消息。

延迟队列

延迟队列存储的对象是对应的延迟消息;所谓“延迟消息” 是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
在RabbitMQ中延迟队列可以通过 过期时间 + 死信队列 来实现;具体如下流程图所示:

在这里插入图片描述

RabbitMQ 的基因中没有延时队列这回事,它不能直接指定一个队列类型为延时队列,然后去延时处理,但是经过上面两节的铺垫,我们可以将 TTL+DLX 相结合,这就能组成一个延时队列。
设想一个场景,下完订单之后 15 分钟未付款我们就要将订单关闭,这就是一个很经典的演示消费的场景,如果拿 RabbitMQ 来做,我们就需要结合 TTL+DLX 了。
先把订单消息设置好 15 分钟过期时间,然后过期后队列将消息转发给我们设置好的 DLX-Exchange,DLX-Exchange 再将分发给它绑定的队列,我们的消费者再消费这个队列中的消息,就做到了延时十五分钟消费。

RabbitMQ 有两个特性,一个是 Time-To-Live Extensions,另一个是 Dead Letter Exchanges。
Time-To-Live Extensions
RabbitMQ允许我们为消息或者队列设置TTL(time to live),也就是过期时间。TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后 “死亡”,成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。
Dead Letter Exchanges
在 RabbitMQ 中,一共有三种消息的 “死亡” 形式:
● 消息被拒绝。通过调用 basic.reject 或者 basic.nack 并且设置的 requeue 参数为 false;
● 消息因为设置了TTL而过期;
● 队列达到最大长度。

DLX同一般的 Exchange 没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当队列中有 DLX 消息时,RabbitMQ就会自动的将 DLX 消息重新发布到设置的 Exchange 中去,进而被路由到另一个队列,publish 可以监听这个队列中消息做相应的处理。
由上简介大家可以看出,RabbitMQ本身是不支持延迟队列的,只是他的特性让勤劳的 中国脱发群体 急中生智(为了完成任务)弄出了这么一套可用的方案。
可用的方案就是:

  1. 如果有事件需要延迟那么将该事件发送到MQ 队列中,为需要延迟的消息设置一个TTL;
  2. TTL到期后就会自动进入设置好的DLX,然后由DLX转发到配置好的实际消费队列;
  3. 消费该队列的延迟消息,处理事件。

方案优劣:

优点:
大品牌组件,用的放心。如果面临大数据量需求可以很容易的横向扩展,同时消息支持持久化,有问题可回滚。
缺点:

  1. 配置麻烦,额外增加一个死信交换机和一个死信队列的配置;
  2. RabbitMQ 是一个消息中间件,TTL 和 DLX 只是他的一个特性,将延迟队列绑定在一个功能软件的某一个特性上,可能会有风险。不要杠,当你们组不用 RabbitMQ 的时候迁移很痛苦;
  3. 消息队列具有先进先出的特点,如果第一个进入队列的消息 A 的延迟是10分钟,第二个进入队列的消息B 的延迟是5分钟,期望的是谁先到 TTL谁先出,但是事实是B已经到期了,而还要等到 A 的延迟10分钟结束A先出之后,B 才能出。所以在设计的时候需要考虑不同延迟的消息要放到不同的队列。另外该问题官方已经给出了插件来支持:插件地址。

延迟队列的实现有两种方式:

通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能;
使用 RabbitMQ-delayed-message-exchange 插件实现延迟功能。
针对任务丢失的代价过大,高并发的场景
优点: 支持集群,分布式,高并发场景;
缺点: 引入额外的消息队列,增加项目的部署和维护的复杂度。
场景:为一个委托指定期限,委托到期后,委托关系终止,相关业务权限移交回原拥有者 这里采用的是RabbitMq的死信队列加TTL消息转化为延迟队列的方式(RabbitMq没有延时队列)

①声明一个队列设定其的死信队列  
@Configuration
public class MqConfig {
    public static final String GLOBAL_RABBIT_TEMPLATE = "rabbitTemplateGlobal";
 
    public static final String DLX_EXCHANGE_NAME = "dlxExchange";
    public static final String AUTH_EXCHANGE_NAME = "authExchange";
 
    public static final String DLX_QUEUE_NAME = "dlxQueue";
    public static final String AUTH_QUEUE_NAME = "authQueue";
    public static final String DLX_AUTH_QUEUE_NAME = "dlxAuthQueue";
 
    @Bean
    @Qualifier(GLOBAL_RABBIT_TEMPLATE)
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        return rabbitTemplate;
    }
 
    @Bean
    @Qualifier(AUTH_EXCHANGE_NAME)
    public Exchange authExchange() {
        return ExchangeBuilder.directExchange (AUTH_EXCHANGE_NAME).durable (true).build ();
    }
 
    /**
     * 死信交换机
     * @return
     */
    @Bean
    @Qualifier(DLX_EXCHANGE_NAME)
    public Exchange dlxExchange() {
        return ExchangeBuilder.directExchange (DLX_EXCHANGE_NAME).durable (true).build ();
    }
 
    /**
     * 记录日志的死信队列
     * @return
     */
    @Bean
    @Qualifier(DLX_QUEUE_NAME)
    public Queue dlxQueue() {
        // Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
        return QueueBuilder.durable (DLX_QUEUE_NAME).build ();
    }
 
    /**
     * 委托授权专用队列
     * @return
     */
    @Bean
    @Qualifier(AUTH_QUEUE_NAME)
    public Queue authQueue() {
        return QueueBuilder
                .durable (AUTH_QUEUE_NAME)
                .withArgument("x-dead-letter-exchange", DLX_EXCHANGE_NAME)
                .withArgument("x-dead-letter-routing-key", "dlx_auth")
                .build ();
    }
 
    /**
     * 委托授权专用死信队列
     * @return
     */
    @Bean
    @Qualifier(DLX_AUTH_QUEUE_NAME)
    public Queue dlxAuthQueue() {
        // Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
        return QueueBuilder
                .durable (DLX_AUTH_QUEUE_NAME)
                .withArgument("x-dead-letter-exchange", DLX_EXCHANGE_NAME)
                .withArgument("x-dead-letter-routing-key", "dlx_key")
                .build ();
    }
 
    @Bean
    public Binding bindDlxQueueExchange(@Qualifier(DLX_QUEUE_NAME) Queue dlxQueue, @Qualifier(DLX_EXCHANGE_NAME) Exchange dlxExchange){
        return BindingBuilder.bind (dlxQueue).to (dlxExchange).with ("dlx_key").noargs ();
    }
 
    /**
     * 委托授权专用死信队列绑定关系
     * @param dlxAuthQueue
     * @param dlxExchange
     * @return
     */
    @Bean
    public Binding bindDlxAuthQueueExchange(@Qualifier(DLX_AUTH_QUEUE_NAME) Queue dlxAuthQueue, @Qualifier(DLX_EXCHANGE_NAME) Exchange dlxExchange){
        return BindingBuilder.bind (dlxAuthQueue).to (dlxExchange).with ("dlx_auth").noargs ();
    }
 
    /**
     * 委托授权专用队列绑定关系
     * @param authQueue
     * @param authExchange
     * @return
     */
    @Bean
    public Binding bindAuthQueueExchange(@Qualifier(AUTH_QUEUE_NAME) Queue authQueue, @Qualifier(AUTH_EXCHANGE_NAME) Exchange authExchange){
        return BindingBuilder.bind (authQueue).to (authExchange).with ("auth").noargs ();
    }
 
}
 

 ②发送含过期时间的消息  
 向授权交换机,发送路由为"auth"的消息(指定了业务所需的超时时间) =》发向MqConfig.AUTH_QUEUE_NAME 队列  
rabbitTemplate.convertAndSend(MqConfig.AUTH_EXCHANGE_NAME, "auth", "类型:END,信息:{id:1,fromUserId:111,toUserId:222,beginData:20201204,endData:20211104}", message -> {
            /**
             * MessagePostProcessor:消息后置处理
             * 为消息设置属性,然后返回消息,相当于包装消息的类
             */
 
            //业务逻辑:过期时间=xxxx
            String ttl = "5000";
            //设置消息的过期时间
            message.getMessageProperties ().setExpiration (ttl);
            return message;
        });
复制代码
③超时后队列MqConfig.AUTH_QUEUE_NAME会将消息转发至其配置的死信路由"dlx_auth",监听该死信队列即可消费定时的消息
 	/**
     * 授权定时处理
     * @param channel
     * @param message
     */
    @RabbitListener(queues = MqConfig.DLX_AUTH_QUEUE_NAME)
    public void dlxAuthQ(Channel channel, Message message) throws IOException {
        System.out.println ("\n死信原因:" + message.getMessageProperties ().getHeaders ().get ("x-first-death-reason"));
        //1.判断消息类型:1.BEGIN 2.END
        try {
            //2.1 类型为授权到期(END)
            //2.1.1 修改报件办理人
            //2.1.2 修改授权状态为0(失效)
 
            //2.2 类型为授权开启(BEGIN)
            //2.2.1 修改授权状态为1(开启)
            System.out.println (new String(message.getBody (), Charset.forName ("utf8")));
            channel.basicAck (message.getMessageProperties ().getDeliveryTag (),  false);
            System.out.println ("已处理,授权相关信息修改成功");
        } catch (Exception e) {
            //拒签消息
            channel.basicNack (message.getMessageProperties ().getDeliveryTag (), false, false);
            System.out.println ("授权相关信息处理失败, 进入死信队列记录日志");
        }
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/407019.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VBA语言専攻资料周末新增

各位T3学员∶本周VBA技术资料增加5讲到385讲&#xff0c;看到通知后联络我免费领取资料。成果来之不易&#xff0c;您更新后请说声谢谢&#xff0c;感恩我的成果。MF381&#xff1a;使工作表使用区域适合窗口MF382&#xff1a;引用复制后的工作表MF383&#xff1a;处理Excel中存…

外星文明会是朋友还是敌人?科学家用AI模拟揭示惊人答案!

引言&#xff1a;人类与外星文明的潜在互动 自古以来&#xff0c;人类就对外太空充满了好奇与向往&#xff0c;无数科幻作品中都描绘了人类与外星文明的潜在互动。然而&#xff0c;这些互动并非总是和平友好的&#xff0c;正如物理学家Stephen Hawking所警告的&#xff0c;盲目…

Linux安装jdktomcatMySQl一战完成

一、jdk安装具体步骤 1、查询是否有jdk java -version 2、进入opt目录 cd /opt/ 连接服务器工具 进入opt目录&#xff0c;把压缩文件上传 查询是否查询成功 进入解压到的目录 cd /usr/local/创建新文件夹 mkdir java 再回到opt目录进行解压 cd /opt 解压到刚刚创建的文…

如何使用 NFTScan NFT API 在 Mantle 网络上开发 Web3 应用

Mantle Network 是建立在以太坊区块链之上的第 2 层扩展解决方案&#xff0c;采用了 Optimistic Rollups 技术&#xff0c;由 BitDAO 孵化&#xff0c;以提供比以太坊更快速和更经济的交易体验。由于 Mantle 基础链构建在 OP Stack 之上并与 EVM 兼容&#xff0c;因此以太坊网络…

LeetCode 102. 二叉树的层序遍历

题目链接https://leetcode.cn/problems/binary-tree-level-order-traversal/description/ 通过队列实现层序遍历 class Solution {public List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> res new ArrayList<>();Queue<…

读书笔记之《巨富》:如何成为全球顶级富豪?

《巨富—全球超级新贵的崛起和其他人的没落》的作者是[美]克里斯蒂娅•弗里兰&#xff0c; 原作名: Plutocrats: The Rise of the New Global Super-Rich and the Fall of Everyone Else &#xff0c;2013年出版。 克里斯蒂娅•弗里兰&#xff08;Chrystia Freeland&#xff…

Linux使用C语言获取进程信息

Linux使用C语言获取进程信息 Author: OnceDay Date: 2024年2月22日 漫漫长路&#xff0c;才刚刚开始… 全系列文章可查看专栏: Linux实践记录_Once_day的博客-CSDN博客 参考文档: Linux proc目录详解_/proc/mounts-CSDN博客Linux下/proc目录介绍 - 知乎 (zhihu.com)Linux内…

Android 如何添加自定义字体

Android 如何添加自定义字体 比如我要添加 jetbrains 相关字体 在 res 文件夹中添加 font 文件夹。里面放入你的字体文件 .ttf .otf&#xff0c;字体文件名需要是小写&#xff0c;只能是字母和下划线。 在 xml 布局文件中直接通过 android:fontFamily"font/jetbrainsmo…

Python实战:xlsx文件的读写

Python实战&#xff1a;xlsx文件的读写 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程 &#x1f448; 希望得到您的订阅和支持~ &#…

基于java Springboot实现教务管理系统

基于java Springboot实现教务管理系统《视频版-建议收藏》 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言 文…

Springboot集成prometheus快速入门demo

一、介绍 prometheus Prometheus 是由前 Google 工程师从 2012 年开始在 Soundcloud 以开源软件的形式进行研发的系统监控和告警工具包&#xff0c;自此以后&#xff0c;许多公司和组织都采用了 Prometheus 作为监控告警工具。Prometheus 的开发者和用户社区非常活跃&#xff0…

【深度学习笔记】3_5 图像分类数据集fashion-mnist

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 3.5 图像分类数据集&#xff08;Fashion-MNIST&#xff09; 在介绍softmax回归的实现前我们先引入一个多类图像分类数据集。它将在后面的章节中被多次使用&#xff0c…

抖音视频评论数据提取软件|抖音数据抓取工具

一、开发背景&#xff1a; 在业务需求中&#xff0c;我们经常需要下载抖音视频。然而&#xff0c;在网上找到的视频通常只能通过逐个复制链接的方式进行抓取和下载&#xff0c;这种操作非常耗时。我们希望能够通过关键词自动批量抓取并选择性地下载抖音视频。因此&#xff0c;为…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的犬种识别系统(附完整代码资源+UI界面+PyTorch代码)

摘要&#xff1a;本文介绍了一种基于深度学习的犬种识别系统系统的代码&#xff0c;采用最先进的YOLOv8算法并对比YOLOv7、YOLOv6、YOLOv5等算法的结果&#xff0c;能够准确识别图像、视频、实时视频流以及批量文件中的犬种。文章详细解释了YOLOv8算法的原理&#xff0c;并提供…

RabbitMQ-消息队列:发布确认高级

18、发布确认高级 在生产环境中由于一些不明原因&#xff0c;导致 RabbitMQ 重启&#xff0c;在 RabbitMQ 重启期间生产者消息投递失败&#xff0c; 导致消息丢失&#xff0c;需要手动处理和恢复。于是&#xff0c;我们开始思考&#xff0c;如何才能进行 RabbitMQ 的消息可靠投…

零基础手把手教你创建微信小程序(二)·创建第一个微信小程序以及了解小程序代码的构成

零基础手把手教你创建微信小程序&#xff08;一&#xff09;微信小程序开发账号的注册以及开发者工具的安装和使用-CSDN博客 目录 ​编辑 1. 创建微信小程序 1.1 基本信息 1.2 在模拟器上查看项目效果 1.3 在真机上预览项目效果 1.4 主界面的5个组成部分 1.4.1 菜单…

Android studio 下的APK打包失败问题解决办法

嗨&#xff0c;各位小伙伴们&#xff0c;我是你们的好朋友咕噜铁蛋&#xff01;作为移动应用开发者&#xff0c;在使用Android Studio进行APK打包时&#xff0c;有时候可能会遇到各种问题导致打包失败&#xff0c;这给我们的开发工作带来了一定的挑战。今天&#xff0c;我将和大…

Excel 面试题及答案(2)

一、VLOOKUP+IF案例: A1 :根据左侧数据源,按姓名匹配《职级》,仅限用函数,不能做任何辅助A2 :根据左侧数据源,按姓名匹配《部门》,仅限用函数,不能做任何辅助A3 :根据右侧考核规则,匹配《绩效比例》,用函数完成(可适当做辅助的单元格区域) =VLOOKUP(F8,IF({1,0},…

qt波位图

1&#xff0c;QPainter 绘制&#xff0c;先绘制这一堆蓝色的东西, 2&#xff0c;在用定时器&#xff1a;QTimer&#xff0c;配合绘制棕色的圆。用到取余&#xff0c;取整 #pragma once#include <QWidget> #include <QPaintEvent>#include <QTimer>QT_BEGIN_…

小程序配置服务器域名:一步步教你如何设置

小程序配置服务器域名&#xff1a;一步步教你如何设置 在当今数字化时代&#xff0c;小程序已经成为了连接用户与服务的重要桥梁。然而&#xff0c;为了让小程序能够正常地与服务器进行通信&#xff0c;我们需要对小程序进行服务器域名的配置。本文将为大家详细介绍小程序配置…
最新文章