标签:EOS智能合约 等于 1.5 include 内容 文件名 log 是什么 函数名
原文链接:醒者呆的博客园,https://www.cnblogs.com/Evsward/p/eos-exchange.html
EOS智能合约中包含一个exchange合约,它支持用户创建一笔交易,是任何两个基本货币类型之间的交易。这个合约的作用是跨不同币种(都是EOS上的标准货币类型)的,通过各自与EOS主链价值进行锚定,然后再相互发起交易兑换。要搞清楚的是,这与区块链“传统的”交易所并不一样,那个主要是集中在交易撮合上面,而且必须是同一币种。
关键字:EOS token 经济模型,exchange,Pegged Currency,LTV,cmake,跨token交易,ubuntu编译boost库,通证模型,抵押资产,token价值转换
上面一直提到标准货币(standard currency),在EOS.io中也针对这一标准货币的设计给出了官方文档,本章先来弄清楚标准货币的概念,然后再继续分析exchange。
文章的名字叫《Pegged Derivative Currency Design》
Pegged Currency的含义是什么?
Currency pegging是用来固定汇率的一种方式,通过匹配当前货币与其他货币的价值锚定,或者其他价值度量方式,例如黄金和白银。所以Pegged Currency可以理解为汇率稳定的货币。
所以,这篇文章的题目可以翻译为:一种价值稳定的衍生货币设计。
目前市面已有的Pegged Derivative 货币,例如BitUSD,比特美元,它是一种以加密货币作为抵押。发行人是短线持有美元而额外长地持有加密货币。而购买者是单纯地长期持有美元。
比特股BitShares创造了第一个可行的价值稳定的资产系统:
允许任何人获得一个最小的抵押状况是,公布抵押物和获得的BitUSD的价值比率在最小的1.5:1(抵押物:负债率)。
最少的抵押状况下,要强迫BitUSD持有人在任何市场价格下跌超过美元几个百分点以下的时候的流动性(如果BitUSD持有人选择使用强制清算是不允许的)。换句话说,就是在当前货币下跌的时候,也要保证货币流通性,这是为了货币状况健康运营而考虑。
为了防止价格补偿(直接通过增发和卖出来控制价格)的滥用,所有的强制清算会被推迟。当发生“黑天鹅”事件(极小可能性发生,但实际上却发生了)的时候,所有的短线会通过价格补偿拥有他们自己的清算状况,而所有的BitUSD的持有者只能承诺一个固定的赎回率(清算时固定一个赎回率)。
这种设计的问题体现在:
这是一种让短线持股人通过提供一种高流通性的价值稳定的资产来稳定货币资产。他们会通过鼓励人们交易他们的稳值资产来获益,赚取交易费而不是在投机市场寻求高杠杆。也会通过赚取短线持股的利息。
一个初始用户存储了一个可担保货币(C)到一个智能合约,并且提供一个初始化价格补偿。通过C:D等于1.5:1的价格补偿率发行一个新的负债token(D),这个token会被存储在Bancor市场制造商。
Bancor是为以太坊的代币的价值判断以及流通机制。通过智能合约,可将这些代币作为储备金,让任何人随时随地通过智能合约进行快速兑换,销毁代币,提高代币流通性。
这样一来,因为没有D token被卖出,所以市场制造商是0杠杆。那个初始用户也会收到交换token(E)从市场制造商那里。
我们继续,人们现在可以购买E或者D token,Bancor算法会提供基于C,E,D之间的流通性。由于市场制造商的收费,E的价值将随着C而增长。
C = 智能代币或者母货币储备
D或E = 奖励代币(发放给初期持有人,以及社区贡献者)
抵押率C:D = 价值C(抵押物)借款D(负债)比(反过来就是LTV,loan to value)
我们做了这么多工作,其实目的就是要保障D这种token(token本身就是衍生货币)是符合Pegged Currency的设定。最直接的指标就是与美元价值(C就可以是这个角色)的锚定浮动范围,要尽可能小,这个范围的浮动要小到让更多的人(套汇者)愿意持有和交易D token。下面有几种可能性:
市场体量 = 联结者体量(Bancor)
赎回价格:在到期之前,发行人可以回购持有者的token。
基于上面标准货币体系,我们可以看到在EOS上面的token的经济模型,这是一个很有竞争力的模型,可以保证每个token的价值稳定,而不是狂涨狂跌,真正使EOS上的经济生态健康稳定运转起来。下面来研究exchange智能合约的主要功能。
首先来看CMake设置,上文《【精解】EOS智能合约演练》中也有CMake的应用,但并没有搞太清楚,这里在讨论exchange的CMakeLists配置之前,我们先来搞定cmake本身。
cmake
CMake于C++ 类似maven于java的存在,它可以用来对软件进行构建、测试以及打包等工作。我们在研究C++ 项目的时候,CMake是很好的构建工具,一定要熟悉掌握。
正如maven的配置文件主要是通过pom.xml一样,CMake的工作是通过CMakeLists.txt文件来描述的。所以掌握CMakeLists.txt文件的配置方法是必要的。
我们在IDE中,也可以像直接通过项目中的pom文件导入maven项目那样,通过项目中的CMakelists.txt文件导入CMake项目。
像以上这种设置命令有很多,我们可以参照《CMake官方文档:命令解释》来查阅相关命令的含义以及使用。
exchange cmake
file(GLOB ABI_FILES "*.abi")
add_wast_executable(TARGET exchange
INCLUDE_FOLDERS "${STANDARD_INCLUDE_FOLDERS}"
LIBRARIES libc++ libc eosiolib
DESTINATION_FOLDER ${CMAKE_CURRENT_BINARY_DIR}
)
configure_file("${ABI_FILES}" "${CMAKE_CURRENT_BINARY_DIR}" COPYONLY)
add_dependencies( exchange currency )
add_executable(test_exchange test_exchange.cpp )
#bfp/lib/pack.c bfp/lib/posit.cpp bfp/lib/util.c bfp/lib/op2.c)
target_link_libraries( test_exchange fc )
target_include_directories( test_exchange PUBLIC fixed_point/include )
我们在源码位置eos/CMakeModules中可以找到wasm.cmake文件,进去以后可以发现
macro(add_wast_executable)
自定义module也是以宏(命令集对外为单一命令)的形式(Excel中我之前写过宏脚本,也是同一个词macro)。这一段add_wast_executable内容很多,我就不粘贴了,主要内容是为了wasm环境构建代码,包括对打包内容的描述,对状态的判断处理等各种命令的集合,其中又包含了很多module宏。
abi文件是通过eosiocpp工具通过exchange.cpp生成的,具体可参照《EOS智能合约演练》。
从这里开始我们来详细分析exchange合约的源码内容,exchange.cpp需要引用exchange_accounts, exchange_state以及market_state这三个库。其中market_state又依赖两外两个库,因此我们先从比较独立的exchange_accounts入手。
exchange_accounts.hpp
#pragma once
#include <eosiolib/asset.hpp>
#include <eosiolib/multi_index.hpp>
namespace eosio {
using boost::container::flat_map;// 相当于java中的import,下面可以直接使用flat_map方法。
/**
* 每个用户都有他们自己的账户,并且这个账户是带有exchange合约的。这可以让他们保持跟踪一个用户是如何抵押各种扩展资产类型的。假定存储一个独立的flat_map,包含一个特定用户的所有余额,这个用户比起打破使用扩展标识来排序的多重索引表,将更加实际的
*/
struct exaccount {
account_name owner;// uint64_t类型,64位无符号整型
flat_map<extended_symbol, int64_t> balances;// extended_symbol是asset.hpp中定义的
uint64_t primary_key() const { return owner; }// 结构体包含一个primary_key方法是不可变的,const,实现也有了,直接是返回owner。
EOSLIB_SERIALIZE( exaccount, (owner)(balances) )// EOSLIB_SERIALIZE这是一种自定义的模板,是一种反射机制,可以给结构体赋值,第一个参数为结构体名字,后面的参数用括号分别括起来,传入当前两个成员变量。
};
typedef eosio::multi_index<N(exaccounts), exaccount> exaccounts;// multi_index是一个类,这行定义了一个变量exaccounts,它的类型是一种多重索引。
/**
* 提供一个抽象接口,为用户的余额排序。这个类缓存了数据表,以提供高效地多重访问。
*/
struct exchange_accounts {
exchange_accounts( account_name code ):_this_contract(code){}// 给私有成员赋值
void adjust_balance( account_name owner, extended_asset delta, const string& reason = string() );//调整余额,传入owner、扩展资产,reason。exchange\_accounts.cpp会实现该函数。
private:
account_name _this_contract;// 私有成员 \_this\_contract
/**
* 保留一个缓存,用来存储我们访问的所有用户表
*/
flat_map<account_name, exaccounts> exaccounts_cache;// flat_map类型的缓存exaccounts_cache,存储的是账户名和以上结构体exaccounts。
};
} /// namespace eosio
multi_index这里再简单介绍一下。它的模板定义是
template<uint64_t TableName, typename T, typename... Indices>
泛型中第一个参数是表名,第二个是多重索引。
N函数的源码:
/**
* @brief 用来生成一个编译时间,它是64位无符号整型。传入的参数X是一个base32编码的字符串的解释。
* @ingroup types
*/
#define N(X) ::eosio::string_to_name(#X)
可参考文章《EOS技术研究:合约与数据库交互》,下面来看一下exchange_accounts.cpp源码:
#include <exchange/exchange_accounts.hpp>
namespace eosio {
void exchange_accounts::adjust_balance( account_name owner, extended_asset delta, const string& reason ) {
(void)reason;// reason当做一个备注,不可修改的。
auto table = exaccounts_cache.find( owner );//通过account\_name查找到对应的exaccount结构体对象数据。
if( table == exaccounts_cache.end() ) {// 如果这个数据是最后一个,则将当前数据重新包装放入exaccounts_cache,同时将exaccounts_cache第一位的数据重新赋值给table
table = exaccounts_cache.emplace( owner, exaccounts(_this_contract, owner ) ).first;
}
auto useraccounts = table->second.find( owner );//table现在有值了,在table下一个位置查找owner
if( useraccounts == table->second.end() ) {// 如果这个用户是table下一个位置的结尾数据,则将owner重新组装数据放入table
table->second.emplace( owner, [&]( auto& exa ){
exa.owner = owner;
exa.balances[delta.get_extended_symbol()] = delta.amount;
eosio_assert( delta.amount >= 0, "overdrawn balance 1" );//断言,当extended_assert资产的数目小于0时,打印日志:透支余额1
});
} else {// 如果该用户不是table下一个位置的结尾数据,则修改以该用户为key的数据,
table->second.modify( useraccounts, 0, [&]( auto& exa ) {
const auto& b = exa.balances[delta.get_extended_symbol()] += delta.amount;// 扩展标识的余额加上extended_assert资产的数目为b
eosio_assert( b >= 0, "overdrawn balance 2" );// 断言,当b小于0时,打印日志:透支余额2
});
}
}
} /// namespace eosio
它实现了adjust_balance函数。这个函数主要实现了对账户数据的管理,余额的判断与处理。
exchange_state库的源码我就不张贴了,这里进行一个总结:
下面是connector的源码部分:
struct connector {
extended_asset balance;// 余额
uint32_t weight = 500;// 权重
margin_state peer_margin; /// peer_connector 抵押借贷余额,margin_state类型
EOSLIB_SERIALIZE( connector, (balance)(weight)(peer_margin) )还是那个初始化工具。
};
exchange_state库中最重要的函数就是上面这几个转换函数,掌握这些函数都能干哪些事,未来我们可以直接测试调用或者在其他源码中出现继续分析。
这是基于以上exchange_accounts以及exchange_state两个库的库,它的内容也很多,不适宜全部粘贴出来。
这是整个exchange合约的主库(通常我会将一个名字的头文件加源文件合并称为一个库,这也是C++ 的命名习惯)。
exchange.hpp
头文件,主要声明了一个类exchange,这里面包含了三个私有成员,以及七个公有函数,还有三个公有结构体,下面贴一下源码吧:
#include <eosiolib/types.hpp>
#include <eosiolib/currency.hpp>
#include <boost/container/flat_map.hpp>
#include <cmath>
#include <exchange/market_state.hpp>
namespace eosio {
/**
* 这个合约可以让用户在任意一对标准货币类型之间创建一个exchange,这个exchange是基于一个在购买方和发行方双边的价值等额的条件下而创建的。为了预防舍入误差,初始化金额应该包含大量的base以及quote货币的数量,并且exchange 共享应该在最大初始化金额的100倍的数量。用户在他们通过exchange交易前,必须先存入资金到exchange。每次一个exchange创建一个新的货币时,相应的交易市场制造商也会被创建。货币供应以及货币符号必须是唯一的并且它使用currency合约的表来管理。
*/
class exchange {
private:
account_name _this_contract;// 私有,账户名
currency _excurrencies;// 货币
exchange_accounts _accounts;// exchange的账户
public:
exchange( account_name self )
:_this_contract(self),
_excurrencies(self),
_accounts(self)
{}
// 创建
void createx( account_name creator,
asset initial_supply,
uint32_t fee,
extended_asset base_deposit,
extended_asset quote_deposit
);
// 订金
void deposit( account_name from, extended_asset quantity );
// 提现
void withdraw( account_name from, extended_asset quantity );
// 借出
void lend( account_name lender, symbol_type market, extended_asset quantity );
// 不借?
void unlend(
account_name lender,
symbol_type market,
double interest_shares,
extended_symbol interest_symbol
);
// 边缘覆盖结构体
struct covermargin {
account_name borrower;
symbol_type market;
extended_asset cover_amount;
};
// 上侧边缘
struct upmargin {
account_name borrower;
symbol_type market;
extended_asset delta_borrow;
extended_asset delta_collateral;
};
// 交易结构体
struct trade {
account_name seller;
symbol_type market;
extended_asset sell;
extended_asset min_receive;
uint32_t expire = 0;
uint8_t fill_or_kill = true;
};
// 函数名根据参数列表方法重载,在xxx上执行exchange
void on( const trade& t );
void on( const upmargin& b );
void on( const covermargin& b );
void on( const currency::transfer& t, account_name code );
// 应用
void apply( account_name contract, account_name act );
};
} // namespace eosio
该源文件中实现了以上头文件中定义的所有公有方法。
先定义两个标准货币base和quote,他们都是exchange_state类型:
exchange_state state;
state.supply = 100000000000ll;// 发行量
//state.base.weight = state.total_weight / 2.;
state.base.balance.amount = 100000000;
state.base.balance.symbol = "USD";
state.base.weight = .49;
//state.quote.weight = state.total_weight / 2.;
state.quote.balance.amount = state.base.balance.amount;
state.quote.balance.symbol = "BTC";
state.quote.weight = .51;
print_state( state );
插曲:ubuntu编译boost库
首先在boost官网下载最新库文件,目前我下载的版本是boost_1_67_0.tar.bz2。
然后,我们再打开CLion,CMake自动编译项目eos,会发现console中已经显式编译成功的字样。
接下来继续我们的测试。直接run 主函数,首先打印出来的是"USD"和"BTC"的发行信息,
-----------------------------
supply: 1e+11
base: 1e+08 USD
quote: 1e+08 BTC
-----------------------------
可以看到,这与代码中定义的总发行量以及包含的两种符号类型的token的各自发行量,都是准确的。
exchange_state是在测试类中我们自定义的数字资产类型,下面是它的结构:
struct exchange_state {
token_type supply;// 发行量
symbol_type symbol = exchange_symbol;// exchange符号
// 两个连接器base和quote
connector base;
connector quote;
// 交易
void transfer( account_name user, asset q ) {
output[balance_key{user,q.symbol}] += q.amount;
}
map<balance_key, token_type> output;
vector<margin> margins;
};
exchange_state数字资产中,包含一个总发行量,两个成员资产base和quote,他们是connector类型,这个类型也是自定义的(与上面介绍的源码稍有不同,稍后在测试完成以后会总结他们的区别),交易函数以及一个自定义集合output和margins,下面来看connector的定义:
struct connector {
asset balance; // asset资产类型
real_type weight = 0.5;
token_type total_lent; /// 发行商从用户的贷款
token_type total_borrowed; /// 发行商借给用户
token_type total_available_to_lend; /// 可借出的有效数量
token_type interest_pool; /// 利息池,是所获得的总利息,但不一定每个用户都可以申请使用
// 以下三个方法都在本文件下被实现了。
void borrow( exchange_state& ex, const asset& amount_to_borrow );
asset convert_to_exchange( exchange_state& ex, const asset& input );
asset convert_from_exchange( exchange_state& ex, const asset& input );
};
这个connector有一个余额,一个权重(可理解为占有exchange_state数字资产的比例),它的一些银行资产功能属性,贷款拆借利息等,以及connector本身作为资产可以与其他exchange_state数字资产进行转换,拆借等功能。余额成员是asset资产类型,这个类型也是一个自定义结构体:
struct asset {
token_type amount;
symbol_type symbol;
};
它具备一个总数量和符号两个成员。所以以上我们给exchange_state数字资产定义了两个connector,“BTC”和“USD”以及它们各自的发行量,正是采用这个asset的结构进行赋值的。
打印出state内容以后,显示的是两种token"USD"和"BTC"的发行信息,接下来,我们利用exchange中的一些函数功能进行两种token之间的转换及交易。
auto new_state = convert(state, "dan", asset{100, "USD"}, asset{0, "BTC"});
print_state(new_state);
看一下这里面的convert函数的声明:
/**
* 通过给出的一个当前state,计算出一个新的state返回。
*/
exchange_state convert( const exchange_state& current,// 当前state
account_name user,// 用户
asset input,// 输入资产
asset min_output,// 最小输出资产
asset* out = nullptr) {
所以我们来解读第一行convert代码的意思为:
一个名为“dan”的用户,现有资产状态为上面已打印的state,输入资产为100个USD,最小输出资产为0个BTC(注意输入资产和最小输出资产必须是不同的,否则无法转化)。
下面看输出print_\state结果:
-----------------------------
supply: 1e+11
base: 1e+08 USD
quote: 9.99999e+07 BTC
dan 96.0783 BTC
dan 0 EXC
dan -100 USD
结果解读:
重新解读这一行convert代码的意思为:
state数字资产(我们最早设置的),dan根据state资产的格式拿出来自己账户中的100个USD(dan本身没有USD,所以是欠款状态)作为抵押想exchange BTC,,而BTC是quote(base和quote也可以理解为用户)的符号,所以quote的数量少了相应的100个BTC。最后,要将这100个BTC打入dan的账户里面,而为什么编程了96.0783个而不是100个呢?
## 调试
上面我们将问题抛了出来,下面我们对代码进行debug,来分析这100个BTC在发放给用户的时候是如何转变的?我们打个断点,开始运行程序,走到convert函数中,由于我们的USD等于base的符号,所以执行到了convert_to_exchange函数。
asset connector::convert_to_exchange(exchange_state &ex, const asset &input) {
real_type R(ex.supply);// 1e+11
real_type S(balance.amount + input.amount); //100000100,等于state资产得新发行100个USD
real_type F(weight);//0.489999999999999991118,USD所占比重,state初始化时已经设置好
real_type T(input.amount);//100
real_type ONE(1.0);//1
auto E = R * (ONE - std::pow(ONE + T / S, F));// 根据这个算法得到对应的state资产的增发量的值。pow是cmath库的一个函数,有两个参数,返回结果为第一个参数为底,第二个参数为幂值的结果。
// (1e+11)*(1-(1+100/100000100)^+0.489999999999999991118),这得借助计算器了,算出结果为:-48999.9385,约等于程序执行结果-48999.938505084501827。
token_type issued = -E; //48999.9385,增发100个USD,实际上要增发state这么多。
ex.supply += issued;// 更新总发行量,加入以上计算的值。
balance.amount += input.amount;//state的USD connector(可理解为基于某稳值数字资产下的token)的余额可以增加100个USD了。
return asset{issued, exchange_symbol};// 最后是以EXC资产增发48999.9385个的形式返回。
}
EXC是state的“原值”符号,USD和BTC是基于EXC抵押资产发行的数字token。
继续调试,回到convert函数中。我们获得了要对应增发EXC的数量,那么要具体执行影响到state数字资产,是通过:
result.output[balance_key{user, final_output.symbol}] += final_output.amount;// 将增发EXC的数量添加至state的output集合中。
output存放形式:
结果就是EXC总账户通过dan增发了48999.9385,然后接下来继续,
result.output[balance_key{user, input.symbol}] -= input.amount;
这是给dan账户进行减持,同样的,我们列出output的存放形式:
结果就是dan个人账户欠了100个USD,dan在调用convert的时候,要求最小输出资产是BTC类型的,而现在针对输入资产类型USD以及EXC相应的操作已经做完。下面要做的是EXC和BTC的convert。
if (min_output.symbol != final_output.symbol) {// 当计算的最终输出资产的符号与传入的最小输出资产不一致时,要调用本身convert来转换。
return convert(result, user, final_output, min_output, out);
}
携带新的参数EXC和BTC再次进入convert函数时,state数字资产已经发生了变化,它的总发行量变为100000048999.93851,base的USD的余额变为100000100,quote的BTC的余额不变,仍旧为1亿。我们新带过来的参数是:
由于我们这一次处理的输入资产类型就是state的默认符号EXC,所以会走另外一个处理分支,根据最小输出资产类型会执行convert_from_exchange函数:
initial_output = result.quote.convert_from_exchange(result, initial_output);
convert_from_exchange函数源码:
asset connector::convert_from_exchange(exchange_state &ex, const asset &input) {
real_type R(ex.supply - input.amount);// 先找回原值:1e+11
real_type S(balance.amount);// BTC余额不变,仍为1亿个1e+8
real_type F(weight);// 权重为0.51
real_type E(input.amount);// EXC的输入数量48999.93851
real_type ONE(1.0);
real_type T = S * (std::pow(ONE + E / R, ONE / F) - ONE);// 1e+8*((1+48999.93851/1e+11)^(1/0.51)-1),通过科学计算器了,算出结果为:96.07833342,约等于程序执行结果96.0783334103356735645。
// 这是通过抵押资产EXC的增发量来反推对应的BTC的增发量。
auto out = T;
ex.supply -= input.amount;// 将EXC增发的部分减掉,其实是维持了原有增发量1e+11不变。
balance.amount -= token_type(out);// BTC的总量减少了96.07833342(这部分发给dan了),变为99999903.921666592。
return asset{token_type(out), balance.symbol};//最终以BTC减掉(发放出去)96.07833342的形式返回。
}
它的函数体与上面的convert_to_exchange函数很相似,但细一看会发现里面的某些数值运算发生了变化。然后,我们继续回到二重convert函数中,BTC发给dan的部分(实际上从dan的角度上来讲,可以是BTC增发)具体执行为:
result.output[balance_key{user, final_output.symbol}] += final_output.amount;// 将发给dan的96.07833342加到dan的账户里。
结果就是dan账户中多了96.07833342个BTC。然后对作为输入资产的EXC进行处理:
result.output[balance_key{user, input.symbol}] -= input.amount;
结果就是EXC总账户通过dan减持掉48999.9385。此时,由于上面的convert_from_exchange函数返回的是BTC的资产,与原始最小输出资产类型相同,所以不必要再次进入一个convert嵌套。直接返回state,包含以上四个加粗信息,这里再重新列出来:
1和4互相抵消,等于state的总发行量不变,仍旧为原始的1e+11。所以state中会新增账户dan的信息,它的USD和BTC以及EXC(中间涉及到了中转交易,EXC相当于一个中间价值锚定,用来建立两种token交易的通道)。最终达到了与程序输出相等的结果:
-----------------------------
supply: 1e+11
base: 1e+08 USD
quote: 9.99999e+07 BTC
dan 96.0783 BTC
dan 0 EXC
dan -100 USD
-----------------------------
我们通过一个简单的测试完成了对exchange合约的学习,exchange合约教会我们可以通过EOS建立自己的生态模型,通证模型,我们可以锚定抵押资产,发行token,通过权重的形式发行多个token等等,非常灵活,这与本篇文章前半部分所描述的那种价值稳定的数字货币的设计是吻合的。在测试程序中,我们简单实现了exchange源码中的convert函数,各种自定义结构体,例如connector,exchange_state等等,基本上所有测试文件中的函数与结构都可以在exchange源码中找到。我们在上面源码分析的过程中还比较混沌,但通过测试文件的梳理,再回头去看上面的源码分析,会有新的体会。源码中各种结构以及函数是更加精密与强壮的,但是测试文件和exchange源码相同的是:他们的通证模型是相同的。我们通过测试和源码更加充分理解了EOS的灵活的通证模型。有任何问题,欢迎来讨论。
圆方圆学院汇集大批区块链名师,打造精品的区块链技术课程。 在各大平台都长期有优质免费公开课,欢迎报名收看。
公开课地址:https://ke.qq.com/course/345101
标签:EOS智能合约 等于 1.5 include 内容 文件名 log 是什么 函数名
原文地址:https://www.cnblogs.com/yuanfangyuan/p/10119028.html