377-logger日志系统设计与实现

logger日志系统的设计

在这里插入图片描述
图中画圆圈的是我们实现的mprpc框架,这个框架是给别人使用的,把本地的服务发布成远程的RPC服务,框架里最重要的两个成员就是RpcProvider和RpcChannel,他们在运行的过程中会有很多正常的输出信息和错误的信息,我们不可能都cout它们到屏幕上,因为运行时间长了,屏幕上输出的信息特别多,如果万一有什么问题,我们也不好定位,真正用起来的话不方便。所以,一般出问题,我们最直接的方式就是看日志!!!
日志可以记录正常软件运行过程中出现的信息和错误的信息,当我们定位问题,就打开相应的日志去查看,查找。
假如我们在框架后边输出一个日志模块,我们想把框架运行中的正常的信息和错误信息都记录在日志文件,该怎么做?
左边的两个箭头表示RPC的请求和RPC的响应。
RPC请求过来的时候,我们的框架在执行的时候会产生很多日志文件,我们要写日志,写日志信息的过程是磁盘I/O,磁盘I/O速度不快,我们不能把磁盘I/O的花销算在RPC请求的业务执行部门里面(否则造成RPC请求处理的效率慢),我们不能把日志花费的时间算在框架业务的执行时间里面,所以,一般在我们的服务器中,增加一个kafka,就可以用在日志系统中做一个消息队列中间件,我们把日志写在一个缓存队列里(这个队列相当于异步的日志写入机制),我们的框架做的就是写日志到内存的队列里面,不做磁盘I/O操作。然后我们在后面有一个专门写日志的线程,就是做磁盘I/O操作,从队列头取出日志信息,然后把日志信息写到日志文件中,这样它的磁盘I/O就不会算在我们的RPC请求的业务当中。
在这里插入图片描述
我们的mprpc框架的RpcProvier端是用muduo库实现的,采用的是epoll加多线程,很有可能RPC的处理过程是在多个线程中都会去做这个,多个线程都会去写日志,也就是多个线程都会在这个缓存队列里面添加数据,所以我们的这个缓存队列必须保证线程安全。
我们的C++中有queue这个队列,也就是C++的容器,但是C++容器只考虑使用应用,没有考虑线程安全,所以我们用的是线程互斥机制来维护入队出队的线程按照,写日志的线程也是一个独立的线程,用唯一的互斥锁实现。
如果队列是空的话,也就是之前的日志都写到日志文件了,写日志的线程这时就不用抢这把互斥锁了,因为没有东西可写。导致写入队列的线程无法及时获取锁,信息无法及时写到队列,破坏了RPC请求的业务的效率。
在这里插入图片描述
所以,我们还要处理的就是线程间的通信。
队列如果是空,写日志的线程就一直等待,队列不空的时候,写日志的线程才有必要去抢锁,把日志信息写到日志文件。
我们的日志文件放在当前目录下log。
每一天都生成新的日志文件,有助于在发生问题的时候快速定位。
而且如果当天我们的日志文件如果容量太大,比如说超过20M,就会产生新的!
在这里插入图片描述
kafka是开源的,非常著名,分布式消息队列,其中功能:在分布式环境中提供异步的日志写入服务器中间件,日志写入的系统,和我们的queue本质相同,但是它设计高级,高效,高可用性,可容灾,稳定。

logger日志系统的实现

我们在src下的include增加头文件:logger.h

#pragma once
#include "lockqueue.h"
#include <string>

//定义宏 LOG_INFO("xxx %d %s", 20, "xxxx")
//可变参,提供给用户更轻松的使用logger 
//snprintf, 缓冲区,缓冲区的长度,写的格式化字符串, ##__VA_ARGS__。
//代表了可变参的参数列表,填到缓冲区当中,然后 logger.Log(c)
#define LOG_INFO(logmsgformat, ...) \
    do \
    {  \
        Logger &logger = Logger::GetInstance(); \
        logger.SetLogLevel(INFO); \
        char c[1024] = {0}; \
        snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
        logger.Log(c); \
    } while(0) \

#define LOG_ERR(logmsgformat, ...) \
    do \
    {  \
        Logger &logger = Logger::GetInstance(); \
        logger.SetLogLevel(ERROR); \
        char c[1024] = {0}; \
        snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
        logger.Log(c); \
    } while(0) \

//定义日志的级别
enum LogLevel
{
    INFO, //普通信息
    ERROR,//错误信息
};

//Mprpc框架提供的日志系统
class Logger
{
public:
    //获取日志的单例
    static Logger& GetInstance();
    //设置日志级别 
    void SetLogLevel(LogLevel level);
    //写日志
    void Log(std::string msg);
private:
    int m_loglevel;//记录日志级别
    LockQueue<std::string>  m_lckQue;//日志缓冲队列

    Logger();
    Logger(const Logger&) = delete;
    Logger(Logger&&) = delete;
};

我们在src下的include增加头文件:lockqueue.h
(异步缓冲队列的实现)

#pragma once
#include <queue>
#include <thread>
#include <mutex>//其实就是调用pthread_mutex_t
#include <condition_variable>//其实就是调用pthread_condition_t

//异步写日志的日志队列
template<typename T>
class LockQueue
{
public:
    //多个worker线程都会写日志queue 
    void Push(const T &data)
    {
        std::lock_guard<std::mutex> lock(m_mutex);//获取锁,函数结束自动释放锁 
        m_queue.push(data);//入队列 
        m_condvariable.notify_one();//唤醒一个线程 
    }

    //一个线程读日志queue,写日志文件
    T Pop()
    {
        std::unique_lock<std::mutex> lock(m_mutex);//获取锁,函数结束自动释放锁 
        while (m_queue.empty())//为空 
        {
            //日志队列为空,线程进入wait状态
            m_condvariable.wait(lock);//线程挂起,阻塞,释放锁 
        }

        T data = m_queue.front();//取队列头 
        m_queue.pop();//出队列 
        return data;
    }
private:
    std::queue<T> m_queue;//队列 
    std::mutex m_mutex;//互斥锁 
    std::condition_variable m_condvariable;//条件变量 
};

我们在src下创建logger.cc

#include "logger.h"
#include <time.h>
#include <iostream>

//获取日志的单例
Logger& Logger::GetInstance()//初始化
{
    static Logger logger;
    return logger;
}

Logger::Logger()
{
    //启动专门的写日志线程
    std::thread writeLogTask([&](){
        for (;;)
        {
            //获取当前的日期,然后取日志信息,写入相应的日志文件当中 a+ //追加的方式,如果没有就创建
            time_t now = time(nullptr);//获取当前的时间,按秒算的,1970年到现在的
            tm *nowtm = localtime(&now);//返回tm结构的指针

            char file_name[128];//文件名
            sprintf(file_name, "%d-%d-%d-log.txt", nowtm->tm_year+1900, nowtm->tm_mon+1, nowtm->tm_mday);//构建文件,年-月-日-log.txt

            FILE *pf = fopen(file_name, "a+");//追加的方式打开
            if (pf == nullptr)//打开失败
            {
                std::cout << "logger file : " << file_name << " open error!" << std::endl;
                exit(EXIT_FAILURE);
            }

            std::string msg = m_lckQue.Pop();//出队列

            char time_buf[128] = {0};
            sprintf(time_buf, "%d:%d:%d =>[%s] ", 
                    nowtm->tm_hour, 
                    nowtm->tm_min, 
                    nowtm->tm_sec,
                    (m_loglevel == INFO ? "info" : "error"));
            msg.insert(0, time_buf);//时 分 秒
            msg.append("\n");

            fputs(msg.c_str(), pf);
            fclose(pf);
        }
    });
    //设置分离线程,相当于一个守护线程,在后台专门去写日志
    writeLogTask.detach();
}

//设置日志级别 
void Logger::SetLogLevel(LogLevel level)
{
    m_loglevel = level;
}

//写日志,把日志信息写入lockqueue缓冲区当中,这是RPC请求的业务做的
void Logger::Log(std::string msg)
{
    m_lckQue.Push(msg);
}

我们完善src的CMakeLists.txt
在这里插入图片描述
保存。我们开始编译。
在这里插入图片描述
编译成功。

测试

我们打开example下的callee(RPC服务的提供者)
打开并提交logger.h到friendservice.cc

#include <iostream>
#include <string>
#include "friend.pb.h"
#include "mprpcapplication.h"
#include "mprpcprovider.h"
#include <vector>
#include "logger.h"

class FriendService : public fixbug::FiendServiceRpc
{
public:
    std::vector<std::string> GetFriendsList(uint32_t userid)//返回好友的列表,本地方法
    {
        std::cout << "do GetFriendsList service! userid:" << userid << std::endl;
        std::vector<std::string> vec;
        vec.push_back("linyouhua");
        vec.push_back("lincanhui");
        vec.push_back("zhang san");
        return vec;
    }

    //重写基类方法,框架帮我们调用的
    void GetFriendsList(::google::protobuf::RpcController* controller,
                       const ::fixbug::GetFriendsListRequest* request,
                       ::fixbug::GetFriendsListResponse* response,
                       ::google::protobuf::Closure* done)
    {
        uint32_t userid = request->userid();//获取用户的id号
        std::vector<std::string> friendsList = GetFriendsList(userid);//调用本地方法
        response->mutable_result()->set_errcode(0);//执行成功
        response->mutable_result()->set_errmsg("");
        for (std::string &name : friendsList)//遍历好友列表
        {
            std::string *p = response->add_friends();//添加
            *p = name;
        }
        done->Run();
    }
};

int main(int argc, char **argv)
{
    LOG_ERR("ddddd");
    LOG_INFO("ddddd");

    //调用框架的初始化操作
    MprpcApplication::Init(argc, argv);

    //provider是一个rpc网络服务对象。把UserService对象发布到rpc节点上
    RpcProvider provider;
    provider.NotifyService(new FriendService());

    //启动一个rpc服务发布节点 Run以后,进程进入阻塞状态,等待远程的rpc调用请求
    provider.Run();

    return 0;
}

保存。
重新编译。

在这里插入图片描述
我们打开终端,输入命令测试。
在这里插入图片描述
测试成功。

把日志集成到系统中

我们完善mprpcprovider.cc

#include "mprpcprovider.h"
#include "mprpcapplication.h"
#include "rpcheader.pb.h" 
#include "logger.h"

/*
service_name =>对于 service描述   
                        =》对应 service* 记录服务对象
                        多个method_name  =>对应多个method方法对象
*/
//这里是框架提供给外部使用的,可以发布rpc方法的函数接口
void RpcProvider::NotifyService(google::protobuf::Service *service)
{
    ServiceInfo service_info;//结构体

    //获取了服务对象的描述信息
    const google::protobuf::ServiceDescriptor *pserviceDesc = service->GetDescriptor();
	//因为返回类型是指针。获取服务对象的描述信息。存储名字之类的。

    //获取服务的名字
    std::string service_name = pserviceDesc->name();
    //获取服务对象service的方法的数量
    int methodCnt = pserviceDesc->method_count();

    //std::cout << "service_name:" << service_name << std::endl;
    LOG_INFO("service_name:%s", service_name.c_str());   

    for (int i=0; i < methodCnt; ++i)
    {
        //获取了服务对象指定下标的服务方法的描述(抽象的描述) UserService   Login
        const google::protobuf::MethodDescriptor* pmethodDesc = pserviceDesc->method(i);
        std::string method_name = pmethodDesc->name();
        service_info.m_methodMap.insert({method_name, pmethodDesc});//插入键值对到map中

        //std::cout<<"method_name:"<<method_name<<std::endl;//打印
        LOG_INFO("method_name:%s", method_name.c_str());        
    }
    service_info.m_service = service;//记录服务对象
    m_serviceMap.insert({service_name, service_info});//存储一下服务及其具体的描述
}


//启动rpc服务节点,开始提供rpc远程网络调用服务
void RpcProvider::Run()
{
    //读取配置文件rpcserver的信息
    std::string ip = MprpcApplication::GetInstance().GetConfig().Load("rpcserverip");//ip
    uint16_t port = atoi(MprpcApplication::GetInstance().GetConfig().Load("rpcserverport").c_str());//port,因为atoi返回char *,所以要c_str()
    muduo::net::InetAddress address(ip, port);

    //创建TcpServer对象
    muduo::net::TcpServer server(&m_eventLoop, address, "RpcProvider");

    //绑定连接回调和消息读写回调方法 ,muduo库的好处是:分离了网络代码和业务代码
    server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));//预留1个参数std::placeholders::_1
    server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, 
            std::placeholders::_2, std::placeholders::_3));//预留3个参数std::placeholders::_1,2,3

    //设置muduo库的线程数量
    server.setThreadNum(4);//1个是I/O线程,3个是工作线程

    //rpc服务端准备启动,打印信息
    std::cout << "RpcProvider start service at ip:" << ip << " port:" << port << std::endl;
    
    //启动网络服务
    server.start();
    m_eventLoop.loop();//相当于启动了epoll_wait,阻塞,等待远程连接
}

//新的socket连接回调
void RpcProvider::OnConnection(const muduo::net::TcpConnectionPtr &conn)
{
    if (!conn->connected())
    {
        //和rpc client的连接断开了
        conn->shutdown();//关闭文件描述符 
    }
}

/*
在框架内部,RpcProvider和RpcConsumer协商好之间通信用的protobuf数据类型
怎么商量呢? 
包含:service_name  method_name   args   
对应:16UserService   Login    zhang san123456   
我们在框架中定义proto的message类型,进行数据头的序列化和反序列化
service_name method_name args_size(防止粘包的问题) 

怎么去区分哪个是service_name, method_name, args
我们把消息头表示出来 
header_size(4个字节) + header_str + args_str
前面几个字节是服务名和方法名。 
为了防止粘包,我们还要记录参数的字符串的长度 
我们统一:一开始读4个字节,数据头的长度,也就是除了方法参数之外的所有数据:服务名字和方法名字 
10 "10"
10000 "1000000"
std::string   insert和copy方法 
*/

//已建立连接用户的读写事件回调,如果远程有一个rpc服务的调用请求,那么OnMessage方法就会响应
void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr &conn, 
                            muduo::net::Buffer *buffer, 
                            muduo::Timestamp)
{
    //网络上接收的远程rpc调用请求的字符流 包含了RPC方法的名字Login和参数args
    std::string recv_buf = buffer->retrieveAllAsString();

    //从字符流中读取前4个字节的内容
    uint32_t header_size = 0;
    recv_buf.copy((char*)&header_size, 4, 0);//从0下标位置拷贝4个字节的内容到header_size 

    std::string rpc_header_str = recv_buf.substr(4, header_size);
	//从第4个下标,前4个字节略过。读取包含了service_name method_name args_size 
	//根据header_size读取数据头的原始字符流,反序列化数据,得到rpc请求的详细信息
    mprpc::RpcHeader rpcHeader;
    std::string service_name;
    std::string method_name;
    uint32_t args_size;
    if (rpcHeader.ParseFromString(rpc_header_str))
    {
        //数据头反序列化成功
        service_name = rpcHeader.service_name();
        method_name = rpcHeader.method_name();
        args_size = rpcHeader.args_size();
    }
    else
    {
        //数据头反序列化失败
        std::cout << "rpc_header_str:" << rpc_header_str << " parse error!" << std::endl;
        return;//不用往后走了 
    }

    //获取rpc方法参数的字符流数据
    std::string args_str = recv_buf.substr(4 + header_size, args_size);
    //header_size(4个字节) + header_str + args_str

    //打印调试信息
    std::cout << "============================================" << std::endl;
    std::cout << "header_size: " << header_size << std::endl; 
    std::cout << "rpc_header_str: " << rpc_header_str << std::endl; 
    std::cout << "service_name: " << service_name << std::endl; 
    std::cout << "method_name: " << method_name << std::endl; 
    std::cout << "args_str: " << args_str << std::endl; 
    std::cout << "============================================" << std::endl;
     
    //获取service对象和method对象
    auto it = m_serviceMap.find(service_name);//用[]会有副作用 
    if (it == m_serviceMap.end())//根本没有的服务 
    {
        std::cout << service_name << " is not exist!" << std::endl;
        return;
    }

    auto mit = it->second.m_methodMap.find(method_name);
    if (mit == it->second.m_methodMap.end())//服务里没有这个方法 
    {
        std::cout << service_name << ":" << method_name << " is not exist!" << std::endl;
        return;
    }

    google::protobuf::Service *service = it->second.m_service;//获取service对象  对应的就是像new UserService这种 
    const google::protobuf::MethodDescriptor *method = mit->second;//获取method对象 对应的是像Login这种 

    //生成rpc方法调用的请求request和响应response参数
    google::protobuf::Message *request = service->GetRequestPrototype(method).New();
	//在框架以抽象的方式表示。new生成新对象,传给userservice 
	
    if (!request->ParseFromString(args_str))//解析 
    {
        std::cout << "request parse error, content:" << args_str << std::endl;
        return;
    }
    google::protobuf::Message *response = service->GetResponsePrototype(method).New();//响应 

    //CallMethod需要closure参数
	//给下面的method方法的调用,绑定一个Closure的回调函数
    google::protobuf::Closure *done = google::protobuf::NewCallback<RpcProvider, 
                                                                    const muduo::net::TcpConnectionPtr&, 
                                                                    google::protobuf::Message*>
                                                                    (this, 
                                                                    &RpcProvider::SendRpcResponse, 
                                                                    conn, response);

    //在框架上根据远端rpc请求,调用当前rpc节点上发布的方法
    
    service->CallMethod(method, nullptr, request, response, done);//做完本地业务,根据结果写好reponse给框架,框架再给调用方 
    //相当于new UserService().Login(controller, request, response, done)
}

//Closure的回调操作,用于序列化rpc的响应和网络发送
void RpcProvider::SendRpcResponse(const muduo::net::TcpConnectionPtr& conn, google::protobuf::Message *response)
{
    std::string response_str;
    if (response->SerializeToString(&response_str))//对response进行序列化
    {
        //序列化成功后,通过网络把rpc方法执行的结果发送会rpc的调用方
        conn->send(response_str);
    }
    else//序列化失败
    {
        std::cout << "serialize response_str error!" << std::endl; 
    }
    conn->shutdown(); //模拟http的短链接服务,由rpcprovider主动断开连接,给更多的rpc调用方提供服务
}

保存。编译。
编译成功。
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
毕业设计--基于Python+HTML的面向高考招生咨询的问答系统设计实现.zip 如何导入该项目? # 1. git clone 当前项目 git clone .git # 2. 新建Python 环境 python -m venv .venv # 3.安装依赖库 pip install --upgrade pip pip install -r requirements.txt # 4.准备数据库信息 # 确定数据库链接的信息,默认是root@localhost 密码:123456 # 执行 InfomationGet/InsertAdmissionData.py 插入数据 cd InfomationGet # create database and sheet python MysqlOperation.py # insert data python InsertAdmissionData.py # 5.打开软件 cd SystemUi python QASystem.py 【原题:毕业设计--基于知识图谱的大学领域知识自动问答系统设计实现】 一、InfomationGet:完成领域知识的获取和数据库构建工作 1、Infomation:存储获取到的信息 (1)、九校联盟:C9数据--表格型(招生计划、录取分数(分省、分专业)) (2)、大学:大学学科字段(百度百科)、常用问题集(C9常用问题集.csv) 2、py文件 (1)、CreateFolder.py(创建文件夹--九校联盟) (2)、InternetConnect.py(网络连接) (3)、GetDictionaryData.py(获取相关词典数据) (4)、GetPlanInfo.py(获取招生计划数据) (5)、GetScoreInfo.py(获取录取分数数据) (6)、MysqlOperation.py(MySQL数据库操作) (7)、Neo4jOperation.py(Neo4j数据库操作) (8)、InsertAdmissionData.py(MySQL数据库插入数据) (9)、GetFrequentQuestion.py(获取高考网常用问题集数据) 二、FileRead:获取招生数据过程中的文件读取 1、py文件 (1)、FileNameRead.py(读取文件名) (2)、ImageRead.py(读取图片) (3)、PDFRead.py(读取PDF(表格、文字)) (4)、XLSRead.py(读取excel表格) 三、HanLP:中文自然语言处理工具 1、py文件 (1)、HanLPTest.py(HanLP测试) 四、LTP:中文自然语言处理工具 1、ltp_data:LTP模型库+自定义词典文本 2、py文件 (1)、LTPInterface.py(LTP使用接口) (2)、XFYunWebAPI.py(讯飞云网络接口) 五、QuestionAnalysis:自然语言问句分析 1、py文件 (1)、QuestionPretreatment.py(自然语言问句预处理(关键词:(年份、学校、专业、地区)识别与预处理)) (2)、KeywordNormalize.py(关键词正则化) 六、SimilarityCalculate:相似度计算 1、py文件 (1)、SemanticSimilarity.py(语义相似度计算,需要api) 七、TemplateLoad:模板加载 1、Template:模板文件 2、py文件 (1)、QuestionTemplate.py(问题模板的创建与加载) 八、QuestionQuery:自然语言问句查询 1、py文件 (1)、MysqlQuery.py(MySQL表(admission_plan、admission_score_pro、admission_score_major)查询(可缺省关键词)) 九、SystemUI:自动问答系统设计 1、images:UI图片 2、py文件 (1)、QASystem.py(自动问答系统界面设计(自动问答+可选数据库目录查询)) 十、Log:日志系统 1、py文件 (1)、Logger.py(自定义日志(all.log+error.log,all.log可按时间每日切分))
好的,下面是一个使用spdlog库封装的日志系统的示例代码: ```cpp #include <spdlog/spdlog.h> #include <spdlog/sinks/basic_file_sink.h> class Logger { public: enum class Level { Trace, Debug, Info, Warn, Error, Critical, Off }; static void init(const std::string& filename, Level level = Level::Debug) { auto logger = spdlog::basic_logger_mt("logger", filename); spdlog::set_default_logger(logger); spdlog::set_level(static_cast<spdlog::level::level_enum>(level)); } static std::shared_ptr<spdlog::logger> get() { return spdlog::default_logger(); } }; #define LOG_TRACE(...) Logger::get()->trace(__VA_ARGS__) #define LOG_DEBUG(...) Logger::get()->debug(__VA_ARGS__) #define LOG_INFO(...) Logger::get()->info(__VA_ARGS__) #define LOG_WARN(...) Logger::get()->warn(__VA_ARGS__) #define LOG_ERROR(...) Logger::get()->error(__VA_ARGS__) #define LOG_CRITICAL(...) Logger::get()->critical(__VA_ARGS__) ``` 首先,我们包含了必要的头文件,包括spdlog和basic_file_sink头文件,用于向文件中记录日志。 接下来,我们定义了一个Logger,它有一个公共的init函数,用于初始化日志系统。该函数接受一个文件名和一个日志级别参数。在该函数中,我们创建了一个基本的文件日志器,并将其设置为默认日志器。我们还设置了日志级别,该级别将转换为spdlog的level_enum型。 接下来,我们定义了一组宏,用于记录不同级别的日志。这些宏调用spdlog默认日志器的相应函数,如trace、debug、info、warn、error和critical。这些宏可以在我们的应用程序中的任何地方使用,以记录相应级别的日志。 最后,我们使用这些宏来记录日志。例如: ```cpp int main() { Logger::init("logs/log.txt", Logger::Level::Debug); LOG_INFO("Starting application..."); LOG_DEBUG("Some debug information..."); LOG_WARN("A warning message..."); LOG_ERROR("An error occurred!"); LOG_CRITICAL("A critical error occurred!"); return 0; } ``` 这将初始化我们的日志系统,并使用LOG_INFO、LOG_DEBUG、LOG_WARN、LOG_ERROR和LOG_CRITICAL宏记录不同级别的日志。这些日志将被记录在“logs/log.txt”文件中。 希望这个封装的示例代码可以帮助您设计一个高级的spdlog日志系统

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

林林林ZEYU

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

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

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

打赏作者

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

抵扣说明:

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

余额充值