MyBatis不写方法 为啥可访问数据库?(图解+秒懂+史上最全)

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面:

最近大厂机会多了, 在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的MyBatis 面试题。

前几天一个小伙面京东,被问 : MyBatis 如何实现 “面向接口” 查询的 ? mybatis 的用接口怎么实现查询的?

答案原文

昨天 小伙伴面试 阿里,又遇到了这个问题。

但是由于 没有回答好, 小伙伴 被刷了。

小伙伴面试完了之后,来求助尼恩。

那么,遇到 这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增。

4抡暴击,帮助大家逆天翻盘。

可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V175版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

第1抡暴击:介绍 MyBatis 基础使用

MyBatis 是一个半自动化 ORM 框架,它的核心目标是简化 Java 操作数据库的流程。相比原生 JDBC,它自动处理了连接管理、SQL 执行、结果映射等重复性工作,开发者只需关注两件事:

(1) 定义 Mapper 接口(声明方法)

(2) 编写 SQL 映射配置(XML 或注解)

调用时通过 SqlSession.getMapper() 获取代理对象,直接调用接口方法即可执行数据库操作,无需手动实现类。

原理拆解:为什么能“无类”调用?

MyBatis 利用 JDK 动态代理 技术,在运行时为 Mapper 接口生成代理对象。当你调用接口方法时(如 userMapper.selectById(1L)),MyBatis 会:

  • 根据方法名(selectById)和接口类型(UserMapper.class)定位到对应的 SQL 语句;
  • 解析参数并执行 SQL;
  • 将结果集自动映射成 Java 对象返回。

这就实现了“只写接口 + 写 SQL 配置 = 可调用 DAO”的开发模式。

类比理解:就像你点外卖,只需要说“来一份红烧肉”(调用方法),平台自动找到对应餐厅、下单、配送(执行 SQL 并返回结果),你不用亲自做饭(写 JDBC 代码)。

流程分析:从调用到执行的完整流程

以下是使用 MyBatis 查询一条用户记录的核心流程:

(1) 定义 Mapper 接口

(2) 编写 XML 映射文件

(3) 获取 SqlSession

(4) 通过 getMapper() 获得代理实例

(5) 调用接口方法触发 SQL 执行

示例代码 1. 定义 Mapper 接口


public interface UserMapper {
    User selectById(Long id);
}

示例代码 2. 定义映射文件

<!-- 2. UserMapper.xml 映射文件 -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectById" resultType="com.example.entity.User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

示例代码 3~5. 使用流程

// 3~5. 使用流程
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.selectById(1L); // 自动执行 SQL 并映射结果
    System.out.println(user);
}

基础规则:接口方法→ 到 → sql 语句 的 关联

  • UserMapper.selectById 方法→ 关联 → 对应 XML 中 <select id="selectById">
  • #{id} 是占位符,自动接收方法参数 Long id
  • resultType 指定将结果集映射为 User 类型对象
关键细节:各组件作用解析
组件作用
SqlSessionFactory创建 SqlSession 的工厂,通常全局唯一,由配置文件构建
SqlSession表示一次数据库会话,提供 CRUD 和获取 Mapper 的能力
Mapper 接口约定数据访问方法签名,MyBatis 自动生成实现
XML 映射文件包含 SQL 语句与方法的绑定关系,支持动态 SQL
namespace + id共同构成唯一标识,用于匹配接口中的方法

💡 45岁老架构师尼恩提示:

  • 接口全限定名必须与 XML 的 namespace 一致
  • 方法名必须与 <select><insert> 等标签的 id 属性一致
  • 参数通过 #{paramName} 注入,避免 SQL 注入
MyBatis 调用流程

MyBatis 调用流程 要点 小结
  • 两个必须:接口 + 映射配置(XML/注解)
  • 一次代理getMapper() 返回的是 JDK 动态代理对象
  • 三者绑定:接口方法名 ↔ XML 中的 id ↔ 实际 SQL 语句
  • 自动映射:结果集字段按名称匹配 Java 对象属性(可自定义规则)

此模式让 Java 代码与 SQL 彻底解耦,既保留了 SQL 的灵活性,又提升了开发效率,适合对 SQL 有精细控制需求的场景。

✅尼恩 总结 第1抡暴击 路径

【这个 阶段覆盖 核心点】

要点1、清晰阐述了 MyBatis 作为半自动化 ORM 的核心价值,准确指出其相较于原生 JDBC 在连接管理、SQL 执行和结果映射上的封装优势

要点2、正确描述了 Mapper 接口与 XML 映射文件之间的绑定关系,包括 namespace 与接口全限定名的对应、方法名与 SQL id 的匹配规则

【 这个 阶段的欠缺和不足】

停留在“是什么”和“怎么用”的表层叙述,未触及 MyBatis 如何实现 “无实现类调用” 的底层机制,缺乏 深挖

技术思维 停留 在使用者层面,尚未进化 的架构认知高度

【面试官的表情变化】

面试官 短暂露出笑意(情绪缓和),但 很快消失 。

整体处于“还算满意,但不惊喜”的状态,距离拍案叫绝、主动追问细节还差一大截

【开始 组织 下一阶段 暴击升级】:

提升方向1:必须由外而内,切入 JDK 动态代理的核心机制,讲清楚 MapperProxy 如何成为所有方法调用的统一入口
提升方向2:深入 MapperMethod 的作用,说明它如何封装 SqlCommand 和 MethodSignature,完成 SQL 定位与参数解析的双重职责

第2抡暴击:介绍 MyBatis 核心原理, 动态代理与映射绑定

MyBatis 的核心机制: 通过 Java 动态代理为 Mapper 接口生成代理对象,结合 SQL 映射配置,实现接口调用自动转化为数据库操作

大家写的 userMapper.selectById(1) 并没有实际实现类,而是由 MyBatis 自动生成的代理对象完成整个数据库访问流程。

这一过程 背后, 依赖几个关键组件协同工作:

  • MapperProxy:代理控制器,拦截方法调用
  • MapperMethod:封装了 SQL 指令和执行逻辑
  • SqlSession:对外交互的入口
  • Executor:真正的 SQL 执行引擎
原理拆解:从接口调用到数据库查询

我们以一个典型的调用为例:


User user = userMapper.selectById(1);

虽然 UserMapper 是一个接口,但能成功执行,是因为它被 MyBatis 包装成了代理对象。

整个流程可分为四个阶段:

1. 拦截调用 —— 调用的是代理对象的方法

当你获取 userMapper 时(例如通过 sqlSession.getMapper(UserMapper.class)),MyBatis 使用 Java 动态代理 创建了一个代理实例。

这个代理对象的类型是 MapperProxy,它实现了你定义的 Mapper 接口,并在 invoke() 方法中统一处理所有方法调用。

2. 映射定位 —— 找到对应的 SQL 语句

代理对象拿到方法调用后,会根据以下信息作为唯一标识去查找 SQL 配置:


接口全限定名 + 方法名
 
 如:com.example.UserMapper.selectById

这个 ID 会匹配 XML 中的 <select> 标签或注解上的 SQL:


<!-- UserMapper.xml -->
<select id="selectById" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>

或者使用注解方式:


@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);

MyBatis 在启动时, 将这些映射关系加载到内存中(保存在 Configuration 对象里),所以可以快速查找到对应 SQL 和返回类型。

3. 参数转换与 SQL 执行 —— 准备并执行数据库命令

找到 SQL 后,MyBatis 将 Java 方法参数 , 按照规则填充进 SQL 。

这一步由 MapperMethod 完成参数解析和包装。

然后,MapperMethod 调用 sqlSession 的增删改查方法,sqlSession 最终交由 Executor 执行。

Executor 是 MyBatis 的执行引擎,负责:

  • 管理事务
  • 一级缓存处理
  • 调用 JDBC 的 PreparedStatementResultSet
4. 结果映射 —— 把数据库记录转成 Java 对象

SQL 执行完成后,JDBC 返回 ResultSet

MyBatis 根据你在 <select> 中指定的 resultTyperesultMap,自动将结果集字段映射到 Java 对象属性上。

比如:


<resultMap id="UserResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
</resultMap>

支持基本类型、POJO、嵌套对象等多种映射方式。

一次查询的完整步骤流程分析:

以下是上述过程的清晰分步说明:

步骤操作内容对应组件
1调用 userMapper.selectById(1)开发者代码
2MapperProxy.invoke() 拦截调用MapperProxy
3构造 MapperMethod,查找 SQL 映射MapperMethod
4解析参数,构建 SQL 执行指令MapperMethod
5通过 SqlSession 调用执行器SqlSession
6Executor 执行 JDBC 操作Executor
7处理 ResultSet,映射为 User 对象DefaultResultSetHandler
8返回结果给调用方——
MapperProxy/MapperMethod/SqlSession/Executor 四个类的关系

四个 核心角色

  • MapperProxy:代理控制中心
  • MapperMethod:SQL 指令封装
  • SqlSession:外部调用入口
  • Executor:底层执行引擎

MyBatis核心执行流程中这几个关键类的协作关系图。

(1) 入口(MapperProxy):所有对Mapper接口的方法调用,首先被MapperProxy这个动态代理拦截。

(2) 封装(MapperMethod):代理将 方法调用 封装成一个MapperMethod。这个对象内部依赖SqlCommand(知道要执行什么SQL)和MethodSignature(知道方法的参数和返回类型)。

(3) 路由(SqlSession):MapperMethod根据SqlCommand中的SQL类型(SELECT/INSERT等),决定调用SqlSession对应的CRUD方法(如selectOne)。SqlSession扮演统一门户,接收指令。

(4) 执行(Executor):SqlSession并不真正处理数据库,而是将工作委托给Executor(真正的执行引擎)。Executor负责管理缓存、通过Statement执行SQL、处理结果集映射。

(5) 返回:数据库返回的结果沿着原路返回,最终由代理对象MapperProxy将结果返回给业务调用方。

这个流程清晰展示了MyBatis如何将一個接口方法的调用,优雅地转换为一条数据库SQL的执行。

MapperProxy /MapperMethod /SqlSession /Executor 四个类的关键细节
(1)MapperProxy:动态代理的核心

源码片段参考(简化版):


public class MapperProxy<T> implements InvocationHandler {
    private SqlSession sqlSession;
    private Class<T> mapperInterface;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只处理接口中的方法
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        // 封装成 MapperMethod 并执行
        final MapperMethod mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
        return mapperMethod.execute(sqlSession, args);
    }
}

这段代码说明:所有 Mapper 接口方法都会被 MapperProxy 拦截,并封装为 MapperMethod 执行。

(2)MapperMethod:SQL 指令的封装者

MapperMethod 内部有两个重要内部类:

  • SqlCommand:存储 SQL 语句类型(SELECT/INSERT等)和实际 SQL 字符串
  • MethodSignature:描述方法参数、返回类型等元信息

执行时根据 SQL 类型决定调用 sqlSession.selectOne() 还是 insert() 等方法。

(3)SqlSession:统一入口

SqlSession 提供了所有数据库操作的 API,但它不直接执行,而是委托给 Executor

可以把 SqlSession 看作是一个“前台接待”,接收请求后转交给后台厨房(Executor)去做菜(执行 SQL)。

SqlSession 通过 “门面模式” 整合分散功能,既简化使用又保障框架内聚性。

SqlSession 管理缓存 生命周期 / 生效范围,保证缓存一致性。

(4)Executor:真正的执行引擎

常见的实现有:

  • SimpleExecutor:默认,每次重新创建 Statement
  • ReuseExecutor:缓存 Statement,重用
  • BatchExecutor:用于批量操作

Executor 还管理一级缓存,不过 一级缓存 生命周期 SqlSession强绑定 :

  • 当创建 SqlSession 时,Executor 管理的 localCache 缓存随之初始化;
  • 执行查询后,结果会存入该 Executor 的localCache 缓存中;
  • 若同一 SqlSession 内执行相同查询(参数、SQL 一致),直接从缓存获取,无需重复访问数据库;
  • 当 SqlSession 执行 commit、rollback 或关闭时,Executor 管理的 localCache 缓存被清空,避免事务内数据不一致。
  • SqlSession 管理 Executor 缓存 localCache 的生命周期 / 生效范围,保证缓存一致性。
  • 也就是说,虽然Executor 管理的 localCache,但是 localCache的生命周期 与 SqlSession 强绑定。
MapperProxy /MapperMethod /SqlSession /Executor 四个类的 核心流程图

小节 要点
  • 动态代理MapperProxy 拦截接口调用
  • 映射绑定:通过 “接口全限定名.方法名” 找到 SQL
  • 四步流程:拦截 → 定位 → 执行 → 映射

这套机制让你只需定义接口和 SQL 映射,就能完成完整的数据访问,既简洁又灵活,是 MyBatis 设计精妙之处。

✅尼恩 总结 第2抡暴击 路径

【这里 阶段覆盖 核心点】

MyBatis 的动态代理机制、MapperProxy 的拦截作用、MapperMethod 对 SQL 请求的封装、SqlSession 与 Executor 的执行链路、结果映射流程。

清晰地拆解了从接口调用到数据库查询的四个阶段,逻辑链条完整,组件职责分明,展现了扎实的框架认知基础。

【 这 阶段的欠缺和不足】

没有 源码层级揭示代理创建的时机与注册机制;没有 剖析 MapperRegistry 如何维护接口与工厂的映射关系;

【面试官的表情变化】

面试官 认可 你的 知识广度,但 未激起情绪峰值,做不到 拍案叫绝 。

【开始 组织 下一阶段 暴击升级】

深入源码层级揭示代理创建的时机与注册机制;剖析 MapperRegistry 如何维护接口与工厂的映射关系; 将零散知识点升维为“框架初始化→代理生成→方法调度→SQL 执行”的系统性架构思维。

第3抡暴击:进行 MapperProxy 底层源码 解析

MyBatis 能让接口方法直接执行 SQL,靠的不是魔法,而是 动态代理机制

真正的“执行者”并不是你写的 Mapper 接口实现类(因为你根本没写),而是一个由 MapperProxy 自动生成的代理对象。

这个代理对象在你调用任意 Mapper 方法时,会拦截调用,解析方法意图,并最终转发给 SqlSession 去执行对应的 SQL。

整个流程可以概括为:

获取代理 → 拦截方法 → 封装请求 → 执行SQL

原理拆解:从 getMapper 到方法调用的背后
1. 动态代理的起点:SqlSession.getMapper

当你调用:


UserMapper mapper = sqlSession.getMapper(UserMapper.class);

MyBatis 并不会去实例化 UserMapper 这个接口,因为接口不能被 new。
它通过 JDK 动态代理 创建一个代理对象,该对象实现了 UserMapper 接口,所有方法调用都会被统一处理。

这个过程的核心是 MapperRegistryMapperProxyFactory

  • MapperRegistry 是一个注册中心,保存了所有 Mapper 接口与其对应工厂的映射。
  • MapperProxyFactory 是工厂类,专门负责为某个 Mapper 接口生成代理实例。
2. 工厂创建代理:MapperProxyFactory.newInstance

以下是关键代码片段:


// MapperProxyFactory 关键代码
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(
        mapperInterface.getClassLoader(), 
        new Class[] { mapperInterface }, 
        mapperProxy
    );
}

这段代码的作用是:

  • 使用 JDK 的 Proxy.newProxyInstance 方法;
  • mapperInterface(如 UserMapper.class)作为要实现的接口;
  • MapperProxy 实例作为调用处理器(InvocationHandler);

也就是说,每一个 Mapper 接口的方法调用,都会交给 MapperProxy.invoke 来处理

一次方法调用的完整路径

来看这样一个调用:


User user = userMapper.selectById(1);

这行代码背后发生了什么?

下面是分步流程:

(1) 调用被 MapperProxy.invoke 拦截;

(2) 根据当前 Method 对象查找或缓存对应的 MapperMethod

(3) MapperMethod 封装了该方法对应的 SQL 信息(比如 SQL 语句、类型、参数映射等);

(4) 调用 mapperMethod.execute(sqlSession, args) 开始执行;

(5) 内部根据 SQL 类型决定调用 sqlSession.selectOneupdate 等具体方法;

(6) 最终由 MyBatis 执行 SQL 并返回结果。

源码 MapperProxy.invoke

// MapperProxy.invoke 方法核心逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 如果是 Object 自身的方法(如 toString),直接放行
    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }
    
    // 缓存机制:避免重复创建 MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    
    // 执行命令
    return mapperMethod.execute(sqlSession, args);
}

说明

  • cachedMapperMethod(method):先查缓存,没有就根据方法签名和 Mapper 配置构建一个新的 MapperMethod
  • mapperMethod.execute(...):进入命令执行阶段,真正触发 SQL 调用。
MapperMethod 如何决定执行方式?

MapperMethod 是一个封装类,它知道每个方法对应的是查询、更新还是其他操作。

它的 execute 方法会根据 SQL 类型和返回值类型,选择合适的 SqlSession 方法:


// 简化版 execute 逻辑示意
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
            } else {
                result = executeForObject(sqlSession, args);
            }
            break;
        case UPDATE:
        case INSERT:
        case DELETE:
            result = executeUpdate(sqlSession, args);
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
}

技术点总结**:

  • command.getType() 来自于 SQL 注解或 XML 中的 <select><update> 等标签;
  • 返回类型的判断(是否集合、是否 Map、是否有 ResultHandler)决定了调用 selectList 还是 selectOne
  • 整个设计体现了 命令模式(Command Pattern) —— 把一次数据库操作封装成对象来执行。
Mapper 方法调用全过程(Mermaid)流程图:

代码与流程的对应关系
流程阶段对应代码位置作用
获取代理MapperProxyFactory.newInstance创建 JDK 动态代理实例
拦截调用MapperProxy.invoke拦截所有接口方法调用
封装请求MapperMethod 构造函数解析方法签名 + SQL 映射信息
执行命令MapperMethod.execute根据类型调用 SqlSession 相应方法
小结:为什么需要 MapperProxy?
  • 接口无法实例化 → 必须用代理生成实现;
  • 方法无实现体 → 需要在运行时解析注解/XML 映射;
  • 统一调度入口 → 所有调用集中到 MapperProxy.invoke 处理;
  • 解耦 SQL 执行与接口定义 → 通过 MapperMethod 封装命令,符合开闭原则。

✅尼恩 总结 第3轮 暴击路径

【此 阶段覆盖 核心点】

完整的执行链路清晰拆解为“获取代理 → 拦截方法 → 封装请求 → 执行SQL”四个步骤;

源码层级的分析展示了对 JDK 动态代理、Proxy.newProxyInstance 调用逻辑以及方法缓存机制的理解。

【 此 阶段的欠缺和不足】

技术细节 慢慢, 但是 未能揭示这一整套机制背后所服务的更高层次架构目标,导致技术细节与系统思想脱节,呈现出“知其然不知其所以然”的断层。

【面试官的表情变化】

从最初的微微点头、身体前倾表现出兴趣,到听到源码解析时轻敲桌面表示认可

再到中期逐渐放缓笔记速度,显露出“这些我都知道,接下来呢?”

【开始 组织 下一阶段 暴击升级】

接下来, 需要 引入架构视角, 进入 “半自动化ORM”的 整体设计目标的介绍;

揭示“约定优于配置”和“分离关注点” 的架构思想;

完成从执行者到设计者的思维跃迁。

第4抡暴击: 架构升华, 升华到 MyBatis 架构思想

MyBatis 的核心架构思想是:在灵活性与自动化之间取得平衡

它不是完全隐藏 SQL 的“全自动”ORM(如 Hibernate),也不是像 JDBC 那样事无巨细都要手动处理,而是采用 半自动化的 SQL 映射机制,让开发者写 SQL,框架负责执行流程的自动化。

这种设计实现了:

  • SQL 可控、可优化;
  • Java 代码简洁、无冗余;
  • 开发效率高,同时兼顾性能。

MyBatis 架构思想

MyBatis的这一设计体现了 “约定优于配置”“分离关注点” 的架构思想。

它将“做什么”(接口声明)和“怎么做”(SQL实现)清晰分离,SQL集中管理便于审查和优化,Java代码则保持极致的简洁。

**“架构思想1: 约定优于配置”:**MyBatis 通过默认约定减少配置成本,如 Mapper 接口与 XML 文件同名同路径、方法名与 SQL 标签 id 一致,无需额外配置即可关联,仅在特殊场景才需显式配置,提升开发效率。

“架构思想2:分离关注点”: 将数据操作的 “定义(接口方法)” 与 “实现(SQL 逻辑)” 拆分,Java 代码专注关注业务接口,SQL 在 XML / 注解中独立维护,既保证 Java 代码简洁,又便于 SQL 优化与管理,实现业务与数据访问逻辑的解耦。

MyBatis 设计权衡

  • 设计思想:灵活性与自动化之间做出的 权衡

    MyBatis 不是一个全自动ORM/ “黑箱” ORM(如Hibernate)。MyBatis 是一个半自动化/半透明的SQL映射框架。这种设计体现了其在 灵活性与自动化之间做出的精妙权衡。它承认SQL的复杂性和重要性,不试图完全封装SQL ,而是将SQL的控制权交还给开发者,同时通过动态代理自动化处理所有重复的模板代码(参数设置、结果映射、资源管理等)。

  • 优劣权衡

    相比于全自动ORM,MyBatis需要开发者编写SQL,学习成本稍高,但换来了对SQL的极致控制和优化能力,避免了因框架自动生成SQL而导致的性能问题。相比于纯JDBC,它通过动态代理等机制自动化了繁琐流程,大大提升了开发效率。这种“SQL友好”的设计思想,使其在需要复杂查询和高性能要求的互联网场景中备受青睐。

… 略5000字+

…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值