【4】自己写数据库函数库 — 获取一条记录

函数db_fetch为用户接口,根据传入的键值寻找对应的数据,如果找到,则返回指向数据的指针,否则返回NULL,代码如下:

/* 根据给定的键读取一条记录 */
char *db_fetch(DBHANDLE h, const char *key)
{
	DB   *db = h;
	char *ptr;

	/* 参数三:0表示读,1表示写,根据此函数决定加什么锁 */
	if (_db_find_and_lock(db, key, 0) < 0)
	{
		/* 返回-1表示未找到记录 */
		ptr = NULL;
		db->cnt_fetcherr++;
	}
	else	/* 返回0表示查找成功 */
	{
		ptr = _db_readdat(db);	/* 返回找到的数据 */
		db->cnt_fetchok++;
	}

	if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	return ptr;
}

此函数的最后用到了un_lock宏函数来解锁,因为在_db_find_and_lock函数中获得了一个锁。db_fetch函数的大致流程是:根据传入的key调用_db_find_and_lock查找对应的索引记录,如果找到,则继续调用_db_readdat读取对应的数据记录,未找到,则不会调用_db_readdat函数,而是直接返回NULL。 所以,用户在调用db_fetch时一定要判断返回值。我在刚开使用时由于未判断返回值而直接打印返回的指针,结果导致在未找到的情况下出现Segmentation fault。


函数_db_find_and_lock是关键所在,下面是它的代码:

/* writelock:0表示读,1表示写
 * 返回值:0查找成功,-1查找失败
 */
static int _db_find_and_lock(DB *db, const char *key, int writelock)
{
	off_t offset, nextoffset;

	/* 根据散列函数定位桶的起始字符,这里要跳过第一个空闲链表 */
	db->chainoff = (_db_hash(db, key) * PTR_SZ) + db->hashoff;
	db->ptroff   = db->chainoff;	/* 同样指向某个桶 */

	if (writelock)	/* 写锁 */
	{
		/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
		if (writew_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
		{
			printf("%d\n", __LINE__);
			exit(-1);
		}
	}
	else	/* 读锁 */
	{
		/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
		if (readw_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
		{
			printf("%d\n", __LINE__);
			exit(-1);
		}
	}

	/* 知道偏移量,知道指针宽度,atol将指针转换成数字类型 */
	offset = _db_readptr(db, db->ptroff);		/* 直接读取桶中的元素,即第一个节点的偏移量 */

	/* 沿着桶向下查找 */
	while (offset != 0)							/* 如果为0则该散列链为空 */
	{
		nextoffset = _db_readidx(db, offset);	/* 读取一条索引记录存入db->idxbuf */

		/* db->idxbuf = 键值 '\0' 数据记录偏移量 '\0' 数据记录长度 '\0' */

		if (strcmp(db->idxbuf, key) == 0)
			break;
		db->ptroff = offset;		/* ptroff记录前一索引记录的地址 */
		offset = nextoffset;
	}

	return (offset == 0 ? -1 : 0);	/* offset = 0则没有找到记录 */
}

_db_find_and_lock函数根据传入的key计算出散列值,然后定位到散列表中的某个链表,从链表头向后遍历每条记录,直到找出某条记录保存的key和传入的key相等或到链表结尾为止。散列函数如下:

/* 散列函数 */
static DBHASH _db_hash(DB *db, const char *key)
{
	DBHASH hval = 0;
	char c;
	int i;

	/* 以下为散列函数 */
	for (i = 1; (c = *key++) != 0; i++)
		hval += c * i;
	return (hval % db->nhash);	/* db->nhash = 137 */
}

下面介绍_db_find_and_lock调用到的相关函数。首先是_db_readptr,代码如下:

/* 读取一个指针并转换为数值型,凡是要获取指针值都要调用这个函数
 * 根据offset知道指针所在位置,指针所占字节宽度也已知,就能读取一个指针的值了
 * 注意:此函数未进行加锁操作,需要手动进行
 */
static off_t _db_readptr(DB *db, off_t offset)
{
	char asciiptr[PTR_SZ + 1];

	if (lseek(db->idxfd, offset, SEEK_SET) == -1)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (read(db->idxfd, asciiptr, PTR_SZ) != PTR_SZ)	/* PTR_SZ = 6,一次读6个字符 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	asciiptr[PTR_SZ] = 0;	/* 追加一个空字符,为了atol函数辨识结尾处 */

	/* atol会跳过前面的空格,遇到非数字或字符串结束时停止转换 */
	return (atol(asciiptr));
}

由于所有的记录都是以字符的形式保存在文件中的,所以如果想要获得指针值,则必须将字符串类型转换成数值类型,上述函数就是这个作用。根据偏移量,就能知道指针所在位置,然后读取这个字符串,把字符串转换成数值类型,然后返回。


接下来是_db_readidx函数:

/* 根据索引记录的偏移值offset读取一条索引记录,填充DB结构许多成员
 * 返回值:下一条记录的偏移量
 */
static off_t _db_readidx(DB *db, off_t offset)
{
	ssize_t i;
	char *ptr1, *ptr2;
	char asciiptr[PTR_SZ + 1], asciilen[IDXLEN_SZ + 1];
	struct iovec iov[2];

	/* 保存当前索引记录偏移量
	 * 如果offset为0,表示从当前偏移量处读
	 */
	if ((db->idxoff = lseek(db->idxfd, offset, offset == 0 ? SEEK_CUR : SEEK_SET)) == -1)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 散布读,第一部分放asciiptr中,第二部分放asciilen中 */
	iov[0].iov_base = asciiptr;
	iov[0].iov_len  = PTR_SZ;		/* 6 */
	iov[1].iov_base = asciilen;
	iov[1].iov_len  = IDXLEN_SZ;	/* 4 */
	if ((i = readv(db->idxfd, &iov[0], 2)) != PTR_SZ + IDXLEN_SZ)
	{
		/* 返回读取的字节数,结尾返回-1 */
		if (i == 0 && offset == 0)
			return -1;	/* 这个返回值是给函数db_nextrec使用的,_db_find_and_lock永远不可能返回-1 */
		else
		{
			printf("%d\n", __LINE__);
			exit(-1);
		}
	}

	/* 下一条索引记录偏移量 */
	asciiptr[PTR_SZ] = 0;
	db->ptrval = atol(asciiptr);

	/* 当前索引记录长度 */
	asciilen[IDXLEN_SZ] = 0;
	if ((db->idxlen = atoi(asciilen)) < IDXLEN_MIN || db->idxlen > IDXLEN_MAX)
	{
		/* 索引记录长度必须在6~1024字节之间 */
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 下面读取实际的索引记录,文件指针已经在调用readv后指向正确位置,即key开头 */
	if ((i = read(db->idxfd, db->idxbuf, db->idxlen)) != db->idxlen)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (db->idxbuf[db->idxlen -1] != NEWLINE)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	db->idxbuf[db->idxlen - 1] = 0;	/* 把换行符替换为空字符,为了atol函数做准备 */

	/* 找出分隔符 */
	if ((ptr1 = strchr(db->idxbuf, SEP)) == NULL)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	*ptr1++ = 0;	/* 分隔符替换为空字符,ptr1现在指向数据记录的偏移量 */

	if ((ptr2 = strchr(ptr1, SEP)) == NULL)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	*ptr2++ = 0;	/* 分隔符替换为空字符,ptr2现在指向数据记录的长度 */

	if (strchr(ptr2, SEP) != NULL)	/* 只有两个分隔符 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 保存实际数据的偏移量 */
	if ((db->datoff = atol(ptr1)) < 0)	/* atol遇到空字符结束 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 保存实际数据的长度,范围必须在2~1024字节之间 */
	if ((db->datlen = atol(ptr2)) <= 0 || db->datlen > DATLEN_MAX)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	return (db->ptrval);
}

由_db_readptr知道索引记录偏移量,在调用_db_readidx函数从文件中读取一条索引记录到一个buffer中,这个buffer实际上就是db->idxbuf。在调用完这个函数后,DB结构体中的许多成员都记录了某条记录的信息,例如数据偏移量,数据长度,索引记录长度等。知道了数据偏移量和数据长度,就可以读取数据文件中实际的数据了,此即函数_db_readdat的职责:

/* 读取实际数据到缓存db->datbuf中
 * 返回值:NULL结尾的实际数据
 */
static char* _db_readdat(DB *db)
{
	if (lseek(db->datfd, db->datoff, SEEK_SET) == -1)			/* 定位 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (read(db->datfd, db->datbuf, db->datlen) != db->datlen)	/* 读入缓存 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (db->datbuf[db->datlen - 1] != NEWLINE)	/* 数据文件中的一个条必以换行符结尾 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	db->datbuf[db->datlen - 1] = 0;				/* 换行符替换成空字符 */
	return (db->datbuf);
}

由于存放在文件中时,每条数据记录都是以换行符结尾,此函数读取一条数据记录后,把换行符修改为结束符,这方便了用户代码直接打印返回的数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ARCH4系统开发指南 1 ARCH4开发小组 1 1 前言 7 1.1 文档编目的 7 1.2 文档适用范围 7 1.3 修订历史 8 1.4 参考资料 8 2 常用功能处理方式 9 2.1 如何执行条件查询 9 2.1.1 查询示例 9 2.1.2 查询限制 9 2.1.2.1 Like 9 2.1.2.2 In 10 2.1.2.3 Between 10 2.1.2.4 Equal 10 2.1.2.5 NotEqual 10 2.1.2.6 GreaterThan 10 2.1.2.7 GreaterEqual 11 2.1.2.8 LessThan 11 2.1.2.9 LessEqual 11 2.1.2.10 直接Sql限制 11 2.1.2.11 多条件限制 11 2.1.3 查询排序 11 2.2 如何执行HQL查询 11 2.3 如何执行SQL查询 12 2.4 如何使用Mail 12 2.4.1 确保资源文件正确 12 2.4.1.1 确认mail.properties文件设置正确 12 2.4.1.2 确认applicationContext-mail.xml文件设置正确 12 2.4.1.3 确认applicationContext-service.xml文件设置正确 12 2.4.2 编使用mailService的代码 12 2.5 如何使用规则引擎 14 2.5.1 规则引擎使用说明 14 2.5.2 确保资源文件正确 14 2.5.2.1 确认applicationContext-rule.xml文件设置正确 14 2.5.2.2 确认applicationContext-service.xml文件设置正确 15 2.5.3 规则引擎的使用方式 15 2.6 如何使用代码翻译 16 2.6.1 配置文件 16 2.6.2 在Action中翻译代码 17 2.6.3 在jsp页面中翻译代码 18 2.7 如何使用取单号服务 18 2.7.1 配置文件 19 2.7.2 取单号使用方式 19 2.8 如何使用Hibernate操作Informix数据库 20 2.8.1 实现对LOB数据对象的支持 20 2.8.1.1 配置dataAccessContext-hibernate.xml 20 2.8.1.2 操作CLOB数据 20 2.8.1.3 操作BLOB数据 21 2.8.2 实现Sequence主键生成策略 21 2.9 如何清除Hibernate Session中的对象 22 2.10 使用日期控件 22 2.11 如何引入外部文件 23 2.11.1 主页面采用include方式,引入各个子页面jsp文件 23 2.11.2 主页面用${ctx}方式,引入超链接或资源文件等。 23 2.11.3 主页面用${ctx}方式,引入frame中包含的各个页面。 23 2.12 如何处理代码双击域 24 2.12.1 普通的代码双击域 24 2.12.2 多行输入域中的代码双击域 26 2.12.3 代码双击域的后台处理 27 2.12.4 代码双击域的常见问题 28 2.13 表单提交时调用的JS函数 28 2.13.1 submitFormToSave(form, funcSubmit) 28 2.13.2 submitFormWithoutConfirm(form, funcSubmit) 28 2.13.3 为什么要引入funcSubmit参数 29 2.13.4 为什么要先弹出“Are you sure?”确认信息再判断funcSubmit变量 29 2.14 日期时间和DateTime对象 29 2.15 Service和Dao的关系 31 2.16 如何调用其他Service 31 2.17 校验框架 32 2.17.1 单行校验 32 2.17.2 多行校验 32 2.18 事件处理机制 34 2.18.1 使用Observer模式 34 2.18.2 程序修改方式 34 2.19 用AJAX获取数据(通用做法) 36 2.19.1 “查看条款内容”的操作情景 36 2.19.2 前台JS函数调用后台JAVA类 36 2.19.3 后台JAVA类为前台JS函数返回结果 42 2.19.4 总结 45 2.20 金额的JAVA精确计算 45 2.20.1 double型数据不能进行精确计算 45 2.20.2 四舍五入 46 2.20.3 科学记数法 47 2.20.4 java.math.BigDecimal介绍 48 2.20.4.1 BigDecimal的构造方法 48 2.20.4.2 用BigDecimal进行四则运算 49 2.20.4.3 舍入模式 51 2.20.4.3.1 ROUND_CEILING 51 2.20.4.3.2 ROUND_FLOOR 52 2.20.4.3.3 ROUND_DOWN 53 2.20.4.3.4 ROUND_UP 54 2.20.4.3.5 ROUND_ UNNECESSARY 55 2.20.4.3.6 ROUND_HALF_DOWN 56 2.20.4.3.7 ROUND_HALF_UP 57 2.20.4.3.8 ROUND_HALF_EVEN 58 2.20.4.4 用BigDecimal进行除法运算 60 2.20.5 系统中精确计算的原则 60 2.21 金额的JS精确计算 60 2.22 根据语种取得代码名称 62 2.23 取兑换率的Javascript函数 62 2.23.1 一个原币和一个折币getGgExchProcess 62 2.23.2 一个原币和多个折币getGgExchListProcess 63 2.23.3 多个原币和一个折币getGgExchListListProcess 63 2.23.4 关于回调函数 64 2.24 同一个Edit页面用于多模块多actionType的实现方法 64 2.25 在Action和ServiceSpringImpl中读取资源文件 65 2.26 异常管理 65 2.26.1 分类概述 65 2.26.2 异常与系统架构的关系图 66 2.26.3 异常类的使用方法 67 2.26.3.1 ExceptionCause 67 2.26.3.2 BusinessException和PermissionException 67 2.26.3.3 DataVerifyException 68 2.26.3.4 ExceptionHelper 68 2.26.3.5 应用服务器异常 69 2.27 日志管理 69 2.27.1 概述 69 2.27.2 基本用法 69 2.27.3 什么是NDC 70 2.28 校验管理 70 2.28.1 Struts2校验框架简述 70 2.28.2 Struts2校验框架应用 71 2.28.2.1 校验配置文件 71 2.28.2.2 角色Insert页面示例 71 2.28.2.3 用户Query页面示例 73 2.28.2.4 多行输入域校验 75 2.29 多语言管理 77 2.29.1 定义输入域的name属性和资源文件 77 2.29.2 用户登录语种 78 2.30 使用select…for update解决并发问题 78 2.31 新增成功/失败弹出对话框而不离开窗口 78 2.32 权限检查调用点 79 2.32.1 为查询方法增加权限控制(PowerUtils.addPower()) 79 2.32.2 为按钮增加权限控制(在平台子系统配置方法任务关联) 81 2.33 保存成功后提示alert()而不跳转页面 82 3 页面多行录入的处理方案 83 3.1 概述 83 3.2 设计思想 84 3.3 方法调用顺序 85 3.3.1 点击增加按钮时 85 3.3.2 点击删除按钮时 85 3.4 Javascript API 85 3.4.1 insertRow 85 3.4.2 deleteRow 85 3.4.3 getOrderForMulLine 86 3.4.4 getFirstOrderForMulLine 86 3.4.5 getLastOrderForMulLine 86 3.4.6 getRecentDeletedTBody 87 3.4.7 getInnerOrderForMulLine 88 3.4.8 getTableOrderForMulLine 88 3.5 举例 88 3.5.1 两层嵌套的多行输入域举例 88 3.5.1.1 样例演示 88 3.5.1.2 内层和外层嵌套的关系图 92 3.5.1.3 注意事项 93 3.5.2 三层嵌套的多行输入域举例 93 3.5.2.1 样例演示 93 3.5.2.2 三层嵌套的关系图 94 3.5.3 四层嵌套的多行输入域举例 95 3.5.3.1 样例演示 95 3.5.3.2 四层嵌套的关系图 95 4 JSP页面风格规范 95 4.1 新增/修改页面风格 95 4.2 查询条件页面风格 97 4.3 查询结果页面风格 98 5 缓存管理方案 99 5.1 EHCache介绍 99 5.2 应用EHCache 99 6 系统的JSP标签库 100 6.1 双击域 100 6.2 <app:validate/> 103 6.3 <app:select/> 103 6.4 <app:navigate/>和<app:command/> 104 6.5 <app:localeInputName/> 104 6.6 <app:translateCode/> 105 6.7 <app:localeName/> 105 7 系统基础类库 106 7.1 ins.framework.dao 106 7.1.1 GenericDao<T, PK extends Serializable> 106 7.1.2 EntityDaoHibernate 106 7.1.3 GenericDaoHibernate<T, PK extends Serializable> 107 7.2 ins.framework.common 107 7.2.1 Page 107 7.2.2 DateTime 107 8 开发常见问题 110 8.1 informix jdbc中文配置 110 8.2 服务器启动时报Connection refused 110 8.3 OC4J不能启动和清理OC4J缓存 111 8.4 “a different object with the session identifier…”异常 111 8.5 声明对象句柄时不需要使用new关键字 112 8.6 一个POJO对象只能映射为一条数据库记录 114 8.6.1 实体对象生命周期 115 8.6.2 实体对象的识别 115 8.7 String对象、StringBuilder对象和原始数据类型的特点 116 8.7.1 String对象的“equals()”和“==” 116 8.7.2 StringBuilder对象 116 8.7.3 原始数据类型的“==” 117 8.8 代码双击域onchange之后弹出iframe窗口 117 8.9 系统登录安全认证配置 119 8.10 Service互调的实现 119 8.11 页面刷新几次后,有些输入域出现空白现象 120 9 系统遗留问题 120 10 附录 121 10.1 嵌套的多行输入域的样例文件 121 10.1.1 NestedMultiLineExampleLayer2.jsp源码 121 10.1.2 NestedMultiLineExampleLayer3.jsp源码 125 10.1.3 NestedMultiLineExampleLayer4.jsp源码 129 10.2 JSP页面风格规范样例文件 134 10.2.1 UserList.jsp 134 10.2.2 UserEdit.jsp 134
### 回答1: 好的,以下是一个简单的考勤管理系统的Python代码示例,使用MySQL数据库存储数据: ```python import mysql.connector # 连接MySQL数据库 db = mysql.connector.connect( host="localhost", user="yourusername", password="yourpassword", database="yourdatabase" ) # 获取数据库游标 cursor = db.cursor() # 创建考勤表 cursor.execute("CREATE TABLE attendance (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), time DATETIME)") # 插入考勤记录 name = input("请输入姓名:") sql = "INSERT INTO attendance (name, time) VALUES (%s, NOW())" val = (name,) cursor.execute(sql, val) db.commit() print(cursor.rowcount, "条记录插入成功。") # 查询考勤记录 cursor.execute("SELECT * FROM attendance") results = cursor.fetchall() print("考勤记录:") for result in results: print(result) # 关闭数据库连接 db.close() ``` 注意,在使用此代码之前,需要安装 `mysql-connector-python` 模块: ``` pip install mysql-connector-python ``` 另外,你需要将 `yourusername`、`yourpassword` 和 `yourdatabase` 替换为你自己MySQL数据库的用户名、密码和数据库名。 ### 回答2: 要用Python一个考勤管理系统并连接数据库,可以按照以下步骤进行实现: 1. 首先,需要安装Python的数据库连接库,如MySQL Connector、psycopg2等,根据使用的数据库类型选择相应的库进行安装。 2. 在Python中创建一个数据库连接的函数,用于连接到数据库。可以使用库提供的方法,传入数据库地址、用户名、密码等参数,建立与数据库的连接。 3. 创建一个考勤管理系统的类或函数,用于处理考勤相关的操作。这些操作可以包括添加考勤记录、查询考勤记录、统计考勤数据等。 4. 在考勤管理系统中,可以使用SQL语句或ORM(对象关系映射)工具,如SQLAlchemy等,根据数据库表的结构进行增删改查操作。 5. 通过调用数据库连接函数,在考勤管理系统中建立与数据库的连接。 6. 根据需求,编具体的功能函数或方法,如添加考勤记录的函数、查询考勤记录的函数等。在这些函数中,可以使用SQL语句或ORM工具执行数据库操作。 7. 测试考勤管理系统的各项功能,确保其与数据库的连接正常,并能正确执行相应的操作。 8. 在主程序中调用考勤管理系统的相关函数,实现考勤管理系统的运行。 总之,通过以上步骤,可以使用Python编一个连接数据库的考勤管理系统。需要注意的是,考勤管理系统的具体实现方式、数据库的选择以及功能细节等,根据具体需求和环境可以有所不同。 ### 回答3: 考勤管理系统是一种用于管理员工上下班考勤记录的软件系统。Python是一种功能强大的编程语言,可以用于编这样的考勤管理系统,并与数据库进行连接。 首先,在Python中我们可以使用PyMySQL或者SQLAlchemy等库来连接数据库。假设我们选择使用PyMySQL来连接MySQL数据库。 我们可以在Python中创建一个考勤管理类,其中包含一些与考勤管理相关的方法。首先,我们需要连接到数据库。我们可以在构造函数中使用PyMySQL来连接到MySQL数据库,配置数据库的连接信息,如主机地址、用户名、密码、数据库名称等。 然后,我们可以定义一些方法来实现考勤管理的功能,比如添加考勤记录、删除考勤记录、查询考勤记录等。在这些方法中,我们可以使用SQL语句来操作数据库,执行插入、删除和查询等操作。我们可以使用PyMySQL提供的方法来执行SQL语句,比如execute()来执行插入和删除操作,fetchall()来获取查询结果等。 在考勤管理系统中,我们通常会涉及到员工信息的管理,比如员工的姓名、工号、部门等信息。我们可以创建一个员工类,其中包含这些员工信息的属性。在考勤管理系统中,我们可以使用员工对象来表示员工,并将其作为参数传递给考勤管理类的方法。 最后,在主程序中,我们可以创建一个考勤管理对象,并且调用其方法来完成考勤管理的操作。我们可以提供一个用户界面,通过用户的输入来调用相应的方法,完成考勤管理的功能。 总结起来,使用Python编考勤管理系统可以通过连接到数据库,使用SQL语句操作数据库,定义相关的方法实现考勤管理的功能。通过对员工信息的管理,实现更加精确和高效的考勤管理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值