java5.1MyBatis

本文详细介绍了MyBatis框架的使用,包括环境搭建、核心配置文件解析、增删改查操作、结果集映射、日志工厂、注解开发、Lombok集成以及动态SQL和缓存机制。内容涵盖了从创建项目到实现复杂查询的全过程,旨在帮助开发者深入理解并熟练运用MyBatis。
摘要由CSDN通过智能技术生成

概述

MyBatis 是一个 Maven 项目

  • 是基于Java的持久层框架
  • 简单化 SQL,可定制
  • 避免几乎所有的 JDBC
  • 可使用简单的 XML、注解、接口为数据库中的记录

安装依赖

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
 <groupId>org.mybatis</groupId>
 <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
   </dependency>

gitHab : https://github.com/mybatis/mybatis-3

中文文档 : https://mybatis.org/mybatis-3/zh/index.html

特点:解除sql与程序代码的耦合(sql写在xml配置文件中)

一.创建MyBatis程序

思路:搭建环境 > 导入MyBatis > 编程 > 测试

1.1 搭建环境

(1)创建数据库
在这里插入图片描述

(2)导包和配置

导入依赖

<dependencies>
    <!-- 数据库驱动:https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.27</version>
    </dependency>
    <!-- myBatis:https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    <!-- junit:https://mvnrepository.com/artifact/junit/junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
    </dependency>
</dependencies>

注:需要配置xml文件打包

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*</include>
            </includes>
        </resource>
    </resources>
</build>
1.2 创建模块

(1)编写核心配置文件

文件名 mybatis-config.xml (用于连接数据库)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--configuration 核心配置文件-->
<configuration>
    <environments default="development">
        <!--environment 环境-->
        <environment id="development">
            <!--transactionManager 事务管理-->
            <transactionManager type="JDBC"/>
            <!--dataSource 数据源-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123"/>
            </dataSource>
        </environment>
    </environments>
    <!--映射器:每一个 mapper.xml 要在 Mybatis 核心配置文件中注册-->
    <!--resource路径必须使用'/',而不能使用'.'-->
    <mappers>
        <mapper resource="com/zheng/dao/UserMapper.xml"/>
    </mappers>
</configuration>

(2)编写工具类

public class MyBatis {
    //==== 1.构建 sqlSessionFactory 对象
    private static SqlSessionFactory sqlSessionFactory;
    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //==== 2.从 SqlSessionFactory 中获取 SqlSession
    public static SqlSession getSqlSession(){
        return sqlSessionFactory.openSession();//输入参数true则每次事务完成自动提交
    }
}
2.3 编程

(1)实体类

(属性私有,get/set)略…

(2)Mapper接口

public interface UserMapper {
    List<User> getUserList();
}

(3)Mapper配置文件

映射的sql语句(在一个 XML 映射文件中,可以定义无数个映射语句)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace属性:绑定一个对应的Mapper接口-->
<mapper namespace="com.zheng.dao.UserMapper">
    <!--id:对应方法名 resultType:返回类型(对应方法的泛型)-->
    <select id="getUserList" resultType="com.zheng.pojo.User">
        select * from mybatis.user
    </select>
</mapper>

2.4 测试

public class UserDaoTest {
    @Test
    public void test(){
        //1.获取sqlSession对象
        SqlSession sqlSession = MyBatis.getSqlSession();
        try{
            //2.执行SQL
            //获取 UserMapper 接口对象
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            List<User> userList = mapper.getUserList();
            for (User user : userList) {
                System.out.println(user);
            }
        }
        finally {
            //3.关闭SqlSession
            sqlSession.close();
        }
    }
}

在这里插入图片描述

2.5 补充

(1)作用域问题

SqlSessionFactoryBuilder 工厂创建者

这个类用于获取sqlSessionFactory,之后没有作用。可当做局部变量,需要时才创建。

sqlSessionFactory 数据库连接池

一旦被创建就一直存在,不能丢弃和重建。最佳作用域是应用作用域。

SqlSession

每个线程都有自己的 SqlSession 实例,不能共享。最佳作用域是方法作用域,请求时打开一个sqlSession,返回响应后必须关闭

不能将其放在静态域或实例变量。

注:【SqlSessionFactoryBuilder】 是工厂创建者,创建完成后便不再需要。【sqlSessionFactory】 是数据库连接池,全局只需要一个连接池即可,所有作用域应该是最高,并且中途不能删除或新建。【SqlSession】 是线程池中取出的一个实例,每次连接完成后需要关闭,才能将分出去的连接还到连接池中,避免占用。

(2)项目结构

在这里插入图片描述

二.增删改查

3.1 select

【属性】

  • id:对应 namespace 中的方法名;

  • resultType:sql语句执行的返回值类型;

  • parameterType:参数类型;

【例】

//在接口中添加此方法
//根据id查询用户
User getUserById(int id);
<!--在对应的配置文件中添加对应的查询语句,参数id对应接口方法中的形参-->
<select id="getUserById" resultType="com.zheng.pojo.User" parameterType="int">
    select * from mybatis.user where id=#{id}
</select>
3.2 增删改

注:增删改执行结束需要提交事务,sqlSession.commit();

@Test
public void test04(){
    SqlSession sqlSession = MyBatis.getSqlSession();
    try{
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        int i = mapper.updataUser(new User(111, "李某迪", "123321"));
        System.out.println(i>0?"成功":"失败");
        sqlSession.commit();//提交事务
    }
    finally {
        sqlSession.close();
    }
}

【例】

//插入数据
int addUser(User u);
//修改数据
int updataUser(User u);
//删除数据
int deletetUser(int id);
<insert id="addUser" parameterType="com.zheng.pojo.User">
    insert into user values(#{id},#{name},#{pass})
</insert>
<update id="updataUser" parameterType="com.zheng.pojo.User">
    update user set pass=#{pass} where id=#{id}
</update>
<delete id="deletetUser" parameterType="int">
    delete from user where id=#{id}
</delete>
3.3 拓展

(1)使用map代替实体类

当实体类对象较大时,为了方便传递多个参数,可用map。一般用于条件查询

接口和配置

//万能map
int addMapUser(Map<String,Object> map);
<!--map传值可直接取出key作为参数-->
<insert id="addMapUser" parameterType="map">
    insert into user values(#{uid},#{uname},#{upass})
</insert>

测试类

SqlSession sqlSession = MyBatis.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<String, Object>();
map.put("uid",123);
map.put("uname","李某迪");
map.put("upass","123456");
mapper.addMapUser(map);
sqlSession.commit();
sqlSession.close();

(2)模糊查询

<!--方法一-->
<select id="getUserLike" resultType="com.zheng.pojo.User">
    select * from user where name like #{value}
</select>
<!--方法二:like #{value}"%",但需要放在SQL注入-->
SqlSession sqlSession = MyBatis.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userLike = mapper.getUserLike("李%");
//使用方法二只需要传mapper.getUserLike("李");
for (User user : userLike) {
    System.out.println(user);
}
sqlSession.close();

(3)Limit实现分页

核心是SQL实现,map实现传参

List<User> getUserLimit(Map<String,Integer> map);
<!-- 从startIndex开始,查询pageSize个 -->
<select id="getUserLimit" parameterType="map" resultType="user">
    select * from user limit #{startIndex},#{pageSize}
</select>
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("startIndex",2);
map.put("pageSize",2);
List<User> userLimit = mapper.getUserLimit(map);
3.4 结果集映射ResultMap

【问题】:解决属性名和字段名不一致的问题

例如:当数据库表字段分别为 id、name、passworld,而实体类为 id、name、pass

查询结果如下:

在这里插入图片描述

解决方法:

<!--结果集映射:id=对应的sql字段 type=对应的实体类-->
<resultMap id="UserMap" type="user">
    <!--column(列)=数据库中字段 property(属性)=实体类中字段-->
    <result column="id" property="id"></result>
    <result column="name" property="name"></result>
    <result column="passworld" property="pass"></result>
</resultMap>
<select id="getUserList" resultType="user" resultMap="UserMap">
    select * from mybatis.user
</select>

四.配置解析

4.0 配置文件

MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。

配置文档的顶层结构如下

configuration(核心配置)

  • properties(属性)
  • settings(设置)
  • typeAliases(类型别名)
  • environments(环境配置)
    • environment(环境变量)
      • transactionManager(事务管理器)
      • dataSource(数据源)
  • mappers(映射器)
  • …(其余了解即可)
4.1 环境配置

(1)多道环境

【说明】MyBatis 可以配置多种环境,即可以有多个environment。例如,开发、测试和生产环境。

但每个 SqlSessionFactory 实例只能选择一种环境

【使用】通过environments的default属性进行切换,每一个环境对应一个id

(2)事务管理器 transactionManager

type=“[JDBC|MANAGED]” 默认:JDBC

(3)数据源 dataSource:配置 JDBC 连接对象的资源

type=“[UNPOOLED|POOLED|JNDI]” 默认:POOLED

4.2 属性优化

通过 properties 标签实现引用配置文件

【外部文件 db.properties】

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false
uname=root
upass=123
<!--通过该标签导入外部文件。使用格式:${url} -->
<properties resource="db.properties"></properties>
<!--properties标签中可使用多个propertie,与外部文件属性同意,且外部文件优先级高-->
4.3 别名优化
<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
</typeAliases>

如上配置,使得在任何地方都可以使用别名 Author 代替 domain.blog.Author

也可以使用 package 指定一个包名

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

如上配置,直接使用该包下类名作为该类地址别名,一般不可指定别名名称

思考:如果两个包中类名相同怎么办

@Alias("author")
public class Author {...}

使用注解为实体类起别名

常见的 Java 类型内建的类型别名

别名映射
_intint
intinteger
mapMap
4.4 设置
设置名描述有效值默认值
cacheEnabled全局性地开启或关闭所有映射器配置文件中已配置的任何缓存true\falsetrue
lazyLoadingEnabled开启时,任一方法的调用都会加载该对象的所有延迟加载属性;否则,每个延迟加载属性会按需加载。懒加载true\falsefalse
logImpl指定 MyBatis 所用日志的具体实现,未指定时将自动查找。LOG4J\STDOUT_LOGGING-
<!--设置例子-->
<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
</settings>
4.5 映射器

【常用写法】

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>

即一个 mapper 标签对应一个 mapper 文件

【了解】

<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
<!-- 将包内的映射器接口实现【全部】注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

以上两种方法适用于一定的条件,但有以下局限

  • 接口和绑定接口的Mapper文件 必须同名
  • 接口和绑定接口的Mapper文件 必须在同一目录下

五.日志工厂

用于排错,代替原来的debug

配置文件>设置>logImpl

  • LOG4J
  • STDOUT_LOGGING(标准日志输出)

具体使用哪一个日志实现,在设置中设定

5.1 STDOUT_LOGGING(标准)

<!--核心配置文件:不用导包直接使用-->
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

在这里插入图片描述

5.2 LOG4J

链接:Log4j入门教程

1.配置和导包

<settings>
    <setting name="logImpl" value="LOG4J"/>
</settings>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2.创建一个配置文件 log4j.properties

#将等级分为DEBUG的日志信息输出到console和file这两个目的地
log4j.rootLogger=DEBUG,console,file

#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold = DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = [%c]-%m%n

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File = ./log/myLog.log
log4j.appender.file.MaxFileSize = 10mb
log4j.appender.file.Threshold = DEBUG
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = [%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis = DEBUG
log4j.logger.java.sql = DEBUG
log4j.logger.java.sql.Statement = DEBUG
log4j.logger.java.sql.ResultSet = DEBUG
log4j.logger.java.sql.PreparedStatement = DEBUG

3.log4j的使用

在这里插入图片描述
4.自定义日志信息
// Logger所在包:import org.apache.log4j.Logger;
static Logger logger = Logger.getLogger(UserTest.class);
在这里插入图片描述

六.注解开发

6.1 使用java注解开发

<!--更改映射-->
<mappers>
    <mapper class="zheng.dao.UserMapper"></mapper>
</mappers>
<!--直接在注解中写入SQL-->
package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}

注1:本质就是使用反射获取类的信息(参数及返回值类型等)

注2:使用注解来映射语句会使代码显得更加简洁,但只能用于简单语句(做不到结果集映射)

6.2 @Param() 注解 【注】

多个基本类型的参数或者String类型参数,需要加上该注解

@Select("select * from user where id>#{to} and id<#{from}")
List<User> getMyUser(@Param("to")int to,@Param("from")int from);
List<User> users = mapper.getMyUser(101,105);//调用

七.Lombok

【说明】Lombox是一款java插件,可以使用注解简化代码,常用于实体类(POJO)简化get/set/构造器等的配置
1)IDEA中安装插件
在这里插入图片描述

2)在项目中导入jar包

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>

3)参数配置

@Data

  • 无参构造器
  • get \ set
  • equals \ hashCode \ toString

@AllArgsConstructor

@NoArgsConstructor

  • 显示有参\无参构造(显示定义无参会消失,需要手动添加)

在这里插入图片描述

八.复杂查询

在这里插入图片描述

8.1 多对一

//学生实体类:student
public class Student {
    private int id;
    private String name;
    //学生需要关联老师
    private Teacher teacher;
}

(1)按查询进行嵌套

public interface StuMapper {
    //查询学生信息和对应老师信息
    List<Student> getAllUser();
}
<!--子查询:使用 resultMap 作为中间值-->
<mapper namespace="com.zheng.mapper.StuMapper">
    <select id="getAllUser" resultMap="ST">
        select * from student
    </select>
    <resultMap id="ST" type="Student">
        <!--复杂对象需要单独处理:对象:association;集合:collection-->
        <!--column=数据库列 property=实体类属性 javaType=实体类对象 select=子查询-->
        <association column="Tid" property="teacher" javaType="Teacher" select="getTea"></association>
    </resultMap>
    <select id="getTea" resultType="Teacher">
        select * from teacher where id=#{id}
    </select>
</mapper>

(2)按结果嵌套处理√

<!--联表查询【推荐使用】-->
<select id="getAllUser2" resultMap="ST2">
    select s.id,s.name sname,t.name tname
    from student s,teacher t
    where s.Tid=t.id
</select>
<resultMap id="ST2" type="Student">
    <result column="sname" property="name"></result>
    <association property="teacher" javaType="Teacher">
        <result column="tname" property="name"></result>
    </association>
</resultMap>

在这里插入图片描述

8.2 一对多

按结果嵌套处理

//学生实体类
public class Student {
    private int id;
    private String name;
    private int tid;
}
//教师实体类:添加一个学生的集合
public class Teacher {
    private int id;
    private String name;
    private List<Student> students;
}
/*注:当使用一对多时,8.1在Student实体类中使用的关联应该去除*/
public interface TeaMapper {
    //查询指定教师的所有学生集合
    Teacher getAllUser(@Param("tid") int id);
}
<!--两个表有相同列名的,尽量一一列出取别名-->
<select id="getAllUser" parameterType="_int" resultMap="ST">
    select s.id sid,s.name sname,t.name tname,t.id tid
    from student s,teacher t
    where s.Tid=t.id and t.id=#{tid}
</select>
<resultMap id="ST" type="Teacher">
    <result column="tid" property="id"></result>
    <result column="tname" property="name"></result>
    <collection property="students" ofType="Student">
        <result column="sid" property="id"></result>
        <result column="sname" property="name"></result>
        <result column="Tid" property="tid"></result>
    </collection>
</resultMap>

在这里插入图片描述

8.3 小结

1)关联 -association【多对一】

2)集合 -collection【一对多】

3)javaType & ofType

  • javaType:用来指定实体类中属性pojo类型
  • ofType:用来指定实体类中集合的泛型pojo类型

九.动态SQL

本质就是根据不同情况,拼接SQL

数据库:
在这里插入图片描述

9.1 IF

public interface BlogMapper {
    //输入标题或作者查询对应博客,否则查询全部
    List<Blog> queryBlogIF(Map map);
}
<select id="queryBlogIF" resultType="Blog" parameterType="map">
    select * from blog where 1=1
    <if test="title != null">
        and title=#{title}
    </if>
    <if test="author != null">
        and author=#{author}
    </if>
</select>

9. 2 trim(where\set)

where

  1. 只会在子元素返回任何内容的情况下才插入 “WHERE” 子句
  2. 若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除

如上SQL可改装为:

<!--如果只有author,会自动去除开头的and-->
<select id="queryBlogIF" resultType="Blog" parameterType="map">
    select * from blog
    <where>
        <if test="title != null">title=#{title}</if>
        <if test="author != null">and author=#{author}</if>
    </where>
</select>

set用于动态更新语句

set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号

<update id="updataFn" parameterType="map">
    update blog
    <set>
        <if test="title!=null">title=#{title},</if>
        <if test="views!=null">views=#{views},</if>
    </set>
    where id=#{id}
</update>

trim

<!--where元素等价于-->
<trim prefix="WHERE" prefixOverrides="AND |OR ">...</trim>
<!--set元素等价于-->
<trim prefix="SET" suffixOverrides=",">...</trim>

9. 3 choose

choose (when , otherwise):不想使用所有的条件,而只是想从多个条件中选择一个使用。类似于switch语句

<!--若有title,则不管走其他选项;若没有title与author,则执行otherwise-->
<select id="queryBlogIF" resultType="Blog" parameterType="map">
    select * from blog where
    <choose>
        <when test="title != null">
            title=#{title}
        </when>
        <when test="author != null">
            author=#{author}
        </when>
        <otherwise>author="宋应星"</otherwise>
    </choose>
</select>

9. 4 foreach

//forEach
List<Blog> queryBlogEc(@Param("lis") List lis);
<select id="queryBlogEc" resultType="Blog" parameterType="list">
    select * from blog
    <where>
        id in
        <!--foreach:集合列表,元素名称,开始拼接符号,分隔符,结束拼接符号-->
        <foreach collection="lis" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </where>
</select>
<!--等价于:select * from blog where id in('GZ01','GZ02')-->

9. 5 拓展

  • 提取公共 sql 片段
<!--提取部分尽量简洁可复用-->
<sql id="abcName">
    <if test="title != null">and title=#{title}</if>
    <if test="author != null">and author=#{author}</if>
</sql>
<select id="queryBlogIF" resultType="Blog" parameterType="map">
    select * from blog
    <where>
        <include refid="abcName"></include>
    </where>
</select>

十.缓存

10.1 一级缓存

  • SqlSession级,也称为本地缓存(会话创建到关闭之间有效)
  • 之后需要查询相同的数据时,则直接在本地获取(地址相同)
  • 每次增、删、改都会更新缓存
  • 手动清理缓存 SqlSession.clearCache();

10.2 二级缓存

  • 基于 namespace 级别缓存(接口)
  • 不同的 mapper 使用不同的缓存
  • 手动开启,添加 标签
  • 原理:会话关闭时,将一级缓存中的缓存信息保存到二级缓存

【使用步骤】

  1. 在配置中显示的开启缓存
<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
  1. 在需要开启缓存mapper文件下添加配置<cache/>

    <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
    <!--配置创建了一个FIFO缓存,每隔60秒刷新,最多可以存储结果对象或列表的512个引用,返回的对象是只读的-->
    

    清除策略

    LRU – 最近最少使用:移除最长时间不被使用的对象

    FIFO – 先进先出:按对象进入缓存的顺序来移除它们

  2. 实体类添加序列化 ,即实现接口implements Serializable

  3. 对于例如频繁改动的数据,可手动关闭缓存useCache="false"(select元素特有属性)

【缓存执行顺序】

  1. 开始时先查二级缓存
  2. 二级缓存没有,则查一级缓存
  3. 一级缓存也没有,最近查数据库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值