GoodsService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <?php
  2. namespace addons\shopro\service\goods;
  3. use app\admin\model\shopro\goods\Goods;
  4. use app\admin\model\shopro\goods\SkuPrice;
  5. use app\admin\model\shopro\app\ScoreSkuPrice;
  6. use app\admin\model\shopro\Category;
  7. use think\Db;
  8. use addons\shopro\library\Tree;
  9. use addons\shopro\service\SearchHistory;
  10. use addons\shopro\facade\Activity as ActivityFacade;
  11. class GoodsService
  12. {
  13. protected $query = null;
  14. protected $order = [];
  15. protected $md5s = [];
  16. protected $format = null;
  17. protected $is_activity = false; // 是否处理活动
  18. protected $show_score_shop = false; // 查询积分商城商品
  19. public function __construct(\Closure $format = null, \think\db\Query $query = null)
  20. {
  21. $this->query = $query ?: new Goods();
  22. $this->format = $format;
  23. }
  24. /**
  25. * with 关联
  26. *
  27. * @param mixed $with
  28. * @return self
  29. */
  30. public function with($with)
  31. {
  32. $this->query->with($with);
  33. return $this;
  34. }
  35. /**
  36. * 查询上架的商品,包含隐藏商品
  37. *
  38. * @return self
  39. */
  40. public function show()
  41. {
  42. $this->md5s[] = 'show';
  43. $this->query = $this->query->whereIn('status', ['up', 'hidden']);
  44. return $this;
  45. }
  46. /**
  47. * 查询上架的商品,不包含隐藏商品
  48. *
  49. * @return self
  50. */
  51. public function up()
  52. {
  53. $this->md5s[] = 'up';
  54. $this->query = $this->query->where('status', 'up');
  55. return $this;
  56. }
  57. /**
  58. * 是否要处理活动相关数据
  59. *
  60. * @return self
  61. */
  62. public function activity($activity = false)
  63. {
  64. $this->is_activity = $activity ? true : false;
  65. return $this;
  66. }
  67. /**
  68. * 查询积分商城商品
  69. *
  70. * @return self
  71. */
  72. public function score()
  73. {
  74. $this->show_score_shop = true;
  75. // 获取所有积分商品
  76. $scoreGoodsIds = ScoreSkuPrice::where('status', 'up')->group('goods_id')->cache(60)->column('goods_id');
  77. $this->query = $this->query->whereIn('id', $scoreGoodsIds);
  78. return $this;
  79. }
  80. /**
  81. * 连表查询 库存,暂时没用
  82. *
  83. * @return self
  84. */
  85. public function stock()
  86. {
  87. $this->md5s[] = 'stock';
  88. $skuSql = SkuPrice::field('sum(stock) as stock, goods_id as sku_goods_id')->group('goods_id')->buildSql();
  89. $this->query = $this->query->join([$skuSql => 'sp'], 'id = sp.sku_goods_id', 'left');
  90. return $this;
  91. }
  92. /**
  93. * 搜索商品
  94. *
  95. * @return self
  96. */
  97. public function search($keyword)
  98. {
  99. $keyword = is_string($keyword) ? $keyword : json_encode($keyword); // 转字符串
  100. $this->md5s[] = 'search:' . $keyword; // 待补充
  101. // 添加搜索记录
  102. $searchHistory = new SearchHistory();
  103. $searchHistory->save(['keyword' => $keyword]);
  104. $this->query = $this->query->where('title|subtitle', 'like', '%' . $keyword . '%');
  105. return $this;
  106. }
  107. /**
  108. * 根据 ids 获取商品
  109. *
  110. * @param string|array $ids ids 数组或者 , 隔开的字符串
  111. * @return self
  112. */
  113. public function whereIds($ids = '')
  114. {
  115. $ids = is_array($ids) ? join(',', $ids) : $ids;
  116. $this->md5s[] = 'ids:' . $ids;
  117. if ($ids) {
  118. $this->query = $this->query->orderRaw('field(id, ' . $ids . ')'); // 按照 ids 里面的 id 进行排序
  119. $this->query = $this->query->whereIn('id', $ids);
  120. }
  121. return $this;
  122. }
  123. /**
  124. * 根据商品分类获取商品
  125. *
  126. * @param integer $category_id
  127. * @return self
  128. */
  129. public function category($category_id, $is_category_deep = true)
  130. {
  131. $this->md5s[] = 'category_id:' . $category_id;
  132. $category_ids = [];
  133. if (isset($category_id) && $category_id != 0) {
  134. if ($is_category_deep) {
  135. // 查询分类所有子分类,包括自己
  136. $category_ids = (new Tree(function () {
  137. // 组装搜索条件,排序等
  138. return (new Category)->normal();
  139. }))->getChildIds($category_id);
  140. } else {
  141. $category_ids = [$category_id];
  142. }
  143. }
  144. $this->query->where(function ($query) use ($category_ids) {
  145. // 所有子分类使用 find_in_set or 匹配,亲测速度并不慢
  146. foreach ($category_ids as $key => $category_id) {
  147. $query->whereOrRaw("find_in_set($category_id, category_ids)");
  148. }
  149. });
  150. return $this;
  151. }
  152. /**
  153. * 排序,方法参数同 thinkphp order 方法
  154. *
  155. * @param string $sort
  156. * @param string $order
  157. * @return self
  158. */
  159. public function order($sort = 'weigh', $order = 'desc')
  160. {
  161. $sort = $sort == 'price' ? Db::raw('convert(`price`, DECIMAL(10, 2)) ' . $order) : $sort;
  162. $this->query->order($sort, $order);
  163. return $this;
  164. }
  165. /**
  166. * 获取商品列表
  167. *
  168. * @param bool $is_cache 是否缓存
  169. * @return array|\think\model\Collection
  170. */
  171. public function select($is_cache = false)
  172. {
  173. $this->md5s[] = 'select';
  174. // 默认排序
  175. $this->order();
  176. $goods = $this->query->field('*,(sales + show_sales) as total_sales')
  177. // ->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))
  178. ->select();
  179. // 格式化数据
  180. foreach ($goods as $key => $gd) {
  181. $gd = $this->defaultFormat($gd);
  182. if ($this->format instanceof \Closure) {
  183. $gd = ($this->format)($gd, $this);
  184. }
  185. $goods[$key] = $gd;
  186. }
  187. return $goods;
  188. }
  189. public function select_autopage($is_cache = false)
  190. {
  191. $this->md5s[] = 'select';
  192. // 默认排序
  193. $this->order();
  194. $goods = $this->query->field('id,image,title,price,price as pricemin,is_sku,status,type,dispatch_type')
  195. // ->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))
  196. ->autopage()
  197. ->select();
  198. // 格式化数据
  199. /*foreach ($goods as $key => $gd) {
  200. $gd = $this->defaultFormat($gd);
  201. if ($this->format instanceof \Closure) {
  202. $gd = ($this->format)($gd, $this);
  203. }
  204. $goods[$key] = $gd;
  205. }*/
  206. return $goods;
  207. }
  208. /**
  209. * 获取商品列表
  210. *
  211. * @param bool $is_cache 是否缓存
  212. * @return \think\Paginator
  213. */
  214. public function paginate($is_cache = false)
  215. {
  216. $this->md5s[] = 'paginate';
  217. // 默认排序
  218. $this->order();
  219. $goods = $this->query->field('*,(sales + show_sales) as total_sales')
  220. // ->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))
  221. ->paginate(request()->param('list_rows', 10));
  222. // 格式化数据
  223. $goods->each(function($god) {
  224. $god = $this->defaultFormat($god);
  225. if ($this->format instanceof \Closure) {
  226. ($this->format)($god, $this);
  227. }
  228. });
  229. return $goods;
  230. }
  231. /**
  232. * 获取单个商品
  233. *
  234. * @param bool $is_cache
  235. * @return Goods
  236. */
  237. public function find($is_cache = false)
  238. {
  239. $this->md5s[] = 'find';
  240. $goods = $this->query
  241. // ->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))
  242. ->find();
  243. if ($goods && $this->format instanceof \Closure) {
  244. // 格式化数据
  245. $goods = $this->defaultFormat($goods);
  246. ($this->format)($goods, $this);
  247. }
  248. return $goods;
  249. }
  250. /**
  251. * 获取单个商品,找不到抛出异常
  252. *
  253. * @param bool $is_cache
  254. * @return Goods
  255. */
  256. public function findOrFail($is_cache = false)
  257. {
  258. $this->md5s[] = 'find';
  259. $goods = $this->query->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))->find();
  260. if (!$goods) {
  261. error_stop('商品不存在');
  262. }
  263. if ($this->format instanceof \Closure) {
  264. // 格式化数据
  265. $goods = $this->defaultFormat($goods);
  266. ($this->format)($goods, $this);
  267. }
  268. return $goods;
  269. }
  270. /**
  271. * 把活动相关数据覆盖到商品
  272. *
  273. * @param array|object $goods
  274. * @return array
  275. */
  276. public function defaultFormat($goods)
  277. {
  278. $skuPrices = $goods->sku_prices; //第一次调用 model/goods 里的 getSkuPricesAttr
  279. $activity = $this->is_activity ? $goods->activity : null; //第一次调用 model/goods 里的 getActivityAttr
  280. if ($activity) {
  281. $skuPrices = ActivityFacade::recoverSkuPrices($goods, $activity);
  282. // unset($goods['activity']['activity_sku_prices']); // db 获取活动这里删除报错
  283. }else{
  284. foreach ($skuPrices as $key => $skuPrice) {
  285. $skuPrice->old_price = $skuPrice['price'];
  286. }
  287. unset($skuPrice);
  288. }
  289. if ($this->show_score_shop) {
  290. $scoreSkuPrices = $goods->all_score_sku_prices; // 包含下架的积分规格
  291. // 积分商城,覆盖积分商城规格
  292. foreach ($skuPrices as $key => &$skuPrice) {
  293. $stock = $skuPrice->stock; // 下面要用
  294. $skuPrice->stock = 0;
  295. $skuPrice->sales = 0;
  296. foreach ($scoreSkuPrices as $scoreSkuPrice) {
  297. if ($skuPrice->id == $scoreSkuPrice->goods_sku_price_id) {
  298. $skuPrice->stock = ($scoreSkuPrice->stock > $stock) ? $stock : $scoreSkuPrice->stock; // 积分商城库存不能超过商品库存
  299. $skuPrice->sales = $scoreSkuPrice->sales;
  300. $skuPrice->price = $scoreSkuPrice->price;
  301. $skuPrice->score = $scoreSkuPrice->score;
  302. $skuPrice->status = $scoreSkuPrice->status; // 采用积分的上下架
  303. // $skuPrice->score_price = $scoreSkuPrice->score_price;
  304. // 记录对应活动的规格的记录
  305. $skuPrice->item_goods_sku_price = $scoreSkuPrice;
  306. break;
  307. }
  308. }
  309. }
  310. }
  311. // 移除下架的规格
  312. foreach ($skuPrices as $key => $skuPrice) {
  313. if ($skuPrice['status'] != 'up') {
  314. unset($skuPrices[$key]);
  315. }
  316. }
  317. $skuPrices = $skuPrices instanceof \think\Collection ? $skuPrices->values() : array_values($skuPrices);
  318. if ($activity) {
  319. // 处理活动相关的价格,销量等
  320. // 这里由 getPriceAttr 计算,这里后续删除
  321. // $prices = $skuPrices instanceof \think\Collection ? $skuPrices->column('price') : array_column($skuPrices, 'price');
  322. // $goods['price'] = $prices ? min($prices) : $goods['price']; // min 里面不能是空数组
  323. // if ($activity['type'] == 'groupon') {
  324. // $grouponPrices = $skuPrices instanceof \think\Collection ? $skuPrices->column('groupon_price') : array_column($skuPrices, 'groupon_price');
  325. // $goods['groupon_price'] = $grouponPrices ? min($grouponPrices) : $goods['price'];
  326. // }
  327. // if ($activity['type'] == 'groupon_ladder') {
  328. // // @sn 阶梯拼团,商品详情如何显示拼团价格,阶梯拼团
  329. // }
  330. // if ($activity['rules'] && isset($activity['rules']['sales_show_type']) && $activity['rules']['sales_show_type'] == 'real') {
  331. // // 活动设置显示真实销量
  332. // $goods['sales'] = array_sum($skuPrices instanceof \think\Collection ? $skuPrices->column('sales') : array_column($skuPrices, 'sales'));
  333. // } else {
  334. // // 活动显示总销量
  335. // $goods['sales'] += $goods['show_sales'];
  336. // }
  337. } elseif ($this->show_score_shop) {
  338. // 积分商城这里显示的是真实销量, 目前都由 getSalesAttr 计算
  339. // $goods['sales'] = array_sum($skuPrices instanceof \think\Collection ? $skuPrices->column('sales') : array_column($skuPrices, 'sales'));
  340. } else {
  341. // 没有活动,商品销量,加上虚增销量
  342. // $goods['sales'] += $goods['show_sales'];
  343. }
  344. if ($this->show_score_shop) {
  345. // 积分商城商品
  346. $goods['show_score_shop'] = $this->show_score_shop;
  347. }
  348. // 不给 sku_prices 赋值,会触发 getSkuPricesAttr 计算属性,覆盖计算好的 sku_prices| 但是这样 sku_prices 会存在下标不连续的情况
  349. $goods['new_sku_prices'] = $skuPrices; // 商品详情接口用, 过滤掉了 下架的规格
  350. // $goods['sales'] = $goods->salesStockFormat($goods['sales'], $goods['sales_show_type']); // 格式化销量,前端格式化,这里可删除
  351. $stocks = $skuPrices instanceof \think\Collection ? $skuPrices->column('stock') : array_column($skuPrices, 'stock'); // 获取规格中的库存
  352. $stock = array_sum($stocks);
  353. $goods['stock'] = $stock;
  354. // $goods['stock'] = $goods->salesStockFormat($stock, $goods['stock_show_type']); // 格式化库存,前端格式化,这里可删除
  355. $goods['activity_type'] = $activity['type'] ?? null;
  356. return $goods;
  357. }
  358. /**
  359. * 获取缓存 key
  360. *
  361. * @param bool $is_cache 是否缓存
  362. * @return string|bool
  363. */
  364. protected function getCacheKey($is_cache = false)
  365. {
  366. if ($is_cache) {
  367. sort($this->md5s);
  368. $key = 'goods-service-' . md5(json_encode($this->md5s));
  369. }
  370. return $key ?? false;
  371. }
  372. /**
  373. * 默认调用 query 中的方法,比如 withTrashed()
  374. *
  375. * @param [type] $funcname
  376. * @param [type] $arguments
  377. * @return void
  378. */
  379. public function __call($funcname, $arguments)
  380. {
  381. $this->query->{$funcname}(...$arguments);
  382. return $this;
  383. }
  384. }