mongodb 前后端通讯

mongodb 官网: https://www.mongodb.com/zh-cn
官方文档:https://www.mongodb.com/docs/manual/reference/
通讯协议文档: https://www.mongodb.com/docs/manual/reference/mongodb-wire-protocol/
mongo drivers 文档:https://www.mongodb.com/docs/drivers/

文档型数据库

mongodb 的简介

mongodb 是一种文档型数据库,它是由字段和值对组成的数据结构。MongoDB文档类似于JSON对象。字段的值可能包括其他文档、数组和文档数组。
mongodb 文档存储在collections中, collections 类似于传统数据库中的表结构。

MongoDB将数据记录存储为文档(特别是BSON文档),这些文档收集在集合中。数据库存储一个或多个文档集合。

同一个集合中的集合文档结构可能是不同的,可以通过update操作对文档进行更新,对于一个文档来说,在创建集合时,会为文档创建一个独一无二的UUID,这个UUID唯一标识一个文档。

集合与视图

与传统的数据库相同的是,mongodb 也有视图的概念,下面是视图和集合概念的区别
在MongoDB中,视图(Views)和集合(Collections)是两种不同的数据组织方式。

  • 集合(Collections):

    • 集合是MongoDB中的基本数据存储单位,它类似于关系型数据库中的表。
      集合是由多个文档(Documents)组成的,每个文档是一个键值对的集合,类似于关系型数据库中的行。
    • 集合可以存储不同类型的文档,每个文档可以有不同的结构,没有固定的模式。
    • 集合提供了丰富的查询、插入、更新和删除操作,可以方便地处理各种数据操作需求。
  • 视图(Views):

    • 视图是一种虚拟的集合,它是从一个或多个现有的集合中获取数据并以特定的逻辑方式组织起来。
    • 视图可以看作是对现有集合的动态查询结果的一种封装,类似于关系型数据库中的视图。
    • 视图并不实际存储数据,而是根据定义的查询条件和逻辑关系,在查询时动态生成结果。
    • 视图可以简化复杂的查询逻辑,提供更高层次的数据抽象和封装,提高查询的可读性和可维护性。

总结:
集合是MongoDB中存储数据的基本单位,它可以存储多个文档,每个文档是一个键值对的集合。通过集合,可以进行各种数据操作,如查询、插入、更新和删除。而视图是一种虚拟的集合,它通过定义查询条件和逻辑关系来组织现有集合中的数据,并在查询时动态生成结果。视图可以简化复杂的查询逻辑,提供更高层次的数据抽象和封装。

基础操作

插入

插入单个文档

bson_t *document = bson_new();
BSON_APPEND_UTF8(document, "name", "Alice");
if (!mongoc_collection_insert_one(collection, document, NULL, NULL, &error)) {
    fprintf(stderr, "%s\n", error.message);
}
bson_destroy(document);

批量插入文档

mongoc_bulk_operation_t *bulk;
bson_t *doc1 = BCON_NEW("name", BCON_UTF8("Alice"));
bson_t *doc2 = BCON_NEW("name", BCON_UTF8("Bob"));
bulk = mongoc_collection_create_bulk_operation_with_opts(collection, NULL);

mongoc_bulk_operation_insert(bulk, doc1);
mongoc_bulk_operation_insert(bulk, doc2);

if (!mongoc_bulk_operation_execute(bulk, NULL, &error)) {
    fprintf(stderr, "%s\n", error.message);
}

bson_destroy(doc1);
bson_destroy(doc2);
mongoc_bulk_operation_destroy(bulk);

删除

删除单个文档

bson_t *query = bson_new();
BSON_APPEND_UTF8(query, "name", "Alice");
if (!mongoc_collection_delete_one(collection, query, NULL, NULL, &error)) {
    fprintf(stderr, "%s\n", error.message);
}
bson_destroy(query);

更新

更新单个文档

bson_t *query = bson_new();
bson_t *update = bson_new();
BSON_APPEND_UTF8(query, "name", "Alice");
BSON_APPEND_DOCUMENT_BEGIN(update, "$set", child);
BSON_APPEND_UTF8(child, "name", "Alice Updated");
bson_append_document_end(update, child);
if (!mongoc_collection_update_one(collection, query, update, NULL, NULL, &error)) {
    fprintf(stderr, "%s\n", error.message);
}
bson_destroy(query);
bson_destroy(update);

批量更新文档

bulk = mongoc_collection_create_bulk_operation_with_opts(collection, NULL);
bson_t *update = BCON_NEW("$set", "{", "name", BCON_UTF8("Alice Updated"), "}");

mongoc_bulk_operation_update(bulk, query, update, false);
mongoc_bulk_operation_update(bulk, query, update, false); // 可以添加更多的更新操作

if (!mongoc_bulk_operation_execute(bulk, NULL, &error)) {
    fprintf(stderr, "%s\n", error.message);
}
bson_destroy(query);
bson_destroy(update);
mongoc_bulk_operation_destroy(bulk);

查找

查询

bson_t *query = bson_new();
BSON_APPEND_UTF8(query, "name", "Alice");
mongoc_cursor_t *cursor = mongoc_collection_find_with_opts(collection, query, NULL, NULL);
const bson_t *doc;
while (mongoc_cursor_next(cursor, &doc)) {
    char *str = bson_as_canonical_extended_json(doc, NULL);
    printf("%s\n", str);
    bson_free(str);
}
bson_destroy(query);
mongoc_cursor_destroy(cursor);

wire protocol

mongodb 前后端通讯协议是一种基于socket的简单通讯协议,属于典型的请求应答型协议。默认端口是27017,所有的整型数字都是采用小端对齐
mongodb使用OP_MSG 来标记前后端通讯类型,在一些老旧的版本中,会有多种不同的消息类型去标记但是这些消息都已经被弃用了。

标准消息头结构

通常情况下,一个标准的消息,是由一个消息头跟上后面的消息数据组成的,一个标准的消息头结构如下所示

struct MsgHeader {
    int32   messageLength;   // 消息总大小(包括本身)
    int32   requestID;      // 消息标识符
    int32   responseTo;     // 用于数据库响应
    int32   opCode;        // 消息类型
}

字段说明:

字段描述
messageLen该字段标记消息长度,单位时bytes, 占四个字节
requestID消息的唯一识别码
responseTo来自于客户端请求消息的请求ID
opCode消息类型

消息类型说明

编号消息名称消息值(整型)说明
1OP_COMPRESSED2012使用压缩包操作
2OP_MSG2013发送标准消息格式,用于客户端与服务端通讯
3OP_REPLY1响应客户端请求
4OP_UPDATE2001更新文档
5OP_INSERT2002插入文档
6RESERVED2003用于获取OID
7OP_QUERY2004查询一个集合
8OP_GET_MORE2005从一个集合中获取更多的数据
9OP_DELETE2006删除文档
10OP_KILL_CURSOTS2007通知数据库客户端已经结束

需要说明的是,除消息1,2以外,其余的操作类型,均已经在5.1以后的版本中删除,除极为早期的版本外,3-10 的消息类型均已弃用,只有在链接初期,才会有3,7 两种消息用以确定客户端版本等信息。

消息结构

OP_COMPRESSED

在mongodb 3.4中引入

任何类型的操作码都能够包装在该操作中,OP_COMPRESSED消息包含原始压缩操作码消息以及处理和解压缩操作码消息所需的元数据。

具体结构如下所示:

struct {
    MsgHeader header;            // 标准消息头
    int32  originalOpcode;       // 操作值
    int32  uncompressedSize;     // 大小,不包含消息头
    uint8  compressorId;         
    char    *compressedMessage;  // 
}
字段描述
msgHeader标准消息头,这个在前面已经说明,这里不再赘述
originalOpcode被压缩前的原始操作码
compressorID压缩消息的压缩器id
compressedMessage消息被压缩前除消息头外的内容

compressorID 说明

压缩idhandshake value描述
0noop这里面的消息是没有被压缩过的,这种消息一般用于测试
1snappy使用的是snappy进行压缩
2zlib使用的是zlib进行压缩
3zstd使用的是zstd进行压缩
4-255reserved保留字段

OP_MSG

OP_MSG 是一种可扩展的消息格式,用于对网络上的客户端请求和服务器回复进行编码。其基本格式为

OP_MSG {
    MsgHeader header;           // standard message header
    uint32 flagBits;            // message flags
    Sections[] sections;        // data sections
    optional<uint32> checksum;  // optional CRC-32C checksum
}

其中: header 是指标准消息头, checksum指的是crc-32c 校验码。这里重点说明flagBits和sections两个字段

  • flagBits 整数是用于修改 OP_MSG 的格式和行为的位掩码编码标志。

在 MongoDB 中,OP_MSG 是一个服务器和客户端之间通信的消息格式。从 MongoDB 3.6 版本开始使用,OP_MSG 允许更灵活、更丰富的命令和数据交换。每个 OP_MSG 消息都有一个 “Flag Bits” 字段,该字段是一个 32 位整数,用于控制消息的特定行为。

以下是 “Flag Bits” 字段中定义的标志及其含义:

Checksum Present (bit 0):如果设置了此位(0x1),则表示消息的末尾包含一个 4 字节的校验和。这用于验证消息在网络上传输时的完整性。

More To Come (bit 1):如果设置了此位(0x2),则表示在没有接收到回复的情况下,还有更多的消息要发送。这对于服务器执行长序列的回复非常有用,无需等待客户端针对每条消息都发送确认。

Exhaust Allowed (bit 16):如果设置了此位(0x10000),则表示客户端允许服务器在不接收到进一步请求的情况下发送后续的 OP_MSG 消息序列。

这是一个 32 位字段,所以可能有多达 32 个不同的标志位,但并非所有的位都在 MongoDB 的当前版本中使用。这些标志位允许客户端和服务器就如何处理消息达成一致。例如,客户端可能通过发送带有 “More To Come” 标志的消息来指示它希望服务器不要等待回复即可发送进一步的数据。

需要额外说明的是,该字段中前16位是必须要设置的,如果设置出错则解析器一定会报错,后十六位是可选的,如果设置错误,解析器会忽略掉,根据版本不同,这个flagbits支持的内容也不同,目前各个版本仅支持前文中所述的bit位赋值


  • sections

一个OP_MSG中通常会包含一个或者多个sections,每一个section都会从一个kind位开始,用于标识该section类型, 在kind位后紧跟着的就是section的实际载荷

下面是三种kind类型说明

  • kind(0) body
    这个结构里只会存在一个bson object,section的大小就是bson对象的大小

  • kind(1) document sequence
    这种kind中可以同时存在多个document,其基本结构如下所示

  1. int32: 标记着这个section的大小
  2. c string 文档sequence 识别
  3. 零个或者多个bson对象
  • kind(2) 校验
    内部使用不过多说明

遗留的操作类型

对于增删改查操作,对应的消息结构较为简单,这里只做列举,不再详细说明,5.0以后已全部删除

  • op_delete
struct {
   MsgHeader header;             // standard message header
   int32     ZERO;               // 0 - reserved for future use
   cstring   fullCollectionName; // "dbname.collectionname"
   int32     flags;              // bit values - see below for details.
   document  selector;           // query object.  See below for details.
}
  • op_get_more
struct {
    MsgHeader header;             // standard message header
    int32     ZERO;               // 0 - reserved for future use
    cstring   fullCollectionName; // "dbname.collectionname"
    int32     numberToReturn;     // number of documents to return
    int64     cursorID;           // cursorID from the OP_REPLY
}
  • op_insert
struct {
    MsgHeader header;             // standard message header
    int32     flags;              // bit values - see below
    cstring   fullCollectionName; // "dbname.collectionname"
    document* documents;          // one or more documents to insert into the collection
}
  • op_kill_cursors
struct {
    MsgHeader header;            // standard message header
    int32     ZERO;              // 0 - reserved for future use
    int32     numberOfCursorIDs; // number of cursorIDs in message
    int64*    cursorIDs;         // sequence of cursorIDs to close
}
  • op_update
struct OP_UPDATE {
    MsgHeader header;             // standard message header
    int32     ZERO;               // 0 - reserved for future use
    cstring   fullCollectionName; // "dbname.collectionname"
    int32     flags;              // bit values. see below
    document  selector;           // the query to select the document
    document  update;             // specification of the update to perform
}
  • op_query
struct OP_QUERY {
    MsgHeader header;                 // standard message header
    int32     flags;                  // bit values of query options.  See below for details.
    cstring   fullCollectionName ;    // "dbname.collectionname"
    int32     numberToSkip;           // number of documents to skip
    int32     numberToReturn;         // number of documents to return
                                      //  in the first OP_REPLY batch
    document  query;                  // query object.  See below for details.
  [ document  returnFieldsSelector; ] // Optional. Selector indicating the fields
                                      //  to return.  See below for details.
}
  • op_reply
struct {
    MsgHeader header;         // standard message header
    int32     responseFlags;  // bit values - see details below
    int64     cursorID;       // cursor ID if client needs to do get more's
    int32     startingFrom;   // where in the cursor this reply is starting
    int32     numberReturned; // number of documents in the reply
    document* documents;      // documents
}

其他说明

1. cursor是什么 cursorIDs又是什么

在数据库系统中,特别是在 MongoDB 中,cursor 是一个非常重要的概念。cursor 是一个服务器端的数据库查询的状态指示器,它允许客户端查询数据库并以可控的方式检索结果集。具体来说,cursor 使得客户端可以逐步地、一次获取部分结果,而不是一次性地加载整个结果集,这在处理大量数据时非常有用以避免大量的内存使用。

当一个查询执行时,如果预期结果集较大或者存在其他需要分批获取数据的情况,数据库服务器会创建一个 cursor,并返回给客户端一个 cursor ID。客户端随后可以使用这个 cursor ID 来逐步获取整个结果集,通常是通过“下一个”(next)操作来请求更多的数据。

cursorIDs 是指那些与当前打开的 cursors 相关联的唯一标识符。每个 cursor 都有一个唯一的 ID,客户端可通过这个 ID 来管理 cursor 的生命周期,包括获取下一组数据、检查 cursor 状态或关闭 cursor。

在 MongoDB 中,当你执行一个查询时,你会得到一个 cursor 对象,这个对象使你能够遍历结果集。例如,在 MongoDB 的 shell 中或者使用其驱动程序时,可以这样操作:

// 例如,在 MongoDB shell 中执行一个查询
var myCursor = db.collection.find();

// 使用 cursor 遍历结果
while (myCursor.hasNext()) {
   print(tojson(myCursor.next()));
}

在上述代码中,find() 方法返回了一个 cursor 对象 myCursor,你可以使用 hasNext()next() 方法来遍历查询结果。

如果查询只返回了部分结果,并且还有更多结果可检索,cursor ID 就会被用来在后续的操作中检索剩余的结果。当所有的结果都被检索完或者 cursor 被显式关闭时,cursor ID 就不再有效。

总的来说,cursor 是一种用于逐步读取数据库查询结果的机制,而 cursor IDs 是服务器用来追踪和管理这些 cursors 的标识符。

2. OID是什么

OID 通常指的是 Object Identifier(对象标识符),在不同的上下文中可能有不同的含义,但大多数情况下,它涉及到为对象分配一个唯一的标识符。

在数据库领域,尤其是在 MongoDB 中,OID 通常指的是 ObjectId,这是一个用于唯一标识文档(document)的特殊类型的字段。在 MongoDB 中,每个文档存储时都会自动赋予一个 _id 字段,该字段的默认类型就是 ObjectId。这个 ObjectId 是一个 12 字节的 BSON 类型的字段,确保了集合中每个文档的唯一性。

ObjectId 结构如下:

  • 前 4 个字节为 Unix 时间戳,表示 ObjectId 的创建时间。
  • 接下来的 3 个字节是机器标识符,通常是主机名的散列值。
  • 然后是 2 个字节的进程 ID,用于标识生成该 ObjectId 的进程。
  • 最后 3 个字节是一个递增的计数器,用于确保同一秒内生成的 ObjectId 是唯一的。

例如,一个典型的 ObjectId 可能看起来像这样:507f191e810c19729de860ea

ObjectId 的这种结构设计使得它们通常是按生成时间的顺序排序的,这使得它们在插入时天然索引化,而且可以从 ObjectId 本身解析出文档的创建时间,这是一项非常有用的属性。

在其他上下文中,比如网络管理和 LDAP (轻量级目录访问协议) 中,OID 也用于唯一标识信息的对象,例如设备、服务器、证书等等。 OID 在这些领域通常表示为一串由点分隔的数字,如 “1.3.6.1.4.1”,这种格式遵循了 ITU-T X.660 和 ISO/IEC 9834-1 标准。

3. 在sections字段中 不同kind之间有什么联系

在 MongoDB 的 OP_MSG 消息协议中,一个消息可以包含多个 sections,每个 section 都有一个 kind 字段来指示其类型。这些 sections 按顺序排列,每种类型的 section 扮演不同的角色。以下是 kind 0 和 kind 1 sections 的基本理解和它们之间关系的解释:

  • Section Kind 0: 这种类型的 section 通常包含一个 BSON 对象,它代表着操作的元数据或参数。这可能是一个命令(如 find 或 insert)以及相应的命令参数。在大多数操作中,kind 0 section 是必需的,因为它包含了 MongoDB 服务器需要了解的操作指令。

  • Section Kind 1: 这种类型的 section 用于包含一个更复杂的数据结构,通常是一系列 BSON 文档。kind 1 section 与一个 c string 标签(如 “documents”)配合使用,该标签描述了这些 BSON 文档的预期用途。这种 section 类型常用于传输如插入多个文档的数据集合。

关系如下:

  1. 序列: 在一个 OP_MSG 消息中,kind 0 section 通常首先出现,它定义了该消息的主要操作和上下文。kind 1 section(如果存在的话)紧随其后,提供更多的数据,例如要插入或修改的文档集合。

  2. 依赖性: kind 1 section 依赖于 kind 0 section 中定义的上下文。例如,在一个插入操作中,kind 0 section 将指明目标数据库和集合,而 kind 1 section 将包含要插入的实际文档数据。

  3. 补充性: kind 1 section 可以看作是对 kind 0 section 的补充。在某些操作中,使用 kind 1 section 能更有效地传输大量的文档数据。

  4. 独立性: 虽然 kind 1 section 通常与 kind 0 section 配合使用,但它也可以独立出现在 OP_MSG 操作中。它的出现取决于具体要执行的操作。

  5. 可选性: 不是所有的 OP_MSG 消息都包含 kind 1 section。如果操作不需要传输一系列文档,那么可能只有 kind 0 section。

综上所述,kind 0 和 kind 1 sections 在 OP_MSG 消息中共同工作,以支持复杂的数据库操作。kind 0 定义了操作的主要参数,而 kind 1(如果需要的话)则提供了额外的文档数据。在构建或解析 OP_MSG 消息时,必须正确理解和处理这些 sections 之间的关系,以确保操作的正确执行。

4. bson结构是如何在互联网中传输的,并且他的序列化过程是怎样的

传输过程
在MongoDB中,BSON文档在前后端间的传输通常是通过MongoDB的驱动程序和数据库服务器之间进行的。以下是这一过程的大致步骤:

  1. 序列化:

    • 在前端(通常指的是客户端应用程序),当你创建或者检索数据时,MongoDB的驱动程序会将你的操作转换为对应的BSON文档格式。
    • 对于需要发送到数据库的数据(如插入或更新操作),MongoDB的驱动程序会将高级数据结构(如JavaScript对象)序列化(或转换)成BSON格式。
  2. 传输:

    • 序列化后的BSON文档通过网络发送到MongoDB服务器。
    • 这通常涉及到TCP/IP协议栈,并可能使用TLS/SSL加密来保保护数据的安全。
  3. 反序列化:

    • MongoDB服务器接收到BSON文档后,它会对这些文档进行解析,并执行相应的数据库操作(如查询、插入、更新或删除)。
    • 对于需要返回给前端的数据,MongoDB服务器会将查询结果集序列化成BSON文档并发送回客户端。
  4. 解析:

    • 客户端的MongoDB驱动程序接收到BSON文档后,会将它们反序列化成客户端编程语言可以理解和操作的数据结构(如JavaScript对象)。
    • 应用程序随后可以使用这些数据结构来显示信息、进一步处理数据或者触发其它业务逻辑。

这个过程对于终端用户是透明的,因为MongoDB的驱动程序和数据库引擎处理了所有的序列化和反序列化过程。开发者可以像操作普通的数据结构一样与数据库交互,而不用担心BSON文档的底层细节。

序列化过程
序列化是将数据结构或对象状态转换为可以存储或传输的格式的过程,以便稍后可以通过反序列化重新创建原始对象。在MongoDB中,序列化通常指的是将客户端语言中的数据结构转换为BSON格式,以便能够通过网络发送给MongoDB服务器,或者存储在磁盘上。

序列化的过程大致包括以下几个步骤:

  1. 数据类型映射: 将客户端语言的数据类型映射到BSON支持的数据类型。比如,将JavaScript的对象转换为BSON文档,把日期对象转换为BSON的日期类型,等等。

  2. 编码数据: 将数据编码为BSON的二进制格式。BSON文档是由一系列以null字节结尾的键值对构成的。每个键值对前都有一个表示数据类型的字节。

    每个键值对大致遵循以下格式:

    • 一个字节的类型标识符
    • 键名(以null终止的字符串)
    • 值(其格式取决于类型)
    • 文档以null字节结束
  3. 顺序与尺寸: 在BSON中,键值对保持着它们被添加到文档中的顺序。同时,每个BSON文档的开始处会有一个四字节的长度字段,指明整个文档的尺寸,这使得解析可以更高效。

  4. 特殊字段处理: 对于BSON支持而客户端语言特有的数据类型,如ObjectId、二进制数据、正则表达式等,序列化过程会按照BSON规范进行特殊处理。

  5. 输出BSON二进制数据: 完成上述步骤后,你将得到一个BSON格式的二进制数据流,它可以被MongoDB服务器解析。

序列化的过程是由MongoDB的驱动程序自动完成的,开发者通常不需要手动进行这些操作。例如,在Node.js中,你可以简单地构造一个JavaScript对象,并使用MongoDB Node.js驱动程序提供的方法将其插入数据库,序列化发生在底层,对开发者透明。

遗留问题

  1. bson结构获取之后应该如何处理,写一个bson的解码器肯定不现实,应该如何利用libbson库,需要花时间研究
  2. 对于早期版本,没有对应的数据包,无法得知已经遗弃的消息类型,具体的报文格式是怎样的。
  3. 确认一下bson中有没有文件内容
  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,那我先简单介绍一下Spring Boot和MongoDB,再讲一下如何使用它们进行前后端分离开发员工管理系统。 Spring Boot是一个基于Spring框架的快速开发框架,它可以帮助开发者快速构建基于Spring的应用程序,而不需要手动配置很多复杂的配置文件。Spring Boot还提供了很多快速开发的功能,比如自动配置、控制反转、面向切面编程等等,极大地提高了开发效率。 MongoDB是一种NoSQL数据库,它是一种文档型数据库,使用JSON格式存储数据MongoDB具有高可用性、自动分片、自动故障转移、快速查询等特点,非常适合处理大量的非结构化数据前后端分离开发是一种新的开发模式,它将前端和后端分离开发,前端负责展示数据交互和UI设计,后端负责数据的存储、处理和业务逻辑。这种开发模式可以提高开发效率,同时也使得前端和后端可以独立开发、独立测试、独立部署。 下面是一个使用Spring Boot和MongoDB进行前后端分离开发的员工管理系统的简单步骤: 1. 首先需要安装MongoDB,并创建一个名为employee的数据库和employee_info的集合。 2. 创建一个Spring Boot项目,并添加MongoDB的依赖。 3. 创建一个Employee类,用来表示员工信息,包括姓名、年龄、性别等等。 4. 创建一个EmployeeRepository接口,用来定义数据库操作,比如增删改查等。 5. 创建一个EmployeeController类,用来处理HTTP请求,比如查询所有员工、添加员工、更新员工信息等等。 6. 创建一个前端页面,用来展示员工信息列表,以及添加、修改员工信息。 7. 使用JavaScript等前端技术,通过Ajax请求后端数据实现前后端交互。 这样,一个简单的前后端分离的员工管理系统就完成了,你可以根据自己的需求添加更多的功能,比如员工离职、工资管理等等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

望晓天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值