资讯专栏INFORMATION COLUMN

商品价格的多币种方案

wpw / 2250人阅读

摘要:主要遇到的问题如下涉及商品价格的系统众多各上层系统调用商品价格接口繁多商品价格相关字段较多为了实现快速上线,我们在原人民币的商品价格基础架构上,只能进行少量且合适的改造。可行性调研改造商品中心,商品价格支持卢比。

</>复制代码

  1. 首发于 樊浩柏科学院

假若,你是某个国内电商平台的商品中心项目负责人。突然今天,接到了一个这样的需求:商品在原人民币价格的基础架构上,须支持卢比(印度)价格。

需求

需求点,可以描述为:

购买的用户,商品价格需要支持卢比;

营运人员,商品管理系统依然使用人民币价格;

同样这个需求,定了以下两个硬指标:

必须实现需求;

必须快速上线;

问题

首先,我们必须承认的是,这确实是个简单的需求,但这也是个够坑爹的需求。主要遇到的问题如下:

涉及商品价格的系统众多;

各上层系统调用商品价格接口繁多;

商品价格相关字段较多;

为了实现快速上线,我们在原人民币的商品价格基础架构上,只能进行少量且合适的改造。所以,最后我们的改造方向为:尽量只改造商品价格源头系统,即商品中心,其他上层系统尽量不改动。

可行性调研

改造商品中心,商品价格支持卢比。可行的改造方案有 2 种:

1、数据表价格字段存卢比

将原人名币价格相关的数据表字段,存卢比值,数据表并新增人名币字段。

2、接口输出数据时转化为卢比

原人名币相关的数据表字段依然存人民币值,在接口输出数据时,将价格相关字段值转化为卢比。

针对以上方案,我们需要注意 2 个问题:

汇率会每天变化,所以商品价格也会变化;

后续商品价格,可能须支持多币种;

上述 方案 ①,商品中心只需改造数据表。然后每天根据汇率刷新商品价格,原价格字段就都变成了卢比。方案相对简单,也容易操作,但缺点是:对任然需要人民币价格的系统,即商品管理系统须改造。
方案 ②,需要改造商品中心业务逻辑。由于涉及的价格字段较多,改造较复杂,主要优点是:汇率变动对商品价格影响较小,且可拓展支持多币种价格(可以根据地区标识,获取相应的商品价格)。

解决方案

最终,为了系统的可扩展性,我们选择了方案 ②。

这里主要改造了商品中心,主要解决 透传地区标识 和 支持多币种价格 这 2 个问题。

透传地区标识

我们的业务系统主要分为 API 和 Service 项目,API 暴露出 HTTP 接口,API 与 Service 和 Service 与 Service 之前使用 RPC 接口通信。由于商品中心涉及到价格的接口繁多,不可能对每个接口都增加地区标识的参数。所以我们弄了一套调用链路透传地区标识的机制。

机制原理

思路就是,先将地区标识放在全局上下文中,API 接口通过 Header 头X-Location携带地区标识;而对于 RPC 接口,我们的 RPC 框架已支持了 Context,不需要改造。

代码实现
传递全局上下文

由于 RPC 框架已支持了 Context,所以 API 和 RPC 接口透传全局上下文略有不同。实现如下:

</>复制代码

  1. class Location
  2. {
  3. public static function init()
  4. {
  5. global $context;
  6. if (empty($context["location"])) {
  7. return;
  8. }
  9. // API在这里直接获取X-Location头
  10. if (!empty($_SERVER["HTTP_X_LOCATION"])) {
  11. $context["location"] = $_SERVER["HTTP_X_LOCATION"];
  12. }
  13. // RPC Server会自动获取Context
  14. }
  15. }

</>复制代码

  1. 上述init()方法,需要在项目入口位置初始化。

其中,RPC 接口不需要操作全局上下文。因为 RPC Client 在调用时会自动获取全局变量$context值并在 RPC 协议数据中追加 Context,同时 RPC Server 在收到请求时会自动获取 RPC 协议数据中的 Context 值并设置全局变量$context

RPC Client 传递 Context 实现如下:

</>复制代码

  1. protected function addGlobalContext($data)
  2. {
  3. global $context;
  4. $context = !is_array($context) ? array() : $context;
  5. // data为待请求的RPC协议数据
  6. $data["Context"] = $context;
  7. return $data;
  8. }

RPC Server 获取 Context 实现如下:

</>复制代码

  1. public function getGlobalContext($packet)
  2. {
  3. global $context;
  4. $context = array();
  5. // packet为接收的RPC协议数据
  6. if(isset($packet["Context"])) {
  7. $context = $packet["Context"];
  8. }
  9. }

当设置了 Context 后,RPC 通信时协议数据会携带location字段,内容如下:

</>复制代码

  1. RPC
  2. 325
  3. {"data":"{"version":"1.0","user":"xxx","password":"xxx","timestamp":1553225486.5455,"class":"xxx","method":"xxx","params":[1]}","signature":"xxx","Context":{"location":"india"}}
设置地区标识

到这里,我们只需要在全局上下文设置地区标识即可。一旦我们设置了地区标识,所有业务系统就会在本次的调用链路中透传这个地区标识。实现如下:

</>复制代码

  1. class Location
  2. {
  3. public static function set($location)
  4. {
  5. global $context;
  6. $context["location"] = $location;
  7. // API需要在这里多带带设置X-Location头
  8. header("X-Location: " . $context["location"]);
  9. }
  10. }
获取地区标识

设置了地区标识后,就可以在本次调用链路的所有业务系统中直接获取。实现如下:

</>复制代码

  1. class Location
  2. {
  3. public static function get()
  4. {
  5. global $context;
  6. if (!isset($context["location"])) {
  7. return "china";
  8. }
  9. return $context["location"];
  10. }
  11. }
支持多币种价格 商品中心

有了地区标识后,商品中心服务就可以根据地区标识对价格字段进行转化了。因为设计到价格的数据表和价格字段较多,这里直接从数据层(Model)进行改造。

改造获取数据方法

下述的ReadBase类是所有数据表 Model 的基类,所有获取数据表数据的方法都继承或调用自getOne()getAll()方法,所以我们只需要改造这两个方法。

</>复制代码

  1. class ReadBase
  2. {
  3. public function getOne(array $cond, $fields)
  4. {
  5. $data = $this->getReader()->select($this->getFields($fields))->from($this->getTableName())->where($cond)->queryRow();
  6. return $this->getExchangePrice($data);
  7. }
  8. public function getAll(array $cond, $fields)
  9. {
  10. $data = $this->getReader()->select($this->getFields($fields))->from($this->getTableName())->where($cond)->queryAll();
  11. if ($data) {
  12. foreach ($data as &$one) {
  13. $this->getExchangePrice($one);
  14. }
  15. }
  16. return $data;
  17. }
  18. }
后缀匹配价格字段

由于涉及到价格字段名字较多,且具有不确定性,所以这里使用后缀方式匹配。为了防止一些字段命名不规范,这里引入了黑名单机制。

</>复制代码

  1. protected function isExchangeField($field)
  2. {
  3. $priceSuffix = array("cost", "_price");
  4. $black = array();
  5. $len = strlen($field) ;
  6. foreach ($priceSuffix as $suffix) {
  7. $lastPos = $len - strlen($suffix);
  8. // 非黑名单且非is_
  9. if (!in_array($field, $black)
  10. && false === strpos($field, "is_")
  11. && $lastPos === strpos($field, $suffix)
  12. ) {
  13. return true;
  14. }
  15. }
  16. return false;
  17. }

</>复制代码

  1. 前缀为is_的字段一般定义为标识字段,默认为非价格字段。
计算地区价格

上述getExchangePrice()方法,用来根据地区标识转化价格覆盖到原价格字段,并自增以_origin后缀的人民币价格字段。

</>复制代码

  1. public function getExchangePrice(&$data)
  2. {
  3. if (empty($data)) {
  4. return $data;
  5. }
  6. $originPrice = array();
  7. foreach ($data as $field => &$value) {
  8. // 是否是价格字段
  9. if ($this->isExchangeField($field)) {
  10. $originField = $field . "_origin";
  11. $originPrice[$originField] = $value;
  12. // 获取对应地区的价格
  13. $value = $this->getExchangePrice($value);
  14. }
  15. }
  16. $data = array_merge($originPrice, $data);
  17. return $data;
  18. }
  19. public static function getExchangePrice($price)
  20. {
  21. // 获取地区标识
  22. $location = Location::get();
  23. // 汇率
  24. $exchangeRateConfig = Config::$exchangeRate;
  25. if ($location === "china") {
  26. return $price;
  27. } else if (isset($exchangeRateConfig[$location])) {
  28. $exchangeRate = $exchangeRateConfig[$location];
  29. } else {
  30. throw new BusinessException("not found $location exchange rate");
  31. }
  32. // 向上取值并保留两位小数
  33. $exchangePrice = bcmul($price, $exchangeRate, 3);
  34. return number_format(ceil($exchangePrice * 100) / 100, 2, ".", "");
  35. }

其中,getExchangePrice()方法会调用Location::get()获取地区标识,并根据汇率计算实时价格。

最终,商品中心改造后,得到的部分商品价格信息,如下:

</>复制代码

  1. # 人民币价格10,汇率10.87
  2. market_price: 108.7
  3. market_price_origin: 10
API系统

对于所有 API 的项目,我们只需要让客户端在所有的请求中增加X-Location头即可。

</>复制代码

  1. GET /product/detail/1 HTTP/1.1
  2. Request Headers
  3. X-Location: india

API 项目需在入口文件处,初始化地区标识。如下:

</>复制代码

  1. Location::init();
商品管理系统

对于商品管理系统,我们为了方便运营操作,所有商品价格都应以人民币。因此,我们只需要初始化地区标识为中国,如下:

</>复制代码

  1. Location::init();
  2. // 地区设置为中国
  3. Location::set("china");
总结

为了实现需求很容易,但是要做到合理且快速却不简单。本文的实现的方案,避免了很多坑,但同时也可能又埋下了一些坑。没有一套方案是万能的,慢慢去优化吧!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/11987.html

相关文章

  • 商品价格的多币种方案

    摘要:主要遇到的问题如下涉及商品价格的系统众多各上层系统调用商品价格接口繁多商品价格相关字段较多为了实现快速上线,我们在原人民币的商品价格基础架构上,只能进行少量且合适的改造。可行性调研改造商品中心,商品价格支持卢比。 首发于 樊浩柏科学院 假若,你是某个国内电商平台的商品中心项目负责人。突然今天,接到了一个这样的需求:商品在原人民币价格的基础架构上,须支持卢比(印度)价格。 showIm...

    Chao 评论0 收藏0
  • SegmentFault 在杭成功举办黑客马拉松,发布区块链创新基金

    摘要:上周,在杭州欧美金融城创投中心启动全球黑客马拉松,本次活动吸引了众多长三角开发者的关注,有多位嘉宾出席活动进行项目交流讨论,现场更有币圈大佬现场撒币。 showImg(https://segmentfault.com/img/bVbbQCS); 上周,SegmentFault 在杭州欧美金融城 G5 创投中心启动全球黑客马拉松,本次活动吸引了众多长三角开发者的关注,有多位嘉宾出席活动进...

    Lorry_Lu 评论0 收藏0
  • koa2开发微信公众号: 不定期推送最新币圈消息

    摘要:背景比特币说好的分叉最后却分叉不成,如今算力又不够,于是比特现金想篡位没一个星期就涨了快倍,错过这趟快车甚是后悔,于是打算写一个可不定期推送最新消息的微信公众号。既然是利用微信这个平台载体,当然要熟悉微信的,遂封装了一下。 背景:比特币说好的segwit2x分叉最后却分叉不成,如今算力又不够,于是比特现金想篡位? 没一个星期就涨了快10倍,错过这趟快车甚是后悔,于是打算写一个可不定期推...

    xi4oh4o 评论0 收藏0
  • 小牛市启示录:有价值何必等风来

    摘要:很多人将这一波的上涨解读为比特币小牛市的到来,无论从技术层面还是从消息层面来看,比特币都有逐步回暖的迹象。到日,关于英雄链网络诈骗案被破获的报道便铺天盖地地传播开来。 摘要:不在风口上,长了翅膀的项目同样可以起飞,价值终究会超越时间。 showImg(https://segmentfault.com/img/bVbrS0N?w=4096&h=3575); 自四月初以来,比特币就开启了起...

    xiaoqibTn 评论0 收藏0

发表评论

0条评论

wpw

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<