couchdb_CouchDB上的口袋妖怪速成课程

couchdb

In this tutorial, we’ll walk through working with CouchDB, a NoSQL database from Apache. This tutorial will focus more on the practical side, so we won’t cover what CouchDB is good for, how to install it, why use it, etc. We’ll focus on how to perform database operations through CouchDB’s HTTP API and how to work with it in PHP, laying the foundation for future, more complex posts.

在本教程中,我们将逐步使用CouchDB (Apache的NoSQL数据库)。 本教程将侧重于实践方面,因此我们将不介绍CouchDB的优点,如何安装, 为何使用它等。我们将重点介绍如何通过CouchDB的HTTP API执行数据库操作以及如何在PHP中使用它,为以后更复杂的帖子打下基础。

We’ll assume you’ve already set up CouchDB and Futon (CouchDB’s web-based administration console) on your machine. If you’d like to follow along, we recommend you use our HI box.

我们假设您已经在计算机上设置了CouchDB和Futon(CouchDB的基于Web的管理控制台) 。 如果您想继续,我们建议您使用HI框

CouchDB Logo

Note: for simplicity, we’ll refer to our local machine with localhost here, but if you’re using a fully configured VM you probably have a custom vhost and local domain set up, along with forwarded ports. With Homestead Improved it’s just a matter of listing the ports you want forwarded in the Homestead.yaml configuration file before provisioning the virtual box.

注意:为简单起见,我们将在此处使用localhost引用本地计算机,但是如果您使用的是完全配置的VM,则可能已设置了自定义虚拟主机和本地域以及转发的端口。 使用Homestead Improvementd ,在配置虚拟盒之前,只需在Homestead.yaml配置文件中列出要转发的端口即可。

创建数据库 (Creating a Database)

To create a new CouchDB database, visit Futon at http://localhost:5984/_utils/. You’ll see the following interface:

要创建一个新的CouchDB数据库,请访问位于http://localhost:5984/_utils/ Futon。 您将看到以下界面:

futon

Click on create database, enter a database name and click on create to create the database.

单击创建数据库 ,输入数据库名称,然后单击创建以创建数据库。

Once created, you’ll be greeted with the following screen:

创建完成后,将出现以下屏幕:

couchdb database

Notice that there’s only an option to create a new document. In CouchDB, a document is the equivalent of a table row in a relational database. So, how do we create tables?

请注意,只有一个选项可以创建一个新文档。 在CouchDB中,文档等效于关系数据库中的表行。 那么,我们如何创建表?

If you’re coming from a NoSQL database such as MongoDB, the first thing that you have to know is that there’s no such thing as collections or tables in CouchDB. There are only documents. However, this doesn’t mean that you can only store one type of data per database. Since each document that you create in CouchDB doesn’t belong to any table, you can have a different structure for each type of data. For example, if you want to store user data, you can have a structure similar to the following:

如果您来自NoSQL数据库(例如MongoDB),那么首先要了解的是CouchDB中没有集合或表之类的东西。 只有文件。 但是,这并不意味着每个数据库只能存储一种类型的数据。 由于您在CouchDB中创建的每个文档都不属于任何表,因此每种数据类型可以具有不同的结构。 例如,如果要存储用户数据,则可以具有类似于以下内容的结构:

{
    "id": 123,
    "fname": "doppo",
    "lname": "kunikida",
    "pw": "secret",
    "hobbies": ["reading", "sleeping"]
}

On the other hand, if you want to store blog post information, you can have the following structure:

另一方面,如果要存储博客文章信息,则可以具有以下结构:

{
    "title": "The big brown fox",
    "author": "fox",
    "text": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Earum, quasi in id voluptates. Natus qui iure corporis ea voluptatem eius, possimus optio modi facere blanditiis quo, sequi suscipit eos nostrum.",
    "publish_date": "2016-07-07"
}

To make it easier to query a specific type of document (e.g. users, blog posts) you can add a field for storing the type of document:

为了使查询特定类型的文档(例如,用户,博客文章)更加容易,您可以添加一个字段来存储文档的类型:

{
    "id": "123",
    "fname": "doppo",
    "lname": "kunikida",
    "pw": "secret",
    "hobbies": ["reading", "sleeping"],
    "type": "users"
}

Note that type isn’t a special type of field. This is only used for convenience.

请注意, type不是特殊的字段类型。 这只是为了方便起见。

与HTTP API交谈 (Talking to the HTTP API)

Since CouchDB exposes an HTTP API, we can also use curl to create a database:

由于CouchDB公开了HTTP API,因此我们还可以使用curl创建数据库:

curl -X PUT http://localhost:5984/<database name>

Executing the above command should return the following:

执行以上命令应返回以下内容:

{"ok":true}

CouchDB returns JSON strings as the response. This makes it very easy to use in both the browser and on the server side of things.

CouchDB返回JSON字符串作为响应。 这使得在浏览器和服务器端都非常容易使用。

We recommend Postman for experimentation during this tutorial, as it allows for easy communication with CouchDB’s HTTP API. If you’re new to Postman, this intro might help: API Building and Testing Made Easier with Postman.

我们建议在本教程中进行实验的Postman ,因为它可以方便地与CouchDB的HTTP API通信。 如果您是Postman的新手,那么此介绍可能会有所帮助: 使用Postman可以更轻松地进行API构建和测试

建立新文件 (Creating New Documents)

To create new documents, we need to send a POST request to the database we’ve created:

要创建新文档,我们需要向创建的数据库发送POST请求:

http://localhost:5984/test_couch

When sending a request to CouchDB, one should always remember the following:

向CouchDB发送请求时,请务必记住以下几点:

  • Specify the Content-Type of application/json in the header when passing in some data through POST, PUT or DELETE.

    通过POSTPUTDELETE传递一些数据时,在标头中指定application/jsonContent-Type

  • Wrap strings in double quotes.

    将字符串用双引号引起来。

Here’s an example request for creating a new document:

这是创建新文档的示例请求:

Creating a new document with Postman

批量插入 (Bulk Insert)

To insert multiple rows of data in a single request:

要在单个请求中插入多行数据:

Bulk inserting via Postman

We will also use this data when we play around with retrieving documents later. If you want to follow along, here’s the data to insert: couchdb-bulk.json

稍后我们在处理文档时也将使用此数据。 如果您想继续,请插入以下数据: couchdb-bulk.json

检索文件 (Retrieving Documents)

Let’s try to retrieve all the documents that are currently stored in the database:

让我们尝试检索当前存储在数据库中的所有文档:

Retrieving all documents

By default, CouchDB only returns the unique ID, a key (the same as a unique ID), and the value which is, by default, an object containing the latest revision number (unique string that represents a specific version of the document). We’ll talk more about revisions later.

默认情况下,CouchDB仅返回唯一ID,键(与唯一ID相同)和值(默认情况下是包含最新修订号的对象)(代表文档特定版本的唯一字符串)的值。 稍后我们将详细讨论修订。

To retrieve the data that we have saved earlier, we have to specify include_docs as a query parameter and set it to true:

要检索我们之前保存的数据,我们必须指定include_docs作为查询参数并将其设置为true

http://localhost:5984/test_couch/_all_docs?include_docs=true

检索特定文件 (Retrieving Specific Documents)

In CouchDB, you can retrieve specific documents by using the document ID:

在CouchDB中,您可以使用文档ID检索特定的文档:

http://localhost:5984/test_couch/8939b0d23a0ba7a5ed55fd981d0010a0?include_docs=true

You can also retrieve a specific version of the document by specifying a revision number through the query parameter rev:

您还可以通过查询参数rev指定修订版本号来检索文档的特定版本:

http://localhost:5984/test_couch/8939b0d23a0ba7a5ed55fd981d0010a0?rev=1-1841dec358ff29eca8c42a52f1c2a1d0&include_docs=true

Every time you create a new document or update an existing one, a unique revision number is generated by CouchDB. It then assigns it to that state of the document. For example, if you add a new field called skill and then save the document, CouchDB still keeps a copy of the document right before the skill field was added. It does this every time you make a change (e.g. updating a value for a specific field, removing a field, renaming a field, adding a new field) to the document. This is really useful if you need to keep historical data.

每次创建新文档或更新现有文档时,CouchDB都会生成一个唯一的修订号。 然后将其分配给文档的该状态。 例如,如果您添加了一个称为skill的新字段,然后保存了文档,则CouchDB仍会在添加skill字段之前保留该文档的副本。 每次您对文档进行更改(例如,更新特定字段的值,删除字段,重命名字段,添加新字段)时,都会执行此操作。 如果您需要保留历史数据,这真的很有用。

If you access a specific document in Futon, you can also navigate through its previous versions:

如果您在Futon中访问特定文档,则还可以浏览其先前版本:

Accessing revisions

观看次数 (Views)

Views allow us to extract specific data from the database and order them in a specific way.

视图使我们能够从数据库中提取特定数据并以特定方式对其进行排序。

To create a view, access your database on Futon and in the drop-down in the upper right corner select temporary view. This will show you the following interface:

要创建视图,请在Futon上访问数据库,然后在右上角的下拉列表中选择临时视图 。 这将显示以下界面:

create temporary view

For the rest of the tutorial we’ll be using the data that we’ve inserted earlier.

在本教程的其余部分中,我们将使用我们之前插入的数据

First, let’s look at a function that filters Pokemon by their trainer:

首先,让我们看一下一个功能,该功能可以通过其培训人员过滤神奇宝贝:

function(doc) {
  emit(doc.trainer, doc.name);
}

Add this function as the value for Map Function. This function uses the built-in emit method, which accepts two arguments: the key and the value. The key is the one that’s used for filtering the documents, while the value is the value we want returned for each row.

将此函数添加为Map Function的值。 此函数使用内置的emit方法,该方法接受两个参数:键和值。 关键字是用于过滤文档的关键字,而值是我们希望为每一行返回的值。

Here’s the response that we get when we execute the function:

这是执行函数时得到的响应:

view response

As you can see, it just returns everything. This is because we haven’t really specified any value to be used as the filter. To actually see this function in action, we need to save the view by clicking the save as button.

如您所见,它只会返回所有内容。 这是因为我们还没有真正指定任何值用作过滤器。 要实际查看此功能的实际效果,我们需要单击“ 另存为”按钮来保存视图。

save view

This will ask us for the name of the design document and the name of the view. You can think of design documents as a collection of related views. We’ll name it pokemon since the documents that we’re working on mainly deal with Pokemon data. As for the view, we’ll name it after what it does: filter_by_trainer.

这将要求我们提供设计文档的名称和视图的名称。 您可以将设计文档视为相关视图的集合。 我们将其命名为pokemon,因为我们正在处理的文档主要处理Pokemon数据。 至于视图,我们将其命名为: filter_by_trainer

Now let’s make a request to the view that we’ve just created:

现在,让我们对刚刚创建的视图进行请求:

http://localhost:5984/test_couch/_design/pokemon/_view/filter_by_trainer?key="Ash"

This will return the following results:

这将返回以下结果:

filter by trainer results

What ever you pass in as the value for the query parameter key will be used for filtering the results.

您传递的任何查询参数key都将用于过滤结果。

按数组字段过滤 (Filtering by an Array Field)

What if we need to filter by an array field such as the type field? For that, we’ll need to loop through all the array items and emit documents from inside the loop like so:

如果我们需要按数组字段(例如type字段)进行过滤怎么办? 为此,我们需要遍历所有数组项并从循环内部发出文档,如下所示:

function(doc) {
  for(var x = 0; x < doc.type.length; x++){
     emit(doc.type[x], doc.name); 
  }
}

Save this view as filter_by_type then send a GET request to the following URL:

将此视图另存为filter_by_type然后将GET请求发送到以下URL:

http://localhost:5984/test_couch/_design/pokemon/_view/filter_by_type?key="Fire"

This will return all the Pokemon with “Fire” as one of their types:

这将返回所有以“火”作为其类型之一的神奇宝贝:

filter by type response

排序和限制结果 (Sorting and Limiting Results)

To sort results, all you have to do is emit the field you want to sort with. In this case, we’ll emit the owned field as the key so that we can sort by the date on which the Pokemon was owned or caught.

要对结果进行排序,您要做的就是发出要与之进行排序的字段。 在这种情况下,我们将发出owned字段作为键,以便我们可以按口袋妖怪拥有或被捕的日期排序。

function(doc){
   emit(doc.owned, doc.name);  
}

Save this view as order_by_owned then send a request to the following URL:

将此视图另存为order_by_owned然后将请求发送到以下URL:

http://localhost:5984/test_couch/_design/pokemon/_view/sort_by_owned

By default, CouchDB returns the documents in ascending order, so the Pokemon that have been owned the longest come first. To sort the documents in descending order, specify descending=true as a query parameter:

默认情况下,CouchDB以升序返回文档,因此拥有时间最长的Pokemon排在第一位。 要按降序对文档进行排序,请指定descending=true作为查询参数:

http://localhost:5984/test_couch/_design/pokemon/_view/sort_by_owned?descending=true

To limit the number of documents returned, set limit equal to the number of documents to return:

要限制返回的文档数,请将limit设置为等于要返回的文档数:

http://localhost:5984/test_couch/_design/pokemon/_view/sort_by_owned?limit=5

分组结果 (Grouping Results)

If you want to return the number of Pokemon that each unique trainer has, we need to use a reduce function. The reduce function allows us to perform grouping and aggregation operations on the results returned by the map function. CouchDB comes with three built-in reduce functions: _count, _sum, and _stats. We’ll only look at the _count function in this section.

如果您想返回每个独特的教练员所拥有的Pokemon的数量,我们需要使用reduce函数。 reduce函数允许我们对map函数返回的结果执行分组和聚合操作。 CouchDB带有三个内置的reduce函数: _count_sum_stats 。 我们仅在本节中查看_count函数。

We can use the _count function to get the number of Pokemon that each trainer has. Start by adding the following map function:

我们可以使用_count函数获取每个培训师拥有的口袋妖怪的数量。 首先添加以下地图功能:

function(doc) {
   emit(doc.trainer, doc.name); 
}

For the reduce function, put in _count. Then save the view with the name group_by_trainer.

对于reduce函数,请输入_count 。 然后使用名称group_by_trainer保存该视图。

Make a request to the following URL:

请求以下URL:

http://localhost:5984/test_couch/_design/pokemon/_view/group_by_trainer?group=true

Setting group=true is important because we’ll get the following result from the reduce function by default:

设置group=true很重要,因为默认情况下,我们将从reduce函数获得以下结果:

{
  "rows": [
    {
      "key": null,
      "value": 9
    }
  ]
}

The result above only shows the total number of documents that are currently in the database. This means that the reduce function considered the whole result set returned by the map function to be a single group.

上面的结果仅显示数据库中当前的文档总数。 这意味着reduce函数将map函数返回的整个结果集视为一个单独的组。

Setting group=true tells CouchDB to group the documents by the specified key (doc.trainer), which then returns the following result:

设置group=true告诉CouchDB通过指定的键( doc.trainer )对文档进行doc.trainer ,然后返回以下结果:

{
  "rows": [
    {
      "key": "Ash",
      "value": 5
    },
    {
      "key": "Brock",
      "value": 1
    },
    {
      "key": "Misty",
      "value": 2
    },
    {
      "key": "Team Rocket",
      "value": 1
    }
  ]
}

更新文件 (Updating Documents)

To update documents, we send a PUT request to the same URL used for retrieving a specific document and pass in the latest revision number along with the updated data:

要更新文档,我们将PUT请求发送到用于检索特定文档的相同URL,并将最新的修订号与更新的数据一起传递:

update document

Based on the screenshot above, you can see that CouchDB doesn’t support updating of specific fields: you have to fetch the existing data, do your updates and then send the updated data back to the database.

根据上面的屏幕截图,您可以看到CouchDB不支持特定字段的更新:您必须获取现有数据,进行更新,然后将更新后的数据发送回数据库。

删除文件 (Deleting Documents)

To delete a document, perform a DELETE request to the following URL:

要删除文档,请对以下URL执行DELETE请求:

http://localhost:5984/test_couch/<Document ID>?rev=<Revision ID>

It has the same format as the URL for retrieving a document, and because we’re passing in a revision ID, this means that we can delete specific revisions of a document as well (undo function anyone?).

它具有与检索文档的URL相同的格式,并且由于我们传递的是修订ID,因此这意味着我们也可以删除文档的特定修订(撤消功能吗?)。

使用PHP (Working with PHP)

There are two ways to work with CouchDB in PHP. The first is through Guzzle, and the other one through a library specifically created to work with CouchDB, like the CouchDB Client from Doctrine. We’ll take a look at how to work with each of these libraries in this section.

有两种在PHP中使用CouchDB的方法。 第一个是通过Guzzle进行的 ,另一个是通过专门为与CouchDB配合使用而创建的库,例如Doctrine的CouchDB Client 。 在本节中,我们将研究如何使用这些库。

uzz (Guzzle)

When retrieving data, we have to use a GET request and then pass in a query to specify the options.

检索数据时,我们必须使用GET请求,然后传递query以指定选项。

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new GuzzleHttp\Client(['base_uri' => 'http://localhost:5984']);

$response = $client->request('GET', '/test_couch/_all_docs', [
    'query' => [
        'include_docs' => 'true'
    ]
]);

$json = $response->getBody()->getContents();
$data = json_decode($json, true);

Next, let’s do a bulk insert:

接下来,让我们进行批量插入:

$docs = [
    'docs' => [
        [
            "name" => "Tangela",
            "type" => ["Grass"],
            "trainer" => "Erika",
            "gender" => "f",
            "owned" => "1999-07-27"
        ],
        [
            "name" => "Wobbuffet",
            "type" => ["Psychic"],
            "trainer" => "Elesa",
            "gender" => "m",
            "owned" => "2014-09-09"
        ],
        [
            "name" => "Gogoat",
            "type" => ["Grass"],
            "trainer" => "Ramos",
            "gender" => "m",
            "owned" => "2015-10-17"
        ]
    ]
];

$client->request('POST', '/test_couch/_bulk_docs', [
    'headers' => [
        'Content-Type' => 'application/json'
    ],
    'body' => json_encode($docs)
]);

From the above code, you can see that the same rules still apply.

从上面的代码中,您可以看到相同的规则仍然适用。

To update a document, use a PUT request, pass in the ID of the document as a path right after the name of the database. And then pass in the modified document in the body:

要更新文档,请使用PUT请求,将文档ID作为路径传递到数据库名称之后。 然后将修改后的文档传递到body

$doc = [
    '_rev' => '2-ff235602e45c46aed0f8834c32817546',
    'name' => 'Blastoise',
    'type' => ['Water'],
    'gender' => 'm',
    'trainer' => 'Ash',
    'owned' => '1999-05-21'
];

$client->request('PUT', '/test_couch/5a6a50b7c98499a4d8d69d4bfc00029a', [
    'headers' => [
        'Content-Type' => 'application/json'
    ],
    'body' => json_encode($doc)
]);

To delete a document, use a DELETE request, pass in the document ID as a path after the database name and then pass in the latest revision number in the query:

要删除文档,请使用DELETE请求,在数据库名称后将文档ID作为路径传递,然后在query传递最新的修订号:

$client->request('DELETE', '/test_couch/7c7f800ee10a39f512a456339e0019f3', [
    'query' => [
        'rev' => '1-967a00dff5e02add41819138abb3284d'
    ]
]);

教义CouchDB客户端 (Doctrine CouchDB Client)

Next, let’s take a look at how to work with a CouchDB database with the CouchDB Client.

接下来,让我们看一下如何通过CouchDB Client使用CouchDB数据库。

First, we have to create a new instance of the CouchDBClient and pass in an array containing the name of the database we want to work with.

首先,我们必须创建一个新的CouchDBClient实例,并传递一个包含要使用的数据库名称的数组。

<?php
require 'vendor/autoload.php';

$client = \Doctrine\CouchDB\CouchDBClient::create(['dbname' => 'test_couch']);

Then, we pass in the document we wish to create as an array. Behind the scenes, this will be converted into a JSON string that is accepted by CouchDB.

然后,我们将要创建的文档作为数组传入。 在幕后,它将被转换为CouchDB接受的JSON字符串。

$client->postDocument([
    "name" => "Lucario",
    "type" => ["Fighting", "Steel"],
    "trainer" => "Korrina",
    "gender" => "f",
    "owned" => "2015-02-13"
]);

To retrieve documents using a specific view, we pass in the name of the design document and the name of the view as arguments to the createViewQuery function. Then, we can set the key by using the setKey method. To get a response, we call the execute method.

要使用特定视图检索文档,我们将设计文档的名称和视图的名称作为参数传递给createViewQuery函数。 然后,我们可以使用setKey方法设置密钥。 为了获得响应,我们调用execute方法。

$query = $client->createViewQuery('pokemon', 'filter_by_trainer');
$query->setKey('Ash');
$result = $query->execute();
echo "Pokemon trained by Ash: <br>";
foreach($result as $row){
    echo $row['value'] . "<br>";
}

This will produce the following result:

这将产生以下结果:

Pokemon trained by Ash: 
Blastoise
Pikachu
Charizard
Talonflame
Froakie

If you have added a reduce function to the view, you have to call the setGroup method and set it to true so that the reduce function will not consider the whole result set to be a single group. Setting setGroup to true means that every unique key (the trainer field) is considered to be a single group.

如果在视图中添加了reduce函数,则必须调用setGroup方法并将其设置为true以便reduce函数不会将整个结果集视为单个组。 将setGroup设置为true意味着将每个唯一键( trainer字段)视为一个组。

$query = $client->createViewQuery('pokemon', 'group_by_trainer');
$query->setReduce('true');
$query->setGroup('true');
$result = $query->execute();
foreach($result as $row){
    echo $row['key'] . "<br>";
    echo "Pokemon count: " . $row['value'] . "<br>";
    echo "<br>";
}

This will give us the following result:

这将为我们带来以下结果:

Ash
Pokemon count: 5

Brock
Pokemon count: 1

Elesa
Pokemon count: 1

Erika
Pokemon count: 1

Korrina
Pokemon count: 1

Misty
Pokemon count: 3

Ramos
Pokemon count: 1

Team Rocket
Pokemon count: 1

To update a document, first we have to find the most recent version of it. This is because the CouchDB client doesn’t provide the functionality that lets us just pass in the fields that we want to update. CouchDB is an append-only database so we always need to get the current version of the document, update or add the fields that we want, and then do the actual update.

要更新文档,首先我们必须找到它的最新版本。 这是因为CouchDB客户端不提供使我们仅传递要更新的字段的功能。 CouchDB是一个仅附加数据库,因此我们始终需要获取文档的当前版本,更新或添加所需的字段,然后进行实际更新。

$doc_id = '5a6a50b7c98499a4d8d69d4bfc003c9c';
$doc = $client->findDocument($doc_id);

$updated_doc = $doc->body;
$updated_doc['name'] = 'Golduck';
$updated_doc['owned'] = '1999-07-29';

$client->putDocument($updated_doc, $doc->body['_id'], $doc->body['_rev']);

Just like with updating a document, we need to do a GET request to the database so we can get the latest revision number. Then, we call the deleteDocument function and pass in the document ID and the latest revision number as its arguments.

就像更新文档一样,我们需要对数据库执行GET请求,以便获得最新的修订号。 然后,我们调用deleteDocument函数并传递文档ID和最新修订号作为其参数。

$doc_id = '7c7f800ee10a39f512a456339e0027fe';
$doc = $client->findDocument($doc_id);
$client->deleteDocument($doc_id, $doc->body['_rev']);

结论 (Conclusion)

CouchDB is a very user friendly and easy-to-use document database with an emphasis on performance and version control.

CouchDB是一个非常用户友好且易于使用的文档数据库,重点在于性能和版本控制。

You’ve now learned how to work with it and perform different database operations by using Postman and two PHP clients. If you want to learn more, I recommend you check out the Definitive Guide which contains everything you need to know about CouchDB. In a future post, we’ll go much more in depth and build a proper multi-platform app using everything we’ve seen so far.

您现在已经了解了如何使用Postman和两个PHP客户端使用它并执行不同的数据库操作。 如果您想了解更多信息,建议您阅读《权威指南》 ,其中包含您需要了解的有关CouchDB的所有信息。 在以后的文章中,我们将进行更深入的研究,并使用到目前为止所看到的一切来构建适当的多平台应用程序。

Until then, do like their slogan says – relax!

在此之前,请按照他们的口号所说-放松!

翻译自: https://www.sitepoint.com/a-pokemon-crash-course-on-couchdb/

couchdb

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值