Shipping.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <?php
  2. /*
  3. * FecShop file.
  4. *
  5. * @link http://www.fecshop.com/
  6. * @copyright Copyright (c) 2016 FecShop Software LLC
  7. * @license http://www.fecshop.com/license/
  8. */
  9. namespace fecshop\services;
  10. use Yii;
  11. use yii\base\InvalidConfigException;
  12. /**
  13. * Shipping services.
  14. * @author Terry Zhao <2358269014@qq.com>
  15. * @since 1.0
  16. */
  17. class Shipping extends Service
  18. {
  19. public $shippingConfig;
  20. public $shippingCsvDir;
  21. // 存放运费csv表格的文件路径。
  22. // 体积重系数,新体积重计算 = 长(cm) * 宽(cm) * 高(cm) / 体积重系数 , 因此一立方的体积的商品,体积重为200Kg
  23. public $volumeWeightCoefficient = 5000;
  24. // 在init函数初始化,将shipping method的配置值加载到这个变量
  25. protected $_shipping_methods ;
  26. // 是否缓存shipping method 配置数据(因为csv部分需要读取csv文件,稍微耗时一些,可以选择放到缓存里面)
  27. protected $_cache_shipping_methods_config = 0;
  28. // 可用的shipping method,计算出来的值保存到这个类变量中。
  29. protected $_available_shipping;
  30. // 缓存key
  31. const CACHE_SHIPPING_METHODS_CONFIG = 'cache_shipping_methods_config';
  32. /**
  33. * 1.从配置中取出来所有的shipping method
  34. * 2.对于公式为csv的shipping method,将对应的csv文件中的配置信息取出来
  35. * 3.如果开启了数据缓存,那么直接从缓存中读取。
  36. * 最后合并成一个整体的配置文件。赋值与-> $this->_shipping_methods
  37. */
  38. public function init()
  39. {
  40. parent::init();
  41. if (!$this->_shipping_methods) {
  42. // 是否开启缓存,如果开启,则从缓存中直接读取
  43. if ($this->_cache_shipping_methods_config) {
  44. $cache = Yii::$app->cache->get(self::CACHE_SHIPPING_METHODS_CONFIG);
  45. if (is_array($cache) && !empty($cache)) {
  46. $this->_shipping_methods = $cache;
  47. }
  48. }
  49. // 如果无法从缓存中读取
  50. if (!$this->_shipping_methods) {
  51. $allmethod = $this->shippingConfig;
  52. $this->_shipping_methods = [];
  53. // 从配置中读取shipping method的配置信息
  54. if (is_array($allmethod) && !empty($allmethod)) {
  55. foreach ($allmethod as $s_method => $v) {
  56. $formula = $v['formula'];
  57. if ($formula == 'csv') {
  58. $csv_content = $this->getShippingByTableCsv($s_method);
  59. if ($csv_content) {
  60. $this->_shipping_methods[$s_method] = $v;
  61. $this->_shipping_methods[$s_method]['csv_content'] = $csv_content;
  62. }
  63. } else {
  64. $this->_shipping_methods[$s_method] = $v;
  65. }
  66. }
  67. }
  68. if ($this->_cache_shipping_methods_config) {
  69. Yii::$app->cache->set(self::CACHE_SHIPPING_METHODS_CONFIG, $this->_shipping_methods);
  70. }
  71. }
  72. }
  73. return $this->_shipping_methods;
  74. }
  75. /**
  76. * @param $long | Float ,长度,单位cm
  77. * @param $width | Float ,宽度,单位cm
  78. * @param $high | Float ,高度,单位cm
  79. * @return 体积重,单位Kg
  80. */
  81. public function getVolumeWeight($long, $width, $high)
  82. {
  83. $volume_weight = ($long * $width * $high) / $this->volumeWeightCoefficient;
  84. return (float)$volume_weight;
  85. }
  86. /**
  87. * @param $long | Float ,长度,单位cm
  88. * @param $width | Float ,宽度,单位cm
  89. * @param $high | Float ,高度,单位cm
  90. * @return 体积体积,单位cm
  91. */
  92. public function getVolume($long, $width, $high)
  93. {
  94. return Yii::$service->helper->format->number_format($long * $width * $high);
  95. }
  96. /**
  97. * @proeprty $shipping_method 货运方式的key
  98. * @param $shippingConfig Array (MIX),配置信息。
  99. * @proeprty $weight 产品的总重量
  100. * @proeprty $country 货运国家
  101. * @proeprty $region 货运省份
  102. * @return float 通过计算,得到在【基础货币】下的运费金额。
  103. * 本部分只针对csv类型的shipping进行计算。
  104. */
  105. protected function actionGetShippingCostByCsv($shipping_method, $shippingConfig, $weight, $country, $region)
  106. {
  107. if (!$weight) {
  108. return 0;
  109. }
  110. // 从配置中读取出来csv表格的数组信息(处理后的)。
  111. $shippingArr = $shippingConfig['csv_content'];
  112. $country = $country ? $country : '*';
  113. $region = $region ? $region : '*';
  114. if (isset($shippingArr[$country][$region])) {
  115. $priceData = $shippingArr[$country][$region];
  116. } elseif (isset($shippingArr[$country]['*'])) {
  117. $priceData = $shippingArr[$country]['*'];
  118. } elseif (isset($shippingArr['*']['*'])) {
  119. $priceData = $shippingArr['*']['*'];
  120. } else {
  121. throw new InvalidConfigException('error,this country is config in csv table');
  122. }
  123. // 找到相应的配置后,是各个区间值,根据区间值,得到相应的运费。
  124. $prev_weight = 0;
  125. $prev_price = 0;
  126. $last_price = 0;
  127. if (is_array($priceData)) {
  128. foreach ($priceData as $data) {
  129. $csv_weight = (float) $data[0];
  130. $csv_price = (float) $data[1];
  131. if ($weight >= $csv_weight) {
  132. $prev_weight = $csv_weight;
  133. $prev_price = $csv_price;
  134. continue;
  135. } else {
  136. $last_price = $prev_price;
  137. break;
  138. }
  139. }
  140. if (!$last_price) {
  141. $last_price = $csv_price;
  142. }
  143. return $last_price;
  144. } else {
  145. throw new InvalidConfigException('error,shipping info config is error');
  146. }
  147. }
  148. /**
  149. * @proeprty $shipping_method 货运方式的key
  150. * @proeprty $weight 产品的总重量
  151. * @proeprty $country 货运国家
  152. * @return array 当前货币下的运费的金额。
  153. * 此处只做运费计算,不管该shipping是否可用。
  154. * 结果数据示例:
  155. * [
  156. * 'currCost' => 66,
  157. * 'baseCost' => 11,
  158. * ]
  159. */
  160. protected function actionGetShippingCost($method, $shippingInfo, $weight, $country, $region)
  161. {
  162. // 得到shipping计算的字符串公式
  163. $formula = $shippingInfo['formula'];
  164. // 如果公式的值为`csv`
  165. if ($formula === 'csv') {
  166. // 通过csv表格的配置,得到运费(基础货币)
  167. $usdCost = $this->getShippingCostByCsv($method, $shippingInfo, $weight, $country, $region);
  168. // 当前货币
  169. $currentCost = Yii::$service->page->currency->getCurrentCurrencyPrice($usdCost);
  170. return [
  171. 'currCost' => $currentCost,
  172. 'baseCost' => $usdCost,
  173. ];
  174. } else { // 通过公式计算得到运费。
  175. $formula = str_replace('[weight]', $weight, $formula);
  176. $baseCost = eval("return $formula;");
  177. $currCost = Yii::$service->page->currency->getCurrentCurrencyPrice($baseCost);
  178. return [
  179. 'currCost' => Yii::$service->helper->format->number_format($currCost, 2),
  180. 'baseCost' => Yii::$service->helper->format->number_format($baseCost, 2),
  181. ];
  182. }
  183. }
  184. /**
  185. * @proeprty $customShippingMethod 自定义的货运方式,这个一般是通过前端传递过来的shippingMethod
  186. * @proeprty $cartShippingMethod 购物车中的货运方式,这个是从购物车表中取出来的。
  187. * @return string 返回当前的货运方式。
  188. */
  189. protected function actionGetCurrentShippingMethod($customShippingMethod, $cartShippingMethod, $country, $region, $weight)
  190. {
  191. $available_method = $this->getAvailableShippingMethods($country, $region, $weight);
  192. if ($customShippingMethod) {
  193. if (isset($available_method[$customShippingMethod])) {
  194. return $customShippingMethod;
  195. }
  196. }
  197. if ($cartShippingMethod) {
  198. if (isset($available_method[$cartShippingMethod])) {
  199. return $cartShippingMethod;
  200. }
  201. }
  202. // 如果都不存在,则将可用物流中的第一个取出来$available_method
  203. foreach ($available_method as $method => $v) {
  204. return $method;
  205. }
  206. }
  207. /**
  208. * @param $shipping_method | String
  209. * @return 得到货运方式的名字
  210. */
  211. protected function actionGetShippingLabelByMethod($shipping_method)
  212. {
  213. $s = $this->_shipping_methods[$shipping_method];
  214. return isset($s['label']) ? $s['label'] : '';
  215. }
  216. /**
  217. * @param $shipping_method | String
  218. * @return bool 发货方式
  219. * 判断前端传递的shipping method是否有效(做安全性验证)
  220. */
  221. protected function actionIfIsCorrect($country, $region, $shipping_method, $weight)
  222. {
  223. $available_method = $this->getAvailableShippingMethods($country, $region, $weight);
  224. if (isset($available_method[$shipping_method]) && !empty($available_method[$shipping_method])) {
  225. return true;
  226. } else {
  227. return false;
  228. }
  229. }
  230. /**
  231. * @param $countryCode | String
  232. * @param $region | String
  233. * @param weight | Float
  234. * 将可用的shipping method数组的第一个取出来作为默认的shipping。
  235. */
  236. public function getDefaultShippingMethod($countryCode, $region, $weight)
  237. {
  238. $available_method = $this->getAvailableShippingMethods($countryCode, $region, $weight);
  239. foreach ($available_method as $method => $v) {
  240. return $method;
  241. }
  242. }
  243. /**
  244. * @param $country | String 国家 如果没有国家,则传递空字符串
  245. * @param $region | String 省市 如果没有省市,则传递空字符串
  246. * @param $shipping_method | String 货运方式
  247. * @param $weight | Float ,重量,如果某些物流存在重量限制,那么,重量不满足的将会被剔除
  248. * @return $availableShipping | Array ,可用的shipping method
  249. * 在全部的shipping method,存在1.国家省市限制,2.重量限制
  250. * 不符合条件的被剔除,剩下的就是可用的shipping method
  251. * 该函数调用的地方比较多,因此将结果存储到类变量中,节省计算。
  252. */
  253. public function getAvailableShippingMethods($countryCode, $region, $weight = 0)
  254. {
  255. $c_key = $countryCode ? $countryCode : 'noKey';
  256. $r_key = $region ? $region : 'noKey';
  257. $w_key = $weight ? $weight : 'noKey';
  258. // 通过类变量记录,只计算一次。
  259. if (!isset($this->_available_shipping[$c_key][$r_key][$w_key])) {
  260. $this->_available_shipping[$c_key][$r_key][$w_key] = [];
  261. //var_dump($this->_shipping_methods);
  262. $availableShipping = [];
  263. if (!$countryCode && !$region) {
  264. $availableShipping = $this->_shipping_methods;
  265. } else {
  266. foreach ($this->_shipping_methods as $shipping_method => $v) {
  267. $countryLimit = isset($v['country']) ? $v['country'] : '';
  268. if ($this->isCountryLimit($countryLimit, $countryCode)) {
  269. continue;
  270. }
  271. // 如果是csv类型,查看在csv中是否存在
  272. $formula = isset($v['formula']) ? $v['formula'] : '';
  273. if ($formula === '') {
  274. continue; // 代表shipping配置有问题,不可用
  275. } elseif ($formula === 'csv') {
  276. if ($this->isCsvCountryReginLimit($v, $countryCode, $region)) {
  277. continue;
  278. }
  279. }
  280. $availableShipping[$shipping_method] = $v;
  281. }
  282. }
  283. // 重量限制
  284. if ($weight) {
  285. $availableShipping = $this->wegihtAllowedShipping($availableShipping, $weight);
  286. }
  287. $this->_available_shipping[$c_key][$r_key][$w_key] = $availableShipping;
  288. }
  289. return $this->_available_shipping[$c_key][$r_key][$w_key];
  290. }
  291. /**
  292. * @param $shipping_method | String 货运方式的key
  293. * @return array ,通过csv表格,得到对应的运费数组信息
  294. * 内部函数,将csv表格中的shipping数据读出来
  295. * 返回的数据格式为:
  296. * [
  297. * 'fast_shipping' => [
  298. * 'US' => [
  299. * '*' => [
  300. * [0.5100, 22.9],
  301. * [1.0100, 25.9],
  302. * [2.5100, 34.9],
  303. * ]
  304. * ],
  305. * 'DE' => [
  306. * '*' => [
  307. * [0.5100, 22.9],
  308. * [1.0100, 25.9],
  309. * [2.5100, 34.9],
  310. * ]
  311. * ],
  312. * ]
  313. * ]
  314. */
  315. protected function getShippingByTableCsv($shipping_method)
  316. {
  317. $shippingCsvArr = [];
  318. // 从csv文件中读取shipping信息。
  319. $commonDir = Yii::getAlias($this->shippingCsvDir);
  320. $csv = $commonDir.'/'.$shipping_method.'.csv';
  321. if (!file_exists($csv)) {
  322. return false;
  323. }
  324. $fp = fopen($csv, 'r');
  325. $i = 0;
  326. while (!feof($fp)) {
  327. if ($i) {
  328. $content = fgets($fp);
  329. $arr = explode(',', $content);
  330. $country = $arr[0];
  331. $Region = $arr[1];
  332. $Weight = $arr[3];
  333. $ShippingPrice = $arr[4];
  334. $shippingCsvArr[$country][$Region][] = [$Weight, $ShippingPrice];
  335. }
  336. $i++;
  337. }
  338. fclose($fp);
  339. return $shippingCsvArr;
  340. }
  341. /**
  342. * @param $countryLimit | Array 配置中的国家限制数组
  343. * @param $countryCode | String 判断的国家code
  344. * 判断 $countryCode 是否存在国家方面的限制
  345. */
  346. protected function isCountryLimit($countryLimit, $countryCode)
  347. {
  348. // 如果存在国家方面的限制
  349. if (is_array($countryLimit) && !empty($countryLimit)) {
  350. $type = isset($countryLimit['type']) ? $countryLimit['type'] : '';
  351. $code = isset($countryLimit['code']) ? $countryLimit['code'] : '';
  352. if ($type == 'allow') {
  353. // 如果不存在于数组,则代表不允许
  354. if (!in_array($countryCode, $code)) {
  355. return true;
  356. }
  357. } elseif ($type == 'not_allow') {
  358. if (in_array($countryCode, $code)) {
  359. return true;
  360. }
  361. }
  362. }
  363. return false;
  364. }
  365. /**
  366. * @param $shippingConfig | Array , shipping method对应的配置
  367. * @param $countryCode | String 国家简码
  368. * @param $region | String 省市
  369. * 根据csv content里面的配置,判断是否存在国家 省市限制
  370. */
  371. protected function isCsvCountryReginLimit($shippingConfig, $countryCode, $region)
  372. {
  373. $csv_content = isset($shippingConfig['csv_content']) ? $shippingConfig['csv_content'] : '';
  374. // 如果不存在全局国家,省市 的通用配置
  375. if (!isset($csv_content['*']['*'])) {
  376. // 如果当前的国家对应的配置不存在,则不可用
  377. if (!isset($csv_content[$countryCode])) {
  378. return true;
  379. } elseif ($region) { // 如果参数传递的$region不为空
  380. // 国家可用,如果不存在省市的通用配置
  381. if (!isset($csv_content[$countryCode]['*'])) {
  382. // 如果不存在相应省市的配置,则不可用
  383. if (!isset($csv_content[$countryCode][$region])) {
  384. return true;
  385. }
  386. }
  387. }
  388. }
  389. return false;
  390. }
  391. /**
  392. * @param $availableShipping | Array ,shipping method 数组
  393. * @param $weight | Float 重量
  394. * @return Array
  395. * 返回满足重量限制的shipping method
  396. */
  397. protected function wegihtAllowedShipping($availableShipping, $weight)
  398. {
  399. $available_shipping = [];
  400. // 查看是否存在重量限制,如果存在,则不可用
  401. foreach ($availableShipping as $method => $v) {
  402. $weightLimit = isset($v['weight']) ? $v['weight'] : '';
  403. if (isset($weightLimit['min']) && $weightLimit['min'] > $weight) {
  404. continue;
  405. }
  406. if (isset($weightLimit['max']) && $weightLimit['max'] < $weight) {
  407. continue;
  408. }
  409. $available_shipping[$method] = $v;
  410. }
  411. return $available_shipping;
  412. }
  413. }