C++程序练习(多线程)

C++程序练习

本程序是从工作中一个项目里得到的灵感,借鉴了项目里的设计思路。本程序只用到了多线程,多态性、C++标准
容器、C++随机数生成引擎、模版类、模版函数、宏定义函数以及system系统调用等等。
个人感觉还是有质量的,看1遍是不可能有人能看出来个意思的(如果不看我写的程序结构的话),呃呃呃。

程序结构

  1. DesData:目标数据类。其有5个属性: int sonage; int sonmoney; int dauage; bool isdaubea; int daumoney;(由于种种原因导致属性名称显得很是奇怪,因为是记录,就懒得改了),为了方便操作,只设置了int和bool类型。

  2. Adapter< T > 类:一个抽象基类,作为操作实体类Son,BeaAgeM的接口,声明一些操作函数,如Parse()虚函数,目的在于接受模拟数据并对其解析,UpdateData()为了及时更新发送数据,保证数据及时更新。因为是案例,全都把本类的函数设置成了纯虚函数,也就是其子类必须全部重写这些函数,即子类都具有数据的发送和接收的功能。

  3. Son:主要模拟接收和发送目标数据类DesData的sonage以及sonmoney属性。

  4. Daughter:主要模拟接收和发送目标数据类DesData的dauage、isdaubea以及daumoney属性。

  5. Manager< T >:可看做一个的管理数据的容器。主要有三个成员属性:
    a. unordered_map<uint32_t, unique_ptr<Adapter > > _recvmap; 保存接收数据的实体类指针。
    b. unordered_map<uint32_t, unique_ptr<Adapter > > _sendmap; 保存发送数据的实体类指针。
    c. T mydata; 只要操作的目标数据实体。

  6. RecvData:用于不停接受模拟数据的接收线程。

  7. SendData: 用于不停更新并发送数据的发送线程。

  8. SendFrame: 用于更新发送数据的接口类。主要有三个属性:Adapter* pdata = nullptr; 及时获取数据变化,绑定于TestData中建立的用于发送数据的实体类指针;Frame data_update;用于把更新内容保存下来,从pdata取值; Frame data_send; 用于发送出去的数据,从data_update取值。

  9. SensorT< T >: 用于把管理类Manager、RecvData以及SendData结合接口。实现初始化以及启动接收和发送线程,也同时把建立具体对象实体和对象共有的操作分开,使代码更简洁。

  10. TestData:SensorT的子类,主要建立并初始化Son以及Daughter等具体的数据类实体。通过Manage的AddRecv< T >()以及AddSend< T >()方法,为数据类Son,Daughter分配内存,并用unique_ptr管理其生命周期,以各自的id为键存入Manager里对应的Map里。 这里注意RegisterSend()方法,对于需要发送的数据类,必须建立一个该对象指针,并调用RegisterSend()函数,一是用于初始化该指针(通过类的指定ID,指向Manager的Map里找到其对应的一块内存),二是初始化SendData的vsendlist_里的发送对象实体SendFrame,把其中的pdata指针与其绑定,以便及时更新要发送的数据。总结起来,对于发送,一共有三处指针指向同一块内存空间起初是调用AddSend()方法时,在Manager的sendMap里初始化了一块内存记为M,然后在调用RegisterSend()方法后,使得TestData类中的用于处理发送数据的指针(记为TP)初始化指向了M内存空间,最后,把SendFrame的pdata指针与TestData类TP*绑定也指向M内存空间。*

  11. TMain:主要看下面两个线程:一个HandleRecvThread线程,一个UpdateSendThread线程,此处的HandleRecvThread线程只是把从后台RecvData里的接收线程已经收到的DesData数据,以一定周期显示出来。UpdateSendThread线程则是把从后台收到的DesData数据,作为新数据,以一定周期更新发送数据,后台SendData里的发送线程则会一直以指定的周期发送数据。为了保证数据一致性,数据的更新周期要小于数据的发送周期,本程序是缩小了10倍。

程序运行效果

在此借用了”boxes“显示输出工具,所以要先使用"sudo apt-get install boxes"命令,安装boxes,再运行程序。

在这里插入图片描述

在此借用了”xcowsay“显示输出工具,所以要先使用"sudo apt-get install xcowsay"命令,安装boxes,
并更改程序开头的“SYSDISPALYFUN”字段为"xcowsay",再运行程序。

在这里插入图片描述

编译命令

在程序当前目录使用:g++ main.cpp -std=c++11 -lpthread  -o main
运行            : ./main

程序代码

	为了阅读和书写方便把所有代码只写在了同一个cpp里以及方法的实现都是类内实现。
注意:由于打印输出用到了系统调用,所以运行本程序之前,需要在Linux系统上安装"boxes",或者其他
可用管道同步输出的工具,比如“cowsay"或者 	"xcowsay"、“toilet”、”figlet“等等, 然后替换
程序开头的“SYSDISPALYFUN”字段即可。本程序默认使用”boxes“显示,推荐试下"xcowsay",不要太有趣~!
/******************************************************************************
 * Complete by Solitary_Tang(Tang xiao long) at 2021-3-10 11:05.
 * Destination: just regard as a exersice.
 * Language: C++
 * System: Linux
 * Tips: May be, you should install "boxes" and "xcowsay" before run this code.
 * CSND blog link: https://blog.csdn.net/qq_37174816/article/details/114633330
 *****************************************************************************/
#include <iostream>
#include <vector>
#include <memory>
#include <iomanip>
#include <cstring>
#include <unordered_map>
#include <thread>
#include <bitset>
#include <random>
#include <mutex>
#include <chrono>
#include "util.h"
#define MAX_DATA_LEN 32
using namespace std;
using namespace std::chrono;
using Timepointer = std::chrono::system_clock::time_point;
std::mutex m_recv_data;
std::mutex m_send_data;
/**
 * @brief delta_period
 * thread delay time
 * default is 100ms
 */
uint32_t delta_period = 100;
/**
 * @brief SYSDISPALYFUN
 * Dispaly info with the command named "boxes",
 * you can change it to "xcowsay" for example.
 */
string SYSDISPALYFUN = "boxes";
struct Frame{
    uint32_t ID;
    uint8_t len;
    uint32_t data[3];
    Frame():ID(0),len(0)
    {
        std::memset(data, 0, sizeof(data));
    }
};
struct realData
{
    __uint32_t id;
    __uint8_t dlc;
    uint32_t data[3];
};
/**
 * @brief The DesData class
 * Main data class
 */
class DesData
{
public:
    DesData() = default;
    //    DesData(DesData&& fa):age(std::move(fa.age)),money(std::move(fa.money)){}
    int getSonmoney() const
    {
        return sonmoney;
    }
    void setSonmoney(int value)
    {
        this->sonmoney = value;
    }

    int getDauage() const
    {
        return dauage;
    }
    void setDauage(int value)
    {
        this->dauage = value;
    }

    bool getIsdaubea() const
    {
        return isdaubea;
    }
    void setIsdaubea(bool value)
    {
        this->isdaubea = value;
    }

    int getDaumoney() const
    {
        return daumoney;
    }
    void setDaumoney(int value)
    {
        this->daumoney = value;
    }

    int getSonage() const
    {
        return sonage;
    }
    void setSonage(int value)
    {
        this->sonage = value;
    }
    void console() const
    {
        string strinfo = "\necho '";
        strinfo += "RecvData:\n";
        strinfo += "Sonage\tSonmoney($)\tdauage\tdaumoney($)\tDbeautiful\n";
        strinfo += to_string(sonage)+"\t"+to_string(sonmoney)+"\t\t";
        strinfo += to_string(dauage)+"\t"+to_string(daumoney)+"\t\t";
        strinfo += (isdaubea>0) ? "true\t\t' |"+SYSDISPALYFUN : "false\t\t' | "+SYSDISPALYFUN;
        system( strinfo.c_str() );
    }
private:
    int sonage;
    int sonmoney;
    int dauage;
    bool isdaubea = false;
    int daumoney;
};

/**
 * @brief The Adapter class
 * The basic class of class order to Recv or Send elements
 */
template <typename SensorType>
class Adapter
{
public:
    Adapter() = default;
    virtual void print() const = 0;
    virtual uint8_t GetLen() const = 0;
    virtual uint32_t GetId() const = 0;
    virtual void Parse(const uint32_t* , SensorType* ) const = 0;
    virtual void UpdateData(uint32_t* ) =0;
};

class Son : public Adapter<DesData>
{
public:
    static const uint32_t ID ;
    Son() = default;
    Son(int age, int money)
    {
        this->age = age;
        this->money = money;
    }
    void print() const override
    {
        string strinfo = "\necho '";
        strinfo += "SendData:\n";
        strinfo += "Sonid\tSonage\tSonmoney($)\t\t\t\n";
        strinfo += to_string(ID)+" \t"+to_string(age)+" \t";
        strinfo += to_string(money)+"\t' | "+SYSDISPALYFUN;
        system( strinfo.c_str() );
    }
    void setAge(int a)
    {
        age = a;
    }
    void setMoney(int m)
    {
        money = m;
    }
    void setLen(uint8_t len)
    {
        this->len = len;
    }

    uint8_t GetLen() const override
    {
        return len;
    }
    uint32_t GetId() const override
    {
        return ID;
    }
    void Parse(const uint32_t* data, DesData* fa) const override
    {
        fa->setSonage(static_cast<int>(data[0]));
        fa->setSonmoney(static_cast<int>(data[1]));
    }
    void UpdateData(uint32_t* des) override
    {
        struct Temp{
            int age;
            int money;
        };
        Temp t = {age, money};
        memcpy(des, &t, sizeof(Temp));
    }
private:
    uint8_t len = 8;
    int age;
    int money;
};

class Daughter : public Adapter<DesData>
{
public:
    static const uint32_t ID;
    Daughter() = default;
    Daughter(int age)
    {
        this->age = age;
    }
    Daughter(int age, bool beautiful, int money):beautiful(beautiful)
    {
        this->age = age;
        this->money = money;
    }
    void print() const override
    {
        string strinfo = "\necho '";
        strinfo += "SendData:\n";
        strinfo += "Dauid\tDauage\tDauisBeautiful\tDaumoney($)\t\t\t\n";
        strinfo += to_string(ID)+" \t"+to_string(age)+" \t";
        strinfo += (beautiful>0) ? "true\t\t" : "false\t\t";
        strinfo += to_string(money)+"\t' | "+SYSDISPALYFUN;
        system( strinfo.c_str() );
    }
    void setAge(int a)
    {
        age = a;
    }
    void setbeautiful(bool b)
    {
        beautiful = b;
    }
    void setMoney(int m)
    {
        money = m;
    }
    uint8_t GetLen() const override
    {
        return len;
    }
    uint32_t GetId() const override
    {
        return ID;
    }
    void Parse(const uint32_t* data, DesData* fa) const override
    {
        fa->setDauage(data[0]);
        fa->setIsdaubea(static_cast<bool>(data[1]));
        fa->setDaumoney(data[2]);
    }
    void UpdateData(uint32_t* des) override
    {
        des[0] = age;
        des[1] = static_cast<uint32_t>(beautiful);
        des[2] = money;
    }
private:
    uint8_t len = 12;
    int age = 0;
    bool beautiful = false;
    int money = 0;
};
const uint32_t Son::ID = 0x01;
const uint32_t Daughter::ID = 0x02;

/**
 * @brief The Manager class
 * You can regard it as a vector to reserve Adapter-type pointer.
 */
template <typename T>
class Manager
{
private:
    unordered_map<uint32_t, unique_ptr<Adapter<T> > > _recvmap;
    unordered_map<uint32_t, unique_ptr<Adapter<T> > > _sendmap;
    T mydata;

public:
    Manager() = default;
    ~Manager(){cout<<"~Manager()"<<endl;}
    template<typename S>
    inline bool Addrecv()
    {
        _recvmap[S::ID] = make_unique<S>();
        return true;
    }
    template<typename S>
    inline bool Addsend()
    {
        _sendmap[S::ID] = make_unique<S>();
        return true;
    }
    Adapter<T>* GetMutableAdapterById(const uint32_t id)
    {
        if(_sendmap.find(id) == _sendmap.end())
            return nullptr;
        return _sendmap[id].get();
    }
    void Parse(const uint32_t* data, const uint32_t id)
    {
        const auto& it = _recvmap.find(id);
        if(it == _recvmap.end())
        {
            cout << "Recv Error Id is "<<id<<endl;
        }else
        {
            std::lock_guard<std::mutex> lock(m_recv_data);
            it->second.get()->Parse(data, &mydata);
        }


    }
    void GetData(T* p) const
    {
        std::lock_guard<std::mutex> lock(m_recv_data);
        *p = mydata;
    }
};

/**
 * @brief The RecvData class
 * A thread-class to receive the simulate data.
 */
template<typename T>
class RecvData
{
private:
    Manager<T>* recvmanager_;
    unique_ptr<thread> thread_;
    bool isrunning = false;
public:
    RecvData() = default;
    ~RecvData()
    {
        StopRecv();
    }

    bool init(Manager<T>* sh)
    {
        if(nullptr == sh)
            ERROR("init RecvData manager pointer Failed!");
        recvmanager_ = sh;
        return true;
    }
    void StopRecv()
    {
        if(isrunning)
        {
            std::cout << "stop recv data thread begin...." << std::endl;
            isrunning = false;
            if(thread_ != nullptr && thread_->joinable())
                thread_->join();
            thread_.reset();
            std::cout << "stop recv data successfully end" << std::endl;
        }else
            std::cout << "the recv data thread has stoped!" << std::endl;
    }
    void SimulateRecv(Frame& frame)
    {
        realData realdata;
        static default_random_engine e(time(NULL));
        uniform_int_distribution<unsigned int> uid(1,2);
        uniform_int_distribution<int> uage(10,50);
        uniform_int_distribution<int> umoney(100000,9000000);
        realdata.id = uid(e);
        if(1 == realdata.id)
        {
            realdata.data[0] = uage(e);
            realdata.data[1] = umoney(e);
            realdata.dlc = 8;
        }else if(2 == realdata.id)
        {
            bernoulli_distribution b;
            realdata.data[0] = uage(e);
            realdata.data[1] = b(e);
            realdata.data[2] = umoney(e);
            realdata.dlc = 12;
        }
        frame.ID = realdata.id;
        frame.len = realdata.dlc;
        memcpy(frame.data, realdata.data, frame.len);
    }

    void HandleRecvThread()
    {
        Timepointer tm_start;
        Timepointer tm_end;
        uint64_t sleep_interval = 0;
        while(isrunning)
        {
            tm_start = std::chrono::system_clock::now();
            Frame frame;
            SimulateRecv(frame);
            recvmanager_->Parse(frame.data, frame.ID);
            tm_end = std::chrono::system_clock::now();
            sleep_interval = delta_period -
                    (std::chrono::duration_cast<std::chrono::milliseconds>
                     (tm_end - tm_start).count())/1e6;
            if(sleep_interval > 0)
                std::this_thread::sleep_for(std::chrono::milliseconds(sleep_interval));
        }
    }

    bool StartRecv()
    {
        if(isrunning)
            ERROR("recv data thread is running!");
        isrunning = true;
        thread_.reset(new thread(
                          [this]
        {
            HandleRecvThread();
        }));
        return true;
    }
};

template <typename T>
class SendFrame
{
public:
    SendFrame() = default;
    ~SendFrame() {cout<<"~SendFrame()"<<endl;}
    SendFrame(uint32_t id, Adapter<T>* pt)
    {
        if(nullptr == pt)
            cout<<"Error!!! try initial 'pdata' (Adapter<T>*)of SendFrame with a null pointer"
               <<endl;
        data_update.ID = id;
        data_update.len = pt->GetLen();
        pdata = pt;
        Update();
    }
    bool Update()
    {
        if(nullptr == pdata)
        {
            string errorinfo = "Update failed! Cuz the data pointer is null! id = ";
            errorinfo.append(to_string(data_update.ID));
            ERROR(errorinfo);
        }
        pdata->UpdateData(data_update.data);

        std::lock_guard<mutex> lock(m_send_data);
        data_send = data_update;
        return true;
    }
    Frame GetSendData() const
    {
        std::lock_guard<mutex> lock(m_send_data);
        return data_send;
    }

private:
    Frame data_update;
    Frame data_send;
    Adapter<T>* pdata = nullptr;
};

/**
 * @brief The SendData class
 * A thread-class to send the data that received by RecvData class.
 */
template<typename T>
class SendData
{
public:
    SendData() = default;
    ~SendData()
    {
        StopSend();
    }

    bool AddSendData(const uint32_t id, Adapter<T>* p)
    {
        if(nullptr == p)
            ERROR("Invailed data pointer!");
        vsendlist_.emplace_back(SendFrame<T>(id, p));
        return true;
    }

    void Update()
    {
        for(auto &i : vsendlist_)
        {
            i.Update();
        }
    }

    bool init(Manager<T>* sh)
    {
        if(nullptr == sh)
            ERROR("init SendData manager pointer Failed!");
        sendmanager_ = sh;
        return true;
    }
    void StopSend()
    {
        if(isrunning)
        {
            std::cout << "stop send data thread begin...." << std::endl;
            isrunning = false;
            if(thread_ != nullptr && thread_->joinable())
                thread_->join();
            thread_.reset();
            std::cout << "stop send data successfully end" << std::endl;
        }else
            std::cout << "the send data thread has stoped!" << std::endl;
    }

    bool StartSend()
    {
        if(isrunning)
            ERROR("The send thread has running!");
        isrunning = true;
        thread_.reset(new thread(
                          [this]
        {
            HandleSendThread();
        }
        ));
        return true;
    }
    bool SendSingleFrame(Frame frame)
    {
        Son Sonre;
        Daughter Daughterre;
        realData real;
        memcpy(&real, &frame, sizeof(frame));
        switch (frame.ID) {
        case 1:
            Sonre = Son(static_cast<int>(real.data[0]), static_cast<int>(real.data[1]));
            Sonre.print();
            break;
        case 2:
            Daughterre = Daughter(static_cast<int>(real.data[0]),static_cast<bool>(real.data[1]),
                    static_cast<int>(real.data[2]));
            Daughterre.print();
            break;
        default:
            ERROR("Data id is not found! id = "+to_string(frame.ID));
            break;
        }
        return true;
    }

    void HandleSendThread()
    {
        Timepointer tm_start;
        Timepointer tm_end;
        uint64_t sleep_inteval = 0;
        while(isrunning)
        {
            tm_start = std::chrono::system_clock::now();
            for(auto &item : vsendlist_)
            {
                SendSingleFrame(item.GetSendData());
            }
            tm_end = std::chrono::system_clock::now();
            sleep_inteval = delta_period -
                    (std::chrono::duration_cast<std::chrono::milliseconds>
                     (tm_end - tm_start).count())/1e6;
            if(sleep_inteval > 0)
                std::this_thread::sleep_for(std::chrono::milliseconds(sleep_inteval));
        }
    }
private:
    Manager<T>*  sendmanager_;
    unique_ptr<thread> thread_;
    bool isrunning = false;
    vector<SendFrame<T> > vsendlist_;
};

/**
 * @brief The SensorT class
 * Combine the Manager, RecvData and SendData class.
 * Regard as a interface
 */
template<typename T>
class SensorT
{
public:
    SensorT() = default;
    ~SensorT(){cout<<"~SensorT()"<<endl;}
    bool Init()
    {
        sensorManager_ = make_unique< Manager<T> >();
        senddata.init(sensorManager_.get());
        recvdata.init(sensorManager_.get());
        return true;
    }
    bool Start()
    {
        if(!recvdata.StartRecv())
            ERROR("start recv thread Failed!");
        if(!senddata.StartSend())
            ERROR("start recv thread Failed!");
        return true;
    }

    template<typename S>
    bool RegisterSend(S*& sh)
    {
        sh = (dynamic_cast<S*>(sensorManager_->GetMutableAdapterById(S::ID)));
        if(nullptr == sh)
            ERROR("RegisterSend Failed!");
        senddata.AddSendData(S::ID, sh);
        return true;
    }

    unique_ptr<Manager<T>> sensorManager_;
    RecvData<T> recvdata;
    SendData<T> senddata;
};

/**
 * @brief The TestData class
 * Start the interface with some necessary initial.
 */
class TestData : public SensorT<DesData>
{
public:
    TestData() = default;
    ~TestData(){cout<<"~TestData()"<<endl;}
    bool initTestData()
    {
        if(!Init())
        {
            cout << "Init recv or send interface Failed!" << endl;
            return false;
        }
        sensorManager_->Addrecv<Son>();
        sensorManager_->Addrecv<Daughter>();

        sensorManager_->Addsend<Son>();
        sensorManager_->Addsend<Daughter>();

        RegisterSend<Son>(son);
        RegisterSend<Daughter>(daughter);
        return true;
    }
    void UpdateSendData(DesData& data_)
    {
        daughter->setAge(data_.getDauage());
        daughter->setbeautiful(data_.getIsdaubea());
        daughter->setMoney(data_.getDaumoney());

        son->setAge(data_.getSonage());
        son->setMoney(data_.getSonmoney());
        senddata.Update();
    }

private:
    Daughter* daughter = nullptr;
    Son* son = nullptr;
};

/**
 * @brief The TMain class
 * Entrance of the programe.
 */
class TMain
{
public:
    TMain() = default;
    ~TMain(){cout<<"~Tmain()"<<endl;}
    void HandleRecvThread(int intervaltimetime)
    {
        Timepointer tm_start, tm_end;
        uint64_t sleep_interval = 0;
        cout<<"HandleRecvThread start..."<<endl;
        while(1)
        {
            tm_start = system_clock::now();

            DesData dataTemp;
            testdata.sensorManager_->GetData(&dataTemp);
            _mutex_.lock();
            data_ = dataTemp;
            _mutex_.unlock();
            dataTemp.console();
            tm_end = system_clock::now();
            sleep_interval = intervaltimetime -
                    (duration_cast<milliseconds>(tm_end - tm_start).count())/1e6;
            if(sleep_interval > 0)
                std::this_thread::sleep_for(milliseconds(sleep_interval));
        }
    }

    void UpdateSendThread(int intervaltimetime)
    {
        cout<<"UpdateSendThread start..."<<endl;
        Timepointer tm_start, tm_end;
        uint64_t sleep_interval = 0;
        DesData senddata;
        while(1)
        {
            tm_start = system_clock::now();
            _mutex_.lock();
            senddata = data_;
            _mutex_.unlock();
            testdata.UpdateSendData(senddata);
            tm_end = system_clock::now();
            sleep_interval = intervaltimetime -
                    (duration_cast<milliseconds>(tm_end - tm_start).count())/1e6;
            if(sleep_interval > 0)
                std::this_thread::sleep_for(milliseconds(sleep_interval));
        }
    }
    bool Initial()
    {
        if(!testdata.initTestData())
            ERROR("Init Testdata Failed!");
        return true;
    }
    bool Start()
    {
        if(!testdata.Start())
            ERROR("start recv and send Thread Failed!");
        std::thread mainrecvT(&TMain::HandleRecvThread, this, delta_period);
        mainrecvT.detach();
		//数据的更新周期要小于数据的发送周期
        std::thread mainsendT(&TMain::UpdateSendThread, this, delta_period/10);
        mainsendT.join();
        return true;
    }

private:
    TestData testdata;
    DesData data_;
    std::mutex _mutex_;
};

int main()
{
    TMain maint;
    maint.Initial();
    maint.Start();
    return 0;
}

util.h 代码:

/******************************************************************************
 * Complete by Solitary_Tang(Tang xiao long) at 2021-3-10 11:05.
 * Destination: just regard as a tools-lib.
 * Language: C++
 * System: Linux
 * CSND blog link: https://blog.csdn.net/qq_37174816/article/details/114633330
 *****************************************************************************/
#ifndef UTIL
#define UTIL

#include <iostream>
#include <memory>

/**
  * @brief ERROR  complete output errorinfo, filename, function name and row number.
  * @param ERR    the errorinfo
  */
#define ERROR(ERR)\
{\
    std::cout<<"Error: "<<ERR<<", at file:"<<__FILE__<<\
        ", function: "<<__FUNCTION__<<", line: "<<__LINE__<<std::endl;\
    return false;\
}\

/**
 * @brief IntchangeBin   Get binary with string of T type
 * @param x
 * @return
 */
template<typename T>
const std::string IntchangeBin(T& x)
{
    std::string str = "";
    for(int i = sizeof(T)*8-1; i>=0;--i)
        str.append(std::to_string((x>>i)&1));
    return str;
}

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}


#endif // UTIL


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值