微信登录详细流程

流程分析

1.在登录页准备微信登录的按钮
2.当用户点击微信扫码登录,页面向微信发起获取授权的请求     ①
    http://xxxxxx.xxx.cn/callback.html
 === 3.微信直接展示扫描二维码给用户(询问用户要不要给我们项目授权)
     4.用户扫码,确认授权页面可以获取微信用户信息
5.微信收到确认,生成code(授权码),通过回调域(pethome-web)拼接code返回
     http:/xxxxxx.xxx.cn/callback.html?code=asdfaosur464f6asdfasdfadf
6.我们项目在callback.html页面上就可以获取授权码了
 
一:微信登录流程
1.在callback.html页面中,我们定义钩子函数。
   从请求栏中获取code,并且触发调用后端的微信登录接口,将code传送到后端
2.后端接口收到请求,交给service处理
3.service业务流
4.code不能为空
5.根据code从微信获取token  使用httpClient         ②
6.拿到token+openId
7.判断openId是已经存在(查询t_wxUser),
   7.1.如果已经有了并且userid不为空,直接免密登录
   7.2 如果为空,需要让用户绑定个人用户信息
      返回token+openId 前端帮我们跳转到绑定页面
 
二:微信绑定流程
1.在callback.html页面的钩子函数中
2.接收微信登录流程的返回值:
   AjaxResult {success:false,resultObj:"?token=asdfaf$openId=136462315546"}
3.跳转到binder.html页面
  location.href="binder.html"+resultObj;
4.binder.html页面解析地址栏参数并且接收用户输入的参数
5.发起微信绑定流程
   phone  verifyCode   token    openId
6.后端接收参数交给service处理
7.service业务流
  一:校验
   1.空校验
   2.判断验证码
  二:判断phone是否被注册 user
   1.如果注册了,那么我们可以直接绑定微信用户了
   2.如果没有注册过,生成t_user + t_loginInfo
  三:通过 token+openId 查询微信信息 wxuser         ③
     生成t_wxuser
  四:绑定user和wxuser
  五:免密登录

具体实现流程

打开微信开发平台(https://open.weixin.qq.com/):

 

注册完成后,需要进行开发者的认证:

 

登录后,点击登录名,进入:

大致需要个人身份证等真实信息,还需要企业签字盖章等流程,认证一次300人民币。 

认证成功后,创建网站应用,也需要企业签字盖章,还需要备案的域名,作为微信的回调 

 

创建完成后,获取到appid和appsecret,配置好回调域名。

一切参照官方文档:

 

 

 

上线:

   开发阶段:

     路由解析原理

 

6.4 配置电脑HOST文件

Host文件配置

127.0.0.1 xxxxxxx.xxxxx.xx(此处为微信开发平台注册申请的域名)

通过原理分析,要发三个请求

  1. 授权请求-a标签链接过去就OK
  2. 获取AccessToken等信息请求-Httpclient

    在我们项目中以后这样用代码来发送请求的情况有可能很多,所以抽取一个工具类,以后专门发请求。

流程分析

 

      1. 工具类的封装

package cn.itsource.basic.util;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

import java.io.IOException;

/**
 * 使用httpclient组件发送http请求
 *   get:现在只用到get
 *   post
 */
public class HttpClientUtils {
    /**
     * 发送get请求
     * @param url 请求地址
     * @return 返回内容 json
     */
    public static String httpGet(String url){

        // 1 创建发起请求客户端
        try {
            HttpClient client = new HttpClient();
            // 2 创建要发起请求-tet
            GetMethod getMethod = new GetMethod(url);
//            getMethod.addRequestHeader("Content-Type",
//                    "application/x-www-form-urlencoded;charset=UTF-8");
            getMethod.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET,"utf8");
            // 3 通过客户端传入请求就可以发起请求,获取响应对象
            client.executeMethod(getMethod);
            // 4 提取响应json字符串返回
            String result = new String(getMethod.getResponseBodyAsString().getBytes("utf8"));
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

      1. 常量封装

package cn.itsource.user.constant;

//微信登录相关常量
public class WxConstants {
    public static final String APPID = "wxd853562a0548a7d0";
    public static final String SECRET = "4a5d5615f93f24bdba2ba8534642dbb6";
    public static final String GET_ACK_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
    public static final String GET_USER_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID";

}

检查配置回调域名hosts配置

 

核心代码实现

<!--处理json-->

        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->

        <dependency>

            <groupId>com.alibaba</groupId>

            <artifactId>fastjson</artifactId>

            <version>1.2.58</version>

        </dependency>

跳转授权界面

Login.html

<script type="text/javascript">
          //创建vue是需要给他传递一个对象
          new Vue({
              el: "#loginMain", //id在hmtl模板中要对应
              data: { //数据模型
                  loginForm: {
                      username: '',
                      password: ''
                  },
                  wxAuthUrl:' https://open.weixin.qq.com/connect/qrconnect?appid=wxd853562a0548a7d0' +
                  '&redirect_uri=http://bugtracker.itsource.cn/callback.html&response_type=code&scope=snsapi_login&state=1#wechat_redirect'
              },

 

      1. 回调页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>回调</title>

    <!--集成vueaxios,还要axios全局配置(导入common.js-->
    <!--script src方式引入vueaxios-->
    <script src="js/plugins/vue/dist/vue.js"></script>
    <script src="js/plugins/axios/dist/axios.js"></script>
    <!--全局配置,以后只要用vue+axios的页面都引入common.js-->
    <script src="js/common.js"></script>
</head>
<body>
    <div id="myDiv">

    </div>
    <script type="text/javascript">
        new Vue({
            el:"#myDiv",
            mounted(){
                //解析参数对象
                let url = location.href;
                let paramObj = parseUrlParams2Obj(url);

                //获取发送请求参数
                let binderUrl = "http://bugtracker.itsource.cn/binder.html"
                let params = {"code":paramObj.code,"binderUrl":binderUrl};

                //发起微信登录请求
                this.$http.post("/login/wechat",params)
                    .then(result=>{
                        result = result.data;
                        if(result.success){ //已经关联了
                            //做登录
                            //提示
                            alert("登录成功!")
                            //tokenloginInfo存放到localStorage
                            let {token,loginInfo} = result.resultObj;
                            localStorage.setItem("token",token);
                            //把对象转换为json字符串存放
                            localStorage.setItem("loginInfo",JSON.stringify(loginInfo));
                            //跳转主页
                            location.href = "/index.html";
                        }else{ //没有关联跳转关联页面
                            let url = result.resultObj;
                            location.href = url;
                        }
                    })
                    .catch(result=>{
                        alert("系统错误");
                        console.log(result);
                    })
            }
        });
    </script>
</body>
</html>

      1. 跳转后台微信登录

LoginInfocontroller 判断是否已经绑定,如果绑定就免密登录,否则返回一个未绑定错误,配合前端跳转到绑定页面

@PostMapping("/wechat")
public AjaxResult loginWechat(@RequestBody Map<String,String> params){

    try {
        return  loginInfoService.loginWechat(params);
    } catch (Exception e) {
        e.printStackTrace();
        return AjaxResult.me().setMessage("系统错误!"+e.getMessage());
    }
}

LoginInfoServiceImpl

 @Override
public AjaxResult loginWechat(Map<String, String> params) {
    //1 获取code
    String code = params.get("code");
    String binderUrl = params.get("binderUrl");
    //2 获取accessToken
    String getAckUrl = WxConstants.GET_ACK_URL
            .replace("APPID", WxConstants.APPID)
            .replace("SECRET", WxConstants.SECRET)
            .replace("CODE", code);
    String jsonStr = HttpClientUtils.httpGet(getAckUrl);
    JSONObject jsonObject = JSONObject.parseObject(jsonStr);
    String accessToken = jsonObject.getString("access_token");
    String openid = jsonObject.getString("openid"); //就相当于微信号

    //3 判断是否已经关联了
    WxUser wxUser = wxUserMapper.loadByOpenId(openid);
    if(wxUser!=null && wxUser.getUser_id()!=null){

        //查询Logininfo
        LoginInfo loginInfo =loginInfoMapper.loadByUserId(wxUser.getUser_id());
        //3.1 如果关联了实现免密登录
        String token = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(token,loginInfo,30, TimeUnit.MINUTES);
        Map<String,Object> result = new HashMap<>();
        result.put("token",token);
        loginInfo.setSalt(null);
        loginInfo.setPassword(null);
        result.put("loginInfo",loginInfo);
        return AjaxResult.me().setResultObj(result);
    }else{

        //3.2 否则跳转到绑定页面
        binderUrl = binderUrl+"?accessToken="+accessToken+"&openId="+openid;
        return AjaxResult.me().setSuccess(false).setResultObj(binderUrl);
    }
}

      1. 绑定页面

<!DOCTYPE html>
<html>

   <head lang="en">
      <meta charset="UTF-8">
      <title>注册</title>
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
      <meta name="format-detection" content="telephone=no">
      <meta name="renderer" content="webkit">
      <meta http-equiv="Cache-Control" content="no-siteapp" />

      <link rel="stylesheet" href="./AmazeUI-2.4.2/assets/css/amazeui.min.css" />
      <link href="./css/dlstyle.css" rel="stylesheet" type="text/css">
      <script src="./AmazeUI-2.4.2/assets/js/jquery.min.js"></script>
      <script src="./AmazeUI-2.4.2/assets/js/amazeui.min.js"></script>

      <!--script src方式引入vueaxios-->
      <script src="js/plugins/vue/dist/vue.js"></script>
      <script src="js/plugins/axios/dist/axios.js"></script>
      <!--全局配置,以后只要用vue+axios的页面都引入common.js-->
      <script src="js/common.js"></script>


      <!--一个一个页面配置,搞一个公共common.js,以后只需要引入它就ok-->
      <!--<script type="text/javascript">-->
            <!--//配置axios的全局基本路径-->
            <!--axios.defaults.baseURL='http://localhost:8080/'-->
            <!--// axios.defaults.baseURL='/api' //前端跨域配置-->
            <!--//全局属性配置,在任意组件内可以使用this.$http获取axios对象-->
            <!--Vue.prototype.$http = axios-->

      <!--</script>-->

   </head>

   <body>

      <div class="login-boxtitle">
         <a href="home/demo.html"><img alt="" src="./images/logobig.png" /></a>
      </div>

      <div class="res-banner">
         <div class="res-main">
            <div class="login-banner-bg"><span></span><img src="./images/big.jpg" /></div>
            <div class="login-box">

                  <div class="am-tabs" id="doc-my-tabs">
                     <div class="am-tabs-bd">
                        <div class="am-tab-panel am-active"id="myDiv" >
                           <form method="post">
                 <div class="user-phone">
                            <label for="phone"><i class="am-icon-mobile-phone am-icon-md"></i></label>
                            <input type="tel" name="" id="phone" v-model="phoneUserForm.phone" placeholder="请输入手机号">
                 </div>                                                          
                              <div class="verification">
                                 <label for="code"><i class="am-icon-code-fork"></i></label>
                                 <input type="tel" name="" id="code" v-model="phoneUserForm.verifyCode" placeholder="请输入验证码">
                                 <!--<a class="btn" href="javascript:void(0);" οnclick="sendMobileCode();" id="sendMobileCode">-->
                                    <!--<span id="dyMobileButton">获取</span></a>-->
                                 <button type="button" @click="sendMobileCode">获取</button>
                              </div>
                           </form>
                         <div class="login-links">
                              <label for="reader-me">
                                 <input id="reader-me" type="checkbox"> 点击表示您同意商城《服务协议》
                              </label>
                        </div>
                              <div class="am-cf">
                                 <input type="button" @click="binder" name="" value="绑定授权" class="am-btn am-btn-primary am-btn-sm am-fl">
                              </div>
                        
                           <hr>
                        </div>

                        <script>
                           $(function() {
                               $('#doc-my-tabs').tabs();
                             })
                        </script>

                     </div>
                  </div>

            </div>
         </div>
         
               <div class="footer ">
                  <div class="footer-hd ">
                     <p>
                        <a href="# ">恒望科技</a>
                        <b>|</b>
                        <a href="# ">商城首页</a>
                        <b>|</b>
                        <a href="# ">支付宝</a>
                        <b>|</b>
                        <a href="# ">物流</a>
                     </p>
                  </div>
                  <div class="footer-bd ">
                     <p>
                        <a href="# ">关于恒望</a>
                        <a href="# ">合作伙伴</a>
                        <a href="# ">联系我们</a>
                        <a href="# ">网站地图</a>
                        <em>© 2015-2025 Hengwang.com 版权所有. 更多模板 <a href="http://www.cssmoban.com/" target="_blank" title="模板之家">模板之家</a> - Collect from <a href="http://www.cssmoban.com/" title="网页模板" target="_blank">网页模板</a></em>
                     </p>
                  </div>
               </div>
   </body>

    <script type="text/javascript">
      new Vue({
         "el":"#myDiv",
         data:{
                phoneUserForm:{
                    phone:"13330964748",
               verifyCode:"",
               accessToken:null,
               openId:null
            }
         },
            methods:{
                binder(){
                    this.$http.post("/login/binder/wechat",this.phoneUserForm)
                  .then(result=>{
                      result = result.data;
                            //提示
                            alert("登录成功!")
                            //tokenloginInfo存放到localStorage
                            let {token,loginInfo} = result.resultObj;
                            localStorage.setItem("token",token);
                            //把对象转换为json字符串存放
                            localStorage.setItem("loginInfo",JSON.stringify(loginInfo));
                            console.log(result,"fjfjjfjfjfjfjjfjf")
                            //跳转主页
                            location.href = "/index.html";
                  })
                  .catch(result=>{
                      console.log(result,"jjjjj")
                      alert("系统错误!");
                  })
            },
                sendMobileCode(){
                    //1.判断手机号不为空
                    if(!this.phoneUserForm.phone){
                        alert("手机号不能为空");
                        return;
                    }

                    //2.获取按钮,禁用按钮  发送时灰化不能使用,发送成功倒计时60才能使用,如果发送失败立即可以发送
                    var sendBtn = $(event.target);
                    sendBtn.attr("disabled",true);
                    this.$http.post('/verifycode/smsCode',
                        {"phone":this.phoneUserForm.phone,"type":"binder"}).then((res) => {
                        console.log(res);
                        var ajaxResult = res.data;
                        if(ajaxResult.success){
                            alert("手机验证码已经发送到您的手机,请在3分钟内使用");
                            //3.1.发送成:倒计时
                            var time = 60;

                            var interval = window.setInterval( function () {
                                //每一条倒计时减一
                                time = time - 1 ;

                                //把倒计时时间搞到按钮上
                                sendBtn.html(time);

                                //3.2.倒计时完成恢复按钮
                                if(time <= 0){
                                    sendBtn.html("重发");
                                    sendBtn.attr("disabled",false);
                                    //清除定时器
                                    window.clearInterval(interval);
                                }
                            },1000);
                        }else{
                            //3.3.发送失败:提示,恢复按钮
                            sendBtn.attr("disabled",false);
                            alert("发送失败:"+ajaxResult.message);
                        }
                    });
                }
            },
         mounted(){
            let paramObj =  parseUrlParams2Obj(location.href);
            if(paramObj){
                this.phoneUserForm.accessToken = paramObj.accessToken;
                this.phoneUserForm.openId = paramObj.openId;
               }

         }
      })
   </script>

</html>

改造发送短信验证接口

@RestController
@RequestMapping("/verifycode")
public class VerifyCodeController {

    @Autowired
    private IVerifyCodeService verifyCodeService;
    //一定情况下Map能够代替类使用
    @PostMapping("/smsCode") //注册验证码
    public AjaxResult sendSmsCode(@RequestBody Map<String,String> params){
        String phone = params.get("phone");
        String type = params.get("type"); //register binder login
        try {
            verifyCodeService.sendSmsCode(params);
            return AjaxResult.me();
        }
        catch (BusinessException e){
            e.printStackTrace();
            return AjaxResult.me().setMessage("发送失败!"+e.getMessage());
        }
        catch (Exception e) {
            return AjaxResult.me().setMessage("系统错误!"+e.getMessage());
        }
    }
}

VerifyCodeServiceImpl

@Override
    public void sendSmsCode(Map<String,String> params) {
        String phone = params.get("phone");
        String type = params.get("type");

        //1 校验
        //1.1 手机号不能为null
        if (!StringUtils.hasLength(phone))
            throw new BusinessException("请输入手机号!");
        if ("register".equals(type)){ //注册
            //1.2 不能被注册
            User user = userMapper.loadByPhone(phone);
            if (user!=null)
                throw new BusinessException("用户已经被注册!");

            String businessKey = UserConstants.REGISTER_CODE_PREFIX + phone;
            sendSmsCode(businessKey);
        }else if("binder".equals(type)){ //绑定
            String businessKey = UserConstants.BINDER_CODE_PREFIX + phone;
            sendSmsCode(businessKey);
        }


    }

    private void  sendSmsCode(String businessKey){
        //2 判断原来的是否有效
        Object codeObj = redisTemplate
                .opsForValue().get(businessKey); //code:time
        String code = "";
        //2.1 如果有效
        if (codeObj!=null){
            String codeStr = (String) codeObj;
            //2.1.1 判断是否已过重发时间
            String time = codeStr.split(":")[1]; //114555558888
            long intervalTime = System.currentTimeMillis() - Long.valueOf(time);
            if (intervalTime<=1*60*1000){
                //2.1.1.1 如果没有过提示错误
                throw new BusinessException("请勿重复发送短信验证码!");
            }
            //2.1.1.2 如果过了,就使用原来验证码
            code = codeStr.split(":")[0];
        }else{
            //2.2 如果没有
            //2.2.1 重新生成验证码
            code = StrUtils.getComplexRandomString(4);
        }
        //3 保存验证码到redis
        redisTemplate.opsForValue().set(businessKey
                ,code+":"+System.currentTimeMillis()
                ,3, TimeUnit.MINUTES);
        //4 调用短信接口发送短信
//        SmsUtil.sendMsg(phone,"您的验证码为:"+code+",请在3分钟之内使用!");
        System.out.println("您的验证码为:"+code+",请在3分钟之内使用!");
    }

   

      1. 绑定实现

LoginController

@PostMapping("/binder/wechat")
public AjaxResult binderWechat(@RequestBody Map<String,String> params){

    try {
        return  loginInfoService.binderWechat(params);
    } catch (Exception e) {
        e.printStackTrace();
        return AjaxResult.me().setMessage("系统错误!"+e.getMessage());
    }
}

LoginInfoService

//前台输入手机号是否有账号,如果有创建wxUser帮上就ok,如果没有创建账号在绑定
@Override
public AjaxResult binderWechat(Map<String, String> params) {
    //参数
    String phone = params.get("phone");
    String verifyCode = params.get("verifyCode");
    String accessToken = params.get("accessToken");
    String openId = params.get("openId");

    //0 验证码比对
    Object codeObj = redisTemplate.opsForValue().get(UserConstants.BINDER_CODE_PREFIX + phone);
    if(codeObj==null){
        return AjaxResult.me().setMessage("请重新获取验证码后再操作!");
    }else{
        String codeStr = (String) codeObj;
        String code = codeStr.split(":")[0];//codetime
        if (!verifyCode.equalsIgnoreCase(code)){
            return AjaxResult.me().setMessage("请输入正确验证码后再操作!");
        }
    }


    //1 获取微信用户信息
    String url = WxConstants.GET_USER_URL
            .replace("ACCESS_TOKEN", accessToken)
            .replace("OPENID", openId);

    String jsonStr = HttpClientUtils.httpGet(url);

    //2通过电话和type获取用户登录信息
    LoginDto loginDto = new LoginDto();
    loginDto.setUsername(phone);
    loginDto.setLoginType(1);
    LoginInfo info = loginInfoMapper.loadByPhone(loginDto);
    //3如果用户登录信息不存在
    User user = null;
    if (info==null){
        user = wxUser2User(phone);
        info = user2LoginInfo(user);
        //3.1 创建loginInfo
        loginInfoMapper.save(info);
        //3.2 创建User
        user.setInfo(info);
        userMapper.save(user);
    }else{
        //4用户存在 查询用户
        user = userMapper.loadByPhone(phone);

    }
    //5把用户和wxUser进行绑定
    WxUser wxUser = wxUserJsonStr2WxUser(jsonStr);
    wxUser.setUser_id(user.getId());
    wxUserMapper.save(wxUser);
    //6做免密登录
    //3.1 如果关联了实现免密登录
    String token = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set(token,info,30, TimeUnit.MINUTES);
    Map<String,Object> result = new HashMap<>();
    result.put("token",token);
    info.setSalt(null);
    info.setPassword(null);
    result.put("loginInfo",info);
    return AjaxResult.me().setResultObj(result);

}

private LoginInfo user2LoginInfo(User user) {
    LoginInfo info = new LoginInfo();
    BeanUtils.copyProperties(user,info); //按照同名原则拷贝属性
    return info;
}

private User wxUser2User(String phone) {

    User user = new User();
    user.setUsername(phone);
    user.setPhone(phone);
    user.setEmail(null);
    //给一个随机密码
    String salt = UUID.randomUUID().toString();
    String password = MD5Utils.encrypByMd5(StrUtils.getComplexRandomString(6)+salt);
    user.setPassword(password);
    user.setSalt(salt);
    user.setState(1);
    user.setCreatetime(new Date());
    return user;
}

private WxUser wxUserJsonStr2WxUser(String jsonStr) {
    JSONObject jsonObject = JSONObject.parseObject(jsonStr);
    WxUser wxUser = new WxUser();
    wxUser.setOpenid(jsonObject.getString("openid"));
    wxUser.setNickname(jsonObject.getString("nickname"));
    wxUser.setSex(jsonObject.getInteger("sex"));
    wxUser.setAddress(null);
    wxUser.setHeadimgurl(jsonObject.getString("headimgurl"));
    wxUser.setUnionid(jsonObject.getString("unionid"));
    return wxUser;
}

      1. 后台获取登录用户

package cn.itsource.basic.util;

import cn.itsource.user.domain.LoginInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * 登录的上下文
 *    登录用户的一些信息放到这里面,以后直接获取就ok
 *    1 获取登录用户
 *    2 获取登录用户的角色或权限
 *    ....
 * ==========只是一个工具类不需要交给Spring管理=================
 *
 * ?? 一个不受spirng管理的bean,要获取受spring管理的bean
 */
public class LoginContext {

    /**
     * 获取当前登录用户信息
     * @param request
     * @return
     */
    public static LoginInfo getLoginInfo(HttpServletRequest request){
        //从请求头中获取token
        String token = request.getHeader("token");
        //使用tokenredis中获取登录信息
        if (!StringUtils.isEmpty(token)){
            //1 获取spring容器
            WebApplicationContext context = WebApplicationContextUtils
                    .getWebApplicationContext(request.getServletContext());
            //2 通过容器获取bean
            RedisTemplate redisTemplate = (RedisTemplate) context
                    .getBean("redisTemplate");

            //3 获取登录信息
            Object loginInfoObj = redisTemplate.opsForValue().get(token);
            if (loginInfoObj!=null)
                return (LoginInfo) loginInfoObj;
        }
        return null;
    }
}

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值