首发于范浩波科学院假设,你是国内某电商平台商品中心项目负责人。今天突然接到这样一个需求:在人民币原价的基础上,商品必须支持卢比(印度)的价格。需求点可以描述为:购买用户需要支持以卢比为单位的商品价格;操作人员,商品管理系统仍使用人民币价格;对于同一个需求,设置了以下两个硬指标:需求必须实现;它必须快速启动;问题是首先,我们必须承认,这确实是一个简单的需求,但同时也是一个作弊需求。遇到的主要问题有:商品价格相关制度较多;各上位系统调用的商品价格接口较多;与商品价格相关的字段很多;为了实现快速上线,我们只能进行少量且合适的改造。所以最终我们的改造方向是:尽量只改造商品价源系统,即商品中心,尽量不改造其他上层系统。改造商品中心的可行性研究,商品价格支持卢比。可行的改造方案有2种:1.将Rupee保存在数据表的price字段中。在与原人民币价格相关的数据表字段中保存卢比值,并在数据表中添加一个人民币字段。2.接口输出数据时,转换成卢比。与原名称货币相关的数据表字段仍然存储人民币值。接口输出数据时,将价格相关字段的值换算成卢比。对于上述方案,我们需要注意两个问题:汇率每天都会变化,所以产品价格也会变化;后续产品价格可能需要支持多种货币;以上方案①,产品中心只需要修改数据表即可。然后每天按照汇率刷新商品价格,原来的价格字段变成了卢比。该方案相对简单易操作,但缺点是仍需改革人民币价格的制度,即商品管理制度。对于方案②,需要改造商品中心的业务逻辑。由于涉及的价格字段较多,转换较为复杂。主要优点是:汇率变动对商品价格影响小,可扩展支持多币种价格(可根据区域标识获取对应的商品价格)。解决方案最终,为了系统的可扩展性,我们选择了解决方案②。这里主要改造商品中心,主要解决透明传输区域识别和支持多币种价格两个问题。透传区识别我们的业务系统主要分为API和Service项目,API暴露HTTP接口,API和Service以及Service和Service之前使用RPC接口进行通信。由于商品中心与价格相关的接口较多,不可能在每个接口都添加区域标识的参数。所以我们创建了一种机制,通过调用链接透传区域标识。该机制的原理是先将区域标识放在全局上下文中,API接口通过HeaderX-Location携带区域标识;对于RPC接口,我们的RPC框架已经支持Context,不需要修改。代码实现传递全局上下文。由于RPC框架已经支持Context,所以API和RPC接口之间全局上下文的透传略有不同。实现如下:classLocation{publicstaticfunctioninit(){global$context;如果(空($context['location'])){返回;}//这里API直接获取X-Location头if(!empty($_SERVER['HTTP_X_LOCATION'])){$context['location']=$_SERVER['HTTP_X_LOCATION'];}//RPCServer会自动获取Context}}上面的init()方法需要在项目入口位置进行初始化。其中,RPC接口不需要操作全局上下文。因为RPCClient在调用时会自动获取全局变量$context值并将Context追加到RPC协议数据中,而RPCServer在接收到RPC协议数据时会自动获取RPC协议数据中的Context值并设置全局变量$context要求。RPCClient传递Context的实现如下:protectedfunctionaddGlobalContext($data){global$context;$context=!is_array($context)?数组():$上下文;//data是要请求的RPC协议数据$data['Context']=$context;return$data;}RPCServer获取Context如下:publicfunctiongetGlobalContext($packet){global$context;$context=array();//packet是接收到的RPC协议数据if(isset($packet['Context'])){$context=$packet['Context'];}}设置Context时,RPC通信时协议数据会携带location字段,内容如下:RPC325{"data":"{\"version\":\"1.0\",\"user\":\"xxx\",\"密码\":\"xxx\",\"时间戳\":1553225486.5455,\"类\":\"xxx\",\"方法\":\"xxx\",\"params\":[1]}","signature":"xxx","Context":{"location":"india"}}这里设置区域标识,我们只需要设置区域标识在全球范围内。一旦我们设置了地域标识,所有业务系统都会在这个调用链路中透传地域标识。实现如下:classLocation{publicstaticfunctionset($location){global$context;$context['location']=$location;//API需要在这里单独设置X-Locationheaderheader('X-Location:'.$context['location']);}}获取区域ID设置区域ID后,可以在本次调用链接的所有业务系统中直接获取。实现如下:classLocation{publicstaticfunctionget(){global$context;如果(!isset($context['location'])){返回'中国';}返回$context['location'];}}支持多币种价格商品中心有地区标识后,商品中心服务可以根据地区标识转换价格字段。因为为价格设计的数据表和价格字段很多,这里我们直接从数据层(Model)进行改造。数据获取方法的改造下面的ReadBase类是所有数据表Models的基类,所有获取数据表数据的方法都是从getOne()和getAll()方法继承或调用的,所以我们只需要修改这两个方法。类ReadBase{publicfunctiongetOne(array$cond,$fields){$data=$this->getReader()->select($this->getFields($fields))->from($this->getTableName())->where($cond)->queryRow();返回$this->getExchangePrice($data);}publicfunctiongetAll(array$cond,$fields){$data=$this->getReader()->select($this->getFields($fields))->from($this->getTableName())->where($cond)->queryAll();if($data){foreach($dataas&$one){$this->getExchangePrice($one);}}返回$数据;}}后缀匹配价格字段由于价格字段涉及的名称较多,存在不确定性,所以这里采用后缀的方式进行匹配。为了防止某些字段的命名不规范,这里引入黑名单机制。受保护的函数isExchangeField($field){$priceSuffix=array('cost','_price');$黑色=数组();$len=strlen($field);foreach($priceSuffixas$suffix){$lastPos=$len-strlen($suffix);//未列入黑名单且不是is_if(!in_array($field,$black)&&false===strpos($field,'is_')&&$lastPos===strpos($field,$suffix)){returntrue;}}returnfalse;}is_前缀的字段一般定义为标识字段,默认为非价格字段。计算地区价格上述getExchangePrice()方法用于根据地区标识转换价格覆盖原有价格字段,并自增_origin后缀的人民币价格字段。publicfunctiongetExchangePrice(&$data){if(empty($data)){return$data;}$originPrice=array();foreach($dataas$field=>&$value){//是否为价格字段if($this->isExchangeField($field)){$originField=$field.'_起源';$originPrice[$originField]=$value;//获取对应区域的价格$value=$this->getExchangePrice($value);}}$data=array_merge($originPrice,$data);return$data;}publicstaticfunctiongetExchangePrice($price){//获取区域ID$location=Location::get();//汇率$exchangeRateConfig=\Config::$exchangeRate;if($location==='china'){return$price;}elseif(isset($exchangeRateConfig[$location])){$exchangeRate=$exchangeRateConfig[$location];}else{thrownew\BusinessException("未找到$location汇率");}//四舍五入并保留两位小数$exchangePrice=bcmul($price,$exchangeR吃了,3);returnnumber_format(ceil($exchangePrice*100)/100,2,'.','');}其中,getExchangePrice()方法会调用Location::get()获取区域ID,并根据exchangerate计算实时价格最后商品中心改造后,得到的部分商品价格信息如下:#人民币价格10,汇率10.87market_price:108.7market_price_origin:10APIsystem对于所有API项目,我们只需要让客户端在所有的请求中加上X-Location头即可。GET/product/detail/1HTTP/1.1RequestHeadersX-Location:indiaAPI项目需要在入口文件处初始化区域标识符。如下:Location::init();商品管理系统商品管理系统,为方便操作,所有商品价格均以人民币为单位。因此,我们只需要初始化区域为中国,如下:Location::init();//区域设置为中国Location::set('china');总结实现需求很容易,但一定要合理、快速但并不简单。本文的实现方案避免了很多坑,但同时也可能会埋下一些坑。没有放之四海而皆准的方案,慢慢优化吧!
