Tornado-03-Tornado、数据库、同步和异步、Tornado的协程(异步Web请求客户端、基于gen.coroutine的协程异步、并行协程)、Tornado的WebSocket

Tornado

一、数据库

与Django框架相比,Tornado没有自带ORM,对于数据库需要自己去适配。我们使用MySQL数据库。

在Tornado3.0版本以前提供tornado.database模块用来操作MySQL数据库,而从3.0版本开始,此模块就被独立出来,作为torndb包单独提供。torndb只是对MySQLdb的简单封装,不支持Python 3。所以如果在当前版本中使用torndb进行数据库操作,需要修改源代码,所以在此,我们使用pymysql。

项目中如果要使用ORM,可以使用SQLAlchemy,但开发中,很少有人这么使用.
同时,tornado强大的地方在于其异步非阻塞,所以我们后面关于数据库操作,不管是mysql, mongodb还是redis基本都是异步读写操作。

MySQL

pip install pymysql

mysql.py,代码:

import pymysql
class MySQL(object):
    def __init__(self, host, user, pwd, name):
        self.host = host
        self.user = user
        self.pwd = pwd
        self.name = name
        self.data = None
        self.last_sql = None

    def connect(self):
        self.db = pymysql.Connect(host=self.host, user=self.user, passwd=self.pwd, db=self.name)
        self.cursor = self.db.cursor()

    def close(self):
        self.cursor.close()
        self.db.close()

    def get_one(self, sql):
        try:
            self.connect()
            self.cursor.execute(sql)
            res = self.cursor.fetchone()
            self.data = res
            self.last_sql = sql
            self.close()
        except:
            print("fail to select")

        return self
    def get_all(self, sql):
        try:
            self.connect()
            self.cursor.execute(sql)
            res = self.cursor.fetchall()
            self.last_sql = sql
            self.data = res
            self.close()
        except:
            print("fail to select")
        return self

    def get_all_obj(self, sql):
        resList = []
        fieldList = []
        arr = sql.lower().split(" ")
        while "" in arr:
            arr.remove("")
        tableName = arr[arr.index("from")+1]
        fieldSql = "select COLUMN_NAME from information_schema.COLUMNS where table_name='%s' and table_schema='%s'" % (
            tableName, self.name
        )
        fields = self.get_all(fieldSql).data
        for item in fields:
            fieldList.append(item[0])
        
        res = self.get_all(sql).data
        for item in res:
            obj = {}
            count = 0
            for x in item:
                obj[fieldList[count]] = x
                count += 1
            resList.append(obj)
        self.data = resList
        return self

    def insert(self, sql):
        return self.execute(sql)

    def update(self, sql):
        return self.execute(sql)

    def delete(self, sql):
        return self.execute(sql)

    def execute(self, sql):
        count = 0
        try:
            self.connect()
            count = self.cursor.execute(sql)
            self.db.commit()
            self.data = self.db.rowcount()
            self.last_sql = sql
            self.close()
        except:
            print("fail execute sql")
            self.db.rollback()
        return count

server.py,代码:

from tornado import ioloop
from tornado import web
from mysql import MySQL


mysql = {
    "host": "127.0.0.1",
    "user": "root",
    "pwd": "123",
    "db": "students"
}

db = MySQL(mysql["host"], mysql["user"], mysql["pwd"], mysql["db"])

settings = {
    'debug': True,
}

class Request(web.RequestHandler):
    def initialize(self,db):
        self.db = db

    def write(self, chunk: web.Union[str, bytes, dict, list]) -> None:
        if self._finished:
            raise RuntimeError("Cannot write() after finish()")
        if not isinstance(chunk, (bytes, web.unicode_type, dict, list)):
            message = "write() only accepts bytes, unicode, and dict objects"
            # if isinstance(chunk, list):
            #     message += (
            #         ". Lists not accepted for security reasons; see "
            #         + "http://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.write"  # noqa: E501
            #     )
            raise TypeError(message)
        if isinstance(chunk, (dict,list)):
            chunk = web.escape.json_encode(chunk)
            self.set_header("Content-Type", "application/json; charset=UTF-8")
        chunk = web.utf8(chunk)
        self._write_buffer.append(chunk)

class Home(Request):
    def get1(self):
        """查询一条数据"""
        data = self.db.get_one("select * from tb_student").data
        print(data)
        self.write("hello,get")

    def get2(self):
        """查询多条数据[返回元祖]"""
        data = self.db.get_all("select * from tb_student").data
        print(data)
        self.write("hello,get")

    def get3(self):
        """查询多条数据[返回元祖]"""
        data = self.db.get_all("select * from tb_student").data
        print(data)
        """查询多条数据[返回列表]"""
        data = self.db.get_all_json("select * from tb_student").data
        print(data)
        self.write("hello,get")

    def get(self):
        """添加/删除/修改"""
        # """添加一条"""
        # name = "xiaobai"
        # avatar = "2.png"
        # age = 16
        # sex = True
        # money = 100
        # sql = "INSERT INTO tb_student (name,avatar,age,sex,money) VALUES ('%s','%s',%s,%s,%s)" % (name,avatar,age,sex,money)
        # data = self.db.insert(sql).data
        # print(data)

        """添加多条"""
        # student_list = [
        #     {"name":"xiaohui1","avatar":"1.png","age":16,"sex":True,"money":1000},
        #     {"name":"xiaohui2","avatar":"2.png","age":16,"sex":False,"money":1000},
        #     {"name":"xiaohui3","avatar":"3.png","age":16,"sex":True,"money":1000},
        #     {"name":"xiaohui4","avatar":"4.png","age":16,"sex":False,"money":1000},
        # ]
        # table = "tb_student"
        #
        # fields = ",".join( student_list[0].keys() )
        # sql = "INSERT INTO %s (%s) VALUES " % (table,fields)
        #
        # for student in student_list:
        #     sql += "('%s','%s',%s,%s,%s)," % (student["name"],student["avatar"],student["age"],student["sex"],student["money"])
        # sql = sql[:-1]
        # data = self.db.insert(sql).data
        # print(data)

        """更新"""
        # name = "小辉"
        # sql = "UPDATE tb_student set name='%s' WHERE id = 3" % (name)
        # data = self.db.update(sql).data
        # print(data)

        """删除"""
        # sql = "DELETE FROM tb_student WHERE id = 16"
        # data = self.db.delete(sql).data
        # print(data)

        self.write("hello,get")

# 设置路由列表
urls = [
    (r"/", Home,{"db":db}),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, **settings)
    # 设置监听的端口和地址
    app.listen(8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

二、同步和异步

概念

同步是指代在程序执行多个任务时,按部就班的依次执行,必须上一个任务执行完有了结果以后,才会执行下一个任务。

异步是指代在程序执行多个任务时,没有先后依序,可以同时执行,所以在执行上一个任务时不会等待结果,直接执行下一个任务。一般最终在下一个任务中通过状态的改变或者通知、回调的方式来获取上一个任务的执行结果。

①同步

server.py,代码:

import time
def client_A():
    """模拟客户端A"""
    print('开始处理请求1-1')
    time.sleep(5)
    print('完成处理请求1-2')


def client_B():
    """模拟客户端B"""
    print('开始处理请求2-1')
    print('完成处理请求2-2')


def tornado():
    """模拟tornado框架"""
    client_A()
    client_B()


if __name__ == "__main__":
    tornado()
②异步

server.py,代码:

from threading import Thread
from time import sleep

def async(func):
    def wrapper(*args, **kwargs):
        thread = Thread(target=func, args=args, kwargs=kwargs)
        thread.start()
    return wrapper

@async
def funcA():
    sleep(5)
    print("funcA执行了")

def funcB():
    print("funcB执行了")

def tornado():
    funcA()
    funcB()

if __name__ == "__main__":
    tornado()
③协程

要理解什么是协程(Coroutine),必须先清晰迭代器生成器的概念。

迭代器

迭代器就是一个对象,一个可迭代的对象,是可以被for循环遍历输出的对象。当然专业的说,就是实现了迭代器协议的对象。

任何一个对象,只要类中实现了``iter()`就是一个可迭代对象(iterable)。

任何一个对象,只要类中实现了__iter__()__next__()就是一个迭代器(iterator)。

迭代器一定是可迭代对象,可迭代对象不一定是迭代器。

要了解迭代器,我们先编写一个代码来看看python提供的可迭代对象。常见的有:str,list ,tuple,dic,set,文件对象。

迭代器是惰性执行的,可以节省内存,不能反复, 只能向下取值。

server.py,代码:

# 可迭代对象
# arr = [4,5,6,7]
# arr = "abcd"
# print(dir(arr))
# for item in arr:
#     print(item)

# 不可迭代对象
# num = 123
# print(dir(num))
# for item in num: # TypeError: 'int' object is not iterable
#     print(item)

# 自定义可迭代对象
class Colors(object):
    def __init__(self):
        self.data = ["红色", "橙色", "紫色", "黄色"]

    def __iter__(self):
        # __iter__ 必须有返回值,并且只能返回迭代器对象
        return self.data.__iter__()

colors = Colors()
print(dir(colors))
for item in colors:
    print(item)

查看一个对象是否是可迭代对象或迭代器:

from collections import Iterable, Iterator
data = [1,2,3,4]
print(isinstance(data,Iterable)) # True       # 查看是不是可迭代对象
print(isinstance(data,Iterator)) # False      # 查看是不是迭代器
print(isinstance(data.__iter__(),Iterator))   # True,
# 所有的迭代对象都有一个__iter__方法,该方法的作用就是返回一个迭代器对象

接下来,动手编写一个迭代器
server.py,代码:

class Num(object):
    def __init__(self,max):
        self.max = max
        self.current = 0

    def __next__(self):
        # print("current=",self.current)
        if self.current >= self.max:
            raise StopIteration

        self.current += 1
        return self.current

    def __iter__(self):
        return self

num = Num(3) # 迭代器
# print(dir(num))
# for的内部本质上就是不断调用了迭代器的__next__(),
# 并在遇到StopIteration异常以后,终止程序的执行
# for item in num:
#     print(item)

while True:
    try:
        print(num.__next__())
    except StopIteration:
        break

__iter__() 方法返回一个特殊的迭代器对象, 这个迭代器对象实现了 __next__() 方法并通过 StopIteration 异常标识迭代的完成。
__next__() 方法返回下一个迭代器对象。
StopIteration 异常用于标识迭代的完成,防止出现无限循环,在 __next__() 方法中可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。

生成器

在 Python 中,使用了 yield 的函数被称为生成器函数。

生成器函数执行以后的返回结果就是生成器generator),是一种特殊的迭代器。生成器只能用于迭代操作。

yield 是一个python内置的关键字,它的作用有一部分类似return,可以返回函数的执行结果。但是不同的是,return 会终止函数的执行,yield 不会终止生成器函数的执行。两者都会返回一个结果,但return只能一次给函数的调用处返回值,而yield是可以多次给next()方法返回值,而且yield还可以接受外界send()方法的传值。所以,更准确的来说,yield是暂停程序的执行权并记录了程序当前的运行状态的标记符.同时也是生成器函数外界和内部进行数据传递的通道
server.py,代码:

def func():
    for item in [4,5,6]:
        return item

def gen1():
    for item in [4,5,6]:
        yield item

def gen2():
    key = 0
    print(">>>>> 嘟嘟,开车了")
    while True:
        food = yield "第%s次" % key
        print('接收了,%s'% food)
        key +=1

f = func()
print(f)
g1 = gen1()
print(g1)
for item in g1:
    print(item)

g2 = gen1()
print(g2)
print(next(g2))
print(next(g2))
print(g2.__next__())
# print(next(g2))

g3 = gen2()
g3.send(None) # g3.__next__() 预激活生成器,让生成器内部执行到第一个yield位置,否则无法通过send传递数据给内部的yield
for item in ["苹果","芒果"]:
    print(g3.send(item))

使用生成器可以让代码量更少,内存使用更加高效节约。
所以在工作中针对海量数据查询,大文件的读取加载,都可以考虑使用生成器来完成。因为一次性读取大文件或海量数据必然需要存放内容,而往往读取的内容大于内存则可能导致内存不足,而使用生成器可以像挤牙膏一样,一次读取一部分数据通过yield返回,每次yield返回的数据都是保存在同一块内存中的,所以比较起来肯定比一次性读取大文件内容来说,内存的占用更少。

yield 和 yield from

yield from 也叫委派生成器.委派生成器的作用主要是用于多个生成器之间进行嵌套调用时使用的.
server.py,代码:

def gen1():
    a = 0
    while True:
        # print("+++++++")
        a = yield a**2

def gen2(gen):
    yield from gen
    # a = 0
    # b = 1
    # gen.send(None)
    # while True:
    #     # print("-------")
    #     b = yield a
    #     a = gen.send(b)

if __name__ == '__main__':
    g1 = gen1()
    g2 = gen2(g1)
    g2.send(None)
    for i in range(5):
        # print(">>>> %s" % i)
        print(g2.send(i))
基于生成器来实现协程异步

这也是协程的实现原理,任务交替切换执行(遇到IO操作时进行判断任务切换才有使用价值, 当前此处我们使用的生成器实现的协程,是无法判断当前任务是否是遇到IO的,我们通过第三方模块: geventlet来实现判断是否遇到IO操作)。
server.py,代码:

import time
def gen1():
    while True:
        print("--1")
        yield
        print("--2")
        time.sleep(1)

def gen2():
    while True:
        print("--3")
        yield
        print("--4")
        time.sleep(1)

if __name__ == "__main__":
    g1 = gen1()
    g2 = gen2()
    for i in range(3):
        next(g1)
        print("主程序!")
        next(g2)

三、Tornado的协程

Tornado的异步编程也主要体现在网络IO的异步上,即异步Web请求。

1.异步Web请求客户端

Tornado提供了一个异步Web请求客户端tornado.httpclient.AsyncHTTPClient用来进行异步Web请求。

①fetch(request)

用于执行一个web请求request,并异步返回一个tornado.httpclient.HTTPResponse响应。
request可以是一个url,也可以是一个tornado.httpclient.HTTPRequest对象。如果是url地址,fetch方法内部会自己构造一个HTTPRequest对象。

②HTTPRequest

HTTP请求类,HTTPRequest的构造函数可以接收众多构造参数,最常用的如下:

  • url (string) – 要访问的url,此参数必传,除此之外均为可选参数
  • method (string) – HTTP访问方式,如“GET”或“POST”,默认为GET方式
  • headers (HTTPHeaders or dict) – 附加的HTTP协议头
  • body – HTTP请求的请求体
③HTTPResponse

HTTP响应类,其常用属性如下:

  • code: HTTP状态码,如 200 或 404
  • reason: 状态码描述信息
  • body: 响应体字符串
  • error: 异常(可有可无)

2.基于gen.coroutine的协程异步

from tornado import web,httpclient,gen,ioloop
import json
class Home(web.RequestHandler):
    @gen.coroutine
    def get(self):
        http = httpclient.AsyncHTTPClient()
        ip = "123.112.18.111"
        response = yield http.fetch("http://ip-api.com/json/%s?lang=zh-CN" % ip)
        if response.error:
            self.send_error(500)
        else:
            data = json.loads(response.body)
            if 'success' == data["status"]:
                self.write("国家:%s 省份: %s 城市: %s" % (data["country"], data["regionName"], data["city"]))
            else:
                self.write("查询IP信息错误")

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, debug=True)
    # 设置监听的端口和地址
    app.listen(port=8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

将异步Web请求单独抽取出来

from tornado import web,httpclient,gen,ioloop
import json
class Home(web.RequestHandler):
    @gen.coroutine
    def get(self):

        ip = "123.112.18.111"
        data = yield self.get_ip_info(ip)
        if data["status"] == 'success':
            self.write("国家:%s 省份: %s 城市: %s" % (data["country"], data["regionName"], data["city"]))
        else:
            self.write("查询IP信息错误")


    @gen.coroutine
    def get_ip_info(self,ip):
        http = httpclient.AsyncHTTPClient()
        response = yield http.fetch("http://ip-api.com/json/%s?lang=zh-CN" % ip)
        if response.error:
            rep = {"status": "fail"}
        else:
            rep = json.loads(response.body)

        raise gen.Return(rep)
        # 此处需要注意,生成器函数中是不能直接return 返回数据的,否则出错,
        # 所以我们需要再此通过tornado 封装的异常对象gen.Return(rep)把结果进行抛出

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, debug=True)
    # 设置监听的端口和地址
    app.listen(port=8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

3.并行协程

Tornado可以同时执行多个异步,并发的异步可以使用列表或字典,如下:

from tornado import web,httpclient,gen,ioloop
import json
class Home(web.RequestHandler):
    @gen.coroutine
    def get(self):
        ips = ["123.112.18.111",
               "112.112.233.89",
               "119.112.23.3",
               "120.223.70.76"]
        rep1, rep2 = yield [self.get_ip_info(ips[0]), self.get_ip_info(ips[1])]
        self.write_response(ips[0], rep1)
        self.write_response(ips[1], rep2)
        rep_dict = yield dict(rep3=self.get_ip_info(ips[2]), rep4=self.get_ip_info(ips[3]))
        self.write_response(ips[2], rep_dict['rep3'])
        self.write_response(ips[3], rep_dict['rep4'])

    def write_response(self,ip, rep):
        if 'success' == rep["status"]:
            self.write("IP:%s 国家:%s 省份: %s 城市: %s<br>" % (ip,rep["country"], rep["regionName"], rep["city"]))
        else:
            self.write("查询IP信息错误<br>")
    @gen.coroutine
    def get_ip_info(self, ip):
        http = httpclient.AsyncHTTPClient()
        response = yield http.fetch("http://ip-api.com/json/%s?lang=zh-CN" % ip)
        if response.error:
            rep = {"status":"fail"}
        else:
            rep = json.loads(response.body)
        raise gen.Return(rep)  # 此处需要注意

# 设置路由列表
urls = [
    (r"/", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, debug=True)
    # 设置监听的端口和地址
    app.listen(port=8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

四、Tornado的WebSocket

WebSocket是HTML5规范中新提出的客户端-服务器通信协议,协议本身使用新的ws://URL格式。

WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议的101状态码进行协议升级,使用的 TCP 端口是80,可以用于绕过大多数防火墙的限制。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端直接向客户端主动推送数据而不需要客户端进行再次请求,两者之间可以创建持久性连接,并允许数据进行双向通信。

Tornado提供支持WebSocket的模块是tornado.websocket,其中提供了一个WebSocketHandler类用来处理通讯。

1.常用方法

①open()

当一个WebSocket连接建立后被调用。

②on_message(message)

当客户端发送消息message过来时被调用,注意此方法必须被重写

③on_close()

当WebSocket连接关闭后被调用。

④write_message(message, binary=False)

向客户端发送消息messagea,message可以是字符串或字典(字典会被转为json字符串)。若binary为False,则message以utf8编码发送;二进制模式(binary=True)时,可发送任何字节码。

⑤close()

关闭WebSocket连接。

⑥check_origin(origin)

判断源origin,对于符合条件(返回判断结果为True)的请求源origin允许其连接,否则返回403。可以重写此方法来解决WebSocket的跨域请求(如始终return True)。

2.快速使用

server.py,代码:

from tornado import web,ioloop
from tornado.websocket import WebSocketHandler

class Index(web.RequestHandler):
    def get(self):
        self.render("templates/index.html")

class Home(WebSocketHandler):
    def open(self):
        # 
        self.write_message("欢迎来到socket.html")

    def on_message(self, message):
        print("接收数据:%s" % message)

    def on_close(self):
        print("socket连接断开")

    def check_origin(self, origin):
        return True  # 允许WebSocket的跨域请求

# 设置路由列表
urls = [
    (r"/", Index),
    (r"/home", Home),
]

if __name__ == "__main__":
    # 创建应用实例对象
    app = web.Application(urls, debug=True)
    # 设置监听的端口和地址
    app.listen(port=8888)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

tempales/index.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<div id="content"></div>
<script>
    var ws = new WebSocket("ws://127.0.0.1:8888/home"); // 新建一个ws连接
    ws.onopen = function() {  // 连接建立好后的回调
       ws.send("Hello, world");  // 向建立的连接发送消息
    };
    ws.onmessage = function (evt) {  // 收到服务器发送的消息后执行的回调
       content.innerHTML+=evt.data+"<br>";  // 接收的消息内容在事件参数evt的data属性中
    };
</script>
</body>
</html>

3.案例:聊天室

server.py,代码:

from tornado import web,ioloop,httpserver,options
import datetime

from tornado.web import RequestHandler
from tornado.websocket import WebSocketHandler

class Index(RequestHandler):
    def get(self):
        self.render("templates/chat.html")

class Chat(WebSocketHandler):
    users = set()  # 用来存放用户的容器,必须类静态属性

    def open(self):
        self.users.add(self)  # 建立连接后保存客户端的socket连接对象到users容器中
        key = list(self.users).index(self)
        for user in self.users:  # 向已在线用户发送消息
            user.write_message("[%s]-%02d-[%s]-登录" % (self.request.remote_ip, key, datetime.datetime.now().strftime("%H:%M:%S")))

    def on_message(self, message):
        key = list(self.users).index(self)
        for user in self.users:  # 向在线用户广播消息
            user.write_message("[%s]-%02d-[%s]-发送:%s" % (self.request.remote_ip, key, datetime.datetime.now().strftime("%H:%M:%S"), message))

    def on_close(self):
        key = list(self.users).index(self)
        self.users.remove(self) # 用户关闭连接后从容器中移除用户
        for user in self.users:
            user.write_message("[%s]-%02d-[%s]-退出" % (self.request.remote_ip, key, datetime.datetime.now().strftime("%H:%M:%S")))

    def check_origin(self, origin):
        return True  # 允许WebSocket的跨域请求

urls = [
    (r"/", Index),
    (r"/chat", Chat),
]


if __name__ == '__main__':
    # 创建应用实例对象
    app = web.Application(urls)
    server = httpserver.HTTPServer(app)
    # 设置监听的端口和地址
    server.listen(port=8888,address="0.0.0.0")
    server.start(1)
    # ioloop,全局的tornado事件循环,是服务器的引擎核心,start表示创建IO事件循环
    ioloop.IOLoop.current().start()

templates/index.html,代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>聊天室</title>
</head>
<body>
    <div>
        <textarea id="msg"></textarea>
        <button onclick="sendMsg()">发送</button>
    </div>
    <div id="content" style="height:500px;overflow:auto;"></div>
    <script>
        var ws = new WebSocket("ws://127.0.0.1:8888/chat");
        ws.onmessage = function(message) {
            console.log("接收数据:", message);
            content.innerHTML +="<p>" + message.data + "</p>";
        };

        function sendMsg() {
            console.log("发送数据:", msg.value);
            ws.send(msg.value);
            msg.value = "";
        }
    </script>
</body>
</html>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值