Flutter架构搭建《一》 网络的封装

Flutter架构搭建《一》 网络的封装

刚把公司的一个小内部项目转成flutter应用,flutter刚出来到现在也好几年过去了,现在使用的原因是,flutter各方面已经稳点了,开源库也有很多,问题各个论坛也有很好的解答,整体使用下来,datt语言保持了kotlin的使用味道,反正我用起来真的很爽,与java类似,添加了很多语法糖,回调在dart中真的是一个很大胆的尝试,但是使用下来还是非常棒的这里我会将我写的model提供出来。是对于dio网络请求的封装,总共分三层,配置启动层,中间层和具体使用,我自己使用的很香,你可以参考我这种写法去写你的中间层

导包

在你的pubspec.yaml文件中加入依赖

dependencies:
  flutter:
    sdk: flutter
  dio: ^2.1.13

配置层

基础BeanModel

一般你的网络回调数据都是类似下面这种格式:

class BaseResp<T> {
  String status;
  int code;
  String message;
  T data;

  BaseResp(this.status, this.code, this.message, this.data);

  @override
  String toString() {
    StringBuffer sb = new StringBuffer('{');
    sb.write("\"status\":\"$status\"");
    sb.write(",\"code\":$code");
    sb.write(",\"message\":\"$message\"");
    sb.write(",\"data\":\"$data\"");
    sb.write('}');
    return sb.toString();
  }
}

范型为你的基础数据,可能是list也可能是其他所有类型,后面的中间层会使用到
还有一种类型,为后台返回的是流,那么可以使用下面这个类型

class BaseRespR<T> {
  String status;
  int code;
  String message;
  T data;
  Response response;

  BaseRespR(this.status, this.code, this.message, this.data, this.response);

  @override
  String toString() {
    StringBuffer sb = new StringBuffer('{');
    sb.write("\"status\":\"$status\"");
    sb.write(",\"code\":$code");
    sb.write(",\"message\":\"$message\"");
    sb.write(",\"data\":\"$data\"");
    sb.write('}');
    return sb.toString();
  }
}

response为dio中的类,
链接的path工具,如你的接口的不同路径,不包括baseurl

/**
 * @Author: Nimodou
 * @Blog: https://blog.csdn.net/qq_28535319
 * @Email: 344451903@qq.com
 * @Email: Nimodou93@163.com
 * @Date: 2019/7/27
 * @Description:
 */
///
class ApiUrls{
  //登陆
  static const String LOGIN="xxxxx/sss/xxx";
  //获取设备列表
  static const String GETDEVICELIST="xxxxx/xxxx";
  //获取绑定列表
  static const String GETDEVICEBINDLIST="xxxx/xxxxxx";

	//baseurl
  static const BASEURL="http://www.hhhh-cloud.com/sss/sss/sss/v1/";


  static String getPath({String path: ''}) {
    StringBuffer sb = new StringBuffer(path);
    return sb.toString();
  }
}

请求执行层


/**
 * @Author: Nimodou
 * @Blog: https://blog.csdn.net/qq_28535319
 * @Email: 344451903@qq.com
 * @Email: Nimodou93@163.com
 * @Date: 2019/7/27
 * @Description:
 */
///

/// 请求方法.
class Method {
  static final String get = "GET";
  static final String post = "POST";
  static final String put = "PUT";
  static final String head = "HEAD";
  static final String delete = "DELETE";
  static final String patch = "PATCH";
}

///Http配置.
class HttpConfig {
  /// constructor.
  HttpConfig({
    this.status,
    this.code,
    this.msg,
    this.data,
    this.options,
    this.pem,
    this.pKCSPath,
    this.pKCSPwd,
  });

  /// BaseResp [String status]字段 key, 默认:status.
  String status;

  /// BaseResp [int code]字段 key, 默认:errorCode.
  String code;

  /// BaseResp [String msg]字段 key, 默认:errorMsg.
  String msg;

  /// BaseResp [T data]字段 key, 默认:data.
  String data;

  /// Options.
  BaseOptions options;

  /// 详细使用请查看dio官网 https://github.com/flutterchina/dio/blob/flutter/README-ZH.md#Https证书校验.
  /// PEM证书内容.
  String pem;

  /// 详细使用请查看dio官网 https://github.com/flutterchina/dio/blob/flutter/README-ZH.md#Https证书校验.
  /// PKCS12 证书路径.
  String pKCSPath;

  /// 详细使用请查看dio官网 https://github.com/flutterchina/dio/blob/flutter/README-ZH.md#Https证书校验.
  /// PKCS12 证书密码.
  String pKCSPwd;
}

/// 单例 DioUtil.
/// debug模式下可以打印请求日志. DioUtil.openDebug().
/// dio详细使用请查看dio官网(https://github.com/flutterchina/dio).
class DioUtil {
  static final DioUtil _singleton = DioUtil._init();
  static Dio _dio;

  /// BaseResp [String status]字段 key, 默认:status.
  String _statusKey = "status";

  /// BaseResp [int code]字段 key, 默认:errorCode.
  String _codeKey = "code";

  /// BaseResp [String msg]字段 key, 默认:errorMsg.
  String _msgKey = "message";

  /// BaseResp [T data]字段 key, 默认:data.
  String _dataKey = "data";

  /// Options.
  BaseOptions  _options = getDefOptions();

  /// PEM证书内容.
  String _pem;

  /// PKCS12 证书路径.
  String _pKCSPath;

  /// PKCS12 证书密码.
  String _pKCSPwd;

  /// 是否是debug模式.
  static bool _isDebug = false;

  static DioUtil getInstance() {
    return _singleton;
  }

  factory DioUtil() {
    return _singleton;
  }

  DioUtil._init() {
    _dio = new Dio(_options);
    _dio.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志
  }

  /// 打开debug模式.
  static void openDebug() {
    _isDebug = true;
  }

  void setCookie(String cookie) {
    Map<String, dynamic> _headers = new Map();
    _headers["Cookie"] = cookie;
    _dio.options.headers.addAll(_headers);
  }

  /// set Config.
  void setConfig(HttpConfig config) {
    _statusKey = config.status ?? _statusKey;
    _codeKey = config.code ?? _codeKey;
    _msgKey = config.msg ?? _msgKey;
    _dataKey = config.data ?? _dataKey;
    _mergeOption(config.options);
    _pem = config.pem ?? _pem;
    if (_dio != null) {
      _dio.options = _options;
      if (_pem != null) {//证书校验
        (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
          client.badCertificateCallback=(X509Certificate cert, String host, int port){
            if(cert.pem==_pem){ // Verify the certificate
              return true;
            }
            return false;
          };
        };
      }
      if (_pKCSPath != null) {
        (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
          SecurityContext sc = new SecurityContext();
          //file is the path of certificate
          sc.setTrustedCertificates(_pKCSPath);
          HttpClient httpClient = new HttpClient(context: sc);
          return httpClient;
        };
      }
    }
  }

  /// Make http request with options.
  /// [method] The request method.
  /// [path] The url path.
  /// [data] The request data
  /// [options] The request options.
  /// <BaseResp<T> 返回 status code msg data .
  Future<BaseResp<T>> request<T>(String method, String path,
      {data, Options options, CancelToken cancelToken,  Map<String, dynamic> queryParameters,String pathReplace,String match }) async {

    if(match!=null&&pathReplace!=null){
      path=path.replaceAll(match, pathReplace);
    }
//    print("$path");
    print(data);
    Response response = await _dio.request(path,
        data: data,
        queryParameters:queryParameters,
        options: _checkOptions(method, options),
        cancelToken: cancelToken);
    _printHttpLog(response);
    String _status;
    int _code;
    String _msg;
    T _data;
    if (response.statusCode == HttpStatus.ok ||
        response.statusCode == HttpStatus.created) {
      try {
        if (response.data is Map) {
          _status = (response.data[_statusKey] is int)
              ? response.data[_statusKey].toString()
              : response.data[_statusKey];
          _code = (response.data[_codeKey] is String)
              ? int.tryParse(response.data[_codeKey])
              : response.data[_codeKey];
          _msg = response.data[_msgKey];
          _data = response.data[_dataKey];
        } else {
          Map<String, dynamic> _dataMap = _decodeData(response);
          _status = (_dataMap[_statusKey] is int)
              ? _dataMap[_statusKey].toString()
              : _dataMap[_statusKey];
          _code = (_dataMap[_codeKey] is String)
              ? int.tryParse(_dataMap[_codeKey])
              : _dataMap[_codeKey];
          _msg = _dataMap[_msgKey];
          _data = _dataMap[_dataKey];
        }
        return new BaseResp(_status, _code, _msg, _data);
      } catch (e) {
        return new Future.error(new DioError(
          response: response,
          message: "data parsing exception...",
          type: DioErrorType.RESPONSE,
        ));
      }
    }
    return new Future.error(new DioError(
      response: response,
      message: "statusCode: $response.statusCode, service error",
      type: DioErrorType.RESPONSE,
    ));
  }

  /// Make http request with options.
  /// [method] The request method.
  /// [path] The url path.
  /// [data] The request data
  /// [options] The request options.
  /// <BaseRespR<T> 返回 status code msg data  Response.
  Future<BaseRespR<T>> requestR<T>(String method, String path,
      {data, Options options, CancelToken cancelToken,Map<String, dynamic> queryParameters}) async {
    Response response = await _dio.request(path,
        data: data,
        queryParameters:queryParameters,
        options: _checkOptions(method, options),
        cancelToken: cancelToken);
    _printHttpLog(response);
    String _status;
    int _code;
    String _msg;
    T _data;
    if (response.statusCode == HttpStatus.ok ||
        response.statusCode == HttpStatus.created) {
      try {
        if (response.data is Map) {
          _status = (response.data[_statusKey] is int)
              ? response.data[_statusKey].toString()
              : response.data[_statusKey];
          _code = (response.data[_codeKey] is String)
              ? int.tryParse(response.data[_codeKey])
              : response.data[_codeKey];
          _msg = response.data[_msgKey];
          _data = response.data[_dataKey];
        } else {
          Map<String, dynamic> _dataMap = _decodeData(response);
          _status = (_dataMap[_statusKey] is int)
              ? _dataMap[_statusKey].toString()
              : _dataMap[_statusKey];
          _code = (_dataMap[_codeKey] is String)
              ? int.tryParse(_dataMap[_codeKey])
              : _dataMap[_codeKey];
          _msg = _dataMap[_msgKey];
          _data = _dataMap[_dataKey];
        }
        return new BaseRespR(_status, _code, _msg, _data, response);
      } catch (e) {
        return new Future.error(new DioError(
          response: response,
          message: "data parsing exception...",
          type: DioErrorType.RESPONSE,
        ));
      }
    }
    return new Future.error(new DioError(
      response: response,
      message: "statusCode: $response.statusCode, service error",
      type: DioErrorType.RESPONSE,
    ));
  }

  /// Download the file and save it in local. The default http method is "GET",you can custom it by [Options.method].
  /// [urlPath]: The file url.
  /// [savePath]: The path to save the downloading file later.
  /// [onProgress]: The callback to listen downloading progress.please refer to [OnDownloadProgress].
  Future<Response> download(
      String urlPath,
      savePath, {
        ProgressCallback onProgress,
        CancelToken cancelToken,
        data,
        Options options,
      }) {
    return _dio.download(urlPath, savePath,
        onReceiveProgress: onProgress,
        cancelToken: cancelToken,
        data: data,
        options: options);
  }

  Future<BaseResp<T>> upload<T>(String method, String path,
      {data, Options options, CancelToken cancelToken,Map<String, dynamic> queryParameters}) async {
//    FormData formData = new FormData.from(data);
    /*FormData formData = new FormData.from({
      "name": "wendux",
      "age": 25,
      "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),
      //支持直接上传字节数组 (List<int>) ,方便直接上传内存中的内容
      "file2": new UploadFileInfo.fromBytes(
          utf8.encode("hello world"), "word.txt"),
      // 支持文件数组上传
      "files": [
        new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),
        new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
      ]
    });*/
    Response response = await _dio.post(path, data: data,options: _checkOptions(method, options),
        cancelToken: cancelToken,queryParameters:queryParameters);
    String _status;
    int _code;
    String _msg;
    T _data;
    if (response.statusCode == HttpStatus.ok ||
        response.statusCode == HttpStatus.created) {
      try {
        if (response.data is Map) {
          _status = (response.data[_statusKey] is int)
              ? response.data[_statusKey].toString()
              : response.data[_statusKey];
          _code = (response.data[_codeKey] is String)
              ? int.tryParse(response.data[_codeKey])
              : response.data[_codeKey];
          _msg = response.data[_msgKey];
          _data = response.data[_dataKey];
        } else {
          Map<String, dynamic> _dataMap = _decodeData(response);
          _status = (_dataMap[_statusKey] is int)
              ? _dataMap[_statusKey].toString()
              : _dataMap[_statusKey];
          _code = (_dataMap[_codeKey] is String)
              ? int.tryParse(_dataMap[_codeKey])
              : _dataMap[_codeKey];
          _msg = _dataMap[_msgKey];
          _data = _dataMap[_dataKey];
        }
        return new BaseResp(_status, _code, _msg, _data);
      } catch (e) {
        return new Future.error(new DioError(
          response: response,
          message: "data parsing exception...",
          type: DioErrorType.RESPONSE,
        ));
      }
    }
    return new Future.error(new DioError(
      response: response,
      message: "statusCode: $response.statusCode, service error",
      type: DioErrorType.RESPONSE,
    ));
  }


  /// decode response data.
  Map<String, dynamic> _decodeData(Response response) {
    if (response == null ||
        response.data == null ||
        response.data.toString().isEmpty) {
      return new Map();
    }
    return json.decode(response.data.toString());
  }

  /// check Options.
  Options _checkOptions(method, options) {
    Options opNew;
    if (options == null) {
      opNew = new Options(method: method,responseType: ResponseType.json,contentType: ContentType.parse("application/json; charset=utf-8"));
    }else{
      opNew=options;
    }
    return opNew;
  }

  /// merge Option.
  void _mergeOption(BaseOptions opt) {
    _options.method = opt.method ?? _options.method;
    _options.headers = (new Map.from(_options.headers))..addAll(opt.headers);
    _options.baseUrl = opt.baseUrl ?? _options.baseUrl;
    _options.connectTimeout = opt.connectTimeout ?? _options.connectTimeout;
    _options.receiveTimeout = opt.receiveTimeout ?? _options.receiveTimeout;
    _options.responseType = opt.responseType ?? _options.responseType;
//    _options.data = opt.data ?? _options.data;
    _options.extra = (new Map.from(_options.extra))..addAll(opt.extra);
    _options.contentType = opt.contentType ?? _options.contentType;
    _options.validateStatus = opt.validateStatus ?? _options.validateStatus;
    _options.followRedirects = opt.followRedirects ?? _options.followRedirects;
  }

  /// print Http Log.
  void _printHttpLog(Response response) {
    if (!_isDebug) {
      return;
    }
    try {
      print("----------------Http Log----------------" +
          "\n[statusCode]:   " +
          response.statusCode.toString() +
          "\n[request   ]:   " +
          _getOptionsStr(response.request));
      _printDataStr("reqdata ", response.request.data);
      _printDataStr("response", response.data);
    } catch (ex) {
      print("Http Log" + " error......");
    }
  }

  /// get Options Str.
  String _getOptionsStr(RequestOptions request) {
    return "method: " +
        request.method +
        "  baseUrl: " +
        request.baseUrl +
        "  path: " +request.path
        ;
  }

  /// print Data Str.
  void _printDataStr(String tag, Object value) {
    String da = value.toString();
    while (da.isNotEmpty) {
      if (da.length > 512) {
        print("[$tag  ]:   " + da.substring(0, 512));
        da = da.substring(512, da.length);
      } else {
        print("[$tag  ]:   " + da);
        da = "";
      }
    }
  }

  /// get dio.
  Dio getDio() {
    return _dio;
  }

  /// create new dio.
  static Dio createNewDio([BaseOptions options]) {
    options = options ?? getDefOptions();
    Dio dio = new Dio(options);
    return dio;
  }

  /// get Def Options.
  static BaseOptions  getDefOptions() {
    BaseOptions  options = new BaseOptions ();
    options.contentType =
        ContentType.parse("application/json; charset=utf-8");
    options.connectTimeout = 10000 * 30;
    options.receiveTimeout = 10000 * 30;
    return options;
  }
}

这里有
static final String get = “GET”;
static final String post = “POST”;
static final String put = “PUT”;
static final String head = “HEAD”;
static final String delete = “DELETE”;
static final String patch = “PATCH”;
等操作,这里并不是具体使用,我么用一个中间层将model与请求执行层耦合起来

中间层

数据model

首先你要生成你的数据模型的bean,比如一个用户信息UserBean等

class UserBean {
  String value;
  int timestamp;
  int userId;

  UserBean({this.value, this.timestamp, this.userId});

  UserBean.fromJson(Map<String, dynamic> json) {
    value = json['value'];
    timestamp = json['timestamp'];
    userId = json['userId'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['value'] = this.value;
    data['timestamp'] = this.timestamp;
    data['userId'] = this.userId;
    return data;
  }
}

这里有一个网站可以直接将接口数据转成Model格式,我经常使用,veryhappy,https://javiercbk.github.io/json_to_dart/ 把你的json数据放入文本框可以直接生成数据Model,然后复制到你创建的dart中即可

ff在这里插入图片描述
然后就可以写中间层了,这里我就不一一把我所有的请求都放出来,我就放几个常用的

/**
 * @Author: Nimodou
 * @Blog: https://blog.csdn.net/qq_28535319
 * @Email: 344451903@qq.com
 * @Email: Nimodou93@163.com
 * @Date: 2019/7/27
 * @Description:
 */
///
///
class DioModelControl{
  DioModelControl();
  DioModelControl _dioModelControl;
  DioModelControl.getInstans() {
    if(null==_dioModelControl){
      _dioModelControl = new DioModelControl();
    }
  }
   /**
   * 登陆
   */
  Future<UserBean> logion(String loginLabel,String password,{Function printError,BuildContext context}) async {
    BaseResp<Map<String,dynamic>> baseResp = await DioUtil().request<Map<String,dynamic>>(
        Method.post, ApiUrls.getPath(path: ApiUrls.LOGIN),
      data: {"loginLabel":loginLabel,"password":password}
    );
    UserBean userBean;

    if(baseResp.code==200||baseResp.code==201||baseResp.code==204){
      if (baseResp.data != null) {
        userBean =  UserBean.fromJson(baseResp.data);
      }
      return userBean;
    }
    return _error<UserBean>(baseResp,printError: printError,context: context);
  }
/**
   * 获取父设备列表
   */
  Future<DeviceListBean> getDeviceList(String roomNo,String orgName,String devId,int pageNumber,{String parentDevId="000000000000",Function printError,BuildContext context}) async{
    BaseResp<Map<String,dynamic>> baseResp=await DioUtil().request<Map<String,dynamic>>(
        Method.post,ApiUrls.getPath(path:ApiUrls.GETDEVICELIST ),data: {"roomNo":roomNo,"orgName":orgName,"devId":devId,"parentDevId":parentDevId},queryParameters: {"pageNumber":pageNumber}
    );
    DeviceListBean deviceListBean;
    if(baseResp.code==200||baseResp.code==201||baseResp.code==204){
      if (baseResp.data != null) {
        deviceListBean =  DeviceListBean.fromJson(baseResp.data);
      }
      return deviceListBean;
    }
    return _error<DeviceListBean>(baseResp,printError: printError,context: context);
  }
}
  /**
   * 根据名字获取机构
   */
  Future<List<MechanismBean>> getMechanisListBeanWithName(String name,{Function printError,BuildContext context}) async{
    BaseResp<List> baseResp=await DioUtil().request<List>(
        Method.get,ApiUrls.getPath(path:ApiUrls.GETMECHANISLISTBEANWITHNAME ),queryParameters: {"name":name}
    );
    List<MechanismBean> list;
    if(baseResp.code==200||baseResp.code==201||baseResp.code==204){
      if (baseResp.data != null) {
        list=baseResp.data.map((value){
          return MechanismBean.fromJson(value);
        }).toList();
      }
      return list;
    }
    return _error<List<MechanismBean>>(baseResp,printError: printError,context: context);
  }
 /**
   * 删除设备
   */
  Future<String> deleteDevice(int id,{Function printError,BuildContext context}) async{
    BaseResp baseResp=await DioUtil().request(
        Method.delete,ApiUrls.getPath(path:ApiUrls.DELETEDEVICE ),match: "{id}",pathReplace: "$id"
    );
    String message;
    if(baseResp.code==200||baseResp.code==201||baseResp.code==204){
      if (baseResp.message != null) {
        message =  baseResp.message;
      }
      return message;
    }
    return _error<String>(baseResp,printError: printError,context: context);
  }
   /**
   * 上传文件
   */
  Future<UploadBean> uploadFile(String directory,String mediaType,UploadFileInfo upfile,{Function printError,BuildContext context}) async{
    FormData formData = new FormData.from({
      "file": upfile
    });
    BaseResp<Map<String,dynamic>> baseResp=await DioUtil().request<Map<String,dynamic>>(
        Method.post,ApiUrls.getPath(path:ApiUrls.UPLOADFILE ),
        data: formData,
        queryParameters: {
          "directory":directory,"mediaType":mediaType,},
    );
    UploadBean uploadBean;
    if(baseResp.code==200||baseResp.code==201||baseResp.code==204){
      if (baseResp.data != null) {
        uploadBean =  UploadBean.fromJson(baseResp.data);
      }
      return uploadBean;
    }
    return _error<UploadBean>(baseResp,printError: printError,context: context);
  }
  //处理错误,可以自己在ui层.error处理
	Future _error<T>(BaseResp baseResp,{Function printError,BuildContext context}){
	    if(baseResp.code==401&&context!=null){
	      RouteUtil.goLogin(context);
	      return new Future<T>.error(baseResp.message);
	    }
	    showToast(baseResp.message);
	    if(printError!=null){
	      printError(baseResp.message);
	      return new Future<T>.error(baseResp.message);
	    }else{
	      if(baseResp.code==401){
	        return new Future<T>.error("tokenerror");
	      }
	      return new Future<T>.error(baseResp.message);
	    }
	
	  }

注意这里query参数用queryParameters: ,map参数用data,path可以使用我删除设备的操作,上传文件用我例子中的即可

业务层

业务层就是具体使用了,我也写几个简单的例子,到这一步基本就是在你的state中实现了


///上传文件
 void  _sendUpload (){
    UploadFileInfo upfile=UploadFileInfo(new File(filePath),filePath.split('/').last);
    PopuUtils.showLoading(context, "正在上传");
    DioModelControl.getInstans().uploadFile("assistant",widget.type==1? "voice":"video",upfile ).then((value){
        ctMusic.text=value.urlPrefix+value.url;
        widget.sceneResponseList[0]?.url=value.urlPrefix+value.url;
        Navigator.of(context).pop();
        RouteUtil.popAndBackResult(context, widget.sceneResponseList);
    });
  }
void _login(){
DioModelControl.getInstans()
         .logion(_unameController.value.text,
            _pwdController.value.text,printError: (value){
              showToast(value);
            })
           .then((value) {
            if (!ObjectUtil.isEmptyString(
             value.value)) {
           SpUtil.putString(
            BaseConfig.keyAppToken,
                 value.value);
            }
            }).then((value){
              RouteUtil.goMain(context);
        });                
}

 void _onRefresh() {
    _page=1;
    _list.clear();
    DioModelControl.getInstans().getDeviceList(_room_no, _org_name, _mac_str, _page,context: context).then(
        (value){
          if(value!=null&&value.items!=null){
            _list.addAll(value.items);
            if (mounted) setState(() {});
            _refreshController.refreshCompleted();
          }
        }
    );

  }

具体的使用优化你可自己操作,如改变option,由于dart’的可选参数类型的设定,我们在不改变原有的代码基础上非常简单就能实现,我觉得flutter还是大有作为的,用起来反正很香

路还很长,慢慢走

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值