轻松掌握MybatisPlus --学习笔记

什么是MybatisPlus?

一个Mybatis的增强工具,简化单表操作,对Mybatis的功能有很多的增强但不改变,让开发更加的简单,高效

使用MybatisPlus的准备工作:

依赖注入:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

此starter包含对mybatis的自动装配,因完全可以替换掉Mybatis的starter 

定义xxxMapper接口继承BaseMapper接口

示例:

public interface UserMapper extends BaseMapper<User> {
}

然后就可以使用该xxxMapper进行基本的CRUD操作了

BaseMapper中的方法都很简单易懂,一目了然,select就是查询系列,update就是修改,delete就是删除,insert就是增加

示例:

@Test
    void testInsert() { //增
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userMapper.insert(user);
    }

    @Test
    void testSelectById() { //查询
        User user = userMapper.selectById(5L);
        System.out.println("user = " + user);
    }


    @Test
    void testQueryByIds() { //批量查询
        List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L));
        users.forEach(System.out::println);
    }

    @Test
    void testUpdateById() { //修改
        User user = new User();
        user.setId(5L);
        user.setBalance(20000);
        userMapper.updateById(user);
    }

    @Test
    void testDeleteUser() { //删
        userMapper.deleteById(5L);
    }

 MybatisPlus的常见注解:

MybatisPlus是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:

  • MybatisPlus会把PO实体的类名驼峰转下划线作为表名

  • MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型

  • MybatisPlus会把名为id的字段作为主键

但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息:

@TableName

@Data
@TableName("user")  //标注数据库中的表名,未标注默认以类名驼峰转下划线作为默认表名
public class User {

如果po实体类的类名不对应表名,可以使用@TableName注解的value值绑定表名

@TableId 

@TableId(value = "id",type = IdType.AUTO)  //标注数据库中的主键字段名,未标注默认以名为id的字段为主键,IdType.AUTO 类型为自增长,未赋值默认为雪花算法
    private Long id;

如果po实体类中的主键名不为id,可以使用@TableId注解的value值绑定主键,type属性可指定主键的类型,IdType.AUTO为自增,不填默认为雪花算法(即随机生成一串数字),type比较重要的属性值还有INPUT型,意为用户自己输入主键值

@TableField

@TableField("username")
    private String username;    //标注变量对应数据库的字段的字段名,默认是变量名驼峰形式转下划线作为对应字段名

如果po实体的变量名不对应数据库的字段名,可以使用@TableField注解的value值绑数据库的字段

还有俩种特殊情况也需要使用到@TableField注解:

  • 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。

  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加转义字符:``

@TableField注解比较常用的属性还有exist,声明该成员变量是否为数据库字段

当然关于这三个注解还有很多属性没有说明,详细可参考官方文档:注解 | MyBatis-Plus (baomidou.com)

 MybatisPlus的常见配置:

MybatisPlus也支持基于yaml文件的自定义配置,不过大部分配置都有默认值,这里介绍几个比较常用的:

mybatis-plus:
  type-aliases-package: 实体类的别名扫描包(例:com.zeyu.mp.domain.po)
  global-config:
    db-config:
      id-type: auto # 全局id类型(这里设为自增长)
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。

MyBatisPlus也支持手写SQL的,而mapper文件的读取地址可以自己配置,即上面的mapper-locations

其它的配置详见官方文档:使用配置 | MyBatis-Plus (baomidou.com)

条件构造器:

除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件

参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法

而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段

而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:

QueryWrapper

无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件

:查询出名字中带o的,存款大于等于1000元的人。代码如下:

@Test
void testQueryWrapper() {
    // 1.构建查询条件 where name like "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
            .select("id", "username", "info", "balance")//查询"id","username","info","balance"字段
            .like("username", "o")//"username"字段 包含 “o” 即 username like "%o%"
            .ge("balance", 1000);//balance 值 >= 1000
    // 2.查询数据
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}

UpdateWrapper

基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。 例如:更新id为1,2,4的用户的余额,扣200,对应的SQL应该是:

UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)

SET的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper中的setSql功能了:

@Test
    void testUpdateWrapper2(){
        UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
                .setSql("balance = balance - 200")
                        .in("id",List.of(1L,2L,4L));

        userMapper.update(null,wrapper);
    }

LambdaQueryWrapper

无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?

其中一种办法是基于变量的get方法结合反射技术。因此我们只要将条件对应的字段的get方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用Lambda表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:

  • LambdaQueryWrapper

  • LambdaUpdateWrapper

分别对应QueryWrapper和UpdateWrapper

其使用方式如下:

   @Test
    void testQueryWrapper(){
        //构建查询条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                .select(User::getId,User::getUsername,User::getInfo,User::getBalance)   //查询"id","username","info","balance"字段
                        .like(User::getUsername,"o")   //"username"字段 包含 “o” 即 username like "%o%"
                                .ge(User::getBalance,1000);    //balance 值 >= 1000
        //查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

更多的条件构造方法使用详见官方文档:条件构造器 | MyBatis-Plus (baomidou.com)

自定义SQL

@Test
    void testUpdateWrapper2(){
        UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
                .setSql("balance = balance - 200")
                        .in("id",List.of(1L,2L,4L));

        userMapper.update(null,wrapper);
    }

上述代码中,如 setSql("balance = balance - 200")这种写法是不符合开发规范的,因为SQL语句最好都维护在持久层,而不是业务层,所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL

就比如上面这个例子,我们可以换一种写法:

首先业务层去除sql语句部分,只负责传递条件:

    @Test
    void testCustomSqlUpdate(){
        List<Long> ids = List.of(1L,2L,4L); //要操作的用户id组
        int amount = 200;    //要扣除的balance值

        QueryWrapper<User> wrapper = new QueryWrapper<User>()   //条件
                .in("id",ids);

        userMapper.updateBalanceByIds(wrapper,amount);
    }

然后在UserMapper中自定义SQL:

@Update(" update user set balance = balance - #{amount} ${ew.customSqlSegment}")
    void updateBalanceByIds(@Param("ew") QueryWrapper<User> wrapper,@Param("amount") int amount);

这样就省去了编写复杂查询条件的烦恼了

lambdaQuery()和lambdaUpdate()

在对数据库进行操作时,动态构建SQL是一个常见的需求。MyBatis Plus作为一个增强版的MyBatis,提供了强大的条件构造器,尤其是Lambda条件构造器功能,使动态SQL的编写变得既安全又优雅

例,使用lambdaQuery()方法进行条件查询:

// 创建一个LambdaQueryWrapper实例,使用Lambda表达式构建查询条件
LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
            // 如果username不为null,则添加用户名模糊查询条件
            .like(username != null, User::getUsername, username)
            // 如果status不为null,则添加用户状态查询条件
            .eq(status != null, User::getStatus, status)
            // 如果minBalance不为null,则添加查询用户余额大于等于minBalance的条件
            .ge(minBalance != null, User::getBalance, minBalance)
            // 如果maxBalance不为null,则添加查询用户余额小于等于maxBalance的条件
            .le(maxBalance != null, User::getBalance, maxBalance);
// 使用构建好的wrapper作为查询条件,调用list方法获取符合条件的用户列表
List<User> users = userService.list(wrapper);

按照之前的思路就是先创建一个wrapper,然后将wrapper塞到userService.list里面去,其实可以省略下面那步,直接使用Lambda条件构造器完成对数据的查询:

// 使用lambdaQuery()方法开始构建Lambda查询
List<User> users = lambdaQuery()
                    // 如果name不为null,则添加用户名模糊查询条件
                    .like(name != null, User::getUsername, name)
                    // 如果status不为null,则添加用户状态查询条件
                    .eq(status != null, User::getStatus, status)
                    // 如果minBalance不为null,则添加查询用户余额大于minBalance的条件
                    .gt(minBalance != null, User::getBalance, minBalance)
                    // 如果maxBalance不为null,则添加查询用户余额小于maxBalance的条件
                    .lt(maxBalance != null, User::getBalance, maxBalance)
                    // 执行查询,返回满足条件的用户列表
                    .list();

无需再定义wrapper,使用lambdaQuery()直接一步到位,当然,有lambdaQuery()也有lambdaUpdate(),除了各自提供的方法有所区别,使用方法基本一致:

使用lambdaUpdate()方法更新用户的余额和状态:

// 使用lambdaUpdate()方法开始构建Lambda更新
boolean success = lambdaUpdate()
                    // 设置用户的balance属性为remainBalance
                    .set(User::getBalance, remainBalance)
                    // 如果remainBalance为0,设置用户状态为冻结状态
                    .set(remainBalance == 0, User::getStatus, UserStatus.FROZEN)
                    // 增加更新条件,只更新id属性为id的用户
                    .eq(User::getId, id)
                    // 增加更新条件,只更新balance属性为user.getBalance()的用户
                    .eq(User::getBalance, user.getBalance())
                    // 执行更新操作,成功返回true,失败返回false
                    .update();

Service接口

MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。

通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:

save:新增

  • save是新增单个元素
  • saveBatch是批量新增
  • saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
  • saveOrUpdateBatch是批量的新增或修改

remove:删除

  • removeById:根据id删除
  • removeByIds:根据id批量删除
  • removeByMap:根据Map中的键值对为条件删除
  • remove(Wrapper<T>):根据Wrapper条件删除

update:更新

  • updateById:根据id修改
  • update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含setwhere部分
  • update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据
  • updateBatchById:根据id批量修改

get:查询单个结果

  • getById:根据id查询1条数据
  • getOne(Wrapper<T>):根据Wrapper查询1条数据
  • getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper

list:查询集合结果

  • listByIds:根据id批量查询
  • list(Wrapper<T>):根据Wrapper条件查询多条数据
  • list():查询所有

count:计数

  • count():统计所有数量
  • count(Wrapper<T>):统计符合Wrapper条件的数据数量

getBaseMapper():当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法

准备:

自定义Service接口,继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了(ServiceImpl中有

示例:

public interface IUserService extends IService<User> {  //自定义接口继承IService接口
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {   //实现类继承ServiceImpl   
}

然后在controller类中注入对应Service实体类,就可以使用Service接口准备的那些方便的方法了

例:

  @ApiOperation("新增用户接口")
    @PostMapping
    public void saveUser(@RequestBody UserFormDTO userDto){
        //1、把DTO拷贝到PO
        User user = BeanUtil.copyProperties(userDto, User.class);
        //2、新增
        userService.save(user);
    }
 @ApiOperation("删除用户接口")
    @DeleteMapping("{id}")
    public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id){
        userService.removeById(id);
    }

通过Service接口中的方法,再对数据库进行简单的CRUD操作时甚至不需要在逻辑层和持久层编写一句代码,直接在控制层就完成了,优雅!

当然这不代表所有的操作都可以在控制层完成,当需要进行逻辑操作,就需要自定义逻辑层方法了,当需要使用自定义sql时,就需要定义自定义持久层方法了

注:要使用IService中的批量新增功能的话,最好在application.yml中的数据库连接配置里添加&rewriteBatchedStatements=true参数。这个参数对于提高批量插入或更新的性能至关重要,因为它允许JDBC驱动将单次批量请求重写为少数几个请求

示例:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_database?rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: your_username
    password: your_password

saveBatch方法(批量新增方法)允许指定一个batchSize参数,表示每次批量插入的记录数。调整这个大小可以帮助平衡性能和内存使用

插件推荐:MybatisPlus

MybatisPlus

基于图形化界面完成MybatisPlus的代码生成,非常简单

使用:

在Idea顶部菜单中,找到other,选择Config Database

填写数据库连接的基本信息并保存

再次点击Idea顶部菜单中的other,然后选择Code Generator

填写相关信息:

点击提交,代码生成,妙哉

静态工具类:Db

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,结合lambdaQuery()lambdaUpdate()方法,我们可以以更流畅、类型安全的方式进行动态sql操作

循环依赖:

循环依赖是在软件开发中一个模块(可以是类、文件或组件)依赖另一个模块,而被依赖的模块同时也依赖于依赖它的那个模块的情况。这样形成一个闭环,没有一个可以独立于另一个存在,导致了依赖关系的循环,循环依赖通常是不健康的设计,其可能导致多种问题,如无法编译、代码难以理解和维护、难以实施变更等

而为了避免在逻辑层互相的依赖注入,就可以选择使用Db工具类,在不注入相关依赖的情况下执行相关查询

语法:

Db.lambdaQuery(类的字节码对象)
                  .相关查询语句
                  .相关查询语句
                  ......

举例:

查询用户信息,而用户的地址信息封装在另一个address表中,用外键user_id关联主表的id主键,此时要连同用户信息和用户地址信息一起返回的话,就需要在查询用户信息的同时,去查询address表中符合address.user_id == user.id的数据,并封装到用户信息中一同返回:

逻辑层代码:

@Override
    public UserVO queryUserAndAddressById(Long id) {
        //1、查询用户
        User user = getById(id);
        if(user == null || user.getStatus() == UserStatus.FROZEN){
            throw new RuntimeException("用户状态异常!");
        }
        //2、查询地址
        List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();
        //3、封装VO
        //转User的po为vo
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        //转地址vo
        if(CollUtil.isNotEmpty(addresses)){
            userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }

如此,就在不注入相关依赖的情况下完成了对其它表的访问查询操作,避免了循环依赖的问题

逻辑删除

逻辑删除,顾名思义,只做逻辑上的删除,不做物理上的删除

逻辑删除,实际上是在数据库中保留被删除记录的数据,通过一个字段来标记该记录已被“删除”。在查询操作时,通常会忽略这些标记为已删除的记录,使其在应用层面上"看不见",但在物理存储层面上,这些记录仍然存在

既然逻辑上已经不存在,那么对标记为删除的数据肯定也无法做相应的查询删除等操作

假设标记字段为delete,那么难道每次查询或修改的时候都要在where条件最后加上一个 and delete = 0 吗?

当然不,为了解决这个问题,MybatisPlus添加了对逻辑删除的支持

只需要在配置文件中加上以下配置,MybatisPlus就会帮我们做好相关的逻辑删除操作

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。 

测试:

@Test
    void testLogicDelete(){
        //1、删除
        addressService.removeById(59L);
        
        //2、查询
        Address address = addressService.getById(59L);
        System.out.println("address = " + address);
    }

可以看到,MybatisPlus自动就帮忙加上了AND deleted = 0,👍

通用枚举

在定义各种各样的类时,难免会遇到一些表示状态的字段,比如1表示正常,2表示冻结......之类的,这个时候可以选择在相应实体类中将其类型定义为enum(枚举)型,更加便于代码的可维护性、可读性

就拿刚才说的表示正常,2表示冻结举例,可以定义这么一个枚举类来表示相关字段:

@Getter
public enum UserStatus {
    NORMAL(1,"正常"),
    FROZEN(2,"冻结"),
    ;
    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

但是数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举Integer转换,非常麻烦

此时就可以通过在配置文件中配置枚举处理器,并且通过@EnumValue指定对应状态码的相应字段,就可以达到枚举类与数据库的int类型相互转换的效果,并且还可以通过@JsonValue字段指定枚举类的返回值

配置枚举处理器:

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

添加注解:

@Getter
public enum UserStatus {
    NORMAL(1,"正常"),
    FROZEN(2,"冻结"),
    ;
    @EnumValue  //与数据库对应值关联的枚举类成员变量
    private final int value;
    @JsonValue  //指定枚举类返回值(默认为枚举项名字,NORMAL、FROZEN)
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

测试:

 JSON类型处理器

数据库存储的JSON类型数据都是字符串,在Java中,一般用实体类或集合存储JSON类型数据,那么从数据库读取JSON类型数据或将JSON类型写入数据库的时候就需要进行一个转换操作,将字符串与相应实体类类型互相转换,为了处理这种情况,MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器

使用方法非常简单,直接使用MybatisPlus常用注解中的@TableField注解,配置其中的typeHandler = JacksonTypeHandler.class,然后将注解添加到相应的字段之上即可

然后MybatisPlus就会自动进行相关转换了:

分页插件

在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效。 所以,我们必须配置分页插件

配置步骤

1、在项目中新建一个配置类

 2、编写配置类:

package com.itheima.mp.config;

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

@Configuration
public class MyBatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //1、创建分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);   //数据库类型
        paginationInnerInterceptor.setMaxLimit(1000L);  //查询最大上限
        //2、添加分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}

一旦完成了上述的分页插件配置后,我们就可以在MyBatis Plus项目中轻松实现分页查询功能。接下来,我将展示如何在实际应用中使用这个分页插件

首先,请确保你已经将上述分页插件配置成功集成到你的项目中

在Service层使用分页查询

MyBatis Plus提供了Page<>类作为分页查询的返回类型,这使得分页查询变得极其简单。以下是一个使用分页插件进行分页查询的示例:

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserService extends IService<User> {
    default IPage<User> findUserByPage(int currentPage, int pageSize) {
        // 创建Page对象
        Page<User> page = new Page<>(currentPage, pageSize);
        // 调用MyBatis Plus提供的分页查询方法
        return this.page(page);
    }
}

在上面的例子中,UserService是扩展自IService<User>的一个接口,在其中我们定义了一个findUserByPage方法,有两个参数:currentPage(当前页)和pageSize(页大小)。在方法中,我们首先创建了一个Page<User>对象,并将其作为参数传递给page方法,后者是IService接口中定义的一个分页查询方法

添加排序条件的方法

使用Page对象的addOrder方法可以添加排序条件。以下是如何在分页查询中添加排序条件的示例代码:

        // 添加排序条件,例如按照用户ID降序排列
        page.addOrder(OrderItem.desc("user_id"));
        // 也可以添加多个排序条件
        // page.addOrder(OrderItem.desc("another_column"));

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

A泽予

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

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

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

打赏作者

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

抵扣说明:

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

余额充值