钉钉机器人SDK 封装预警消息发送工具

1 群机器人

    (1) 引言
    钉钉聊天群内支持的群机器人, 类似QQ 群机器人, 可以发天气, 讲笑话那样;
    钉钉群机器人支持自定义机器人, 允许开发者管理机器人做预警消息通知;
    (2) 限制

  • 6 个机器人/每群, 20条消息/每分钟
  • 不支持应答模式, 仅做群消息通知

    (3) 消息格式
    支持普通文本消息, 链接消息, markdown 格式(注意仅部分语法支持)文本, 图片及链接, 支持FeedCard, ActionCard 等消息
    (4) 官方开发文档
    https://open-doc.dingtalk.com/microapp/serverapi3/iydd5h

2 自定义机器人
2.1 创建机器人

    自定义机器人依赖于钉钉群, 首先创建钉钉群(群主有权限查看access_token), 拿到webhook 信息, 通过网络发送请求, 钉钉开放平台响应请求发送预警消息到指定access_token 的钉钉群
    钉钉群创建方式是在聊天页面, 点击"+"添加其他人来创建一个钉钉群.
在这里插入图片描述

图2-1. 钉钉群创建图

    进入群, 打开设置, 选中群机器人, 创建自定义机器人, 完善机器人基本信息后创建完成, 机器人自动推送一条欢迎语到钉钉群.
    支持配置安全设置,来保障信息安全,设置方式包括自定义关键词校验、加签密钥校验、IP地址白名单校验;此处以加签为例。
在这里插入图片描述

图2-2. 添加自定义机器人图

在这里插入图片描述

图2-3. 创建自定义机器人图

在这里插入图片描述

图2-4. 群机器人欢迎语图
    查看群机器人, 可以看到当前钉钉群已有的机器人列表, 可以管理添加机器人, 移除机器人, 编辑已有机器人名称,头像; 其中群机器人支持关闭开启消息推送和指定webhook 重置功能. webhook 可以理解为access_token 是身份标识, 此身份标识了指定群, 也标识了拥有群机器人消息推送的权限.

在这里插入图片描述

图2-5. 群机器人管理页面图
2.2 Postman 通过webhook 发送消息演示

    此处使用Postman 模拟请求, 如下所示, 发送普通文本消息后, 钉钉群会收到机器人的预警消息.(此处为旧版本未加签时的钉钉消息发送测试图,仅作演示参考)
在这里插入图片描述

图2-6. Postman 通过webhook 发送普通文本消息图

图2-7. 群机器人发送消息成功图
3 创建项目引入钉钉机器人SDK

    本机开发环境win10, 已创建钉钉群, 并获取到webhook, 本机使用的IDEA, 下载的Java SDK

3.1 创建Maven 项目引入SDK

    (1)Java SDK 下载
    下载SDK: https://open-doc.dingtalk.com/microapp/faquestions/vzbp02, 官方支持以下版本, 此处使用Java SDK;

  • JAVA版本
  • PHP版本
  • .NET版
  • Python版
        解压后是官方直接打好的jar 包, 也包括源码包

    (2)创建Maven 项目引入Jar
    创建Maven 项目引入打好的jar 包: 本机jar 包地址: D:\download\dingtalk-sdk-java\taobao-sdk-java-auto_1479188381469-20190704.jar

  • mvn 命令

mvn install:install-file
-Dfile=D:\download\dingtalk-sdk-java\taobao-sdk-java-auto_1479188381469-20190704.jar -DgroupId=com.dingtalk -DartifactId=com-dingtalk-api
-Dversion=1.0.0 -Dpackaging=jar

  • 参数说明

-Dfile jar包所在路径,需要包含jar包名.此处D:\download\dingtalk-sdk-java\taobao-sdk-java-auto_1479188381469-20190704.jar
-DgroupId 指定导入jar时的groupid,可以自定义,此处com.dingtalk
-DartifactId指定导入jar时的artifactId,可以自定义,此处com-dingtalk-api
-Dversion 指定导入jar时的版本号,可以自定义,此处1.0.0
-Dpackaging 指定文件类型 ,由于这里是jar包的形式,所以这里得是jar

[外链图片转存失败(img-GQS8mBgB-1562409744840)(https://raw.githubusercontent.com/niaonao/ImageIcon/master/IDEAProject/DingTalk/dingding008.png)]

图3-1. 项目引入SDK 图
    (3)配置依赖, 如果项目没有自动引入依赖,可以点击reimport 重新加载依赖

在这里插入图片描述

图3-2. 重新加载依赖图
3.2 预警消息发送工具类封装
3.2.1 工具类说明

    项目成功引入SDK, 支持通过SDK 指定自定义机器人去发送群消息, 前面有说明群机器人支持发送多种类型的消息, 也支持消息带跳转链接, 或者图片带跳转链接等;
    工具类创建客户端client 实例需要拥有正确的webhook, 就是图2-5. 群机器人管理页面图中群设置中可以查看或者重置的hook 标识.
    此处安全设置了加签,需要对请求服务链接做一次封装;维护时间戳及签名;签名通过创建机器人时的密钥进行加密转换获得。密钥如图2-3. 创建自定义机器人图中加签设置下以SEC开头的密钥串。
https://oapi.dingtalk.com/robot/send?access_token=xxx&timestamp=xxx&sign=xxx
加密方法如下

    /**
     * 钉钉群设置 webhook, 支持重置
     */
    private static final String ACCESS_TOKEN = "https://oapi.dingtalk.com/robot/send?access_token=40d2e1ef8c83b0ade5c7d2ae43553988c68373c1fa0901dcd701b0c2f5f90c59";
    /**
     * 加签密钥,支持重置
     */
    private static final String SECRET = "SECc90f8dac81401632962362d280f79a86e2875d8fa2282c7ee80249385be76b38";
    /**
     * 安全设置:是否加签
     */
    private static boolean isSign = true;

    /**
     * 客户端实例
     */
    public static DingTalkClient client = new DefaultDingTalkClient(getServerUrl(ACCESS_TOKEN, SECRET));
    
    /**
     * @param: accessToken  token
     * @param: secret       加签密钥
     * @description: 获取 serverUrl
     * @return: java.lang.String
     * @author: niaonao
     */
    private static String getServerUrl(String accessToken, String secret) {
        StringBuilder serverUrl = new StringBuilder(accessToken);
        // isSign 是否加签
        if (!isSign) {
            return serverUrl.toString();
        }
        try {
            Long timestamp = System.currentTimeMillis();
            String stringToSign = timestamp + "\n" + secret;
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
            byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
            String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)),"UTF-8");

            serverUrl.append("&timestamp=").append(timestamp)
                    .append("&sign=").append(sign);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return serverUrl.toString();
    }

    util 下即封装的工具类RobotHelperUtil.java, 其中test 包下有做单元测试
在这里插入图片描述

图3-3. 项目结构图
3.3.2 测试发送消息

    此处单元测试测试方法sendMessageByMarkdown(), sendMessageByActionCardMulti(), sendMessageByFeedCard() 等方法, 群机器人会发送群消息, 其中方法参数title 效果如图中左侧(联系人列表页面)透出的消息内容, text 是进入群后的真正的消息.;

在这里插入图片描述

图3-4. 单元测试消息发送方法图

在这里插入图片描述

图3-5. sendMessageByMarkdown 消息发送成功图

    此处发送的支持单独跳转的消息, 初始化了三个按钮附带跳转链接, 按钮默认纵向排列. 点击按钮即可跳转指定链接查看详细内容.
在这里插入图片描述

图3-6. sendMessageByActionCardMulti 消息发送成功图

    此处发送的支持多个Link 的消息, 初始化了三个link , 指定标题导图及跳转链接; 首个link 效果更突出, 这个还挺好玩的.
在这里插入图片描述

图3-7. sendMessageByFeedCard 消息发送成功图

    钉钉消息支持配置内容、图片跳转链接,多为钉钉内部打开查看;同时有提供钉钉打开外部链接的消息类型 msgType:link,测试类的方法 sendMessageByLink,运行结果如下图所示。
在这里插入图片描述

图3-8. sendMessageByLink 外部链接消息发送成功图
3.3 项目源码
  • 枚举类 MsgTypeEnum.java
package pers.niaonao.dingtalkrobot.enums;

/**
 * @className: MsgTypeEnum
 * @description: 消息类型枚举嘞
 * @date: 2021/6/15
 * @author niaonao
 **/
public enum MsgTypeEnum {

    MSG_TYPE_TEXT("text", "MSG_TYPE_TEXT"),
    MSG_TYPE_LINK("link", "MSG_TYPE_LINK"),
    MSG_TYPE_MARKDOWN("markdown", "MSG_TYPE_MARKDOWN"),
    MSG_TYPE_ACTION_CARD("actionCard", "MSG_TYPE_ACTION_CARD"),
    MSG_TYPE_FEED_CARD("feedCard", "MSG_TYPE_FEED_CARD");

    private String value;
    private String code;

    MsgTypeEnum(String value, String code) {
        this.code = code;
        this.value = value;
    }

    public static MsgTypeEnum getEnumValue(String value){
        for (MsgTypeEnum constants : values()) {
            if (constants.getValue() == value) {
                return constants;
            }
        }
        return null;
    }

    public String getValue() {
        return value;
    }

    public String getCode() {
        return code;
    }
}
  • 工具类 DataValidUtil.java:
package pers.niaonao.dingtalkrobot.util;

import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Field;

/**
 * @className: DataValidUtil
 * @description: 数据基础校验工具
 * @author: niaonao
 * @date: 2019/7/6
 **/
@Slf4j
public class DataValidUtil {
    /**
     * @description: 判空方法
     * @param obj
     * @return: boolean
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static boolean checkNotEmpty(Object... obj) {
        for (Object object : obj) {
            if (null == object) {
                return false;
            }
        }
        return true;
    }

    /**
     * @description: 校验接口入参对象方法
     *      校验object 对象是否全部属性都不为null
     * @param object
     * @return: boolean
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static boolean checkExistEmpty(Object object) {
        if (null == object) {
            return false;
        }
        boolean flag = false;
        Class clazz = object.getClass();
        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            //设置权限, 获取private的属性
            field.setAccessible(true);
            Object fieldValue = null;
            try {
                fieldValue = field.get(object);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                log.error("对象属性值为空校验, 异常捕获{}", e.getMessage());
            }
            //只要有一个属性值不为null 就返回false 表示对象不为null
            if (null == fieldValue) {
                flag = true;
                break;
            }
        }
        return flag;
    }
}

钉钉消息发送工具类 RobotHelperUtil.java

package pers.niaonao.dingtalkrobot.util;

import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest ;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.TaobaoRequest;
import com.taobao.api.TaobaoResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import pers.niaonao.dingtalkrobot.enums.MsgTypeEnum;

import java.net.URLEncoder;
/**
 * @className: RobotHelperUtil
 * @description: 机器人工具类
 *      每个机器人每分钟最多发送20条
 *      限制6 个机器人/群
 *      https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71
 * @author: niaonao
 * @date: 2019/7/6
 **/
@Slf4j
public class RobotHelperUtil {

    /**
     * 钉钉群设置 webhook, 支持重置
     */
    private static final String ACCESS_TOKEN = "https://oapi.dingtalk.com/robot/send?access_token=40d2e1ef8c83b0ade5c7d2ae43553988c68373c1fa0901dcd701b0c2f5f90c59";
    /**
     * 加签密钥,支持重置
     */
    private static final String SECRET = "SECc90f8dac81401632962362d280f79a86e2875d8fa2282c7ee80249385be76b38";
    /**
     * 安全设置:是否加签
     */
    private static boolean isSign = true;

    /**
     * 客户端实例
     */
    public static DingTalkClient client = new DefaultDingTalkClient(getServerUrl(ACCESS_TOKEN, SECRET));

    /**
     * @description: 官方演示示例
     *      title 是消息列表下透出的标题
     *      text 是进入群后看到的消息内容
     *
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static void sdkDemoJava() {
        OapiRobotSendRequest request = new OapiRobotSendRequest();
        request.setMsgtype("text");
        OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
        text.setContent("测试文本消息");
        request.setText(text);
        OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
        at.setAtMobiles(Arrays.asList("13261303345"));
        request.setAt(at);

        request.setMsgtype("link");
        OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link();
        link.setMessageUrl("https://www.dingtalk.com/");
        link.setPicUrl("");
        link.setTitle("时代的火车向前开");
        link.setText("这个即将发布的新版本,创始人陈航(花名“无招”)称它为“红树林”。\n" +
                "而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是“红树林");
        request.setLink(link);

        request.setMsgtype("markdown");
        OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
        markdown.setTitle("杭州天气");
        markdown.setText("#### 杭州天气 @156xxxx8827\n" +
                "> 9度,西北风1级,空气良89,相对温度73%\n\n" +
                "> ![screenshot](https://gw.alipayobjects.com/zos/skylark-tools/public/files/84111bbeba74743d2771ed4f062d1f25.png)\n"  +
                "> ###### 10点20分发布 [天气](http://www.thinkpage.cn/) \n");
        request.setMarkdown(markdown);
        requestExecute(request);
    }

    /**
     * @description: 发送普通文本消息
     * @param content   文本消息
     * @param mobileList    指定@ 联系人
     * @param isAtAll       是否@ 全部联系人
     * @return: com.dingtalk.api.response.OapiRobotSendResponse
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static OapiRobotSendResponse sendMessageByText(String content, List<String> mobileList, boolean isAtAll) {
        if (StringUtils.isEmpty(content)) {
            return null;
        }

        OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
        at.setIsAtAll(isAtAll);
        //atMobiles	被@人的手机号
        if (!CollectionUtils.isEmpty(mobileList)) {
            at.setAtMobiles(mobileList);
            if (!isAtAll) {
                content = content + "\n";
                for (String mobile : mobileList) {
                    content = content + "@" + mobile;
                }
            }
        }
        //参数	参数类型	必须	说明
        OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
        text.setContent(content);
        OapiRobotSendRequest request = new OapiRobotSendRequest();
        request.setAt(at);
        request.setMsgtype(MsgTypeEnum.MSG_TYPE_TEXT.getValue());
        request.setText(text);

        OapiRobotSendResponse response = (OapiRobotSendResponse) requestExecute(request);
        return response;
    }

    /**
     * @description: 发送link 类型消息,点击标题实现在钉钉外部打开链接
     * @param title 消息标题
     * @param text  消息内容
     * @param messageUrl     点击消息后跳转的url
     * @param picUrl    插入图片的url
     * @return: com.dingtalk.api.response.OapiRobotSendResponse
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static OapiRobotSendResponse sendMessageByLink(String title, String text, String messageUrl, String picUrl) {
        if (!DataValidUtil.checkNotEmpty(title, text, messageUrl)) {
            return null;
        }
        OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link();
        link.setTitle(title);
        link.setText(text);
        link.setMessageUrl(messageUrl);
        link.setPicUrl(picUrl);

        OapiRobotSendRequest request = new OapiRobotSendRequest();
        request.setMsgtype(MsgTypeEnum.MSG_TYPE_LINK.getValue());
        request.setLink(link);

        OapiRobotSendResponse response = (OapiRobotSendResponse) requestExecute(request);
        return response;
    }


    /**
     * @description: 发送Markdown 编辑格式的消息
     * @param title 标题
     * @param markdownText  支持markdown 编辑格式的文本信息
     * @param mobileList    消息@ 联系人
     * @param isAtAll   是否@ 全部
     * @return: com.dingtalk.api.response.OapiRobotSendResponse
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static OapiRobotSendResponse sendMessageByMarkdown(String title, String markdownText, List<String> mobileList, boolean isAtAll) {
        if (!DataValidUtil.checkNotEmpty(title, markdownText)) {
            return null;
        }
        OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
        at.setIsAtAll(isAtAll);
        if (!CollectionUtils.isEmpty(mobileList)) {
            at.setAtMobiles(mobileList);
            if (!isAtAll) {
                markdownText = markdownText + "\n";
                for (String mobile : mobileList) {
                    markdownText = markdownText + "@" + mobile;
                }
            }
        }
        OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown();
        markdown.setTitle(title);
        markdown.setText(markdownText);

        OapiRobotSendRequest request = new OapiRobotSendRequest();
        request.setMsgtype(MsgTypeEnum.MSG_TYPE_MARKDOWN.getValue());
        request.setMarkdown(markdown);
        request.setAt(at);

        OapiRobotSendResponse response = (OapiRobotSendResponse) requestExecute(request);
        return response;
    }

    /**
     * @description: 整体跳转ActionCard类型的消息发送
     * @param title 消息标题, 会话消息会展示标题
     * @param markdownText  markdown格式的消息
     * @param singleTitle   单个按钮的标题
     * @param singleURL 单个按钮的跳转链接
     * @param btnOrientation    是否横向排列(true 横向排列, false 纵向排列)
     *      参数	类型	必选	说明
     *      msgtype	string	true	此消息类型为固定actionCard
     *      title	string	true	首屏会话透出的展示内容
     *      text	string	true	markdown格式的消息
     *      singleTitle	string	true	单个按钮的方案。(设置此项和singleURL后btns无效)
     *      singleURL	string	true	点击singleTitle按钮触发的URL
     *      btnOrientation	string	false	0-按钮竖直排列,1-按钮横向排列
     * @return: com.dingtalk.api.response.OapiRobotSendResponse
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static OapiRobotSendResponse sendMessageByActionCardSingle(String title, String markdownText, String singleTitle, String singleURL, boolean btnOrientation) {
        if (!DataValidUtil.checkNotEmpty(title, markdownText)) {
            return null;
        }
        OapiRobotSendRequest.Actioncard actionCard = new OapiRobotSendRequest.Actioncard();
        actionCard.setTitle(title);
        actionCard.setText(markdownText);
        actionCard.setSingleTitle(singleTitle);
        actionCard.setSingleURL(singleURL);
        // 此处默认为0
        actionCard.setBtnOrientation(btnOrientation ? "1" : "0");

        OapiRobotSendRequest request = new OapiRobotSendRequest();
        request.setMsgtype(MsgTypeEnum.MSG_TYPE_ACTION_CARD.getValue());
        request.setActionCard(actionCard);
        OapiRobotSendResponse response = (OapiRobotSendResponse) requestExecute(request);
        return response;
    }

    /**
     * @description: 独立跳转ActionCard类型 消息发送
     * @param title 标题
     * @param markdownText  文本
     * @param btns  按钮列表
     * @param btnOrientation    是否横向排列(true 横向排列, false 纵向排列)
     *      参数	类型	必选	说明
     *      msgtype	string	true	此消息类型为固定actionCard
     *      title	string	true	首屏会话透出的展示内容
     *      text	string	true	markdown格式的消息
     *      btns	array	true	按钮的信息:title-按钮方案,actionURL-点击按钮触发的URL
     *      btnOrientation	string	false	0-按钮竖直排列,1-按钮横向排列
     * @return: com.dingtalk.api.response.OapiRobotSendResponse
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static OapiRobotSendResponse sendMessageByActionCardMulti(String title, String markdownText, List<OapiRobotSendRequest.Btns> btns, boolean btnOrientation) {
        if (!DataValidUtil.checkNotEmpty(title, markdownText) || CollectionUtils.isEmpty(btns)) {
            return null;
        }
        OapiRobotSendRequest.Actioncard actionCard = new OapiRobotSendRequest.Actioncard();
        actionCard.setTitle(title);
        actionCard.setText(markdownText);
        // 此处默认为0
        actionCard.setBtnOrientation(btnOrientation ? "1" : "0");

        actionCard.setBtns(btns);

        OapiRobotSendRequest request = new OapiRobotSendRequest();
        request.setMsgtype(MsgTypeEnum.MSG_TYPE_ACTION_CARD.getValue());
        request.setActionCard(actionCard);
        OapiRobotSendResponse response = (OapiRobotSendResponse) requestExecute(request);
        return response;
    }

    /** 
     * @description: 发送FeedCard类型消息
     *      msgtype	string	true	此消息类型为固定feedCard
     *      title	string	true	单条信息文本
     *      messageURL	string	true	点击单条信息到跳转链接(在钉钉内部打开,不能跳转到外部)
     *      picURL	string	true	单条信息后面图片的URL
     * @param links
     * @return: com.dingtalk.api.response.OapiRobotSendResponse
     * @author: niaonao
     * @date: 2019/7/6
     */
    public static OapiRobotSendResponse sendMessageByFeedCard(List<OapiRobotSendRequest.Links> links) {
        if (CollectionUtils.isEmpty(links)) {
            return null;
        }

        OapiRobotSendRequest.Feedcard feedcard = new  OapiRobotSendRequest.Feedcard();
        feedcard.setLinks(links);
        OapiRobotSendRequest request = new OapiRobotSendRequest();
        request.setMsgtype(MsgTypeEnum.MSG_TYPE_FEED_CARD.getValue());
        request.setFeedCard(feedcard);
        OapiRobotSendResponse response = (OapiRobotSendResponse) requestExecute(request);
        return response;
    }

    /**
     * SDK 请求
     */
    public static TaobaoResponse requestExecute(TaobaoRequest request){
        TaobaoResponse response = null;
        try {
            response = RobotHelperUtil.client.execute(request);
        } catch (Exception e) {
            log.error("钉钉监控机器人发送消息失败, 异常捕获{}", e.getMessage());
        }
        return response;
    }

    /**
     * @param: accessToken  token
     * @param: secret       加签密钥
     * @description: 获取 serverUrl
     * @return: java.lang.String
     * @author: niaonao
     */
    private static String getServerUrl(String accessToken, String secret) {
        StringBuilder serverUrl = new StringBuilder(accessToken);
        // isSign 是否加签
        if (!isSign) {
            return serverUrl.toString();
        }
        try {
            Long timestamp = System.currentTimeMillis();
            String stringToSign = timestamp + "\n" + secret;
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
            byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
            String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)),"UTF-8");

            serverUrl.append("&timestamp=").append(timestamp)
                    .append("&sign=").append(sign);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return serverUrl.toString();
    }
}

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

niaonao

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

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

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

打赏作者

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

抵扣说明:

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

余额充值