超详细的python搭建区块链(中)

经过上期 超详细的Python搭建区块链(上)的准备工作我们可以开始进入编码阶段了。

1.使用pycharm新建一个文件命名为blockchain.py,然后在里面创建一个Blockchain

   然后在Blockchain类里面自定义函数如下所示:

​
class Blockchain(object):
    def __init__(self):
        self.chain = []  # 此列表表示区块链对象本身。
        self.currentTransaction = []  # 此列表用于记录目前在区块链网络中已经经矿工确认合法的交易信息,等待写入新区块中的交易信息。
        self.nodes = set()  # 建立一个无序元素集合。此集合用于存储区块链网络中已发现的所有节点信息
        # Create the genesis block(创建创世区块)
        self.new_block(proof=100, previous_hash=1)
 
    def new_block(self):
        # 创建一个新的区块,并将其加入到链中
        pass
 
    def new_transaction(self):
        # 向交易列表中添加一个新的交易
        pass
 
    @staticmethod
    def hash(block):
        # Hashes a Block 散列块
        pass
 
    @property
    def last_block(self):
        # 返回区块链中最新的区块
        pass
 

 接下来我们来完善里面的函数

 1.1 创建新区块

 # 创建新区块
    def new_block(self, proof, previous_hash = None):
        # Creates a new Block and adds it to the chain(创建一个新的区块,并将其加入到链中)
        """
        生成新块
        :param proof: <int> The proof given by the Proof of Work algorithm
        :param previous_hash: (Optional) <str> Hash of previous Block
        :return: <dict> New Block
         """
        block = {
            'index': len(self.chain) + 1,   # 区块编号
            'timestamp': time(),  # 时间戳
            'transactions': self.currentTransaction,  # 交易信息
            'proof': proof,  # 矿工通过算力证明(工作量证明)成功得到的Number Once值,证明其合法创建了一个区块(当前区块)
            'previous_hash': previous_hash or self.hash(self.chain[-1])  # 前一个区块的哈希值
        }

        # Reset the current list of transactions(重置当前事务列表)
        '''
        因为已经将待处理(等待写入下一下新创建的区块中)交易信息列表(变量是:transactions)
        中的所有交易信息写入了区块并添加到区块链末尾,则此处清除此列表中的内容'
        '''
        self.currentTransaction = []
        # 将当前区块添加到区块链末端
        self.chain.append(block)
        return block

 下面我们来通过一个例子来解释下这个区块所包含的东西,下面的代码所示

block = {
    'index': 1,
    'timestamp': 1506057125.900785,
    'transactions': [
        {
            'sender': "8527147fe1f5426f9dd545de4b27ee00",
            'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
            'amount': 5,
        }
    ],
    'proof': 324984774000,
    'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

在区块链中,每一个区块包含一个索引、一个时间戳、一个交易列表、一个证明(之后更多)和前一个区块的哈希值。

1.2 创建新交易

       在区块链上如何创建一笔交易呢 ?代码如下所示

  # 创建新交易
    def new_transaction(self, sender, recipient, amount):
        # Adds a new transaction to the list of transactions(向交易列表中添加一个新的交易)
        """
                生成新交易信息,此交易信息将加入到下一个待挖的区块中
                :param sender: Address of the Sender  # 发送方
                :param recipient: Address of the Recipient # 接收方
                :param amount: Amount  # 数量
                :return: The index of the Block that will hold this transaction # 需要将交易记录在下一个区块中
        """
        self.currentTransaction.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        # 下一个待挖的区块中
        return self.last_block['index'] + 1

 1.3 生成这个区块的哈希值

    @staticmethod
    def hash(block):
        # 根据一个区块 来生成这个区块的哈希值(散列值)
        """
               生成块的 SHA-256 hash值
               :param block: <dict> Block
               :return: <str>
               转化为json编码格式之后hash,最后以16进制的形式输出
         """

        # 我们必须确保字典是有序的,否则我们会有不一致的哈希值,sort_keys=True指明了要进行排序
        '''
        首先通过json.dumps方法将一个区块打散,并进行排序(保证每一次对于同一个区块都是同样的排序)
        这个时候区块被转换成了一个json字符串(不知道怎么描述)
        然后,通过json字符串的encode()方法进行编码处理。
        其中encode方法有两个可选形参,第一个是编码描述字符串,另一个是预定义错误信息
        默认情况下,编码描述字符串参数就是:默认编码为 'utf-8'。此处就是默认编码为'utf-8'
        '''
        block_string = json.dumps(block, sort_keys=True).encode()
        # hexdigest(…)以16进制的形式输出
        return hashlib.sha256(block_string).hexdigest()

 1.4  返回最后一个区块

    @property
    def last_block(self):
        return self.chain[-1]  # 区块链的最后一个区块

 以上的代码合并在一起如下所示

import hashlib
import json
from time import time
from uuid import uuid4
from flask import Flask, jsonify, request


class Blockchain(object):
    # 区块链初始化
    def __init__(self):
        self.chain = []  # 此列表表示区块链对象本身。
        self.currentTransaction = []  # 此列表用于记录目前在区块链网络中已经经矿工确认合法的交易信息,等待写入新区块中的交易信息。
        self.nodes = set()  # 建立一个无序元素集合。此集合用于存储区块链网络中已发现的所有节点信息
        # Create the genesis block(创建创世区块)
        self.new_block(proof=100, previous_hash=1)

    # 创建新区块
    def new_block(self, proof, previous_hash = None):
        # Creates a new Block and adds it to the chain(创建一个新的区块,并将其加入到链中)
        """
        生成新块
        :param proof: <int> The proof given by the Proof of Work algorithm
        :param previous_hash: (Optional) <str> Hash of previous Block
        :return: <dict> New Block
         """
        block = {
            'index': len(self.chain) + 1,   # 区块编号
            'timestamp': time(),  # 时间戳
            'transactions': self.currentTransaction,  # 交易信息
            'proof': proof,  # 矿工通过算力证明(工作量证明)成功得到的Number Once值,证明其合法创建了一个区块(当前区块)
            'previous_hash': previous_hash or self.hash(self.chain[-1])  # 前一个区块的哈希值
        }

        # Reset the current list of transactions(重置当前事务列表)
        '''
        因为已经将待处理(等待写入下一下新创建的区块中)交易信息列表(变量是:transactions)
        中的所有交易信息写入了区块并添加到区块链末尾,则此处清除此列表中的内容'
        '''
        self.currentTransaction = []
        # 将当前区块添加到区块链末端
        self.chain.append(block)
        return block

    # 创建新交易
    def new_transaction(self, sender, recipient, amount):
        # Adds a new transaction to the list of transactions(向交易列表中添加一个新的交易)
        """
                生成新交易信息,此交易信息将加入到下一个待挖的区块中
                :param sender: Address of the Sender  # 发送方
                :param recipient: Address of the Recipient # 接收方
                :param amount: Amount  # 数量
                :return: The index of the Block that will hold this transaction # 需要将交易记录在下一个区块中
        """
        self.currentTransaction.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        # 下一个待挖的区块中
        return self.last_block['index'] + 1


    @staticmethod
    def hash(block):
        # 根据一个区块 来生成这个区块的哈希值(散列值)
        """
               生成块的 SHA-256 hash值
               :param block: <dict> Block
               :return: <str>
               转化为json编码格式之后hash,最后以16进制的形式输出
         """

        # 我们必须确保字典是有序的,否则我们会有不一致的哈希值,sort_keys=True指明了要进行排序
        '''
        首先通过json.dumps方法将一个区块打散,并进行排序(保证每一次对于同一个区块都是同样的排序)
        这个时候区块被转换成了一个json字符串(不知道怎么描述)
        然后,通过json字符串的encode()方法进行编码处理。
        其中encode方法有两个可选形参,第一个是编码描述字符串,另一个是预定义错误信息
        默认情况下,编码描述字符串参数就是:默认编码为 'utf-8'。此处就是默认编码为'utf-8'
        '''
        block_string = json.dumps(block, sort_keys=True).encode()
        # hexdigest(…)以16进制的形式输出
        return hashlib.sha256(block_string).hexdigest()

    @property
    def last_block(self):
        return self.chain[-1]  # 区块链的最后一个区块

   

 在区块链创建完成后,我们需要创建一个创世区块(也就是区块链上的第一个区块)。当然,创世区块也需要被证明,这需要通   过Pow(工作量证明)的挖矿机制。

 Pow的目标是找出一个符合特定条件的数字,这个数字很难计算出来,但容易验证,这就是工作量证明的核心思想。

 举个简单的例子

 假设一个整数 x 乘以另一个整数 y 的积的 Hash 值必须以 0 结尾,即 hash(x * y) = ac23dc…0。设变量 x = 5,求 y 的值?

2 工作量证明

    # 工作量证明
    def proof_of_work(self, lastProof):
        """
        简单的工作量证明:
         - 查找一个 p' 使得 hash(pp') 以4个0开头
         - p 是上一个块的证明,  p' 是当前的证明
        :param last_proof: <int>
        :return: <int>
        """

        # 下面通过循环来使proof的值从0开始每次增加1来进行尝试,直到得到一个符合算法要求 的proof值为止
        proof = 0
        while self.valid_proof(lastProof, proof) is False:
            proof += 1  # 如果得到的proof值不符合要求,那么就继续寻找。
        # 返回这个符合算法要求的proof值
        return proof

    #  此函数是上一个方法函数的附属部分,用于检查哈希值是否满足挖矿条件。用于工作函数的证明中
    @staticmethod
    def valid_proof(lastProof, proof):
        """
        验证证明: 是否hash(last_proof, proof)以4个0开头?
        :param last_proof: <int> Previous Proof
        :param proof: <int> Current Proof
        :return: <bool> True if correct, False if not.
        """
        # 根据传入的参数proof来进行尝试运算,得到一个转码为utf-8格式的字符串
        guess = f'{lastProof}{proof}'.encode()
        # 将此字符串(guess)进行sha256方式加密,并转换为十六进制的字符串
        guessHash = hashlib.sha256(guess).hexdigest()
        # 验证该字符前4位是否为0,如果符合要求,就返回True,否则 就返回False
        return guessHash[:4] == '0000'

上面是我们自己定义的一个Pow算法,规则是:寻找一个数 p,使得它与前一个区块的 proof 拼接成的字符串的 Hash 值以 4 个零开头。

2.1 上面的代码合并如下所示

import hashlib
import json
from time import time
from uuid import uuid4
from flask import Flask, jsonify, request


class Blockchain(object):
    # 区块链初始化
    def __init__(self):
        self.chain = []  # 此列表表示区块链对象本身。
        self.currentTransaction = []  # 此列表用于记录目前在区块链网络中已经经矿工确认合法的交易信息,等待写入新区块中的交易信息。
        self.nodes = set()  # 建立一个无序元素集合。此集合用于存储区块链网络中已发现的所有节点信息
        # Create the genesis block(创建创世区块)
        self.new_block(proof=100, previous_hash=1)

    # 创建新区块
    def new_block(self, proof, previous_hash = None):
        # Creates a new Block and adds it to the chain(创建一个新的区块,并将其加入到链中)
        """
        生成新块
        :param proof: <int> The proof given by the Proof of Work algorithm
        :param previous_hash: (Optional) <str> Hash of previous Block
        :return: <dict> New Block
         """
        block = {
            'index': len(self.chain) + 1,   # 区块编号
            'timestamp': time(),  # 时间戳
            'transactions': self.currentTransaction,  # 交易信息
            'proof': proof,  # 矿工通过算力证明(工作量证明)成功得到的Number Once值,证明其合法创建了一个区块(当前区块)
            'previous_hash': previous_hash or self.hash(self.chain[-1])  # 前一个区块的哈希值
        }

        # Reset the current list of transactions(重置当前事务列表)
        '''
        因为已经将待处理(等待写入下一下新创建的区块中)交易信息列表(变量是:transactions)
        中的所有交易信息写入了区块并添加到区块链末尾,则此处清除此列表中的内容'
        '''
        self.currentTransaction = []
        # 将当前区块添加到区块链末端
        self.chain.append(block)
        return block

    # 创建新交易
    def new_transaction(self, sender, recipient, amount):
        # Adds a new transaction to the list of transactions(向交易列表中添加一个新的交易)
        """
                生成新交易信息,此交易信息将加入到下一个待挖的区块中
                :param sender: Address of the Sender  # 发送方
                :param recipient: Address of the Recipient # 接收方
                :param amount: Amount  # 数量
                :return: The index of the Block that will hold this transaction # 需要将交易记录在下一个区块中
        """
        self.currentTransaction.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        # 下一个待挖的区块中
        return self.last_block['index'] + 1


    @staticmethod
    def hash(block):
        # 根据一个区块 来生成这个区块的哈希值(散列值)
        """
               生成块的 SHA-256 hash值
               :param block: <dict> Block
               :return: <str>
               转化为json编码格式之后hash,最后以16进制的形式输出
         """

        # 我们必须确保字典是有序的,否则我们会有不一致的哈希值,sort_keys=True指明了要进行排序
        '''
        首先通过json.dumps方法将一个区块打散,并进行排序(保证每一次对于同一个区块都是同样的排序)
        这个时候区块被转换成了一个json字符串(不知道怎么描述)
        然后,通过json字符串的encode()方法进行编码处理。
        其中encode方法有两个可选形参,第一个是编码描述字符串,另一个是预定义错误信息
        默认情况下,编码描述字符串参数就是:默认编码为 'utf-8'。此处就是默认编码为'utf-8'
        '''
        block_string = json.dumps(block, sort_keys=True).encode()
        # hexdigest(…)以16进制的形式输出
        return hashlib.sha256(block_string).hexdigest()

    @property
    def last_block(self):
        return self.chain[-1]  # 区块链的最后一个区块

    # 工作量证明
    def proof_of_work(self, lastProof):
        """
        简单的工作量证明:
         - 查找一个 p' 使得 hash(pp') 以4个0开头
         - p 是上一个块的证明,  p' 是当前的证明
        :param last_proof: <int>
        :return: <int>
        """

        # 下面通过循环来使proof的值从0开始每次增加1来进行尝试,直到得到一个符合算法要求 的proof值为止
        proof = 0
        while self.valid_proof(lastProof, proof) is False:
            proof += 1  # 如果得到的proof值不符合要求,那么就继续寻找。
        # 返回这个符合算法要求的proof值
        return proof

    #  此函数是上一个方法函数的附属部分,用于检查哈希值是否满足挖矿条件。用于工作函数的证明中
    @staticmethod
    def valid_proof(lastProof, proof):
        """
        验证证明: 是否hash(last_proof, proof)以4个0开头?
        :param last_proof: <int> Previous Proof
        :param proof: <int> Current Proof
        :return: <bool> True if correct, False if not.
        """
        # 根据传入的参数proof来进行尝试运算,得到一个转码为utf-8格式的字符串
        guess = f'{lastProof}{proof}'.encode()
        # 将此字符串(guess)进行sha256方式加密,并转换为十六进制的字符串
        guessHash = hashlib.sha256(guess).hexdigest()
        # 验证该字符前4位是否为0,如果符合要求,就返回True,否则 就返回False
        return guessHash[:4] == '0000'

3 使用Flask框架来简历区块服务器

   我们打算使用 Python 的 Flask 框架,它是一个轻型框架,可以很容易实现端点到Python函数的映射。这样,我们就可以使用 HTTP 请求通过网页访问我们的区块链了。

我们分别创建以下三个方法:

/transactions/new 为一个区块创建一个新的交易。

/mine : 告诉我们的服务器开采一个新的区块。

/chain :返回完整的 Blockchain 类。

接下来,我们来搭建Flask框架,我们的服务器会在区块链网络中形成单个节点。代码如下所示:

import hashlib
import json
from time import time
from uuid import uuid4
from flask import Flask, jsonify, request


class Blockchain(object):
    # 区块链初始化
    def __init__(self):
        self.chain = []  # 此列表表示区块链对象本身。
        self.currentTransaction = []  # 此列表用于记录目前在区块链网络中已经经矿工确认合法的交易信息,等待写入新区块中的交易信息。
        self.nodes = set()  # 建立一个无序元素集合。此集合用于存储区块链网络中已发现的所有节点信息
        # Create the genesis block(创建创世区块)
        self.new_block(proof=100, previous_hash=1)

    # 创建新区块
    def new_block(self, proof, previous_hash = None):
        # Creates a new Block and adds it to the chain(创建一个新的区块,并将其加入到链中)
        """
        生成新块
        :param proof: <int> The proof given by the Proof of Work algorithm
        :param previous_hash: (Optional) <str> Hash of previous Block
        :return: <dict> New Block
         """
        block = {
            'index': len(self.chain) + 1,   # 区块编号
            'timestamp': time(),  # 时间戳
            'transactions': self.currentTransaction,  # 交易信息
            'proof': proof,  # 矿工通过算力证明(工作量证明)成功得到的Number Once值,证明其合法创建了一个区块(当前区块)
            'previous_hash': previous_hash or self.hash(self.chain[-1])  # 前一个区块的哈希值
        }

        # Reset the current list of transactions(重置当前事务列表)
        '''
        因为已经将待处理(等待写入下一下新创建的区块中)交易信息列表(变量是:transactions)
        中的所有交易信息写入了区块并添加到区块链末尾,则此处清除此列表中的内容'
        '''
        self.currentTransaction = []
        # 将当前区块添加到区块链末端
        self.chain.append(block)
        return block

    # 创建新交易
    def new_transaction(self, sender, recipient, amount):
        # Adds a new transaction to the list of transactions(向交易列表中添加一个新的交易)
        """
                生成新交易信息,此交易信息将加入到下一个待挖的区块中
                :param sender: Address of the Sender  # 发送方
                :param recipient: Address of the Recipient # 接收方
                :param amount: Amount  # 数量
                :return: The index of the Block that will hold this transaction # 需要将交易记录在下一个区块中
        """
        self.currentTransaction.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        # 下一个待挖的区块中
        return self.last_block['index'] + 1


    @staticmethod
    def hash(block):
        # 根据一个区块 来生成这个区块的哈希值(散列值)
        """
               生成块的 SHA-256 hash值
               :param block: <dict> Block
               :return: <str>
               转化为json编码格式之后hash,最后以16进制的形式输出
         """

        # 我们必须确保字典是有序的,否则我们会有不一致的哈希值,sort_keys=True指明了要进行排序
        '''
        首先通过json.dumps方法将一个区块打散,并进行排序(保证每一次对于同一个区块都是同样的排序)
        这个时候区块被转换成了一个json字符串(不知道怎么描述)
        然后,通过json字符串的encode()方法进行编码处理。
        其中encode方法有两个可选形参,第一个是编码描述字符串,另一个是预定义错误信息
        默认情况下,编码描述字符串参数就是:默认编码为 'utf-8'。此处就是默认编码为'utf-8'
        '''
        block_string = json.dumps(block, sort_keys=True).encode()
        # hexdigest(…)以16进制的形式输出
        return hashlib.sha256(block_string).hexdigest()

    @property
    def last_block(self):
        return self.chain[-1]  # 区块链的最后一个区块

    # 工作量证明
    def proof_of_work(self, lastProof):
        """
        简单的工作量证明:
         - 查找一个 p' 使得 hash(pp') 以4个0开头
         - p 是上一个块的证明,  p' 是当前的证明
        :param last_proof: <int>
        :return: <int>
        """

        # #下面通过循环来使proof的值从0开始每次增加1来进行尝试,直到得到一个符合算法要求 的proof值为止
        proof = 0
        while self.valid_proof(lastProof, proof) is False:
            proof += 1  # 如果得到的proof值不符合要求,那么就继续寻找。
        # 返回这个符合算法要求的proof值
        return proof

    #  此函数是上一个方法函数的附属部分,用于检查哈希值是否满足挖矿条件。用于工作函数的证明中
    @staticmethod
    def valid_proof(lastProof, proof):
        """
        验证证明: 是否hash(last_proof, proof)以4个0开头?
        :param last_proof: <int> Previous Proof
        :param proof: <int> Current Proof
        :return: <bool> True if correct, False if not.
        """
        # 根据传入的参数proof来进行尝试运算,得到一个转码为utf-8格式的字符串
        guess = f'{lastProof}{proof}'.encode()
        # 将此字符串(guess)进行sha256方式加密,并转换为十六进制的字符串
        guessHash = hashlib.sha256(guess).hexdigest()
        # 验证该字符前4位是否为0,如果符合要求,就返回True,否则 就返回False
        return guessHash[:4] == '0000'


# 实例化我们的节点;加载 Flask 框架
app = Flask(__name__)

# 为我们的节点创建一个随机名称
node_identifier = str(uuid4()).replace('-', '')

# 实例化 Blockchain 类
blockchain = Blockchain()


# 创建 /transactions/new 端点,这是一个 POST 请求,我们将用它来发送数据
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    # 将请求参数做了处理,得到的是字典格式的,因此排序会打乱依据字典排序规则
    values = request.get_json()

    # 检查所需字段是否在过账数据中
    required = ['sender', 'recipient', 'amount']
    if not all(k in values for k in required):
        return 'Missing values', 400  # HTTP状态码等于400表示请求错误

    # 创建新交易
    index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])
    response = {'message': f'Transaction will be added to Block {index}'}
    return jsonify(response), 201


# 创建 /mine 端点,这是一个GET请求
@app.route('/mine', methods=['GET'])
def mine():
    # 我们运行工作证明算法来获得下一个证明
    last_block = blockchain.last_block  # 取出区块链现在的最后一个区块
    last_proof = last_block['proof']  # 取出这最后 一个区块的哈希值(散列值)
    proof = blockchain.proof_of_work(last_proof)  # 获得了一个可以实现优先创建(挖出)下一个区块的工作量证明的proof值。

    # 由于找到了证据,我们会收到一份奖励
    # sender为“0”,表示此节点已挖掘了一个新货币
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifier,
        amount=1,
    )

    # 将新块添加到链中打造新的区块
    previous_hash = blockchain.hash(last_block)  # 取出当前区块链中最长链的最后一个区块的Hash值,用作要新加入区块的前导HASH(用于连接)
    block = blockchain.new_block(proof, previous_hash)  # 将新区快添加到区块链最后

    response = {
        'message': "New Block Forged",
        'index': block['index'],
        'transactions': block['transactions'],
        'proof': block['proof'],
        'previous_hash': block['previous_hash'],
    }
    return jsonify(response), 200


# 创建 /chain 端点,它是用来返回整个 Blockchain类
@app.route('/chain', methods=['GET'])
# 将返回本节点存储的区块链条的完整信息和长度信息。
def full_chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain),
    }
    return jsonify(response), 200






# 设置服务器运行端口为 5000
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)



【测试阶段】

  运行我们的blockchain.py文件,出现如下所示说明运行没问题

  

  然后打开我们的Postman软件,在里面输入:http://localhost:5000/chain

  这里显示的是我们的链上有多少个区块如下所示

因为我们没有创建新的区块,所以这里显示的是我们初始化的创世区块。

这时候我们来创建一个区块,在里面输入:http://localhost:5000/mine

如图所示我们创建了一个区块,区块号为2,然后我们看看现在这条链上有多少个区块

接下来我们在区块2上面创建一笔交易,在Postman里面输入:http://localhost:5000/transactions/new

上面图中有些细节我们这里需要注意,我都给大家标注出来了,里面要输入的代码如下

 {
                    "amount": 5,
                    "recipient": "someone-other-address",
                    "sender": "d4ee26eee15148ee92c6cd394edd974e"
 }

点击send然后出现如下所示:

可以看到交易成功,返回201,表示更新了服务器数据,并且说这个交易即将被更新到区块3上面。

此时我们在创建一个区块,输入http://localhost:5000/chain

可以看到我们的交易添加到了区块3上面了。

【小结】

  至此,我们就创建了一个简单的区块链了。

  如果你按照我一步步来操作的话应该是没问题的,如果你只是急于求成想运行结果,那么你出错的概率很大。

  所以在这里告诫大家,按照我的步骤一步步来是没有什么问题的,如果你出了什么问题请自己找错。

  下一篇文章:超详细的python搭建区块链(下)

猜你喜欢

转载自blog.csdn.net/wyf2017/article/details/108227875