【从 0 开始开发一款直播 APP】4.4 网络封装之 OkHttp -- 网络请求实现直播登录

1,021 阅读9分钟
原文链接: www.cniao5.com
本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类”腾讯直播”的商业化项目
视频地址:www.cniao5.com/course/1012…

【从 0 开始开发一款直播 APP】4.1 网络封装之 Okhttp — 基础回顾
【从 0 开始开发一款直播 APP】4.2 网络封装之 OkHttp — GET,POST,前后端交互
【从 0 开始开发一款直播 APP】4.3 网络封装之 OkHttp — 封装 GET,POST FORM,POST JSON
【从 0 开始开发一款直播 APP】4.4 网络封装之 OkHttp — 网络请求实现直播登录


上一章讲了 OkHttp 的封装,现在来使用一下封装吧。

网络请求进行手机登录和用户名登录。


先来介绍一个请求工具 postman,Google 浏览器上的一个插件,也有对应的应用程序,亲测很好用,推荐给大家。

好了,开始讲解网络请求,测试一下封装的 Okhttp 框架。
讲解之前添加依赖以及相关权限

compile 'com.google.code.gson:gson:2.3.1'
compile 'com.zhy:okhttputils:2.6.2'

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

一、界面布局

里面用到了一个控件,很漂亮,看到输入框的文字会跳动对吧,就是这个控件。android.support.design.widget.TextInputLayout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg_dark"
    android:id="@+id/rl_login_root"
    tools:context="com.dali.admin.livestreaming.activity.LoginActivity">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:layout_marginTop="19dp"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="100dp">

        //验证码
        <Button
            android:id="@+id/btn_verify_code"
            android:layout_width="90dp"
            android:layout_height="35dp"
            android:layout_alignEnd="@+id/til_login"
            android:layout_alignRight="@+id/til_login"
            android:layout_below="@+id/til_login"
            android:layout_marginRight="5dp"
            android:layout_marginTop="2dp"
            android:background="@drawable/btn_login"
            android:clickable="true"
            android:minWidth="0dp"
            android:padding="4dp"
            android:text="@string/activity_login_verify_code"
            android:textSize="12sp"
            style="@style/loginButton"
            />
        <android.support.design.widget.TextInputLayout
            android:id="@+id/til_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            >
            //用户名
            <AutoCompleteTextView
                android:id="@+id/et_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ellipsize="end"
                android:hint="@string/activity_login_username"
                android:inputType="textEmailAddress"
                android:maxLength="24"
                android:maxLines="1"
                android:singleLine="true"
                android:textColor="@drawable/tv_selector"
                android:textColorHint="@color/white"
                />
        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:id="@+id/til_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/til_login">
            //密码
            <EditText
                android:id="@+id/et_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@+id/et_login"
                android:ellipsize="end"
                android:hint="@string/activity_login_password"
                android:imeActionId="@+id/login"
                android:imeOptions="actionUnspecified"
                android:inputType="textPassword"
                android:maxLength="24"
                android:maxLines="1"
                android:singleLine="true"
                android:textColor="@color/white"
                android:textColorHint="@color/white"/>
        </android.support.design.widget.TextInputLayout>

        //登录
        <Button
            android:id="@+id/btn_login"
            android:layout_alignParentEnd="true"
            android:layout_below="@+id/til_password"
            android:background="@drawable/btn_login"
            android:clickable="true"
            android:padding="0dp"
            android:text="@string/activity_login_btnlogin"
            style="@style/loginButton"/>

        //手机号登录
        <Button
            android:id="@+id/btn_phone_login"
            android:layout_width="wrap_content"
            android:layout_below="@+id/btn_login"
            android:layout_marginTop="10dp"
            android:text="@string/activity_login_btnphone"
            android:background="?attr/selectableItemBackground"
            style="@style/loginButton"
            />

        //注册新用户
        <Button
            android:id="@+id/btn_register"
            android:layout_width="wrap_content"
            android:layout_alignEnd="@+id/btn_login"
            android:layout_alignRight="@+id/btn_login"
            android:layout_below="@+id/btn_login"
            android:background="?attr/selectableItemBackground"
            android:layout_marginTop="10dp"
            android:text="@string/activity_login_register"
            style="@style/loginButton"
            />

        <ProgressBar
            android:id="@+id/progressbar"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_alignTop="@+id/btn_login"
            android:layout_centerHorizontal="true"
            android:visibility="gone"/>
    </RelativeLayout>
</RelativeLayout>

二、登录界面逻辑实现

2.1、类的初始化

创建 LoginActivity 类,继承 BaseActivity,实现其抽象方法。实例化控件。

public class LoginActivity extends BaseActivity implements View.OnClickListener{
    //控件
      private ProgressBar progressBar;
    private EditText etPassword;
    private EditText etLogin;
    private Button btnLogin;
    private Button btnPhoneLogin;
    private TextInputLayout tilLogin, tilPassword;
    private Button btnRegister;
    private TextView tvVerifyCode;
      //是否是手机号登录
    private boolean isPhoneLogin = false;

    @Override
    protected void setActionBar() {
    }

    @Override
    protected void setListener() {
    }

    @Override
    protected void initData() {
    }

    @Override
    protected void initView() {
        etLogin = obtainView(R.id.et_username);
        etPassword = obtainView(R.id.et_password);
        btnRegister = obtainView(R.id.btn_register);
        btnPhoneLogin = obtainView(R.id.btn_phone_login);
        btnLogin = obtainView(R.id.btn_login);
        progressBar = obtainView(R.id.progressbar);
        tilLogin = obtainView(til_login);
        tilPassword = obtainView(R.id.til_password);
        tvVerifyCode = obtainView(R.id.btn_verify_code);
          //第一次进入系统默认是用户名登录
          userNameLoginViewInit();
    }

    @Override
    protected int getLayoutId() {
        return R.layout.activity_login;
    }

      @Override
    public void onClick(View v) {
    }
}

2.2、用户名登录实现

这里实现逻辑,解释一下实现思路,而登录请求网络后面介绍。

//用户名登录方法
public void userNameLoginViewInit() {
    //用户名登录控件初始化
    userLoginTrans();

    //注册监听
    btnRegister.setOnClickListener(this);
    //这里只是用做切换界面,用户名登录情况下点击按钮显示到手机号登录,显示获取验证码按钮
    btnPhoneLogin.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //手机号登录切换
            phoneLoginViewinit();
        }
    });
    //登录按钮监听
    btnLogin.setOnClickListener(this);
}
//用户名登录界面控件显示
private void userLoginTrans() {
          //将手机号登录设置为 false
        isPhoneLogin = false;
          //隐藏获取验证码按钮
        tvVerifyCode.setVisibility(View.GONE);
        AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
        alphaAnimation.setDuration(250);
        tvVerifyCode.setAnimation(alphaAnimation);
          //设置输入框输入类型为 文本输入
        etLogin.setInputType(EditorInfo.TYPE_CLASS_TEXT);
          //清空输入框内容
        etLogin.setText("");
        etPassword.setText("");
          //设置用户名登录按钮文字为 手机号登录
        btnPhoneLogin.setText("手机号登录");
        //设置 TextInputLayout 文字为 用户名和密码
        tilLogin.setHint("用户名");
        tilPassword.setHint("密码");
}

2.3、手机号登录实现

如上,实现逻辑,登录请求网络接下来介绍。

//手机号登录方法
public void phoneLoginViewinit() {
      //用户名登录控件初始化
    phoneLoginTrans();
    //这里只是用做切换界面,用户名登录情况下点击按钮显示到手机号登录,显示获取验证码按钮
    btnPhoneLogin.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //转换为用户名登录界面
            userNameLoginViewInit();
        }
    });
    //登录按钮监听
    btnLogin.setOnClickListener(this);
}
//手机号登录控件显示
private void phoneLoginTrans() {
      //将手机号登录设置为 true
    isPhoneLogin = true;
      //显示获取验证码按钮
    tvVerifyCode.setVisibility(View.VISIBLE);
    AlphaAnimation alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
    alphaAnimation.setDuration(250);
    tvVerifyCode.setAnimation(alphaAnimation);
    //设定点击优先级于最前(避免被EditText遮挡的情况)
    tvVerifyCode.bringToFront();
    //设置输入框输入类型为 手机号
    etLogin.setInputType(EditorInfo.TYPE_CLASS_PHONE);
    //清空输入框内容
    etLogin.setText("");
    etPassword.setText("");
    //手机号登录按钮文字改为 用户名登录
    btnPhoneLogin.setText("用户名登录");
      //设置 TextInputLayout 文字为 手机号和密码
    tilLogin.setHint("手机号");
    tilPassword.setHint("密码");
}

2.4、请求网络实现登录


2.4.1、用户名登录

之前介绍的工具还没用上呢,现在来请求网络,看看需要那些数据,请求参数有登录方式 action,用户名 userName 和 密码 password。

2.4.2、为如上数据创建一个请求实体类。

public class LoginRequest extends IRequest {
   public LoginRequest(int requestId, String userName, String password) {
      mRequestId = requestId;
      mParams.put("action", "login");
      mParams.put("userName", userName);
      mParams.put("password", password);
   }

   //getHost():返回的 API,这里在封装 OkHttp 框架的时候有的,HOST_PUBLIC = "http://live.demo.cniao5.com/Api/";
   @Override
   public String getUrl() {
      return getHost() + "User";
   }

   @Override
   public Type getParserType() {
      return new TypeToken<Response<UserInfo>>() {
      }.getType();
   }
}

2.4.3、网络请求实现用户名登录

public void usernameLogin(final String userName, final String password) {
      //检查网络状态,需要添加网络使用权限:ACCESS_NETWORK_STATE
    if (checkUserNameLogin(userName, password)) {
          // RequestComm.login 是 requestId:值是120,后台提供
        LoginRequest req = new LoginRequest(RequestComm.login, userName, password);
        AsyncHttp.instance().postJson(req, new AsyncHttp.IHttpListener() {
            @Override//请求网络之前调用
            public void onStart(int requestId) {
                  //显示环形进度条
                showOnLoading(true);
            }

            @Override
            public void onSuccess(int requestId, Response response) {
                  //RequestComm.SUCCESS 状态验证:值是0,上面请求网络有显示
                if (response.status == RequestComm.SUCCESS) {
                    Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
                }
                  //隐藏环形进度条
                showOnLoading(false);
            }

            @Override
            public void onFailure(int requestId, int httpStatus, Throwable error) {
                showOnLoading(false);//隐藏环形进度条
                Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

2.5、手机登录实现

手机号登录先来测试网络数据API,看看实体类需要的数据。请求参数有登录方式 action,手机号 mobile 和 验证码 verifyCode。

2.5.1、创建手机登录请求实体类

public class PhoneLoginRequest extends IRequest {
   public PhoneLoginRequest(int requestId, String mobile, String verifyCode) {
      mRequestId = requestId;
      mParams.put("action", "phoneLogin");
      mParams.put("mobile", mobile);
      mParams.put("verifyCode", verifyCode);
   }

  //getHost():返回的 API,这里在封装 OkHttp 框架的时候有的,HOST_PUBLIC = "http://live.demo.cniao5.com/Api/";
   @Override
   public String getUrl() {
      return getHost() + "User";
   }

   @Override
   public Type getParserType() {
      return new TypeToken<Response>() {
      }.getType();
   }
}

2.5.2、网络请求实现手机号登录

public void phoneLogin(final String mobile, String verifyCode) {
      //检查网络状态,需要添加网络使用权限:ACCESS_NETWORK_STATE
    if (checkPhoneLogin(mobile, verifyCode)) {//requestId = 1200,后台提供
        PhoneLoginRequest req = new PhoneLoginRequest(1200, mobile, verifyCode);
        AsyncHttp.instance().postJson(req, new AsyncHttp.IHttpListener() {
            @Override//请求网络之前调用
            public void onStart(int requestId) {
                showOnLoading(true);//显示环形进度条
            }

            @Override
            public void onSuccess(int requestId, Response response) {
                  //RequestComm.SUCCESS 状态验证:值是0,上面请求网络有显示
                if (response.status == RequestComm.SUCCESS) {
                    Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
                }
                showOnLoading(false);//隐藏环形进度条
            }

            @Override
            public void onFailure(int requestId, int httpStatus, Throwable error) {
                showOnLoading(false);//隐藏环形进度条
                Toast.makeText(LoginActivity.this, "网络异常", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

检查用户名登录、手机登录、环形进度条显示或隐藏代码

//监测用户名登录
public boolean checkUserNameLogin(String userName, String password) {
    if (OtherUtils.isUsernameVaild(userName)) {
        if (OtherUtils.isPasswordValid(password)) {
            if (OtherUtils.isNetworkAvailable(this)) {
                return true;
            } else {
                ToastUtils.showShort(this, "当前无网络连接");
            }
        } else {
            ToastUtils.showShort(this, "密码过短");
        }
    } else {
        ToastUtils.showShort(this, "用户名不符合规范");
    }
    return false;
}
//监测手机号登录
public boolean checkPhoneLogin(String phone, String verifyCode) {
        if (OtherUtils.isPhoneNumValid(phone)) {
            if (OtherUtils.isVerifyCodeValid(verifyCode)) {
                if (OtherUtils.isNetworkAvailable(this)) {
                    return true;
                } else {
                    ToastUtils.showShort(this, "当前无网络连接");
                }
            } else {
                ToastUtils.showShort(this, "验证码错误");
            }
        } else {
            ToastUtils.showShort(this, "手机格式错误");
        }
        return false;
}
//进度条显示与隐藏
public void showOnLoading(boolean active) {
        if (active) {
              //显示进度条
            progressBar.setVisibility(View.VISIBLE);
              //显示登录按钮
            btnLogin.setVisibility(View.INVISIBLE);
              //输入框设置不可用
            etLogin.setEnabled(false);
            etPassword.setEnabled(false);
              //手机号按钮和按钮设置为不可点击
            btnPhoneLogin.setClickable(false);
              btnRegister.setClickable(false);
 btnRegister.setTextColor(getResources().getColor(R.color.colorTransparentGray));          btnPhoneLogin.setTextColor(getResources().getColor(R.color.colorTransparentGray));

        } else {
              //隐藏进度条
            progressBar.setVisibility(View.GONE);
              //隐藏登录按钮
            btnLogin.setVisibility(View.VISIBLE);
              //输入框设置可用
            etLogin.setEnabled(true);
            etPassword.setEnabled(true);
              //手机号按钮和按钮设置为可点击
            btnPhoneLogin.setClickable(true);
            btnRegister.setClickable(true);
            btnRegister.setTextColor(getResources().getColor(R.color.white));
            btnPhoneLogin.setTextColor(getResources().getColor(R.color.white));
        }
}

2.6、手机号登录和用户名登录切换

@Override
public void onClick(View v) {
    if (isPhoneLogin){
        phoneLogin(etLogin.getText().toString(), etPassword.getText().toString());
    }else {
        usernameLogin(etLogin.getText().toString(), etPassword.getText().toString());
    }

}

三、完整的代码实现

public class LoginActivity extends BaseActivity implements View.OnClickListener {
    private ProgressBar progressBar;
    private EditText etPassword;
    private EditText etLogin;
    private Button btnLogin;
    private Button btnPhoneLogin;
    private TextInputLayout tilLogin, tilPassword;
    private Button btnRegister;
    private TextView tvVerifyCode;
    private boolean isPhoneLogin = false;

    @Override
    protected void setActionBar() {
    }

    @Override
    protected void setListener() {
    }

    @Override
    protected void initData() {
    }

    @Override
    protected void initView() {
        etLogin = obtainView(R.id.et_username);
        etPassword = obtainView(R.id.et_password);
        btnRegister = obtainView(R.id.btn_register);
        btnPhoneLogin = obtainView(R.id.btn_phone_login);
        btnLogin = obtainView(R.id.btn_login);
        progressBar = obtainView(R.id.progressbar);
        tilLogin = obtainView(til_login);
        tilPassword = obtainView(R.id.til_password);
        tvVerifyCode = obtainView(R.id.btn_verify_code);
        userNameLoginViewInit();
    }

    @Override
    protected int getLayoutId() {
        return R.layout.activity_login;
    }

    /**
     * 用户名密码登录界面
     */
    public void userNameLoginViewInit() {
        userLoginTrans();
        btnRegister.setOnClickListener(this);
        btnPhoneLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //手机号登录
                phoneLoginViewinit();
            }
        });
        //用户名登录
        btnLogin.setOnClickListener(this);

    }

    public void phoneLoginViewinit() {
        phoneLoginTrans();
                //转换为用户名登录界面
                userNameLoginViewInit();
            }
        });
        btnLogin.setOnClickListener(this);
    }

    private void phoneLoginTrans() {
        isPhoneLogin = true;
        tvVerifyCode.setVisibility(View.VISIBLE);
        AlphaAnimation alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
        alphaAnimation.setDuration(250);
        tvVerifyCode.setAnimation(alphaAnimation);
        tvVerifyCode.bringToFront();
        etLogin.setInputType(EditorInfo.TYPE_CLASS_PHONE);
        etLogin.setText("");
        etPassword.setText("");
        btnPhoneLogin.setText("用户名登录");
        tilLogin.setHint("手机号");
        tilPassword.setHint("密码");
    }

    private void userLoginTrans() {
        isPhoneLogin = false;
        tvVerifyCode.setVisibility(View.GONE);
        AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
        alphaAnimation.setDuration(250);
        tvVerifyCode.setAnimation(alphaAnimation);
        etLogin.setInputType(EditorInfo.TYPE_CLASS_TEXT);
        etLogin.setText("");
        etPassword.setText("");
        btnPhoneLogin.setText("手机号登录");
        tilLogin.setHint("用户名");
        tilPassword.setHint("密码");
    }

    /**
     * 手机登录和用户名登录界面显示或隐藏
     */
    public void showOnLoading(boolean active) {
        if (active) {
            progressBar.setVisibility(View.VISIBLE);
            btnLogin.setVisibility(View.INVISIBLE);
            etLogin.setEnabled(false);
            etPassword.setEnabled(false);
            btnPhoneLogin.setClickable(false);
 btnRegister.setTextColor(getResources().getColor(R.color.colorTransparentGray));         btnPhoneLogin.setTextColor(getResources().getColor(R.color.colorTransparentGray));
            btnRegister.setClickable(false);
        } else {
            progressBar.setVisibility(View.GONE);
            btnLogin.setVisibility(View.VISIBLE);
            etLogin.setEnabled(true);
            etPassword.setEnabled(true);
            btnPhoneLogin.setClickable(true);
            btnRegister.setClickable(true);
            btnRegister.setTextColor(getResources().getColor(R.color.white));
            btnPhoneLogin.setTextColor(getResources().getColor(R.color.white));
        }
    }

    public void phoneLogin(final String mobile, String verifyCode) {
        if (checkPhoneLogin(mobile, verifyCode)) {
            PhoneLoginRequest req = new PhoneLoginRequest(1200, mobile, verifyCode);
            AsyncHttp.instance().postJson(req, new AsyncHttp.IHttpListener() {
                @Override
                public void onStart(int requestId) {
                    showOnLoading(true);
                }

                @Override
                public void onSuccess(int requestId, Response response) {
                    if (response.status == RequestComm.SUCCESS) {
                        Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
                    } else {
                        Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
                    }
                    showOnLoading(false);
                }
                @Override
                public void onFailure(int requestId, int httpStatus, Throwable error) {
                    showOnLoading(false);
                    Toast.makeText(LoginActivity.this, "网络异常", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }

    public boolean checkPhoneLogin(String phone, String verifyCode) {
        if (OtherUtils.isPhoneNumValid(phone)) {
            if (OtherUtils.isVerifyCodeValid(verifyCode)) {
                if (OtherUtils.isNetworkAvailable(this)) {
                    return true;
                } else {
                    ToastUtils.showShort(this, "当前无网络连接");
                }
            } else {
                ToastUtils.showShort(this, "验证码错误");
            }
        } else {
            ToastUtils.showShort(this, "手机格式错误");
        }
        return false;
    }

    public void usernameLogin(final String userName, final String password) {
        if (checkUserNameLogin(userName, password)) {
            LoginRequest req = new LoginRequest(RequestComm.login, userName, password);
            AsyncHttp.instance().postJson(req, new AsyncHttp.IHttpListener() {
                @Override
                public void onStart(int requestId) {
                    showOnLoading(true);
                }

                @Override
                public void onSuccess(int requestId, Response response) {
                    if (response.status == RequestComm.SUCCESS) {
                        Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
                    } else {
                        Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
                    }
                    showOnLoading(false);
                }

                @Override
                public void onFailure(int requestId, int httpStatus, Throwable error) {
                    showOnLoading(false);
                    Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }

    public boolean checkUserNameLogin(String userName, String password) {
        if (OtherUtils.isUsernameVaild(userName)) {
            if (OtherUtils.isPasswordValid(password)) {
                if (OtherUtils.isNetworkAvailable(this)) {
                    return true;
                } else {
                    ToastUtils.showShort(this, "当前无网络连接");
                }
            } else {
                ToastUtils.showShort(this, "密码过短");
            }
        } else {
            ToastUtils.showShort(this, "用户名不符合规范");
        }
        return false;
    }

    @Override
    public void onClick(View v) {
        if (isPhoneLogin){
            phoneLogin(etLogin.getText().toString(), etPassword.getText().toString());
        }else {
            usernameLogin(etLogin.getText().toString(), etPassword.getText().toString());
        }
    }
}

写得不到位的地方还望大家指正。

更多内容,请关注菜鸟窝(微信公众号ID: cniao5),程序猿的在线学习平台。 如需转载,请注明出处(菜鸟窝 , 原文链接: http://www.cniao5.com/forum/thread/acf3dbd419f211e7a3c000163e0230fa

关注公众号免费领取" N套客户端实战项目教程"