和蕙健康小程序后端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
11 KiB

9 months ago
<?php
namespace addons\shopro\service;
use app\admin\model\shopro\goods\Goods;
use app\admin\model\shopro\goods\SkuPrice;
use app\admin\model\shopro\app\ScoreSkuPrice;
use app\admin\model\shopro\activity\SkuPrice as ActivitySkuPriceModel;
use app\admin\model\shopro\order\OrderItem;
use addons\shopro\facade\ActivityRedis as ActivityRedisFacade;
use addons\shopro\facade\Redis;
use addons\shopro\traits\StockWarning;
/**
* 库存销量
*/
class StockSale
{
use StockWarning;
/**
* 下单锁定库存
*
* @param array $buyInfo
* @return void
*/
public function stockLock($buyInfo)
{
$goods = $buyInfo['goods'];
// 普通商品还是积分商品
$order_type = request()->param('order_type', '');
$order_type = $order_type ?: 'goods';
$buy_num = $buyInfo['goods_num'];
// 记录缓存的 hash key
$keyCacheStockHash = $this->getCacheStockHashKey();
if ($order_type == 'score') {
// 普通商品销量处理 (积分和普通商品的缓存 key 不相同)
$keyScoreGoodsLockedNum = $this->getLockedGoodsKey($buyInfo['goods_id'], $buyInfo['goods_sku_price_id'], 'score');
$score_locked_num = redis_cache($keyScoreGoodsLockedNum);
// 验证积分商品库存是否充足(mysql 悲观锁,此方法可靠,但如果大规模秒杀,容易mysql 死锁,请将商品添加为秒杀)
$stock = ScoreSkuPrice::where('goods_id', $buyInfo['goods_id'])->where('goods_sku_price_id', $buyInfo['current_sku_price']->id)->lock(true)->value('stock');
if (($stock - $score_locked_num) < $buy_num) {
error_stop('积分商品库存不足');
}
// 锁积分库存
redis_cache()->INCRBY($keyScoreGoodsLockedNum, $buy_num);
// 记录已经缓存的库存的key,如果下单出现异常,将所有锁定的库存退回
redis_cache()->HSET($keyCacheStockHash, $keyScoreGoodsLockedNum, $buy_num);
}
// 普通商品销量处理 (积分和普通商品的缓存 key 不相同)
$keyGoodsLockedNum = $this->getLockedGoodsKey($buyInfo['goods_id'], $buyInfo['goods_sku_price_id'], 'goods');
$locked_num = redis_cache($keyGoodsLockedNum);
// 验证商品库存是否充足(mysql 悲观锁,此方法可靠,但如果大规模秒杀,容易mysql 死锁,请将商品添加为秒杀)
$stock = SkuPrice::where('id', $buyInfo['current_sku_price']->id)->lock(true)->value('stock');
if (($stock - $locked_num) < $buy_num) {
error_stop('商品库存不足');
}
// 锁库存
redis_cache()->INCRBY($keyGoodsLockedNum, $buy_num);
// 记录已经缓存的库存的key,如果下单出现异常,将所有锁定的库存退回
redis_cache()->HSET($keyCacheStockHash, $keyGoodsLockedNum, $buy_num);
}
/**
* 下单中断释放锁定的库存
*
* @return void
*/
public function faildStockUnLock()
{
// 记录缓存的 hash key
$keyCacheStockHash = $this->getCacheStockHashKey();
$cacheStocks = redis_cache()->HGETALL($keyCacheStockHash);
foreach ($cacheStocks as $key => $num) {
$this->unLockCache($key, $num);
}
redis_cache()->delete($keyCacheStockHash);
}
/**
* 下单成功,删除锁定库存标记
*
* @return void
*/
public function successDelHashKey()
{
// 记录缓存的 hash key
$keyCacheStockHash = $this->getCacheStockHashKey();
redis_cache()->delete($keyCacheStockHash);
}
/**
* 库存解锁
*/
public function stockUnLock($order)
{
$items = $order->items;
foreach ($items as $key => $item) {
$this->stockUnLockItem($order, $item);
}
}
public function stockUnLockItem($order, $item)
{
if ($order['type'] == 'score') {
$keyScoreGoodsLockedNum = $this->getLockedGoodsKey($item['goods_id'], $item['goods_sku_price_id'], 'score');
$this->unLockCache($keyScoreGoodsLockedNum, $item->goods_num);
}
$keyGoodsLockedNum = $this->getLockedGoodsKey($item['goods_id'], $item['goods_sku_price_id'], 'goods');
$this->unLockCache($keyGoodsLockedNum, $item->goods_num);
}
private function unLockCache($key, $num)
{
$locked_num = redis_cache()->DECRBY($key, $num);
if ($locked_num < 0) {
$locked_num = redis_cache()->set($key, 0);
}
}
// 真实正向 减库存加销量(支付成功扣库存,加销量)
public function forwardStockSale($order) {
$items = OrderItem::where('order_id', $order['id'])->select();
foreach ($items as $key => $item) {
// 增加商品销量
Goods::where('id', $item->goods_id)->setInc('sales', $item->goods_num);
$skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
if ($skuPrice) {
SkuPrice::where('id', $item->goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
SkuPrice::where('id', $item->goods_sku_price_id)->setInc('sales', $item->goods_num); // 增加销量
// 库存预警检测
$this->checkStockWarning($skuPrice);
}
if ($item->item_goods_sku_price_id) {
if ($order['type'] == 'score') {
// 积分商城商品,扣除积分规格库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setInc('sales', $item->goods_num);
} else {
// 扣除活动库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setInc('sales', $item->goods_num);
}
}
// 真实库存已减,库存解锁(非活动)
if (!$item['activity_id']) {
$this->stockUnLockItem($order, $item);
}
}
}
// 真实反向 加库存减销量(订单退全款)
public function backStockSale($order, $items = [])
{
if (!$items) {
$items = OrderItem::where('order_id', $order['id'])->select();
}
foreach ($items as $key => $item) {
// 返还商品销量
Goods::where('id', $item->goods_id)->setDec('sales', $item->goods_num);
// 返还规格库存
$skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
if ($skuPrice) {
SkuPrice::where('id', $item->goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
SkuPrice::where('id', $item->goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
// 库存预警检测
$this->checkStockWarning($skuPrice);
}
if ($item->item_goods_sku_price_id) {
if ($order['type'] == 'score') {
// 积分商城商品,扣除积分规格库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
} else {
// 扣除活动库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
}
}
}
}
// cache 正向加销量,添加订单之前拦截
public function cacheForwardSale($buyInfo) {
$goods = $buyInfo['goods'];
$activity = $goods['activity'];
if (has_redis()) {
$keys = ActivityRedisFacade::keysActivity([
'goods_id' => $goods->id,
'goods_sku_price_id' => $buyInfo['current_sku_price']->id,
], [
'activity_id' => $activity['id'],
'activity_type' => $activity['type'],
]);
extract($keys);
// 活动商品规格
$goodsSkuPrice = Redis::HGET($keyActivity, $keyGoodsSkuPrice);
$goodsSkuPrice = json_decode($goodsSkuPrice, true);
// 活动商品库存
$stock = $goodsSkuPrice['stock'] ?? 0;
// 当前销量 + 购买数量 ,salekey 如果不存在,自动创建
$sale = Redis::HINCRBY($keyActivity, $keySale, $buyInfo['goods_num']);
if ($sale > $stock) {
$sale = Redis::HINCRBY($keyActivity, $keySale, -$buyInfo['goods_num']);
error_stop('活动商品库存不足');
}
}
}
// cache 反向减销量,取消订单/订单自动关闭 时候
public function cacheBackSale($order) {
$items = OrderItem::where('order_id', $order['id'])->select();
foreach ($items as $key => $item) {
$this->cacheBackSaleByItem($item);
}
}
// 通过 OrderItem 减预库存
private function cacheBackSaleByItem($item)
{
if (has_redis()) {
$keys = ActivityRedisFacade::keysActivity([
'goods_id' => $item['goods_id'],
'goods_sku_price_id' => $item['goods_sku_price_id'],
], [
'activity_id' => $item['activity_id'],
'activity_type' => $item['activity_type'],
]);
extract($keys);
if (Redis::EXISTS($keyActivity) && Redis::HEXISTS($keyActivity, $keySale)) {
$sale = Redis::HINCRBY($keyActivity, $keySale, -$item['goods_num']);
}
return true;
}
}
/**
* 获取库存锁定 key
*
* @param int $goods_id
* @param int $goods_sku_price_id
* @return string
*/
private function getLockedGoodsKey($goods_id, $goods_sku_price_id, $order_type = 'goods')
{
$prefix = 'locked_goods_num:' . $order_type . ':' . $goods_id . ':' . $goods_sku_price_id;
return $prefix;
}
private function getCacheStockHashKey()
{
$params = request()->param();
$goodsList = $params['goods_list'] ?? [];
$key = client_unique() . ':' . json_encode($goodsList);
return md5($key);
}
}