MyBatis学习笔记

MyBatis学习笔记

log4j日志

添加依赖

<dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-log4j12</artifactId>
 <version>1.7.30</version>
</dependency>

编写配置文件

# 日志模块的配置
log4j.rootLogger=ERROR, stdout

# 打印级别
log4j.logger.site.leric.mybatis=DEBUG
# trace打印更多
#log4j.logger.site.leric.mybatis=TRACE
# 打印某个mapper 甚至单个方法的日志级别
log4j.logger.site.leric.mybatis.dao.VideoMapper.selectById=TRACE

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

主函数

  1. Mybatis使⽤流程
  2. 创建mybatis-config.xml 全局的配置⽂件
  3. 创建XXXMapper.xml配置⽂件
  4. 创建SqlSessionFactory
  5. ⽤SqlSessionFactory创建SqlSession对象
  6. ⽤SqlSession执⾏增删改查CRUD
public class Main {
    public static void main(String[] args) throws IOException {
        // 读取配置文件
        String resources = "config/mybatis-config.xml";
        // 构建sessionFactory
        InputStream inputStream = Resources.getResourceAsStream(resources);
        SqlSessionFactory factory =  new SqlSessionFactoryBuilder().build(inputStream);
        // 获取session
        // 使用try方法获取会自动关闭
        try(SqlSession session = factory.openSession()){
            VideoMapper mapper = session.getMapper(VideoMapper.class);
            Video video = mapper.selectById(36);
            System.out.println(video.toString());
            // 使用注解
//            List<Video> videos = mapper.selectList();

            List<Video> videos = mapper.selectListByXML();
            for (Video video1 : videos) {
                System.out.println(video1.toString());
            }
        }
    }

配置文件

<?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>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

<!--  一个表会对应一个mapper,配置sql语句  -->
    <mappers>
<!--    注册mapper,被配置上的mapper会被自动扫描    -->
        <mapper resource="mapper/VideoMapper.xml"/>
    </mappers>
</configuration>

映射器

一个映射器由接口与mapper配置文件组成

public interface VideoMapper {
    /**
     * 根据视频id查找对象
     * @param videoId
     * @return
     */
    // 使用注解给参数取别名 取了别名就要用别名
    Video selectById(@Param("video_id") int videoId);


    /**
     * 查找全部视频成列表
     * @return
     */
    @Select("Select * from video")  // 使用注解实现sql绑定
    List<Video> selectList();
}    
  • namespace:名称空间,一般保持全局唯一,最好能与dao曾的java接口一致,将自动关联相关方法

    可以映射相关的sql语句到对应的方法和参数、返回类型

  • id:当前mapper下唯一

  • resultType:sql查询结果集的封装对象

传参与入参

  • 在接口的方法里使用@Param为参数指定别名

  • 配置问文件SQL语句使用#{ } 或者 ${ } 引用参数

  • 推荐使用#{ } ,因为${ } 实际上是拼接SQL命令,有面临QL注入的风险

<mapper namespace="site.leric.mybatis.dao.VideoMapper">
    <select id="selectById" resultType="site.leric.mybatis.domain.Video">
        select * from video where id = #{video_id}
    </select>
    
    <select id="selectListByXML" resultType="site.leric.mybatis.domain.Video">
        select * from video
    </select>
</mapper>

简单查询

Select语句
  1. 根据id查询一条记录
<!--    参数类型并不需要过多的关注,MyBatis可以自动转化     -->
<!--    jdbcType 用于指定传入数据库的参数的转换类型,通常来说是需要手动指定,否则会出现无效类型错误      -->
    <select id="selectById" parameterType="java.lang.Integer" resultType="site.leric.mybatis.domain.Video">

        select * from video where id = #{videoid,jdbcType=INTEGER}

    </select>
  1. 使用paramaterType指定自定义参数类型

    在select语句中若传入封装类,需要取得封装类的字段作为参数,在sql语句中需要pojo.field

<!--    传参为自定义对象,入参时需要POJO.filed    -->
    <select id="selectByPointAndTitleLikeOnVideo" parameterType="site.leric.mybatis.domain.Video" resultType="site.leric.mybatis.domain.Video">

        select * from video where point = #{video.point} and title like concat('%',#{video.title},'%')

    </select>
  1. 模糊查询

    模糊查询不能使用字符串拼接,需要使用自带函数concat(‘%’,paramater,‘%’)

<!--    模糊查询不能使用'%%' 字符串拼接的方式    -->
    <select id="selectByPointAndTitleLike" resultType="site.leric.mybatis.domain.Video">

        select * from video where point = #{point} and title like concat('%',title,'%')

    </select>
  1. sql片段复用
<mapper namespace="site.leric.mybatis.dao.VideoMapper">

<!--    定义sql片段     -->
    <sql id="base_select" >

        id,summary,title

    </sql>

<!--    配置了别名之后使用别名     -->
    <select id="selectById" resultType="video">
    
<!--    复用sql片段   -->
        select <include refid="base_select"/> from video where id = #{video_id}

    </select>
    
</mapper>

insert语句
  1. 插入一条数据并回填主键

    插入数据若参数类型是自定义的封装类,则入参是只需要调用封装对象的属性名即可

     <!--    插入记录
                useGeneratedKeys="true"     主键回填
                keyProperty="id"            映射Java对象属性
                keyColumn="id"              映射数据库字段
                当参数非集合的时候,MyBatis能自动识别字段
         -->
        <insert id="add" parameterType="site.leric.mybatis.domain.Video" useGeneratedKeys="true" keyProperty="id"
                keyColumn="id">
    
        INSERT INTO `springboot`.`video`(`title`, `summary`, `cover_img`, `price`, `create_time`, `point`)
        VALUES
        (#{title,jdbcType=VARCHAR},#{summary,jdbcType=VARCHAR},#{coverImg,jdbcType=VARCHAR},
        #{price,jdbcType=INTEGER},#{createTime,jdbcType=TIMESTAMP},#{point,jdbcType=DOUBLE});
    
        </insert>
    
  2. 批量插入多条数据

    参数类型是集合,在MyBatis中需要使用<foreach>标签进行迭代

    并且在入参时,需要使用index.field 取得封装对象的属性

主键回填
  • useGeneratedKeys=“true” 开启主键回填

  • keyColumn=“id” 指定数据库中的主键列名

  • keyProperty=“id” 指定映射主键的field

    <!--    批量插入
            item:每⼀个元素进⾏迭代时的别名
            index:索引的属性名,在集合数组情况下值为当前索引值,当迭代对象是map时,这个值是map的key
            open:整个循环内容的开头字符串
            close:整个循环内容的结尾字符串
            separator: 每次循环的分隔符
            当插入参数是集合时,需要使用item.field来指定参数
    -->
    <insert id="addBatch" parameterType="site.leric.mybatis.domain.Video" useGeneratedKeys="true" keyProperty="id"
            keyColumn="id">

        INSERT INTO `springboot`.`video`(`title`, `summary`, `cover_img`, `price`, `create_time`, `point`)
        VALUES
        <foreach collection="list" item="video" separator=",">
            (#{video.title,jdbcType=VARCHAR},#{video.summary,jdbcType=VARCHAR},#{video.coverImg,jdbcType=VARCHAR},
            #{video.price,jdbcType=INTEGER},#{video.createTime,jdbcType=TIMESTAMP},#{video.point,jdbcType=DOUBLE})
        </foreach>

    </insert>
update语句
  1. 更新一条记录
    <update id="updateVideo" parameterType="site.leric.mybatis.domain.Video">
        UPDATE `springboot`.`video`
        SET
        `title` = #{title,jdbcType = VARCHAR},
        `summary` = #{summary,jdbcType = VARCHAR},
        `cover_img` = #{coverImg,jdbcType = VARCHAR},
        `price` = #{price,jdbcType = INTEGER},
        `create_time` = #{createTime,jdbcType = TIMESTAMP},
        `point` = #{point,jdbcType = DOUBLE}
        WHERE `id` = #{id,jdbcType = VARCHAR};
    </update>
  1. 更新添加记录

    trim用于判断字符串的开始与结尾且可以指定字符穿前缀以及去除最后一个字符的后缀

    if 标签可以通过判断传⼊的值来确定查询条件,test 指定⼀个OGNL表达式

    使用if标签,判断的时候需要依靠pojo的数据类型来判断条件,基本数据类型默认值与引用数据类型的默认是是不一样的

<!--
        只有参数传入的字段不为空才进行更新操作
        trim 用于判断最后一个字符,加上指定的符号;
             并在第一个字符前加上前缀
-->
    <update id="updateVideoSelective" parameterType="video">
        UPDATE `springboot`.`video`

        <trim prefix="set" suffixOverrides=",">
            <if test="title !=null">`title` = #{title,jdbcType = VARCHAR},</if>
            <if test="summary !=null">`summary` = #{summary,jdbcType = VARCHAR},</if>
            <if test="coverImg !=null">`cover_img` = #{coverImg,jdbcType = VARCHAR},</if>
            <if test="price !=0">`price` = #{price,jdbcType = INTEGER},</if>
            <if test="createTime !=null">`create_time` = #{createTime,jdbcType = TIMESTAMP},</if>
            <!-- 要以POJO的类型的默认值来决定条件 -->
            <if test="point != 0.0">`point` = #{point,jdbcType = DOUBLE},</if>
        </trim>

        WHERE `id` = #{id,jdbcType = VARCHAR}

    </update>

delete语句
<!--    大于小于号需要转义字符     -->
    <delete id="deleteByCreateTimeAndPrice" parameterType="java.util.Map">

        delete from video where create_time > #{createTime} and price <![CDATA[ < ]]> #{price}

    </delete>
MyBatis转义

由于MyBatis的sql写在XML⾥⾯, 有些sql的语法符号和xml⾥⾯的冲突

⼤于等于 <![CDATA[ >= ]]>
⼩于等于 <![CDATA[ <= ]]>
配置别名

typeAlias 配置全局别名

  • 类型别名,给类取个别名,可以不⽤输⼊类的全限定名
<!--<select id="selectById" parameterType="java.lang.Integer"
resultType="net.xdclass.online_class.domain.Video">-->
 <select id="selectById" parameterType="java.lang.Integer"
resultType="Video">
 select * from video where id = #{video_id,jdbcType=INTEGER}
 </select>

​ 别名配置之后使用全类名依然能正常使用

  • 包扫描配置
<typeAliases>
 <!--<typeAlias type="net.xdclass.online_class.domain.Video"
alias="Video"/>-->
<!--        指定domain包下的全部类都以类名作为别名-->
 <package name="net.xdclass.online_class.domain"/>
</typeAliases>

复杂查询

Mybatis的SQL语句返回结果有两种

  • resultType
    • 查询出的字段在相应的pojo中必须有和它相同的字段对应(列名精准匹配),或者基本数据类型
    • 适合简单查询
    • 实际上Mybatis在后台创建了一个ResultMap
  • resultMap
  • 从查询出来的结果中映射到对象,定义映射规则
    • 需要⾃定义字段,或者多表查询,⼀对多等关系,⽐resultType更强⼤
    • 适合复杂查询
<!--根据sql查询结果自动映射到指定的实体类里面-->
    <resultMap id="VideoResultMap" type="Video">

<!--
        id:映射主键
        colunm 对应数据库的列
        property 映射到POJO类属性的名称
        jdbcType 数据库参数类型
		从查询结果中按照colonm与实体类的property完成映射
-->
        <id column="id" property="id" jdbcType="INTEGER"/>
		
        <result column="video_title" property="title" jdbcType="VARCHAR"/>
        <result column="summary" property="summary" jdbcType="VARCHAR"/>
        <result column="cover_img" property="coverImg" jdbcType="VARCHAR"/>

    </resultMap>

一对一查询association
一对多查询collection

缓存与懒加载

一级缓存

程序经常要调⽤的对象存在内存中,⽅便其使⽤时可以快速调⽤,不必去数据库或者其他持久化设备中查询,主要就是提⾼性能

  • Mybatis⼀级缓存
    • 简介:⼀级缓存的作⽤域是SQLSession,session关闭之后缓存失效
    • 同⼀个SqlSession中执⾏相同的SQL查询(相同的SQL和参数),第⼀次会去查询数据库并写在缓存中,第⼆次会直接从缓存中取
    • 基于PerpetualCache 的 HashMap本地缓存
    • 默认开启⼀级缓存
  • 失效策略:当执⾏SQL时候两次查询中间发⽣了增删改的操作,即insert、update、delete等操作commit后会清空该SQLSession缓存; ⽐如sqlsession关闭,或者清空
 // 演示一级缓存
for (int i = 0; i < 2; i++) {
    Video video = mapper.selectBseFieldByIdWithResultMap(36);

    System.out.println(video.toString());
}
==============只发送了一次sql==============
DEBUG [main] - ==>  Preparing: SELECT id , title AS video_title,summary,cover_img from video where id = ? 
DEBUG [main] - ==> Parameters: 36(Integer)
DEBUG [main] - <==      Total: 1
Video{id=36, title='19年录制ES6教程ES7ES8实战应用', summary='https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/video/2019_frontend/es67/es67_detail.png
', CoverImg='https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/video/2019_frontend/es67/es.png
', price=0, creteTime=null, point=0.0}
Video{id=36, title='19年录制ES6教程ES7ES8实战应用', summary='https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/video/2019_frontend/es67/es67_detail.png
', CoverImg='https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/video/2019_frontend/es67/es.png
', price=0, creteTime=null, point=0.0}
二级缓存
  • 简介:⼆级缓存是namespace级别的,多个SqlSession去操作同⼀个namespace下的Mapper的sql语句,多个SqlSession可以共⽤

    ⼆级缓存,如果两个mapper的namespace相同,

    (即使是两个mapper,那么这两个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中,但是建议是每个Mapper单独的名称空间)

  • 基于PerpetualCache 的 HashMap本地缓存,可⾃定义存储源,如 Ehcache/Redis等

  • 默认是没有开启⼆级缓存

  • 操作流程:第⼀次调⽤某个namespace下的SQL去查询信息,查询到的信息会存放该mapper对应的⼆级缓存区域。

    第⼆次调⽤同个namespace下的mapper映射⽂件中,相同的sql去查询信息,会去对应的⼆级缓存内取结果

  • 失效策略:执⾏同个namespace下的mapepr映射⽂件中增删改sql,并执⾏了commit操作,会清空该⼆级缓存

  • 注意:实现⼆级缓存的时候,MyBatis建议返回的POJO是可序列化的, 也就是建议实现Serializable接⼝

    使用自定义存储源头必须实现序列化接口

  • 缓存淘汰策略:会使⽤默认的 LRU 算法来收回(最近最少使⽤的)

  • 如何开启某个⼆级缓存 mapper.xml⾥⾯配置

mapper配置

<!--开启mapper的namespace下的⼆级缓存-->
 <!--
 eviction:代表的是缓存回收策略,常⻅下⾯两种。
 (1) LRU,最近最少使⽤的,⼀处最⻓时间不⽤的对象
 (2) FIFO,先进先出,按对象进⼊缓存的顺序来移除他们
 flushInterval:刷新间隔时间,单位为毫秒,这⾥配置的是100秒刷新,如果不配置
它,当SQL被执⾏的时候才会去刷新缓存。
 size:引⽤数⽬,代表缓存最多可以存储多少个对象,设置过⼤会导致内存溢出
 readOnly:只读,缓存数据只能读取⽽不能修改,默认值是false
 -->
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>

全局配置

<!-- 全局配置 -->
<settings>
<!--这个配置使全局的映射器(⼆级缓存)启⽤或禁⽤缓存,全局总开关,这⾥关闭,mapper中开启
了也没⽤-->
 <setting name="cacheEnabled" value="true" />
</settings>
二级缓存演示

演示代码

SqlSession session = factory.openSession();

VideoOrderMapper mapper1 = session.getMapper(VideoOrderMapper.class);
List<VideoOrder> videoOrders = mapper1.queryVideoOrderList();
System.out.println(videoOrders.size());

// 提交清空一级缓存
session.commit();

VideoOrderMapper mapper2 = session.getMapper(VideoOrderMapper.class);
List<VideoOrder> videoOrders2 = mapper2.queryVideoOrderList();
System.out.println(videoOrders2.size());

在没有开启二级缓存的情况下

DEBUG [main] - ==>  Preparing: select o.id,o.user_id,o.create_time,o.state,o.total_fee,o.video_id,o.video_title, u.name,u.head_img,u.create_time,u.phone from video_order as o left join user as u on o.user_id = u.id 
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 6
6
DEBUG [main] - ==>  Preparing: select o.id,o.user_id,o.create_time,o.state,o.total_fee,o.video_id,o.video_title, u.name,u.head_img,u.create_time,u.phone from video_order as o left join user as u on o.user_id = u.id 
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 6
6
==============发送了两次sql==============

开启了二级缓存后

DEBUG [main] - Cache Hit Ratio [site.leric.mybatis.dao.VideoOrderMapper]: 0.0
DEBUG [main] - ==>  Preparing: select o.id,o.user_id,o.create_time,o.state,o.total_fee,o.video_id,o.video_title, u.name,u.head_img,u.create_time,u.phone from video_order as o left join user as u on o.user_id = u.id 
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 6
6
DEBUG [main] - Cache Hit Ratio [site.leric.mybatis.dao.VideoOrderMapper]: 0.5
6
==============只发送一次sql==============

在关闭二级请求,并且清理一级缓存

VideoOrderMapper mapper1 = session.getMapper(VideoOrderMapper.class);
List<VideoOrder> videoOrders = mapper1.queryVideoOrderList();
System.out.println(videoOrders.size());

// 提交清空一级缓存
session.commit();

List<VideoOrder> videoOrders2 = mapper1.queryVideoOrderList();
System.out.println(videoOrders2.size());

优先查询二级缓存==>一级缓存==>数据库

DEBUG [main] - ==>  Preparing: select o.id,o.user_id,o.create_time,o.state,o.total_fee,o.video_id,o.video_title, u.name,u.head_img,u.create_time,u.phone from video_order as o left join user as u on o.user_id = u.id 
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 6
6
DEBUG [main] - ==>  Preparing: select o.id,o.user_id,o.create_time,o.state,o.total_fee,o.video_id,o.video_title, u.name,u.head_img,u.create_time,u.phone from video_order as o left join user as u on o.user_id = u.id 
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 6
6
==============发送了两次sql==============

控制方法是否使用二级缓存

<!--useCache是否使用二级缓存-->
    <select id="queryVideoOrderList" resultMap="VideoOrderResultMap" useCache="false">

        select o.id,o.user_id,o.create_time,o.state,o.total_fee,o.video_id,o.video_title,
               u.name,u.head_img,u.create_time,u.phone
        from video_order as o left join user as u on o.user_id = u.id

    </select>
懒加载
  • 什么是懒加载: 按需加载,先从单表查询,需要时再从关联表去关联查询,能⼤⼤提⾼数据库性能,并不是所有场景下使⽤懒加载都能提⾼效率

  • Mybatis懒加载: resultMap⾥⾯的association、collection有延迟加载功能

  • 开启懒加载

    • 全局配置
    <!--全局参数设置-->
    <settings>
     <!--延迟加载总开关-->
     <setting name="lazyLoadingEnabled" value="true"/>
     <!--将aggressiveLazyLoading设置为false表示按需加载,默认为true-->
     <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
    
    • Mapperp配置
    <resultMap id="VideoOrderResultMapLazy" type="VideoOrder">
     <id column="id" property="id"/>
     <result column="user_id" property="userId"/>
     <result column="out_trade_no" property="outTradeNo"/>
     <result column="create_time" property="createTime"/>
     <result column="state" property="state"/>
     <result column="total_fee" property="totalFee"/>
     <result column="video_id" property="videoId"/>
     <result column="video_title" property="videoTitle"/>
     <result column="video_img" property="videoImg"/>
    <!--
    select: 指定延迟加载需要执⾏的statement id
    column: 和select查询关联的字段
    -->
    <association property="user" javaType="User" column="user_id"
    select="findUserByUserId"/>
    </resultMap>
     <!--⼀对⼀管理查询订单, 订单内部包含⽤户属性
    
    懒加载-->
    <select id="queryVideoOrderListLazy" resultMap="VideoOrderResultMapLazy">
     select
     o.id id,o.user_id ,o.out_trade_no,o.create_time,o.state,o.total_fee,o.video_id,o.video_title,o.video_img
     from video_order o
    </select>
    <select id="findUserByUserId" resultType="User">
     select * from user where id=#{id}
    </select>
    

数据库事务

  • 使⽤JDBC的事务管理
    使⽤ java.sql.Connection对象完成对事务的提交(commit())、回滚(rollback())、关闭(close())

  • 使⽤MANAGED的事务管理
    MyBatis⾃身不会去实现事务管理,⽽让程序的容器如(Spring, JBOSS)来实现对事务的管理

    使用MANAGED一般不设置自动提交

  • Mybatis事务⼯⼚TransactionFactory 的两个实现类,对应两种事务管理方式

    • JdbcTransactionFactory->JdbcTransaction
    • ManagedTransactionFactory->ManagedTransaction
<environment id="development">
    
 <!-- mybatis使⽤jdbc事务管理⽅式 -->
 <transactionManager type="JDBC"/>
 <!-- mybatis使⽤MANAGED事务管理⽅式(使用外部的管理方式) -->
 <transactionManager type="MANAGED"/>
    
 <dataSource type="POOLED">
 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
 <property name="url"
value="jdbc:mysql://127.0.0.1:3306/xdclass?
useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false"/>
 <property name="username" value="root"/>
 <property name="password" value="xdclass.net"/>
 </dataSource>
</environment>

两种数据库引擎

区别项Innodbmyisam
事务⽀持不⽀持
锁粒度⾏锁,适合⾼并发表锁,不适合⾼并发
是否默认默认⾮默认
⽀持外键⽀持外键不⽀持
适合场景读写均衡,写⼤于读场景,需要事务读多写少场景,不需要事务
全⽂索引可以通过插件实现, 更多使⽤ElasticSearch⽀持全⽂索引

MyISAM不⽀持事务,如果需要事务则改为innodb引擎 更改数据库的表⾥⾯的引擎

// 设置事务自动提交
SqlSession session = factory.openSession(true);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值