Android开发之推送服务(一)集成Umeng(友盟)推送

目前所在的公司让我把之前项目中的推送重新整理一下。因为现在的需求是在应用被后台杀死的情况下,依然可以推送消息,那么只是单一的使用一个推送工具是无法实现的,比如友盟。那么就需要同时集成Umeng,华为,小米,Oppo等手机厂商提供的PushSDK。这本来是简简单单的一件事,突然之后,工作量无形之后增大。不过好在经过一段时间的尝试,终于集成成功,今天把这个历程记录下来,方面以后查看。

Umeng推送

这个是官方网址,里面介绍的还算是比较详细,所以还是把官网地址粘出来:官网地址
在官网中,集成友盟推送的方式有两种,一种是通过gradle的maven仓库,另外一种是通过jar包引入的方式。这里我直接选择第一种,因为感觉gradle至少不用来回拷贝jar包,在更新的时候,直接更改Gradle依赖的版本既可。这里官方还非常贴心的给出了一个官方的demo。结合官方的例子,集成Umeng推送的步骤如下所示。

关于Appkey的申请等工作,这里就不在叙述,具体内容请参看官方文档

接入SDK

接入SDK时需要在gradle的文件中,添加如下代码,然后重新编译既可

//PushSDK必须依赖基础组件库,所以需要加入对应依赖
compile 'com.umeng.umsdk:common:1.5.4'
//PushSDK必须依赖utdid库,所以需要加入对应依赖
compile 'com.umeng.umsdk:utdid:1.1.5.3'
//PushSDK
compile 'com.umeng.umsdk:push:5.0.2'

这里使用了三个依赖,其中第一个和第二个是公共库,只要接入Umeng的内容,不管是推送,还是数据统计,都需要引入的内容。第三个是Umeng推送需要依赖的内容。这里使用的是当前的最新版5.0.2.
在执行编译之前,需要在项目的build.gradle中添加maven库,具体代码如下:

buildscript {
    repositories {
        google()
        jcenter()
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.4'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
}

在执行完成之后,就可以开始编译了。

基础接口引入

这里的基础接口只是能实现基本的推送接收功能,比如获取一个从控制台推送的推送。

初始化

必须在项目中重新自定义Application,并且在自定义的Application中的onCreate方法中添加推送的注册等操作,具体如下所示

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        initPush();
    }

    protected void initPush() {
        UMConfigure.init(this, UMENG_APPKEY, UMENG_APPCHANNEL, UMConfigure.DEVICE_TYPE_PHONE, UMENG_APPSECRET);
    }
}

其中
UMENG_APPKEYUMENG_APPSECRET是我们在注册账号时Umeng官方为我们的应用分配的,直接使用即可。
UMENG_CHANNEL是渠道名称。
UMConfigure.DEVICE_TYPE_PHONE表示的是设配类型是手机,除此之外还有UMConfigure.DEVICE_TYPE_BOX表示设备类型是盒子。

在执行初始化之后,我们的基本操作完成了,但是为了后面能够更好的使用推送,我们需要执行下面更多的操作。

注册

我们需要通过Umeng向Umeng官方注册,其实也就是告诉Umeng控制台,这里我们是有一个手机设备需要接受推送消息的。并且在注册成功之后,获取注册的DeviceToken

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        initPush();
    }

    protected void initPush() {
        UMConfigure.init(this, UMENG_APPKEY, UMENG_APPCHANNEL, UMConfigure.DEVICE_TYPE_PHONE, UMENG_APPSECRET);

        //获取推送代理,这个代理可以帮我们去执行诸如点击事件,样式不同的通知栏等操作
        PushAgent pushAgent = PushAgent.getInstance(this);
        pushAgent.register(new IUmengRegisterCallback() {
            @Override
            public void onSuccess(String s) {
                //注册成功
                Log.e("NPL", "deviceToken=" + s);
            }

            @Override
            public void onFailure(String s, String s1) {
                //注册失败
            }
        });
    }
}

这里注册成功之后,一般会想我们自己的后台发送一个deviceToken,这样方便以后发送推送消息是由后台控制的。例如给某个特定用的用户推送消息。

之后还有一个操作,就是帮助Umeng后台统计日活情况的主要依据,一定要加上。Activity中所有的都要添加,建议直接在BaseActivity中的oncreate方法中实现。

PushAgent.getInstance(context).onAppStart();

到这里基本的推送就完成了,下面来尝试推送一个新消息到特定的手机。
这里的操作可以直接参考官方的,官方的截图和各种步骤提示已经非常清楚了,这里就不在一一赘述。

高级功能

Umeng推送高级功能实现了自定义通知栏,自定义显示通知的动作等,这里的高级功能只适应于Android4.0以上版本,除此之后,可能由于不同的手机会出现不同的效果,如ROM定制型较强的小米,华为手机,部分效果无法显示。这里官方也提供了一个demo

自定义图标和自定义标题栏声音

drawable目录下放置两张图片,分别命名为umeng_push_notification_default_large_iconumeng_push_notification_default_small_icon,那么在Umeng推送中,就会使用提供的图标,如果没有,则使用应用默认图标。

小米手机暂时无法使用自定义图标

res/raw/目录下添加资源文件,并且命名为umeng_push_notification_default_sound。如果没有,则使用系统的通知声音。

若需要在线配置声音,则需先将与配置的声音文件放置在res/raw下,然后自发送后台指定声音的id,即R.raw.[sound]里的sound;

自定义通知栏声音仅在Android 8.0以下机型生效,如需适配Android 8.0及以上版本,请参考自定义通知栏样式,重写getNotification方法,设置声音。

自定义通知栏是否显示

有时候会有这样的需求,如果当前应用出去前台展示,则不用显示通知栏推送,只有当应用处于后台时,才需要在通知栏显示,那么这是用就可使用下面的代码

mPushAgent.setNotificaitonOnForeground(false);

这个设置只能在执行完注册(regist)方法之后,才能调用

自定义通知栏样式

UmengMessageHandler类负责处理消息,包括通知和自定义消息。我们可以通过getNotification函数设置不同的通知栏样式。

protected void initPush() {
        UMConfigure.init(this, UMENG_APPKEY, UMENG_APPCHANNEL, UMConfigure.DEVICE_TYPE_PHONE, UMENG_APPSECRET);

        //获取推送代理,这个代理可以帮我们去执行诸如点击事件,样式不同的通知栏等操作
        PushAgent pushAgent = PushAgent.getInstance(this);
        pushAgent.register(new IUmengRegisterCallback() {
            @Override
            public void onSuccess(String s) {
                //注册成功
                Log.e("NPL", "deviceToken=" + s);
            }

            @Override
            public void onFailure(String s, String s1) {
                //注册失败
            }
        });

        pushAgent.setNotificaitonOnForeground(true);

        UmengMessageHandler umengMessageHandler = new UmengMessageHandler() {
            @Override
            public Notification getNotification(Context context, UMessage uMessage) {
                /**
                 * context:上下文
                 * uMessage:表示当前传递过来的消息,在消息中,我们通过变量builder_id判断使用哪种样式
                 */
                switch (uMessage.builder_id) {
                    case UMENG_NOTIFICATION_NORMAL:
                        //创建通知栏对象
                        Notification.Builder builder = new Notification.Builder(context);
                        RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.view_notification_normal);
                        remoteView.setTextViewText(R.id.notification_title, uMessage.title);
                        remoteView.setTextViewText(R.id.notification_text, uMessage.text);
                        remoteView.setImageViewBitmap(R.id.notification_large_icon,
                                getLargeIcon(context, uMessage));
                        remoteView.setImageViewResource(R.id.notification_small_icon,
                                getSmallIconId(context, uMessage));
                        builder.setContent(remoteView)
                                .setSmallIcon(getSmallIconId(context, uMessage))
                                .setTicker(uMessage.ticker)
                                .setAutoCancel(true);
                        return builder.getNotification();
                    case UMENG_NOTIFICATION_LARGE:
                        return null;
                    case UMENG_NOTIFICATION_SMALL:
                        return null;
                    default:
                        return super.getNotification(context, uMessage);
                }

            }
        };
        pushAgent.setMessageHandler(umengMessageHandler);
    }

在测试时,发送的消息需要在下面的截图中添加相应的类型:

在这里插入图片描述

自定义通知栏打开动作

我们通过解析UMessage中custome字段的内容,可以实现自定义通知栏打开动作。当我们需要执行自定义动作时,需要重写dealWithCustomAction方法。在自定义动作的时候,一般是通过传递不同的数据,来实现动态判断执行哪种动作的。
在传递数据时,通过如下的内容实现:
在这里插入图片描述
这里我们传递了三个参数,分别是type,url,innerUrl。那么我们就通过type类型来判断跳转不同的页面,然后在通过url,判断跳转的子页面,通过innerUrl来判断跳转到H5页面。

protected void initPush() {
        UMConfigure.init(this, UMENG_APPKEY, UMENG_APPCHANNEL, UMConfigure.DEVICE_TYPE_PHONE, UMENG_APPSECRET);
        //获取推送代理,这个代理可以帮我们去执行诸如点击事件,样式不同的通知栏等操作
        PushAgent pushAgent = PushAgent.getInstance(this);
        pushAgent.register(new IUmengRegisterCallback() {
            @Override
            public void onSuccess(String s) {
                //注册成功
                Log.e("NPL", "deviceToken=" + s);
            }
            @Override
            public void onFailure(String s, String s1) {
                //注册失败
            }
        });
        pushAgent.setNotificaitonOnForeground(true);
        UmengMessageHandler umengMessageHandler = new UmengMessageHandler() {
            @Override
            public Notification getNotification(Context context, UMessage uMessage) {
                /**
                 * context:上下文
                 * uMessage:表示当前传递过来的消息,在消息中,我们通过变量builder_id判断使用哪种样式
                 */
                switch (uMessage.builder_id) {
                    case UMENG_NOTIFICATION_NORMAL:
                        //创建通知栏对象
                        Notification.Builder builder = new Notification.Builder(context);
                        RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.view_notification_normal);
                        remoteView.setTextViewText(R.id.notification_title, uMessage.title);
                        remoteView.setTextViewText(R.id.notification_text, uMessage.text);
                        remoteView.setImageViewBitmap(R.id.notification_large_icon,
                                getLargeIcon(context, uMessage));
                        remoteView.setImageViewResource(R.id.notification_small_icon,
                                getSmallIconId(context, uMessage));
                        builder.setContent(remoteView)
                                .setSmallIcon(getSmallIconId(context, uMessage))
                                .setTicker(uMessage.ticker)
                                .setAutoCancel(true);
                        return builder.getNotification();
                    case UMENG_NOTIFICATION_LARGE:
                        return null;
                    case UMENG_NOTIFICATION_SMALL:
                        return null;
                    default:
                        return super.getNotification(context, uMessage);
                }
            }
        };
        pushAgent.setMessageHandler(umengMessageHandler);
        UmengNotificationClickHandler umengNotificationClickHandler = new UmengNotificationClickHandler() {
            @Override
            public void dealWithCustomAction(Context context, UMessage uMessage) {
                //这里需要解析uMessage,然后通过custome属性执行不同的操作
                if (uMessage == null) {
                    return;
                }
                HashMap<String, String> hm = (HashMap<String, String>) uMessage.extra;
                String type = "";
                String url = "";
                String innerUrl = "";
                if (hm != null && hm.size() > 0) {
                    if (hm.containsKey("type")) {
                        type = hm.get("type");
                    }
                    if (hm.containsKey("url")) {
                        url = hm.get("url");
                    }
                    if (hm.containsKey("innerUrl")) {
                        innerUrl = hm.get("innerUrl");
                    }
                }
                if (type.equalsIgnoreCase("login")){
                    //跳转到登录页面
                }else if (type.equalsIgnoreCase("regist")){
                    //跳转到注册页面
                }else if (type.equalsIgnoreCase("im")){
                    //跳转到聊天页面
                }else {
                    //跳转到web页面
                    if(TextUtils.isEmpty(innerUrl)){
                        innerUrl = "http://www.baidu.com";
                    }
                }
            }
        };
        pushAgent.setNotificationClickHandler(umengMessageHandler);
    }

其实在UmengNotificationClickHandler中,除了上面的方法之外,还有别的方法可以完成通知栏的点击操作,如launchAppopenUrlopenActivitydealWithCustomAction等,这几个方法代表了点击通知栏之后不同的操作,但是都会传递UMessage对象,所以所能执行到的效果类似,就不一一演示了。

自定义消息(透传消息)

透传消息不是通知,也就不会在通知栏上显示,友盟会将透传消息传递给SDK,之后透传消息需要展示的样式和执行的操作,完全由代码决定。
在这里插入图片描述
自定义消息可以用于应用内部或者特殊的逻辑。如我们需要推送的内容不是在通知栏显示,而是以一个弹窗的样式展示,则可以通过自定义消息。
想要实现对自定义消息的处理,需要在UmengMessageHandler中重写dealWithCustomMessage方法。这个方法就是当发送自定义消息时,由SDK触发的。透传消息的自定义内容是放在UMessage对象中的custome参数中的,返回的数据是一个String类型,通过解析String内容,获取自定义消息。

protected void initPush() {
        UMConfigure.init(this, UMENG_APPKEY, UMENG_APPCHANNEL, UMConfigure.DEVICE_TYPE_PHONE, UMENG_APPSECRET);
        //获取推送代理,这个代理可以帮我们去执行诸如点击事件,样式不同的通知栏等操作
        PushAgent pushAgent = PushAgent.getInstance(this);
        pushAgent.register(new IUmengRegisterCallback() {
            @Override
            public void onSuccess(String s) {
                //注册成功
                Log.e("NPL", "deviceToken=" + s);
            }
            @Override
            public void onFailure(String s, String s1) {
                //注册失败
            }
        });
        pushAgent.setNotificaitonOnForeground(true);
        UmengMessageHandler umengMessageHandler = new UmengMessageHandler() {
            @Override
            public Notification getNotification(Context context, UMessage uMessage) {
                /**
                 * context:上下文
                 * uMessage:表示当前传递过来的消息,在消息中,我们通过变量builder_id判断使用哪种样式
                 */
                switch (uMessage.builder_id) {
                    case UMENG_NOTIFICATION_NORMAL:
                        //创建通知栏对象
                        Notification.Builder builder = new Notification.Builder(context);
                        RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.view_notification_normal);
                        remoteView.setTextViewText(R.id.notification_title, uMessage.title);
                        remoteView.setTextViewText(R.id.notification_text, uMessage.text);
                        remoteView.setImageViewBitmap(R.id.notification_large_icon,
                                getLargeIcon(context, uMessage));
                        remoteView.setImageViewResource(R.id.notification_small_icon,
                                getSmallIconId(context, uMessage));
                        builder.setContent(remoteView)
                                .setSmallIcon(getSmallIconId(context, uMessage))
                                .setTicker(uMessage.ticker)
                                .setAutoCancel(true);
                        return builder.getNotification();
                    case UMENG_NOTIFICATION_LARGE:
                        return null;
                    case UMENG_NOTIFICATION_SMALL:
                        return null;
                    default:
                        return super.getNotification(context, uMessage);
                }
            }
            @Override
            public void dealWithCustomMessage(final Context context, final UMessage uMessage) {
                //自定义消息的内容是放在uMessage中的custome参数中的
                if (uMessage == null) {
                    return;
                }
                String custome = uMessage.custom;
                if (TextUtils.isEmpty(custome)) {
                    return;
                }
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        // 对于自定义消息,PushSDK默认只统计送达。若开发者需要统计点击和忽略,则需手动调用统计方法。
                        boolean isClickOrDismissed = true;
                        if (isClickOrDismissed) {
                            //自定义消息的点击统计
                            UTrack.getInstance(getApplicationContext()).trackMsgClick(uMessage);
                        } else {
                            //自定义消息的忽略统计
                            UTrack.getInstance(getApplicationContext()).trackMsgDismissed(uMessage);
                        }
                        Toast.makeText(context, uMessage.custom, Toast.LENGTH_LONG).show();
                        //一般需要将custome转换成Json数据,然后通过Json数据中的内容,判断需要执行的操作
                    }
                });
            }
        };
        pushAgent.setMessageHandler(umengMessageHandler);
        UmengNotificationClickHandler umengNotificationClickHandler = new UmengNotificationClickHandler() {
            @Override
            public void dealWithCustomAction(Context context, UMessage uMessage) {
                //这里需要解析uMessage,然后通过custome属性执行不同的操作
                if (uMessage == null) {
                    return;
                }
                HashMap<String, String> hm = (HashMap<String, String>) uMessage.extra;
                String type = "";
                String url = "";
                String innerUrl = "";
                if (hm != null && hm.size() > 0) {
                    if (hm.containsKey("type")) {
                        type = hm.get("type");
                    }
                    if (hm.containsKey("url")) {
                        url = hm.get("url");
                    }
                    if (hm.containsKey("innerUrl")) {
                        innerUrl = hm.get("innerUrl");
                    }
                }
                if (type.equalsIgnoreCase("login")) {
                    //跳转到登录页面
                } else if (type.equalsIgnoreCase("regist")) {
                    //跳转到注册页面
                } else if (type.equalsIgnoreCase("im")) {
                    //跳转到聊天页面
                } else {
                    //跳转到web页面
                    if (TextUtils.isEmpty(innerUrl)) {
                        innerUrl = "http://www.baidu.com";
                    }
                }
            }
        };
        pushAgent.setNotificationClickHandler(umengMessageHandler);
    }

标签&别名

我们可以这样理解,如果我们想给一群特定的人推送消息,这一群人可以是会员,或者被系统拉黑的用户等等,而其他的用户是接收不到这个推送消息的。为了简化这样的流程,引入了标签的概念。
别名是我们可一个特定的某一个用户推送消息。例如一个用户的好友将他删除了,我们可以通过发送推送,告诉被删除者,你的好友将你删除了。
在代码中,我们通过addTagsaddAlias来实现添加标签和别名。
标签:

//添加标签,将tag1,tag2添加到当前设备中,一般情况下,我们会有一些判断,然后再为不同的用户添加不同的tag
        pushAgent.getTagManager().addTags(new TagManager.TCallBack() {
            @Override
            public void onMessage(boolean b, ITagManager.Result result) {

            }
        }, "tag1", "tag2");

        //删除标签,将tag1,tag2从当前设备中删除
        pushAgent.getTagManager().deleteTags(new TagManager.TCallBack() {
            @Override
            public void onMessage(boolean b, ITagManager.Result result) {

            }
        }, "tag1", "tag2");

        //获取服务器端所有的标签
        pushAgent.getTagManager().getTags(new TagManager.TagListCallBack() {
            @Override
            public void onMessage(boolean b, List<String> list) {
                
            }
        });

别名:

//增加别名,将某一类型的别名绑定至某一设备,老的绑定设备信息依然保留,别名和deviceToken的映射关系是一对多
pushAgent.addAlias("alias1", "type1", new UTrack.ICallBack() {
            @Override
            public void onMessage(boolean b, String s) {

            }
});
//移除别名
pushAgent.deleteAlias("alias1", "type1", new UTrack.ICallBack() {
            @Override
            public void onMessage(boolean b, String s) {
                
            }
});

设置别名之前,需要先进行注册并且获取到deviceToken。

插屏消息

这个类型其实我们在平时也很常见,具体的例子,如图显示(盗图狂魔上线)
在这里插入图片描述
使用demo

InAppMessageManager.getInstance(this).showCardMessage(this, this.getClass().getSimpleName(), new IUmengInAppMsgCloseCallback() {
            @Override
            public void onClose() {
                //差评消息关闭时调用该方法
                Log.e("NPL","关闭了插屏消息");
            }
        });

InAppMessageManager.getInstance(Context context).showCardMessage(Activity activity, String label, IUmengInAppMsgCloseCallback callback);
注意

  1. label :表示当前插屏消息的标识
  2. 客户端先调用showCardMessage,将label发送给服务端,之后U-Push后台展示位置才会出现可选label
  3. 插屏消息的图片会执行缓存,但有新消息来时,旧消息的缓存会被删除

同时我们可以自定义插屏消息的样式,需要在添加布局文件umeng_custom_card_message.xml。使用的模板如下,里面除了一个ImageView和两个button不能改变之外,其他的均可以改变

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:background="#33000000">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:layout_margin="60dp">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="20dp">

            <ImageView
                android:id="@+id/umeng_card_message_image"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"/>

            <Button
                android:id="@+id/umeng_card_message_ok"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/umeng_card_message_image"
                android:layout_marginTop="20dp"
                android:text="确定"/>
        </RelativeLayout>

        <Button
            android:id="@+id/umeng_card_message_close"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="top|right"
            android:text="关闭"/>

    </FrameLayout>

</RelativeLayout>

其他

检查集成配置文件

为了便于开发者更好的集成配置文件,我们提供了对于AndroidManifest配置文件的检查工具,可以自行检查开发者的配置问题。SDK默认是不检查集成配置文件的

mPushAgent.setPushCheck(true);

关闭推送

mPushAgent.disable(new IUmengCallback() {

    @Override
    public void onSuccess() {

    }

    @Override
    public void onFailure(String s, String s1) {

    }

});

在调用关闭推送之后,想要再次打开推送,则使用下面的代码

mPushAgent.enable(new IUmengCallback() {

    @Override
    public void onSuccess() {

    }

    @Override
    public void onFailure(String s, String s1) {

    }

});

常见问题汇总

  1. 集成推送之后,小米手机会报一个错误,错误截图如下:
    在这里插入图片描述
    解决方法:
    在app项目的manifest文件中加入tools:replace=”android:allowBackup”标签
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.push.umeng.umengomnipushdemo">

    <application
        tools:replace="android:allowBackup"
        android:allowBackup="true"
        android:name=".App"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        >
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
  1. 对于从老版本升级上来的Umeng推送,之前的Appkey等信息是在清单配置文件中设置的 ,如下所示
<!--友盟正式AppKey-->
        <meta-data
            android:name="UMENG_APPKEY"
            android:value="xxxxxxxxxxxxxxxxxxxxxx" />
        <!--友盟正式渠道-->
        <meta-data
            android:name="UMENG_CHANNEL"
            android:value="ipush" />
        <!--正式推送SECRET-->
        <meta-data
            android:name="UMENG_MESSAGE_SECRET"
            android:value="xxxxxxxxxxxxxxxxxxxxxx" />

在新版本中,就算在Manifest文件中设置了AppKey等信息,同样要执行UMConfigure.init()方法。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值