区块链开发(二)truffle v4.1.5 搭建智能合约测试环境、开发、编译、部署

网上很多的truffle的环境部署都是2.x或者是3.x。最新的truffle已经更新至4.1.x版本。4.1.x版本的命令较以前版本有较大区别,很多新手(包括我)在刚开始的时候按照网上的教程怎么都搭载不出环境,也报了很多的错误,导致很难受,走了很多的弯路,今天我把最新的truffle 4.1.5的搭载做一个总结,希望能够有所帮助,欢迎批评指正。

以太坊是区块链开发领域最好的编程平台,而truffle是以太坊(Ethereum)最受欢迎的一个开发框架, 搭建truffle开发 环境,运行第一个区块链程序(Dapp)。

1. 使用solidity语言撰写智能合约

Ethereum上的智能合约需要使用solidity语言来撰写。虽然还有其他能用来撰写智能合约的语言如Serpent(类Python)、lll(类Fortran),但目前看到所有公开的智能合约都是使用solidity撰写。

宣传上说,solidity是一种类似Javascript的语言,而且围绕着solidity的各种开发工具链,都是使用属于Javascript生态系的npm来提供的。但我觉得solidity还是比较像Java或C#。因为和Javascript不同,solidity与Java或C#同属于强类型(Strong Type,在定义变数时需要指定类型)语言、在定义函式(function)时同样需指定回传的类型(type)、同样也需要先编译才能执行。这些特性都是Javascript所不具备的。

2.开发前的准备

本文将使用当前最活跃的智能合约开发框架truffle为基础来开发。ENS(Ethereum Name Service)也是采用truffle框架。其他选择还有embark等。

就像一般网站或App开发一样,在提供公开服务之前,开发者会在自己用于写程序的电脑(又称作本机)或透过测试网络来测试程序执行的效果,测试完成后,才会部署到公开的网络上提供服务。开发区块链智能合约(程序)的过程也是如此。特别是公开链上所有写入或读取计算结果的操作都需要真金白银(虚拟代币),而且根据网络状况,每个公开链上的操作都需要要一小段反应时间(15秒~数分钟),这些等待颇浪费宝贵的开发时间⏳。

此外,开发前还需准备一个合手的编辑器。我目前是使用Atom搭配solidity插件来开发。solidity插件除了支持语法高亮之外,也会透过Solium检查并提示基本的语法错误,相当方便。其他编辑器应该也有类似的插件可选择。

这里写图片描述

3. 安装所需工具

通过ubuntu官方apt安装工具安装的node是最新LTS版本的,而本人是个有点强迫症的人,喜欢追求新的东西,也就是想方设法想要去安装最新版本的node,所以本文也就产生了,附上ubuntu安装node和npm的命令行命令:

sudo apt update -y
sudo apt install -y nodejs nodejs-legacy npm
sudo npm config set registry https://registry.npm.taobao.org
sudo npm install n -g
sudo n stable

4、然后通过n模块安装指定版本的nodejs,n模块更多介绍请参考官方文档

//安装官方最新版本
sudo n latest
//安装官方稳定版本
sudo n stable
//安装官方最新LTS版本
sudo n lts

我们通过安装稳定版本的nodejs,然后通过node -v可以发现node安装成功,并且版本号为node-v9.10.1稳定版本

安装truffle框架

sudo  npm install -g truffle

安装以太坊客户端

智能合约必须要部署到链上进行测试。可以选择部署到一些公共的测试链比如Rinkeby或者Ropsten上,缺点是部署和测试时间比较长,而且需要花费一定的时间赚取假代币防止out of gas

还有一种方式就是部署到私链上,Truffle官方推荐使用以下两种客户端:

  • Ganache

  • truffle develop

1、 Ganache

Ganache这个名字比较陌生,但是它的前身testRPC却是大名鼎鼎,网上的很多老文章里都是用testRPCGanache是奶油巧克力的意思,据说是很久之前有个学徒做巧克力的放多了牛奶,师傅正要骂他,结果一尝发现味道还真不错,于是一种新的巧克力就诞生了~ Truffle是松露巧克力,一般是以Ganache为核,然后上面撒上可可粉,所以这两个产品的名字还是很贴切的。
Ganache现在有两个版本,一个是带图形界面的版本,下载地址:
https://github.com/trufflesuite/ganache/releases
这些可执行文件的后缀都比较奇葩,参见下面的对应关系:

  • Windows: Ganache-*.appx

  • Mac: Ganache-*.dmg

  • Linux: Ganache-*.AppImage

还有一个就是命令行版本了,下载方法:

sudo npm install -g ganache-cli  

具体的命令行参数配置参见github:https://github.com/trufflesuite/ganache-cli

2、truffle develop
这个是truffle内置的客户端,跟命令行版本的Ganache基本类似。唯一要注意的是在truffle develop里执行truffle命令的时候需要省略前面的truffle,比如truffle compile只需要敲compile就可以了。

4. 新建第一个项目,分别尝试部署到不同客户端

root@iZbp1faoikuws48tv2f3j0Z:~# cd /usr/local
root@iZbp1faoikuws48tv2f3j0Z:/usr/local# mkdir love && cd love
root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# truffle unbox metacoin
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

  Compile contracts: truffle compile
  Migrate contracts: truffle migrate
  Test contracts:    truffle test

默认会生成一个MetaCoin的demo,可以从这个demo中学习truffle的架构

网上很多老的教程是truffle init webpack命令,但是truffle新的版本不支持,只能是truffle unbox metacoin

目录结构:

  • /contracts:存放智能合约原始代码的地方,可以看到里面已经有三个sol文件,我们开发的Calculator.sol文件就存放在这里。

  • /migrations:这是 Truffle用来部署智能合约的功能,待会儿我们会修改2_deploy_contracts.js来部署 Calculator.sol

  • /test: 测试智能合约的代码放在这里,支持js 与 sol 测试。
  • truffle.js: Truffle 的设置文档。

4.1. truffle develop 方式

truffle develop

运行结果:这个终端控制台就成了truffle的开发控制台(开发终端),不要关闭,里面生成了几个测试账户。

root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# truffle develop
Truffle Develop started at http://127.0.0.1:9545/

Accounts:
(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57
(1) 0xf17f52151ebef6c7334fad080c5704d77216b732
(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5
(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de

Private Keys:
(0) c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3
(1) ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f
(2) 0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1
(3) c88b703fb08cbea894b6aeff5a544fb92e78a18e19814cd85da83b71f772aa6c
(4) 388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418
(5) 659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63
(6) 82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8
(7) aa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7
(8) 0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4
(9) 8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5

Mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat

⚠️  Important ⚠️  : This mnemonic was created for you by Truffle. It is not secure.
Ensure you do not use it on production blockchains, or else you risk losing funds.

truffle(develop)> 

可以看到启动后自动建立了10个帐号(Accounts),与每个帐号对应的私钥(Private Key)。每个帐号中都有100个测试用的以太币(Ether)。

ps:我们留心控制台一句很重要的话Truffle Develop started at http://127.0.0.1:9545/

4.2. Ganache

Ganache默认运行在7545端口,可以在界面右上方的“设置”里进行更改。运行后默认创建10个账号,每个账号里有100ETH的余额。
要部署到链上,需要把IP、端口、网络ID告诉truffle。修改truffle.js

module.exports = {  
    networks: {  
        development: {  
            host: '127.0.0.1',  
            port: '8545',  
            network_id: '*' // Match any network id  
        }  
    }  
};  

因为我们用的是命令行版本,所以启动

root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# ganache-cli
Ganache CLI v6.1.0 (ganache-core: 2.1.0)

Available Accounts
==================
(0) 0x903c3d364c48442297bb2390e7b4fca6480ba1a9
(1) 0x3c1b296673f691ccf4909b6858a52f1f8dfb7992
(2) 0xd182dcd50ed7c901711ad5df5d9981eed1683b2b
(3) 0xbcd532f6fb3e7738fb963fe62f8a36ac01c07868
(4) 0x7608e2b9c29bcd3cc22958670a86f5c6c43728b6
(5) 0xc4b1c366e1947a7439683a767d3305c5a6b24449
(6) 0x196720992be0ef746de49daf85cb8766ab4139d9
(7) 0x5ee5cb300e9e2f0c977225e6041e89cc37648972
(8) 0x1a1382ef079059e25f0a7bf37c0f4cb75e366ad6
(9) 0xff2c551919c38f5665823c94b00c853f6d9a5cce

Private Keys
==================
(0) 0a1af21c6edb579bbdaffbb106121a36d1e0e428ee4752f4a5cee667648d8b1e
(1) 0f5152adb96252bc98ea6b0cda810e1fe498b86abd76ae12d279e7882925a810
(2) 32e57c96a99148263cd95a9640e7394a14ab4de8483ae45b87efc0d296b97ddd
(3) 0ea197c67a91e04043db4dd2d8dffc2c5cf422c3a14200a46d6331865b35cfe1
(4) 2c2353a748540132793fa450c21fff375e43843222872e9ecb38eb96c84b29df
(5) e2464aae137b1898b9b69abe37cf4bdc6e2a8a2510363a9f84a03f0193f5b80d
(6) a667dbd2d2683b2656dbc6978d3da15eb7998f76c721c6c7854df1aa843f71c2
(7) d5a482701e0555c325ea5ce00dc5d29909da1303c4df137986f6ca87c324026d
(8) 916b89b9b9ed5a3bcfc512be40ea76f009785c1b600553bfffece2481df1d342
(9) 218bfa1ae2dc77bb193f881e4edc4e00851a5348d7b8a90d24a2b5f116fba41f

HD Wallet
==================
Mnemonic:      crew thought coffee elite habit keen horse travel permit walnut design various
Base HD Path:  m/44'/60'/0'/0/{account_index}

Listening on localhost:8545

//这里很奇怪端口是8545,图形界面版本是7545

然用下面两条命令编译和部署:

truffle compile  
truffle migrate  

测试合约

metacoin的示例代码里已经把测试代码写好了,直接用下面的命令运行就可以了:

truffle test  

去Ganache上看一下运行结果:

  • Accounts标签:第一个账户里ETH略有减少,因为交易消耗了gas

  • Blocks标签:Ganache是自动挖矿,生成了6个新区块,每个区块里有一个交易

  • Transactions标签:有6笔新交易,可以点开看交易详情

  • Logs标签:显示交易和挖矿日志

这里写图片描述

5. 新建Calculator合约

contracts文件夹下新建Calculator.sol文件,当然也可以直接在love路径下面直接执行truffle create contract Calculator命令来创建Calculator.sol

这里写代码片

Calculator.sol文件內容如下:

pragma solidity ^0.4.4;

contract Calculator { 

    function multiply(uint a) returns(uint d){

        return a * 10;
    }

}

5.1. 讲解

pragma solidity ^0.4.4;

第一行指名目前使用的solidity版本,不同版本的solidity可能会编译出不同的bytecode。^代表兼容solidity0.4.4 ~ 0.4.9的版本。

contract Calculator {
    ...
}

contract关键字类似于其他语言中较常见的class。因为solidity是专为智能合约(Contact)设计的语言,声明contract后即内置了开发智能合约所需的功能。也可以把这句理解为class Calculator extends Contract

function multiply(uint a) returns(uint d){
        return a * 10;
    }

函数的结构与其他程序类似,但如果有传入的参数或回传值,需要指定参数或回传值的类型(type)。

5.2. 编译

现在执行truffle compile命令,我们可以将Calculator.sol原始码编译成Ethereum bytecode

truffle compile可选参数:

  • --compile-all: 强制编译所有合约。
  • --network 名称:指定使用的网络,保存编译的结果到指定的网络上。
truffle(develop)> compile
Compiling ./contracts/ConvertLib.sol...
Compiling ./contracts/Calculator.sol...
Compiling ./contracts/MetaCoin.sol...
Compiling ./contracts/Migrations.sol...

Compilation warnings encountered:

/usr/local/love/contracts/Calculator.sol:4:3: Warning: No visibility specified. Defaulting to "public".
  function multiply(uint a) returns(uint d){
  ^ (Relevant source part starts here and spans across multiple lines).
,/usr/local/love/contracts/MetaCoin.sol:23:3: Warning: Invoking events without "emit" prefix is deprecated.
                Transfer(msg.sender, receiver, amount);
                ^------------------------------------^
,/usr/local/love/contracts/Calculator.sol:4:3: Warning: Function state mutability can be restricted to pure
  function multiply(uint a) returns(uint d){
  ^ (Relevant source part starts here and spans across multiple lines).

Writing artifacts to ./build/contracts
//上面有警告的地方,可以忽略。(原来给的文件里面没有指定函数的可见性)

编译成功后,会在love文件夹下面的build/contracts文件夹下面看见Calculator.json文件。

5.3. 部署

truffle框架中提供了方便部署合约的脚本。打开migrations/2_deploy_contracts.js文件(脚本使用Javascript编写),将内容修改如下:

var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");
var Calculator = artifacts.require("Calculator");

module.exports = function(deployer) {
  deployer.deploy(ConvertLib);
  deployer.link(ConvertLib, MetaCoin);
  deployer.deploy(MetaCoin);
  deployer.deploy(Calculator);
};

使用artifacts.require语句来取得准备部署的合约。使用deployer.deploy语句将合约部署到区块链上。这边Calculatorcontract的名称而不是文件名。因此可以用此语法读入任一.sol文件中的任一合约。

现在执行truffle migrate命令:

truffle(develop)> migrate 
Using network 'develop'.

Running migration: 1_initial_migration.js
  Replacing Migrations...
  ... 0xf49900f572bfc1360478d3c0c5eab740cb17bcf48d7a5b420a2d6e9e03fd34d6
  Migrations: 0x9fbda871d559710256a2502a2517b794b482db40
Saving successful migration to network...
  ... 0xd25ff9b6c664ef81d7120ca521d702a0dfec8dc7098262c8bf2bd0613e4b735d
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying Calculator...
  ... 0x3698332ec8b36f2b17d71237f933605dc8d105427bf887ab15cfdc39e3e5b110
  Calculator: 0x30753e4a8aad7f8597332e813735def5dd395028
Saving successful migration to network...
  ... 0x0f418f206ad968c836e9a2b87cc8e7c604fd22e1c7bfbf854aaa1b1455ad1883
Saving artifacts...
truffle(develop)> 

5.4. 与合约互动

可用Javascript来和刚刚部署的合约互动。

truffle(develop)> Calculator.deployed().then(instance => contract = instance)
TruffleContract {
  constructor: 
   { [Function: TruffleContract]
...
truffle(develop)> 

讲解

Calculator.deployed().then(instance => contract = instance)

truffle console中预载了truffle-contract函数库,以方便操作部署到区块链上的合约。

truffle console可选参数:

  • --network 名称:指定要使用的网络
  • --verbose-rpc:输出Truffle与RPC通信的详细信息。

这边使用Calculator.deployed().then语句来取得Calculator合约的Instance(实例),并存到contract变量中,以方便后续的调用。

上面用的是Javascript ES6+的语法,这句也可以写成:

Calculator.deployed().then(instance => {
    contract = instance
});

还可以用Javascript ES5的写法:

Calculator.deployed().then(function(instance) {
  hello = instance;
});

这里直接呼叫contract.multiply(6)也会得到一样的结果。truffle-contract提供使用call()来读取只读(read only)的数据,这样就不需提供gas。因此如果遇到的操作需要向区块链写入数据,我们就不能用call语句了。

truffle(develo)> contract.multiply.call(6)
BigNumber { s: 1, e: 1, c: [ 60 ] }

如此一来,我们已写好并部署完成了第一个智能合约,也验证了合约确实可以运作。

另外还有一种通过智能合约地址的方式来运行Calculator智能合约命令,
输入Calculator智能合约命令,显示打印出一个json结构,展示了它的各种属性内容。

truffle(development)> Calculator
{ [Function: TruffleContract]
  _static_methods: 
   { setProvider: [Function: setProvider],
     new: [Function: new],
     at: [Function: at],
     deployed: [Function: deployed],
     defaults: [Function: defaults],
     hasNetwork: [Function: hasNetwork],
     isDeployed: [Function: isDeployed],
     detectNetwork: [Function: detectNetwork],
     setNetwork: [Function: setNetwork],
     resetAddress: [Function: resetAddress],
     link: [Function: link],
     clone: [Function: clone],
     addProp: [Function: addProp],
     toJSON: [Function: toJSON] },
......
truffle(development)>      
truffle(development)> Calculator.at("0xb44e18062ff2868f6fd607bd1a6562e2a30b02a7").multiply.call(6)
BigNumber { s: 1, e: 1, c: [ 60 ] }
truffle(development)> 

6. 部署智能合约至Ethereum节点

以上的智能合约都是运行在truffle develop或者 Ganache

如果是真实环境呢,我们需要部署到自己的私有链中,或者以太坊网络。

truffle compile
truffle migrate --network live

truffle migrate可选的参数:

  • --reset: 从头运行所有的移植。
  • --network 名称:指定要使用的网络,并将编译后的资料保存到那个网络。
  • --to number:将版本从当前版本移植到序号指定的版本。
  • --compile-all: 强制编译所有的合约
  • --verbose-rpc:打印Truffle与RPC交互的详细日志。

你还可以选择性的指定rpc的配置信息。下面是一个示例:

networks: {
  "live": {
    network_id: 1, // Ethereum public network 以太坊公网
    // optional config values
    // host - defaults to "localhost"
    // port - defaults to 8545
    // gas
    // gasPrice
    // from - default address to use for any transaction Truffle makes during migrations
  },
  "morden": {
    network_id: 2,        // Official Ethereum test network
    host: "178.25.19.88", // Random IP for example purposes (do not use)
    port: 80
  },
  "staging": {
    network_id: 1337 // custom private network
    // use default rpc settings
  },
  "private": {
    host: '127.0.0.1',
    port: 8545,
    network_id: '15' 
  }
}

RPC

关于如何连接到以太坊客户端的一些细节。host和port是需要,另外还需要一些其它的。

  • host:指向以太坊客户端的地址。本机开发时,一般为localhost
  • port:以太坊客户端接收请求的端口,默认是8545
  • gas:部署时的Gas限制,默认是4712388
  • gasPrice:部署时的Gas价格。默认是100000000000(100 Shannon)
  • from:移植时使用的源地址。如果没有指定,默认是你的以太坊客户端第一个可用帐户。

使用truffle compile对合约进行编译,使用truffle migrate将合约部署到区块链上会出现一下错误:

root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# Error encountered, bailing. Network state unknown. Review successful transactions manually.
Error: authentication needed: password or unlockError: command not found
root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# 

这说明我们要对账户进行解锁才可以使用。

回到geth 运行的终端中, 输入以下命令进行解锁:

personal.unlockAccount(web3.eth.coinbase, "password", 15000)

其中password部分为之前创建账户的密码,15000对应的是解锁的时间。
如果再次使用truffle migrate进行部署,仍旧出现以下错误:

root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# Error encountered, bailing. Network state unknown. 
Review successful transactions manually.
Error: authentication needed: password or unlockError: command not found
root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# 

这说明我们没有足够的钱来部署合约,需要矿工挖矿来获得ether

继续使用truffle migrate来部署,terminal及继续报错:

root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# truffle migrate --network private
Using network 'private'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... undefined
Error encountered, bailing. Network state unknown. Review successful transactions manually.
Error: exceeds block gas limit
    at Object.InvalidResponse (/usr/local/lib/node_modules/truffle/build/webpack:/~/web3/lib/web3/errors.js:38:1)
....
    at endReadableNT (_stream_readable.js:1106:12)
    at process._tickCallback (internal/process/next_tick.js:114:19)

去truffle github issues中查找,找到一行解决办法,粘贴如下:

Possibility: you're giving the transaction too high of a gasLimit. 
If the transaction has a limit of 2,000,000, 
it'd stop you since it could theoretically go over the block gas limit, 
even if in practice it won't. If this is the case, 
see if you can reduce the transaction's gasLimit while remaining above the amount it actually needs--that might do the trick.

好,我们再修改一下truffle.js如下:

module.exports = {  
    networks: {  
        development: {  
            host: '127.0.0.1',  
            port: '8545',  
            network_id: '*' // Match any network id  
        },
        private: {  
            host: '127.0.0.1',  
            port: '8545',  
            network_id: '15',
            gas:500000
        }    
    }  
};  

继续执行truffle migrate --network private,执行成功。

root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love# truffle migrate --network private --reset
Using network 'private'.

Running migration: 1_initial_migration.js
  Replacing Migrations...
  ... 0xce5ddbc9c7f4f97ad81e871a3461ca2b6cbf631c62950a115648360beb7d9a48
  Migrations: 0x6189ae103b68f83a1c8e5e366232fa2380b330f7
Saving successful migration to network...
  ... 0xd731f9c8ccf6342333798a4ef38ecb8cdd507496341b9329824a30955901c7b0
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Replacing Calculator...
  ... 0xfcee25fbe5b351bb9f6502d0b7d5e409380dc6b1ffed2310bcdc66efd35d12ae
  Calculator: 0x6cebe9057d6c5d3d10d660812e95ecbb9b1948e8
Saving successful migration to network...
  ... 0x01593cdf4df7c5fa208505557e0257186afd7bd1fde943542b1c0dcb5833dd30
Saving artifacts...
root@iZbp1faoikuws48tv2f3j0Z:/usr/local/love#

可以看到,我们的智能合约已经被成功部署了,且日志中的hash值与上面监听状态的terminal中显式的是相同的,说明是一致的。下面我们就可以在终端使用该智能合约了。

注意,truffle.js文件中的network_id必须和私有链中命令geth --networkid 15 --datadir data --rpc --rpcport 8545 --nodiscover console中设置的保持一致

7. GETH正式环境运行合约

  • 智能合约地址Address

这里写图片描述

  • 智能合约ABI(Application Binary Interface)

这里写图片描述

> var calculator = eth.contract([{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]).at("0xe20d707e09c537d481f2a5df57270faa94a37bac")
undefined
> calculator.multiply.call(6)
60
> calc.multiply.call(6)
42

参考
http://truffle.tryblockchain.org/Truffle-introduce-%E4%BB%8B%E7%BB%8D.html

猜你喜欢

转载自blog.csdn.net/dreamsunday/article/details/79870359