三天撸完了MyBatis,各位随便问!!(冰河吐血整理,建议收藏)

return null;

}

总结:主要是通过ClassLoader.getResourceAsStream()方法获取指定的classpath路径下的Resource 。

通过SqlSessionFactoryBuilder创建SqlSessionFactory

//SqlSessionFactoryBuilder是一个建造者模式

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

public SqlSessionFactory build(InputStream inputStream) {

return build(inputStream, null, null);

}

//XMLConfigBuilder也是建造者模式

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

try {

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

return build(parser.parse());

} catch (Exception e) {

throw ExceptionFactory.wrapException(“Error building SqlSession.”, e);

} finally {

ErrorContext.instance().reset();

try {

inputStream.close();

} catch (IOException e) {

// Intentionally ignore. Prefer previous error.

}

}

}

//接下来进入XMLConfigBuilder构造函数

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {

this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);

}

//接下来进入this后,初始化Configuration

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {

super(new Configuration());

ErrorContext.instance().resource(“SQL Mapper Configuration”);

this.configuration.setVariables(props);

this.parsed = false;

this.environment = environment;

this.parser = parser;

}

//其中parser.parse()负责解析xml,build(configuration)创建SqlSessionFactory

return build(parser.parse());

parser.parse()解析xml

public Configuration parse() {

//判断是否重复解析

if (parsed) {

throw new BuilderException(“Each XMLConfigBuilder can only be used once.”);

}

parsed = true;

//读取配置文件一级节点configuration

parseConfiguration(parser.evalNode(“/configuration”));

return configuration;

}

private void parseConfiguration(XNode root) {

try {

//properties 标签,用来配置参数信息,比如最常见的数据库连接信息

propertiesElement(root.evalNode(“properties”));

Properties settings = settingsAsProperties(root.evalNode(“settings”));

loadCustomVfs(settings);

loadCustomLogImpl(settings);

//实体别名两种方式:1.指定单个实体;2.指定包

typeAliasesElement(root.evalNode(“typeAliases”));

//插件

pluginElement(root.evalNode(“plugins”));

//用来创建对象(数据库数据映射成java对象时)

objectFactoryElement(root.evalNode(“objectFactory”));

objectWrapperFactoryElement(root.evalNode(“objectWrapperFactory”));

reflectorFactoryElement(root.evalNode(“reflectorFactory”));

settingsElement(settings);

// read it after objectFactory and objectWrapperFactory issue #631

//数据库环境

environmentsElement(root.evalNode(“environments”));

databaseIdProviderElement(root.evalNode(“databaseIdProvider”));

//数据库类型和Java数据类型的转换

typeHandlerElement(root.evalNode(“typeHandlers”));

//这个是对数据库增删改查的解析

mapperElement(root.evalNode(“mappers”));

} catch (Exception e) {

throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);

}

}

总结:parseConfiguration完成的是解析configuration下的标签

private void mapperElement(XNode parent) throws Exception {

if (parent != null) {

for (XNode child : parent.getChildren()) {

//解析

if (“package”.equals(child.getName())) {

String mapperPackage = child.getStringAttribute(“name”);

//包路径存到mapperRegistry中

configuration.addMappers(mapperPackage);

} else {

//解析

String resource = child.getStringAttribute(“resource”);

String url = child.getStringAttribute(“url”);

String mapperClass = child.getStringAttribute(“class”);

if (resource != null && url == null && mapperClass == null) {

ErrorContext.instance().resource(resource);

//读取Mapper.xml文件

InputStream inputStream = Resources.getResourceAsStream(resource);

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream,

configuration, resource, configuration.getSqlFragments());

mapperParser.parse();

} else if (resource == null && url != null && mapperClass == null) {

ErrorContext.instance().resource(url);

InputStream inputStream = Resources.getUrlAsStream(url);

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream,

configuration, url, configuration.getSqlFragments());

mapperParser.parse();

} else if (resource == null && url == null && mapperClass != null) {

Class<?> mapperInterface = Resources.classForName(mapperClass);

configuration.addMapper(mapperInterface);

} else {

throw new BuilderException(“A mapper element may only specify a url, resource or class, but not more than one.”);

}

}

}

}

}

总结: 通过解析configuration.xml文件,获取其中的Environment、Setting,重要的是将下的所有解析出来之后添加到

Configuration,Configuration类似于配置中心,所有的配置信息都在这里。

mapperParser.parse()对 Mapper 映射器的解析

public void parse() {

if (!configuration.isResourceLoaded(resource)) {

//解析所有的子标签

configurationElement(parser.evalNode(“/mapper”));

configuration.addLoadedResource(resource);

//把namespace(接口类型)和工厂类绑定起来

bindMapperForNamespace();

}

parsePendingResultMaps();

parsePendingCacheRefs();

parsePendingStatements();

}

//这里面解析的是Mapper.xml的标签

private void configurationElement(XNode context) {

try {

String namespace = context.getStringAttribute(“namespace”);

if (namespace == null || namespace.equals(“”)) {

throw new BuilderException(“Mapper’s namespace cannot be empty”);

}

builderAssistant.setCurrentNamespace(namespace);

//对其他命名空间缓存配置的引用

cacheRefElement(context.evalNode(“cache-ref”));

//对给定命名空间的缓存配置

cacheElement(context.evalNode(“cache”));

parameterMapElement(context.evalNodes(“/mapper/parameterMap”));

//是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象

resultMapElements(context.evalNodes(“/mapper/resultMap”));

//可被其他语句引用的可重用语句块

sqlElement(context.evalNodes(“/mapper/sql”));

//获得MappedStatement对象(增删改查标签)

buildStatementFromContext(context.evalNodes(“select|insert|update|delete”));

} catch (Exception e) {

throw new BuilderException(“Error parsing Mapper XML. The XML location is '” + resource + "'. Cause: " + e, e);

}

}

//获得MappedStatement对象(增删改查标签)

private void buildStatementFromContext(List list) {

if (configuration.getDatabaseId() != null) {

buildStatementFromContext(list, configuration.getDatabaseId());

}

buildStatementFromContext(list, null);

}

//获得MappedStatement对象(增删改查标签)

private void buildStatementFromContext(List list, String requiredDatabaseId) {

//循环增删改查标签

for (XNode context : list) {

final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);

try {

//解析insert/update/select/del中的标签

statementParser.parseStatementNode();

} catch (IncompleteElementException e) {

configuration.addIncompleteStatement(statementParser);

}

}

}

public void parseStatementNode() {

//在命名空间中唯一的标识符,可以被用来引用这条语句

String id = context.getStringAttribute(“id”);

//数据库厂商标识

String databaseId = context.getStringAttribute(“databaseId”);

if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {

return;

}

String nodeName = context.getNode().getNodeName();

SqlCommandType sqlCommandType =

SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

//flushCache和useCache都和二级缓存有关

//将其设置为true后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false

boolean flushCache = context.getBooleanAttribute(“flushCache”, !isSelect);

//将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true

boolean useCache = context.getBooleanAttribute(“useCache”, isSelect);

boolean resultOrdered = context.getBooleanAttribute(“resultOrdered”, false);

// Include Fragments before parsing

XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);

includeParser.applyIncludes(context.getNode());

//会传入这条语句的参数类的完全限定名或别名

String parameterType = context.getStringAttribute(“parameterType”);

Class<?> parameterTypeClass = resolveClass(parameterType);

String lang = context.getStringAttribute(“lang”);

LanguageDriver langDriver = getLanguageDriver(lang);

// Parse selectKey after includes and remove them.

processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: and were parsed and removed)

KeyGenerator keyGenerator;

String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;

keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);

if (configuration.hasKeyGenerator(keyStatementId)) {

keyGenerator = configuration.getKeyGenerator(keyStatementId);

} else {

keyGenerator = context.getBooleanAttribute(“useGeneratedKeys”, configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;

}

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

StatementType statementType =

StatementType.valueOf(context.getStringAttribute(“statementType”,

StatementType.PREPARED.toString()));

Integer fetchSize = context.getIntAttribute(“fetchSize”);

Integer timeout = context.getIntAttribute(“timeout”);

String parameterMap = context.getStringAttribute(“parameterMap”);

//从这条语句中返回的期望类型的类的完全限定名或别名

String resultType = context.getStringAttribute(“resultType”);

Class<?> resultTypeClass = resolveClass(resultType);

//外部resultMap的命名引用

String resultMap = context.getStringAttribute(“resultMap”);

String resultSetType = context.getStringAttribute(“resultSetType”);

ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

String keyProperty = context.getStringAttribute(“keyProperty”);

String keyColumn = context.getStringAttribute(“keyColumn”);

String resultSets = context.getStringAttribute(“resultSets”);

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,

fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,

resultSetTypeEnum, flushCache, useCache, resultOrdered,

keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

}

public MappedStatement addMappedStatement(

String id,

SqlSource sqlSource,

StatementType statementType,

SqlCommandType sqlCommandType,

Integer fetchSize,

Integer timeout,

String parameterMap,

Class<?> parameterType,

String resultMap,

Class<?> resultType,

ResultSetType resultSetType,

boolean flushCache,

boolean useCache,

boolean resultOrdered,

KeyGenerator keyGenerator,

String keyProperty,

String keyColumn,

String databaseId,

LanguageDriver lang,

String resultSets) {

if (unresolvedCacheRef) {

throw new IncompleteElementException(“Cache-ref not yet resolved”);

}

id = applyCurrentNamespace(id, false);

boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration,

id, sqlSource, sqlCommandType)

.resource(resource)

.fetchSize(fetchSize)

.timeout(timeout)

.statementType(statementType)

.keyGenerator(keyGenerator)

.keyProperty(keyProperty)

.keyColumn(keyColumn)

.databaseId(databaseId)

.lang(lang)

.resultOrdered(resultOrdered)

.resultSets(resultSets)

.resultMaps(getStatementResultMaps(resultMap, resultType, id))

.resultSetType(resultSetType)

.flushCacheRequired(valueOrDefault(flushCache, !isSelect))

.useCache(valueOrDefault(useCache, isSelect))

.cache(currentCache);

ParameterMap statementParameterMap = getStatementParameterMap(parameterMap,

parameterType, id);

if (statementParameterMap != null) {

statementBuilder.parameterMap(statementParameterMap);

}

MappedStatement statement = statementBuilder.build();

//持有在configuration中

configuration.addMappedStatement(statement);

return statement;

}

public void addMappedStatement(MappedStatement ms){

//ms.getId = mapper.UserMapper.getUserById

//ms = MappedStatement等于每一个增删改查的标签的里的数据

mappedStatements.put(ms.getId(), ms);

}

//最终存放到mappedStatements中,mappedStatements存放的是一个个的增删改查

protected final Map<String, MappedStatement> mappedStatements = new StrictMap(“Mapped Statements collection”).conflictMessageProducer((savedValue, targetValue) ->

". please check " + savedValue.getResource() + " and " + targetValue.getResource());

解析bindMapperForNamespace()方法

把 namespace(接口类型)和工厂类绑定起来

private void bindMapperForNamespace() {

//当前Mapper的命名空间

String namespace = builderAssistant.getCurrentNamespace();

if (namespace != null) {

Class<?> boundType = null;

try {

//interface mapper.UserMapper这种

boundType = Resources.classForName(namespace);

} catch (ClassNotFoundException e) {

}

if (boundType != null) {

if (!configuration.hasMapper(boundType)) {

configuration.addLoadedResource(“namespace:” + namespace);

configuration.addMapper(boundType);

}

}

}

}

public void addMapper(Class type) {

mapperRegistry.addMapper(type);

}

public void addMapper(Class type) {

if (type.isInterface()) {

if (hasMapper(type)) {

throw new BindingException(“Type " + type + " is already known to the MapperRegistry.”);

}

boolean loadCompleted = false;

try {

//接口类型(key)->工厂类

knownMappers.put(type, new MapperProxyFactory<>(type));

MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);

parser.parse();

loadCompleted = true;

} finally {

if (!loadCompleted) {

knownMappers.remove(type);

}

}

}

}

生成SqlSessionFactory对象

XMLMapperBuilder.parse()方法,是对 Mapper 映射器的解析里面有两个方法:

(1)configurationElement()解析所有的子标签,最终解析Mapper.xml中的insert/update/delete/select标签的id(全路径)组成key和整个标签和数据连接组成MappedStatement存放到Configuration中的 mappedStatements这个map里面。

(2)bindMapperForNamespace()是把接口类型(interface mapper.UserMapper)和工厂类存到放MapperRegistry中的knownMappers里面。

SqlSessionFactory的创建


public SqlSessionFactory build(Configuration config) {

return new DefaultSqlSessionFactory(config);

}

直接把Configuration当做参数,直接new一个DefaultSqlSessionFactory。

SqlSession会话的创建过程


mybatis操作的时候跟数据库的每一次连接,都需要创建一个会话,我们用openSession()方法来创建。这个会话里面需要包含一个Executor用来执行 SQL。Executor又要指定事务类型和执行器的类型。

创建Transaction(两种方式)

| 属性 | 产生工厂类 | 产生事务 |

| — | — | — |

| JDBC | JbdcTransactionFactory | JdbcTransaction |

| MANAGED | ManagedTransactionFactory | ManagedTransaction |

  • 如果配置的是 JDBC,则会使用Connection 对象的 commit()、rollback()、close()管理事务。

  • 如果配置成MANAGED,会把事务交给容器来管理,比如 JBOSS,Weblogic。

SqlSession sqlSession = sqlSessionFactory.openSession();

public SqlSession openSession() {

//configuration中有默认赋值protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE

return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);

}

创建Executor

//ExecutorType是SIMPLE,一共有三种SIMPLE(SimpleExecutor)、REUSE(ReuseExecutor)、BATCH(BatchExecutor)

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

Transaction tx = null;

try {

//xml中的development节点

final Environment environment = configuration.getEnvironment();

//type配置的是Jbdc所以生成的是JbdcTransactionFactory工厂类

final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

//Jdbc生成JbdcTransactionFactory生成JbdcTransaction

tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

//创建CachingExecutor执行器

final Executor executor = configuration.newExecutor(tx, execType);

//创建DefaultSqlSession属性包括 Configuration、Executor对象

return new DefaultSqlSession(configuration, executor, autoCommit);

} catch (Exception e) {

closeTransaction(tx); // may have fetched a connection so lets call

close()

throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);

} finally {

ErrorContext.instance().reset();

}

}

获得Mapper对象


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

public T getMapper(Class type) {

return configuration.getMapper(type, this);

}

mapperRegistry.getMapper是从MapperRegistry的knownMappers里面取的,knownMappers里面存的是接口类型(interface mapper.UserMapper)和工厂类(MapperProxyFactory)。

public T getMapper(Class type, SqlSession sqlSession) {

return mapperRegistry.getMapper(type, sqlSession);

}

从knownMappers的Map里根据接口类型(interface mapper.UserMapper)取出对应的工厂类。

public T getMapper(Class type, SqlSession sqlSession) {

final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory)

knownMappers.get(type);

if (mapperProxyFactory == null) {

throw new BindingException(“Type " + type + " is not known to the MapperRegistry.”);

}

try {

return mapperProxyFactory.newInstance(sqlSession);

} catch (Exception e) {

throw new BindingException("Error getting mapper instance. Cause: " + e, e);

}

}

public T newInstance(SqlSession sqlSession) {

final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);

return newInstance(mapperProxy);

}

这里通过JDK动态代理返回代理对象MapperProxy(org.apache.ibatis.binding.MapperProxy@6b2ea799)

protected T newInstance(MapperProxy mapperProxy) {

//mapperInterface是interface mapper.UserMapper

return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new

Class[] { mapperInterface }, mapperProxy);

}

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

执行SQL


User user = userMapper.getUserById(1);

调用invoke代理方法

由于所有的 Mapper 都是 MapperProxy 代理对象,所以任意的方法都是执行MapperProxy 的invoke()方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

try {

//判断是否需要去执行SQL还是直接执行方法

if (Object.class.equals(method.getDeclaringClass())) {

return method.invoke(this, args);

//这里判断的是接口中的默认方法Default等

} else if (isDefaultMethod(method)) {

return invokeDefaultMethod(proxy, method, args);

}

} catch (Throwable t) {

throw ExceptionUtil.unwrapThrowable(t);

}

//获取缓存,保存了方法签名和接口方法的关系

final MapperMethod mapperMethod = cachedMapperMethod(method);

return mapperMethod.execute(sqlSession, args);

}

调用execute方法

这里使用的例子用的是查询所以走的是else分支语句。

public Object execute(SqlSession sqlSession, Object[] args) {

Object result;

//根据命令类型走不行的操作command.getType()是select

switch (command.getType()) {

case INSERT: {

Object param = method.convertArgsToSqlCommandParam(args);

result = rowCountResult(sqlSession.insert(command.getName(), param));

break;

}

case UPDATE: {

Object param = method.convertArgsToSqlCommandParam(args);

result = rowCountResult(sqlSession.update(command.getName(), param));

break;

}

case DELETE: {

Object param = method.convertArgsToSqlCommandParam(args);

result = rowCountResult(sqlSession.delete(command.getName(), param));

break;

}

case SELECT:

if (method.returnsVoid() && method.hasResultHandler()) {

executeWithResultHandler(sqlSession, args);

result = null;

} else if (method.returnsMany()) {

result = executeForMany(sqlSession, args);

} else if (method.returnsMap()) {

result = executeForMap(sqlSession, args);

} else if (method.returnsCursor()) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。


《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
, args);

} else if (method.returnsMap()) {

result = executeForMap(sqlSession, args);

} else if (method.returnsCursor()) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-sR7Ddf62-1713495538949)]

[外链图片转存中…(img-t19nisLA-1713495538950)]

[外链图片转存中…(img-Ls5GAnMM-1713495538950)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

[外链图片转存中…(img-ywFek33i-1713495538950)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值