自定义一个简单的 ORM 框架

自定义一个简单的 ORM 框架

前面我们通过跟踪源码的方式剖析了Hibernate 和MyBatis 两个框架是如何应用 ORM 思想的,接下来我们自己定义一个简单的 ORM 框架(名为 MiniORM),希望能通过这种方式让大家亲自零距离的去应用一下 ORM。

4.1MiniORM 框架的结构设计

在这里插入图片描述
1.第一层为配置层:
miniORM.cfg.xml 是框架的核心配置文件,主要用来设置数据库连接信息和映射配置文件路径信息
Xxx.mapper.xml 是框架的映射配置文件,主要用来设置类和表之间以及属性和字段之间的映射关系
Xxx.java 是带有映射注解的实体类,主要用来设置类和表之间以及属性和字段之间的映射关系,和 Xxx.mapper.xml 的作用一样,只不过采用的是注解方式,两者二选一

2.第二层为解析层:
Dom4jUtil 类用来解析 miniORM.cfg.xml 和Xxx.mapper.xml 两个配置文件的数据
AnnotationUtil 类用来解析实体类中的映射注解

3.第三层为封装层:
ORMConfig 类用来封装和存储从 miniORM.cfg.xml 文件中解析得到的数据
Mapper 类用来封装和存储从 Xxx.mapper.xml 或实体类中解析得到的映射数据

4.第四层为功能层:
ORMSession 类主要用来从 ORMConfig 和 Mapper 中获取相关数据,然后生成 sql 语句, 最后通过对 JDBC 的封装最终实现增删改查功能

4.2MiniORM 框架的代码实现

1.pom.xml

<groupId>cn.itcast.framework.miniorm</groupId>
<artifactId>MiniORM</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>


<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
<build>
<finalName>MiniORM</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<bootclasspath>${JAVA_HOME}/jre/lib/rt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

MiniORM 框架依赖 dom4j 和 jdk1.8, 编译时会打成 jar 包并 install 到本地仓库中,如下图所示:
在这里插入图片描述
2.miniORM.cfg.xml 是框架的核心配置文件,主要用来设置数据库连接信息和映射配置文件路径信息,源码如下所示:

<?xml version='1.0' encoding='utf-8'?>
<orm-factory>
<!--数据库连接数据-->
<property name="connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="connection.driverClass">com.mysql.jdbc.Driver</property>
<property name="connection.username">root</property>
<property name="connection.password">123</property>


<!--采用 xml 配置映射数据-->
<mapping resource="cn/itcast/orm/test/entity/Book.mapper.xml"/>
<!--采用实体类注解配置映射数据-->
<entity package="cn.itcast.orm.test.entity"/>
</orm-factory>

3.Xxx.mapper.xml 是框架的映射配置文件,主要用来设置类和表之间以及属性和字段之间的映射关系,以Book.mapper.xml 为例,源码如下所示:

<?xml version='1.0' encoding='UTF-8'?>
<!--实体类和表之间的映射关系配置-->
<orm-mapping>
<class name="cn.itcast.orm.test.entity.Book" table="t_book">
<id name="id" column="bid"/>
<property name="name" column="bname"/>
<property name="author" column="author"/>
<property name="price" column="price"/>
</class>
</orm-mapping>

4.当然 MiniORM 框架也支持在实体类上以注解方式去配置映射关系,以 Book.java 为例,源码如下所示:

import cn.itcast.orm.annotation.ORMColumn; import cn.itcast.orm.annotation.ORMId; import cn.itcast.orm.annotation.ORMTable;

//实体类:图书@ORMTable(name = "t_book") public class Book {

@ORMId @ORMColumn(name = "bid") private int id; //主键

@ORMColumn(name="bname") private String name; //图书名字

@ORMColumn(name="author") private String author; //图书作者

@ORMColumn(name="price") private double price; //图书价格
... ...
}

实体类中的@ORMTable、@ORMId、@ORMColumn 是我们自定义的三个注解,@ORMTable 用来设置当前类和哪个表对应,@ORMColumn 用来设置当前属性和表中哪个字段对应, @ORMId 用来设置哪个属性对应的字段是主键。

5.Dom4jUtil 类是一个基于 Dom4j 的工具类, 主要用来解析 miniORM.cfg.xml 和Xxx.mapper.xml,源码如下所示:

public class Dom4jUtil {


/**
*通过文件的路径获取 xml 的 document 对象
*
*@param path 文件的路径
*@return 返回文档对象
*/
public static Document getXMLByFilePath(String path) { if (null == path) {
return null;
}
Document document = null; try {
SAXReader reader = new SAXReader(); document = reader.read(new File(path));
} catch (Exception e) { e.printStackTrace();
}
return document;
}

/**
*获得某文档中某元素内某属性的值和元素的文本信息
*
*@param document	xml 文档对象
*@param elementName 元素名
*@param attrName	属性名
*@return 返回一个 Map 集合
*/
public static Map<String, String> Elements2Map(Document document, String elementName,
String attrName) {
List<Element> propList = document.getRootElement().elements(elementName); Map<String, String> propConfig = new HashMap<>();
for (Element element : propList) {
String key = element.attribute(attrName).getValue(); String value = element.getTextTrim(); propConfig.put(key, value);
}
return propConfig;
}

/**
*针对 mapper.xml 文件,获得映射信息并存到 Map 集合中
*@param document xml 文档对象
*@return 返回一个 Map 集合
*/
public static Map<String, String> Elements2Map(Document document) { Element classElement = document.getRootElement().element("class"); Map<String, String> mapping = new HashMap<>();

Element idElement = classElement.element("id"); String idKey = idElement.attribute("name").getValue();
String idValue = idElement.attribute("column").getValue();
mapping.put(idKey, idValue);

List<Element> propElements = classElement.elements("property"); for (Element element : propElements) {
String propKey = element.attribute("name").getValue(); String propValue = element.attribute("column").getValue(); mapping.put(propKey, propValue);
}
return mapping;
}

/**
*针对 mapper.xml 文件,获得主键的映射信息并存到 Map 集合中
*
*@param document xml 文档对象
*@return 返回一个 Map 集合
*/
public static Map<String, String> ElementsID2Map(Document document) { Element classElement = document.getRootElement().element("class"); Map<String, String> mapping = new HashMap<>();

Element idElement = classElement.element("id"); String idKey = idElement.attribute("name").getValue();
String idValue = idElement.attribute("column").getValue(); mapping.put(idKey, idValue);

return mapping;
}

/**
*获得某文档中某元素内某属性的值
*
*@param document	xml 文档对象
*@param elementName 元素名
*@param attrName	属性名
*@return 返回一个 Set 集合
*/
public static Set<String> Elements2Set(Document document, String elementName, String attrName) { List<Element> mappingList = document.getRootElement().elements(elementName);
Set<String> mappingSet = new HashSet<>(); for (Element element : mappingList) {
String value = element.attribute(attrName).getValue(); mappingSet.add(value);
}
return mappingSet;
}


/**
*获得某文档中某元素内某属性的值
*
*@param document	xml 文档对象
*@param elementName 元素名
*@param attrName	属性名
*@return 返回一个 Set 集合
*/
public static String getPropValue(Document document, String elementName, String attrName) { Element element = (Element) document.getRootElement().elements(elementName).get(0); return element.attribute(attrName).getValue();
}
}

6.AnnotationUtil 类主要用来通过反射技术解析实体类中的注解,从而获得映射数据,源码如下所示:

public class AnnotationUtil {


/*
得到的类名
*/
public static String getClassName(Class clz) { return clz.getName();
}


/*
得到ORMTable 注解中的表名
*/
public static String getTableName(Class clz) {
if (clz.isAnnotationPresent(ORMTable.class)) {
ORMTable ormTable = (ORMTable) clz.getAnnotation(ORMTable.class); return ormTable.name();
} else {
System.out.println("缺少 ORMTable 注解"); return null;
}
}


/*
得到主键属性和对应的字段
*/
public static Map<String, String> getIdMapper(Class clz) { boolean flag = true;
Map<String, String> map = new HashMap<>(); Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ORMId.class)) { flag = false;
String fieldName = field.getName();
if (field.isAnnotationPresent(ORMColumn.class)) {
ORMColumn ormColumn = field.getAnnotation(ORMColumn.class); String columnName = ormColumn.name();
map.put(fieldName, columnName); break;
} else {
System.out.println("缺少 ORMColumn 注解");
}
}
}
if (flag) {
System.out.println("缺少 ORMId 注解");
}
return map;
}

/*
得到类中所有属性和对应的字段
*/
public static Map<String, String> getPropMapping(Class clz) { Map<String, String> map = new HashMap<>(); map.putAll(getIdMapper(clz));
Field[] fields = clz.getDeclaredFields(); for (Field field : fields) {
if (field.isAnnotationPresent(ORMColumn.class)) {
ORMColumn ormColumn = field.getAnnotation(ORMColumn.class); String fieldName = field.getName();
String columnName = ormColumn.name(); map.put(fieldName, columnName);
}
}
return map;
}

/*
获得某包下面的所有类名
*/
public static Set<String> getClassNameByPackage(String packagePath) { Set<String> names = new HashSet<>();
String packageFile = packagePath.replace(".", "/");
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); if (classpath == null) {
classpath = Thread.currentThread().getContextClassLoader().getResource("/").getPath();
}
try {
classpath = java.net.URLDecoder.decode(classpath, "utf-8");
} catch (UnsupportedEncodingException e) { e.printStackTrace();
}
File dir = new File(classpath + packageFile); if (dir.exists()) {
File[] files = dir.listFiles(); for (File f : files) {
String name = f.getName();
if (f.isFile() && name.endsWith(".class")) {
name = packagePath + "." + name.substring(0, name.lastIndexOf(".")); names.add(name);
}
}
} else {
System.out.println("包路径不存在");
}
return names;
}
}

7.Mapper 类用来封装并存储从 Xxx.mapper.xml 中或从实体类中解析得到的映射信息,哪个表和哪个类映射,哪个字段和哪个属性映射等等,源码如下所示:

public class Mapper {
private String className; //类名private String tableName; //表名
private Map<String,String> idMapper=new HashMap();   //主键字段和属性private Map<String,String> propMapping=new HashMap(); //非主键字段和属性
... ...
}

8.ORMConfig 类主要用来存储 miniORM.cfg.xml 配置文件中的信息和 Mapper 映射信息,该类内部会使用 Dom4jUtil、AnnotationUtil 工具类去解析数据,源码如下所示:

public class ORMConfig {
public static String classpath; //类路径public static File cfgFile; //核心配置文件
public static Map<String, String> propConfig; //核心配置文件数据public static Set<String> mappingSet; //映射配置文件
public static Set<String> entitySet; //实体类
public static List<Mapper> mapperList; //解析出来的 Mapper

// 从 classpath 中加载框架的核心配置文件 miniORM.cfg.xml static {
classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); if (classpath == null) {
classpath = Thread.currentThread().getContextClassLoader().getResource("/").getPath();
}
try {
classpath = java.net.URLDecoder.decode(classpath, "utf-8");
} catch (UnsupportedEncodingException e) { e.printStackTrace();
}
cfgFile = new File(classpath + "miniORM.cfg.xml"); if (cfgFile.exists()) {
Document document = Dom4jUtil.getXMLByFilePath(cfgFile.getPath()); propConfig = Dom4jUtil.Elements2Map(document, "property", "name"); mappingSet = Dom4jUtil.Elements2Set(document, "mapping", "resource"); entitySet = Dom4jUtil.Elements2Set(document, "entity", "package");
} else {
cfgFile = null;
System.out.println("未找到核心配置文件 miniORM.cfg.xml");
}
}

//从 propConfig 获得信息,连接数据库
private Connection getConnection() throws ClassNotFoundException, SQLException { String url = propConfig.get("connection.url");
String driverClass = propConfig.get("connection.driverClass"); String username = propConfig.get("connection.username"); String password = propConfig.get("connection.password"); Class.forName(driverClass);
Connection connection = DriverManager.getConnection(url, username, password); connection.setAutoCommit(true);
return connection;
}

//从 mappingSet 中挨个解析 mapper.xml 配置文件,获得实体类和表之间的映射信息
//从 entitySet 中挨个解析实体类中的注解,获得实体类和表之间的映射信息
private void getMapping() throws ClassNotFoundException { mapperList = new ArrayList<>();
for (String xmlPath : mappingSet) {
Document document = Dom4jUtil.getXMLByFilePath(classpath + xmlPath);

Map<String, String> mapping = Dom4jUtil.Elements2Map(document);

String className = Dom4jUtil.getPropValue(document, "class", "name"); String tableName = Dom4jUtil.getPropValue(document, "class", "table"); Map<String, String> id_id = Dom4jUtil.ElementsID2Map(document);

Mapper mapper = new Mapper(); mapper.setClassName(className); mapper.setTableName(tableName); mapper.setIdMapper(id_id); mapper.sePropMapping(mapping);

mapperList.add(mapper);
}

for (String packagePath : entitySet) {
Set<String> nameSet = AnnotationUtil.getClassNameByPackage(packagePath); for (String name : nameSet) {
Class clz = Class.forName(name);
String className = AnnotationUtil.getClassName(clz); String tableName = AnnotationUtil.getTableName(clz);
Map<String, String> id_id = AnnotationUtil.getIdMapper(clz); Map<String, String> mapping = AnnotationUtil.getPropMapping(clz); Mapper mapper = new Mapper(); mapper.setClassName(className); mapper.setTableName(tableName);
mapper.setIdMapper(id_id); mapper.sePropMapping(mapping);

mapperList.add(mapper);
}
}
}

public ORMSession buildORMSession() throws Exception {
//从 propConfig 获得信息,连接数据库
Connection connection = getConnection();

//从 mappingSet 中挨个解析 mapper.xml 配置文件,获得实体类和表之间的映射信息
getMapping();


//创建 ORMSession 对象
return new ORMSession(connection);
}
}

9.ORMSession 类主要用来从 ORMConfig 和 Mapper 中获取相关数据,然后生成 sql 语句,最后通过对 JDBC 的封装最终实现增删改查功能,源码如下所示:

public class ORMSession {


private Connection connection;


public ORMSession(Connection conn) { this.connection = conn;
}


//保存数据
public void save(Object entity) throws Exception { String insertSQL = "";

//1.  从 ORMConfig 中获得保存有映射信息的集合
List<Mapper> mapperList = ORMConfig.mapperList;


//2. 遍历集合,从集合中找到和 entity 参数相对应的 mapper 对象
for (Mapper mapper : mapperList) {
if (mapper.getClassName().equals(entity.getClass().getName())) { String tableName = mapper.getTableName();
String insertSQL1 = "insert into " + tableName + "( "; String insertSQL2 = " ) values ( ";

//3. 得到当前对象所属类中的所有属性
Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) {
field.setAccessible(true);
//4. 遍历过程中根据属性得到字段名
String columnName = mapper.getPropMapper().get(field.getName());
//5. 遍历过程中根据属性得到它的值
String columnValue = field.get(entity).toString();
//6. 拼接 sql 语句
insertSQL1 += columnName + ","; insertSQL2 += "'" + columnValue + "',";
}
insertSQL = insertSQL1.substring(0, insertSQL1.length() - 1) + insertSQL2.substring(0,
insertSQL2.length() - 1) + " )";
break;
}
}

// 把 sql 语句打印到控制台
System.out.println("MiniORM-save: " + insertSQL);

//7. 通过 JDBC 发送并执行 sql
PreparedStatement statement = connection.prepareStatement(insertSQL); statement.executeUpdate();
statement.close();
}

//根据主键进行数据删除 delete from 表名  where 主键 = 值
public void delete(Object entity) throws Exception { String delSQL = "delete from ";

//1.  从 ORMConfig 中获得保存有映射信息的集合
List<Mapper> mapperList = ORMConfig.mapperList;

//2. 遍历集合,从集合中找到和 entity 参数相对应的 mapper 对象
for (Mapper mapper : mapperList) {
if (mapper.getClassName().equals(entity.getClass().getName())) {
// 3. 得到我们想要的 mapper 对象,并得到表名String tableName = mapper.getTableName(); delSQL += tableName + " where ";
// 4. 得到主键的字段名和属性名
Object[] idProp = mapper.getIdMapper().keySet().toArray(); //idProp[0] Object[] idColumn = mapper.getIdMapper().values().toArray(); //idColumn[0]

// 5. 得到主键的值
Field field = entity.getClass().getDeclaredField(idProp[0].toString()); field.setAccessible(true);
String idVal = field.get(entity).toString();

// 6. 拼接 sql
delSQL += idColumn[0].toString() + " = " + idVal;
// 把 sql 语句打印到控制台
System.out.println("MiniORM-delete: " + delSQL);

break;
}
}
//7. 通过 JDBC 发送并执行 sql
PreparedStatement statement = connection.prepareStatement(delSQL); statement.executeUpdate();
statement.close();
}

// 根据主键进行查询 select * from 表名 where 主键字段 = 值
public Object findOne(Class clz, Object id) throws Exception{

String querySQL = "select * from ";

//1. 从 ORMConfig 中得到存有映射信息的集合
List<Mapper> mapperList=ORMConfig.mapperList;

//2. 遍历集合拿到我们想要的 mapper 对象
for (Mapper mapper : mapperList) {
if (mapper.getClassName().equals(clz.getName())) {

// 3. 获得表名
String tableName = mapper.getTableName();

//4. 获得主键字段名
Object[] idColumn = mapper.getIdMapper().values().toArray(); //idColumn[0]

//5. 拼接 sql
querySQL += tableName + " where " + idColumn[0].toString() + " = " + id;

break;
}
}
System.out.println("MiniORM-findOne:" +querySQL);

//6. 通过 jdbc 发送并执行 sql, 得到结果集
PreparedStatement statement=connection.prepareStatement(querySQL); ResultSet rs=statement.executeQuery();

//7. 封装结果集,返回对象
if(rs.next()){
// 查询到一行数据
// 8.创建一个对象,目前属性的值都是初始值Object obj=clz.newInstance();
// 9. 遍历 mapperList 集合找到我们想要的 mapper 对象
for(Mapper mapper:mapperList){
if (mapper.getClassName().equals(clz.getName())) {
//10. 得到存有属性-字段的映射信息
Map<String,String> propMap = mapper.getPropMapper();
//11. 遍历集合分别拿到属性名和字段名Set<String> keySet = propMap.keySet(); for(String prop:keySet){ //prop 就是属性名
String column = propMap.get(prop);	//column 就是和属性对应的字段名
Field field = clz.getDeclaredField(prop); field.setAccessible(true); field.set(obj,rs.getObject(column));
}
break;
}
}
//12. 释放资源statement.close(); rs.close();

//13. 返回查询出来的对象
return obj;
}else {
// 没有查到数据
return null;
}
}


//关闭连接,释放资源
public void close() throws Exception{ if(connection!=null){
connection.close(); connection = null;
}
}
}

4.3MiniORM 框架的测试使用

我们自定义的 MiniORM 框架主要用来体现 ORM 思想,并不是为了开发一个成熟的持久层框架出来,因此很多逻辑并不完善,很多情况也未去考虑,请各位理解。接下来我们就测试一下该框架。
1.pom.xml

<groupId>cn.itcast.orm</groupId>
<artifactId>TestMiniORM</artifactId>
<version>1.0-SNAPSHOT</version>


<dependencies>
<dependency>
<groupId>cn.itcast.framework.miniorm</groupId>
<artifactId>MiniORM</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.36</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes> <!--为了编译时能加载包中的 xml 文件-->
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource> <!--为了编译时能加载 resources 中的 xml 文件-->
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>

我们前面把 MiniORM 框架打成 jar 包并 install 到了本地 Maven 仓库中,因此在使用该框架时需要从本地仓库进行加载。

2.miniORM.cfg.xml

<?xml version='1.0' encoding='utf-8'?>
<orm-factory>
<!--数据库连接-->
<property name="connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="connection.driverClass">com.mysql.jdbc.Driver</property>
<property name="connection.username">root</property>
<property name="connection.password">123</property>


<!--采用 xml 配置映射数据-->
<mapping resource="cn/itcast/orm/test/entity/Book.mapper.xml"/>


<!--采用注解配置映射数据-->
<entity package="cn.itcast.orm.test.entity"/>
</orm-factory>

这是框架的核心配置文件,我们既采用了 xml 方式配置映射数据,也采用了注解方式在实体
类中配置映射数据,最后可以二选一分别进行功能测试。

3.实体类和映射配置文件

@ORMTable(name = "t_book") public class Book {

@ORMId @ORMColumn(name = "bid") private int id; //主键

@ORMColumn(name="bname") private String name; //图书名字

@ORMColumn(name="author") private String author; //图书作者

@ORMColumn(name="price") private double price; //图书价格
... ...
}
<?xml version='1.0' encoding='UTF-8'?>
<!--实体类和表之间的映射关系配置-->
<orm-mapping>
<class name="cn.itcast.orm.test.entity.Book" table="t_book">
<id name="id" column="bid"/>
<property name="name" column="bname"/>
<property name="author" column="author"/>
<property name="price" column="price"/>
</class>
</orm-mapping>

注意:对于同一个表或实体类,不需要既进行 xml 配置,又进行注解配置,二选一即可,这

里同时进行配置只是为了测试方便。

测试类

public class BookDao {
private ORMConfig config; @Before
public void init() {
config = new ORMConfig();
}


@Test
public void testSave() throws Exception {
ORMSession session = config.buildORMSession(); Book book = new Book();
book.setId(1);
book.setName("降龙十八掌"); book.setAuthor("不知道"); book.setPrice(9.9); session.save(book); session.close();
}


@Test
public void testDelete() throws Exception { ORMSession session = config.buildORMSession(); Book book = new Book();
book.setId(1); session.delete(book); session.close();
}


@Test
public void testFindOne() throws Exception { ORMSession session = config.buildORMSession(); Book book = (Book) session.findOne(Book.class, 1); System.out.println(book);
session.close();
}

我们调用 ORMSession 类的 save 方法完成了数据保存功能,由 MiniORM 框架生成 sql语句,运行效果如下图所示:
在这里插入图片描述
我们调用 ORMSession 类的 findOne 方法完成了数据查询功能,由 MiniORM 框架生成sql语句,运行效果如下图所示:
在这里插入图片描述
我们调用 ORMSession 类的 delete 方法完成了数据删除功能,由 MiniORM 框架生成 sql语句,运行效果如下图所示:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值