基于Springboot2.x+vue3.x整合实现微信扫码登录

第1章 准备工作

1.1 微信开放平台

微信扫码登录,需要在微信开放平台注册账号被认证为开发者才能接入官网地址:https://open.weixin.qq.com/

1.1.1 注册账号并认证成为开发者

下图就是官网,我们点击注册,然后根据要求填写必要的信息,点击提交即可。
在这里插入图片描述

1.1.2 创建网站应用获取应用AppID 和 AppSecret

在这里插入图片描述

1.2 搭建内网穿透

使用免费的内网穿透,官网地址:https://suidao.io/#/

1.2.1 注册账号并登录

先登录网站,然后注册账号。
在这里插入图片描述
注册完账号后,就可以登录了
在这里插入图片描述

1.2.2 创建隧道

先创建隧道。

在这里插入图片描述
在这里插入图片描述

  • 得到外网访问路径:
    在这里插入图片描述

1.2.3 下载客户端

在这里插入图片描述

下载解压后直接双击SuiDao.Client.exe运行即可

在这里插入图片描述

1.3 修改微信开放平台应用授权回调域

把它修改成内网穿透的地址即可,如图所示:

在这里插入图片描述

第2章 后端项目搭建以及开发

2.1 从gitee码云上获取后端demo

gitee地址:https://gitee.com/77jubao2015/springbootdemo

2.1.1 引入相关架包

  <!--httpclient-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>
        <!--commons-io-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.3</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

2.2 创建数据库xueden_wxcode

打开数据库客户端创建数据库,如图说是:

在这里插入图片描述

2.3 修改配置文件application.yml

2.3.1 修改数据库连接账号和密码

在这里插入图片描述

修改为自己本地电脑的即可

2.3.2 配置 AppIDAppSecret 以及授权回调域

代码如下所示:

wxcode:
  appId: wx7aa745fb92387941
  appSecret: xxxxxxxxxxxxxx
  redirectUri: http://wxcode.sh1.k9s.run:2271/wechat/callback
  frontUrl: http://localhost:8080/

2.4 编写一个WechatConfig配置类

代码如下所示:

package cn.xueden.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**微信开放平台配置文件
 * @Auther:梁志杰
 * @Date:2021/11/13
 * @Description:cn.xueden.config
 * @version:1.0
 */
@Configuration
@Data
@ConfigurationProperties(prefix = "wxcode")
public class WechatConfig {

    private String appId;

    private String appSecret;

    private String redirectUri;
}


2.5 编写一个WeChatHttpUtils工具类

代码如下所示:

package cn.xueden.utils;

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;

/**
 * @Auther:梁志杰
 * @Date:2021/11/13
 * @Description:cn.xueden.utils
 * @version:1.0
 */
public class WeChatHttpUtils {

    public static CloseableHttpClient getClient(){
        HttpClientBuilder builder = HttpClientBuilder.create();
        return builder.build();
    }
}


2.6 编写WxApiController控制类

2.6.1 编写一个获取code方法getWxCode

生成微信二维码,代码如下所示:

  //1、生成微信二维码
    @GetMapping("login")
    public String getWxCode() {
        //固定地址,拼接参数
        //微信开放平台授权baseUrl  固定格式
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s" +
                "#wechat_redirect";

        //对redirect_url进行URLEncoder编码
        String redirectUrl = wechatConfig.getRedirectUri();

        try {
            //对URL进行utf-8的编码
            URLEncoder.encode(redirectUrl, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        String url = String.format( //向指定字符串中按顺序替换%s
                baseUrl,
                wechatConfig.getAppId(),
                wechatConfig.getRedirectUri(),
                "Xueden" //自定义(随意设置)
        );


        //请求微信地址
        return "redirect:" + url;
    }

2.6.2 编写一个callback方法

通过code获取access_token 并把扫描人信息添加到数据库里

代码如下所示:

  @GetMapping("callback")
    //1、获取code值,临时票据、类似于验证码(该数据为扫码后跳转时微信方传来)
    public String callback(String code,String state, Model model) {


        //2、拿着code请求微信固定的地址,得到两个值access_token 和 openid
        String baseAccessTokenUrl =
                "https://api.weixin.qq.com/sns/oauth2/access_token" +
                        "?appid=%s" +
                        "&secret=%s" +
                        "&code=%s" +
                        "&grant_type=authorization_code";
        //3、拼接三个参数:id   密钥   code值
        String accessTokenUrl = String.format(
                baseAccessTokenUrl,
                wechatConfig.getAppId(),
                wechatConfig.getAppSecret(),
                code
        );
        HttpGet httpGet = null;
        CloseableHttpResponse response = null;
        URIBuilder uriBuilder = null;
        WxMember wxMember = new WxMember();
        try {
            //请求这个拼接好的地址,得到两个值access_token 和 openid
            //使用httpClient发送请求,得到返回结果
            httpGet = new HttpGet(accessTokenUrl);
            response = WeChatHttpUtils.getClient().execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();

            JSONObject jsonObject = JSON.parseObject(EntityUtils.toString(response.getEntity()));

            String access_token = jsonObject.getString("access_token");
            String openid = jsonObject.getString("openid");
            String unionid = jsonObject.getString("unionid");

            // 获取扫码人信息
            uriBuilder = new URIBuilder("https://api.weixin.qq.com/sns/userinfo");
            uriBuilder.setParameter("access_token",access_token);
            uriBuilder.setParameter("openid",openid);
            uriBuilder.setParameter("lang","zh_CN");
            httpGet.setHeader("Accept", "application/json");
            httpGet.addHeader("Content-type","application/json; charset=utf-8");
            httpGet = new HttpGet(uriBuilder.build());
            response = WeChatHttpUtils.getClient().execute(httpGet);
            JSONObject jsonUserinfo = JSON.parseObject(EntityUtils.toString(response.getEntity()));
            log.info("access_token{},openid{},unionid{},获取信息{}",access_token, openid,unionid,jsonUserinfo);

            wxMember.setCity(jsonUserinfo.getString("city"));
            wxMember.setCountry(jsonUserinfo.getString("country"));
            wxMember.setProvince(jsonUserinfo.getString("province"));
            wxMember.setHeadimgurl(jsonUserinfo.getString("headimgurl"));
            String nickname = new String(jsonUserinfo.getString("nickname").getBytes("ISO-8859-1"), "UTF-8");
            wxMember.setNickname(nickname);
            wxMember.setOpenid(openid);
            wxMember.setUnionid(unionid);
            wxMember.setSex(jsonUserinfo.getInteger("sex"));
            wxMemberService.saveOrUpdate(wxMember);
        } catch (Exception e) {
            e.printStackTrace();

        }
        model.addAttribute("wxmember",wxMember);
        model.addAttribute("frontUrl", wechatConfig.getFrontUrl());
        return "result";
    }

2.6.3 编写一个获取用户列表的方法

代码如下所示:

 /**
     * 带条件分页查询用户列表
     */
    @GetMapping
    public ResponseEntity<Object> getList(WxMemberQueryCriteria criteria, PageVo pageVo) throws Exception {
        int pageNo = pageVo.getPageIndex()-1;
        Pageable pageable = PageRequest.of(  pageNo<0?0:pageNo, pageVo.getPageSize() , Sort.Direction.DESC, "id" );

        return new ResponseEntity<>(wxMemberService.getList(criteria,pageable), HttpStatus.OK);
    }

2.6.4 编写一个获取用户列表的业务接口和实现方法

代码如下所示:

 /**
     *
     * @param criteria
     * @param pageable
     * @return
     */
    Object getList(WxMemberQueryCriteria criteria, Pageable pageable);
/**
     * 根据条件分页获取系统管理员列表信息
     * @param criteria 查询条件
     * @param pageable 分页信息
     * @return
     */
    @Override
    public Object getList(WxMemberQueryCriteria criteria, Pageable pageable) {
        Page<WxMember> page = wxMemberRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable);
        return PageUtil.toPage(page);
    }

2.6.5 编写用户实体类WxMember

代码如下所示:

package cn.xueden.domain;


import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;

import javax.validation.constraints.NotNull;
import javax.persistence.*;
import java.sql.Timestamp;

/**
 * @Auther:梁志杰
 * @Date:2021/5/16
 * @Description:cn.xueden
 * @version:1.0
 */
@Data
@Entity
@Table(name="t_member")
@org.hibernate.annotations.Table(appliesTo = "t_member",comment="微信用户信息表")
public class WxMember {

    /**
     * 自增 id
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    @NotNull(groups = Update.class)
    private Long id;

    /**
     * 用户openid
     */
    private String openid;

    /**
     * 用户unionid
     */
    private String unionid;

    /**
     * 用户昵称
     */
    private String nickname;

    /**
     * 性别
     */
    private Integer sex;

    /**
     * 用户所属省份
     */
    private String province;

    /**
     * 用户所属城市
     */
    private String city;

    /**
     * 用户所属国家
     */
    private String country;

    /**
     * 用户头像
     */
    private String headimgurl;

    /**
     * 创建时间
     */
    @Column(name = "create_time",nullable = false)
    @CreationTimestamp
    private Timestamp createTime;

    public @interface Update {}
}

2.6.6 编写一个查询参数类WxMemberQueryCriteria

代码如下所示:

package cn.xueden.dto;

import cn.xueden.annotation.EnableXuedenQuery;
import lombok.Data;

/**功能描述:查询条件
 * @Auther:梁志杰
 * @Date:2021/5/16
 * @Description:cn.xueden.dto
 * @version:1.0
 */
@Data
public class WxMemberQueryCriteria {

    /**
     * 根据性别查询
     */
    @EnableXuedenQuery
    private int sex;

    /**
     * 根据昵称模糊查询
     */
    @EnableXuedenQuery(blurry = "nickname")
    private String nickname;
}

2.6.7 添加一个result.html页面

在resources文件夹下新建一个名为templates文件夹,并在此文件夹下新建result.html页面

代码如下所示:

<!DOCTYPE html>
<html lang="ch" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
        name="viewport">
  <meta content="ie=edge" http-equiv="X-UA-Compatible">
  <link rel="icon" th:href="@{resource/favicon.ico}" type="image/x-icon"/>
  <title>登录跳转中</title>
</head>
<body>
登录中..
<script th:inline="javascript">
  var response = [[${wxmember}]];
  var frontUrl = [[${frontUrl}]];
  window.onload = function () {
    window.opener.postMessage(response, frontUrl);
    window.close();
  }
</script>
</body>
</html>

第3章 前端项目搭建以及开发

3.1 从gitee获取前端demo

gitee地址:https://gitee.com/77jubao2015/wxpaydemo

3.2 修改订单列表组件index.vue

代码如下所示:

<template>
  <div>
    <div class="search__example--wrap">
      <com-search
        :data="searchData"
        @search-submit="searchSubmit"
        @reset-submit="resetSubmit"
      />
    </div>

    <com-table
      v-loading="loading"
      :columns="columns"
      :data="tableData"
      :pagination="{
        currentPage: defalutParams.pageIndex,
        total: total,
        onSizeChange: handleSizeChange,
        onCurrentChange: handleCurrentChange
      }"
      @selection-change="handleSelectionChange"
    >
      <template #sex="scope">
        <el-tag
          :type="scope.row.sex === 0
            ? 'success'
            : (scope.row.sex === 1
              ? 'warning'
              : 'danger')"
        >{{ scope.row.sex === 0
          ? '男性'
          : (scope.row.sex === 1
            ? '女性'
            : '未知') }}
        </el-tag>
      </template>
    </com-table>

    <com-dialog v-model="dialogVisible" :title="title">
      <info-write
        v-if="comName === 'InfoWrite'"
        :info="info"
        @close="toggleVisible"
        @success="success"
      />
      <face-pay
        v-if="comName === 'FacePay'"
        :info="info"
        @close="toggleVisible"
        @success="success"
      />
      <detail
        v-if="comName === 'Detail'"
        :info="info"
        @close="toggleVisible"
      />
    </com-dialog>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import InfoWrite from './components/InfoWrite.vue'
import Detail from './components/Detail.vue'
import FacePay from './components/FacePay.vue'
import { useExample } from '@/hooks/useExample'
import { Message } from '_c/Message'

import { getListApi, delsApi, nativePayApi, huaBeiPayApi } from './api'
import { formatTime } from '@/utils'

const searchData = [
  {
    label: '用户昵称',
    value: '',
    itemType: 'input',
    field: 'nickname',
    placeholder: '请输入用户昵称',
    clearable: true
  },
  {
    label: '用户性别',
    value: '',
    itemType: 'select',
    field: 'sex',
    options: [{
      title: '男性',
      value: '0'
    }, {
      title: '女性',
      value: '1'
    }]
  }
]

const columns = [
  {
    field: 'createTime',
    label: '登录时间',
    formatter: (row: any, column: any, cellValue: any, index: number) => {
      return formatTime(row.createTime, 'yyyy-MM-dd HH:mm:ss')
    }

  },
  {
    field: 'nickname',
    label: '昵称'
  },
  {
    field: 'openid',
    label: 'openid',
    showOverflowTooltip: true
  },

  {
    field: 'sex',
    label: '性别',
    slots: {
      default: 'sex'
    }
  },
  {
    field: 'unionid',
    label: 'unionid'
  }
]

export default defineComponent({
  // name: 'ExampleDialog',
  components: {
    InfoWrite,
    Detail,
    FacePay
  },
  setup() {
    const info = ref<any>(null)

    const {
      defalutParams,
      tableData,
      loading,
      total,
      dialogVisible,
      title,
      currentChange,
      sizeChange,
      handleSelectionChange,
      selectionData,
      delData,
      comName,
      toggleVisible
    } = useExample()

    // 请求数据
    async function getExampleList(data?: any): Promise<void> {
      try {
        const res = await getListApi({
          params: Object.assign(defalutParams, data || {})
        })
        total.value = res.totalElements
        tableData.value = res.content
      } finally {
        loading.value = false
      }
    }

    // 查询
    function searchSubmit(data: any) {
      // 该方法重置了一些默认参数
      currentChange(1)
      getExampleList(data)
    }

    // 重置
    function resetSubmit(data: any) {
      // 该方法重置了一些默认参数
      currentChange(1)
      getExampleList(data)
    }

    // 展示多少条
    function handleSizeChange(val: number) {
      // 该方法重置了一些默认参数
      sizeChange(val)
      getExampleList()
    }

    // 展示第几页
    function handleCurrentChange(val: number) {
      // 该方法重置了一些默认参数
      currentChange(val)
      getExampleList()
    }

    // 删除多选
    function dels(item?: any) {
      delData(async() => {
        let ids: number[] = []
        if (item.id) {
          ids.push(item.id)
        } else {
          ids = selectionData.value.map((v: any) => {
            return v.id
          })
        }
        const res = await delsApi({
          data: JSON.stringify(ids)
        })
        if (res.status === 200) {
          Message.success(res.message)
          getExampleList()
        }
      }, { hiddenVerify: item.id, text: '此操作将申请退款, 是否继续?' })
    }

    // 打开弹窗
    function open(row: any, component: string) {
      comName.value = component
      title.value = !row ? '新增' : (component === 'Detail' ? '刷脸付' : '当面付(打开支付宝扫一扫)')
      info.value = row || null
      toggleVisible(true)
    }

    // 成功之后的回调
    function success(type: string) {
      if (type === 'add') {
        currentChange(1)
      }
      toggleVisible()
      getExampleList()
    }

    getExampleList()

    // 调用支付宝网站支付
    async function getAliDet(id: number) {
      try {
        const res = await nativePayApi({
          params: {
            id: id
          }
        })
        if (res.status === 200) {
          const body = document.querySelector('body')
          if (body != null) {
            body.innerHTML = res.message // 查找到当前页面的body,将后台返回的form替换掉他的内容
          }
          document.forms[0].setAttribute('target', '_blank') // 新开窗口跳转
          document.forms[0].submit() // 执行submit表单提交,让页面重定向,跳转到支付宝页面
        }
      } catch (e) {
        console.log(e)
      }
    }
    // 调用支付宝花呗分期支付
    async function getHuaBeiDet(id: number) {
      try {
        const res = await huaBeiPayApi({
          params: {
            id: id
          }
        })
        if (res.status === 200) {
          console.info('返回信息:', res)
        }
      } catch (e) {
        console.log(e)
      }
    }

    return {
      info, open,
      searchData, searchSubmit, resetSubmit,
      columns,
      defalutParams,
      loading,
      tableData,
      total,
      title,
      dialogVisible,
      handleSizeChange,
      handleCurrentChange,
      handleSelectionChange,
      dels,
      close, success,
      comName,
      toggleVisible,
      getAliDet,
      getHuaBeiDet
    }
  }
})
</script>

<style>
</style>

3.3 修改api.ts文件

代码如下所示:

export const getListApi = ({ params }: PropsData): any => {
  return fetch({ url: '/wechat', method: 'get', params })
}

3.4 修改登录页面

代码所示:

<template>
  <div class="login-wrap" @keydown.enter="login">
    <div class="login-con">
      <el-card class="box-card">
        <template #header>
          <span class="login--header">登录</span>
        </template>
        <el-form
          ref="loginForm"
          :model="form"
          :rules="rules"
          class="login-form"
        >
          <el-form-item prop="userName">
            <el-input
              v-model="form.userName"
              placeholder="请输入账号 admin or test"
              class="form--input"
            >
              <template #prefix>
                <span class="svg-container">
                  <svg-icon icon-class="user" />
                </span>
              </template>
            </el-input>
          </el-form-item>
          <el-form-item prop="passWord">
            <el-input
              v-model="form.passWord"
              show-password
              :minlength="3"
              :maxlength="18"
              placeholder="请输入密码 admin or test"
              class="form--input"
            >
              <template #prefix>
                <span class="svg-container">
                  <svg-icon icon-class="password" />
                </span>
              </template>
            </el-input>
          </el-form-item>
          <el-form-item>
            <el-button
              :loading="loading"
              type="primary"
              class="login--button"
              @click="login"
            >
              登录
            </el-button>
          </el-form-item>
          <el-form-item>
            <el-button
              :loading="loading"
              type="success"
              class="login--button"
              @click="wxlogin"
            >
              微信扫码登录
            </el-button>
          </el-form-item>
        </el-form>
      </el-card>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, unref, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { permissionStore } from '@/store/modules/permission'
import { appStore } from '@/store/modules/app'
import wsCache from '@/cache'
import { ElNotification } from 'element-plus'

interface FormModule {
  userName: string,
  passWord: string
}
interface RulesModule {
  userName: any[],
  passWord: any[]
}

export default defineComponent({
  name: 'Login',
  setup() {
    const { push, addRoute, currentRoute } = useRouter()
    const loginForm = ref<HTMLElement | null>(null)
    const loading = ref<boolean>(false)
    const redirect = ref<string>('')
    watch(() => {
      return currentRoute.value
    }, (route) => {
      redirect.value = (route.query && route.query.redirect) as string
    }, {
      immediate: true
    })
    const page = reactive({
      width: window.screen.width * 0.5,
      height: window.screen.height * 0.5
    })
    const form = reactive<FormModule>({
      userName: '',
      passWord: ''
    })
    const rules = reactive<RulesModule>({
      userName: [{ required: true, message: '请输入账号' }],
      passWord: [{ required: true, message: '请输入密码' }]
    })
    async function login(): Promise<void> {
      const formWrap = unref(loginForm) as any
      if (!formWrap) return
      loading.value = true
      try {
        formWrap.validate(async(valid: boolean) => {
          if (valid) {
            wsCache.set(appStore.userInfo, form)
            permissionStore.GenerateRoutes().then(() => {
              permissionStore.addRouters.forEach(async(route: RouteRecordRaw) => {
                await addRoute(route.name!, route) // 动态添加可访问路由表
              })
              permissionStore.SetIsAddRouters(true)
              push({ path: redirect.value || '/' })
            })
          } else {
            console.log('error submit!!')
            return false
          }
        })
      } catch (err) {
        console.log(err)
      } finally {
        loading.value = false
      }
    }

    async function resolveSocialLogin(e: any): Promise<void> {
      console.info('传入参数', e.data)
      wsCache.set(appStore.userInfo, e.data)
      permissionStore.GenerateRoutes().then(() => {
        permissionStore.addRouters.forEach(async(route: RouteRecordRaw) => {
          await addRoute(route.name!, route) // 动态添加可访问路由表
        })
        permissionStore.SetIsAddRouters(true)
        push({ path: redirect.value || '/' })
      })
    }

    async function wxlogin(): Promise<void> {
      const url = 'http://wxcode.sh1.k9s.run:2271/wechat/login'
      window.open(url, 'newWindow', `resizable=yes, height=${page.height}, width=${page.width}, top=10%, left=10%, toolbar=no, menubar=no, scrollbars=no, resizable=no,location=no, status=no`)
      window.addEventListener('message', resolveSocialLogin, false)
    }

    ElNotification({
      title: '提示',
      message: '账号 admin 为 前端 控制路由权限,账号 test 为 后端 控制路由权限。密码与账号相同',
      duration: 60000
    })

    return {
      loginForm,
      loading, redirect, form, rules, page,
      login,
      wxlogin
    }
  }
})
</script>

<style lang="less" scoped>
.login-wrap {
  width: 100%;
  height: 100%;
  background-image: url('~@/assets/img/login-bg.jpg');
  background-size: cover;
  background-position: center;
  position: relative;
  .box-card {
    width: 400px;
    .login--header {
      font-size: 24px;
      font-weight: 600;
    }
    .svg-container {
      color: #889aa4;
      vertical-align: middle;
      width: 30px;
      display: inline-block;
    }
    .form--input {
      width: 100%;
      @{deep}(.el-input__inner) {
        padding-left: 40px;
      }
    }
    .login--button {
      width: 100%;
    }
  }
  .login-con {
    position: absolute;
    right: 160px;
    top: 50%;
    transform: translateY(-60%);
  }
}
</style>

e’, resolveSocialLogin, false)
}

ElNotification({
  title: '提示',
  message: '账号 admin 为 前端 控制路由权限,账号 test 为 后端 控制路由权限。密码与账号相同',
  duration: 60000
})

return {
  loginForm,
  loading, redirect, form, rules, page,
  login,
  wxlogin
}

}
})






















  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨鱼老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值