mqtt协议调用示例(包括MQTT一键启动服务+测试工具 MQTTFX云盘下载),对捷顺门禁温感一体机进行人员信息下发

hello, 大家好
我是一只不是在戏精,就是在戏精路上的极品二哈
新年上班第一天,给大家贡献一篇 MQTT 协议使用示例文章
也是本汪自己的一篇实用笔记

本汪先总的说下: MQTT协议进行数据交互,一共有两种方式:第一种,请求时不带任何参数的,走上报订阅,设备直接往服务器上传,我们只需要在程序里订阅固定的主题,就可以接收到实时数据,然后进行解析入库就行(比如后面的接收道闸小门的人员上报打卡实时记录);第二种, 有查询条件的,走下发订阅,请求时把查询条件下发给主题 report1,接收对应数据时,监听对应的主题request1,拿到符合查询条件的数据后进行处理(比如人脸数据下发和根据时间段查询人员打卡记录) report/aaa -> request/aaa,都是一一对应的哈。

一、MQTT 服务安装
windows安装包:
链接:https://pan.baidu.com/s/1wb7yNCeCRbjKxauigoaHHQ
提取码:pswp
解压后直接运行即可:
在这里插入图片描述
在服务中,可以看到对应的 MQTT 服务
在这里插入图片描述

二、MQTT 调用程序

在这里插入图片描述1、 基本原理RRPC 请求消息: 服务器通过 MQTT 下发给设备端的消息;RRPC 响应消息: 设备端通过 MQTT 回复给服务器的消息;RRPC 消息 ID: 服务器为每次 RRPC 调用生成的唯一消息 ID;RRPC 订阅 Topic: 设备端以及服务器订阅 RRPC 消息时传递的 Topic,含有通配符。2、推荐使用的MQTT 测试工具 MQTT.fx ,MQTT.fx 是一个老牌的 MQTT 客户端工具,Azure IoT Hub、AWS IoT、阿里云 IoT 等云服务提供商相关产品文档教程均以 MQTT.fx 为例。个人感觉简洁好用。
官网下载地址:https://mqttfx.jensd.de/index.php/download
本汪使用的是1.7.1,云盘地址:
链接:https://pan.baidu.com/s/1anp7PCgfq3EiCPvuaBD77Q
提取码:66w7

在这里插入图片描述

3、添加MQTT订阅
在Web管理后台的 设备管理 → HTTP/MQTT订阅 菜单,点击创建按钮,如下图所示。
在这里插入图片描述

然后填写上报MQTT服务器的地址和主题。如下图所示:
在这里插入图片描述

项目 填写参考示例
上报方式 MQTT
上报地址 tcp://:@:
MQTT topic <topic_name>/<设备ID>/report
是否启用 勾选
是否上报历史记录 不勾选
是否上传图片 勾选

该教程中设备的ID为5182618e-caaf-45ca-81d5-ae5dce6f8e9f,则MQTT topic为<topic_name>/5182618e-caaf-45ca-81d5-ae5dce6f8e9f/report。
其他字段如、、、和<topic_name>,需要根据MQTT服务器实际情况填写。

4、示例代码:
(1)MQTT 网关, common中的常量

package com.cccc.common.constant;

public class DeviceConstants {
    public static final String GET_DEVICE_INFORMATION = "getDeviceInformation/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/request/abc";
    public static final String OPEN_THE_DOOR = "openTheDoor/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
    public static final String LIST_PEOPLE_INFORMATION_BY_ID =  "listPeopleInformationById/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
    public static final String SELECT_PEOPLE_INFORMATION_BY_FIELD = "selectPeopleInformationByField/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
    public static final String INSERT_PEOPLE = "insertPeople/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/request/abc";
    public static final String ADD_BATCH = "addbatch/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
    public static final String DELETE = "delete/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
    public static final String DELETE_BATCH =  "deleteBatch/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report";
    public static final String GET_CARD_RECORD =  "getCardRecord/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/request/abc";


}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;

/**
 * @ Author: yuezejian
 * @ Description:
 * @ Date: 2021/12/14 11:25
 */
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGateway {
    // 定义重载方法,用于消息发送
    void sendToMqtt(String payload);
    // 指定topic进行消息发送
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, byte[] payload);
}

(2)MQTT config

package com.cccc.framework.config;

import com.cccc.common.constant.DeviceConstants;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;

/**
 * @Description: 订阅并监听MQTT消息,进行相应的处理
 * @Author: yuezejian
 * @Date: 2021/12/14 11:22
 * @Version: 1.0
 **/
@Configuration
public class MQTTConfig {

    private static final Logger LOG = LoggerFactory.getLogger(MQTTConfig.class);
    /**
     * 创建MqttPahoClientFactory,设置MQTT Broker连接属性,如果使用SSL验证,也在这里设置。
     * @return factory
     */
    @Bean
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        MqttConnectOptions options = new MqttConnectOptions();

        // 设置代理端的URL地址,可以是多个
        options.setServerURIs(new String[]{"tcp://192.168.20.48:1883"});

        factory.setConnectionOptions(options);
        return factory;
    }

    /**
     * 入站通道
     */
    @Bean
    public MessageChannel mqttInputChannel() {
        return new DirectChannel();
    }

    /**
     * 入站
     */
    @Bean
    public MessageProducer inbound() {
        // Paho客户端消息驱动通道适配器,主要用来订阅主题
        MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("consumerClient-paho",
                mqttClientFactory(), DeviceConstants.GET_DEVICE_INFORMATION,
                DeviceConstants.OPEN_THE_DOOR,
                DeviceConstants.LIST_PEOPLE_INFORMATION_BY_ID,
                DeviceConstants.SELECT_PEOPLE_INFORMATION_BY_FIELD,
                DeviceConstants.INSERT_PEOPLE,
                DeviceConstants.ADD_BATCH,
                DeviceConstants.DELETE,
                DeviceConstants.DELETE_BATCH,
                DeviceConstants.GET_DEVICE_INFORMATION
               );

        adapter.setCompletionTimeout(5000);

        // Paho消息转换器
        DefaultPahoMessageConverter defaultPahoMessageConverter = new DefaultPahoMessageConverter();
        // 按字节接收消息
//        defaultPahoMessageConverter.setPayloadAsBytes(true);
        adapter.setConverter(defaultPahoMessageConverter);
        adapter.setQos(1); // 设置QoS
        adapter.setOutputChannel(mqttInputChannel());
        return adapter;
    }

    @Bean
    // ServiceActivator注解表明:当前方法用于处理MQTT消息,inputChannel参数指定了用于消费消息的channel。
    @ServiceActivator(inputChannel = "mqttInputChannel")
    public MessageHandler handler() {
        return message -> {
            String payload = message.getPayload().toString();

            // byte[] bytes = (byte[]) message.getPayload(); // 收到的消息是字节格式
            String topic = message.getHeaders().get("mqtt_receivedTopic").toString();

            // 根据主题分别进行消息处理。
            if (topic.equals(DeviceConstants.GET_DEVICE_INFORMATION)) { // 匹配:1/sensor
                String sensorSn = "getDeviceInformation";
                LOG.debug("获取设备信息: " + payload);
            } else if (topic.equals(DeviceConstants.OPEN_THE_DOOR)) {
                LOG.debug("远程开门:" + payload);
            } else if (topic.equals(DeviceConstants.LIST_PEOPLE_INFORMATION_BY_ID)) {
                LOG.debug("根据ID查询录入人员" + payload);
            }  else if (topic.equals(DeviceConstants.SELECT_PEOPLE_INFORMATION_BY_FIELD)) {
                LOG.debug("根据录入信息字段,查询录入人员" + payload);
            }  else if (topic.equals(DeviceConstants.INSERT_PEOPLE)) {
                LOG.debug("新增人员录入" + payload);
            }  else if (topic.equals(DeviceConstants.ADD_BATCH)) {
                LOG.debug("批量添加人员" + payload);
            } else if (topic.equals(DeviceConstants.DELETE)) {
                LOG.debug("删除单个人员" + payload);
            } else if (topic.equals(DeviceConstants.DELETE_BATCH)) {
                LOG.debug("批量删除多个人员录入" + payload);
            } else if (topic.equals(DeviceConstants.GET_CARD_RECORD)) {
                LOG.debug("获取打卡记录" + payload);
            }

        };
    }

    // 发送消息

    /**
     * 出站通道
     */
    @Bean
    public MessageChannel mqttOutboundChannel() {
        return new DirectChannel();
    }

    /**
     * 出站
     */
    @Bean
    @ServiceActivator(inputChannel = "mqttOutboundChannel")
    public MessageHandler outbound() {

        // 发送消息和消费消息Channel可以使用相同MqttPahoClientFactory
        MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler("publishClient", mqttClientFactory());
        messageHandler.setAsync(true); // 如果设置成true,即异步,发送消息时将不会阻塞。
        messageHandler.setDefaultTopic("command");
        messageHandler.setDefaultQos(1); // 设置默认QoS

        // Paho消息转换器
        DefaultPahoMessageConverter defaultPahoMessageConverter = new DefaultPahoMessageConverter();

        // defaultPahoMessageConverter.setPayloadAsBytes(true); // 发送默认按字节类型发送消息
        messageHandler.setConverter(defaultPahoMessageConverter);
        return messageHandler;
    }
}

(3)控制层,包括道闸小门门禁一体机人脸下发和获取打卡记录 2 个调用示例。


/**
 * @Description: 道闸小门门禁一体机数据交互控制层
 * @Author: yuezejian
 * @Date: 2021/12/14 11:28
 * @Version: 1.0
 **/
@RestController
@RequestMapping("/mqtt")
public class MqttController extends BaseController {

    @Autowired
    private MqttGateway mqttGateway;

    @Autowired
    private ICcccHealthyEquipmentinfoService ccccHealthyEquipmentinfoService;

    @Autowired
    private Environment env;


    /**
     * 下发人脸
     * @return
     * @throws IOException
     */
    @PostMapping("/send")
    public String send() throws IOException {
        // 发送消息到指定主题
        ccccHealthyEquipmentinfoService.insertPersionInformation(new DeviceDto(1,"face.set"));
        return "人脸批量新增" ;
    }

    /**
     * 获取道小门的打卡记录
     * @return
     * @throws IOException
     */
    @PostMapping("/record")
    public String record() throws IOException {
        // 发送消息到指定主题
        ccccHealthyEquipmentinfoService.recordGet(new DeviceDto(2,"faceRecord.bulkGet"));
        return "获取打卡记录" ;
    }

}

(4)服务层

/**
 * 设备信息Service接口
 * 
 * @author jiezhaokai
 * @date 2021-12-20
 */
public interface ICcccHealthyEquipmentinfoService 
{
    /**
     * 闸道小门数据全员下发
     * @param deviceDto
     * @throws IOException
     */
    public void insertPersionInformation(DeviceDto deviceDto) throws IOException;

    /**
     * get record from outdoor
     * @param deviceDto
     * @throws IOException
     */
    public void recordGet(DeviceDto deviceDto) throws IOException;

    /**
     * 闸道小门数据下发通过用户id
     * @param deviceDto
     * @throws IOException
     */
    public void insertPersionInformationByUserId(DeviceDto deviceDto, Long userId) throws IOException;

}

(5)服务层具体实现

/**
 * 设备信息Service业务层处理
 * 
 * @author jiezhaokai
 * @date 2021-12-20
 */
@Service
public class CcccHealthyEquipmentinfoServiceImpl implements ICcccHealthyEquipmentinfoService 
{
/**
     * 闸道小门数据全员下发
     * @param deviceDto
     * @throws IOException
     */
    @Override
    public void insertPersionInformation(DeviceDto deviceDto) throws IOException {
        //查询员工

        //
        List<EmployeeUserNameAndJobNumberAndFaceImage>  faceImageList = ccccCheckonworkDeviceMapper.getEmployeeUserNameAndJobNumberAndFaceImage();
        faceImageList.forEach( i -> {
            if (StringUtils.isNotEmpty(i.getImageUrl()) && StringUtils.isNotEmpty(i.getJobNumber())
                    && StringUtils.isNotEmpty(i.getUserName()) && i.getCardNo() != null) {

            Image image = new Image();
            image.setType("URL");
            // 图片地址拼接
            image.setUrl("http://192.168.20.48:8088/prod-api/faceimage/faceRegisterImage/"+i.getImageUrl());
            PersonInformationDto personInformationDto = new PersonInformationDto();
            personInformationDto.setId(0);
            personInformationDto.setImage(image);
            personInformationDto.setTrdID(Integer.valueOf(i.getJobNumber().replaceAll("[a-zA-Z]","" )));
            personInformationDto.setName(i.getUserName());
            //赋值卡号
                if ( i.getCardNo()< 1000000000) {
                    String cardNo = "0" +  i.getCardNo();
                    personInformationDto.setAccessCardNo(cardNo);
                } else {
                    String cardNo = i.getCardNo()+"";
                    personInformationDto.setAccessCardNo(cardNo);
                }
            deviceDto.setParams(JSONObject.parseObject(JSONObject.toJSON(personInformationDto).toString()));
            MyMqttMessage myMqttMessage = new MyMqttMessage();
            myMqttMessage.setTopic(DeviceConstants.INSERT_PEOPLE);
            myMqttMessage.setContent(JSON.toJSONString(deviceDto));
            mqttGateway.sendToMqtt(myMqttMessage.getTopic(), 1, myMqttMessage.getContent());
                LOG.info("闸道小门人脸下发成功,ip为192.168.20.34, 员工名字为:"+ i.getUserName()+ ",员工工号为:"+ i.getJobNumber());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });

    }

    /**
     * 获取道闸小门门禁一体机打卡记录
     * @param deviceDto
     * @throws IOException
     */
    @Override
    public void recordGet(DeviceDto deviceDto) throws IOException {
        RecordHistoryDto recordHistoryDto = new RecordHistoryDto();
        //每次获取当前时间前的两个小时内的数据,因此定时任务的执行时间也应当是没两小时执行一次
        String startDate = DateUtils.getDate() +"T"+ DateUtils.dateRoll(6) + ".32+08:00";
        String endDate = DateUtils.getDate() +"T"+ DateUtils.dateRoll(0)+ ".32+08:00";
        recordHistoryDto.setStartDate(startDate);
        recordHistoryDto.setEndDate(endDate);
        recordHistoryDto.setLimit(30);
        recordHistoryDto.setPage(1);
        recordHistoryDto.setSortBy("id");
        recordHistoryDto.setSortOrder("asc");
        recordHistoryDto.setWithImage(false);
        deviceDto.setParams((JSONObject)JSONObject.toJSON(recordHistoryDto));
        MyMqttMessage myMqttMessage = new MyMqttMessage();
        myMqttMessage.setTopic(DeviceConstants.GET_CARD_RECORD);
        myMqttMessage.setContent(JSON.toJSONString(deviceDto));
        System.out.println(myMqttMessage.toString());
        mqttGateway.sendToMqtt(myMqttMessage.getTopic(), 1, myMqttMessage.getContent());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
/**
     * 每次新增人脸记录时,对道闸小门的门禁一体机进行人脸下发。
     * @param deviceDto
     * @param userId
     * @throws IOException
     */
    @Override
    @Async("threadPoolTaskExecutor1")
    public void insertPersionInformationByUserId(DeviceDto deviceDto, Long userId) throws IOException {
        //查询员工

        //
        List<EmployeeUserNameAndJobNumberAndFaceImage>  faceImageList = ccccCheckonworkDeviceMapper.getEmployeeUserNameAndJobNumberAndFaceImageByUserId(userId);
        //如果当前员工还没有卡号,则工号去掉字母,开头拼接0为其卡号,for example: SC9879 -> 09879
        if (faceImageList.get(0).getCardNo() == null) {
        faceImageList.get(0).setCardNo(Long.valueOf(faceImageList.get(0).getJobNumber().replaceAll("[a-zA-Z]","" )));
        }
        faceImageList.forEach( i -> {
            if (StringUtils.isNotEmpty(i.getImageUrl()) && StringUtils.isNotEmpty(i.getJobNumber())
                    && StringUtils.isNotEmpty(i.getUserName()) && i.getCardNo() != null) {

                Image image = new Image();
                image.setType("URL");
                // 图片地址拼接
                image.setUrl("http://192.168.20.48:8088/prod-api/faceimage/faceRegisterImage/"+i.getImageUrl());
                PersonInformationDto personInformationDto = new PersonInformationDto();
                personInformationDto.setId(0);
                personInformationDto.setImage(image);
                personInformationDto.setTrdID(Integer.valueOf(i.getJobNumber().replaceAll("[a-zA-Z]","" )));
                personInformationDto.setName(i.getUserName());
                //赋值卡号
                if ( i.getCardNo()< 1000000000) {
                    String cardNo = "0" +  i.getCardNo();
                    personInformationDto.setAccessCardNo(cardNo);
                } else {
                    String cardNo = i.getCardNo()+"";
                    personInformationDto.setAccessCardNo(cardNo);
                }
                deviceDto.setParams(JSONObject.parseObject(JSONObject.toJSON(personInformationDto).toString()));
                MyMqttMessage myMqttMessage = new MyMqttMessage();
                myMqttMessage.setTopic(DeviceConstants.INSERT_PEOPLE);
                myMqttMessage.setContent(JSON.toJSONString(deviceDto));
                mqttGateway.sendToMqtt(myMqttMessage.getTopic(), 1, myMqttMessage.getContent());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });

    }
}

5、配置MQTT协议监听对应的主题


import com.alibaba.fastjson.JSONObject;
import com.cccc.business.duty.ondutyhistoryrecord.service.ICcccCheckonworkHistoryRecordService;
import com.cccc.common.core.domain.entity.SysUser;
import com.cccc.common.utils.StringUtils;
import com.cccc.framework.config.BeanWiredAdvice;
import com.cccc.system.mapper.SysUserMapper;
import com.google.gson.JsonObject;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 道闸小门打卡记录监听,MQTT协议
 * @auth yuezejian
 */
//@Component
public class MqttListener implements ApplicationRunner, Ordered {

    @Autowired
    private Environment env;

    public static final Logger LOG = LoggerFactory.getLogger(MqttListener.class);

    /* 打卡历史记录的服务层 */
    ICcccCheckonworkHistoryRecordService ccccCheckonworkHistoryRecordService = BeanWiredAdvice
            .getBean(ICcccCheckonworkHistoryRecordService.class);

    private SysUserMapper sysUserMapper = BeanWiredAdvice.getBean(SysUserMapper.class);

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //MQTT协议前缀 urlFrontSuffix = tcp://:@192.168.20.48:1883
        String urlFrontSuffix = env.getProperty("barriergate.urlFrontSuffix", String.class);
        String clienId = String.valueOf(System.currentTimeMillis());
        System.out.println(clienId);
        //Topic前缀  allTopic = record/ba3ec3e4-2e1f-4e8d-9ff8-e0235f6e1f41/report
        String allTopic = env.getProperty("barriergate.allTopic", String.class);
        try {
            MemoryPersistence memoryPersistence = new MemoryPersistence();
            MqttClient client = new MqttClient(urlFrontSuffix, clienId, memoryPersistence);
            MqttConnectOptions options = new MqttConnectOptions();
            // 设置客户端和服务器是否应在重新启动和重新连接期间记住状态 默认false
            options.setCleanSession(true);
            // 设置会话心跳时间
            options.setKeepAliveInterval(20);
            // 设置超时时间
            options.setConnectionTimeout(10);
            // 设置回调函数,当订阅到信息时调用此方法
            client.setCallback(new MqttCallback() {
                @Override
                public void connectionLost(Throwable throwable) {
                    // 连接失败时调用  重新连接订阅
                    try {
                        System.out.println("开始重连");
                        Thread.sleep(3000);
                        client.connect(options);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (MqttSecurityException e) {
                        e.printStackTrace();
                    } catch (MqttException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
                    // 订阅成功,并接受信息时调用
                    String payload = new String(mqttMessage.getPayload()); // 获取消息内容
                    JSONObject pa=JSONObject.parseObject(payload);
                    String person = pa.getString("person");
                    if (StringUtils.isNotEmpty(person)) {
                        JSONObject personObject = JSONObject.parseObject(person);
                        String jobNum = personObject.getString("trdID");
                        if (StringUtils.isNotEmpty(jobNum)) {
                            SysUser sysUser = sysUserMapper.selectUserByJobNum(jobNum);
                            ccccCheckonworkHistoryRecordService.insertCcccCheckonworkHistoryRecordEntranceGuard(sysUser.getUserId(),env.getProperty("barriergate.ip", String.class),new Date(),0);

                        }
                    }
                    LOG.debug("MqttAccept_________"+ payload);
                }

                @Override
                public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
                    LOG.debug("deliveryComplete");
                }
            });
            client.connect(options);
            client.subscribe(allTopic,0);
            System.out.println("连接成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public int getOrder() {
        return 3;
    }
}

最后附件1,这是协议的部分内容:
一个设备只需要一个上报主题和一个下发主题即可。调用不同的内容,对应的RPC 方法是不同的。
在这里插入图片描述

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖啡汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值