GoodsService.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. <?php
  2. namespace addons\StarChain\common\services;
  3. use addons\StarChain\common\enums\GoodsStatusEnum;
  4. use addons\StarChain\common\enums\SkuStatusEnum;
  5. use addons\StarChain\common\models\GoodsAttr;
  6. use addons\StarChain\common\models\MallGoods;
  7. use addons\StarChain\common\models\Setting;
  8. use addons\StarChain\common\models\Storage;
  9. use Exception;
  10. use Yii;
  11. use yii\helpers\Json;
  12. /**
  13. * 商品服务
  14. */
  15. class GoodsService extends BaseService
  16. {
  17. /**
  18. * 标识
  19. *
  20. * @return string
  21. */
  22. private $goods_source = "StarChain";
  23. /**
  24. * 最大库存
  25. *
  26. * @var integer
  27. */
  28. private $max_stock_num = 999999;
  29. /**
  30. * 最大库存
  31. *
  32. * @var integer
  33. */
  34. private $is_update = false;
  35. /**
  36. * 是否上架
  37. *
  38. * @var boolean
  39. */
  40. private $goods_status = false;
  41. /**
  42. * 当前商城商品ID
  43. *
  44. * @var integer
  45. */
  46. private $goods_id;
  47. /**
  48. * 当前供应链商品ID
  49. *
  50. * @var string
  51. */
  52. private $spu_id;
  53. /**
  54. * 商城设置
  55. *
  56. * @var array
  57. */
  58. private $setting;
  59. /**
  60. * 设置价格
  61. *
  62. * @var array
  63. */
  64. private $price;
  65. /**
  66. * 初始化
  67. *
  68. * @param array $config
  69. */
  70. public function __construct($config)
  71. {
  72. parent::__construct($config);
  73. // 获取设置
  74. $this->setting = Setting::getImportSetting($this->mall_id);
  75. }
  76. /**
  77. * 添加商品
  78. *
  79. * @param string $spu_id
  80. * @return boolean|MallGoods
  81. */
  82. public function addGoods($spu_id)
  83. {
  84. try {
  85. // spu详情
  86. $spu_info = $this->api->getSpuBySpuIds([$spu_id]);
  87. // sku详情
  88. $sku_list_detail = $this->api->listSkuBySpuId($spu_id);
  89. } catch (Exception $e) {
  90. throw new Exception("API接口获取商品失败:" . $e->getMessage());
  91. // return $this->error("API接口获取商品失败:" . $e->getMessage());
  92. }
  93. // 设置当前供应链商品ID
  94. $this->spu_id = $spu_id;
  95. try {
  96. // 是否更新
  97. if ($goods_id = Storage::find()->where(['mall_id' => $this->mall_id, 'is_delete' => 0, 'spu_id' => $spu_id])->select('mall_goods_id')->scalar()) {
  98. $this->goods_id = $goods_id;
  99. $this->is_update = true;
  100. }
  101. // 获取商品属性
  102. $goods_attr = $this->getGoodsAttr($spu_info[0], $sku_list_detail);
  103. // 获取规格组
  104. $attr_groups = $this->getAttrGroups($sku_list_detail);
  105. // $attr_groups = $this->removeAttrGroups($attr_groups, $goods_attr['attr']);
  106. // 匹配规格ID
  107. $goods_attr = $this->matchingAttrId($goods_attr, $attr_groups);
  108. $goods_attr['attr_groups'] = $attr_groups;
  109. // 保存商品
  110. $res = MallGoods::saveGoods($goods_attr);
  111. if ($res === false) {
  112. throw new Exception('商品添加到商城失败');
  113. }
  114. } catch (Exception $e) {
  115. throw new Exception($e->getMessage());
  116. // return $this->error($e->getMessage());
  117. }
  118. return $res;
  119. }
  120. /**
  121. * 下架商品
  122. *
  123. * @param array $spu_ids
  124. * @return void
  125. */
  126. public function goodsPulled($spu_ids)
  127. {
  128. $storage_list = Storage::find()->where(['spu_id' => $spu_ids])->with('goods')->all();
  129. try {
  130. foreach ($storage_list as $storage) {
  131. $storage->goods->goodsPulled();
  132. // 下架
  133. MallGoods::setData([
  134. 'id' => $storage->mall_goods_id,
  135. 'mall_id' => $storage->mall_id,
  136. 'is_on_sale' => 0,
  137. ]);
  138. }
  139. } catch (Exception $e) {
  140. throw new Exception($e->getMessage());
  141. }
  142. }
  143. /**
  144. * 添加商品
  145. *
  146. * @param string $spu_id
  147. * @return boolean|MallGoods
  148. */
  149. public function editGoods($spu_id)
  150. {
  151. // 是否更新
  152. $goods_id = Storage::getMallGoodsIdBySpuId($this->mall_id, $spu_id);
  153. if (empty($goods_id)) {
  154. return false;
  155. }
  156. $this->goods_id = $goods_id;
  157. $this->is_update = true;
  158. try {
  159. // spu详情
  160. $spu_info = $this->api->getSpuBySpuIds([$spu_id]);
  161. // sku详情
  162. $sku_list_detail = $this->api->listSkuBySpuId($spu_id);
  163. } catch (Exception $e) {
  164. throw new Exception("API接口获取商品失败:" . $e->getMessage());
  165. }
  166. // 设置当前供应链商品ID
  167. $this->spu_id = $spu_id;
  168. try {
  169. // 获取商品属性
  170. $goods_attr = $this->getGoodsAttr($spu_info[0], $sku_list_detail);
  171. // 获取规格组
  172. $attr_groups = $this->getAttrGroups($sku_list_detail);
  173. // $attr_groups = $this->removeAttrGroups($attr_groups, $goods_attr['attr']);
  174. // 匹配规格ID
  175. $goods_attr = $this->matchingAttrId($goods_attr, $attr_groups);
  176. $goods_attr['attr_groups'] = $attr_groups;
  177. // 保存商品
  178. $res = MallGoods::saveGoods($goods_attr);
  179. if ($res === false) {
  180. throw new Exception('商品添加到商城失败');
  181. }
  182. } catch (Exception $e) {
  183. throw new Exception($e->getMessage());
  184. }
  185. return $res;
  186. }
  187. /**
  188. * 获取添加商品属性
  189. *
  190. * @param array $goods_info
  191. * @return array
  192. */
  193. private function getGoodsAttr($goods_info, $sku_list_detail)
  194. {
  195. // 获取匹配的Attr
  196. $specs_attr = $this->getSpecsForAttr($sku_list_detail, $goods_info['status']);
  197. $is_attr = count($specs_attr) > 1 ? 1 : 0;
  198. $stock = $this->getStock($specs_attr);
  199. $attr = [
  200. 'attr' => $specs_attr, // 商品规格属性
  201. 'bannar_pic' => $this->getBannarPic($goods_info['carouselImgList']),
  202. 'cover_pic' => $goods_info['carouselImgList'][0], // 主图
  203. 'price' => $this->price['sell'], // 销售价
  204. 'original_price' => $this->price['original'], // 原价
  205. 'cost_price' => $this->price['cost'], // 成本价
  206. 'detail' => $this->getDetailImg($goods_info['detailImgList']), // 商品详情
  207. 'stock' => $stock > $this->max_stock_num ? $this->max_stock_num : $stock, // 总库存
  208. 'goods_name' => $goods_info['name'], // 商品名称
  209. 'goods_source' => $this->goods_source,
  210. 'is_attr' => $is_attr,
  211. 'goods_no' => '',
  212. ];
  213. // 商品规格为空 不添加
  214. if (empty($attr['attr'])) {
  215. throw new Exception("可售规格均已下架, 不能导入");
  216. }
  217. // 当商品为上架时,还需要判断库存是否支持上架
  218. if ($goods_info['status'] == GoodsStatusEnum::STATUS_0) {
  219. $goods_info['status'] = $this->goods_status ? 0 : 1;
  220. }
  221. // 上下架设置
  222. $attr['is_on_sale'] = $goods_info['status'] == GoodsStatusEnum::STATUS_0 ? 1 : 0;
  223. if ($this->is_update) {
  224. // 当商品上架时 不需要改变系统商品状态1
  225. if ($goods_info['status'] == 1) {
  226. unset($goods_info['status']);
  227. }
  228. $detail = MallGoods::getDetail($this->goods_id);
  229. return $this->mergeDetail($detail, $attr);
  230. } else {
  231. // 获取分类 仅新增时添加分类
  232. $mall_cate_service = new MallCateService(['mall_id' => $this->mall_id]);
  233. $cats = $mall_cate_service->getCats(
  234. $goods_info['categoryId1'],
  235. $goods_info['categoryId2'],
  236. $goods_info['categoryId3']
  237. );
  238. return array_merge($attr, [
  239. 'goods_type' => 1, // 默认1
  240. 'subtitle' => '', //
  241. 'cats' => [$cats], // 分类
  242. 'labels' => [], // 商品标签
  243. 'mchCats' => [], // 多商户分类
  244. 'services' => [], // 商品服务
  245. 'unit' => '', // 单位
  246. 'virtual_sales' => 0, // 虚拟销量
  247. 'stock_warning' => 0,
  248. 'goods_weight' => 0, // 商品权重
  249. 'goods_no' => '', // 商品货号
  250. 'is_show_stock' => 1,
  251. 'is_show_sales' => 0, // 是否显示销量
  252. 'freight_type' => [],
  253. 'freight_rules_type' => 1,
  254. 'freight_id' => '',
  255. 'shipping_fee' => 0,
  256. 'free_shipping_num' => 0,
  257. 'free_shipping_money' => 0,
  258. 'is_cross_border' => 0,
  259. 'is_id_card' => 0,
  260. 'extra' => '', // 额外属性
  261. 'coupon_selection' => [],
  262. 'goods_setting' => [],
  263. 'is_area_limit' => 0,
  264. 'area_limit' => [],
  265. 'attr_groups' => [],
  266. 'mch_id' => 0,
  267. 'mall_id' => $this->mall_id,
  268. ]);
  269. }
  270. }
  271. /**
  272. * 获取商品规格属性
  273. *
  274. * @param array $specs
  275. * @return array
  276. */
  277. private function getSpecsForAttr($sku_list, $status)
  278. {
  279. if (empty($sku_list)) {
  280. throw new Exception('');
  281. }
  282. $attr = [];
  283. foreach ($sku_list as $sku) {
  284. // 仅需要上架的
  285. if ($sku['status'] == SkuStatusEnum::STATUS_0 || $status == SkuStatusEnum::STATUS_1) {
  286. $temp = [
  287. 'attr_list' => $this->getAttrList($sku['skuPropertyList']), // 规格
  288. 'stock' => $sku['stock'] > $this->max_stock_num ? $this->max_stock_num : $sku['stock'], // 库存
  289. 'price' => $this->getPrice($sku, 'sell'), // 价格
  290. 'original_price' => $this->getPrice($sku, 'original'), // 价格
  291. 'cost_price' => $this->getPrice($sku, 'cost'), // 价格
  292. 'goods_no' => '',
  293. 'weight' => 0,
  294. 'pic_url' => $sku['skuPicUrl'],
  295. 'distributionLevelList' => [],
  296. 'member_price' => [],
  297. 'levelPrice' => [],
  298. 'sku_id' => $sku['skuId'],
  299. 'chain_goods_id' => $this->spu_id,
  300. ];
  301. // 如果是更新
  302. if ($this->is_update) {
  303. $params = [
  304. 'sku_id' => $temp['sku_id'],
  305. 'mall_id' => $this->mall_id,
  306. 'mall_goods_id' => $this->goods_id,
  307. ];
  308. $sku_model = GoodsAttr::find()->where($params)->select('attr_id')->one();
  309. if ($sku_model) {
  310. $temp['id'] = $sku_model->attr_id;
  311. }
  312. }
  313. // 如果库存大于0 可以上架
  314. if ($sku['stock'] > 0) {
  315. $this->goods_status = true;
  316. }
  317. $attr[] = $temp;
  318. }
  319. }
  320. // 剔除没有规格的商品
  321. foreach ($attr as $k => $v) {
  322. if (empty($v['attr_list'])) {
  323. unset($attr[$k]);
  324. }
  325. }
  326. return $attr;
  327. }
  328. /**
  329. * 获取规格属性
  330. *
  331. * @param array $skus
  332. * @return array
  333. */
  334. private function getAttrGroups($skus)
  335. {
  336. $first_key = array_key_first($skus);
  337. $sku_property_list = $skus[$first_key]['skuPropertyList'];
  338. $attr_groups = [];
  339. $attr_group_id = 1;
  340. foreach ($sku_property_list as $option) {
  341. $attr_groups[] = [
  342. 'attr_group_id' => $attr_group_id,
  343. 'attr_group_name' => $option['specName'],
  344. ];
  345. $attr_group_id++;
  346. }
  347. $temp = 0;
  348. $attr_name = [];
  349. $attr_id = 1;
  350. foreach ($skus as $sku) {
  351. if ($sku['skuPropertyList']) {
  352. foreach ($attr_groups as $k => $group) {
  353. foreach ($sku['skuPropertyList'] as $option) {
  354. if (
  355. $option['specName'] == $group['attr_group_name'] &&
  356. !in_array($option['specValueName'], $attr_name)
  357. ) {
  358. $attr_name[] = $option['specValueName'];
  359. $attr_groups[$k]['attr_list'][] = [
  360. 'attr_id' => $attr_id,
  361. 'attr_name' => $option['specValueName'],
  362. // 图片仅第一个添加
  363. 'pic_url' => $sku['skuPicUrl'],
  364. ];
  365. $attr_id++;
  366. }
  367. }
  368. $temp++;
  369. }
  370. }
  371. }
  372. $attr_id = 0;
  373. return array_map(function ($item) use (&$attr_id) {
  374. return array_merge($item, ['attr_list' => array_map(function ($attr) use (&$attr_id) {
  375. $attr_id++;
  376. return array_merge($attr, [
  377. 'attr_id' => $attr_id,
  378. ]);
  379. }, $item['attr_list'])]);
  380. }, $attr_groups);
  381. }
  382. /**
  383. * 匹配规格ID
  384. *
  385. * @param array $goods_attr
  386. * @param array $attr_groups
  387. * @return array
  388. */
  389. private function matchingAttrId($goods_attr, $attr_groups)
  390. {
  391. foreach ($goods_attr['attr'] as $k => $sku) {
  392. $sign = [];
  393. foreach ($sku['attr_list'] as $k1 => $attr) {
  394. $attr_group_id = $this->getAttrGroupId($attr_groups, $attr['attr_group_name']);
  395. $attr_id = $this->getAttrId($attr_groups, $attr['attr_name']);
  396. $sign[] = $attr_id;
  397. $goods_attr['attr'][$k]['attr_list'][$k1]['attr_id'] = $attr_id;
  398. $goods_attr['attr'][$k]['attr_list'][$k1]['attr_group_id'] = $attr_group_id;
  399. }
  400. // 拼接sign_id
  401. // asort($sign);
  402. count($sign) > 1 && $goods_attr['attr'][$k]['sign_id'] = implode(':', $sign);
  403. }
  404. return $goods_attr;
  405. }
  406. /**
  407. * 获取规格属性ID
  408. *
  409. * @param array $attr_groups 规格组
  410. * @param string $name 规格名称
  411. * @return integer|null
  412. */
  413. private function getAttrId($attr_groups, $name)
  414. {
  415. foreach ($attr_groups as $group) {
  416. foreach ($group['attr_list'] as $attr) {
  417. if ($attr['attr_name'] == $name) {
  418. return $attr['attr_id'];
  419. }
  420. }
  421. }
  422. }
  423. /**
  424. * 获取规格属性组ID
  425. *
  426. * @param array $attr_groups 规格组
  427. * @param string $name 属性组名称
  428. * @return integer|null
  429. */
  430. public function getAttrGroupId($attr_groups, $name)
  431. {
  432. foreach ($attr_groups as $group) {
  433. if ($group['attr_group_name'] == $name) {
  434. return $group['attr_group_id'];
  435. }
  436. }
  437. }
  438. /**
  439. * 获取商品规格属性
  440. *
  441. * @param array $options
  442. * [
  443. * [
  444. * 'spec_name' => '颜色'
  445. * 'spec_item_name' => '纯白'
  446. * ]
  447. * [
  448. * 'spec_name' => '尺码'
  449. * 'spec_item_name' => '39'
  450. * ]
  451. * ]
  452. * @return array
  453. */
  454. private function getAttrList($list)
  455. {
  456. $attr = [];
  457. foreach ($list as $option) {
  458. !empty($option['specName']) && !empty($option['specValueName']) && $attr[] = [
  459. 'attr_group_name' => $option['specName'],
  460. 'attr_name' => $option['specValueName'],
  461. ];
  462. }
  463. return $attr;
  464. }
  465. /**
  466. * 获取商品轮播图
  467. *
  468. * @param array $gallery
  469. * @return array
  470. */
  471. private function getBannarPic($list)
  472. {
  473. $pic = [];
  474. foreach ($list as $v) {
  475. $pic[] = [
  476. 'id' => 0,
  477. 'pic_url' => $v,
  478. ];
  479. }
  480. return $pic;
  481. }
  482. /**
  483. * 获取库存
  484. *
  485. * @param array $attr
  486. * @return integer
  487. */
  488. private function getStock($attr)
  489. {
  490. $stock = 0;
  491. foreach ($attr as $v) {
  492. $stock = bcadd($stock, $v['stock']);
  493. }
  494. return $stock;
  495. }
  496. /**
  497. * 融合属性详情
  498. *
  499. * @param array $detail
  500. * @param array $attr
  501. * @return array
  502. */
  503. private function mergeDetail($detail, $attr)
  504. {
  505. // 全局设置
  506. $setting = $this->setting;
  507. // 更新价格 自动更新为1 不更新为0 不更新的时候进入方法进行价格删除
  508. if (!$setting['auto_present_price']) {
  509. // 删除更新获取回来的价格
  510. unset($attr['price']);
  511. unset($attr['original_price']);
  512. unset($attr['cost_price']);
  513. unset($attr['stock']);
  514. // 查询旧属性,重组获取旧价格
  515. $old_attr = [];
  516. // 查询所有循环重组 减少sql查询
  517. $model = GoodsAttr::find();
  518. $sku_id_list = $model->select('attr_id, sku_id')->where([
  519. 'mall_goods_id' => $detail['id'],
  520. 'is_delete' => 0,
  521. ])->asArray()->indexBy('attr_id')->all();
  522. foreach ($detail['attr'] as $k => $v) {
  523. // 按sku_id重组旧规格
  524. if (isset($sku_id_list[$v['id']])) {
  525. $sku_id = $sku_id_list[$v['id']]['sku_id'];
  526. $old_attr[$sku_id] = $v;
  527. }
  528. }
  529. // 旧价格写入新属性
  530. foreach ($attr['attr'] as $k => $v) {
  531. // 旧属性存在
  532. if (isset($old_attr[$v['sku_id']])) {
  533. // 替换新属性价格为旧属性价格
  534. $temp = $old_attr[$v['sku_id']];
  535. $attr['attr'][$k]['price'] = $temp['price'];
  536. $attr['attr'][$k]['original_price'] = $temp['original_price'];
  537. $attr['attr'][$k]['cost_price'] = $temp['cost_price'];
  538. }
  539. }
  540. }
  541. // 更新属性 自动更新为1 不更新为0 不更新的时候进入方法进行价格删除
  542. if (!$setting['auto_base_info']) {
  543. unset($attr['detail']);
  544. unset($attr['name']);
  545. unset($attr['goods_num']);
  546. }
  547. return array_merge($detail, $attr);
  548. }
  549. /**
  550. * 获取详情
  551. *
  552. * @param array $imgs
  553. * @return string
  554. */
  555. private function getDetailImg($imgs)
  556. {
  557. $str = '';
  558. foreach ($imgs as $v) {
  559. $str .= '<p><img src="' . $v . '" /></p>';
  560. }
  561. return $str;
  562. }
  563. /**
  564. * 获取价格
  565. *
  566. * @param array $info
  567. * @param array $type
  568. * @return float
  569. */
  570. private function getPrice($info, $type)
  571. {
  572. $setting = $this->setting;
  573. $price_field = $setting[$type . '_name'];
  574. $price_value = $setting[$type . '_value'];
  575. if (empty($price_field) || empty($price_value)) {
  576. throw new Exception('自动设置价格字段有误');
  577. }
  578. $price = $info[$price_field];
  579. if (empty($price)) {
  580. throw new Exception('自动设置价格不存在');
  581. }
  582. $last_price = bcdiv(bcmul($price, $price_value, 2), 100, 2);
  583. // 商品详情不存在价格 存储规格中对应最低价格作为商品详情的价格
  584. empty($this->price[$type])
  585. ? $this->price[$type] = $last_price
  586. : ($last_price > $this->price[$type]
  587. ?: $this->price[$type] = $last_price);
  588. return $last_price;
  589. }
  590. }