MongoDB的Mongoose简介

by Nick Karnik

尼克·卡尼克(Nick Karnik)

MongoDB的Mongoose简介 (Introduction to Mongoose for MongoDB)

Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It manages relationships between data, provides schema validation, and is used to translate between objects in code and the representation of those objects in MongoDB.

Mongoose是用于MongoDB和Node.js的对象数据建模(ODM)库。 它管理数据之间的关系,提供架构验证,并用于在代码中的对象和MongoDB中的这些对象的表示之间进行转换。

MongoDB is a schema-less NoSQL document database. It means you can store JSON documents in it, and the structure of these documents can vary as it is not enforced like SQL databases. This is one of the advantages of using NoSQL as it speeds up application development and reduces the complexity of deployments.

MongoDB是一种无模式的NoSQL文档数据库。 这意味着您可以在其中存储JSON文档,并且这些文档的结构可能会有所不同,因为它不像SQL数据库那样强制执行。 这是使用NoSQL的优点之一,因为它可以加速应用程序开发并降低部署的复杂性。

Below is an example of how data is stored in Mongo vs. SQL Database:

以下是在Mongo与SQL数据库中如何存储数据的示例:

术语 (Terminologies)

馆藏 (Collections)

‘Collections’ in Mongo are equivalent to tables in relational databases. They can hold multiple JSON documents.

Mongo中的“集合”等效于关系数据库中的表。 它们可以容纳多个JSON文档。

文件资料 (Documents)

‘Documents’ are equivalent to records or rows of data in SQL. While a SQL row can reference data in other tables, Mongo documents usually combine that in a document.

“文档”等效于SQL中的记录或数据行。 虽然SQL行可以引用其他表中的数据,但是Mongo文档通常将其合并到文档中。

领域 (Fields)

‘Fields’ or attributes are similar to columns in a SQL table.

“字段”或属性类似于SQL表中的列。

架构图 (Schema)

While Mongo is schema-less, SQL defines a schema via the table definition. A Mongoose ‘schema’ is a document data structure (or shape of the document) that is enforced via the application layer.

尽管Mongo没有架构,但SQL通过表定义定义了架构。 猫鼬的“模式”是通过应用程序层实施的文档数据结构(或文档形状)。

楷模 (Models)

‘Models’ are higher-order constructors that take a schema and create an instance of a document equivalent to records in a relational database.

“模型”是采用模式并创建与关系数据库中的记录等效的文档实例的高阶构造函数。

入门 (Getting Started)

Mongo安装 (Mongo Installation)

Before we get started, let’s setup Mongo. You can choose from one of the following options (we are using option #1 for this article):

在开始之前,让我们设置Mongo。 您可以从以下选项之一中进行选择 (本文使用的是选项#1):

  1. Download the appropriate MongoDB version for your Operating System from the MongoDB Website and follow their installation instructions

    MongoDB网站下载适合您的操作系统的MongoDB版本,并按照其安装说明进行操作

  2. Create a free sandbox database subscription on mLab

    在mLab上创建免费的沙箱数据库订阅

  3. Install Mongo using Docker if you prefer to use docker

    如果您更喜欢使用docker ,请使用Docker安装Mongo。

Let’s navigate through some of the basics of Mongoose by implementing a model that represents data for a simplified address book.

让我们通过实现代表简化通讯录数据的模型来浏览猫鼬的一些基础知识。

I am using Visual Studio Code, Node 8.9, and NPM 5.6. Fire up your favorite IDE, create a blank project, and let’s get started! We will be using the limited ES6 syntax in Node, so we won’t be configuring Babel.

我正在使用Visual Studio Code,Node 8.9和NPM 5.6。 启动您喜欢的IDE,创建一个空白项目,然后开始吧! 我们将在Node中使用受限的ES6语法,因此我们将不会配置Babel。

NPM安装 (NPM Install)

Let’s go to the project folder and initialize our project

我们转到项目文件夹并初始化我们的项目

npm init -y

Let’s install Mongoose and a validation library with the following command:

让我们使用以下命令安装Mongoose和一个验证库:

npm install mongoose validator

The above install command will install the latest version of the libraries. The Mongoose syntax in this article is specific to Mongoose v5 and beyond.

上面的install命令将安装最新版本的库。 本文中的Mongoose语法特定于Mongoose v5及更高版本。

数据库连接 (Database Connection)

Create a file ./src/database.js under the project root.

创建一个文件./src/database.js 在项目根目录下。

Next, we will add a simple class with a method that connects to the database.

接下来,我们将添加一个简单的类,该类具有连接到数据库的方法。

Your connection string will vary based on your installation.

您的连接字符串将根据您的安装而有所不同。

let mongoose = require('mongoose');

const server = '127.0.0.1:27017'; // REPLACE WITH YOUR DB SERVER
const database = 'fcc-Mail';      // REPLACE WITH YOUR DB NAME

class Database {
  constructor() {
    this._connect()
  }
  
_connect() {
     mongoose.connect(`mongodb://${server}/${database}`)
       .then(() => {
         console.log('Database connection successful')
       })
       .catch(err => {
         console.error('Database connection error')
       })
  }
}

module.exports = new Database()

The require(‘mongoose’) call above returns a Singleton object. It means that the first time you call require(‘mongoose’), it is creating an instance of the Mongoose class and returning it. On subsequent calls, it will return the same instance that was created and returned to you the first time because of how module import/export works in ES6.

require('mongoose') 上面的调用返回一个Singleton对象。 这意味着您第一次调用require('mongoose') ,它正在创建Mongoose类的实例并返回它。 在后续调用中,由于模块导入/导出在ES6中的工作方式,它将返回与第一次创建并返回给您的实例相同的实例。

Similarly, we have turned our Database class into a singleton by returning an instance of the class in the module.exports statement because we only need a single connection to the database.

同样,我们通过在module.exports语句中返回该类的实例,将Database类变成单例,因为我们只需要与数据库的单个连接即可。

ES6 makes it very easy for us to create a singleton (single instance) pattern because of how the module loader works by caching the response of a previously imported file.

ES6使我们很容易创建单例(单个实例)模式,因为模块加载器通过缓存先前导入的文件的响应来工作。

猫鼬模式与模型 (Mongoose Schema vs. Model)

A Mongoose model is a wrapper on the Mongoose schema. A Mongoose schema defines the structure of the document, default values, validators, etc., whereas a Mongoose model provides an interface to the database for creating, querying, updating, deleting records, etc.

Mongoose模型是Mongoose模式的包装。 猫鼬模式定义了文档的结构,默认值,验证器等,而猫鼬模型则提供了数据库的接口,用于创建,查询,更新,删除记录等。

Creating a Mongoose model comprises primarily of three parts:

创建猫鼬模型主要包括三个部分:

1.引用猫鼬 (1. Referencing Mongoose)
let mongoose = require('mongoose')

This reference will be the same as the one that was returned when we connected to the database, which means the schema and model definitions will not need to explicitly connect to the database.

此引用将与我们连接到数据库时返回的引用相同,这意味着架构和模型定义将不需要显式连接到数据库。

2.定义架构 (2. Defining the Schema)

A schema defines document properties through an object where the key name corresponds to the property name in the collection.

模式通过一个对象定义文档属性,该对象的键名与集合中的属性名相对应。

let emailSchema = new mongoose.Schema({
  email: String
})

Here we define a property called email with a schema type String which maps to an internal validator that will be triggered when the model is saved to the database. It will fail if the data type of the value is not a string type.

在这里,我们定义了一个名为email的属性,其模式类型为String ,该属性映射到内部验证器,该内部验证器将在模型保存到数据库时触发。 如果值的数据类型不是字符串类型,它将失败。

The following Schema Types are permitted:

允许使用以下架构类型:

  • Array

    数组
  • Boolean

    布尔型
  • Buffer

    缓冲
  • Date

    日期
  • Mixed (A generic / flexible data type)

    混合(通用/灵活数据类型)
  • Number

  • ObjectId

    对象编号
  • String

Mixed and ObjectId are defined under require(‘mongoose’).Schema.Types.

Mixed和ObjectId在require('mongoose').Schema.Types下定义。

3.导出模型 (3. Exporting a Model)

We need to call the model constructor on the Mongoose instance and pass it the name of the collection and a reference to the schema definition.

我们需要在Mongoose实例上调用模型构造函数,并向其传递集合名称和对模式定义的引用。

module.exports = mongoose.model('Email', emailSchema)

Let’s combine the above code into ./src/models/email.js to define the contents of a basic email model:

让我们将上面的代码合并到./src/models/email.js 定义基本电子邮件模型的内容:

let mongoose = require('mongoose')

let emailSchema = new mongoose.Schema({
  email: String
})

module.exports = mongoose.model('Email', emailSchema)

A schema definition should be simple, but its complexity is usually based on application requirements. Schemas can be reused and they can contain several child-schemas too. In the example above, the value of the email property is a simple value type. However, it can also be an object type with additional properties on it.

模式定义应该很简单,但是其复杂性通常基于应用程序需求。 模式可以重复使用,它们也可以包含几个子模式。 在上面的示例中,电子邮件属性的值是一种简单的值类型。 但是,它也可以是带有其他属性的对象类型。

We can create an instance of the model we defined above and populate it using the following syntax:

我们可以创建上面定义的模型的实例,并使用以下语法填充它:

let EmailModel = require('./email')

let msg = new EmailModel({
  email: 'ada.lovelace@gmail.com'
})

Let’s enhance the Email schema to make the email property a unique, required field and convert the value to lowercase before saving it. We can also add a validation function that will ensure that the value is a valid email address. We will reference and use the validator library installed earlier.

让我们增强“电子邮件”架构,使“电子邮件”属性成为唯一的必填字段,然后在保存之前将其值转换为小写。 我们还可以添加验证功能,以确保该值是有效的电子邮件地址。 我们将参考并使用之前安装的验证程序库。

let mongoose = require('mongoose')
let validator = require('validator')

let emailSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    validate: (value) => {
      return validator.isEmail(value)
    }
  }
})

module.exports = mongoose.model('Email', emailSchema)

基本操作 (Basic Operations)

Mongoose has a flexible API and provides many ways to accomplish a task. We will not focus on the variations because that is out of scope for this article, but remember that most of the operations can be done in more than one way either syntactically or via the application architecture.

猫鼬具有灵活的API,并提供了多种完成任务的方法。 我们不会专注于变化,因为这超出了本文的范围,但是请记住,大多数操作可以通过语法或通过应用程序体系结构以多种方式完成。

创建记录 (Create Record)

Let’s create an instance of the email model and save it to the database:

让我们创建电子邮件模型的实例并将其保存到数据库中:

let EmailModel = require('./email')

let msg = new EmailModel({
  email: 'ADA.LOVELACE@GMAIL.COM'
})

msg.save()
   .then(doc => {
     console.log(doc)
   })
   .catch(err => {
     console.error(err)
   })

The result is a document that is returned upon a successful save:

结果是成功保存后返回的文档:

{ 
  _id: 5a78fe3e2f44ba8f85a2409a,
  email: 'ada.lovelace@gmail.com',
  __v: 0 
}

The following fields are returned (internal fields are prefixed with an underscore):

返回以下字段(内部字段带有下划线前缀):

  1. The _id field is auto-generated by Mongo and is a primary key of the collection. Its value is a unique identifier for the document.

    _id字段由Mongo自动生成,并且是集合的主键。 它的值是文档的唯一标识符。

  2. The value of the email field is returned. Notice that it is lower-cased because we specified the lowercase:true attribute in the schema.

    返回email字段的值。 注意,它是小写的,因为我们在模式中指定了lowercase:true属性。

  3. __v is the versionKey property set on each document when first created by Mongoose. Its value contains the internal revision of the document.

    __v是Mongoose首次创建时在每个文档上设置的versionKey属性。 它的值包含文档的内部修订版。

If you try to repeat the save operation above, you will get an error because we have specified that the email field should be unique.

如果您尝试重复上述保存操作,则会收到一条错误消息,因为我们指定了电子邮件字段为唯一。

取得记录 (Fetch Record)

Let’s try to retrieve the record we saved to the database earlier. The model class exposes several static and instance methods to perform operations on the database. We will now try to find the record that we created previously using the find method and pass the email as the search term.

让我们尝试检索我们之前保存到数据库的记录。 模型类公开了几种静态方法和实例方法来对数据库执行操作。 现在,我们将尝试使用find方法查找先前创建的记录,并将电子邮件作为搜索项传递。

EmailModel
  .find({
    email: 'ada.lovelace@gmail.com'   // search query
  })
  .then(doc => {
    console.log(doc)
  })
  .catch(err => {
    console.error(err)
  })

The document returned will be similar to what was displayed when we created the record:

返回的文档将类似于我们创建记录时显示的内容:

{ 
  _id: 5a78fe3e2f44ba8f85a2409a,
  email: 'ada.lovelace@gmail.com',
  __v: 0 
}
更新记录 (Update Record)

Let’s modify the record above by changing the email address and adding another field to it, all in a single operation. For performance reasons, Mongoose won’t return the updated document so we need to pass an additional parameter to ask for it:

让我们通过更改电子邮件地址并向其中添加另一个字段来修改上面的记录,所有这些操作只需一次操作。 出于性能方面的考虑,Mongoose不会返回更新后的文档,因此我们需要传递一个附加参数来要求它:

EmailModel
  .findOneAndUpdate(
    {
      email: 'ada.lovelace@gmail.com'  // search query
    }, 
    {
      email: 'theoutlander@live.com'   // field:values to update
    },
    {
      new: true,                       // return updated doc
      runValidators: true              // validate before update
    })
  .then(doc => {
    console.log(doc)
  })
  .catch(err => {
    console.error(err)
  })

The document returned will contain the updated email:

返回的文档将包含更新的电子邮件:

{ 
  _id: 5a78fe3e2f44ba8f85a2409a,
  email: 'theoutlander@live.com',
  __v: 0 
}
删除记录 (Delete Record)

We will use the findOneAndRemove call to delete a record. It returns the original document that was removed:

我们将使用findOneAndRemove调用删除一条记录。 它返回已删除的原始文档:

EmailModel
  .findOneAndRemove({
    email: 'theoutlander@live.com'
  })
  .then(response => {
    console.log(response)
  })
  .catch(err => {
    console.error(err)
  })

帮手 (Helpers)

We have looked at some of the basic functionality above known as CRUD (Create, Read, Update, Delete) operations, but Mongoose also provides the ability to configure several types of helper methods and properties. These can be used to further simplify working with data.

我们已经研究了上面称为CRUD(创建,读取,更新,删除)操作的一些基本功能,但是Mongoose还提供了配置多种类型的帮助程序方法和属性的功能。 这些可用于进一步简化数据处理。

Let’s create a user schema in ./src/models/user.js with the fieldsfirstName and lastName:

让我们在./src/models/user.js使用firstNamelastName字段创建一个用户模式:

let mongoose = require('mongoose')

let userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
})

module.exports = mongoose.model('User', userSchema)
虚拟财产 (Virtual Property)

A virtual property is not persisted to the database. We can add it to our schema as a helper to get and set values.

虚拟属性不会持久保存到数据库中。 我们可以将其作为获取和设置值的助手添加到我们的架构中。

Let’s create a virtual property called fullName which can be used to set values on firstName and lastName and retrieve them as a combined value when read:

让我们创建一个名为fullName的虚拟属性,该属性可用于在firstNamelastName上设置值,并在读取时作为组合值检索它们:

userSchema.virtual('fullName').get(function() {
  return this.firstName + ' ' + this.lastName
})

userSchema.virtual('fullName').set(function(name) {
  let str = name.split(' ')
  
  this.firstName = str[0]
  this.lastName = str[1]
})

Callbacks for get and set must use the function keyword as we need to access the model via the this keyword. Using fat arrow functions will change what this refers to.

get和set的回调必须使用function关键字,因为我们需要通过this来访问模型 关键词。 使用粗箭头功能将更改this指代的内容。

Now, we can set firstName and lastName by assigning a value to fullName:

现在,我们可以通过为fullName分配一个值来设置firstNamelastName

let model = new UserModel()

model.fullName = 'Thomas Anderson'

console.log(model.toJSON())  // Output model fields as JSON
console.log()
console.log(model.fullName)  // Output the full name

The code above will output the following:

上面的代码将输出以下内容:

{ _id: 5a7a4248550ebb9fafd898cf,
  firstName: 'Thomas',
  lastName: 'Anderson' }
  
Thomas Anderson
实例方法 (Instance Methods)

We can create custom helper methods on the schema and access them via the model instance. These methods will have access to the model object and they can be used quite creatively. For instance, we could create a method to find all the people who have the same first name as the current instance.

我们可以在模式上创建自定义帮助方法,并通过模型实例访问它们。 这些方法将可以访问模型对象,并且可以创造性地使用它们。 例如,我们可以创建一个方法来查找所有与当前实例具有相同名字的人。

In this example, let’s create a function to return the initials for the current user. Let’s add a custom helper method called getInitials to the schema:

在此示例中,让我们创建一个函数以返回当前用户的缩写。 让我们向架构添加一个名为getInitials的自定义帮助程序方法:

userSchema.methods.getInitials = function() {
  return this.firstName[0] + this.lastName[0]
}

This method will be accessible via a model instance:

可通过模型实例访问此方法:

let model = new UserModel({
  firstName: 'Thomas',
  lastName: 'Anderson'
})

let initials = model.getInitials()

console.log(initials) // This will output: TA
静态方法 (Static Methods)

Similar to instance methods, we can create static methods on the schema. Let’s create a method to retrieve all users in the database:

与实例方法类似,我们可以在架构上创建静态方法。 让我们创建一个方法来检索数据库中的所有用户:

userSchema.statics.getUsers = function() {
  return new Promise((resolve, reject) => {
    this.find((err, docs) => {
      if(err) {
        console.error(err)
        return reject(err)
      }
      
      resolve(docs)
    })
  })
}

Calling getUsers on the Model class will return all the users in the database:

在Model类上调用getUsers将返回数据库中的所有用户:

UserModel.getUsers()
  .then(docs => {
    console.log(docs)
  })
  .catch(err => {
    console.error(err)
  })

Adding instance and static methods is a nice approach to implement an interface to database interactions on collections and records.

添加实例和静态方法是一种实现接口以与集合和记录进行数据库交互的好方法。

中间件 (Middleware)

Middleware are functions that run at specific stages of a pipeline. Mongoose supports middleware for the following operations:

中间件是在管道的特定阶段运行的功能。 Mongoose支持中间件用于以下操作:

  • Aggregate

    骨料
  • Document

    文件
  • Model

    模型
  • Query

    询问

For instance, models have pre and post functions that take two parameters:

例如,模型具有采用两个参数的prepost功能:

  1. Type of event (‘init’, ‘validate’, ‘save’, ‘remove’)

    事件类型(“ init”,“ validate”,“ save”,“ remove”)
  2. A callback that is executed with this referencing the model instance

    使用引用模型实例执行的回调

Let’s try an example by adding two fields called createdAt and updatedAt to our schema:

让我们通过添加两个字段名为尝试一个例子createdAtupdatedAt到我们的模式:

let mongoose = require('mongoose')

let userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  createdAt: Date,
  updatedAt: Date
})

module.exports = mongoose.model('User', userSchema)

When model.save() is called, there is a pre(‘save’, …) and post(‘save’, …) event that is triggered. For the second parameter, you can pass a function that is called when the event is triggered. These functions take a parameter to the next function in the middleware chain.

调用model.save()时,会model.save()一个pre('save', …)post('save', …)事件。 对于第二个参数,可以传递触发事件时调用的函数。 这些函数将参数带到中间件链中的下一个函数。

Let’s add a pre-save hook and set values for createdAt and updatedAt:

让我们添加一个预存钩和设定值createdAtupdatedAt

userSchema.pre('save', function (next) {
  let now = Date.now()
   
  this.updatedAt = now
  // Set a value for createdAt only if it is null
  if (!this.createdAt) {
    this.createdAt = now
  }
  
  // Call the next function in the pre-save chain
  next()    
})

Let’s create and save our model:

让我们创建并保存我们的模型:

let UserModel = require('./user')

let model = new UserModel({
  fullName: 'Thomas Anderson'
}

msg.save()
   .then(doc => {
     console.log(doc)
   })
   .catch(err => {
     console.error(err)
   })

You should see values for createdAt and updatedAt when the record that is created is printed:

你应该看到值createdAtupdatedAt打印所创建记录:

{ _id: 5a7bbbeebc3b49cb919da675,
  firstName: 'Thomas',
  lastName: 'Anderson',
  updatedAt: 2018-02-08T02:54:38.888Z,
  createdAt: 2018-02-08T02:54:38.888Z,
  __v: 0 }
外挂程式 (Plugins)

Suppose that we want to track when a record was created and last updated on every collection in our database. Instead of repeating the above process, we can create a plugin and apply it to every schema.

假设我们要跟踪记录的创建时间以及对数据库中每个集合的最后更新时间。 无需重复上述过程,我们可以创建一个插件并将其应用于每个架构。

Let’s create a file ./src/model/plugins/timestamp.js and replicate the above functionality as a reusable module:

让我们创建一个文件./src/model/plugins/timestamp.js并将上述功能复制为可重用模块:

module.exports = function timestamp(schema) {

  // Add the two fields to the schema
  schema.add({ 
    createdAt: Date,
    updatedAt: Date
  })

  // Create a pre-save hook
  schema.pre('save', function (next) {
    let now = Date.now()
   
    this.updatedAt = now
    // Set a value for createdAt only if it is null
    if (!this.createdAt) {
      this.createdAt = now
    }
   // Call the next function in the pre-save chain
   next()    
  })
}

To use this plugin, we simply pass it to the schemas that should be given this functionality:

要使用此插件,我们只需将其传递给应具有此功能的架构:

let timestampPlugin = require('./plugins/timestamp')

emailSchema.plugin(timestampPlugin)
userSchema.plugin(timestampPlugin)

查询大厦 (Query Building)

Mongoose has a very rich API that handles many complex operations supported by MongoDB. Consider a query where we can incrementally build query components.

Mongoose具有非常丰富的API,可处理MongoDB支持的许多复杂操作。 考虑一个查询,在这里我们可以逐步构建查询组件。

In this example, we are going to:

在此示例中,我们将执行以下操作:

  1. Find all users

    查找所有用户
  2. Skip the first 100 records

    跳过前100条记录
  3. Limit the results to 10 records

    将结果限制为10条记录
  4. Sort the results by the firstName field

    按名字对结果排序
  5. Select the firstName

    选择名字
  6. Execute that query

    执行该查询
UserModel.find()                   // find all users
         .skip(100)                // skip the first 100 items
         .limit(10)                // limit to 10 items
         .sort({firstName: 1}      // sort ascending by firstName
         .select({firstName: true} // select firstName only
         .exec()                   // execute the query
         .then(docs => {
            console.log(docs)
          })
         .catch(err => {
            console.error(err)
          })

闭幕 (Closing)

We have barely scratched the surface exploring some of the capabilities of Mongoose. It is a rich library full of useful and and powerful features that make it a joy to work with data models in the application layer.

我们勉强摸索着探索猫鼬的某些功能。 它是一个丰富的库,其中包含有用且强大的功能,这使在应用程序层中使用数据模型变得很高兴。

While you can interact with Mongo directly using Mongo Driver, Mongoose will simplify that interaction by allowing you to model relationships between data and validate them easily.

虽然您可以使用Mongo Driver直接与Mongo进行交互,但是Mongoose通过允许您对数据之间的关系进行建模并轻松验证它们,可以简化该交互。

Fun Fact: Mongoose is created by Valeri Karpov who is an incredibly talented engineer! He coined the term The MEAN Stack.

趣闻: 猫鼬Valeri Karpov创作的 谁是一个非常有才华的工程师! 他创造了术语“ MEAN Stack”

如果这篇文章有帮助,??? 然后在Twitter上关注我。 (If this article was helpful, ??? and Follow me on Twitter.)

翻译自: https://www.freecodecamp.org/news/introduction-to-mongoose-for-mongodb-d2a7aa593c57/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值