mybatis和ehcache整合

<?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命名空间,为了对sql语句进行隔离,方便管理 ,mapper开发dao方式,使用namespace有特殊作用
mapper代理开发时将namespace指定为mapper接口的全限定名
 -->
<mapper namespace="cn.learn.mybatis.mapper.UserMapper">
<!-- 在mapper.xml文件中配置很多的sql语句,执行每个sql语句时,封装为MappedStatement对象
mapper.xml以statement为单位管理sql语句
 -->
 	<!-- 开启二级缓存 -->
 	<!-- 单位:毫秒 -->
 	<cache type="org.mybatis.caches.ehcache.EhcacheCache">
 		<property name="timeToIdleSeconds" value="12000"/>
        <property name="timeToLiveSeconds" value="3600"/>
        <!-- 同ehcache参数maxElementsInMemory -->
		<property name="maxEntriesLocalHeap" value="1000"/>
		<!-- 同ehcache参数maxElementsOnDisk -->
        <property name="maxEntriesLocalDisk" value="10000000"/>
        <property name="memoryStoreEvictionPolicy" value="LRU"/>
 	</cache>
 	<!-- 将用户查询条件定义为sql片段
 	建议对单表的查询条件单独抽取sql片段,提高公用性
 	注意:不要将where标签放在sql片段
 	  -->
 	<sql id="query_user_where">
 			<!-- 如果 userQueryVo中传入查询条件,再进行sql拼接-->
			<!-- test中userCustom.username表示从userQueryVo读取属性值-->
			<if test="userCustom!=null">
				<if test="userCustom.username!=null and userCustom.username!=''">
					and username like '%${userCustom.username}%'
				</if>
				<if test="userCustom.sex!=null and userCustom.sex!=''">
					and sex = #{userCustom.sex}
				</if>
				
				<!-- 还有很的查询条件 -->
			</if>
			<if test="ids!=null">
				<!-- 根据id集合查询用户信息 -->
				<!-- 最终拼接的效果:
				SELECT id ,username ,birthday  FROM USER WHERE username LIKE '%小明%' AND id IN (16,22,25)
				collection:集合的属性
				open:开始循环拼接的串
				close:结束循环拼接的串
				item:每次循环取到的对象
				separator:每两次循环中间拼接的串
				 -->
				 <foreach collection="ids" open=" AND id IN ( " close=")" item="id" separator=",">
				 	#{id}
				 </foreach>
				 <!-- 
				 SELECT id ,username ,birthday  FROM USER WHERE username LIKE '%小明%' AND (id = 16 OR id = 22 OR id = 25) 
				  <foreach collection="ids" open=" AND ( " close=")" item="id" separator="OR">
				 	id = #{id}
				 </foreach>
				  -->
			</if>
 	</sql>
 
 	<!-- 定义resultMap,列名和属性名映射配置
 	id:mapper.xml中的唯一标识 
 	type:最终要映射的pojo类型
 	 -->
 	<resultMap id="userListResultMap" type="user" >
 		<!-- 列名
 		id_,username_,birthday_
 		id:要映射结果集的唯 一标识 ,称为主键
 		column:结果集的列名
 		property:type指定的哪个属性中
 		 -->
 		 <id column="id_" property="id"/>
 		 <!-- result就是普通列的映射配置 -->
 		 <result column="username_" property="username"/>
 		 <result column="birthday_" property="birthday"/>
 	
 	</resultMap>

	<!-- 根据id查询用户信息 -->
	<!-- 
		id:唯一标识 一个statement
		#{}:表示 一个占位符,如果#{}中传入简单类型的参数,#{}中的名称随意
		parameterType:输入 参数的类型,通过#{}接收parameterType输入 的参数
		resultType:输出结果 类型,不管返回是多条还是单条,指定单条记录映射的pojo类型
	 -->
	<select id="findUserById" parameterType="int" resultType="user" >
		SELECT * FROM USER WHERE id= #{id}
	
	</select>
	
	<!-- 根据用户名称查询用户信息,可能返回多条
	${}:表示sql的拼接,通过${}接收参数,将参数的内容不加任何修饰拼接在sql中。
	
	 -->
	<select id="findUserByName" parameterType="java.lang.String" resultType="cn.learn.mybatis.po.User">
		select * from user where username like '%${value}%'
	</select>
	
	<!-- 自定义查询条件查询用户的信息
	parameterType:指定包装类型
	%${userCustom.username}%:userCustom是userQueryVo中的属性,通过OGNL获取属性的值
	 -->
	<select id="findUserList" parameterType="userQueryVo" resultType="user">
	
		select id,username,birthday from user
		<!-- where标签相当 于where关键字,可以自动去除第一个and -->
		<where>
			<!-- 引用sql片段,如果sql片段和引用处不在同一个mapper必须前边加namespace -->
			<include refid="query_user_where"></include>
			<!-- 下边还有很其它的条件 -->
			<!-- <include refid="其它的sql片段"></include> -->
		</where>
		
		
		
	</select>
	
	<!-- 使用resultMap作结果映射
	resultMap:如果引用resultMap的位置和resultMap的定义在同一个mapper.xml,
	直接使用resultMap的id,如果不在同一个mapper.xml要在resultMap的id前边加namespace
	
	 -->
	<select id="findUserListResultMap" parameterType="userQueryVo" resultMap="userListResultMap">
	
		select id id_,username username_,birthday birthday_ from user where username like '%${userCustom.username}%'
	</select>
	
	<!-- 输出简单类型
	功能:自定义查询条件,返回查询记录个数,通常用于实现 查询分页
	 -->
	 <select id="findUserCount" parameterType="userQueryVo" resultType="int">
	 	select count(*) from user 
		<!-- where标签相当 于where关键字,可以自动去除第一个and -->
		<where>
			<!-- 引用sql片段,如果sql片段和引用处不在同一个mapper必须前边加namespace -->
			<include refid="query_user_where"></include>
			<!-- 下边还有很其它的条件 -->
			<!-- <include refid="其它的sql片段"></include> -->
		</where>
	 </select>
	
	<!-- 添加用户
	parameterType:输入 参数的类型,User对象 包括 username,birthday,sex,address
	#{}接收pojo数据,可以使用OGNL解析出pojo的属性值
	#{username}表示从parameterType中获取pojo的属性值
	selectKey:用于进行主键返回,定义了获取主键值的sql
	order:设置selectKey中sql执行的顺序,相对于insert语句来说
	keyProperty:将主键值设置到哪个属性
	resultType:select LAST_INSERT_ID()的结果 类型
	
	 -->
	<insert id="insertUser" parameterType="cn.learn.mybatis.po.User" >
		<selectKey keyProperty="id" order="AFTER" resultType="int">
			select LAST_INSERT_ID()
		</selectKey>
		
		INSERT INTO USER(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})
	</insert>
	
	<!-- mysql的uuid生成主键 -->
	<!-- <insert id="insertUser" parameterType="cn.learn.mybatis.po.User">
		<selectKey keyProperty="id" order="BEFORE" resultType="string">
			select uuid()
		</selectKey>
		
		INSERT INTO USER(id,username,birthday,sex,address) VALUES(#{id},#{username},#{birthday},#{sex},#{address})
	</insert> -->
	
	<!-- oracle
	在执行insert之前执行select 序列.nextval() from dual取出序列最大值,将值设置到user对象 的id属性
	 -->
	<!-- <insert id="insertUser" parameterType="cn.learn.mybatis.po.User">
		<selectKey keyProperty="id" order="BEFORE" resultType="int">
			select 序列.nextval() from dual
		</selectKey>
		
		INSERT INTO USER(id,username,birthday,sex,address) VALUES(#{id},#{username},#{birthday},#{sex},#{address})
	</insert> -->
	
	<!-- 用户删除  -->
	<delete id="deleteUser" parameterType="int">
	 delete from user where id=#{id}
	</delete>
	<!-- 用户更新 
	要求:传入的user对象中包括 id属性值
	-->
	<update id="updateUser" parameterType="cn.learn.mybatis.po.User" flushCache="false">
		update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id=#{id}
	</update>

</mapper>

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
	<!--diskStore:缓存数据持久化的目录 地址  -->
	<diskStore path="F:\develop\ehcache" />
	<defaultCache 
		maxElementsInMemory="1000" 
		maxElementsOnDisk="10000000"
		eternal="false" 
		overflowToDisk="false" 
		diskPersistent="true"
		timeToIdleSeconds="120"
		timeToLiveSeconds="120" 
		diskExpiryThreadIntervalSeconds="120"
		memoryStoreEvictionPolicy="LRU">
	</defaultCache>
</ehcache>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

	<!-- 属性定义 加载一个properties文件 在 properties标签 中配置属性值 -->
	<properties resource="db.properties">
		<!-- <property name="" value=""/> -->
	</properties>

	<!-- 全局配置参数 -->
	<settings>
		<!-- 延迟加载总开关 -->
		<setting name="lazyLoadingEnabled" value="true" />	
		<!-- 设置按需加载 -->
		<setting name="aggressiveLazyLoading" value="false" />
		<!-- 开启二级缓存 -->
		<setting name="cacheEnabled" value="true"/>
	</settings>


	<!-- 定义 别名 -->
	<typeAliases>
		<!-- 单个别名的定义 alias:别名,type:别名映射的类型 -->
		<!-- <typeAlias type="cn.learn.mybatis.po.User" alias="user"/> -->
		<!-- 批量别名定义 指定包路径,自动扫描包下边的pojo,定义别名,别名默认为类名(首字母小写或大写) -->
		<package name="cn.learn.mybatis.po" />

	</typeAliases>

	<!-- 和spring整合后 environments配置将废除 -->
	<environments default="development">
		<environment id="development">
			<!-- 使用jdbc事务管理 -->
			<transactionManager type="JDBC" />
			<!-- 数据库连接池 -->
			<dataSource type="POOLED">
				<property name="driver" value="${jdbc.driver}" />
				<property name="url" value="${jdbc.url}" />
				<property name="username" value="${jdbc.username}" />
				<property name="password" value="${jdbc.password}" />
			</dataSource>
		</environment>
	</environments>

	<!--加载mapper映射 如果将和spring整合后,可以使用整合包中提供的mapper扫描器,此处的mappers不用配置了。 -->
	<mappers>
		<!-- 通过resource引用mapper的映射文件 -->
		<mapper resource="sqlmap/User.xml" />
		<!-- <mapper resource="mapper/UserMapper.xml" /> -->
		<!-- 通过class引用mapper接口 class:配置mapper接口全限定名 要求:需要mapper.xml和mapper.java同名并且在一个目录 
			中 -->
		<!-- <mapper class="cn.learn.mybatis.mapper.UserMapper"/> -->
		<!-- 批量mapper配置 通过package进行自动扫描包下边的mapper接口, 要求:需要mapper.xml和mapper.java同名并且在一个目录 
			中 -->
		<package name="cn.learn.mybatis.mapper" />


	</mappers>


</configuration>
package cn.learn.mybatis.mapper;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import cn.learn.mybatis.po.OrderCustom;
import cn.learn.mybatis.po.Orders;
import cn.learn.mybatis.po.User;

public class CacheTest {

	// 会话工厂
	private SqlSessionFactory sqlSessionFactory;

	// 创建工厂
	@Before
	public void init() throws IOException {

		// 配置文件(SqlMapConfig.xml)
		String resource = "SqlMapConfig.xml";

		// 加载配置文件到输入 流
		InputStream inputStream = Resources.getResourceAsStream(resource);

		// 创建会话工厂
		sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

	}
	//一级缓存
	@Test
	public void testCache1() throws Exception {

		SqlSession sqlSession = sqlSessionFactory.openSession();
		UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
		
		//第一次查询用户id为1的用户
		User user = userMapper.findUserById(1);
		System.out.println(user);
		
		//中间修改用户要清空缓存,目的防止查询出脏数据
		/*user.setUsername("测试用户2");
		userMapper.updateUser(user);
		sqlSession.commit();*/
		
		//第二次查询用户id为1的用户
		User user2 = userMapper.findUserById(1);
		System.out.println(user2);
		
		sqlSession.close();
		

	}
	
	//二级缓存的测试
	@Test
	public void testCache2() throws Exception {

		SqlSession sqlSession1 = sqlSessionFactory.openSession();
		SqlSession sqlSession2 = sqlSessionFactory.openSession();
		SqlSession sqlSession3 = sqlSessionFactory.openSession();
		UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
		UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
		UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
		
		//第一次查询用户id为1的用户
		User user = userMapper1.findUserById(1);
		System.out.println(user);
		sqlSession1.close();
		
		//中间修改用户要清空缓存,目的防止查询出脏数据
		/*user.setUsername("测试用户2");
		userMapper3.updateUser(user);
		sqlSession3.commit();
		sqlSession3.close();*/
		
		
		//第二次查询用户id为1的用户
		User user2 = userMapper2.findUserById(1);
		System.out.println(user2);
		
		sqlSession2.close();
		

	}

	
}
/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

/**
 * SPI for cache providers.
 * 
 * One instance of cache will be created for each namespace.
 * 
 * The cache implementation must have a constructor that receives the cache id as an String parameter.
 * 
 * MyBatis will pass the namespace as id to the constructor.
 * 
 * <pre>
 * public MyCache(final String id) {
 *  if (id == null) {
 *    throw new IllegalArgumentException("Cache instances require an ID");
 *  }
 *  this.id = id;
 *  initialize();
 * }
 * </pre>
 *
 * @author Clinton Begin
 */

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key Can be any object but usually it is a {@link CacheKey}
   * @param value The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * As of 3.3.0 this method is only called during a rollback 
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that 
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null 
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be 
   * available instead of hitting the database.
   *
   * 
   * @param key The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance
   */  
  void clear();

  /**
   * Optional. This method is not called by the core.
   * 
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();
  
  /** 
   * Optional. As of 3.2.6 this method is no longer called by the core.
   *  
   * Any locking needed by the cache must be provided internally by the cache provider.
   * 
   * @return A ReadWriteLock 
   */
  ReadWriteLock getReadWriteLock();

}
/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}
/*
 *    Copyright 2010 The MyBatis Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.caches.ehcache;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
 * Cache adapter for Ehcache.
 *
 * @version $Id: EhcacheCache.java 3454 2010-12-29 20:35:44Z simone.tripodi $
 */
public final class EhcacheCache implements Cache {

    /**
     * The cache manager reference.
     */
    private static final CacheManager CACHE_MANAGER = createCacheManager();

    /**
     * Looks for "/ehcache.xml" classpath resource and builds the relative
     * {@code CacheManager}; if it's no found or it is impossible to load it,
     * returns the default manager.
     *
     * @return the application cache manager.
     */
    private static CacheManager createCacheManager() {
        CacheManager cacheManager;
        InputStream input = EhcacheCache.class.getResourceAsStream("/ehcache.xml");

        if (input != null) {
            try {
                cacheManager = CacheManager.create(input);
            } catch (Throwable t) {
                cacheManager = CacheManager.create();
            } finally {
                try {
                    input.close();
                } catch (IOException e) {
                }
            }
        } else {
            cacheManager = CacheManager.create();
        }

        return cacheManager;
    }

    /**
     * The {@code ReadWriteLock}.
     */
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    /**
     * The cache id.
     */
    private final String id;

    /**
     *
     *
     * @param id
     */
    public EhcacheCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
        if (!CACHE_MANAGER.cacheExists(this.id)) {
            CACHE_MANAGER.addCache(this.id);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void clear() {
        this.getCache().removeAll();
    }

    /**
     * {@inheritDoc}
     */
    public String getId() {
        return this.id;
    }

    /**
     * {@inheritDoc}
     */
    public Object getObject(Object key) {
        try {
            Element cachedElement = this.getCache().get(key.hashCode());
            if (cachedElement == null) {
                return null;
            }
            return cachedElement.getObjectValue();
        } catch (Throwable t) {
            throw new CacheException(t);
        }
    }

    /**
     * {@inheritDoc}
     */
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }

    /**
     * {@inheritDoc}
     */
    public int getSize() {
        try {
            return this.getCache().getSize();
        } catch (Throwable t) {
            throw new CacheException(t);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void putObject(Object key, Object value) {
        try {
            this.getCache().put(new Element(key.hashCode(), value));
        } catch (Throwable t) {
            throw new CacheException(t);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Object removeObject(Object key) {
        try {
            Object obj = this.getObject(key);
            this.getCache().remove(key.hashCode());
            return obj;
        } catch (Throwable t) {
            throw new CacheException(t);
        }
    }

    /**
     * Returns the ehcache manager for this cache.
     *
     * @return the ehcache manager for this cache.
     */
    private Ehcache getCache() {
        return CACHE_MANAGER.getCache(this.id);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof Cache)) {
            return false;
        }

        Cache otherCache = (Cache) obj;
        return this.id.equals(otherCache.getId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return this.id.hashCode();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "EHCache {"
                + this.id
                + "}";
    }

}
/*
 *    Copyright 2010 The MyBatis Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.caches.ehcache;

import org.apache.ibatis.cache.decorators.LoggingCache;

/**
 * {@code LoggingCache} adapter for Ehcache.
 *
 * @version $Id: LoggingEhcache.java 3454 2010-12-29 20:35:44Z simone.tripodi $
 */
public final class LoggingEhcache extends LoggingCache {

    public LoggingEhcache(final String id) {
        super(new EhcacheCache(id));
    }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值