首發於 樊浩柏科學院
倘若,你是某個國內電商平臺的商品中心項目負責人。忽然今天,接到了一個這樣的需求:商品在原人民幣價格的基礎架構上,須支持盧比(印度)價格。html
需求點,能夠描述爲:架構
一樣這個需求,定了如下兩個硬指標:框架
首先,咱們必須認可的是,這確實是個簡單的需求,但這也是個夠坑爹的需求。主要遇到的問題以下:優化
爲了實現快速上線,咱們在原人民幣的商品價格基礎架構上,只能進行少許且合適的改造。因此,最後咱們的改造方向爲:儘可能只改造商品價格源頭系統,即商品中心,其餘上層系統儘可能不改動。this
改造商品中心,商品價格支持盧比。可行的改造方案有 2 種:spa
一、數據表價格字段存盧比設計
將原人名幣價格相關的數據表字段,存盧比值,數據表並新增人名幣字段。code
二、接口輸出數據時轉化爲盧比orm
原人名幣相關的數據表字段依然存人民幣值,在接口輸出數據時,將價格相關字段值轉化爲盧比。htm
針對以上方案,咱們須要注意 2 個問題:
上述 方案 ①,商品中心只需改造數據表。而後天天根據匯率刷新商品價格,原價格字段就都變成了盧比。方案相對簡單,也容易操做,但缺點是:對任然須要人民幣價格的系統,即商品管理系統須改造。
方案 ②,須要改造商品中心業務邏輯。因爲涉及的價格字段較多,改造較複雜,主要優勢是:匯率變更對商品價格影響較小,且可拓展支持多幣種價格(能夠根據地區標識,獲取相應的商品價格)。
最終,爲了系統的可擴展性,咱們選擇了方案 ②。
這裏主要改造了商品中心,主要解決 透傳地區標識 和 支持多幣種價格 這 2 個問題。
咱們的業務系統主要分爲 API 和 Service 項目,API 暴露出 HTTP 接口,API 與 Service 和 Service 與 Service 以前使用 RPC 接口通訊。因爲商品中心涉及到價格的接口繁多,不可能對每一個接口都增長地區標識的參數。因此咱們弄了一套調用鏈路透傳地區標識的機制。
思路就是,先將地區標識放在全局上下文中,API 接口經過 Header 頭X-Location
攜帶地區標識;而對於 RPC 接口,咱們的 RPC 框架已支持了 Context,不須要改造。
因爲 RPC 框架已支持了 Context,因此 API 和 RPC 接口透傳全局上下文略有不一樣。實現以下:
class Location { public static function init() { global $context; if (empty($context['location'])) { return; } // API在這裏直接獲取X-Location頭 if (!empty($_SERVER['HTTP_X_LOCATION'])) { $context['location'] = $_SERVER['HTTP_X_LOCATION']; } // RPC Server會自動獲取Context } }
上述
init()
方法,須要在項目入口位置初始化。
其中,RPC 接口不須要操做全局上下文。由於 RPC Client 在調用時會自動獲取全局變量$context
值並在 RPC 協議數據中追加 Context,同時 RPC Server 在收到請求時會自動獲取 RPC 協議數據中的 Context 值並設置全局變量$context
。
RPC Client 傳遞 Context 實現以下:
protected function addGlobalContext($data) { global $context; $context = !is_array($context) ? array() : $context; // data爲待請求的RPC協議數據 $data['Context'] = $context; return $data; }
RPC Server 獲取 Context 實現以下:
public function getGlobalContext($packet) { global $context; $context = array(); // packet爲接收的RPC協議數據 if(isset($packet['Context'])) { $context = $packet['Context']; } }
當設置了 Context 後,RPC 通訊時協議數據會攜帶location
字段,內容以下:
RPC 325 {"data":"{\"version\":\"1.0\",\"user\":\"xxx\",\"password\":\"xxx\",\"timestamp\":1553225486.5455,\"class\":\"xxx\",\"method\":\"xxx\",\"params\":[1]}","signature":"xxx","Context":{"location":"india"}}
到這裏,咱們只須要在全局上下文設置地區標識便可。一旦咱們設置了地區標識,全部業務系統就會在本次的調用鏈路中透傳這個地區標識。實現以下:
class Location { public static function set($location) { global $context; $context['location'] = $location; // API須要在這裏單獨設置X-Location頭 header('X-Location: ' . $context['location']); } }
設置了地區標識後,就能夠在本次調用鏈路的全部業務系統中直接獲取。實現以下:
class Location { public static function get() { global $context; if (!isset($context['location'])) { return 'china'; } return $context['location']; } }
有了地區標識後,商品中心服務就能夠根據地區標識對價格字段進行轉化了。由於設計到價格的數據表和價格字段較多,這裏直接從數據層(Model)進行改造。
下述的ReadBase
類是全部數據表 Model 的基類,全部獲取數據表數據的方法都繼承或調用自getOne()
和getAll()
方法,因此咱們只須要改造這兩個方法。
class ReadBase { public function getOne(array $cond, $fields) { $data = $this->getReader()->select($this->getFields($fields))->from($this->getTableName())->where($cond)->queryRow(); return $this->getExchangePrice($data); } public function getAll(array $cond, $fields) { $data = $this->getReader()->select($this->getFields($fields))->from($this->getTableName())->where($cond)->queryAll(); if ($data) { foreach ($data as &$one) { $this->getExchangePrice($one); } } return $data; } }
因爲涉及到價格字段名字較多,且具備不肯定性,因此這裏使用後綴方式匹配。爲了防止一些字段命名不規範,這裏引入了黑名單機制。
protected function isExchangeField($field) { $priceSuffix = array('cost', '_price'); $black = array(); $len = strlen($field) ; foreach ($priceSuffix as $suffix) { $lastPos = $len - strlen($suffix); // 非黑名單且非is_ if (!in_array($field, $black) && false === strpos($field, 'is_') && $lastPos === strpos($field, $suffix) ) { return true; } } return false; }
前綴爲
is_
的字段通常定義爲標識字段,默認爲非價格字段。
上述getExchangePrice()
方法,用來根據地區標識轉化價格覆蓋到原價格字段,並自增以_origin
後綴的人民幣價格字段。
public function getExchangePrice(&$data) { if (empty($data)) { return $data; } $originPrice = array(); foreach ($data as $field => &$value) { // 是不是價格字段 if ($this->isExchangeField($field)) { $originField = $field . '_origin'; $originPrice[$originField] = $value; // 獲取對應地區的價格 $value = $this->getExchangePrice($value); } } $data = array_merge($originPrice, $data); return $data; } public static function getExchangePrice($price) { // 獲取地區標識 $location = Location::get(); // 匯率 $exchangeRateConfig = \Config::$exchangeRate; if ($location === 'china') { return $price; } else if (isset($exchangeRateConfig[$location])) { $exchangeRate = $exchangeRateConfig[$location]; } else { throw new \BusinessException("not found $location exchange rate"); } // 向上取值並保留兩位小數 $exchangePrice = bcmul($price, $exchangeRate, 3); return number_format(ceil($exchangePrice * 100) / 100, 2, '.', ''); }
其中,getExchangePrice()
方法會調用Location::get()
獲取地區標識,並根據匯率計算實時價格。
最終,商品中心改造後,獲得的部分商品價格信息,以下:
# 人民幣價格10,匯率10.87 market_price: 108.7 market_price_origin: 10
對於全部 API 的項目,咱們只須要讓客戶端在全部的請求中增長X-Location
頭便可。
GET /product/detail/1 HTTP/1.1 Request Headers X-Location: india
API 項目需在入口文件處,初始化地區標識。以下:
Location::init();
對於商品管理系統,咱們爲了方便運營操做,全部商品價格都應以人民幣。所以,咱們只須要初始化地區標識爲中國,以下:
Location::init(); // 地區設置爲中國 Location::set('china');
爲了實現需求很容易,可是要作到合理且快速卻不簡單。本文的實現的方案,避免了不少坑,但同時也可能又埋下了一些坑。沒有一套方案是萬能的,慢慢去優化吧!