Shipping.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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. $currentCost = eval("return $formula;");
  177. return [
  178. 'currCost' => Yii::$service->helper->format->number_format($currentCost, 2),
  179. 'baseCost' => Yii::$service->helper->format->number_format($currentCost, 2),
  180. ];
  181. }
  182. }
  183. /**
  184. * @proeprty $customShippingMethod 自定义的货运方式,这个一般是通过前端传递过来的shippingMethod
  185. * @proeprty $cartShippingMethod 购物车中的货运方式,这个是从购物车表中取出来的。
  186. * @return string 返回当前的货运方式。
  187. */
  188. protected function actionGetCurrentShippingMethod($customShippingMethod, $cartShippingMethod, $country, $region, $weight)
  189. {
  190. $available_method = $this->getAvailableShippingMethods($country, $region, $weight);
  191. if ($customShippingMethod) {
  192. if (isset($available_method[$customShippingMethod])) {
  193. return $customShippingMethod;
  194. }
  195. }
  196. if ($cartShippingMethod) {
  197. if (isset($available_method[$cartShippingMethod])) {
  198. return $cartShippingMethod;
  199. }
  200. }
  201. // 如果都不存在,则将可用物流中的第一个取出来$available_method
  202. foreach ($available_method as $method => $v) {
  203. return $method;
  204. }
  205. }
  206. /**
  207. * @param $shipping_method | String
  208. * @return 得到货运方式的名字
  209. */
  210. protected function actionGetShippingLabelByMethod($shipping_method)
  211. {
  212. $s = $this->_shipping_methods[$shipping_method];
  213. return isset($s['label']) ? $s['label'] : '';
  214. }
  215. /**
  216. * @param $shipping_method | String
  217. * @return bool 发货方式
  218. * 判断前端传递的shipping method是否有效(做安全性验证)
  219. */
  220. protected function actionIfIsCorrect($country, $region, $shipping_method, $weight)
  221. {
  222. $available_method = $this->getAvailableShippingMethods($country, $region, $weight);
  223. if (isset($available_method[$shipping_method]) && !empty($available_method[$shipping_method])) {
  224. return true;
  225. } else {
  226. return false;
  227. }
  228. }
  229. /**
  230. * @param $countryCode | String
  231. * @param $region | String
  232. * @param weight | Float
  233. * 将可用的shipping method数组的第一个取出来作为默认的shipping。
  234. */
  235. public function getDefaultShippingMethod($countryCode, $region, $weight)
  236. {
  237. $available_method = $this->getAvailableShippingMethods($countryCode, $region, $weight);
  238. foreach ($available_method as $method => $v) {
  239. return $method;
  240. }
  241. }
  242. /**
  243. * @param $country | String 国家 如果没有国家,则传递空字符串
  244. * @param $region | String 省市 如果没有省市,则传递空字符串
  245. * @param $shipping_method | String 货运方式
  246. * @param $weight | Float ,重量,如果某些物流存在重量限制,那么,重量不满足的将会被剔除
  247. * @return $availableShipping | Array ,可用的shipping method
  248. * 在全部的shipping method,存在1.国家省市限制,2.重量限制
  249. * 不符合条件的被剔除,剩下的就是可用的shipping method
  250. * 该函数调用的地方比较多,因此将结果存储到类变量中,节省计算。
  251. */
  252. public function getAvailableShippingMethods($countryCode, $region, $weight = 0)
  253. {
  254. $c_key = $countryCode ? $countryCode : 'noKey';
  255. $r_key = $region ? $region : 'noKey';
  256. $w_key = $weight ? $weight : 'noKey';
  257. // 通过类变量记录,只计算一次。
  258. if (!isset($this->_available_shipping[$c_key][$r_key][$w_key])) {
  259. $this->_available_shipping[$c_key][$r_key][$w_key] = [];
  260. //var_dump($this->_shipping_methods);
  261. $availableShipping = [];
  262. if (!$countryCode && !$region) {
  263. $availableShipping = $this->_shipping_methods;
  264. } else {
  265. foreach ($this->_shipping_methods as $shipping_method => $v) {
  266. $countryLimit = isset($v['country']) ? $v['country'] : '';
  267. if ($this->isCountryLimit($countryLimit, $countryCode)) {
  268. continue;
  269. }
  270. // 如果是csv类型,查看在csv中是否存在
  271. $formula = isset($v['formula']) ? $v['formula'] : '';
  272. if ($formula === '') {
  273. continue; // 代表shipping配置有问题,不可用
  274. } elseif ($formula === 'csv') {
  275. if ($this->isCsvCountryReginLimit($v, $countryCode, $region)) {
  276. continue;
  277. }
  278. }
  279. $availableShipping[$shipping_method] = $v;
  280. }
  281. }
  282. // 重量限制
  283. if ($weight) {
  284. $availableShipping = $this->wegihtAllowedShipping($availableShipping, $weight);
  285. }
  286. $this->_available_shipping[$c_key][$r_key][$w_key] = $availableShipping;
  287. }
  288. return $this->_available_shipping[$c_key][$r_key][$w_key];
  289. }
  290. /**
  291. * @param $shipping_method | String 货运方式的key
  292. * @return array ,通过csv表格,得到对应的运费数组信息
  293. * 内部函数,将csv表格中的shipping数据读出来
  294. * 返回的数据格式为:
  295. * [
  296. * 'fast_shipping' => [
  297. * 'US' => [
  298. * '*' => [
  299. * [0.5100, 22.9],
  300. * [1.0100, 25.9],
  301. * [2.5100, 34.9],
  302. * ]
  303. * ],
  304. * 'DE' => [
  305. * '*' => [
  306. * [0.5100, 22.9],
  307. * [1.0100, 25.9],
  308. * [2.5100, 34.9],
  309. * ]
  310. * ],
  311. * ]
  312. * ]
  313. */
  314. protected function getShippingByTableCsv($shipping_method)
  315. {
  316. $shippingCsvArr = [];
  317. // 从csv文件中读取shipping信息。
  318. $commonDir = Yii::getAlias($this->shippingCsvDir);
  319. $csv = $commonDir.'/'.$shipping_method.'.csv';
  320. if (!file_exists($csv)) {
  321. return false;
  322. }
  323. $fp = fopen($csv, 'r');
  324. $i = 0;
  325. while (!feof($fp)) {
  326. if ($i) {
  327. $content = fgets($fp);
  328. $arr = explode(',', $content);
  329. $country = $arr[0];
  330. $Region = $arr[1];
  331. $Weight = $arr[3];
  332. $ShippingPrice = $arr[4];
  333. $shippingCsvArr[$country][$Region][] = [$Weight, $ShippingPrice];
  334. }
  335. $i++;
  336. }
  337. fclose($fp);
  338. return $shippingCsvArr;
  339. }
  340. /**
  341. * @param $countryLimit | Array 配置中的国家限制数组
  342. * @param $countryCode | String 判断的国家code
  343. * 判断 $countryCode 是否存在国家方面的限制
  344. */
  345. protected function isCountryLimit($countryLimit, $countryCode)
  346. {
  347. // 如果存在国家方面的限制
  348. if (is_array($countryLimit) && !empty($countryLimit)) {
  349. $type = isset($countryLimit['type']) ? $countryLimit['type'] : '';
  350. $code = isset($countryLimit['code']) ? $countryLimit['code'] : '';
  351. if ($type == 'allow') {
  352. // 如果不存在于数组,则代表不允许
  353. if (!in_array($countryCode, $code)) {
  354. return true;
  355. }
  356. } elseif ($type == 'not_allow') {
  357. if (in_array($countryCode, $code)) {
  358. return true;
  359. }
  360. }
  361. }
  362. return false;
  363. }
  364. /**
  365. * @param $shippingConfig | Array , shipping method对应的配置
  366. * @param $countryCode | String 国家简码
  367. * @param $region | String 省市
  368. * 根据csv content里面的配置,判断是否存在国家 省市限制
  369. */
  370. protected function isCsvCountryReginLimit($shippingConfig, $countryCode, $region)
  371. {
  372. $csv_content = isset($shippingConfig['csv_content']) ? $shippingConfig['csv_content'] : '';
  373. // 如果不存在全局国家,省市 的通用配置
  374. if (!isset($csv_content['*']['*'])) {
  375. // 如果当前的国家对应的配置不存在,则不可用
  376. if (!isset($csv_content[$countryCode])) {
  377. return true;
  378. } elseif ($region) { // 如果参数传递的$region不为空
  379. // 国家可用,如果不存在省市的通用配置
  380. if (!isset($csv_content[$countryCode]['*'])) {
  381. // 如果不存在相应省市的配置,则不可用
  382. if (!isset($csv_content[$countryCode][$region])) {
  383. return true;
  384. }
  385. }
  386. }
  387. }
  388. return false;
  389. }
  390. /**
  391. * @param $availableShipping | Array ,shipping method 数组
  392. * @param $weight | Float 重量
  393. * @return Array
  394. * 返回满足重量限制的shipping method
  395. */
  396. protected function wegihtAllowedShipping($availableShipping, $weight)
  397. {
  398. $available_shipping = [];
  399. // 查看是否存在重量限制,如果存在,则不可用
  400. foreach ($availableShipping as $method => $v) {
  401. $weightLimit = isset($v['weight']) ? $v['weight'] : '';
  402. if (isset($weightLimit['min']) && $weightLimit['min'] > $weight) {
  403. continue;
  404. }
  405. if (isset($weightLimit['max']) && $weightLimit['max'] < $weight) {
  406. continue;
  407. }
  408. $available_shipping[$method] = $v;
  409. }
  410. return $available_shipping;
  411. }
  412. }