Jelajahi Sumber

1. app-hd 添加新接口:item/clear-limit-buy 与 item/has-limit-buy-custom-list
2. 限购缓存设置与处理

shizhongqi 4 bulan lalu
induk
melakukan
38a5c446d8

+ 31 - 0
app-hd/controllers/ItemController.php

@@ -450,4 +450,35 @@ class ItemController extends BaseController
         util::complete('修改成功');
     }
 
+    //取消花材全部客户的已限数,重新开始计算
+    public function actionClearLimitBuy()
+    {
+        $get = Yii::$app->request->get();
+        $id = $get['id'] ?? '';
+        $item = ItemClass::getById($id, true);
+        if (empty($item)) {
+            util::fail('没有找到花材');
+        }
+        if ($item->mainId != $this->mainId) {
+            util::fail('不是你的花材');
+        }
+        ProductClass::cancelLimitBuy($id);
+        util::complete('清除成功');
+    }
+
+    //花材已购客户列表
+    public function actionHasLimitBuyCustomList()
+    {
+        $get = Yii::$app->request->get();
+        $id = $get['id'] ?? '';
+        $item = ItemClass::getById($id, true);
+        if (empty($item)) {
+            util::fail('没有找到花材');
+        }
+        if ($item->mainId != $this->mainId) {
+            util::fail('不是你的花材');
+        }
+        $customList = \bizGhs\product\classes\ProductClass::getHasLimitBuyList($item);
+        util::success(['customList' => $customList]);
+    }
 }

+ 35 - 3
app-hd/controllers/PurchaseController.php

@@ -144,6 +144,16 @@ class PurchaseController extends BaseController
 
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
+        $transactionFinished = false;
+        register_shutdown_function(function () use (&$transactionFinished, $transaction) {
+            if ($transactionFinished) {
+                return;
+            }
+            ProductClass::rollbackLimitBuySnapshot();
+            if ($transaction->isActive) {
+                $transaction->rollBack();
+            }
+        });
         try {
             $post['book'] = $post['book'] ?? 0;
             $post['sjId'] = $this->sjId;
@@ -201,9 +211,15 @@ class PurchaseController extends BaseController
             }
 
             $transaction->commit();
+            $transactionFinished = true;
+            ProductClass::clearLimitBuyRollbackSnapshot();
             util::success($respond);
-        } catch (Exception $e) {
-            $transaction->rollBack();
+        } catch (\Throwable $e) {
+            if ($transaction->isActive) {
+                $transaction->rollBack();
+            }
+            ProductClass::rollbackLimitBuySnapshot();
+            $transactionFinished = true;
             util::fail();
         }
     }
@@ -253,6 +269,16 @@ class PurchaseController extends BaseController
 
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
+        $transactionFinished = false;
+        register_shutdown_function(function () use (&$transactionFinished, $transaction) {
+            if ($transactionFinished) {
+                return;
+            }
+            ProductClass::rollbackLimitBuySnapshot();
+            if ($transaction->isActive) {
+                $transaction->rollBack();
+            }
+        });
         try {
             $post['book'] = $post['book'] ?? 0;
             $post['sjId'] = $this->sjId;
@@ -706,11 +732,17 @@ class PurchaseController extends BaseController
 
             }
             $transaction->commit();
+            $transactionFinished = true;
+            ProductClass::clearLimitBuyRollbackSnapshot();
 
             util::success($respond);
 
         } catch (Exception $e) {
-            $transaction->rollBack();
+            if ($transaction->isActive) {
+                $transaction->rollBack();
+            }
+            ProductClass::rollbackLimitBuySnapshot();
+            $transactionFinished = true;
             util::fail();
         }
     }

+ 1 - 1
biz-ghs/order/classes/OrderClass.php

@@ -876,7 +876,7 @@ class OrderClass extends BaseClass
         //默认订单要打单
         $data['needPrint'] = $data['needPrint'] ?? dict::getDict('needPrint', 'need');
 
-        $weight = $respond['weight'] ?? 0;
+        $weight = $respond['weight'];
         $data['weight'] = $weight;
         $itemPrice = $respond['price'] ?? 0;
         $data['itemPrice'] = $itemPrice;

+ 20 - 23
biz-ghs/order/classes/OrderItemClass.php

@@ -310,34 +310,32 @@ class OrderItemClass extends BaseClass
      * 替换订单(orderSn)下所有商品项为指定的 product 列表,并根据参数处理附加商详(如每支鲜花 xj)、校验及扣减库存、更新限购等。
      *
      * 步骤说明:
-     * 1. 根据 customId 获取客户等级和属性,格式化 product 信息;
-     * 2. 先删除该订单下历史商品项,再批量插入新 product 项;
+     * 1. 根据 customId 获取客户等级和属性,格式化 products 信息;
+     * 2. 先删除该订单下历史商品项,再批量插入新 products 项;
      * 3. 若包含 xj(每支鲜花)信息,批量关联并校验单支库存、同步写入;
      * 4. 处理每项商品的限购、本次扣减库存(如 stockMayChange 为 true),并记录出库流水。
      * 5. 返回本次批量插入及处理后的汇总结果(带总重量)。
      *
      * 用于订单商品整体替换的核心业务方法。
      */
-    public static function replaceItem($orderSn, $product, $post = null, $stockMayChange = false, $moreParams = [])
+    public static function replaceItem($orderSn, $products, $post = null, $stockMayChange = false, $moreParams = [])
     {
         $customId = $post['customId'] ?? 0;
         $custom = CustomClass::getById($customId, true);
         $level = $custom->level ?? 0;
         $live = isset($custom->live) ? $custom->live : 1;
-        $respond = ProductClass::formatProductInfo($product, $level, $live, $moreParams);
-        $product = $respond['product'];
+        $respond = ProductClass::formatProductInfo($products, $level, $live, $moreParams);
+        $products = $respond['product'];
         $weight = 0;
         $mainId = $post['mainId'] ?? 0;
         self::deleteByCondition(['orderSn' => $orderSn]);
-        foreach ($product as $key => $val) {
-            $val['orderSn'] = $orderSn;
-            $val['mainId'] = $mainId;
-            $item = self::add($val, true);
+        foreach ($products as $key => $p) {
+            $p['orderSn'] = $orderSn;
+            $p['mainId'] = $mainId;
+            $item = self::add($p, true);
 
-            $currentId = $item->id ?? 0;
             $currentPtItemId = $item->itemId ?? 0;
-
-            $unitPrice = $val['unitPrice'] ?? 0;
+            $unitPrice = $p['unitPrice'] ?? 0;
             if (isset($post['xj'][$currentPtItemId]) && !empty($post['xj'][$currentPtItemId])) {
                 $xjArr = $post['xj'][$currentPtItemId];
                 if (is_array($xjArr)) {
@@ -364,7 +362,7 @@ class OrderItemClass extends BaseClass
                         }
 
                         $xjData[] = ['xjId' => $id, 'name' => $name, 'ptItemId' => $currentPtItemId,
-                            'num' => $num, 'price' => $unitPrice, 'itemId' => $currentId, 'cover' => $cover];
+                            'num' => $num, 'price' => $unitPrice, 'itemId' => $item->id, 'cover' => $cover];
                     }
                     OrderXjClass::batchAdd($xjData);
                     $item->variety = 1;
@@ -372,25 +370,23 @@ class OrderItemClass extends BaseClass
                 }
             }
 
-            $productId = $val['productId'];
-            $name = $val['name'] ?? '';
-            $ratio = $val['ratio'] ?? 0;
-            $currentWeight = $val['weight'];
-
-            $bigNum = $val['bigNum'] ?? 0;
-            $smallNum = $val['smallNum'] ?? 0;
+            $ratio = $p['ratio'] ?? 0;
+            $currentWeight = $p['weight'];
+            $bigNum = $p['bigNum'] ?? 0;
+            $smallNum = $p['smallNum'] ?? 0;
             $itemNum = ProductClass::mergeItemNum($bigNum, $smallNum, $ratio);
             $w = bcmul($itemNum, $currentWeight, 2);
             $weight = bcadd($w, $weight, 2);
 
-            $limitBuy = $val['limitBuy'] ?? 0;
+            $limitBuy = $p['limitBuy'];
             $currentNum = floatval($itemNum);
             if ($limitBuy > 0) {
-                ProductClass::setLimitBuyCache($productId, $customId, $limitBuy, $currentNum);
+                ProductClass::handleLimitBuy($p, $customId, $currentNum);
             }
 
             if ($stockMayChange == true) {
                 if ($itemNum > 0) {
+                    $productId = $p['productId'];
                     $checkStock = true;
                     //如果是虚拟客户,说明自己也是虚拟批发店,虚拟批发店不用考虑库存
                     if ($live == 0) {
@@ -401,7 +397,7 @@ class OrderItemClass extends BaseClass
                     $recordData['sjId'] = $post['sjId'] ?? 0;
                     $recordData['shopId'] = $post['shopId'] ?? 0;
                     $recordData['mainId'] = $post['mainId'] ?? 0;
-                    $recordData['itemId'] = $val['itemId'] ?? 0;
+                    $recordData['itemId'] = $p['itemId'] ?? 0;
                     $recordData['itemNum'] = $itemNum;
                     $recordData['oldStock'] = $stockInfo['oldStock'];
                     $recordData['newStock'] = $stockInfo['newStock'];
@@ -413,6 +409,7 @@ class OrderItemClass extends BaseClass
                 }
             }
         }
+
         $respond['weight'] = $weight;
         return $respond;
     }

+ 117 - 30
biz-ghs/product/classes/ProductClass.php

@@ -23,9 +23,9 @@ use bizGhs\order\classes\CheckOrderItemClass;
 use bizGhs\order\traits\OrderTrait;
 use bizGhs\shop\classes\ShopAdminClass;
 use bizGhs\stock\classes\StockRecordClass;
-use bizHd\notify\classes\NotifyClass;
 use common\components\dict;
 use common\components\imgUtil;
+use common\components\noticeUtil;
 use common\components\orderSn;
 use common\components\stringUtil;
 use common\components\util;
@@ -38,6 +38,7 @@ class ProductClass extends BaseClass
     use OrderTrait;
 
     public static $baseFile = '\bizGhs\product\models\Product';
+    const LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY = 'limitBuyRollbackSnapshot'; //记录限购旧值快照缓存键
     const PRICE_LABEL_AUTO = 1; //自动调价
     const PRICE_LABEL_CHANGE = 2; //改价
     const LOCK_STOCK = 'lock_stock';//修改库存锁
@@ -1408,22 +1409,21 @@ class ProductClass extends BaseClass
         $itemInfo = self::mergeItemInfo($itemInfo);
 
         $productIds = array_column($itemInfo, 'productId');
-        $ghsProductData = ProductClass::getProductByIds($productIds);
+        $ghsProductData = self::getProductByIds($productIds);
         $ghsProductData = array_column($ghsProductData, NULL, 'id');
-        if ($live == 1) {
-            $hdProductData = [];
-        } else {
-            $hdIds = array_column($itemInfo, 'hdProductId');
-            $hdProductData = ProductClass::getByIds($hdIds, null, 'id');
-        }
-
-        $book = $moreParams['book'] ?? 0;
-        $customShopAdminId = $moreParams['customShopAdminId'] ?? 0;
+//        if ($live == 1) {
+//            $hdProductData = [];
+//        } else {
+//            $hdIds = array_column($itemInfo, 'hdProductId');
+//            $hdProductData = self::getByIds($hdIds, null, 'id');
+//        }
 
         $totalPrice = 0;
         $totalBigNum = 0;
         $totalSmallNum = 0;
 
+        $book = $moreParams['book'] ?? 0;
+        $customShopAdminId = $moreParams['customShopAdminId'] ?? 0;
         $custom = $moreParams['custom'] ?? [];
         //不同等级对应 price addPrice
         $priceMap = CustomClass::$levelPriceKeyMap;
@@ -1439,12 +1439,15 @@ class ProductClass extends BaseClass
         $totalCost = 0;
         foreach ($itemInfo as $v) {
             $ghsProductId = $v['productId'];
-
             $currentGhsProduct = $ghsProductData[$ghsProductId] ?? [];
-            $itemId = $currentGhsProduct['itemId'] ?? 0;
-            $ratio = $currentGhsProduct['ratio'] ?? 0;
-            $cost = $currentGhsProduct['avCost'] ?? 0;
-            $classId = $currentGhsProduct['classId'] ?? 0;
+            if (empty($currentGhsProduct)) {
+                noticeUtil::push("ghsProductId={$ghsProductId} 在 ghsProductData 中为空");
+                continue;
+            }
+            $itemId = $currentGhsProduct['itemId'];
+            $ratio = $currentGhsProduct['ratio'];
+            $cost = $currentGhsProduct['avCost'];
+            $classId = $currentGhsProduct['classId'];
 
             $bigNum = $v['bigNum'] ?? 0;
             $smallNum = $v['smallNum'] ?? 0;
@@ -1514,10 +1517,13 @@ class ProductClass extends BaseClass
             $tmp['sjId'] = $currentGhsProduct['sjId'] ?? 0;
             $tmp['shopId'] = $currentGhsProduct['shopId'] ?? 0;
             $tmp['mainId'] = $currentGhsProduct['mainId'] ?? 0;
-            $tmp['limitBuy'] = $currentGhsProduct['limitBuy'] ?? 0;
+            $tmp['limitBuy'] = $currentGhsProduct['limitBuy'];
             $tmp['smallUnitPrice'] = $itemPrice;
             $tmp['classId'] = $classId;
             $tmp['belongCost'] = $currentGhsProduct['belongCost'] ?? 0;
+            $tmp['hjDiscountPrice'] = $currentGhsProduct['hjDiscountPrice'];
+            $tmp['discountPrice'] = $currentGhsProduct['discountPrice'];
+            $tmp['skDiscountPrice'] = $currentGhsProduct['skDiscountPrice'];
 
             $totalBigNum += $bigNum;
             $totalSmallNum += $smallNum;
@@ -1533,16 +1539,10 @@ class ProductClass extends BaseClass
 
                 $smallCost = bcdiv($cost, $ratio, 2);
                 $cost = $smallCost;
-
             }
 
-            $tmp['cost'] = $cost;
-
             $xhPrice = bcmul($xhNum, $itemPrice, 2);
-
-            $currentCost = bcmul($xhNum, $cost, 2);
-            $totalCost = bcadd($totalCost, $currentCost, 2);
-
+            $tmp['cost'] = $cost;
             $tmp['xhNum'] = $xhNum;
             $tmp['xhUnitName'] = $xhUnitName;
             $tmp['xhUnitType'] = $xhUnitType;
@@ -1553,8 +1553,10 @@ class ProductClass extends BaseClass
             $tmp['xhPreUnitType'] = $xhUnitType;
             $tmp['xhPreUnitPrice'] = $itemPrice;
             $tmp['xhPrePrice'] = $xhPrice;
-
             $data[] = $tmp;
+
+            $currentCost = bcmul($xhNum, $cost, 2);
+            $totalCost = bcadd($totalCost, $currentCost, 2); // TODO 根据限购与特价进行价格计算的关键参数
         }
 
         return ['product' => $data, 'totalReachDiscount' => $totalReachDiscount, 'smallNum' => $totalSmallNum,
@@ -2689,26 +2691,111 @@ class ProductClass extends BaseClass
         }
     }
 
-    // 设置限购缓存
-    public static function setLimitBuyCache($productId, $customId, $limitBuy, $num)
+    /**
+     * 记录限购缓存写入前快照(同一次请求内同商品同客户只记录一次)
+     *
+     * @param int $productId
+     * @param int $customId
+     * @param bool $hasOldValue
+     * @param string|int|float $oldValue
+     * @return void
+     */
+    public static function recordLimitBuyRollbackSnapshot($productId, $customId, $hasOldValue, $oldValue = 0)
+    {
+        $productId = intval($productId);
+        $customId = intval($customId);
+        if ($productId <= 0 || $customId <= 0) {
+            return;
+        }
+        $snapshotKey = $productId . '_' . $customId;
+        if (!isset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY]) || !is_array(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY])) {
+            Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY] = [];
+        }
+        if (isset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY][$snapshotKey])) {
+            return;
+        }
+        Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY][$snapshotKey] = [
+            'productId' => $productId,
+            'customId' => $customId,
+            'hasOldValue' => $hasOldValue ? 1 : 0,
+            'oldValue' => strval($oldValue),
+        ];
+    }
+
+    /**
+     * 清理当前请求中记录的限购回滚快照
+     *
+     * @return void
+     */
+    public static function clearLimitBuyRollbackSnapshot()
+    {
+        unset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY]);
+    }
+
+    /**
+     * 回滚当前请求中已记录的限购缓存变更
+     *
+     * @return bool
+     */
+    public static function rollbackLimitBuySnapshot()
+    {
+        $snapshotList = Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY] ?? [];
+        if (empty($snapshotList) || !is_array($snapshotList)) {
+            return true;
+        }
+        foreach ($snapshotList as $snapshot) {
+            $productId = intval($snapshot['productId'] ?? 0);
+            $customId = intval($snapshot['customId'] ?? 0);
+            if ($productId <= 0 || $customId <= 0) {
+                continue;
+            }
+            $limitKey = 'limit_buy_' . $productId;
+            $hasOldValue = intval($snapshot['hasOldValue'] ?? 0);
+            if ($hasOldValue == 1) {
+                $oldValue = $snapshot['oldValue'] ?? '0';
+                Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $oldValue]);
+            } else {
+                Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
+            }
+        }
+        self::clearLimitBuyRollbackSnapshot();
+        return true;
+    }
+
+    // 限购处理
+    public static function handleLimitBuy($product, $customId, $num)
     {
+        $limitBuy = $product['limitBuy'];
         if ($limitBuy <= 0) {
             return;
         }
-        
+
+        $productId = $product['productId'];
         $limitKey = 'limit_buy_' . $productId;
         $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, $customId]);
         if (!empty($has)) {
             $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
             $lastNum = bcadd($hasNum, $num);
             if ($lastNum > $limitBuy) {
-                util::fail('超出限购数');
+                // 如果有特价,则超过的数量按原价卖;否则中断
+                if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
+                    return;
+                }
+
+                util::error(-1, '累计已超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$lastNum - $limitBuy]);
             }
+            self::recordLimitBuyRollbackSnapshot($productId, $customId, true, $hasNum);
             Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
         } else {
             if ($num > $limitBuy) {
-                util::fail('超出限购数');
+                // 如果有特价,则超过的数量按原价卖;否则中断
+                if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
+                    return;
+                }
+
+                util::error(-1, '超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$num - $limitBuy]);
             }
+            self::recordLimitBuyRollbackSnapshot($productId, $customId, false, 0);
             Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $num]);
         }
     }

+ 44 - 0
biz-hd/product/classes/ProductClass.php

@@ -424,4 +424,48 @@ class ProductClass extends BaseClass
         return $list;
     }
 
+    //取消限购的缓存
+    public static function cancelLimitBuy($productId)
+    {
+        $limitKey = 'limit_buy_' . $productId;
+        $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
+        if (!empty($arr)) {
+            foreach ($arr as $field) {
+                self::baseClearLimitBuy($productId, $field);
+            }
+        }
+        self::clearLimitBuyClearMark($productId);
+    }
+
+    //$num -1 表示减全部,大于-1表示减值
+    public static function baseClearLimitBuy($productId, $customId, $num = -1)
+    {
+        $limitKey = 'limit_buy_' . $productId;
+        if ($num <= -1) {
+            //清掉全部
+            Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
+        } else {
+            $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+            $remainNum = $hasNum > $num ? bcsub($num, $hasNum) : 0;
+            $remainNum = floor($remainNum);
+            if ($remainNum > 0) {
+                Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $remainNum]);
+            } else {
+                Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
+            }
+        }
+    }
+
+    // 清理限购清空标记
+    public static function clearLimitBuyClearMark($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $clearMarkKey = 'limit_buy_clear_at:' . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
+        return true;
+    }
+
 }

+ 0 - 8
biz-hd/purchase/services/PurchaseService.php

@@ -3,22 +3,15 @@
 namespace bizHd\purchase\services;
 
 use biz\ghs\classes\GhsClass;
-use biz\ghs\models\Ghs;
 use biz\shop\classes\ShopCapitalClass;
 use biz\shop\classes\ShopClass;
 use biz\shop\classes\ShopCouponClass;
-use biz\shop\models\Shop;
 use biz\stat\classes\StatCgClass;
 use biz\stat\classes\StatCgGhsClass;
 use biz\stat\classes\StatOutClass;
-use biz\wx\classes\WxMessageClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\services\OrderService;
-use bizGhs\product\classes\ProductClass;
-use bizGhs\stock\classes\StockRecordClass;
-use bizGhs\ws\services\WsService;
-use bizHd\admin\classes\ShopAdminClass;
 use bizHd\base\services\BaseService;
 use bizHd\cg\classes\CgRefundClass;
 use bizHd\purchase\classes\PurchaseClass;
@@ -28,7 +21,6 @@ use bizHd\shop\classes\MainClass;
 use common\components\dict;
 use common\components\imgUtil;
 use common\components\noticeUtil;
-use common\components\sms;
 use common\components\util;
 use Yii;
 

+ 2 - 2
common/components/util.php

@@ -298,9 +298,9 @@ class util
     }
 
     //错误输出,带自定义code
-    public static function error($code, $msg)
+    public static function error($code, $msg, $data=[])
     {
-        self::encode(['code' => $code, 'msg' => $msg, 'data' => []]);
+        self::encode(['code' => $code, 'msg' => $msg, 'data' => $data]);
     }
 
     //成功输出内容