Преглед изворни кода

fix(order): 完善库存与秒杀超限错误反馈

- 按实际购买数量校验花材、花束及秒杀剩余库存并返回超出数量
- 统一秒杀限购错误数据,支持前端定位并高亮对应商品

API: 下单库存或秒杀限购超限时返回 code=-1 及商品标识、限制值和超出数量
shizhongqi пре 3 дана
родитељ
комит
09ec7728a6

+ 109 - 11
app-mall/controllers/OrderController.php

@@ -613,11 +613,20 @@ class OrderController extends BaseController
             util::fail('请选择规格');
         }
         $stock = $goodsInfo->stock ?? 0;
-        if ($stock <= 0) {
-            util::fail('库存不足');
-        }
         $saleGoodsId = $specGoodsId > 0 ? $specGoodsId : $goodsId;
         $activityType = strval($post['activityType'] ?? '');
+        if (bccomp($goodsNum, floatval($stock), 2) > 0) {
+            // 库存不足返回商品标识,供结算页红框高亮(与限购超限同形态)
+            $this->failStockExceed([
+                'productId' => $saleGoodsId,
+                'goodsId' => $goodsId,
+                'specGoodsId' => $specGoodsId,
+                'property' => 0,
+                'activityType' => $activityType,
+                'stock' => $stock,
+                'num' => $goodsNum,
+            ]);
+        }
 
         //事务处理
         $connection = Yii::$app->db;
@@ -669,14 +678,27 @@ class OrderController extends BaseController
                 }
                 $seckillStock = floatval($seckillRow['stock'] ?? 0);
                 $soldCount = HomePageModuleClass::getSeckillSoldCount($mainId, $seckillActivityGoodsId);
-                if (bcadd($soldCount, $goodsNum, 2) > $seckillStock) {
-                    util::fail('秒杀库存不足,请重新选择');
+                $seckillRemain = bcsub($seckillStock, $soldCount, 2);
+                if (bccomp($seckillRemain, '0', 2) < 0) {
+                    $seckillRemain = '0';
+                }
+                if (bccomp($goodsNum, $seckillRemain, 2) > 0) {
+                    $this->failStockExceed([
+                        'productId' => $saleGoodsId,
+                        'goodsId' => $goodsId,
+                        'specGoodsId' => $specGoodsId,
+                        'property' => 0,
+                        'activityType' => 'seckill',
+                        'stock' => $seckillRemain,
+                        'num' => $goodsNum,
+                    ]);
                 }
                 $seckillLimit = floatval($seckillRow['limit'] ?? 0);
                 if ($seckillLimit > 0) {
                     $boughtCount = HomePageModuleClass::getSeckillCustomBoughtCount($mainId, $seckillActivityGoodsId, $customId);
                     if (bcadd($boughtCount, $goodsNum, 2) > $seckillLimit) {
-                        util::fail("秒杀商品每人限购{$seckillLimit}件,已超出可购买数量");
+                        // 返回超限商品,供结算页红框高亮;code=-1 与花材限购错误对齐
+                        HomePageModuleClass::failSeckillLimitExceed($saleGoodsId, $goodsId, $specGoodsId, $seckillLimit, $boughtCount, $goodsNum);
                     }
                 }
             }
@@ -1062,6 +1084,19 @@ class OrderController extends BaseController
                         util::fail('有花材信息没有找到');
                     }
                     $num = $element['num'] ?? 0;
+                    $itemStock = floatval($product['stock'] ?? 0);
+                    if (bccomp(floatval($num), $itemStock, 2) > 0) {
+                        // 花材库存不足也返回 productId,确认页/购物车按限购同款红框提示超出量
+                        $this->failStockExceed([
+                            'productId' => $productId,
+                            'goodsId' => 0,
+                            'specGoodsId' => 0,
+                            'property' => 1,
+                            'activityType' => '',
+                            'stock' => $itemStock,
+                            'num' => $num,
+                        ]);
+                    }
                     $limitKey = \bizHd\product\classes\ProductClass::LIMIT_BUY_KEY . $productId;
                     $hasBuyNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $hdCustomId]);
                     $hasBuyNum = $num + ($hasBuyNum == null ? 0 : $hasBuyNum);
@@ -1138,8 +1173,16 @@ class OrderController extends BaseController
                     }
                     $stock = $goodsInfo->stock;
                     $stockSet = $goodsInfo->stockSet;
-                    if ($stockSet == 1 && $stock <= 0) {
-                        util::fail('花束库存不足');
+                    if ($stockSet == 1 && bccomp($goodsNum, floatval($stock), 2) > 0) {
+                        $this->failStockExceed([
+                            'productId' => $saleGoodsId,
+                            'goodsId' => $goodsId,
+                            'specGoodsId' => $specGoodsId,
+                            'property' => 0,
+                            'activityType' => strval($element['activityType'] ?? ''),
+                            'stock' => $stock,
+                            'num' => $goodsNum,
+                        ]);
                     }
 
                     // 秒杀商品:不信任前端传的价格,独立回查秒杀配置核实活动状态/库存/限购后再核价
@@ -1156,14 +1199,27 @@ class OrderController extends BaseController
                         }
                         $seckillStock = floatval($seckillRow['stock']);
                         $soldCount = HomePageModuleClass::getSeckillSoldCount($mainId, $seckillActivityGoodsId);
-                        if (bcadd($soldCount, $goodsNum, 2) > $seckillStock) {
-                            util::fail('秒杀库存不足,请重新选择');
+                        $seckillRemain = bcsub($seckillStock, $soldCount, 2);
+                        if (bccomp($seckillRemain, '0', 2) < 0) {
+                            $seckillRemain = '0';
+                        }
+                        if (bccomp($goodsNum, $seckillRemain, 2) > 0) {
+                            $this->failStockExceed([
+                                'productId' => $saleGoodsId,
+                                'goodsId' => $goodsId,
+                                'specGoodsId' => $specGoodsId,
+                                'property' => 0,
+                                'activityType' => 'seckill',
+                                'stock' => $seckillRemain,
+                                'num' => $goodsNum,
+                            ]);
                         }
                         $seckillLimit = floatval($seckillRow['limit'] ?? 0);
                         if ($seckillLimit > 0) {
                             $boughtCount = HomePageModuleClass::getSeckillCustomBoughtCount($mainId, $seckillActivityGoodsId, $hdCustomId); // TODO 同一个商品,如果缓存没有合理清除,就会一直限购
                             if (bcadd($boughtCount, $goodsNum, 2) > $seckillLimit) {
-                                util::fail("秒杀商品每人限购{$seckillLimit}件,已超出可购买数量");
+                                // 返回超限商品,供混合结算页/购物车红框高亮;code=-1 与花材限购错误对齐
+                                HomePageModuleClass::failSeckillLimitExceed($saleGoodsId, $goodsId, $specGoodsId, $seckillLimit, $boughtCount, $goodsNum);
                             }
                         }
                     }
@@ -2103,6 +2159,48 @@ class OrderController extends BaseController
         return true;
     }
 
+    /**
+     * 库存不足:以 code=-1 返回商品标识、剩余库存与超出数量。
+     * 与限购超限同形态,供混合结算页/购物车红框高亮对应商品并提示超出多少。
+     *
+     * @param array $opts productId/goodsId/specGoodsId/property/activityType/stock/num
+     */
+    protected function failStockExceed($opts)
+    {
+        $productId = intval($opts['productId'] ?? 0);
+        $goodsId = intval($opts['goodsId'] ?? 0);
+        $specGoodsId = intval($opts['specGoodsId'] ?? 0);
+        $property = intval($opts['property'] ?? 0);
+        $activityType = strval($opts['activityType'] ?? '');
+        $stock = floatval($opts['stock'] ?? 0);
+        $num = floatval($opts['num'] ?? 0);
+        if (bccomp($stock, '0', 2) < 0) {
+            $stock = 0;
+        }
+        $exceed = bcsub($num, $stock, 2);
+        if (bccomp($exceed, '0', 2) < 0) {
+            $exceed = '0';
+        }
+        $stockShow = rtrim(rtrim(sprintf('%.2f', $stock), '0'), '.');
+        if ($stockShow === '') {
+            $stockShow = '0';
+        }
+        $msg = $activityType === 'seckill'
+            ? "秒杀商品库存不足,剩余{$stockShow}"
+            : "商品库存不足,剩余{$stockShow}";
+        util::error(-1, $msg, [
+            'productId' => $productId,
+            'goodsId' => $goodsId,
+            'specGoodsId' => $specGoodsId,
+            'property' => $property,
+            'activityType' => $activityType,
+            'reason' => 'stock',
+            'stock' => floatval($stock),
+            'num' => $num,
+            'exceed' => floatval($exceed),
+        ]);
+    }
+
     //获取详情 ssh 20220512
     public function actionInfo()
     {

+ 54 - 0
biz-hd/homePageConfig/classes/HomePageModuleClass.php

@@ -375,6 +375,60 @@ class HomePageModuleClass
         return floatval($val ?? 0);
     }
 
+    /**
+     * 组装秒杀单人限购超限的返回 data,供结算页/购物车按商品高亮并展示超出件数。
+     * productId 与前端提交的花束 productId(规格 id 优先,否则主商品 id)对齐。
+     *
+     * @param int $saleGoodsId 实际售卖商品 id
+     * @param int $goodsId 主商品 id
+     * @param int $specGoodsId 规格 id
+     * @param float $seckillLimit 单人限购件数
+     * @param float $boughtCount 该客户已购件数
+     * @param float $goodsNum 本单购买件数
+     * @return array
+     */
+    public static function buildSeckillLimitExceedData($saleGoodsId, $goodsId, $specGoodsId, $seckillLimit, $boughtCount, $goodsNum)
+    {
+        $seckillLimit = floatval($seckillLimit);
+        $boughtCount = floatval($boughtCount);
+        $goodsNum = floatval($goodsNum);
+        $wantTotal = bcadd($boughtCount, $goodsNum, 2);
+        $exceed = bcsub($wantTotal, $seckillLimit, 2);
+        if (bccomp($exceed, '0', 2) < 0) {
+            $exceed = '0';
+        }
+        return [
+            'productId' => intval($saleGoodsId),
+            'goodsId' => intval($goodsId),
+            'specGoodsId' => intval($specGoodsId),
+            'property' => 0,
+            'activityType' => 'seckill',
+            'reason' => 'limit',
+            'limitBuy' => $seckillLimit,
+            'boughtCount' => $boughtCount,
+            'num' => $goodsNum,
+            'exceed' => floatval($exceed),
+        ];
+    }
+
+    /**
+     * 秒杀单人限购超限:以 code=-1 返回商品标识与超出数量,与花材 handleLimitBuy 的错误形态对齐。
+     * 前端据此红框高亮对应商品,而不是只弹一句笼统提示。
+     *
+     * @param int $saleGoodsId 实际售卖商品 id
+     * @param int $goodsId 主商品 id
+     * @param int $specGoodsId 规格 id
+     * @param float $seckillLimit 单人限购件数
+     * @param float $boughtCount 该客户已购件数
+     * @param float $goodsNum 本单购买件数
+     */
+    public static function failSeckillLimitExceed($saleGoodsId, $goodsId, $specGoodsId, $seckillLimit, $boughtCount, $goodsNum)
+    {
+        $data = self::buildSeckillLimitExceedData($saleGoodsId, $goodsId, $specGoodsId, $seckillLimit, $boughtCount, $goodsNum);
+        $limitShow = rtrim(rtrim(sprintf('%.2f', $data['limitBuy']), '0'), '.');
+        util::error(-1, "秒杀商品每人限购{$limitShow}件,已超出可购买数量", $data);
+    }
+
     /**
      * 预占秒杀名额:库存/限购校验通过后调用,累加"已售"与"该客户已购"计数,
      * 并记录本次请求的回滚快照——下单失败时通过 rollbackSeckillReservationSnapshot 撤销