MybatisPlus(2)

4 条件构造器

警告:
不支持以及不赞成在 RPC 调用中把 Wrapper 进行传输

  1. wrapper 很重
  2. 传输 wrapper 可以类比为你的 controller 用 map 接收值(开发一时爽,维护火葬场)
  3. 正确的 RPC 调用姿势是写一个 DTO 进行传输,被调用方再根据 DTO 执行相应的操作

4.1 wrapper介绍

在这里插入图片描述
带Query字样的是查询用,带Update字样的是update用,带Lambda字样的是可以用Lambda语法的类。

4.2 wrapper使用样例

条件构造器所有使用参见官方文档:https://baomidou.com/pages/10c804/

	@Autowired
    private UserMapper userMapper;

    @Test
    void test1() {//like()、between()、isNotNull()
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //查询name包含a,年龄20-30,邮箱不为null的users
        wrapper.like("user_name", "a").between("age", 20, 30).isNotNull("email");
        //==>  Preparing: SELECT user_id,user_name AS name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (user_name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
        //==> Parameters: %a%(String), 20(Integer), 30(Integer)
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    @Test
    void test2() {//orderByDesc()、orderByAsc()
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //查询,按照年龄降序,年龄相同按照id升序。
        wrapper.orderByDesc("age").orderByAsc("user_id");
        //==>  Preparing: SELECT user_id,user_name AS name,age,email,is_deleted FROM user WHERE is_deleted=0 ORDER BY age DESC,user_id ASC
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    @Test
    void test3() {//delete
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //删除邮箱为null的users
        wrapper.isNull("email");
        //==>  Preparing: UPDATE user SET is_deleted=1 WHERE is_deleted=0 AND (email IS NULL)
        userMapper.delete(wrapper);
    }

    @Test
    void test4() {//or()、gt()
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //将(age>20且user_name包含a)或者(email不为null)的用户修改
        wrapper.gt("age", 20).like("user_name", "a")
                .or()
                .isNotNull("email");
        //修改为name=xiaoming
        User user = new User();
        user.setName("xiaoming");
        //==>  Preparing: UPDATE user SET user_name=? WHERE is_deleted=0 AND (age > ? AND user_name LIKE ? OR email IS NOT NULL)
        //==> Parameters: xiaoming(String), 20(Integer), %a%(String)
        userMapper.update(user, wrapper);//(修改值实体类,被修改对象wrapper)
    }

    @Test
    void test5() {//查询条件的优先级、and()
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //注意和test4的情况区分,lambda表达式的条件优先执行
        //将age>20且(user_name包含a或者email不为null)的用户查找
        wrapper.gt("age", 20)
                .and(i -> i.like("name", "a")
                        .or()
                        .isNotNull("email")
                );
        //==> Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (age > ? AND (name LIKE ? OR email IS NOT NULL))
        //==> Parameters: 20(Integer), %a%(String)
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    @Test
    void test6() {//部分查询
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //部分查询,只查id和name
        wrapper.select("id", "name");
        //==>  Preparing: SELECT id,name FROM user WHERE is_deleted=0
        List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
        System.out.println(maps);
    }

    @Test
    void test7() {//UpdateWrapper<>()使用、ge()
        UpdateWrapper<User> wrapper = new UpdateWrapper<>();

        //修改对象:age>20
        wrapper.ge("age", 20);
        //修改值:name=zhangsan
        wrapper.set("name", "zhangsan");
        //==>  Preparing: UPDATE user SET name=? WHERE is_deleted=0 AND (age >= ?)
        //==> Parameters: zhangsan(String), 20(Integer)
        userMapper.update(null, wrapper);
    }

    @Test
    void test08() {//根据条件是否符合要求,组装条件进行查询、le()
        String name = "";
        Integer ageMin = 20;
        Integer ageMax = 30;
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        //StringUtils.isNotBlank(name):name不为null和空
        wrapper.like(StringUtils.isNotBlank(name), "name", name).ge(ageMin != null, "age", ageMin).le(ageMax != null, "age", ageMax);
        //==> Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
        //==> Parameters: 20(Integer), 30(Integer)
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);

    }


    @Test
    void test09() {//LambdaQueryWrapper<>()
        String name = "";
        Integer ageMin = 20;
        Integer ageMax = 30;
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        //StringUtils.isNotBlank(name):name不为null和空
        wrapper.like(StringUtils.isNotBlank(name), User::getName, name)
                //第二个参数非lambda情况下是要写数据库字段名的,但有可能忘记字段名
                //lambda情况下直接写属性名,让它自己去映射,博主感觉很nice,不用再记属性-字段怎么对应的
                .ge(ageMin != null, User::getAge, ageMin)
                .le(ageMax != null, User::getAge, ageMax);
        //==> Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
        //==> Parameters: 20(Integer), 30(Integer)
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

    @Test
    void test10() {//LambdaUpdateWrapper<>()
        LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
        wrapper.eq(User::getId, 1);
        wrapper.set(User::getEmail, "update@qq.com");
        //==> Preparing: UPDATE user SET email=? WHERE is_deleted=0 AND (id = ?)
        //==> Parameters: update@qq.com(String), 1(Integer)
        userMapper.update(null, wrapper);
    }

5 mybatisplus插件:分页、乐观锁

5.1 分页插件

首先配置分页插件,顺便把@MapperScan也从启动类上拿过来:

package com.coderhao.mybatisplus.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.coderhao.mybatisplus.mapper")
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return mybatisPlusInterceptor;
    }
}

使用:

	@Test
    void test1(){
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        String name="a";
        wrapper.like(StringUtils.isNotBlank(name),User::getName,name);
        Page<User> page = new Page<>(2,3);//第2页,每页显示3条数据
        //==>  Preparing: SELECT COUNT(*) FROM user WHERE is_deleted = 0 AND (name LIKE ?)
        //==> Parameters: %a%(String)
        //==>  Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (name LIKE ?) LIMIT ?,?
        //==> Parameters: %a%(String), 3(Long), 3(Long)
        //分页数据会回显到page里
        userMapper.selectPage(page, wrapper);
        page.getRecords().forEach(System.out::println);
    }

5.2 乐观锁

作用:防止出现第一类和第二类更新丢失。(更新丢失:两个事务操作同一个数据,后提交/回滚的事务覆盖了先提交的事务。)
乐观锁使用version字段来解决该问题,提交/回滚时检查数据版本,如果更改时数据版本不是一开始取出时的版本,则该操作不生效。
悲观锁则是事务阻塞,一个事务在执行,另一个事务则不能执行。
在这里插入图片描述
注意:这里有一个坑!一定要先查询出这条数据,再更新,乐观锁才会生效!!!
在这里插入图片描述
更新丢失可以通过开两个线程,2个线程都对同一数据进行操作,看结果是否符合预期(比如一个线程把一个数据+1,循环500次,另一个-1,循环500次,看结果是不是不变)。

(1)乐观锁配置

mybatisplus的配置类里填上乐观锁组件:

	@Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());//添加乐观锁组件
        return mybatisPlusInterceptor;
    }

(2)创建数据库表、实体类、mapper

表(因为我们这个emp_salary字段是个金融数字,所以用精确的decimal类型):

CREATE TABLE `emp` (
  `emp_id` bigint NOT NULL AUTO_INCREMENT COMMENT '员工id',
  `emp_salary` decimal(10,0) DEFAULT NULL COMMENT '员工工资',
  `emp_version` bigint DEFAULT '0' COMMENT '乐观锁版本号',
  `is_deleted` int DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`emp_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3

填两个数据:
在这里插入图片描述
实体类,注意 @Version注解和BigDecimal类型:

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class Emp {
    @TableId(value = "emp_id",type = IdType.AUTO)
    private Long empId;
    @TableField("emp_salary")
    private BigDecimal empSalary;
    @Version//乐观锁需要的版本号
    private Long empVersion;
    @TableLogic
    private Integer isDeleted;
}

EmpMapper:

public interface EmpMapper extends BaseMapper<Emp> {
}

(3)测试

注意BigDecimal类型的运算方式

@Test
    void test2(){
        LambdaQueryWrapper<Emp> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Emp::getEmpId,1L);
        Emp emp = empMapper.selectOne(wrapper);
        emp.setEmpSalary(emp.getEmpSalary().add(BigDecimal.valueOf(1L)));
        //==>  Preparing: UPDATE emp SET emp_salary=?, emp_version=? WHERE is_deleted=0 AND (emp_id = ? AND emp_version = ?)
        //==> Parameters: 5001(BigDecimal), 1(Long), 1(Long), 0(Long)
        empMapper.update(emp,wrapper);
    }

发现版本号变了,而且更新过程中sql语句也有判断版本号的情况。
在这里插入图片描述

@Version说明:
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
整数类型下 newVersion = oldVersion + 1
newVersion 会回写到 entity 中
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper) 方法下, wrapper 不能复用!!!

6 通用枚举:@EnumValue+枚举包扫描

数据库表中有很多字段的值是固定的几个里面选一个:比如性别(男女选一)。
这里还是以emp表为例,加个emp_sex:
在这里插入图片描述
搞个枚举类,注意@EnumValue,这个标注的是真正往数据库里放的值:

package com.coderhao.mybatisplus.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum SexEnum {
    //设置枚举的内容
    MALE(1,"男"),
    FEMALE(0,"女");
    //枚举的属性
    @EnumValue
    private Integer sex;
    private String sexName;
}

Emp,加个枚举类型的属性:

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class Emp {
    @TableId(value = "emp_id",type = IdType.AUTO)
    private Long empId;
    @TableField("emp_salary")
    private BigDecimal empSalary;
    @Version//乐观锁需要的版本号
    private Long empVersion;
    @TableLogic
    private Integer isDeleted;
    @TableField("emp_sex")
    private SexEnum empSex;
}

yaml里面枚举包扫描:

mybatis-plus:
  # 扫描通用枚举包
  type-enums-package: com.coderhao.mybatisplus.enums

测试:

	@Test
    void test3(){
        Emp emp = new Emp(null,BigDecimal.valueOf(4000L),null,null, SexEnum.MALE);
        empMapper.insert(emp);
    }

有数据,枚举类型填了相应的值,而且其他字段也有默认值。
在这里插入图片描述

7 代码生成器

mybatisplus的代码生成器可以由表生成entity,controller,service,mapper,mapper.xml
虽然这里讲代码生成器,但博主不建议用代码生成器,MP的功能已经很强大了,没必要代码生成,何况生成的controller,service层可能也不满意,实体类也缺逻辑删除和乐观锁的注解。

(1)创建案例表

CREATE TABLE `pre_table_name_one` (
  `t_id` bigint NOT NULL AUTO_INCREMENT,
  `t_name` varchar(30) DEFAULT NULL,
  `t_date` datetime DEFAULT NULL,
  `t_salary` decimal(10,0) DEFAULT NULL,
  `t_is_deleted` int DEFAULT '0',
  `t_version` bigint DEFAULT '0',
  PRIMARY KEY (`t_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3

搞两个数据:
在这里插入图片描述

(2)引入依赖,配置代码生成器

		<!--代码生成器依赖包-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--模板引擎-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>
        <!--生成的controller层要这个包依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

代码生成器配置:

@SpringBootTest
public class MPG {
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;

    @Test
    void test1() {

        FastAutoGenerator.create(url, username, password)
                //全局配置
                .globalConfig(builder -> {
                    builder.author("coderhao") // 设置作者
//                            .enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("F://project//learn//boot-mybatisplus//src//main//java//"); // 指定输出目录
                })
                //包配置
                .packageConfig(builder -> {
                    builder.parent("com.coderhao") // 设置父包名
                            .moduleName("mybatisplus") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "F://project//learn//boot-mybatisplus//src//main//resources//mapper//tableName//")); // 设置mapperXml生成路径
                })
                //策略配置
                .strategyConfig(builder -> {
                    builder.addInclude("pre_table_name_one") // 设置需要生成的表名
                            .addTablePrefix("pre_", "c_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}
  • java代码最终路径是:
    outputDir+parent+moduleName,在这个路径下面是controller/serivce/entity/mapper等包
  • mapper.xml路径:
    pathInfo那个里面的路径

注:可以看到生成的东西:
在这里插入图片描述
其中,实体类没有逻辑删除和乐观锁的注解。

(3)测试

有个小bug,实体类中用到了LocalDateTime类型,查询时数据转换为实体类时报错,把druid的版本弄成1.1.22就行了。

	@Autowired
    private ITableNameOneService tableNameOneService;
    @Autowired
    private TableNameOneMapper tableNameOneMapper;
    
	@Test
    void test2() {
        List<TableNameOne> list = tableNameOneService.list();
        list.forEach(System.out::println);
    }

    @Test
    void test3() {
        TableNameOne tableNameOne = new TableNameOne();
        tableNameOne.settName("name3");
        tableNameOneMapper.insert(tableNameOne);
    }

8 多数据源管理

(1)创建案例库/表

数据表很可能分布在多个数据库中,就需要多数据源管理。
读写分离就是在保持主从库的数据一致性的情况下,主库写,从库读。
创建一个新库叫testdb2,创建一个同样的数据表emp,加两条数据:

CREATE TABLE `emp` (
  `emp_id` bigint NOT NULL AUTO_INCREMENT COMMENT '员工id',
  `emp_salary` decimal(10,0) DEFAULT NULL COMMENT '员工工资',
  `emp_version` bigint DEFAULT '0' COMMENT '乐观锁版本号',
  `is_deleted` int DEFAULT '0' COMMENT '逻辑删除',
  `emp_sex` int DEFAULT '0' COMMENT '性别,只有0/1',
  PRIMARY KEY (`emp_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3

在这里插入图片描述

(2)引入依赖,配置yaml

		<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>

yaml数据源改成这样:

spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://localhost:3306/testdb2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver
        #以上会配置一个默认库master,一个组slave下有1个子库slave_1

(3)@DS切换数据库

package com.coderhao.mybatisplus.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.coderhao.mybatisplus.mapper.EmpMapper;
import com.coderhao.mybatisplus.pojo.Emp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class MDSimpl {
    @Autowired
    private EmpMapper empMapper;

    //添加@DS注解到实现类或者实现类的方法上才可以,同时存在就近原则 方法上注解 优先于 类上注解,测试类发现不太行,不知道为啥
    //不加@DS就是默认数据库
    @DS("master")
    public void f1() {
        List<Emp> emps = empMapper.selectList(null);
        emps.forEach(System.out::println);
    }

	//@DS("slave_1")
    @DS("slave")//组名或者具体库名都行
    public void f2() {
        List<Emp> emps = empMapper.selectList(null);
        emps.forEach(System.out::println);
    }
}

(4)测试

	@Autowired
    private MDSimpl mdSimpl;

    @Test
    void test1(){
        mdSimpl.f1();
        mdSimpl.f2();
    }

9 MybatisX插件

idea平台独有的插件,可以解决一些复杂sql、多表联查问题。
这个插件的功能博主写这篇文章的时候觉得还行。

9.1 安装插件

在这里插入图片描述
安装后重启idea。

9.2 功能描述

(1)xml与mapper的跳转。

点击小鸟实现跳转。
在这里插入图片描述
在这里插入图片描述

(2)代码生成器

  1. idea连上数据库:
    在这里插入图片描述
    在这里插入图片描述
  2. 代码生成器配置
    先把之前MPG代码生成的删除,重新用这个生成。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    生成的代码文件,没有controller层:
    在这里插入图片描述
  3. 补充实体类注解
package com.coderhao.mybatisplus.pojo;

import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;

@TableName(value ="pre_table_name_one")
@Data
public class TableNameOne implements Serializable {
    @TableId(value = "t_id",type = IdType.AUTO)
    private Long id;

    @TableField("t_name")
    private String name;

    @TableField("t_date")
    private Date date;

    @TableField("t_salary")
    private Integer salary;

    @TableField("t_is_deleted")
    @TableLogic
    private Integer isDeleted;
    
    @TableField("t_version")
    @Version
    private Long version;

    @TableField(exist = false)
    //博主推测这个设置代替数据库中的版本字段设置初始值
    private static final long serialVersionUID = 1L;
}
  1. 测试
	@Autowired
    private TableNameOneService tableNameOneService;
    @Autowired
    private TableNameOneMapper tableNameOneMapper;
    
	@Test
    void test2() {
        List<TableNameOne> list = tableNameOneService.list();
        list.forEach(System.out::println);
    }

    @Test
    void test3() {
        TableNameOne tableNameOne = new TableNameOne();
        tableNameOne.setName("name4");
        tableNameOneMapper.insert(tableNameOne);
    }

    @Test
    void test4(){
        tableNameOneMapper.deleteById(1);
        test2();
    }

    @Test
    void test5(){
        LambdaQueryWrapper<TableNameOne> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(TableNameOne::getId,2);
        TableNameOne tableNameOne = tableNameOneMapper.selectOne(wrapper);
        tableNameOne.setSalary(4444);
        tableNameOneMapper.update(tableNameOne,wrapper);
    }

(3)快速生成crud

可以根据mapper的方法快速生成crud。
这里用选择性的条件添加来做例子。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到方法巨多,而且都挺常用的,生成一些复杂的也不在话下,经典前面白学:
在这里插入图片描述
增删改查例子,根据提示来写,易上手,太强了:

public interface TableNameOneMapper extends BaseMapper<TableNameOne> {

    //添加
    int insertSelective(TableNameOne tableNameOne);
    //按照id在区间内查找
    List<TableNameOne> selectAllByIdBetween(@Param("beginId") Long beginId, @Param("endId") Long endId);
    //更新name,salary(按照salary在区间内和时间等于某值)
    int updateNameAndSalaryBySalaryBetweenAndDate(@Param("name") String name, @Param("salary") Integer salary, @Param("beginSalary") Integer beginSalary, @Param("endSalary") Integer endSalary, @Param("date") Date date);
    //删除从某个时候之后的
    int deleteByDateAfter(@Param("date") Date date);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值