Android通用网络请求解析框架.10(发现问题,改善)

19 篇文章 0 订阅
14 篇文章 0 订阅
笔者将通过11篇博客对个人开源框架进行讲解,本篇为第10篇,讲解发现问题,改善。


如果有兴趣一起讨论本框架的内容,请加QQ群:271335749



回忆一下上一篇中,对第三方解析框架的支持。
开发者可以根据自己的需求来使用不同的Listener。在自定义Listener时,开发者也可以根据自己的需求,来实现相应的兼容第三方解析框架的Listener。

在选择框架自带的Listener时,并没问题。

但是,自定义Listener并支持第三方解析框架时,比如NetCustomGsonListener
abstract public class NetCustomGsonListener<Page, T> extends NetHandleListener {

    @Override
    protected void onReceivedRet(NetRetBean netRetBean) throws JSONException {
        String jsonString = netRetBean.getServerData();
        // 使用Gson解析
        Page page = NetGsonUtil.parseItemByGson(getClass(), 0, jsonString);

        JSONObject jsonObject = new JSONObject(jsonString);
        JSONArray jsonArray = jsonObject.getJSONArray("list");
        List<T> list = new ArrayList<>();
        for (int i = 0; i < jsonArray.length(); i++) {
            String string = jsonArray.getString(i);
            // 使用Gson解析
            T t = NetGsonUtil.parseItemByGson(getClass(), 1, string);
            list.add(t);
        }

        Map<String, Object> map = new HashMap<>();
        map.put("page", page);
        map.put("list", list);

        netRetBean.setServerObjectMap(map);
        handleResult(netRetBean);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void onSuccess(CallbackCode successCode, NetRetBean netRetBean) {
        Map<String, Object> map = netRetBean.getServerObjectMap();
        onSuccess((Page) map.get("page"), (List<T>) map.get("list"));
    }

    /**
     * 运行在ui线程,返回多个实体
     *
     * @param ts 当前bean的List
     */
    abstract protected void onSuccess(Page page, List<T> ts);
}

这里要非常注意的一点是,传入的两个Beam,都必须是符合gson规则的,如果哪一个不符合的话,那么他就不能够解析成功。

那么这样做的问题也就很明显了,有时候我传入的两个Bean只有第一个符合gson规则,第二个用默认的解析,要怎么办?

你就必须针对这种需求,再实现不同的监听器
abstract public class NetCustomGsonListener_1<Page, T> extends NetHandleListener {

    @Override
    protected void onReceivedRet(NetRetBean netRetBean) throws JSONException {
        String jsonString = netRetBean.getServerData();
        // 使用Gson解析
        Page page = NetGsonUtil.parseItemByGson(getClass(), 0, jsonString);

        JSONObject jsonObject = new JSONObject(jsonString);
        JSONArray jsonArray = jsonObject.getJSONArray("list");
        List<T> list = new ArrayList<>();
        for (int i = 0; i < jsonArray.length(); i++) {
            String string = jsonArray.getString(i);
            // 使用默认解析
            T t = NetBaseBeanUtil.parseItem(getClass(), 1, string);
            list.add(t);
        }

        Map<String, Object> map = new HashMap<>();
        map.put("page", page);
        map.put("list", list);

        netRetBean.setServerObjectMap(map);
        handleResult(netRetBean);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void onSuccess(CallbackCode successCode, NetRetBean netRetBean) {
        Map<String, Object> map = netRetBean.getServerObjectMap();
        onSuccess((Page) map.get("page"), (List<T>) map.get("list"));
    }

    /**
     * 运行在ui线程,返回多个实体
     *
     * @param ts 当前bean的List
     */
    abstract protected void onSuccess(Page page, List<T> ts);
}

或许再加一个并没什么,但是每个泛型,他都可能是多种情况,
就目前的来说,他可能使用默认解析,gson,fastjson,也就是三种情况。
那么两个泛型的话,3*3就是9种情况,要写9个Listener来适配不同的情况,来供开发者使用?
如果是N个泛型,就要写3的N次方个。
而且这还只是异步请求的情况,同步请求还都要跟着实现一份代码,那么就要再乘以2,这是多么蛋疼啊。
我想开发者也不想这样实现,就算实现了,具体使用时要用哪个Listener,也要找半天。

回想一下上一篇的内容,框架中提供的Listener只带1个泛型的,所以针对不同的需求,比如单个Bean的,要实现3的1次方个,即3个Listener,List<Bean>的也要3个。然后每一个都要复制一份同步请求的实现,最后一共实现了12个Listener。

不过在框架中实现12个是可以接受的,但是让开发者自己使用时来实现,这显然不科学。

那么,有没有办法避免这种情况呢?

我们回忆上一篇中讲到的,在initByJson方法中,支持第三方解析,出现的尴尬的情况
public class NetUserBean extends NetBaseBean {
    private String id;
    private String name;

    @Override
    public void initByJson(JSONObject jsonObject) throws JSONException {
        this.id = jsonObject.optString("id");
        this.name = jsonObject.optString("name");

        /**
         * 本类使用fastjson和gson会出现下面的情况,因为他们没有提供在当前类中去解析当前类
         * 即 fromJson(jsonString, getClass(), this) 这样的方法
         * 而是解析后返回一个新的对象,所以就会出现下面这样尴尬的情况
         */

//        NetUserBean netUserBean = new Gson().fromJson(jsonObject.toString(), NetUserBean.class);
//        this.id = netUserBean.getId();
//        this.name = netUserBean.getName();

//        NetUserBean netUserBean = JSON.parseObject(jsonObject.toString(), NetUserBean.class);
//        this.id = netUserBean.getId();
//        this.name = netUserBean.getName();
    }
}

gson和fastjson只能将解析完的对象返回,那么能不能直接利用这个对象呢?

再来看看使用initByJson的地方
public class NetBaseBeanUtil {

    public static <T extends NetBaseBean> T parseItem(Class aClass, int tIndex, JSONObject jsonObject) throws JSONException {
        T t = getBean(aClass, tIndex);
        t.initByJson(jsonObject);
        return t;
    }

    @SuppressWarnings("unchecked")
    private static <T extends NetBaseBean> T getBean(Class aClass, int tIndex) {
        Class<T> entityClass = (Class<T>) ((ParameterizedType) aClass.getGenericSuperclass()).getActualTypeArguments()[tIndex];
        T entity = null;
        try {
            // 使用newInstance创建实例的类,必须有无参构造方法
            entity = entityClass.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return entity;
    }
}

parseItem方法里,先创建T的实例,再将t用jsonObject解析,再把实例返回。

思来想去,笔者想到了解决方法,直接让initByJson方法拥有返回值就行了,如果initByJson的返回值是T,那么parseItem方法就可以改写了
public static <T extends NetBaseBean> T parseItem(Class aClass, int tIndex, JSONObject jsonObject) throws JSONException {
    T t = getBean(aClass, tIndex);
    t = t.initByJson(jsonObject);
    return t;
}

再简单点
public static <T extends NetBaseBean> T parseItem(Class aClass, int tIndex, JSONObject jsonObject) throws JSONException {
    T t = getBean(aClass, tIndex);
    return t.initByJson(jsonObject);
}

那么,NetUserBean应该是这样的
public class NetUserBean extends NetBaseBean {
    private String id;
    private String name;

    @Override
    public NetUserBean initByJson(JSONObject jsonObject) throws JSONException {
        this.id = jsonObject.optString("id");
        this.name = jsonObject.optString("name");
        return this

//        return new Gson().fromJson(jsonObject.toString(), NetUserBean.class);

//        return JSON.parseObject(jsonObject.toString(), NetUserBean.class);
    }
}

可以看出,这样的话,开发者可以在initByJson里面选择自己想要用到的解析方式,在使用第三方解析的时候,也非常方便,一行代码搞定。

因为initByJson方法改变了,所以父类也需要改变。NetBaseBean就会变成这样
abstract public class NetBaseBean {

    /**
     * 子类实现这个方法,在其中解析json
     *
     * @param jsonObject 将要解析的JsonObject
     * @throws JSONException
     */
    abstract public NetUserBean initByJson(JSONObject jsonObject) throws JSONException;

}

怎么看起来怪怪的,这样写的话,NetUserBean类是没有问题,可是其它类的这个方法,也都要返回NetUserBean?明显不能这样做。

再想一下第一篇中介绍的概念,以及本框架非常重要的知识,对,泛型,再次使用他。
返回的类型不能是NetUserBean,而是泛型T,而且T是继承自NetBaseBean的类。

其实在将NetBaseBeanUtil的parseItem方法实现成下面这样的时候
public static <T extends NetBaseBean> T parseItem(Class aClass, int tIndex, JSONObject jsonObject) throws JSONException {
    T t = getBean(aClass, tIndex);
    return t.initByJson(jsonObject);
}

就已经说明了initByJson只能返回泛型T,且T是继承NetBaseBean的。

那么,经过改写后的NetBaseBean,是这样的
abstract public class NetBaseBean {

    /**
     * 子类实现这个方法,在其中解析json
     *
     * @param jsonObject 将要解析的JsonObject
     * @throws JSONException
     */
    abstract public <T extern NetBaseBean> T initByJson(JSONObject jsonObject) throws JSONException;

}

子类在继承他的时候,要把返回结果改成当前类,比如NetUserBean就要把initByJson方法的返回值改成NetUserBean,否则编译不能通过。


为了不污染原来的代码,这里重新定义了一个类,NetCommonBean
package com.chenjian.net.bean;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * 作者: ChenJian
 * 时间: 2016.12.30 20:33
 */

abstract public class NetCommonBean {

    /**
     * 如果要选择性的使用android自带解析,或者otherJson(第三方解析框架,如gson、fastjson)解析的话,
     * 那么子类要继承本类,而不是继承{@link com.chenjian.net.bean.NetBaseBean},
     * otherJson无法在本类中对自己解析,而initByJson返回类型为void,正是在本类中对自己进行解析,
     * initByJson兼容otherJson会有很别扭的写法,只能在initByJson中使用otherJson解析后再给每个字段赋值
     * <p>
     * 详情请看
     * {@link com.chenjian.net.demo.bean.NetUserBean}
     *
     *
     * 子类重写这个方法时,因为要返回,所以请将返回类型 T 改成具体类型,不然编译会报错
     * 具体使用请看以下两个类
     * {@link com.chenjian.net.demo.bean.NetNewsBean}
     * {@link com.chenjian.net.util.NetCommonBeanUtil}
     *
     * @param jsonObject 将要解析的JsonObject
     * @param <T>        T
     * @return 子类的具体类型
     * @throws JSONException
     */
    abstract public <T extends NetCommonBean> T getBeanByJson(JSONObject jsonObject) throws JSONException;
}

那么要使用这个类的话,NetBaseBeanUtil是不可以了,我们模仿他实现NetCommonBeanUtil
package com.chenjian.net.util;

import com.chenjian.net.bean.NetCommonBean;

import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.ParameterizedType;

/**
 * NetCommonBean工具类,实现对泛型进行创建实例
 * <p>
 * 作者: ChenJian
 * 时间: 2016.12.15 14:22
 */

public class NetCommonBeanUtil {

    /**
     * @param aClass     带泛型 T 的类,应该传入xxBeanListener监听器或其的子类。他的父类必须是带泛型T的
     * @param tIndex     泛型 T 所在下标
     * @param jsonObject 用来解析的 json
     * @param <T>        泛型 T
     * @return 返回泛型T的实例
     * @throws JSONException
     */
    public static <T extends NetCommonBean> T parseItem(Class aClass, int tIndex, JSONObject jsonObject) throws JSONException {
        T t = getBean(aClass, tIndex);
        t = t.getBeanByJson(jsonObject);
        return t;
    }

    /**
     * @param aClass 带泛型 T 的类,应该传入xxBeanListener监听器或其的子类。他的父类必须是带泛型T的
     * @param tIndex 泛型 T 所在下标
     * @param <T>    泛型 T
     * @return 返回泛型T的实例
     */
    @SuppressWarnings("unchecked")
    private static <T extends NetCommonBean> T getBean(Class aClass, int tIndex) {
        Class<T> entityClass = (Class<T>) ((ParameterizedType) aClass.getGenericSuperclass()).getActualTypeArguments()[tIndex];
        T entity = null;
        try {
            // 使用newInstance创建实例的类,必须有无参构造方法
            entity = entityClass.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return entity;
    }
}


接下来开始使用。
假设服务端返回的数据是这样的
{
    "code":"00001",
    "message":"hello",
    "time":"1479807260",
    "data":{
        "title":"title",
        "desc":"desc",
        "date":"date"
    }
}

你需要定义一个Bean,继承自NetCommonBean
package com.chenjian.net.demo.bean;

import com.chenjian.net.bean.NetCommonBean;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * 作者: ChenJian
 * 时间: 2016.12.30 20:40
 */

public class NetNewsBean extends NetCommonBean {
    private String title;
    private String desc;
    private String date;

    @Override
    public NetNewsBean getBeanByJson(JSONObject jsonObject) throws JSONException {
        // 你要自己解析:
//        this.title = jsonObject.optString("title");
//        this.desc = jsonObject.optString("desc");
//        this.date = jsonObject.optString("date");
//        return this;

        // 你要用Gson解析,只需要一行代码:
//        return new Gson().fromJson(jsonObject.toString(), NetNewsBean.class);

        // 你要用fastjson解析,只需要一行代码
//        return JSON.parseObject(jsonObject.toString(), NetNewsBean.class);

        return null;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    @Override
    public String toString() {
        return "NetNewsBean{" +
                "title='" + title + '\'' +
                ", desc='" + desc + '\'' +
                ", date='" + date + '\'' +
                '}';
    }

}

这个Bean是符合gson和fastjson规范的,在getBeanByJson方法里面,可以选择自己要用到的解析方式。

接着,我们开始使用
private void otherjson() {
    UrlParse urlParse = new UrlParse(UrlConst.BASE_URL).appendRegion(UrlConst.INFO);

    /**
     * 需要将NetSingleBeanListener中onReceivedRet方法里的 T t = NetBaseBeanUtil.parseItem(getClass(), 0, object);
     * 改成 T t = NetCommonBeanUtil.parseItem(getClass(), 0, object); 才能用
     * NetSingleBeanListener中的泛型继承类也要修改成NetCommonBean而不是NetBaseBean
     * 不然编译不通过,因为NetNewsBean继承的是NetCommonBean而不是NetBaseBean
     *
     * 当然,你所有的bean都能继承NetCommonBean,在使用通用或者自定义Listener(非otherjson)时候,
     * 把其中的NetBaseBeanUtil.parseItem改成NetCommonBeanUtil.parseItem就行了,
     * 这样用户就可以在bean里面自行选择是否使用otherjson了
     */
//    NetHelper.get(urlParse.toString(), new NetSingleBeanListener<NetNewsBean>() {
//        @Override
//        protected void onError(CallbackCode errorCode, NetRetBean netRetBean) {
//
//        }
//
//        @Override
//        protected void onSuccess(NetNewsBean newsBean) {
//            System.out.println(newsBean.toString());
//        }
//    });
}

正如注释当中所说,需要将NetSingleBeanListener进行修改成下面这样才行
package com.chenjian.net.listener.async;

import com.chenjian.net.bean.NetCommonBean;
import com.chenjian.net.bean.NetRetBean;
import com.chenjian.net.listener.common.CallbackCode;
import com.chenjian.net.util.NetCommonBeanUtil;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * 返回是单个Bean的网络请求Listener
 * <p>
 * 作者: ChenJian
 * 时间: 2016.12.15 14:54
 */

abstract public class NetSingleBeanListener<T extends NetCommonBean> extends NetHandleListener {

    @Override
    protected void onReceivedRet(NetRetBean netRetBean) throws JSONException {
        JSONObject object = new JSONObject(netRetBean.getServerData());
        T t = NetCommonBeanUtil.parseItem(getClass(), 0, object);
        netRetBean.setServerObject(t);
        handleResult(netRetBean);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void onSuccess(CallbackCode successCode, NetRetBean netRetBean) {
        onSuccess((T) netRetBean.getServerObject());
    }

    /**
     * 运行在ui线程,返回单个实体
     *
     * @param t 当前bean
     */
    abstract protected void onSuccess(T t);
}

当然改成这样后,NetSingleBeanListener就不在能给NetBaseBean的子类用了,只能给NetCommonBean的子类用。


最后再来梳理一下使用。
如果服务端返回的数据是这样的
{
    "code":"00001",
    "message":"hello",
    "time":"1479807260",
    "data":{
        "title":"title",
        "desc":"desc",
        "date":"date"
    }
}

你的做法就有很多种了。最通用的做法,就是定义一个Bean,继承自NetCommonBean
package com.chenjian.net.demo.bean;

import com.chenjian.net.bean.NetCommonBean;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * 作者: ChenJian
 * 时间: 2016.12.30 20:40
 */

public class NetNewsBean extends NetCommonBean {
    private String title;
    private String desc;
    private String date;

    @Override
    public NetNewsBean getBeanByJson(JSONObject jsonObject) throws JSONException {
        // 你要自己解析:
//        this.title = jsonObject.optString("title");
//        this.desc = jsonObject.optString("desc");
//        this.date = jsonObject.optString("date");
//        return this;

        // 你要用Gson解析,只需要一行代码:
//        return new Gson().fromJson(jsonObject.toString(), NetNewsBean.class);

        // 你要用fastjson解析,只需要一行代码
//        return JSON.parseObject(jsonObject.toString(), NetNewsBean.class);

        return null;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    @Override
    public String toString() {
        return "NetNewsBean{" +
                "title='" + title + '\'' +
                ", desc='" + desc + '\'' +
                ", date='" + date + '\'' +
                '}';
    }

}

在getBeanByJson方法里,根据自己的喜好和需求,来用不同的方法来解析。
然后就可以使用NetSingleBeanListener来请求
private void otherjson() {
    NetHelper.get("url", new NetSingleBeanListener<NetNewsBean>() {
        @Override
        protected void onError(CallbackCode errorCode, NetRetBean netRetBean) {

        }

        @Override
        protected void onSuccess(NetNewsBean newsBean) {
            System.out.println(newsBean.toString());
        }
    });
}

或者你已经确定使用的是gson,你的Bean只需要符合gson规范就可以了
public class NetNewsBean {
    private String title;
    private String desc;
    private String date;
}

直接使用NetSingleGsonListener
private void otherjson() {
    NetHelper.get("url", new NetSingleGsonListener<NetNewsBean>() {
        @Override
        protected void onError(CallbackCode errorCode, NetRetBean netRetBean) {

        }

        @Override
        protected void onSuccess(NetNewsBean netNewsBean) {
            
        }
    });
}

或者你已经确定使用的是fastjson,你的Bean只需要符合fastjson规范就可以了
public class NetNewsBean {
    private String title;
    private String desc;
    private String date;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }
}

直接使用NetSingleFastjsonListener
private void otherjson() {
    NetHelper.get("url", new NetSingleFastjsonListener<NetNewsBean>() {
        @Override
        protected void onError(CallbackCode errorCode, NetRetBean netRetBean) {

        }

        @Override
        protected void onSuccess(NetNewsBean netNewsBean) {
            
        }
    });
}


从上面的内容可以看出,你使用gson或者fastjson,其实也不需要用到NetSingleGsonListener和NetSingleFastjsonListener,你只要继承NetCommonBean,在getBeanByJson方法中选择自己想要用到的解析方式来解析就行了。但使用NetSingleGsonListener或者NetSingleFastjsonListener,Bean不需要继承NetCommonBean,可以不要写getBeanByJson代码。不过如果不介意多写一行解析直接返回的代码的话,笔者觉得NetSingleGsonListener和NetSingleFastjsonListener就是没用的,包括上一篇中讲解的为了支持第三方框架实现的那些Listener,都是没用的,只要开发者使用NetCommonBean就行了。

再回顾一下本篇开始时候说的问题,如果要支持第三方解析框架,在自定义Listener时会有问题,也就是自定义的Listener不能通用,当泛型有多个的时候,要适应每一种情况,就要写非常多类似的Listener。如果使用NetCommonBean,就根本不需要专门为第三方解析做兼容,不管是自定义Listener还是使用框架普通的Listener,用户都可以在Bean里面的getBeanByJson方法里面选择自己想要的解析方式。

可以说,如果你的项目需要用到第三方解析,而且在多个人协作开发的时候,不确定谁会用到第三方解析,那么就用NetCommonBean,不要再用NetBaseBean了,让实现Bean的人自己在Bean的内部选择用哪一种解析方式,自定义Bean时也不要再考虑多种情况了。当然框架自带的兼容第三方解析的所有Listener,比如NetSingleGsonListener,也可以删掉了。如果留着用也行,用他们时,可以少写一些代码。


但是很遗憾,本框架目前没有用到NetCommonBean。当然你可以修改少量代码就能用了。

因为NetCommonBean只是为了解决第三方解析框架而设计的,个人认为,在Bean中的非静态共有方法,也就是getBeanByJson方法,他返回的是一个本类对象,也就是下面这样
public class NetNewsBean extends NetCommonBean {
    private String title;
    private String desc;
    private String date;

    @Override
    public NetNewsBean getBeanByJson(JSONObject jsonObject) throws JSONException {
        // 你要自己解析:
//        this.title = jsonObject.optString("title");
//        this.desc = jsonObject.optString("desc");
//        this.date = jsonObject.optString("date");
//        return this;

        // 你要用Gson解析,只需要一行代码:
//        return new Gson().fromJson(jsonObject.toString(), NetNewsBean.class);

        // 你要用fastjson解析,只需要一行代码
//        return JSON.parseObject(jsonObject.toString(), NetNewsBean.class);

        return null;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    @Override
    public String toString() {
        return "NetNewsBean{" +
                "title='" + title + '\'' +
                ", desc='" + desc + '\'' +
                ", date='" + date + '\'' +
                '}';
    }

}

这看起来非常的奇怪,如果用到gson和fastjson时,在内存中,还浪费了一个对象空间。显然NetBaseBean中的initByJson方法看起来更规范。只能怪第三方解析的缺陷,不能在本类中将本类进行解析。

为什么是我去改变去兼容第三方框架呢?而不是第三方框架去兼容我呢?
做为一名程序员如果没有一点坚持,那跟咸鱼还有什么差别???
说不定哪一天,gson和fastjson提供了下面这样的解析方式
fromJson(jsonString, getClass(), this)

那么使用NetBaseBean也就可以和他们融合了。

当然到最后,还是要提醒,如果你要使用第三方解析框架,请用NetCommonBean,把用到NetBaseBeanUtil的地方,都改成NetCommonBeanUtil,框架中自带的支持第三方解析的Listener,可用可不用。


至此,发现问题,改善,解决了。

下一篇,将进行总结
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值