StockSale.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <?php
  2. namespace addons\shopro\service;
  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\activity\SkuPrice as ActivitySkuPriceModel;
  7. use app\admin\model\shopro\order\OrderItem;
  8. use addons\shopro\facade\ActivityRedis as ActivityRedisFacade;
  9. use addons\shopro\facade\Redis;
  10. use addons\shopro\traits\StockWarning;
  11. /**
  12. * 库存销量
  13. */
  14. class StockSale
  15. {
  16. use StockWarning;
  17. /**
  18. * 下单锁定库存
  19. *
  20. * @param array $buyInfo
  21. * @return void
  22. */
  23. public function stockLock($buyInfo)
  24. {
  25. $goods = $buyInfo['goods'];
  26. // 普通商品还是积分商品
  27. $order_type = request()->param('order_type', '');
  28. $order_type = $order_type ?: 'goods';
  29. $buy_num = $buyInfo['goods_num'];
  30. // 记录缓存的 hash key
  31. $keyCacheStockHash = $this->getCacheStockHashKey();
  32. if ($order_type == 'score') {
  33. // 普通商品销量处理 (积分和普通商品的缓存 key 不相同)
  34. $keyScoreGoodsLockedNum = $this->getLockedGoodsKey($buyInfo['goods_id'], $buyInfo['goods_sku_price_id'], 'score');
  35. $score_locked_num = redis_cache($keyScoreGoodsLockedNum);
  36. // 验证积分商品库存是否充足(mysql 悲观锁,此方法可靠,但如果大规模秒杀,容易mysql 死锁,请将商品添加为秒杀)
  37. $stock = ScoreSkuPrice::where('goods_id', $buyInfo['goods_id'])->where('goods_sku_price_id', $buyInfo['current_sku_price']->id)->lock(true)->value('stock');
  38. if (($stock - $score_locked_num) < $buy_num) {
  39. error_stop('积分商品库存不足');
  40. }
  41. // 锁积分库存
  42. redis_cache()->INCRBY($keyScoreGoodsLockedNum, $buy_num);
  43. // 记录已经缓存的库存的key,如果下单出现异常,将所有锁定的库存退回
  44. redis_cache()->HSET($keyCacheStockHash, $keyScoreGoodsLockedNum, $buy_num);
  45. }
  46. // 普通商品销量处理 (积分和普通商品的缓存 key 不相同)
  47. $keyGoodsLockedNum = $this->getLockedGoodsKey($buyInfo['goods_id'], $buyInfo['goods_sku_price_id'], 'goods');
  48. $locked_num = redis_cache($keyGoodsLockedNum);
  49. // 验证商品库存是否充足(mysql 悲观锁,此方法可靠,但如果大规模秒杀,容易mysql 死锁,请将商品添加为秒杀)
  50. $stock = SkuPrice::where('id', $buyInfo['current_sku_price']->id)->lock(true)->value('stock');
  51. if (($stock - $locked_num) < $buy_num) {
  52. error_stop('商品库存不足');
  53. }
  54. // 锁库存
  55. redis_cache()->INCRBY($keyGoodsLockedNum, $buy_num);
  56. // 记录已经缓存的库存的key,如果下单出现异常,将所有锁定的库存退回
  57. redis_cache()->HSET($keyCacheStockHash, $keyGoodsLockedNum, $buy_num);
  58. }
  59. /**
  60. * 下单中断释放锁定的库存
  61. *
  62. * @return void
  63. */
  64. public function faildStockUnLock()
  65. {
  66. // 记录缓存的 hash key
  67. $keyCacheStockHash = $this->getCacheStockHashKey();
  68. $cacheStocks = redis_cache()->HGETALL($keyCacheStockHash);
  69. foreach ($cacheStocks as $key => $num) {
  70. $this->unLockCache($key, $num);
  71. }
  72. redis_cache()->delete($keyCacheStockHash);
  73. }
  74. /**
  75. * 下单成功,删除锁定库存标记
  76. *
  77. * @return void
  78. */
  79. public function successDelHashKey()
  80. {
  81. // 记录缓存的 hash key
  82. $keyCacheStockHash = $this->getCacheStockHashKey();
  83. redis_cache()->delete($keyCacheStockHash);
  84. }
  85. /**
  86. * 库存解锁
  87. */
  88. public function stockUnLock($order)
  89. {
  90. $items = $order->items;
  91. foreach ($items as $key => $item) {
  92. $this->stockUnLockItem($order, $item);
  93. }
  94. }
  95. public function stockUnLockItem($order, $item)
  96. {
  97. if ($order['type'] == 'score') {
  98. $keyScoreGoodsLockedNum = $this->getLockedGoodsKey($item['goods_id'], $item['goods_sku_price_id'], 'score');
  99. $this->unLockCache($keyScoreGoodsLockedNum, $item->goods_num);
  100. }
  101. $keyGoodsLockedNum = $this->getLockedGoodsKey($item['goods_id'], $item['goods_sku_price_id'], 'goods');
  102. $this->unLockCache($keyGoodsLockedNum, $item->goods_num);
  103. }
  104. private function unLockCache($key, $num)
  105. {
  106. $locked_num = redis_cache()->DECRBY($key, $num);
  107. if ($locked_num < 0) {
  108. $locked_num = redis_cache()->set($key, 0);
  109. }
  110. }
  111. // 真实正向 减库存加销量(支付成功扣库存,加销量)
  112. public function forwardStockSale($order) {
  113. $items = OrderItem::where('order_id', $order['id'])->select();
  114. $result = [
  115. 'goods_num_sum' => 0,
  116. ];
  117. foreach ($items as $key => $item) {
  118. // 增加商品销量
  119. Goods::where('id', $item->goods_id)->setInc('sales', $item->goods_num);
  120. $result['goods_num_sum'] += $item->goods_num;
  121. $skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
  122. if ($skuPrice) {
  123. SkuPrice::where('id', $item->goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
  124. SkuPrice::where('id', $item->goods_sku_price_id)->setInc('sales', $item->goods_num); // 增加销量
  125. // 库存预警检测
  126. $this->checkStockWarning($skuPrice);
  127. }
  128. if ($item->item_goods_sku_price_id) {
  129. if ($order['type'] == 'score') {
  130. // 积分商城商品,扣除积分规格库存
  131. ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
  132. ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setInc('sales', $item->goods_num);
  133. } else {
  134. // 扣除活动库存
  135. ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
  136. ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setInc('sales', $item->goods_num);
  137. }
  138. }
  139. // 真实库存已减,库存解锁(非活动)
  140. if (!$item['activity_id']) {
  141. $this->stockUnLockItem($order, $item);
  142. }
  143. }
  144. return $result;
  145. }
  146. // 真实反向 加库存减销量(订单退全款)
  147. public function backStockSale($order, $items = [])
  148. {
  149. if (!$items) {
  150. $items = OrderItem::where('order_id', $order['id'])->select();
  151. }
  152. foreach ($items as $key => $item) {
  153. // 返还商品销量
  154. Goods::where('id', $item->goods_id)->setDec('sales', $item->goods_num);
  155. // 返还规格库存
  156. $skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
  157. if ($skuPrice) {
  158. SkuPrice::where('id', $item->goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
  159. SkuPrice::where('id', $item->goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
  160. // 库存预警检测
  161. $this->checkStockWarning($skuPrice);
  162. }
  163. //活动规格|积分商城规格
  164. if ($item->item_goods_sku_price_id) {
  165. if ($order['type'] == 'score') {
  166. // 积分商城商品,扣除积分规格库存
  167. ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
  168. ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
  169. } else {
  170. // 扣除活动库存
  171. ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
  172. ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
  173. }
  174. }
  175. }
  176. }
  177. // cache 正向加销量,添加订单之前拦截
  178. public function cacheForwardSale($buyInfo) {
  179. $goods = $buyInfo['goods'];
  180. $activity = $goods['activity'];
  181. if (has_redis()) {
  182. $keys = ActivityRedisFacade::keysActivity([
  183. 'goods_id' => $goods->id,
  184. 'goods_sku_price_id' => $buyInfo['current_sku_price']->id,
  185. ], [
  186. 'activity_id' => $activity['id'],
  187. 'activity_type' => $activity['type'],
  188. ]);
  189. extract($keys);
  190. // 活动商品规格
  191. $goodsSkuPrice = Redis::HGET($keyActivity, $keyGoodsSkuPrice);
  192. $goodsSkuPrice = json_decode($goodsSkuPrice, true);
  193. // 活动商品库存
  194. $stock = $goodsSkuPrice['stock'] ?? 0;
  195. // 当前销量 + 购买数量 ,salekey 如果不存在,自动创建
  196. $sale = Redis::HINCRBY($keyActivity, $keySale, $buyInfo['goods_num']);
  197. if ($sale > $stock) {
  198. $sale = Redis::HINCRBY($keyActivity, $keySale, -$buyInfo['goods_num']);
  199. error_stop('活动商品库存不足');
  200. }
  201. }
  202. }
  203. // cache 反向减销量,取消订单/订单自动关闭 时候
  204. public function cacheBackSale($order) {
  205. $items = OrderItem::where('order_id', $order['id'])->select();
  206. foreach ($items as $key => $item) {
  207. $this->cacheBackSaleByItem($item);
  208. }
  209. }
  210. // 通过 OrderItem 减预库存
  211. private function cacheBackSaleByItem($item)
  212. {
  213. if (has_redis()) {
  214. $keys = ActivityRedisFacade::keysActivity([
  215. 'goods_id' => $item['goods_id'],
  216. 'goods_sku_price_id' => $item['goods_sku_price_id'],
  217. ], [
  218. 'activity_id' => $item['activity_id'],
  219. 'activity_type' => $item['activity_type'],
  220. ]);
  221. extract($keys);
  222. if (Redis::EXISTS($keyActivity) && Redis::HEXISTS($keyActivity, $keySale)) {
  223. $sale = Redis::HINCRBY($keyActivity, $keySale, -$item['goods_num']);
  224. }
  225. return true;
  226. }
  227. }
  228. /**
  229. * 获取库存锁定 key
  230. *
  231. * @param int $goods_id
  232. * @param int $goods_sku_price_id
  233. * @return string
  234. */
  235. private function getLockedGoodsKey($goods_id, $goods_sku_price_id, $order_type = 'goods')
  236. {
  237. $prefix = 'locked_goods_num:' . $order_type . ':' . $goods_id . ':' . $goods_sku_price_id;
  238. return $prefix;
  239. }
  240. private function getCacheStockHashKey()
  241. {
  242. $params = request()->param();
  243. $goodsList = $params['goods_list'] ?? [];
  244. $key = client_unique() . ':' . json_encode($goodsList);
  245. return md5($key);
  246. }
  247. }