qt中的网络请求QNetworkAccessManager——两种方法同步与异步

目录

一、qt中的网络请求

1.网络的一些基础知识

网络的GET和POST方式

 网络中的鉴权

2.qt实现网络请求方式get和post

GET 请求

3.qt网络请求中添加鉴权信息

1. 生成鉴权字符串

2. 设置鉴权头部到 QNetworkRequest

3. 简单的网络处理响应和错误

二、同步阻塞网络请求

三、异步非阻塞网络请求

四、两种方式结合使用

五、实战进阶

1.当有多个结构体和多个解析函数的时候,有没有更加优化的方式实现上面的功能

2.拼接两个请求的结果

3.有多个请求,并且需要顺序执行完成,各自处理结果

1.先定义一个队列的结构体

2.处理队列的函数,有处理完成和处理下一个两个函数

 3.增加一个过渡传递的处理函数

4.结合之前的同步与异步的处理,在主函数调用


一、qt中的网络请求

1.网络的一些基础知识

网络的GET和POST方式

网络请求中的GET和POST方式存在显著的区别,以下从多个方面进行详细解释:

GETPOST
请求数据大小限制由于请求参数以查询字符串的形式附加在URL上,因此传输数据量受到URL长度的限制。通常,这个长度限制在2048个字符左右,但实际限制可能因浏览器和服务器设置而异请求参数保存在请求体中,因此理论上不受数据大小限制。但在实际应用中,服务器可能会设置POST请求的数据大小上限,这个上限通常远大于GET请求的限制,例如默认为8M。
安全性由于请求参数暴露在URL中,可能会被保存在日志、浏览器历史记录等地方,因此在涉及敏感数据传输时安全性较低。请求参数保存在请求体中,相对于GET请求,POST请求传输的数据更加安全。然而,需要注意的是,即使使用POST请求,也不能完全保证数据的安全性,因为抓包软件仍然可能捕获到请求的内容。如果需要更高的安全性,可以对数据进行加密处理。
缓存与记录如果请求的是静态资源,可能会被缓存;如果是数据,则通常不会被缓存。此外,由于GET请求的参数会暴露在URL中,因此可能会被保存在浏览器历史记录或服务器日志中。请求的数据不会被缓存,也不会保留在浏览器的历史记录中。这增加了数据传输的安全性,但也意味着如果需要重新获取数据,必须重新发送POST请求。
传参方式参数通过URL进行传递,以“key=value&key=value”的形式出现。参数放在请求体中进行传递,可以通过表单或其他方式进行提交。
TCP数据包通常只产生一个TCP数据包,浏览器会一次性将HTTP头部和数据发送出去。可能产生两个TCP数据包。首先,浏览器发送包含HTTP头部的数据包,服务器响应100 Continue后,浏览器再发送包含实际数据的数据包。然而,需要注意的是,在发送POST请求时,如果客户端没有设置Expect头,服务器可能不会发送100 Continue响应。
应用场景常用于从指定的资源请求数据,如搜索、排序和筛选等操作。由于安全性较低,不建议用于提交敏感信息。通常用于向指定的资源提交数据,如表单提交、文件上传、发布文章等操作。由于安全性较高,适合用于提交敏感信息。
 网络中的鉴权

在网络请求中,鉴权(Authentication)是一个过程,用于验证请求者(通常是客户端)的身份和权限,以确保其有权访问特定的资源或执行特定的操作。鉴权是网络安全性的一部分,旨在防止未经授权的访问和数据泄露。

鉴权是网络请求中确保安全性和访问控制的关键环节。通过验证请求者的身份和权限,可以防止未经授权的访问和数据泄露,保护系统的安全性和数据的完整性。

以下是鉴权在网络请求中的一些关键点和常见方法:

  1. 身份验证:验证请求者是否是其所声称的用户。这通常通过用户名和密码、令牌(如JWT、OAuth令牌)、公钥/私钥对、生物识别或其他身份验证机制来完成。
  2. 授权:确定经过身份验证的请求者是否有权访问请求的资源或执行请求的操作。这通常基于用户的角色、权限或策略来决定。
  3. 令牌:在许多现代系统中,鉴权过程使用令牌(Token)作为验证请求者身份和权限的凭据。令牌通常是一串包含用户身份信息和授权信息的字符串,可以在客户端和服务器之间安全地传递。常见的令牌类型包括JWT(JSON Web Tokens)和OAuth令牌。
  4. 会话管理:在传统的基于会话的鉴权系统中,服务器会为用户创建一个会话(Session),并在会话中存储用户的身份验证和授权信息。服务器为每个会话分配一个唯一的会话标识符(如会话ID),并将其发送给客户端。客户端在后续请求中携带会话ID,以便服务器能够识别用户并检索其会话信息。
  5. API密钥:对于API访问,鉴权通常通过API密钥来实现。API密钥是一个唯一的字符串,用于标识和验证调用API的客户端。客户端在API请求中包含API密钥,服务器验证该密钥的有效性,并据此决定是否授权访问。
  6. OAuth和OpenID Connect:OAuth和OpenID Connect是用于第三方应用程序鉴权和授权的开放标准。它们允许用户授权第三方应用程序访问其受保护的资源,而无需共享其用户名和密码。OAuth主要关注授权,而OpenID Connect在OAuth的基础上增加了身份验证功能。
  7. 挑战/响应机制:在某些情况下,服务器可能会向客户端发送一个挑战(Challenge),要求客户端提供证明其身份和权限的信息。客户端响应挑战并提供必要的信息,然后服务器验证这些信息并据此决定是否授权访问。
  8. HTTPS:鉴权过程通常通过HTTPS(HTTP Secure)协议进行,以确保在客户端和服务器之间传输的数据(包括身份验证和授权信息)得到加密保护,防止被恶意第三方截获和篡改。

2.qt实现网络请求方式get和post

GET 请求

  1. 创建 QNetworkAccessManager:首先,你需要一个 QNetworkAccessManager 对象来发送网络请求。

  2. 设置请求 URL:使用 QUrl 对象设置请求的 URL。

  3. 创建 QNetworkRequest:创建一个 QNetworkRequest 对象,并设置其 URL。

  4. 添加鉴权信息:使用 QNetworkRequest::setRawHeader() 方法添加鉴权头。例如,如果你使用的是 Basic Authentication(基本认证),你可能需要生成一个 Base64 编码的字符串,该字符串包含用户名和密码,并将其作为 Authorization 头的值。

QNetworkAccessManager manager;  
QUrl url("http://example.com/api/resource");  
QNetworkRequest request(url);  
  
// 假设你已经有了一个 Base64 编码的鉴权字符串  
QByteArray authHeaderData = "Basic " + yourBase64EncodedAuthString;  
request.setRawHeader(QByteArray("Authorization"), authHeaderData);  
  
// 发送 GET 请求  
manager.get(request);

POST 请求

  1. 创建 QNetworkAccessManager:同样,你需要一个 QNetworkAccessManager 对象。

  2. 设置请求 URL:使用 QUrl 对象设置请求的 URL。

  3. 创建 QNetworkRequest:与 GET 请求相同,你需要创建一个 QNetworkRequest 对象并设置其 URL。

  4. 添加鉴权信息:与 GET 请求相同,使用 QNetworkRequest::setRawHeader() 方法添加鉴权头。

  5. 创建并发送 POST 数据:使用 QNetworkReply 对象的 post() 方法发送 POST 数据。你需要一个 QNetworkRequest 对象和一个包含 POST 数据的 QByteArray 或 QHttpMultiPart 对象。

QNetworkAccessManager manager;  
QUrl url("http://example.com/api/resource");  
QNetworkRequest request(url);  
  
// 添加鉴权信息(与 GET 请求相同)  
QByteArray authHeaderData = "Basic " + yourBase64EncodedAuthString;  
request.setRawHeader(QByteArray("Authorization"), authHeaderData);  
  
// 准备 POST 数据(这里只是一个简单的示例)  
QByteArray postData = "key1=value1&key2=value2";  
  
// 发送 POST 请求  
QNetworkReply *reply = manager.post(request, postData);

3.qt网络请求中添加鉴权信息

在 Qt 中进行网络请求时,无论是 GET 还是 POST 方式,添加鉴权信息通常意味着在请求头(HTTP headers)中包含一些特定的字段,如 Authorization,来验证客户端的身份。以下是如何在 Qt 的 QNetworkRequest 和 QNetworkAccessManager 中为 GET 和 POST 请求添加鉴权信息的步骤:

1. 生成鉴权字符串

首先,你需要生成一个鉴权字符串。这通常取决于你使用的鉴权机制。对于 Basic Authentication(基本认证),你需要将用户名和密码以特定的格式拼接起来,并使用 Base64 编码。

例如,对于 Basic Authentication:

QNetworkAccessManager manager;  
QUrl url("http://example.com/api/resource");  
QNetworkRequest request(url);  
  
// 设置鉴权头部  
request.setRawHeader(QByteArray("Authorization"), "Basic " + authHeaderData);  
  
// 发送 GET 请求  
QNetworkReply *reply = manager.get(request);  
  
// ... 处理 reply 和错误情况 ...

2. 设置鉴权头部到 QNetworkRequest

然后,你可以将这个 Base64 编码的字符串设置为 Authorization 头的值,并添加到 QNetworkRequest 对象中。

对于 GET 请求:

QNetworkAccessManager manager;  
QUrl url("http://example.com/api/resource");  
QNetworkRequest request(url);  
  
// 设置鉴权头部  
request.setRawHeader(QByteArray("Authorization"), "Basic " + authHeaderData);  
  
// 发送 GET 请求  
QNetworkReply *reply = manager.get(request);  
  
// ... 处理 reply 和错误情况 ...

对于 POST 请求:

QNetworkAccessManager manager;  
QUrl url("http://example.com/api/resource");  
QNetworkRequest request(url);  
  
// 设置鉴权头部  
request.setRawHeader(QByteArray("Authorization"), "Basic " + authHeaderData);  
  
// 准备 POST 数据  
QByteArray postData = "key1=value1&key2=value2";  
  
// 发送 POST 请求  
QNetworkReply *reply = manager.post(request, postData);  
  
// ... 处理 reply 和错误情况 ...
在上面的代码中,"Basic "是 Basic Authentication 机制的标准前缀,后面跟着的是 Base64 编码的用户名和密码字符串。
3. 简单的网络处理响应和错误

不要忘记连接 QNetworkAccessManager 和 QNetworkReply 的信号来处理响应和错误。例如,你可以连接 QNetworkReply::finished() 信号来处理响应完成的情况,以及 QNetworkReply::errorOccurred() 信号来处理网络错误。

connect(reply, &QNetworkReply::finished, this, [reply]() {  
    if (reply->error() == QNetworkReply::NoError) {  
        // 处理响应数据  
        QByteArray responseData = reply->readAll();  
        // ...  
    } else {  
        // 处理错误  
        qDebug() << "Error:" << reply->errorString();  
    }  
    reply->deleteLater(); // 清理 QNetworkReply 对象  
});
确保在不再需要 QNetworkReply对象时调用其 deleteLater()方法,以避免内存泄漏。

到此,网络请求的简单方法已经写完了,但是聪明的小伙伴已经发现了,由于网络请求是有延迟的,也就是要等到网络接收finish信号,然后再执行槽函数,而我们知道,如果这个时候有多个请求呢,多个请求用同一个网络对象,这个时候前一个的url地址还没有finish,下一个请求的url地址已经改变,这个时候,第一个的请求就不会收到结束的数据了,此时就需要干预了,本人总结了两种方法来解决这个问题

二、同步阻塞网络请求

主要的方法就是利用QEventLoop在接收网络返回内容结束之前,一直阻塞等待,这样再调用一个函数接口的时候,可以同步返回结果。

定义结构体 A

struct A {
    int data; // 示例数据
    // 其他字段
};

主类定义:

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QEventLoop>
#include <QDebug>

class NetworkManager : public QObject {
    Q_OBJECT

public:
    NetworkManager(QObject* parent = nullptr) : QObject(parent) {
        manager = new QNetworkAccessManager(this);
    }

    A getA() {
        QEventLoop loop;
        A result;

        // 发起网络请求
        QNetworkRequest request(QUrl("http://www.example.com"));
        QNetworkReply* reply = manager->get(request);

        // 连接槽函数解析数据
        connect(reply, &QNetworkReply::finished, [&]() {
            if (reply->error() == QNetworkReply::NoError) {
                QByteArray response = reply->readAll();
                result = parseResponse(response);
            } else {
                qWarning() << "Network error:" << reply->errorString();
            }
            reply->deleteLater();
            loop.quit();
        });

        // 阻塞等待网络请求完成
        loop.exec();

        return result;
    }

private:
    QNetworkAccessManager* manager;

    A parseResponse(const QByteArray& response) {
        A parsedData;
        // 假设解析为整数数据
        parsedData.data = QString(response).toInt();
        return parsedData;
    }
};

主函数:

#include <QCoreApplication>

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    NetworkManager networkManager;
    A data = networkManager.getA();
    qDebug() << "Received data:" << data.data;

    return a.exec();
}

当然同步的方法是有劣势的,比如阻塞,这个时候是会卡住程序的,因为要等待网络接收完成,对于简单的请求或者并不多的请求数量来说是没什么问题的,但是请求多,需要时常更新请求等等问题的时候,就要使用另外一种方式了,那就是异步请求

三、异步非阻塞网络请求

 定义结构体 A

struct A {
    int data; // 示例数据
    // 其他字段
};

主类定义:

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDebug>
#include <functional>

class NetworkManager : public QObject {
    Q_OBJECT

public:
    using Callback = std::function<void(const A&)>;

    NetworkManager(QObject* parent = nullptr) : QObject(parent) {
        manager = new QNetworkAccessManager(this);
    }

    void getA(const Callback& callback) {
        // 发起网络请求
        QNetworkRequest request(QUrl("http://www.example.com"));
        QNetworkReply* reply = manager->get(request);

        // 连接槽函数解析数据
        connect(reply, &QNetworkReply::finished, this, [this, reply, callback]() {
            A result;
            if (reply->error() == QNetworkReply::NoError) {
                QByteArray response = reply->readAll();
                result = parseResponse(response);
            } else {
                qWarning() << "Network error:" << reply->errorString();
            }
            reply->deleteLater();
            callback(result);
        });
    }

private:
    QNetworkAccessManager* manager;

    A parseResponse(const QByteArray& response) {
        A parsedData;
        // 假设解析为整数数据
        parsedData.data = QString(response).toInt();
        return parsedData;
    }
};

主函数:

#include <QCoreApplication>

void handleResult(const A& data) {
    qDebug() << "Received data:" << data.data;
    QCoreApplication::quit();
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    NetworkManager networkManager;
    networkManager.getA(handleResult);

    return a.exec();
}

四、两种方式结合使用

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDebug>
#include <functional>
#include <QEventLoop>

class NetworkManager : public QObject {
    Q_OBJECT

public:
    using Callback = std::function<void(const A&)>;

    NetworkManager(QObject* parent = nullptr) : QObject(parent) {
        manager = new QNetworkAccessManager(this);
    }

    void getAAsync(const Callback& callback) {
        // 发起网络请求
        QNetworkRequest request(QUrl("http://www.example.com"));
        QNetworkReply* reply = manager->get(request);

        // 连接槽函数解析数据
        connect(reply, &QNetworkReply::finished, this, [this, reply, callback]() {
            A result;
            if (reply->error() == QNetworkReply::NoError) {
                QByteArray response = reply->readAll();
                result = parseResponse(response);
            } else {
                qWarning() << "Network error:" << reply->errorString();
            }
            reply->deleteLater();
            callback(result);
        });
    }

    A getASync() {
        QEventLoop loop;
        A result;

        getAAsync([&](const A& data) {
            result = data;
            loop.quit();
        });

        loop.exec(); // 阻塞,直到数据准备好
        return result;
    }

private:
    QNetworkAccessManager* manager;

    A parseResponse(const QByteArray& response) {
        A parsedData;
        // 假设解析为整数数据
        parsedData.data = QString(response).toInt();
        return parsedData;
    }
};

五、实战进阶

1.当有多个结构体和多个解析函数的时候,有没有更加优化的方式实现上面的功能

可以通过泛型和策略模式来优化代码结构。你可以创建一个通用的异步请求处理类,该类可以接受不同的解析函数和回调函数,以处理不同类型的结构体。

定义不同的结构体:

struct A {
    int data;
    // 其他字段
};

struct B {
    QString text;
    // 其他字段
};

定义解析函数:

A parseA(const QByteArray& response) {
    A parsedData;
    // 假设解析为整数数据
    parsedData.data = QString(response).toInt();
    return parsedData;
}

B parseB(const QByteArray& response) {
    B parsedData;
    // 假设解析为字符串数据
    parsedData.text = QString(response);
    return parsedData;
}

通用网络管理器类:

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDebug>
#include <functional>
#include <QEventLoop>

class NetworkManager : public QObject {
    Q_OBJECT

public:
    using ErrorCallback = std::function<void(const QString&)>;

    NetworkManager(QObject* parent = nullptr) : QObject(parent) {
        manager = new QNetworkAccessManager(this);
    }

    template <typename T>
    void getAsync(const QUrl& url, std::function<T(const QByteArray&)> parser, std::function<void(const T&)> callback, ErrorCallback errorCallback = nullptr) {
        QNetworkRequest request(url);
        QNetworkReply* reply = manager->get(request);

        connect(reply, &QNetworkReply::finished, this, [this, reply, parser, callback, errorCallback]() {
            if (reply->error() == QNetworkReply::NoError) {
                QByteArray response = reply->readAll();
                T result = parser(response);
                callback(result);
            } else {
                if (errorCallback) {
                    errorCallback(reply->errorString());
                } else {
                    qWarning() << "Network error:" << reply->errorString();
                }
            }
            reply->deleteLater();
        });
    }

    template <typename T>
    T getSync(const QUrl& url, std::function<T(const QByteArray&)> parser, ErrorCallback errorCallback = nullptr) {
        QEventLoop loop;
        T result;

        getAsync<T>(url, parser, [&](const T& data) {
            result = data;
            loop.quit();
        }, [&](const QString& error) {
            if (errorCallback) {
                errorCallback(error);
            }
            loop.quit();
        });

        loop.exec();
        return result;
    }

private:
    QNetworkAccessManager* manager;
};

主函数:

#include <QCoreApplication>

void handleResultA(const A& data) {
    qDebug() << "Received data for A:" << data.data;
}

void handleResultB(const B& data) {
    qDebug() << "Received data for B:" << data.text;
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    NetworkManager networkManager;

    // 异步请求示例
    networkManager.getAsync<A>(QUrl("http://www.example.com/api/a"), parseA, handleResultA);
    networkManager.getAsync<B>(QUrl("http://www.example.com/api/b"), parseB, handleResultB);

    // 同步请求示例
    A dataA = networkManager.getSync<A>(QUrl("http://www.example.com/api/a"), parseA);
    qDebug() << "Synchronously received data for A:" << dataA.data;

    B dataB = networkManager.getSync<B>(QUrl("http://www.example.com/api/b"), parseB);
    qDebug() << "Synchronously received data for B:" << dataB.text;

    return a.exec();
}

2.拼接两个请求的结果

void Widget::getCsync(std::function<void (const C &)> callback, ErrorCallback errorCallback)
{
    C result;
    int completedRequests = 0;
    const int totalRequests = 2;

    auto checkCompletion = [&]() {
        if (++completedRequests == totalRequests) {
            callback(result);
        }
    };

    getAsync<int>(QUrl("http://www.example.com/api/part1"), parsePart1,
                  [&](const int& data) {
        result.dataA = data;
        checkCompletion();
    },
    errorCallback);

    getAsync<QString>(QUrl("http://www.example.com/api/part2"), parsePart2,
                      [&](const QString& data) {
        result.textB = data;
        checkCompletion();
    },
    errorCallback);
}

3.有多个请求,并且需要顺序执行完成,各自处理结果

因为异步的时候,如果这时候有多个请求呢,就会遇到我说过的问题,前一个请求还没结束,下一个就开始了,这样前一个的槽函数就不会执行,这个时候就需要增加一些方法来让第一个请求结束之后再执行第二个,而且整体都要是异步的。

那么就来到我们最终版本,队列异步处理

1.先定义一个队列的结构体
// 定义一个通用的处理结构
struct RequestInfo {
    std::function<QUrl()> urlGenerator;
    std::function<void(const QByteArray&)> parser;
    std::function<void(const QByteArray&)> callback;
};
2.处理队列的函数,有处理完成和处理下一个两个函数
template <typename T>
void Widget::handleRequestCompleted(const QByteArray& response, std::function<T (const QByteArray&)> parse, std::function<void(const T&)> callback,  QQueue<RequestInfo>& requestQueue) {

    qDebug() << "Request completed.";
    // 处理数据,可以通过回调函数调用不同的处理函数
    T data = parse(response);
    callback(data);

    if (!requestQueue.isEmpty()) {
        processNextRequest(requestQueue);
    } else {
        qDebug() << "All requests completed.";
    }
}

void Widget::processNextRequest(QQueue<RequestInfo>& requestQueue) {
    RequestInfo requestInfo = requestQueue.dequeue();
    QUrl url = requestInfo.urlGenerator();
    qDebug() << "Starting request for URL:";

    auto parserFunction = requestInfo.parser;  // 将解析函数保存在局部变量中
    auto callbackFunction = requestInfo.callback;  // 将回调函数保存在局部变量中

    getAsync<QByteArray>(url, [&](const QByteArray& response) { return this->parseALL(response); }, [callbackFunction, this](const QByteArray& response) {
        callbackFunction(response);  // 调用回调函数
    }, handleError);
}
 3.增加一个过渡传递的处理函数
//异步中多请求过渡的函数
QByteArray Widget::parseALL(const QByteArray& response)
{
    QJsonDocument doc = QJsonDocument::fromJson(response);
    // 检查是否解析成功
    if (doc.isNull()) {
        qDebug() << "解析 JSON 失败";
    } else {
        // 获取根对象
        QJsonObject rootObj = doc.object();

        // 检查是否存在 "code" 键
        if (rootObj.contains("code")) {
            // 获取 "code" 键对应的值
            int codeValue = rootObj["code"].toInt();

            // 打印值(如果不为 0)
            if (codeValue != 0 && codeValue != 200) {
                qDebug() << "parseALL" << QString::fromUtf8(response);
            }else{
                qDebug() << "访问成功code:" << codeValue;
            }

        }
    }
    return response;
}
4.结合之前的同步与异步的处理,在主函数调用
    // 将不同的请求加入队列
    requestQueue.enqueue({ [&]() { return this->generateUrl1(); },
                           [&](const QByteArray& response) { return this->parseALL(response); },
                           [&](const QByteArray& response) {
                               handleRequestCompleted<A>(response,
                               [this](const QByteArray& response) { return this->parseA(response); },
                               [this](const A& data) { return this->handleAData(data); },
                               requestQueue);
                           }});


    requestQueue.enqueue({ [&]() { return this->generateUrl2(); },
                           [&](const QByteArray& response) { return this->parseALL(response); }, 
                           [&](const QByteArray& response) {
                               handleRequestCompleted<B>(response,
                               [this](const QByteArray& response) { return this->parseB(response); },
                               [this](const B& data) { return this->handleResultB(data); },
                               requestQueue);
                           }});

    requestQueue.enqueue({ [&]() { return this->generateUrl3(); },
                           [&](const QByteArray& response) { return this->parseALL(response); }, 
                           [&](const QByteArray& response) {
                               handleRequestCompleted<C>(response,
                               [this](const QByteArray& response) { return this->parseC(response); },
                               [this](const C& data) { return this->handleCData(data); },
                               requestQueue);
                           }});

    if (!requestQueue.isEmpty()) {
        processNextRequest(requestQueue);
    } else {
        qDebug() << "No URLs to process.";
    }

上方的队列,由于我将parse处理函数,generate生成url函数,handle回调处理函数都写成了类的私有函数,所以这里需要传函数指针,如果把这些函数写在类外,则不需要像我的写法这样麻烦。可能写的会有点乱,而且实际的代码已经改的面目全非,如果有问题可以留言评论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值