Ver Fonte

Merge branch 'zhongqi-itemUpdate'

shish há 3 meses atrás
pai
commit
d19586c4f8
47 ficheiros alterados com 2180 adições e 334 exclusões
  1. 2 1
      .gitignore
  2. 1 1
      app-ghs/controllers/ItemController.php
  3. 21 1
      app-ghs/controllers/OrderController.php
  4. 62 0
      app-ghs/controllers/OrderItemController.php
  5. 38 0
      app-ghs/controllers/OrderTreeController.php
  6. 73 69
      app-ghs/controllers/ProductController.php
  7. 11 3
      app-ghs/controllers/TestController.php
  8. 55 0
      app-hd/controllers/ItemController.php
  9. 9 2
      app-hd/controllers/ProductController.php
  10. 81 10
      app-hd/controllers/PurchaseController.php
  11. 0 2
      app-hd/controllers/WxOpenController.php
  12. 58 10
      app-mall/controllers/OrderController.php
  13. 0 7
      biz-ghs/item/models/ItemClass.php
  14. 2 2
      biz-ghs/order/classes/CheckOrderClass.php
  15. 74 9
      biz-ghs/order/classes/OrderClass.php
  16. 61 39
      biz-ghs/order/classes/OrderItemClass.php
  17. 13 0
      biz-ghs/order/classes/OrderTreeClass.php
  18. 15 0
      biz-ghs/order/models/OrderTree.php
  19. 6 8
      biz-ghs/order/services/OrderService.php
  20. 1 1
      biz-ghs/order/traits/OrderTrait.php
  21. 624 50
      biz-ghs/product/classes/ProductClass.php
  22. 3 4
      biz-ghs/product/services/ProductService.php
  23. 13 0
      biz-hd/cg/classes/CgTreeClass.php
  24. 15 0
      biz-hd/cg/models/CgTree.php
  25. 24 9
      biz-hd/deduct/classes/DeductClass.php
  26. 14 1
      biz-hd/order/classes/OrderClass.php
  27. 502 0
      biz-hd/product/classes/ProductClass.php
  28. 26 9
      biz-hd/purchase/classes/PurchaseClass.php
  29. 23 10
      biz-hd/purchase/services/PurchaseService.php
  30. 9 2
      biz-hd/recharge/classes/RechargeClass.php
  31. 0 1
      biz-hd/stat/classes/StatStudentClass.php
  32. 0 1
      biz-hd/stat/classes/StatStudentMonthClass.php
  33. 0 2
      biz-mall/order/classes/OrderClass.php
  34. 0 2
      biz-mall/order/services/OrderService.php
  35. 0 1
      biz/product/classes/ProductClass.php
  36. 76 0
      common/components/rabbitmq/baseConsumer.php
  37. 93 0
      common/components/rabbitmq/cancelLimitBuyConsumer.php
  38. 18 18
      common/components/rabbitmq/customConsumer.php
  39. 24 26
      common/components/rabbitmq/notifyConsumer.php
  40. 15 14
      common/components/rabbitmq/ptConsumer.php
  41. 92 5
      common/components/rabbitmq/stockConsumer.php
  42. 2 2
      common/components/util.php
  43. 22 2
      common/config/rabbitMQ.php
  44. 0 4
      console/controllers/HdOrderController.php
  45. 1 2
      console/controllers/PurchaseController.php
  46. 0 3
      console/controllers/ShopCouponController.php
  47. 1 1
      vendor/mikemadisonweb/yii2-rabbitmq/Configuration.php

+ 2 - 1
.gitignore

@@ -17,4 +17,5 @@ app-hd/web/assets/
 app-pt/web/assets/
 app-ghs/web/assets/
 app-mall/web/assets/
-.aider*
+.aider*
+AGENTS.md

+ 1 - 1
app-ghs/controllers/ItemController.php

@@ -503,7 +503,6 @@ class ItemController extends BaseController
             }
         }
         $level = $custom->level ?? 1;
-        $field = '*';
 
         //只查我负责的花材
         $globalCgMyCharge = $get['globalCgMyCharge'] ?? 0;
@@ -537,6 +536,7 @@ class ItemController extends BaseController
             }
         }
 
+        $field = '*';
         $result = ProductClass::getList($field, $where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC]);
         $list = $result['list'] ?? [];
         if ($requestType == 'hasStockList') {

+ 21 - 1
app-ghs/controllers/OrderController.php

@@ -1246,9 +1246,26 @@ class OrderController extends BaseController
                 }
             }
         }
-
         $post['xj'] = $newXj;
 
+        $treeData = $post['treeData'] ?? [];
+        $newTree = [];
+        if (!empty($treeData)) {
+            $treeArr = json_decode($treeData, true);
+            if (!empty($treeArr) && is_array($treeArr)) {
+                foreach ($treeArr as $treeInfo) {
+                    $ptItemId = $treeInfo['ptItemId'] ?? 0;
+                    $treeList = $treeInfo['list'] ?? [];
+                    if (!empty($treeList)) {
+                        foreach ($treeList as $treeItem) {
+                            $newTree[$ptItemId][] = $treeItem;
+                        }
+                    }
+                }
+            }
+        }
+        $post['treeData'] = $newTree;
+
         $productJson = $post['product'] ?? '';
         if (empty($productJson)) {
             util::fail('请选择花材');
@@ -2053,6 +2070,9 @@ class OrderController extends BaseController
         $staffFinance = $staff->finance ?? 0;
         $info['staffFinance'] = $staffFinance;
 
+        $shopBusiness = $shop->business ?? 0;
+        $info['shopBusiness'] = $shopBusiness;
+
         util::success($info);
     }
 

Diff do ficheiro suprimidas por serem muito extensas
+ 62 - 0
app-ghs/controllers/OrderItemController.php


+ 38 - 0
app-ghs/controllers/OrderTreeController.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace ghs\controllers;
+
+use bizGhs\order\classes\OrderClass;
+use bizGhs\order\classes\OrderTreeClass;
+use bizGhs\product\classes\ProductClass;
+use common\components\imgUtil;
+use common\components\util;
+use Yii;
+
+class OrderTreeController extends BaseController
+{
+
+    public function actionGetTree()
+    {
+        $get = Yii::$app->request->get();
+        $orderSn = $get['orderSn'] ?? '';
+        $productId = $get['productId'] ?? '';
+        $order = OrderClass::getByCondition(['orderSn' => $orderSn], true);
+        if (empty($order)) {
+            util::fail('没有找到订单');
+        }
+        if ($order->mainId != $this->mainId) {
+            util::fail('不是你的订单');
+        }
+        $product = ProductClass::getByCondition(['id' => $productId], true);
+        if (empty($product)) {
+            util::fail('没有找到花材');
+        }
+        if ($product->mainId != $this->mainId) {
+            util::fail('不是你的花材');
+        }
+        $list = OrderTreeClass::getAllByCondition(['orderSn' => $orderSn, 'productId' => $productId], 'id asc', '*');
+        util::success(['list' => $list]);
+    }
+
+}

+ 73 - 69
app-ghs/controllers/ProductController.php

@@ -158,7 +158,12 @@ class ProductController extends BaseController
             }
             $info->presell = $presell;
             $info->presellDate = $newStr;
-            $info->discountPrice = 0;
+
+            //$info->discountPrice = 0;
+            if ($presell == 1 && $info->discountPrice > 0) {
+                util::fail('已设置特价,无法开启预售');
+            }
+
             $info->save();
         }catch(\Exception $e){
             // 失败情况,要把缓存删除
@@ -389,106 +394,94 @@ class ProductController extends BaseController
             $level = $custom->level ?? 1;
         }
         $itemInfoData = [];
-        if ($requestType == 'hasStockList') {
-            //有库存的花材列表
-            $field = 'id,py,cover,name,classId,itemId,price,skPrice,hjPrice,stock,onStock,cost,avCost,addPrice,hjAddPrice,skMore';
+        $fieldMap = [
+            'hasStockList' => 'id,py,cover,name,classId,itemId,price,skPrice,hjPrice,stock,onStock,cost,avCost,addPrice,hjAddPrice,skMore,limitBuy',
+            'hideItem'     => 'id,py,cover,name,classId,itemId,price,skPrice,hjPrice,stock,onStock,cost,avCost,addPrice,hjAddPrice,skMore,limitBuy',
+            'changePrice'  => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,skMore,hjAddPrice,variety,price,stock,onStock,presell,frontHide,reachNum,reachNumDiscount,limitBuy',
+            'changeStock'  => 'id,py,cover,name,cost,itemId,status,classId,addPrice,skPrice,discountPrice,skMore,variety,price,stock,frontHide,limitBuy',
+            'kd'           => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide,reachNum,reachNumDiscount,limitBuy',
+            'book'         => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide,limitBuy',
+            'itemList'     => 'id,py,cover,name,cost,avCost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,stock,onStock,actualSold,status,ratio,ratioType,presell,frontHide,auth,reachNum,reachNumDiscount,addPrice,hjAddPrice,skMore,limitBuy',
+            'cgStaffList'  => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,stock,onStock,actualSold,status,ratio,ratioType,presell,frontHide,auth,reachNum,reachNumDiscount,cgStaffId,cgStaffName,limitBuy',
+            'outList'      => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,limitBuy',
+            'stopList'     => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,delStatus,limitBuy',
+            'removeList'   => 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,limitBuy',
+            'check'        => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,frontHide,limitBuy',
+            'break'        => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,frontHide,limitBuy',
+            'part'         => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,limitBuy',
+            'stockOut'     => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,smallRatio,frontHide,limitBuy',
+            'cg'           => 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,presell,ratioType,smallUnit,bigUnit,ratio,weight,frontHide,limitBuy',
+            'warning'      => 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,discountPrice,stock,onStock,actualSold,presell,bigUnit,stockWarning,status,frontHide,limitBuy',
+        ];
+
+        if (!isset($fieldMap[$requestType])) {
+            util::fail('没有数据');
+        }
+        $field = $fieldMap[$requestType];
+
+        // 获取数据
+        if ($requestType == 'warning') {
+            $itemInfoData = Product::find()->where("mainId={$this->mainId}")
+                ->andWhere('`stock`+`onStock`<`stockWarning`')
+                ->andWhere("delStatus=0")
+                ->select($field)->asArray()->all();
+        } else {
+            // 处理特殊的 where 条件
+            if ($requestType == 'cgStaffList') {
+                unset($where['status']);
+                $where['delStatus'] = 0;
+            } elseif ($requestType == 'outList') {
+                $where['status'] = 2;
+                $where['delStatus'] = 0;
+                $where['removeStatus'] = 0;
+            } elseif ($requestType == 'stopList') {
+                unset($where['status']);
+                $where['delStatus'] = 1;
+                $where['removeStatus'] = 0;
+            } elseif ($requestType == 'removeList') {
+                unset($where['status']);
+                $where['delStatus'] = 1;
+                $where['removeStatus'] = 1;
+            }
+
             $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
+        }
+
+        // 数据后处理
+        if ($requestType == 'hasStockList') {
             if (!empty($itemInfoData)) {
                 foreach ($itemInfoData as $key => $val) {
-                    //以下参数主要是网页批量改价要用到
                     $shortCover = $val['cover'] ?? '';
                     $itemInfoData[$key]['cover'] = imgUtil::groupImg($shortCover) . "?x-oss-process=image/resize,m_fill,h_100,w_100";
                     $itemInfoData[$key]['shortCover'] = $shortCover;
                 }
             }
-        } elseif ($requestType == 'hideItem') {
-            //隐藏花材列表
-            $field = 'id,py,cover,name,classId,itemId,price,skPrice,hjPrice,stock,onStock,cost,avCost,addPrice,hjAddPrice,skMore';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
         } elseif ($requestType == 'changePrice') {
-            $field = 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,skMore,hjAddPrice,addPrice,variety,price,hjPrice,stock,onStock,presell,frontHide,reachNum,reachNumDiscount';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::changePriceGroup($itemInfoData, $level);
         } elseif ($requestType == 'changeStock') {
-            //修改库存列表
-            $field = 'id,py,cover,name,cost,itemId,status,classId,addPrice,skPrice,discountPrice,skMore,variety,price,stock,frontHide';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::changeStockGroup($itemInfoData, $level);
         } elseif ($requestType == 'kd') {
-            //开单的花材列表
-            $field = 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide,reachNum,reachNumDiscount';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::kdItemGroup($itemInfoData, $level, $custom);
         } elseif ($requestType == 'book') {
-            //预订花材列表
-            $field = 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,hjDiscountPrice,skDiscountPrice,discountPrice,stock,weight,stockWarning,presell,ratioType,smallUnit,smallRatio,bigUnit,ratio,frontHide';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::bookItemGroup($itemInfoData, $level);
         } elseif ($requestType == 'itemList') {
-            //花材列表
-            $field = 'id,py,cover,name,cost,avCost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,stock,onStock,actualSold,status,ratio,ratioType,presell,frontHide,auth,reachNum,reachNumDiscount,addPrice,hjAddPrice,skMore';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::itemListGroup($itemInfoData, $level);
         } elseif ($requestType == 'cgStaffList') {
-            unset($where['status']);
-            $where['delStatus'] = 0;
-            $field = 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,hjPrice,discountPrice,hjDiscountPrice,skDiscountPrice,stock,onStock,actualSold,status,ratio,ratioType,presell,frontHide,auth,reachNum,reachNumDiscount,cgStaffId,cgStaffName';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::cgStaffListGroup($itemInfoData, $level);
-        } elseif ($requestType == 'outList') {
-            //下架花材列表
-            $where['status'] = 2;
-            $where['delStatus'] = 0;
-            $where['removeStatus'] = 0;
-            $field = 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
-            $itemInfoData = ProductClass::simpleGroup($itemInfoData, $level);
-        } elseif ($requestType == 'stopList') {
-            //停用花材列表
-            unset($where['status']);
-            $where['delStatus'] = 1;
-            $where['removeStatus'] = 0;
-            $field = 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status,delStatus';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
-            $itemInfoData = ProductClass::simpleGroup($itemInfoData, $level);
-        } elseif ($requestType == 'removeList') {
-            //删除花材列表
-            unset($where['status']);
-            $where['delStatus'] = 1;
-            $where['removeStatus'] = 1;
-            $field = 'id,py,cover,name,cost,itemId,classId,addPrice,skPrice,bigUnit,smallUnit,ratio,ratioType,discountPrice,skMore,variety,price,stock,presell,status';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
+        } elseif (in_array($requestType, ['outList', 'stopList', 'removeList'])) {
             $itemInfoData = ProductClass::simpleGroup($itemInfoData, $level);
         } elseif ($requestType == 'check') {
-            $field = 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,frontHide';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::checkItemGroup($itemInfoData, $level);
         } elseif ($requestType == 'break') {
-            $field = 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,frontHide';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::breakItemGroup($itemInfoData, $level);
         } elseif ($requestType == 'part') {
-            //拆散花材
-            $field = 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::partItemGroup($itemInfoData, $level);
         } elseif ($requestType == 'stockOut') {
-            $field = 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,stockWarning,presell,ratioType,smallUnit,bigUnit,ratio,smallRatio,frontHide';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::stockOutGroup($itemInfoData, $level);
         } elseif ($requestType == 'cg') {
-            $field = 'id,py,cover,name,itemId,classId,variety,price,discountPrice,stock,presell,ratioType,smallUnit,bigUnit,ratio,weight,frontHide';
-            $itemInfoData = ProductClass::getAllByCondition($where, ['inTurn' => SORT_DESC, 'actualSold' => SORT_DESC], $field);
             $itemInfoData = ProductClass::cgItemGroup($itemInfoData, $level);
         } elseif ($requestType == 'warning') {
-            $field = 'id,py,cover,name,cost,itemId,classId,skPrice,variety,price,discountPrice,stock,onStock,actualSold,presell,bigUnit,stockWarning,status,frontHide';
-            $itemInfoData = Product::find()->where("mainId={$this->mainId}")
-                ->andWhere('`stock`+`onStock`<`stockWarning`')
-                ->andWhere("delStatus=0")
-                ->select($field)->asArray()->all();
             $itemInfoData = ProductClass::warningGroup($itemInfoData, $level);
-        } else {
-            util::fail('没有数据');
         }
         $data = ProductService::assembleItemData($classInfo, $itemInfoData);
         util::success($data);
@@ -1229,7 +1222,7 @@ class ProductController extends BaseController
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
         try {
-            ProductClass::updateProduct($post, $this->shop);
+            ProductClass::updateProduct($post, $this->shop, 'ghs');
             $transaction->commit();
             util::complete();
         } catch (\Exception $e) {
@@ -1333,6 +1326,17 @@ class ProductController extends BaseController
         $id = Yii::$app->request->get('id', 0);
         $source = Yii::$app->request->get('source', '');
         $respond = ProductClass::getItemInfo($id);
+        if (empty($respond) || !is_array($respond)) {
+            util::fail('没有找到花材');
+        }
+        $limitBuyClearInfo = ProductClass::getLimitBuyClearInfo($id);
+        if (!is_array($limitBuyClearInfo)) {
+            $limitBuyClearInfo = [];
+        }
+        // foreach ($limitBuyClearInfo as $key => $value) {
+            // $respond[$key] = $value;
+        // }
+        $respond['limitBuyClearInfo'] = $limitBuyClearInfo;
         $ptItemId = $respond['itemId'] ?? 0;
         // 封面图与多花材图片的兼容处理
         if ($respond['images'] == '' && $respond['shortCover'] != '') {

+ 11 - 3
app-ghs/controllers/TestController.php

@@ -39,6 +39,7 @@ use common\components\payUtil;
 use Yii;
 use common\components\util;
 use wkhtmltox\Image\Converter;
+use common\components\rabbitmq\stockConsumer;
 
 
 class TestController extends BaseController
@@ -49,9 +50,16 @@ class TestController extends BaseController
     // ./yii test/rabbit 0
     public function actionRabbit()
     {
-        //放进去
-        ExpressClass::askCreateOrder(['orderId' => 1]);
-        \bizGhs\express\classes\ExpressClass::askCreateOrder(['orderId' => 2]);
+        $data = [
+            "type" => 'limit_buy_clear',
+            "ptType" => 'ghs',
+            "productId" => 27285,
+            "clearAt" => 1778126400,
+            "recurring" => 1,
+            "intervalDays" => 1,
+            "clearHour" => 12
+        ];
+        stockConsumer::clearOrderItemLimitBuy($data);
     }
 
     public function actionFx()

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

@@ -450,4 +450,59 @@ 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 actionClearOneLimitBuy()
+    {
+        $get = Yii::$app->request->get();
+        $productId = $get['productId'] ?? '';
+        $customId = $get['customId'] ?? 0;
+        $item = ItemClass::getById($productId, true);
+        if (empty($item)) {
+            util::fail('没有找到花材');
+        }
+        if ($item->mainId != $this->mainId) {
+            util::fail('不是你的花材');
+        }
+        $custom = CustomClass::getById($customId, true);
+        if (empty($custom)) {
+            util::fail('没有客户');
+        }
+        if ($custom->shopId != $this->shopId) {
+            util::fail('不是你的客户');
+        }
+        ProductClass::baseClearLimitBuy($productId, $customId);
+        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 = ProductClass::getHasLimitBuyList($item);
+        util::success(['customList' => $customList]);
+    }
 }

+ 9 - 2
app-hd/controllers/ProductController.php

@@ -851,6 +851,14 @@ class ProductController extends BaseController
     {
         $id = Yii::$app->request->get('id', 0);
         $respond = ProductClass::getItemInfo($id);
+        if (empty($respond) || !is_array($respond)) {
+            util::fail('没有找到花材');
+        }
+        $limitBuyClearInfo = \bizHd\product\classes\ProductClass::getLimitBuyClearInfo($id);
+        if (!is_array($limitBuyClearInfo)) {
+            $limitBuyClearInfo = [];
+        }
+        $respond['limitBuyClearInfo'] = $limitBuyClearInfo;
         $ptItemId = $respond['itemId'] ?? 0;
         $ptItemInfo = PtItemClass::getById($ptItemId);
         $respond['ptItemInfo'] = $ptItemInfo;
@@ -932,7 +940,7 @@ class ProductController extends BaseController
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
         try {
-            ProductClass::updateProduct($post, $this->shop);
+            ProductClass::updateProduct($post, $this->shop, 'hd');
             $transaction->commit();
             util::complete();
         } catch (\Exception $e) {
@@ -1083,5 +1091,4 @@ class ProductController extends BaseController
         $info->save();
         util::complete('取消成功');
     }
-
 }

+ 81 - 10
app-hd/controllers/PurchaseController.php

@@ -145,6 +145,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;
@@ -202,13 +212,45 @@ 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();
         }
     }
 
+    // 批发商的限购花材信息
+    public function actionLimitBuyInfo()
+    {
+        $post = Yii::$app->request->post();
+        $ghsId = $post['ghsId'] ?? 0;
+        $list = $post['list'] ?? [];
+        if (empty($ghsId)) {
+            util::fail('缺少供货商');
+        }
+        if (empty($list) || !is_array($list)) {
+            util::fail('请选择花材');
+        }
+
+        $ghsInfo = GhsClass::getById($ghsId);
+        if (empty($ghsInfo)) {
+            util::fail('没有找到供货商');
+        }
+        $customId = $ghsInfo['customId'] ?? 0;
+        if (empty($customId)) {
+            util::fail('没有找到客户');
+        }
+
+        $respond = ProductClass::getLimitBuyInfoByList($list, $customId);
+        util::success($respond);
+    }
+
     //生成采购单 ssh 2021.1.17
     public function actionCreateOrder()
     {
@@ -248,12 +290,22 @@ class PurchaseController extends BaseController
         }
         $ghsShopId = $ghsInfo['shopId'] ?? 0;
 
-        if (isset($ghsInfo['mainId']) && !empty($ghsInfo['mainId']) && $ghsInfo['mainId'] == $ghsInfo['ownMainId']) {
+        if (!empty($ghsInfo['mainId']) && $ghsInfo['mainId'] == $ghsInfo['ownMainId']) {
             util::fail('不能跟自己的店买花');
         }
 
         $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;
@@ -290,7 +342,7 @@ class PurchaseController extends BaseController
             if (isset($post['transType']) && $post['transType'] == 4) {
                 if (isset($ghsShopInfo->pfLevel) && $ghsShopInfo->pfLevel == 1) {
                     $notSameCity = PurchaseClass::notSameCity($this->shop, $ghsShopInfo);
-                    if ($notSameCity == false) {
+                    if (!$notSameCity) {
                         util::fail('距离太远,不能选择同城配送');
                     }
                 }
@@ -349,7 +401,7 @@ class PurchaseController extends BaseController
                         if ($book == 1) {
                             util::fail('预订时不能选择预售花材');
                         } else {
-                            if (isset($post['sendTimeWant']) == false || empty($post['sendTimeWant'])) {
+                            if (empty($post['sendTimeWant'])) {
                                 util::fail('请选择配送日期');
                             }
                             $sendTimeWant = $post['sendTimeWant'];
@@ -361,7 +413,7 @@ class PurchaseController extends BaseController
                             if (empty($hasDate)) {
                                 util::fail('预售日期有问题,请提醒门店');
                             }
-                            if (in_array(strtotime($sendTimeWant), $hasDate) == false) {
+                            if (!in_array(strtotime($sendTimeWant), $hasDate)) {
                                 util::fail("有预售花材在{$sendTimeWant}没到货");
                             }
                             $post['presell'] = 1;
@@ -419,6 +471,19 @@ class PurchaseController extends BaseController
                         }
                     }
                 }
+// 已经有了 -- 在 replaceItem()
+//                foreach ($productList as $itemData) {
+//                    $productId = $itemData['productId'] ?? 0;
+//                    $product = $productInfoList[$productId] ?? [];
+//                    if (empty($product)) {
+//                        util::fail('有商品信息没有找到');
+//                    }
+//                    $bigNum = $itemData['bigNum'] ?? 0;
+//                    if (($product['limitBuy'] ?? 0) > 0) {
+//                        $product['productId'] = $productId;
+//                        ProductClass::handleLimitBuy($product, $customId, floatval($bigNum));
+//                    }
+//                }
 
             } else {
                 util::fail('花材不存在');
@@ -745,11 +810,17 @@ class PurchaseController extends BaseController
             }
 
             $transaction->commit();
+            $transactionFinished = true;
+            ProductClass::clearLimitBuyRollbackSnapshot();
 
             util::success($respond);
 
-        } catch (Exception $e) {
-            $transaction->rollBack();
+        } catch (\Exception $e) {
+            if ($transaction->isActive) {
+                $transaction->rollBack();
+            }
+            ProductClass::rollbackLimitBuySnapshot();
+            $transactionFinished = true;
             util::fail();
         }
     }
@@ -896,7 +967,7 @@ class PurchaseController extends BaseController
                 }
 
             }
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             $transaction->rollBack();
             util::fail('支付失败');
         }
@@ -938,7 +1009,7 @@ class PurchaseController extends BaseController
             //余额支付
             purchaseService::balancePay($info);
             $transaction->commit();
-        } catch (Exception $e) {
+        } catch (\Exception $e) {
             $transaction->rollBack();
             util::fail('支付失败');
         }

+ 0 - 2
app-hd/controllers/WxOpenController.php

@@ -3,9 +3,7 @@
 namespace hd\controllers;
 
 use biz\wx\classes\WxOpenClass;
-use common\components\jsSDK;
 use common\components\util;
-use Yii;
 
 class WxOpenController extends BaseController
 {

+ 58 - 10
app-mall/controllers/OrderController.php

@@ -162,6 +162,31 @@ class OrderController extends BaseController
         util::success($respond);
     }
 
+    // 零售商的限购花材信息(与批发端 PurchaseController::actionLimitBuyInfo 对应,按花店客户与 hd_limit_buy Redis)
+    public function actionLimitBuyInfo()
+    {
+        $post = Yii::$app->request->post();
+        $list = $post['list'] ?? [];
+        if (empty($list) || !is_array($list)) {
+            util::fail('请选择花材');
+        }
+
+        $hd = $this->hd;
+        if (empty($hd)) {
+            util::fail('没有找到花店');
+        }
+        if ($hd->shopId != $this->shopId) {
+            util::fail('不是你的花店');
+        }
+        $hdCustomId = $hd->customId ?? 0;
+        if (empty($hdCustomId)) {
+            util::fail('没有找到客户');
+        }
+
+        $respond = \bizHd\product\classes\ProductClass::getLimitBuyInfoByList($list, $hdCustomId);
+        util::success($respond);
+    }
+
     //购买花材 ssh 20220511
     public function actionBuyItem()
     {
@@ -203,13 +228,13 @@ class OrderController extends BaseController
             util::fail('不是你的花店');
         }
         $post['hdName'] = $hd->name ?? '';
-        $customId = $hd->customId ?? 0;
-        $custom = CustomClass::getById($customId, true);
+        $hdCustomId = $hd->customId ?? 0;
+        $custom = CustomClass::getById($hdCustomId, true);
         if (empty($custom)) {
             util::fail('没有找到客户');
         }
         $customName = $custom->name ?? '';
-        $post['customId'] = $customId;
+        $post['customId'] = $hdCustomId;
         $post['customName'] = $customName;
         $post['customNamePy'] = stringUtil::py($customName);
 
@@ -238,7 +263,6 @@ class OrderController extends BaseController
                 if (!isset($currentInfo['mainId']) || $currentInfo['mainId'] != $this->mainId) {
                     util::fail('只能选择同一家的商品哦');
                 }
-                $currentName = $currentInfo['name'] ?? '';
                 $presell = $currentInfo['presell'] ?? 0;
                 $presellData[] = $presell;
                 if ($presell == 1) {
@@ -274,6 +298,17 @@ class OrderController extends BaseController
 
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
+        $transactionFinished = false;
+        register_shutdown_function(function () use (&$transactionFinished, $transaction) {
+            if ($transactionFinished) {
+                return;
+            }
+            \bizHd\product\classes\ProductClass::rollbackLimitBuySnapshot();
+            if ($transaction->isActive) {
+                $transaction->rollBack();
+            }
+        });
+
         try {
             $orderValidTime = !getenv('ORDER_VALID_TIME') ? 600 : getenv('ORDER_VALID_TIME');
             $post['deadline'] = time() + $orderValidTime;
@@ -293,19 +328,27 @@ class OrderController extends BaseController
             foreach ($productList as $eleKey => $element) {
                 $level = 0;
                 $productId = $element['productId'];
-                $current = $productInfoList[$productId];
-                if (empty($current)) {
+                $product = $productInfoList[$productId];
+                if (empty($product)) {
                     util::fail('有商品信息没有找到');
                 }
-                $price = \bizGhs\product\classes\ProductClass::getFinalPrice($current, $level, $priceMap, $addPriceMap);
-                $num = $element['num'] ?? 0;
+                $num = $element['num'] ?? 0; // 此次购买数量
+                $limitKey = \bizHd\product\classes\ProductClass::LIMIT_BUY_KEY . $productId;
+                $hasBuyNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $hdCustomId]);
+                $hasBuyNum = $num + ($hasBuyNum == null ? 0 : $hasBuyNum); // 已购买总数量
+                $price = \bizGhs\product\classes\ProductClass::getFinalPrice($product, $level, $priceMap, $addPriceMap, 0, $num, $hasBuyNum);
                 $currentTotal = bcmul($price, $num, 2);
                 $modifyPrice = bcadd($modifyPrice, $currentTotal, 2);
                 $productList[$eleKey]['unitPrice'] = $price;
-                $weight = $current['weight'] ?? 0;
+                $weight = $product['weight'] ?? 0;
                 $currentWeight = bcmul($num, $weight, 2);
                 $totalWeight = bcadd($totalWeight, $currentWeight, 2);
                 $totalNum = bcadd($totalNum, $num);
+
+                if ($product['limitBuy'] > 0) {
+                    $product['productId'] = $productId;
+                    \bizHd\product\classes\ProductClass::handleLimitBuy($product, $hdCustomId, floatval($num));
+                }
             }
             $post['product'] = $productList;
 
@@ -418,7 +461,7 @@ class OrderController extends BaseController
             if ($hbId > 0) { // 不使用 $hbId =! 0,因为要用负数来表示红包已取消
                 $hb = HbClass::getById($hbId, true);
                 if (!empty($hb)) {
-                    if ($hb->customId != $customId) {
+                    if ($hb->customId != $hdCustomId) {
                         util::fail('不是你的红包');
                     }
                     if ($hb->amount != $post['hbAmount']) {
@@ -483,6 +526,9 @@ class OrderController extends BaseController
 
             $transaction->commit();
 
+            $transactionFinished = true;
+            \bizHd\product\classes\ProductClass::clearLimitBuyRollbackSnapshot();
+
             $orderSn = $return->orderSn ?? '';
             $orderPrice = $return->orderPrice ?? 0;
             $id = $return->id ?? 0;
@@ -490,6 +536,8 @@ class OrderController extends BaseController
             util::success(['orderSn' => $orderSn, 'totalPrice' => $orderPrice, 'couponId' => 0, 'id' => $id, 'getPayType' => $getPayType]);
         } catch (\Exception $e) {
             $transaction->rollBack();
+            \bizHd\product\classes\ProductClass::rollbackLimitBuySnapshot();
+            $transactionFinished = true;
             Yii::error("失败原因:" . $e->getMessage());
             util::fail('下单失败');
         }

+ 0 - 7
biz-ghs/item/models/ItemClass.php

@@ -1,13 +1,7 @@
 <?php
-/**
- * User: admin
- * Date Time: 2021/1/13 14:13
- */
-
 namespace bizGhs\item\models;
 
 use bizGhs\base\models\Base;
-use bizGhs\item\classes\ItemClassClass;
 
 class ItemClass extends Base
 {
@@ -15,5 +9,4 @@ class ItemClass extends Base
     {
         return 'xhGhsItemClass';
     }
-
 }

+ 2 - 2
biz-ghs/order/classes/CheckOrderClass.php

@@ -429,9 +429,9 @@ class CheckOrderClass extends BaseClass
                         $subAmount = bcadd($subAmount, $amount, 2);
                     }
 
-                    //如果花材是自动补充库存类型的花材,则库存低于900时,自动补充到6850,多处要同步修改,关键词 auto_add_stock
+                    //如果花材是自动补充库存类型的花材,则库存低于900时,自动补充到66820,多处要同步修改,关键词 auto_add_stock
                     if ($productData->virtualStock == 1 && $v['itemNum'] < 900) {
-                        ProductClass::updateStockById($productId, 6850, $adminId);
+                        ProductClass::updateStockById($productId, 66820, $adminId);
                     }
 
                 }

+ 74 - 9
biz-ghs/order/classes/OrderClass.php

@@ -829,8 +829,7 @@ class OrderClass extends BaseClass
         $data['payStatus'] = self::PAY_STATUS_UN_PAY;
         $data['customId'] = !empty($data['customId']) ? $data['customId'] : 0;
         $live = $data['live'] ?? 1;
-        $currentShopId = $data['shopId'] ?? 0;
-        $shop = ShopClass::getLockById($currentShopId);
+        $shop = ShopClass::getLockById($shopId);
         if (empty($shop)) {
             util::fail('没有找到门店7');
         }
@@ -878,7 +877,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;
@@ -1946,7 +1945,7 @@ class OrderClass extends BaseClass
         /**************************如果是好多花的云仓还要再打一下标签纸,多处要同步修改,关键词 hdh_yc *****************************/
         $orderMainId = $orderInfo['mainId'] ?? 0;
         if (getenv('YII_ENV') == 'production') {
-            $map = [65726, 58, 25119, 28500, 1294,12925];
+            $map = [65726, 58, 25119, 28500, 1294, 12925];
         } else {
             $map = [828];
         }
@@ -2485,8 +2484,9 @@ class OrderClass extends BaseClass
                 //如果有限购数量占用,也需要清除
                 $customId = $order->customId ?? 0;
                 $num = floor($v['num']);
-                ProductClass::baseClearLimitBuy($v['productId'], $customId, $num);
-
+                if (!empty($customId) && $num > 0) {
+                    ProductClass::baseClearLimitBuy($v['productId'], $customId, $num);
+                }
             }
         }
         return $order;
@@ -3004,7 +3004,7 @@ XL;
         /**************************如果是好多花的云仓还要再打一下标签纸,多处要同步修改,关键词 hdh_yc *****************************/
         $orderMainId = $order->mainId;
         if (getenv('YII_ENV') == 'production') {
-            $map = [65726, 58, 25119, 28500, 1294,12925];
+            $map = [65726, 58, 25119, 28500, 1294, 12925];
         } else {
             $map = [828];
         }
@@ -3107,6 +3107,71 @@ XL;
         return true;
     }
 
+
+    //打印花材的物流标 ssh 20260507
+    public static function printItemLabel($order, $shop, $ext, $product, $num, $boxNum, $orderItem)
+    {
+        $customId = $order->customId ?? 0;
+        $custom = CustomClass::getById($customId, true);
+
+        $customMobile = $order->customMobile ?? '';
+
+        $customName = $order->customName ?? '';
+        $remark = $order->remark ?? '';
+        $shortRemark = stringUtil::subStringUtf8($remark, 8);
+        $wlName = $custom->wlName ?? '';
+        if (empty($wlName)) {
+            $wlName = $shortRemark;
+        }
+        $distId = $custom->distId ?? '';
+        $dist = DistClass::getById($distId, true);
+        $distName = $dist->name ?? '未分区';
+
+        $address = $order->address ?? '';
+        $floor = $order->floor ?? '';
+        $fullAddress = $address . $floor;
+
+        $wlLabelSn = $ext->wlLabelSn ?? '';
+        if (empty($wlLabelSn)) {
+            util::fail('请绑定物流标签机');
+        }
+        $p = new printUtil($wlLabelSn);
+        $p->times = $num;
+        $sameTimeIds = $order->sameTimeIds;
+        $num = 1;
+        if (!empty($sameTimeIds)) {
+            $ids = explode(',', $sameTimeIds);
+            if (!empty($ids)) {
+                $num = count($ids);
+            }
+        }
+        if ($num > 1) {
+            $customName = $customName . ' ' . $num . '单';
+        }
+        $remark = '';
+        if (!empty($orderItem->remark)) {
+            $remark .= $orderItem->remark;
+        }
+        if (!empty($order->remark)) {
+            if(!empty($remark)){
+                $remark .= ' '.$order->remark;
+            }else{
+                $remark .= $order->remark;
+            }
+        }
+
+        $productName = $product->name ?? '';
+        $content = '<TEXT x="400" y="30" font="12" w="2" h="2" r="90">' . $productName . ' 装箱数:' . $boxNum . '</TEXT>';
+        $content .= '<TEXT x="325" y="30" font="12" w="2" h="2" r="90">————————————————————</TEXT>';
+        $content .= '<TEXT x="250" y="30" font="12" w="2" h="2" r="90">客户:' . $customName . '</TEXT>';
+        $content .= '<TEXT x="180" y="30" font="12" w="2" h="2" r="90">地址:' . $fullAddress . '</TEXT>';
+        $content .= '<TEXT x="120" y="30" font="12" w="2" h="2" r="90">电话:' . $customMobile . '</TEXT>';
+        $content .= '<TEXT x="60" y="30" font="12" w="2" h="2" r="90">备注:' . $remark . '</TEXT>';
+        $p->printLabelMsg($content);
+        return true;
+    }
+
+
     // 更新同天内的所有订单的 sameTimeIds
     public static function updateSameTimeIds($order, $date)
     {
@@ -3135,12 +3200,12 @@ XL;
         // 排序并去重
         sort($ids);
         $ids = array_unique($ids);
-        
+
         // 限制最多 90 单,避免 someTimeIds 过长(兼容原先业务逻辑限制)
         if (count($ids) > 90) {
             $ids = array_slice($ids, 0, 90);
         }
-        
+
         $sameTimeIdsStr = implode(',', $ids);
         // 更新所有相关订单
         if (!empty($ids) && in_array($id, $ids)) {

+ 61 - 39
biz-ghs/order/classes/OrderItemClass.php

@@ -306,27 +306,47 @@ class OrderItemClass extends BaseClass
         return $list;
     }
 
-    //添加修改花材 ssh 2021.1.22
-    public static function replaceItem($orderSn, $product, $post = null, $stockMayChange = false, $moreParams = [])
+    /**
+     * 替换订单(orderSn)下所有商品项为指定的 product 列表,并根据参数处理附加商详(如每支鲜花 xj)、校验及扣减库存、更新限购等。
+     *
+     * 步骤说明:
+     * 1. 根据 customId 获取客户等级和属性,格式化 products 信息;
+     * 2. 先删除该订单下历史商品项,再批量插入新 products 项;
+     * 3. 若包含 xj(每支鲜花)信息,批量关联并校验单支库存、同步写入;
+     * 4. 处理每项商品的限购、本次扣减库存(如 stockMayChange 为 true),并记录出库流水。
+     * 5. 返回本次批量插入及处理后的汇总结果(带总重量)。
+     *
+     * 用于订单商品整体替换的核心业务方法。
+     * @param $orderSn
+     * @param $products
+     * @param null $post
+     * @param bool $stockMayChange
+     * @param array $moreParams
+     * @return array|void
+     * @throws \Exception
+     */
+    public static function replaceItem($orderSn, $products, $post = null, $stockMayChange = false, $moreParams = [])
     {
-        $customId = $post['customId'] ?? 0;
+        $customId = intval($post['customId']);
         $custom = CustomClass::getById($customId, true);
+        if (empty($custom)) {
+            return util::fail('没有找到客户');
+        }
+
         $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);
-
-            $currentId = $item->id ?? 0;
-            $currentPtItemId = $item->itemId ?? 0;
+        foreach ($products as $key => $p) {
+            $p['orderSn'] = $orderSn;
+            $p['mainId'] = $mainId;
+            $item = self::add($p, true);
 
-            $unitPrice = $val['unitPrice'] ?? 0;
+            $currentPtItemId = $item->itemId;
+            $unitPrice = $p['unitPrice'];
             if (isset($post['xj'][$currentPtItemId]) && !empty($post['xj'][$currentPtItemId])) {
                 $xjArr = $post['xj'][$currentPtItemId];
                 if (is_array($xjArr)) {
@@ -353,7 +373,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;
@@ -361,39 +381,40 @@ class OrderItemClass extends BaseClass
                 }
             }
 
-            $productId = $val['productId'];
-            $name = $val['name'] ?? '';
-            $ratio = $val['ratio'] ?? 0;
-            $currentWeight = $val['weight'];
+            $productId = $p['productId'] ?? 0;
+            if (!empty($post['treeData'][$currentPtItemId])) {
+                $treeData = $post['treeData'][$currentPtItemId];
+                if (is_array($treeData)) {
+                    foreach ($treeData as $treeItem) {
+                        $treeNum = $treeItem['num'] ?? 0;
+                        $treeParam = [
+                            'orderSn' => $orderSn,
+                            'productId' => $productId,
+                            'num' => $treeNum,
+                        ];
+                        OrderTreeClass::add($treeParam);
+                    }
+                    $dishNum = count($treeData);
+                    $item->dishNum = $dishNum;
+                    $item->save();
+                }
+            }
 
-            $bigNum = $val['bigNum'] ?? 0;
-            $smallNum = $val['smallNum'] ?? 0;
+            $ratio = $p['ratio'] ?? 0;
+            $bigNum = $p['bigNum'] ?? 0;
+            $smallNum = $p['smallNum'] ?? 0;
             $itemNum = ProductClass::mergeItemNum($bigNum, $smallNum, $ratio);
-            $w = bcmul($itemNum, $currentWeight, 2);
+            $w = bcmul($itemNum, $p['weight'], 2);
             $weight = bcadd($w, $weight, 2);
 
-            $limitBuy = $val['limitBuy'] ?? 0;
-            $limitKey = 'limit_buy_' . $productId;
-            $currentNum = floatval($itemNum);
+            $limitBuy = $p['limitBuy'];
             if ($limitBuy > 0) {
-                $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, $customId]);
-                if (!empty($has)) {
-                    $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
-                    $lastNum = bcadd($hasNum, $currentNum);
-                    if ($lastNum > $limitBuy) {
-                        util::fail($name . ' 超出限购数');
-                    }
-                    Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
-                } else {
-                    if ($currentNum > $limitBuy) {
-                        util::fail($name . ' 超出限购数');
-                    }
-                    Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $currentNum]);
-                }
+                ProductClass::handleLimitBuy($p, $customId, floatval($itemNum));
             }
 
             if ($stockMayChange == true) {
                 if ($itemNum > 0) {
+                    $productId = $p['productId'];
                     $checkStock = true;
                     //如果是虚拟客户,说明自己也是虚拟批发店,虚拟批发店不用考虑库存
                     if ($live == 0) {
@@ -404,7 +425,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'];
@@ -416,6 +437,7 @@ class OrderItemClass extends BaseClass
                 }
             }
         }
+
         $respond['weight'] = $weight;
         return $respond;
     }

+ 13 - 0
biz-ghs/order/classes/OrderTreeClass.php

@@ -0,0 +1,13 @@
+<?php
+namespace bizGhs\order\classes;
+
+use common\components\util;
+use Yii;
+use bizGhs\base\classes\BaseClass;
+
+class OrderTreeClass extends BaseClass
+{
+
+    public static $baseFile = '\bizGhs\order\models\OrderTree';
+
+}

+ 15 - 0
biz-ghs/order/models/OrderTree.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace bizGhs\order\models;
+
+use bizGhs\base\models\Base;
+
+class OrderTree extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhGhsOrderTree';
+    }
+
+}

+ 6 - 8
biz-ghs/order/services/OrderService.php

@@ -22,15 +22,12 @@ use bizGhs\order\classes\OrderSendClass;
 use bizGhs\product\classes\ProductClass;
 use bizGhs\shop\classes\MainClass;
 use bizGhs\shop\classes\ShopMoneyChangeClass;
-use bizGhs\shop\classes\ShopMoneyClass;
 use bizGhs\stat\classes\StatYjClass;
-use bizGhs\ws\services\WsService;
 use bizHd\cg\services\CgRefundService;
 use bizHd\purchase\services\PurchaseService;
 use bizHd\stat\classes\StatIncomeClass;
 use bizHd\stat\classes\StatOrderClass;
 use common\components\dict;
-use common\components\noticeUtil;
 use common\components\util;
 use bizHd\purchase\classes\PurchaseClass;
 use common\components\stringUtil;
@@ -212,16 +209,17 @@ class OrderService extends BaseService
         //零售采购单时没有传price字段,供货商可以改价会传price字段,这里转成userPrice以便采购单识别使用改的价还是花材原价!
         $currentProduct = $data['product'];
         foreach ($currentProduct as $currentKey => $currentItem) {
-            if (isset($currentProduct[$currentKey]['price'])) {
-                $currentProduct[$currentKey]['userPrice'] = $currentProduct[$currentKey]['price'];
+            if (isset($currentItem['price'])) {
+                $currentProduct[$currentKey]['userPrice'] = $currentItem['price'];
                 unset($currentProduct[$currentKey]['price']);
             }
         }
 
-        $sendTimeWant = isset($data['sendTimeWant']) && !empty($data['sendTimeWant']) ? $data['sendTimeWant'] : date("Y-m-d");
+        $sendTimeWant = !empty($data['sendTimeWant']) ? $data['sendTimeWant'] : date("Y-m-d");
 
         $remark = $data['remark'] ?? '';
         $xj = $data['xj'] ?? '';
+        $treeData = $data['treeData'] ?? [];
         $book = $data['book'] ?? 0;
 		//后端开单,涉及花材满减,关键词 back_order_reach_discount_price
         $reachDiscountPrice = $data['reachDiscountPrice'] ?? 0;
@@ -252,6 +250,7 @@ class OrderService extends BaseService
             'sendType' => $sendType,
             'remark' => $remark,
             'xj' => $xj,
+            'treeData' => $treeData,
             'discount' => 1,
             'mainId' => $cgMainId,
             'wlName' => $wlName,
@@ -259,7 +258,7 @@ class OrderService extends BaseService
             'transType' => $transType,
             'reachDiscountPrice' => $reachDiscountPrice,
         ];
-        if (isset($data['deadline']) && !empty($data['deadline'])) {
+        if (!empty($data['deadline'])) {
             $purchaseData['deadline'] = $data['deadline'];
         }
         if (isset($data['modifyPrice']) && is_numeric($data['modifyPrice'])) {
@@ -407,7 +406,6 @@ class OrderService extends BaseService
             }
         }
         return $returnOrder;
-
     }
 
     //自取免配送流程处理 ssh 2021.1.24

+ 1 - 1
biz-ghs/order/traits/OrderTrait.php

@@ -127,4 +127,4 @@ trait OrderTrait
     {
 
     }
-}
+}

+ 624 - 50
biz-ghs/product/classes/ProductClass.php

@@ -23,13 +23,14 @@ 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;
 use common\services\xhItemService;
+use PhpAmqpLib\Wire\AMQPTable;
 use Yii;
 
 class ProductClass extends BaseClass
@@ -37,12 +38,16 @@ class ProductClass extends BaseClass
     use OrderTrait;
 
     public static $baseFile = '\bizGhs\product\models\Product';
+    
     const PRICE_LABEL_AUTO = 1; //自动调价
     const PRICE_LABEL_CHANGE = 2; //改价
     const LOCK_STOCK = 'lock_stock';//修改库存锁
     const LOCK_ON_STOCK = 'lock_on_stock';//修改库存锁
-    const LOCK_PRICE = 'lock_price'; //改价锁
-    const LOCK_CHECK_STOCK = 'lock_check_stock'; //判断库存
+
+    const LIMIT_BUY_KEY = 'limit_buy_';
+    const CLEAR_MARK_KEY = 'limit_buy_clear_at:';
+    const CLEAR_LOOP_KEY = 'limit_buy_clear_loop:';
+    const LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY = 'limitBuyRollbackSnapshot'; //记录限购旧值快照缓存键
 
     public static function getNotShowFrontHideMainIds()
     {
@@ -663,6 +668,23 @@ class ProductClass extends BaseClass
             $list[$k]['autoPrice'] = $autoPrice;
             $list[$k]['autoSkPrice'] = $skAutoPrice;
             $list[$k]['userPrice'] = 0;
+
+            // 限购缓存
+            if ($v['limitBuy'] > 0) {
+                $limitKey = self::LIMIT_BUY_KEY . $v['id'];
+                $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, 0]);
+                if (!empty($has)) {
+                    // 获取缓存超时时间
+                    $expire = Yii::$app->redis->executeCommand('TTL', [$limitKey]);
+                    if ($expire > 0) {
+                        $list[$k]['limitBuyClearTime'] = date('Y-m-d H:i:s', time() + $expire);
+                    } else {
+                        $list[$k]['limitBuyClearTime'] = '';
+                    }
+                } else {
+                    $list[$k]['limitBuyClearTime'] = '';
+                }
+            }
         }
         return $list;
     }
@@ -744,9 +766,19 @@ class ProductClass extends BaseClass
         return $list;
     }
 
-    //输出价格 ssh 20211009
-    //$params 原来是 $addPriceMap,现在 $addPriceMap 值没用了,把它改成 $params ,用作传别的参数
-    public static function getFinalPrice($product, $level, $priceMap, $params, $changePrice = 0, $buyNum = 0)
+    /**
+     * 输出价格 ssh 20211009
+     * @param $product 花材信息
+     * @param $level 等级
+     * @param $priceMap 各个等级对应的price addPrice字段
+     * @param $params 原来是 $addPriceMap,现在 $addPriceMap 值没用了,把它改成 $params ,用作传别的参数
+     * @param int $changePrice 开单改价
+     * @param int $buyNum 当前购买数量
+     * @param int $totalBuyNum 已购买总数量
+     * @return int|string|null
+     * @throws \Exception
+     */
+    public static function getFinalPrice($product, $level, $priceMap, $params, $changePrice = 0, $buyNum = 0, $totalBuyNum = 0)
     {
         //开单改价优先级最高
         if ($changePrice > 0) {
@@ -767,10 +799,13 @@ class ProductClass extends BaseClass
             $price = $product[$currentPriceField] ?? 0;
         }
 
+        //特价与限购的综合处理
         $discountPrice = $product['discountPrice'] ?? 0;
         $hjDiscountPrice = $product['hjDiscountPrice'] ?? 0;
         $skDiscountPrice = $product['skDiscountPrice'] ?? 0;
+        $limitBuy = $product['limitBuy'];
         if ($discountPrice > 0) {
+            $oldPrice = $price;
             if ($level == 0) {
                 $price = $skDiscountPrice;
             }
@@ -780,7 +815,24 @@ class ProductClass extends BaseClass
             if ($level == 2) {
                 $price = $hjDiscountPrice;
             }
+
+            //超出限购情况的价格计算
+            if ($limitBuy > 0 && $totalBuyNum > $limitBuy) {
+                $hasBuyNum = $totalBuyNum - $buyNum;
+                if ($hasBuyNum >= $limitBuy) {
+                    $price = $oldPrice;
+                } else {
+                    $overBuyNum = $totalBuyNum - $limitBuy;
+                    $overBuyPrice = bcmul($overBuyNum, $oldPrice, 2); // 超出限购部分的价格
+
+                    $leftBuyNum = $buyNum - $overBuyNum;
+                    $leftBuyPrice = bcmul($leftBuyNum, $price, 2); // 未超出限购部分的价格
+                    $price = bcdiv($overBuyPrice + $leftBuyPrice, $buyNum, 2); // 最终价格
+                }
+            }
         }
+
+        //满多少数量,再优惠多少钱
         $reachNum = $product['reachNum'] ?? 0;
         $reachNumDiscount = $product['reachNumDiscount'] ?? 0;
         if ($reachNum > 0) {
@@ -1067,9 +1119,9 @@ class ProductClass extends BaseClass
             self::updateActualSoldById($productId, $newActualSold);
         }
 
-        //如果花材是自动补充库存类型的花材,则库存低于900时,自动补充到6850,多处要同步修改,关键词 auto_add_stock
+        //如果花材是自动补充库存类型的花材,则库存低于900时,自动补充到66820,多处要同步修改,关键词 auto_add_stock
         if ($productData->virtualStock == 1 && $newStock < 900) {
-            self::updateStockById($productId, 6850);
+            self::updateStockById($productId, 66820);
         }
 
         //库存=0 下架商品,特价去掉,限购去掉 lqh 2021.7.9 ssh 20251106 花材满减去掉 20260318
@@ -1390,22 +1442,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;
@@ -1421,12 +1472,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;
@@ -1442,8 +1496,12 @@ class ProductClass extends BaseClass
                 if ($book == 1 && $customShopAdminId > 0) {
                     $itemPrice = 0.1;
                 } else {
+                    $limitKey = self::LIMIT_BUY_KEY . $ghsProductId;
+                    $hasBuyNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $custom['id']]);
+                    $hasBuyNum = $itemNum + ($hasBuyNum == null ? 0 : $hasBuyNum); // 已购买总数量
+
                     //今日特价、会员价
-                    $itemPrice = self::getFinalPrice($currentGhsProduct, $level, $priceMap, $addPriceMap, $changePrice, $itemNum);
+                    $itemPrice = self::getFinalPrice($currentGhsProduct, $level, $priceMap, $addPriceMap, $changePrice, $itemNum, $hasBuyNum);
                 }
                 $thisPrice = $itemPrice;
 
@@ -1455,7 +1513,6 @@ class ProductClass extends BaseClass
                         $totalReachDiscount = bcadd($totalReachDiscount, $currentReachDiscount, 2);
                     }
                 }
-
             } else {
                 $itemPrice = $v['unitPrice'] ?? 0;
                 $thisPrice = $itemPrice;
@@ -1471,8 +1528,6 @@ class ProductClass extends BaseClass
             $tmp['productId'] = $ghsProductId;
             $tmp['price'] = $price;
 
-            $totalPrice = bcadd($totalPrice, $price, 2);
-
             $tmp['name'] = $currentGhsProduct['name'] ?? '';
             $tmp['smallUnit'] = $currentGhsProduct['smallUnit'] ?? '支';
             $tmp['bigUnit'] = $currentGhsProduct['bigUnit'] ?? '扎';
@@ -1496,11 +1551,16 @@ 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['kind'] = $currentGhsProduct['kind'] ?? 0;
             $tmp['belongCost'] = $currentGhsProduct['belongCost'] ?? 0;
+            $tmp['hjDiscountPrice'] = $currentGhsProduct['hjDiscountPrice'];
+            $tmp['discountPrice'] = $currentGhsProduct['discountPrice'];
+            $tmp['skDiscountPrice'] = $currentGhsProduct['skDiscountPrice'];
 
+            $totalPrice = bcadd($totalPrice, $price, 2);
             $totalBigNum += $bigNum;
             $totalSmallNum += $smallNum;
 
@@ -1515,16 +1575,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;
@@ -1535,8 +1589,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,
@@ -1568,8 +1624,14 @@ class ProductClass extends BaseClass
         return bcmul($mergeNum, $weight, 2);
     }
 
-    //修改product
-    public static function updateProduct($data, $shop)
+    /**
+     * 修改product
+     * @param $data
+     * @param $shop
+     * @param $type 所属平台:ghs 或 hd
+     * @throws \Exception
+     */
+    public static function updateProduct($data, $shop, $type)
     {
         $id = $data['id'] ?? 0;
         $adminId = $data['adminId'] ?? 0;
@@ -1702,7 +1764,11 @@ class ProductClass extends BaseClass
             $upData['limitBuy'] = $data['limitBuy'];
             //取消限购
             if ($data['limitBuy'] == 0) {
-                self::cancelLimitBuy($id);
+                if ($type == 'ghs') {
+                    self::cancelLimitBuy($id);
+                } else {
+                    \bizHd\product\classes\ProductClass::cancelLimitBuy($id);
+                }
             }
         }
         if (isset($data['stockWarning'])) {
@@ -1893,12 +1959,42 @@ class ProductClass extends BaseClass
         /***************产品模块*****************/
 
         self::updateById($id, $upData);
+
+        //创建限购缓存
+        $limitBuy = $data['limitBuy'] ?? 0;
+        if ($limitBuy > 0) {
+            $limitBuyClearType = $data['limitBuyClearType'];
+            $limitBuyClearLoopConfig = self::getLimitBuyClearLoopConfig($data);
+            if ($limitBuyClearType == 1 && !empty($limitBuyClearLoopConfig)) {
+                if ($type == 'ghs') {
+                    self::createRecurringLimitBuyCache($id, $limitBuyClearLoopConfig['intervalDays'], $limitBuyClearLoopConfig['clearHour']);
+                } else {
+                    \bizHd\product\classes\ProductClass::createRecurringLimitBuyCache($id, $limitBuyClearLoopConfig['intervalDays'], $limitBuyClearLoopConfig['clearHour']);
+                }
+            } elseif ($limitBuyClearType == 2 && isset($data['limitBuyClearTime']) && $data['limitBuyClearTime'] != '') {
+                $limitBuyClearTime = strtotime($data['limitBuyClearTime']);
+                if ($limitBuyClearTime > time()) {
+                    if ($type == 'ghs') {
+                        self::createLimitBuyCache($id, $limitBuyClearTime - time());
+                    } else {
+                        \bizHd\product\classes\ProductClass::createLimitBuyCache($id, $limitBuyClearTime - time());
+                    }
+                } else {
+                    util::fail('清空时间必须大于当前时间');
+                }
+            } else { // 没有设置清空时间,则删除缓存
+                if ($type == 'ghs') {
+                    self::createLimitBuyCache($id, 0);
+                } else {
+                    \bizHd\product\classes\ProductClass::createLimitBuyCache($id, 0);
+                }
+            }
+        }
+        
         //不影响其它直营店花材的上下架
         unset($upData['status']);
 
         if (isset($shop->default) && $shop->default == 1 && isset($shop->dataSync) && $shop->dataSync == 1) {
-
-
             /***************产品模块*****************/
             $ptClassList = PtCpClassClass::getAllByCondition(['delStatus' => 0], null, '*', 'id');
             /***************产品模块*****************/
@@ -2091,10 +2187,8 @@ class ProductClass extends BaseClass
 
                 //直营店同步价格等重要因素
                 self::updateById($chainProduct->id, $params);
-
             }
         }
-
     }
 
     //变成自动改价 2021.4.14
@@ -2355,7 +2449,7 @@ class ProductClass extends BaseClass
             $smallUnitId = $data['smallUnitId'] ?? 0;
 
             $changeUnit = 0;
-            if ($ptItemInfo->bigUnitId != $bigUnitId || $ptItemInfo->smallUnitId != $smallUnitId || $ptItemInfo->ratio != $ratio) {
+            if ($ptItemInfo !== null && ($ptItemInfo->bigUnitId != $bigUnitId || $ptItemInfo->smallUnitId != $smallUnitId || $ptItemInfo->ratio != $ratio)) {
                 //如果被修改了单位,则重新创建花材
                 $changeUnit = 1;
             }
@@ -2618,28 +2712,475 @@ class ProductClass extends BaseClass
         return $product['mainId'] ?? 0;
     }
 
+    /**
+     * 创建限购缓存(有过期时间)
+     * @param $productId 花材ID
+     * @param int $seconds 过期时间
+     * @param array $options ['clearAt' => 0, 'recurring' => 0, 'intervalDays' => 0, 'clearHour' => 0] 循环清空配置
+     * @return bool
+     */
+    public static function createLimitBuyCache($productId, $seconds = 0, $options = [])
+    {
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        $recurring = !empty($options['recurring']);
+
+        $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, 0]);
+        if (!empty($has)) {
+            // 获取缓存超时时间
+            $expire = Yii::$app->redis->executeCommand('TTL', [$limitKey]);
+            if ($expire > 0) {
+                //return;
+            }
+            //删除缓存
+            Yii::$app->redis->executeCommand('HDEL', [$limitKey, 0]);
+        }
+
+        Yii::$app->redis->executeCommand('HSET', [$limitKey, 0, 0]);
+        if ($seconds > 0) {
+            $clearAt = intval($options['clearAt'] ?? 0);
+            if ($clearAt <= 0) {
+                $clearAt = time() + intval($seconds);
+            }
+            Yii::$app->redis->executeCommand('EXPIRE', [$limitKey, $seconds]);
+            Yii::$app->redis->executeCommand('SET', [$clearMarkKey, $clearAt]);
+
+            if ($recurring) {
+                $intervalDays = intval($options['intervalDays'] ?? 0);
+                $clearHour = intval($options['clearHour'] ?? -1);
+                Yii::$app->redis->executeCommand('SET', [$clearLoopKey, json_encode([
+                    'intervalDays' => $intervalDays,
+                    'clearHour' => $clearHour,
+                ], JSON_UNESCAPED_UNICODE)]);
+            } else {
+                Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+            }
+
+            // 用 RabbitMQ 延迟消息在到期时清空订单项限购字段
+            $message = [
+                'type' => 'limit_buy_clear',
+                'ptType' => 'ghs',
+                'productId' => $productId,
+                'clearAt' => $clearAt,
+            ];
+            if ($recurring) {
+                $message['recurring'] = 1;
+                $message['intervalDays'] = intval($options['intervalDays'] ?? 0);
+                $message['clearHour'] = intval($options['clearHour'] ?? -1);
+            }
+            $message = serialize($message);
+            $producer = Yii::$app->rabbitmq->getProducer('stockProducer');
+            $producer->publish($message, 'limitBuyDelayExchange', 'limitBuyDelayRoute', [
+                'delivery_mode' => 2,
+                'content_type' => 'application/octet-stream',
+                'application_headers' => new AMQPTable([
+                    'x-delay' => intval($seconds * 1000),
+                ]),
+            ]);
+            Yii::info('限购延迟消息已发送: ' . json_encode([
+                'type' => 'limit_buy_clear',
+                'ptType' => 'ghs',
+                'productId' => intval($productId),
+                'clearAt' => intval($clearAt),
+                'delayMs' => intval($seconds * 1000),
+                'recurring' => $recurring ? 1 : 0,
+            ], JSON_UNESCAPED_UNICODE), __METHOD__);
+        } else {
+            Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
+            Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+        }
+    }
+
+    // 解析循环清空限购配置
+    public static function getLimitBuyClearLoopConfig($data)
+    {
+        if ($data['limitBuyClearType'] == 0) {
+            return [];
+        }
+        $hasIntervalDays = array_key_exists('limitBuyClearIntervalDays', $data) && $data['limitBuyClearIntervalDays'] !== '';
+        $hasClearHour = (array_key_exists('cleartAt', $data) && $data['cleartAt'] !== '')
+            || (array_key_exists('clearAt', $data) && $data['clearAt'] !== '');
+        if (!$hasIntervalDays && !$hasClearHour) {
+            return [];
+        }
+        if (!$hasIntervalDays || !$hasClearHour) {
+            util::fail('请填写循环清空限购的间隔天数和执行时间');
+        }
+        if (!is_numeric($data['limitBuyClearIntervalDays'])) {
+            util::fail('循环清空限购的间隔天数错误');
+        }
+        $clearHourValue = array_key_exists('cleartAt', $data) ? $data['cleartAt'] : $data['clearAt'];
+        if (!is_numeric($clearHourValue)) {
+            util::fail('循环清空限购的执行时间错误');
+        }
+        $intervalDays = intval($data['limitBuyClearIntervalDays']);
+        $clearHour = intval($clearHourValue);
+        if ($intervalDays <= 0) {
+            util::fail('循环清空限购的间隔天数必须大于0');
+        }
+        if ($clearHour < 0 || $clearHour > 23) {
+            util::fail('循环清空限购的执行时间必须在0点到23点之间');
+        }
+        return ['intervalDays' => $intervalDays, 'clearHour' => $clearHour];
+    }
+
+    /**
+     * 创建循环清空限购缓存
+     * @param $productId 花材ID
+     * @param int $intervalDays 循环清空限购的间隔天数
+     * @param int $clearHour 循环清空限购的执行时间点
+     * @param int $clearAt 循环清空限购的下次清空时间
+     * @return bool
+     */
+    public static function createRecurringLimitBuyCache($productId, $intervalDays, $clearHour, $clearAt = 0)
+    {
+        $productId = intval($productId);
+        $intervalDays = intval($intervalDays);
+        $clearHour = intval($clearHour);
+        if ($productId <= 0 || $intervalDays <= 0 || $clearHour < 0 || $clearHour > 23) {
+            return false;
+        }
+        if ($clearAt <= 0) {
+            $clearAt = self::getNextLimitBuyClearAt($intervalDays, $clearHour);
+        }
+        $seconds = max(1, $clearAt - time());
+        self::createLimitBuyCache($productId, $seconds, [
+            'recurring' => 1,
+            'intervalDays' => $intervalDays,
+            'clearHour' => $clearHour,
+            'clearAt' => $clearAt,
+        ]);
+        return true;
+    }
+
+    // 计算下一次循环清空时间
+    public static function getNextLimitBuyClearAt($intervalDays, $clearHour, $baseTime = 0)
+    {
+        $baseTime = $baseTime > 0 ? intval($baseTime) : time();
+        $targetDay = $baseTime + intval($intervalDays) * 86400;
+        $clearAt = strtotime(date('Y-m-d', $targetDay) . ' ' . sprintf('%02d:00:00', intval($clearHour)));
+        while ($clearAt <= $baseTime) {
+            $clearAt = strtotime('+' . intval($intervalDays) . ' days', $clearAt);
+        }
+        return $clearAt;
+    }
+
+    // 获取循环清空限购配置
+    public static function getLimitBuyClearLoopConfigByProductId($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return [];
+        }
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        $config = Yii::$app->redis->executeCommand('GET', [$clearLoopKey]);
+        if (empty($config)) {
+            return [];
+        }
+        $config = json_decode($config, true);
+        return is_array($config) ? $config : [];
+    }
+
+    // 获取限购清空配置,用于商品详情回显
+    public static function getLimitBuyClearInfo($productId)
+    {
+        $productId = intval($productId);
+        $info = [
+            'limitBuyClearType' => 0, // 0不清空 1定时清空 2循环清空
+            'limitBuyClearTypeName' => '不清空',
+            'limitBuyClearAt' => 0,
+            'limitBuyClearTime' => '',
+            'limitBuyNextClearAt' => 0,
+            'limitBuyNextClearTime' => '',
+            'limitBuyClearIntervalDays' => 0,
+            'cleartAt' => '',
+        ];
+        if ($productId <= 0) {
+            return $info;
+        }
+
+        $loopConfig = self::getLimitBuyClearLoopConfigByProductId($productId);
+        if (!empty($loopConfig)) {
+            $info['limitBuyClearType'] = 1;
+            $info['limitBuyClearTypeName'] = '循环清空';
+            $info['limitBuyClearIntervalDays'] = intval($loopConfig['intervalDays'] ?? 0);
+            $info['cleartAt'] = intval($loopConfig['clearHour'] ?? 0);
+            return $info;
+        }
+
+        $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
+        $clearAt = intval(Yii::$app->redis->executeCommand('GET', [$clearMarkKey]));
+        if ($clearAt <= 0) {
+            return $info;
+        }
+
+        $info['limitBuyClearType'] = 2;
+        $info['limitBuyClearTypeName'] = '定时清空';
+        $info['limitBuyClearAt'] = $clearAt;
+        $info['limitBuyClearTime'] = date('Y-m-d H:i:s', $clearAt);
+        $info['limitBuyNextClearAt'] = $clearAt;
+        $info['limitBuyNextClearTime'] = $info['limitBuyClearTime'];
+
+        return $info;
+    }
+
+    // 清理循环清空限购配置
+    public static function clearLimitBuyClearLoopConfig($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+        return true;
+    }
+
+    /**
+     * 记录限购缓存写入前快照(同一次请求内同商品同客户只记录一次)
+     *
+     * @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 = self::LIMIT_BUY_KEY . $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 getLimitBuyInfoByList($list, $customId)
+    {
+        if (empty($list) || !is_array($list)) {
+            return [];
+        }
+
+        $buyNumMap = [];
+        $ids = [];
+        foreach ($list as $item) {
+            $productId = $item['id'] ?? 0;
+            if (empty($productId)) {
+                continue;
+            }
+
+            $productId = intval($productId);
+            $bigNum = $item['bigCount'] ?? ($item['bigNum'] ?? 0);
+            $smallNum = $item['smallCount'] ?? ($item['smallNum'] ?? 0);
+            if (isset($buyNumMap[$productId]) == false) {
+                $buyNumMap[$productId] = [
+                    'bigNum' => 0,
+                    'smallNum' => 0,
+                ];
+            }
+            $buyNumMap[$productId]['bigNum'] = bcadd($buyNumMap[$productId]['bigNum'], $bigNum, 2);
+            $buyNumMap[$productId]['smallNum'] = bcadd($buyNumMap[$productId]['smallNum'], $smallNum, 2);
+            $ids[] = $productId;
+        }
+
+        $ids = array_values(array_unique($ids));
+        if (empty($ids)) {
+            return [];
+        }
+
+        $productData = self::getProductByIds($ids);
+        $productData = array_column($productData, null, 'id');
+        $respond = [];
+        foreach ($ids as $productId) {
+            $product = $productData[$productId] ?? [];
+            if (empty($product)) {
+                continue;
+            }
+
+            $limitBuy = $product['limitBuy'] ?? 0;
+            $isLimit = $limitBuy > 0 ? true : false;
+            $hasBuyNum = 0;
+            if ($isLimit) {
+                $limitKey = self::LIMIT_BUY_KEY . $productId;
+                $hasBuyNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+                $hasBuyNum = $hasBuyNum == null ? 0 : $hasBuyNum;
+            }
+
+            $buyNum = $buyNumMap[$productId] ?? [];
+            $ratio = $product['ratio'] ?? 1;
+            $currentBuyNum = self::mergeItemNum($buyNum['bigNum'] ?? 0, $buyNum['smallNum'] ?? 0, $ratio);
+            $totalBuyNum = bcadd($hasBuyNum, $currentBuyNum, 2);
+            $specialPrice = (
+                ($product['hjDiscountPrice'] ?? 0) > 0
+                && ($product['discountPrice'] ?? 0) > 0
+                && ($product['skDiscountPrice'] ?? 0) > 0
+            ) ? true : false;
+
+            $respond[] = [
+                'id' => $productId,
+                'name' => $product['name'] ?? '',
+                'isLimit' => $isLimit, //是否限购
+                'limitBuy' => (float)$limitBuy, //限购数量
+                'specialPrice' => $specialPrice, //是否特价
+                'hasBuyNum' => $isLimit ? (float)$hasBuyNum : 0, //已购买数量
+                'currentBuyNum' => (float)$currentBuyNum, //此次购买的数量
+                'reachLimitBuyNum' => ($isLimit && $totalBuyNum > $limitBuy) ? true : false, //是否达到限购数量
+                'exceedNum' => $isLimit ? (float)bcsub($hasBuyNum, $limitBuy, 2) : 0, //超出数量, 小于等于0表示未超出
+            ];
+        }
+
+        return $respond;
+    }
+
+    /**
+     * 限购处理
+     * @param $product
+     * @param $customId
+     * @param $num
+     */
+    public static function handleLimitBuy($product, $customId, $num)
+    {
+        $limitBuy = $product['limitBuy'];
+        if ($limitBuy <= 0) {
+            return;
+        }
+
+        $productId = $product['productId'];
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, $customId]);
+        if (!empty($has)) {
+            $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+            self::recordLimitBuyRollbackSnapshot($productId, $customId, true, $hasNum);
+            $lastNum = bcadd($hasNum, $num);
+            if ($lastNum > $limitBuy) {
+                // 如果有特价,则超过的数量按原价卖;否则中断
+                if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
+                    Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
+                    return;
+                }
+
+                util::error(-1, '累计已超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$lastNum - $limitBuy]);
+            }
+            Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
+        } else {
+            self::recordLimitBuyRollbackSnapshot($productId, $customId, false, 0);
+            if ($num > $limitBuy) {
+                // 如果有特价,则超过的数量按原价卖;否则中断
+                if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
+                    Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $num]);
+                    return;
+                }
+
+                util::error(-1, '超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$num - $limitBuy]);
+            }
+            Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $num]);
+        }
+    }
+
     //取消限购的缓存 ssh 20240605
     public static function cancelLimitBuy($productId)
     {
-        $limitKey = 'limit_buy_' . $productId;
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
         $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
         if (!empty($arr)) {
             foreach ($arr as $field) {
                 self::baseClearLimitBuy($productId, $field);
             }
         }
+        self::clearLimitBuyClearMark($productId);
+        self::clearLimitBuyClearLoopConfig($productId);
+    }
+
+    // 校验清空限购消息是否是当前有效版本
+    public static function checkLimitBuyClearMessage($productId, $clearAt)
+    {
+        $productId = intval($productId);
+        $clearAt = intval($clearAt);
+        if ($productId <= 0 || $clearAt <= 0) {
+            return false;
+        }
+
+        $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
+        $currentClearAt = intval(Yii::$app->redis->executeCommand('GET', [$clearMarkKey]));
+        return $currentClearAt > 0 && $currentClearAt == $clearAt;
+    }
+
+    // 清理限购清空标记
+    public static function clearLimitBuyClearMark($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
+        return true;
     }
 
     //$num -1 表示减全部,大于-1表示减值
     public static function baseClearLimitBuy($productId, $customId, $num = -1)
     {
-        $limitKey = 'limit_buy_' . $productId;
+        $limitKey = self::LIMIT_BUY_KEY . $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 = $hasNum > $num ? bcsub($hasNum, $num) : 0;
             $remainNum = floor($remainNum);
             if ($remainNum > 0) {
                 Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $remainNum]);
@@ -2649,11 +3190,23 @@ class ProductClass extends BaseClass
         }
     }
 
+    // 只清空已购买记录,不关闭商品限购配置
+    public static function clearLimitBuyRecordByProductId($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$limitKey]);
+        return true;
+    }
+
     public static function getHasLimitBuyList($product)
     {
         $productId = $product->id;
-        $limitBuy = $product->limitBuy ?? 0;
-        $limitKey = 'limit_buy_' . $productId;
+        $limitBuy = $product->limitBuy;
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
         $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
         $customList = [];
         if (!empty($arr)) {
@@ -2664,18 +3217,39 @@ class ProductClass extends BaseClass
                 $numMap[$customId] = $num;
                 $ids[] = $customId;
             }
+
+            $specialPrice = ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0); // 销售花材是否开启特价
             $customList = CustomClass::getAllByCondition(['id' => ['in', $ids]], null, '*');
             foreach ($customList as $key => $val) {
                 $customId = $val['id'];
                 $num = $numMap[$customId] ?? 0;
                 $customList[$key]['hasLimitBuyNum'] = $num;
-                $customList[$key]['hasReachLimitBuyNum'] = $num >= $limitBuy ? 1 : 0;
+                $customList[$key]['reachLimitBuyNum'] = $num >= $limitBuy ? 1 : 0;
                 $avatar = $val['avatar'] ?? '';
                 $smallAvatar = imgUtil::groupImg($avatar);
                 $customList[$key]['smallAvatar'] = $smallAvatar . "?x-oss-process=image/resize,m_fill,h_80,w_80";
+                $customList[$key]['specialPrice'] = $specialPrice;
             }
         }
         return $customList;
     }
 
+    /**
+     * 根据花材清空订单项限购值
+     *
+     * @param int $productId
+     * @return bool
+     */
+    public static function clearLimitBuyByProductId($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+
+        self::updateById($productId, ['limitBuy' => 0]);
+        return true;
+    }
+
 }
+

+ 3 - 4
biz-ghs/product/services/ProductService.php

@@ -127,11 +127,10 @@ class ProductService extends BaseService
     //组装分类花材数据
     public static function assembleData($classInfo, $itemInfoData, $showTodayDiscount = false)
     {
-        //今日特价
-        $tjItem = [];
-        $mjItem = [];
+        $tjItem = []; //特价
+        $mjItem = []; //满减
         $itemGroup = [];
-        $preSellItem = [];
+        $preSellItem = []; //预售
         if ($itemInfoData) {
             foreach ($itemInfoData as $v) {
                 $reachNum = $v['reachNum'] ?? 0;

+ 13 - 0
biz-hd/cg/classes/CgTreeClass.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace bizHd\cg\classes;
+
+use Yii;
+use bizGhs\base\classes\BaseClass;
+
+class CgTreeClass extends BaseClass
+{
+
+    public static $baseFile = '\bizHd\cg\models\CgTree';
+
+}

+ 15 - 0
biz-hd/cg/models/CgTree.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace bizHd\cg\models;
+
+use bizHd\base\models\Base;
+
+class CgTree extends Base
+{
+
+    public static function tableName()
+    {
+        return 'xhCgTree';
+    }
+
+}

+ 24 - 9
biz-hd/deduct/classes/DeductClass.php

@@ -5,14 +5,13 @@ namespace bizHd\deduct\classes;
 use bizHd\balance\classes\BalanceChangeClass;
 use bizHd\balance\classes\BalanceGiveChangeClass;
 use bizHd\balance\classes\BalancePayChangeClass;
+use bizHd\custom\classes\CustomClass;
 use common\components\dict;
 use common\components\util;
-use Yii;
 use bizHd\base\classes\BaseClass;
 
 class DeductClass extends BaseClass
 {
-
     public static $baseFile = '\bizHd\deduct\models\Deduct';
 
     public static function doDeduct($shop, $custom, $hd, $amount, $params)
@@ -22,10 +21,7 @@ class DeductClass extends BaseClass
             util::fail('余额不够扣');
         }
         $custom->balance = bcsub($custom->balance, $amount, 2);
-        $custom->save();
-
         $hd->balance = bcsub($hd->balance, $amount, 2);
-        $hd->save();
 
         if (floatval($hd->balance) != floatval($custom->balance)) {
             util::fail('余额有问题');
@@ -95,10 +91,8 @@ class DeductClass extends BaseClass
         /************************* 使用充值余额 *****************************/
         $customBalancePay = bcsub($custom->balancePay, $payAmount, 2);
         $custom->balancePay = $customBalancePay;
-        $custom->save();
         $hdBalancePay = bcsub($hd->balancePay, $payAmount, 2);
         $hd->balancePay = $hdBalancePay;
-        $hd->save();
         $payChange = [
             'hdId' => $hdId,
             'hdName' => $hdName,
@@ -129,10 +123,8 @@ class DeductClass extends BaseClass
                 util::fail('余额异常哈!请联系管理员,编号' . $hdId . ' ' . $custom->balanceGive);
             }
             $custom->balanceGive = $customBalanceGive;
-            $custom->save();
             $hdBalanceGive = bcsub($hd->balanceGive, $mustUseGiveAmount, 2);
             $hd->balanceGive = $hdBalanceGive;
-            $hd->save();
             if (floatval($customBalanceGive) != floatval($hdBalanceGive)) {
                 util::fail('余额有问题呢!请联系管理员,编号' . $hdId);
             }
@@ -162,6 +154,29 @@ class DeductClass extends BaseClass
         if (floatval($addBalance) != floatval($custom->balance)) {
             util::fail('账户余额有问题!请检查哈,编号' . $customId);
         }
+
+        // 仅对从充值余额(balancePay)扣减的部分扣积分/成长值
+        $changeAmount = $payAmount;
+        if (bccomp($changeAmount, 0, 2) === 1) {
+            $newIntegral = bcsub($custom->integral, $changeAmount, 2);
+            if (bccomp($newIntegral, 0, 2) === -1) {
+                $newIntegral = 0;
+            }
+            $newGrowth = bcsub($custom->growth, $changeAmount, 2);
+            if (bccomp($newGrowth, 0, 2) === -1) {
+                $newGrowth = 0;
+            }
+            $custom->integral = $newIntegral;
+            $custom->growth = $newGrowth;
+            $memberData = CustomClass::getCustomExpenseLevel($custom->growth, $mainId); // 设置等级多处需要同步修改的,请搜索:getCustomExpenseLevel
+            CustomClass::updateById($customId, [
+                'member' => (int) ($memberData['level'] ?? 0),
+                'memberName' => (string) ($memberData['name'] ?? ''),
+            ]);
+        }
+
+        $custom->save();
+        $hd->save();
     }
 
 }

+ 14 - 1
biz-hd/order/classes/OrderClass.php

@@ -16,7 +16,6 @@ use bizHd\custom\classes\CustomClass;
 use bizHd\custom\classes\HdClass;
 use bizHd\hb\classes\HbClass;
 use bizHd\item\classes\ItemClass;
-use bizHd\member\classes\MemberLevelClass;
 use bizHd\merchant\classes\SjClass;
 use bizHd\refund\classes\HdRefundClass;
 use bizHd\shop\classes\MainClass;
@@ -745,6 +744,7 @@ class OrderClass extends BaseClass
                 StockRecordClass::addSellStockOrderCancelRecord($recordData);
             }
         }
+
         $goodsList = OrderGoodsClass::getAllByCondition(['orderSn' => $orderSn], null, '*', null, true);
         if (!empty($goodsList)) {
             foreach ($goodsList as $goods) {
@@ -774,6 +774,7 @@ class OrderClass extends BaseClass
 
             }
         }
+
         $shopId = $order->shopId ?? 0;
         $shop = ShopClass::getById($shopId, true);
         if (!empty($shop)) {
@@ -798,6 +799,18 @@ class OrderClass extends BaseClass
                 //noticeUtil::push('零售订单:' . $orderSn . ',关单成功。', '15280215347');
             }
         }
+
+        //清理限购缓存
+        $customId = $order->customId ?? 0;
+        if (!empty($customId) && !empty($itemList)) {
+            foreach ($itemList as $item) {
+                $productId = $item['itemId'];
+                $num = floor($item['num']);
+                if (!empty($productId) && $num > 0) {
+                    \bizHd\product\classes\ProductClass::baseClearLimitBuy($productId, $customId, $num);
+                }
+            }
+        }
     }
 
     // 恢复订单 -- 即:把订单的状态从取消状态变成待付款状态

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

@@ -4,14 +4,21 @@ namespace bizHd\product\classes;
 
 use biz\item\classes\PtItemClass;
 use bizHd\base\classes\BaseClass;
+use bizHd\custom\classes\CustomClass;
 use common\components\imgUtil;
 use common\components\noticeUtil;
 use common\components\sms;
 use common\components\util;
+use PhpAmqpLib\Wire\AMQPTable;
 use Yii;
 
 class ProductClass extends BaseClass
 {
+    const LIMIT_BUY_KEY = 'hd_limit_buy:'; // 零售客户与批发客户不同,这个要与批发不同
+
+    const CLEAR_MARK_KEY = 'limit_buy_clear_at:';
+    const CLEAR_LOOP_KEY = 'limit_buy_clear_loop:';
+    const LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY = 'limitBuyRollbackSnapshot'; //记录限购旧值快照缓存键
 
     public static $baseFile = '\bizHd\product\models\Product';
 
@@ -424,4 +431,499 @@ class ProductClass extends BaseClass
         return $list;
     }
 
+    /**
+     * 限购处理
+     * @param $product
+     * @param $customId 零售xhCustom表的id
+     * @param $num
+     */
+    public static function handleLimitBuy($product, $customId, $num, $doCache = true)
+    {
+        $limitBuy = $product['limitBuy'];
+        if ($limitBuy <= 0) {
+            return;
+        }
+
+        $productId = $product['productId'];
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, $customId]);
+        if (!empty($has)) {
+            $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+            self::recordLimitBuyRollbackSnapshot($productId, $customId, true, $hasNum);
+            $lastNum = bcadd($hasNum, $num, 2);
+            if ($lastNum > $limitBuy) {
+                // 如果有特价,则超过的数量按原价卖;否则中断
+                if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
+                    Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
+                    return;
+                }
+
+                util::error(-1, '累计已超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$lastNum - $limitBuy]);
+            }
+            if ($doCache) {
+                Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $lastNum]);
+            }
+        } else {
+            self::recordLimitBuyRollbackSnapshot($productId, $customId, false, 0);
+            if ($num > $limitBuy) {
+                // 如果有特价,则超过的数量按原价卖;否则中断
+                if ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0) {
+                    Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $num]);
+                    return;
+                }
+                util::error(-1, '超出限购数', ['productId'=>$productId, 'limitBuy'=>$limitBuy, 'exceed'=>$num - $limitBuy]);
+            }
+            if ($doCache) {
+                Yii::$app->redis->executeCommand('HSET', [$limitKey, $customId, $num]);
+            }
+        }
+    }
+
+    /**
+     * 获取零售客户当前清单中各花材的限购情况(Redis 键为 hd_limit_buy:,与批发商 limit_buy_ 区分)
+     * @param array $list 花材列表,项含 id 及 bigCount/smallCount 或 bigNum/smallNum
+     * @param int|string $customId 零售端 xhCustom.id(花店客户)
+     */
+    public static function getLimitBuyInfoByList($list, $customId)
+    {
+        if (empty($list) || !is_array($list)) {
+            return [];
+        }
+
+        $buyNumMap = [];
+        $ids = [];
+        foreach ($list as $item) {
+            $productId = $item['id'] ?? 0;
+            if (empty($productId)) {
+                continue;
+            }
+
+            $productId = intval($productId);
+            $bigNum = $item['bigCount'] ?? ($item['bigNum'] ?? 0);
+            $smallNum = $item['smallCount'] ?? ($item['smallNum'] ?? 0);
+            if (isset($buyNumMap[$productId]) == false) {
+                $buyNumMap[$productId] = [
+                    'bigNum' => 0,
+                    'smallNum' => 0,
+                ];
+            }
+            $buyNumMap[$productId]['bigNum'] = bcadd($buyNumMap[$productId]['bigNum'], $bigNum, 2);
+            $buyNumMap[$productId]['smallNum'] = bcadd($buyNumMap[$productId]['smallNum'], $smallNum, 2);
+            $ids[] = $productId;
+        }
+
+        $ids = array_values(array_unique($ids));
+        if (empty($ids)) {
+            return [];
+        }
+
+        $productData = \bizGhs\product\classes\ProductClass::getProductByIds($ids);
+        $productData = array_column($productData, null, 'id');
+        $respond = [];
+        foreach ($ids as $productId) {
+            $product = $productData[$productId] ?? [];
+            if (empty($product)) {
+                continue;
+            }
+
+            $limitBuy = $product['limitBuy'] ?? 0;
+            $isLimit = $limitBuy > 0 ? true : false;
+            $hasBuyNum = 0;
+            if ($isLimit) {
+                $limitKey = self::LIMIT_BUY_KEY . $productId;
+                $hasBuyNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+                $hasBuyNum = $hasBuyNum == null ? 0 : $hasBuyNum;
+            }
+
+            $buyNum = $buyNumMap[$productId] ?? [];
+            $ratio = $product['ratio'] ?? 1;
+            $currentBuyNum = \bizGhs\product\classes\ProductClass::mergeItemNum($buyNum['bigNum'] ?? 0, $buyNum['smallNum'] ?? 0, $ratio);
+            $totalBuyNum = bcadd($hasBuyNum, $currentBuyNum, 2);
+            $specialPrice = (
+                ($product['hjDiscountPrice'] ?? 0) > 0
+                && ($product['discountPrice'] ?? 0) > 0
+                && ($product['skDiscountPrice'] ?? 0) > 0
+            ) ? true : false;
+
+            $respond[] = [
+                'id' => $productId,
+                'name' => $product['name'] ?? '',
+                'isLimit' => $isLimit,
+                'limitBuy' => (float)$limitBuy,
+                'specialPrice' => $specialPrice,
+                'hasBuyNum' => $isLimit ? (float)$hasBuyNum : 0,
+                'currentBuyNum' => (float)$currentBuyNum,
+                'reachLimitBuyNum' => ($isLimit && $totalBuyNum > $limitBuy) ? true : false,
+                'exceedNum' => $isLimit ? (float)bcsub($hasBuyNum, $limitBuy, 2) : 0,
+            ];
+        }
+
+        return $respond;
+    }
+
+    //取消限购的缓存
+    public static function cancelLimitBuy($productId)
+    {
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
+        if (!empty($arr)) {
+            foreach ($arr as $field) {
+                self::baseClearLimitBuy($productId, $field);
+            }
+        }
+        self::clearLimitBuyClearMark($productId);
+        self::clearLimitBuyClearLoopConfig($productId);
+    }
+
+    public static function getHasLimitBuyList($product)
+    {
+        $productId = $product->id;
+        $limitBuy = $product->limitBuy ?? 0;
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        $arr = Yii::$app->redis->executeCommand('HKEYS', [$limitKey]);
+        $customList = [];
+        if (!empty($arr)) {
+            $numMap = [];
+            $ids = [];
+            foreach ($arr as $customId) {
+                $num = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+                $numMap[$customId] = $num;
+                $ids[] = $customId;
+            }
+
+            $specialPrice = ($product['hjDiscountPrice'] > 0 && $product['discountPrice'] > 0 && $product['skDiscountPrice'] > 0); // 销售花材是否开启特价
+            $customList = CustomClass::getAllByCondition(['id' => ['in', $ids]], null, '*');
+            foreach ($customList as $key => $val) {
+                $customId = $val['id'];
+                $num = $numMap[$customId] ?? 0;
+                $customList[$key]['hasLimitBuyNum'] = $num;
+                $customList[$key]['reachLimitBuyNum'] = $num >= $limitBuy ? 1 : 0;
+                $avatar = $val['avatar'] ?? '';
+                $smallAvatar = imgUtil::groupImg($avatar);
+                $customList[$key]['smallAvatar'] = $smallAvatar . "?x-oss-process=image/resize,m_fill,h_80,w_80";
+                $customList[$key]['specialPrice'] = $specialPrice;
+            }
+        }
+        return $customList;
+    }
+
+    /**
+     * @param $productId
+     * @param $customId
+     * @param int $num  -1 表示减全部,大于-1表示减值
+     */
+    public static function baseClearLimitBuy($productId, $customId, $num = -1)
+    {
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        if ($num <= -1) {
+            //清掉全部
+            Yii::$app->redis->executeCommand('HDEL', [$limitKey, $customId]);
+        } else {
+            $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+            $remainNum = $hasNum > $num ? bcsub($hasNum, $num) : 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 clearLimitBuyRecordByProductId($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$limitKey]);
+        return true;
+    }
+
+    // 清理限购清空标记
+    public static function clearLimitBuyClearMark($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
+        return true;
+    }
+
+    /**
+     * 创建限购缓存(有过期时间)
+     * @param $productId 花材ID
+     * @param int $seconds 过期时间
+     * @param array $options ['clearAt' => 0, 'recurring' => 0, 'intervalDays' => 0, 'clearHour' => 0] 循环清空配置
+     * @return bool
+     */
+    public static function createLimitBuyCache($productId, $seconds = 0, $options = [])
+    {
+        $limitKey = self::LIMIT_BUY_KEY . $productId;
+        $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        $recurring = !empty($options['recurring']);
+
+        $has = Yii::$app->redis->executeCommand('HEXISTS', [$limitKey, 0]);
+        if (!empty($has)) {
+            // 获取缓存超时时间
+            $expire = Yii::$app->redis->executeCommand('TTL', [$limitKey]);
+            if ($expire > 0) {
+                //return;
+            }
+            //删除缓存
+            Yii::$app->redis->executeCommand('HDEL', [$limitKey, 0]);
+        }
+
+        Yii::$app->redis->executeCommand('HSET', [$limitKey, 0, 0]);
+        if ($seconds > 0) {
+            $clearAt = intval($options['clearAt'] ?? 0);
+            if ($clearAt <= 0) {
+                $clearAt = time() + intval($seconds);
+            }
+            Yii::$app->redis->executeCommand('EXPIRE', [$limitKey, $seconds]);
+            Yii::$app->redis->executeCommand('SET', [$clearMarkKey, $clearAt]);
+            if ($recurring) {
+                $intervalDays = intval($options['intervalDays'] ?? 0);
+                $clearHour = intval($options['clearHour'] ?? -1);
+                Yii::$app->redis->executeCommand('SET', [$clearLoopKey, json_encode([
+                    'intervalDays' => $intervalDays,
+                    'clearHour' => $clearHour,
+                ], JSON_UNESCAPED_UNICODE)]);
+            } else {
+                Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+            }
+
+            // 用 RabbitMQ 延迟消息在到期时清空订单项限购字段
+            $message = [
+                'type' => 'limit_buy_clear',
+                'ptType' => 'hd',
+                'productId' => $productId,
+                'clearAt' => $clearAt,
+            ];
+            if ($recurring) {
+                $message['recurring'] = 1;
+                $message['intervalDays'] = intval($options['intervalDays'] ?? 0);
+                $message['clearHour'] = intval($options['clearHour'] ?? -1);
+            }
+            $message = serialize($message);
+            $producer = Yii::$app->rabbitmq->getProducer('stockProducer');
+            $producer->publish($message, 'limitBuyDelayExchange', 'limitBuyDelayRoute', [
+                'delivery_mode' => 2,
+                'content_type' => 'application/octet-stream',
+                'application_headers' => new AMQPTable([
+                    'x-delay' => intval($seconds * 1000),
+                ]),
+            ]);
+            Yii::info('限购延迟消息已发送: ' . json_encode([
+                    'type' => 'limit_buy_clear',
+                    'ptType' => 'hd',
+                    'productId' => intval($productId),
+                    'clearAt' => intval($clearAt),
+                    'delayMs' => intval($seconds * 1000),
+                    'recurring' => $recurring ? 1 : 0,
+                ], JSON_UNESCAPED_UNICODE), __METHOD__);
+        } else {
+            Yii::$app->redis->executeCommand('DEL', [$clearMarkKey]);
+            Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+        }
+    }
+
+    // 创建循环清空限购缓存
+    public static function createRecurringLimitBuyCache($productId, $intervalDays, $clearHour, $clearAt = 0)
+    {
+        $productId = intval($productId);
+        $intervalDays = intval($intervalDays);
+        $clearHour = intval($clearHour);
+        if ($productId <= 0 || $intervalDays <= 0 || $clearHour < 0 || $clearHour > 23) {
+            return false;
+        }
+        if ($clearAt <= 0) {
+            $clearAt = self::getNextLimitBuyClearAt($intervalDays, $clearHour);
+        }
+        $seconds = max(1, $clearAt - time());
+        self::createLimitBuyCache($productId, $seconds, [
+            'recurring' => 1,
+            'intervalDays' => $intervalDays,
+            'clearHour' => $clearHour,
+            'clearAt' => $clearAt,
+        ]);
+        return true;
+    }
+
+    // 计算下一次循环清空时间
+    public static function getNextLimitBuyClearAt($intervalDays, $clearHour, $baseTime = 0)
+    {
+        $baseTime = $baseTime > 0 ? intval($baseTime) : time();
+        $targetDay = $baseTime + intval($intervalDays) * 86400;
+        $clearAt = strtotime(date('Y-m-d', $targetDay) . ' ' . sprintf('%02d:00:00', intval($clearHour)));
+        while ($clearAt <= $baseTime) {
+            $clearAt = strtotime('+' . intval($intervalDays) . ' days', $clearAt);
+        }
+        return $clearAt;
+    }
+
+    // 获取循环清空限购配置
+    public static function getLimitBuyClearLoopConfigByProductId($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return [];
+        }
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        $config = Yii::$app->redis->executeCommand('GET', [$clearLoopKey]);
+        if (empty($config)) {
+            return [];
+        }
+        $config = json_decode($config, true);
+        return is_array($config) ? $config : [];
+    }
+
+    // 获取限购清空配置,用于商品详情回显
+    public static function getLimitBuyClearInfo($productId)
+    {
+        $productId = intval($productId);
+        $info = [
+            'limitBuyClearType' => 0, // 0不清空 1循环清空 2定时清空
+            'limitBuyClearTypeName' => '不清空',
+            'limitBuyClearAt' => 0,
+            'limitBuyClearTime' => '',
+            'limitBuyNextClearAt' => 0,
+            'limitBuyNextClearTime' => '',
+            'limitBuyClearIntervalDays' => 0,
+            'cleartAt' => '',
+        ];
+        if ($productId <= 0) {
+            return $info;
+        }
+
+        $loopConfig = self::getLimitBuyClearLoopConfigByProductId($productId);
+        if (!empty($loopConfig)) {
+            $info['limitBuyClearType'] = 1;
+            $info['limitBuyClearTypeName'] = '循环清空';
+            $info['limitBuyClearIntervalDays'] = intval($loopConfig['intervalDays'] ?? 0);
+            $info['cleartAt'] = intval($loopConfig['clearHour'] ?? 0);
+            return $info;
+        }
+
+        $clearMarkKey = self::CLEAR_MARK_KEY . $productId;
+        $clearAt = intval(Yii::$app->redis->executeCommand('GET', [$clearMarkKey]));
+        if ($clearAt <= 0) {
+            return $info;
+        }
+
+        $info['limitBuyClearType'] = 2;
+        $info['limitBuyClearTypeName'] = '定时清空';
+        $info['limitBuyClearAt'] = $clearAt;
+        $info['limitBuyClearTime'] = date('Y-m-d H:i:s', $clearAt);
+        $info['limitBuyNextClearAt'] = $clearAt;
+        $info['limitBuyNextClearTime'] = $info['limitBuyClearTime'];
+
+        return $info;
+    }
+
+    // 清理循环清空限购配置
+    public static function clearLimitBuyClearLoopConfig($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+        return true;
+    }
+
+    /**
+     * 记录限购缓存写入前快照(同一次请求内同商品同客户只记录一次)
+     *
+     * @param int $productId
+     * @param int $customId 零售xhCustom表的id
+     * @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),
+        ];
+    }
+
+    /**
+     * 根据花材清空订单项限购值
+     *
+     * @param int $productId
+     * @return bool
+     */
+    public static function clearLimitBuyByProductId($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+
+        self::updateById($productId, ['limitBuy' => 0]);
+        return true;
+    }
+
+    /**
+     * 回滚当前请求中已记录的限购缓存变更
+     *
+     * @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 = self::LIMIT_BUY_KEY . $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;
+    }
+
+    /**
+     * 清理当前请求中记录的限购回滚快照
+     *
+     * @return void
+     */
+    public static function clearLimitBuyRollbackSnapshot()
+    {
+        unset(Yii::$app->params[self::LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY]);
+    }
+
 }

+ 26 - 9
biz-hd/purchase/classes/PurchaseClass.php

@@ -6,8 +6,6 @@ use biz\ghs\classes\GhsClass;
 use biz\product\classes\XjClass;
 use biz\shop\classes\ShopCapitalClass;
 use biz\shop\classes\ShopClass;
-use biz\shop\classes\ShopCouponClass;
-use biz\sj\classes\SjClass;
 use biz\stat\classes\StatCgClass;
 use biz\stat\classes\StatCgGhsClass;
 use biz\stat\classes\StatOutClass;
@@ -21,12 +19,11 @@ use bizGhs\order\traits\OrderTrait;
 use bizGhs\product\classes\ProductClass;
 use bizGhs\stock\classes\OnStockRecordClass;
 use bizGhs\stock\classes\StockRecordClass;
-use bizGhs\stock\services\StockRecordService;
 use bizHd\cg\classes\CgRefundClass;
+use bizHd\cg\classes\CgTreeClass;
 use bizHd\cg\classes\CgXjClass;
 use bizHd\shop\classes\MainClass;
 use bizHd\stat\classes\StatIncomeClass;
-use bizHd\wx\classes\WxOpenClass;
 use common\components\dict;
 use common\components\imgUtil;
 use common\components\IntraCityExpress;
@@ -819,9 +816,14 @@ class PurchaseClass extends BaseClass
             $itemId = $hdProductData[$hdProductId]['itemId'] ?? 0;
             $rank = $hdProductData[$hdProductId]['rank'] ?? 'B';
             $stock = $hdProductData[$hdProductId]['stock'] ?? 0;
+            $kind = $hdProductData[$hdProductId]['kind'] ?? 0;
             $stockFormat = ProductClass::formatStock($stock, $ratio);
 
+            $limitKey = ProductClass::LIMIT_BUY_KEY . $ghsProductId; // 注意:要使用批发的花材id
+            $hasNum = Yii::$app->redis->executeCommand('HGET', [$limitKey, $customId]);
+            $hasBuyNum = $hasNum ?: 0;
             $itemNum = ProductClass::mergeItemNum($product['bigNum'], $product['smallNum'], $ratio);
+            $hasBuyNum = bcadd($itemNum, $hasBuyNum, 2);  // 已购买总数量
 
             $weight = $hdProductData[$hdProductId]['weight'] ?? 0;
             $currentWeight = bcmul($weight, $itemNum, 2);
@@ -840,7 +842,7 @@ class PurchaseClass extends BaseClass
                     $price = 0.1;
                 } else {
                     //用批发店花材的价格
-                    $price = ProductClass::getFinalPrice($currentGhsProduct, $giveLevel, $priceMap, $addPriceMap, $userPrice, $itemNum);
+                    $price = ProductClass::getFinalPrice($currentGhsProduct, $giveLevel, $priceMap, $addPriceMap, $userPrice, $itemNum, $hasBuyNum);
                 }
                 $thisPrice = $price;
 
@@ -908,6 +910,7 @@ class PurchaseClass extends BaseClass
             $productList[$key]['ghsId'] = $ghsId;
             $productList[$key]['name'] = $itemName;
             $productList[$key]['cover'] = $cover;
+            $productList[$key]['kind'] = $kind;
 
             $currentBigUnit = $hdProductData[$hdProductId]['bigUnit'] ?? '';
             $currentSmallUnit = $hdProductData[$hdProductId]['smallUnit'] ?? '';
@@ -1071,14 +1074,14 @@ class PurchaseClass extends BaseClass
         //保存订单
         $respond = self::add($data, true);
 
-        foreach ($productList as $productKey => $productVal) {
+        foreach ($productList as $productVal) {
             //增加采购单的花材
             $returnItem = PurchaseItemClass::add($productVal, true);
-
+            $productId = $productVal['productId'];
             $unitPrice = $returnItem->price ?? 0;
             $currentId = $returnItem->id ?? 0;
             $currentPtItemId = $returnItem->itemId ?? 0;
-            if (isset($data['xj'][$currentPtItemId]) && !empty($data['xj'][$currentPtItemId])) {
+            if (!empty($data['xj'][$currentPtItemId])) {
                 $xjArr = $data['xj'][$currentPtItemId];
                 if (is_array($xjArr)) {
                     $ids = array_column($xjArr, 'id');
@@ -1097,7 +1100,21 @@ class PurchaseClass extends BaseClass
                     $returnItem->save();
                 }
             }
-
+            if (!empty($data['treeData'][$currentPtItemId])) {
+                $myTreeData = $data['treeData'][$currentPtItemId];
+                foreach($myTreeData as $treeItem) {
+                    $treeNum = $treeItem['num'] ?? 0;
+                    $treeParams = [
+                        'orderSn' => $orderSn,
+                        'num' => $treeNum,
+                        'productId' => $productId,
+                    ];
+                    CgTreeClass::add($treeParams);
+                    $dishNum = count($myTreeData);
+                    $returnItem->dishNum = $dishNum;
+                    $returnItem->save();
+                }
+            }
         }
 
         $shop = ShopClass::getLockById($shopId);

+ 23 - 10
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;
 
@@ -135,8 +127,7 @@ class PurchaseService extends BaseService
             'sendDistance' => $sendDistance,
         ];
         $result = OrderClass::addOrder($arr,$custom);
-        $saleId = $result['id'] ?? 0;
-        $respond->saleId = $saleId;
+        $respond->saleId = $result['id'];
         $respond->save();
 
         return $respond;
@@ -204,6 +195,7 @@ class PurchaseService extends BaseService
         $id = $purchase->id;
         $purchase = PurchaseClass::cancel($id);
         $saleId = $purchase->saleId;
+        
         //销售单失效
         OrderClass::cancel($saleId);
         $couponId = $purchase->hbId;
@@ -211,6 +203,27 @@ class PurchaseService extends BaseService
             $coupon = ShopCouponClass::getLockById($couponId);
             ShopCouponClass::rollback($coupon);
         }
+
+        // OrderClass::cancel($saleId) 中已经清理了
+        //清空限购缓存
+        // $order = OrderClass::getById($saleId, true);
+        // if (!empty($order)) {
+        //     $customId = $order->customId ?? 0;
+        //     $orderSn = $order->orderSn;
+        //     if (!empty($customId)) {
+        //         $orderItemList = OrderItemClass::getOrderItem($orderSn);
+        //         if (!empty($orderItemList)) {
+        //             foreach ($orderItemList as $item) {
+        //                 $productId = $item['productId'] ?? 0;
+        //                 $num = floor($item['num'] ?? 0);
+        //                 if (!empty($productId) && $num > 0) {
+        //                     ProductClass::baseClearLimitBuy($productId, $customId, $num);
+        //                 }
+        //             }
+        //         }
+        //     }
+        // }
+
         return $purchase;
     }
 

+ 9 - 2
biz-hd/recharge/classes/RechargeClass.php

@@ -277,8 +277,15 @@ class RechargeClass extends BaseClass
         $addIntegralAndGrowth = bccomp($changeAmount, 0, 2) === 1; // 如果变化金额大于0,则计算积分与成长值
         if ($addIntegralAndGrowth) {
             //积分与成长值
-            $custom->integral = bcadd($custom->integral, $changeAmount);
-            $custom->growth = bcadd($custom->growth, $changeAmount);
+            $custom->integral = bcadd($custom->integral, $changeAmount, 2);
+            $custom->growth = bcadd($custom->growth, $changeAmount, 2);
+
+            // 由于没有更新 buyAmount,所以得额外添加更新等级
+            $memberData = CustomClass::getCustomExpenseLevel($custom->growth, $mainId); // 设置等级多处需要同步修改的,请搜索:getCustomExpenseLevel
+            CustomClass::updateById($customId, [
+                'member' => (int) ($memberData['level'] ?? 0),
+                'memberName' => (string) ($memberData['name'] ?? ''),
+            ]);
         }
         $custom->save();
         $hd->save();

+ 0 - 1
biz-hd/stat/classes/StatStudentClass.php

@@ -20,7 +20,6 @@ class StatStudentClass extends BaseClass
             $stat = self::add(['shopId' => $shopId, 'sjId' => $sjId, 'time' => $date], true);
         }
         $stat->riseNum += 1;
-        $stat->totalNum = $shop->totalStudent + 1;
         $stat->save();
         StatStudentMonthClass::addStudent($shop);
     }

+ 0 - 1
biz-hd/stat/classes/StatStudentMonthClass.php

@@ -21,7 +21,6 @@ class StatStudentMonthClass extends BaseClass
             $stat = self::add(['sjId' => $sjId, 'shopId' => $shopId, 'time' => $month], true);
         }
         $stat->riseNum += 1;
-        $stat->totalNum = $shop['totalStudent'] + 1;
         $stat->save();
     }
 

+ 0 - 2
biz-mall/order/classes/OrderClass.php

@@ -2,7 +2,6 @@
 
 namespace bizMall\order\classes;
 
-use biz\shop\models\Shop;
 use biz\stat\classes\StatOrderCountClass;
 use bizMall\merchant\classes\MerchantAssetClass;
 use bizMall\shop\classes\MainClass;
@@ -12,7 +11,6 @@ use bizMall\user\classes\UserClass;
 use common\components\imgUtil;
 use common\components\mapUtil;
 use common\components\miniUtil;
-use common\components\noticeUtil;
 use common\components\orderSn;
 use bizMall\base\classes\BaseClass;
 use common\components\util;

+ 0 - 2
biz-mall/order/services/OrderService.php

@@ -3,11 +3,9 @@
 namespace bizMall\order\services;
 
 use bizMall\base\services\BaseService;
-use bizMall\custom\classes\CustomClass;
 use bizMall\goods\classes\GoodsClass;
 use bizMall\item\classes\ItemClass;
 use bizMall\merchant\services\MerchantCapitalService;
-use bizMall\merchant\services\MerchantService;
 use bizMall\order\classes\OrderClass;
 use bizMall\order\classes\OrderGoodsClass;
 use bizMall\order\classes\OrderItemClass;

+ 0 - 1
biz/product/classes/ProductClass.php

@@ -3,7 +3,6 @@
 namespace biz\product\classes;
 
 use biz\base\classes\BaseClass;
-use bizGhs\item\classes\ItemClassClass;
 
 class ProductClass extends BaseClass
 {

+ 76 - 0
common/components/rabbitmq/baseConsumer.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace common\components\rabbitmq;
+
+use mikemadisonweb\rabbitmq\components\ConsumerInterface;
+use common\components\noticeUtil;
+use Yii;
+
+abstract class baseConsumer implements ConsumerInterface
+{
+    /**
+     * 在长驻进程中保证 db 连接可用
+     */
+    protected function ensureDbConnection()
+    {
+        $db = Yii::$app->db;
+        try {
+            if (!$db->isActive) {
+                $db->open();
+                return;
+            }
+            $db->createCommand('SELECT 1')->queryScalar();
+        } catch (\Throwable $e) {
+            // 连接失效时主动重建
+            $db->close();
+            $db->open();
+        }
+    }
+
+    /**
+     * 遇到 MySQL 断连时自动重连并重试一次
+     *
+     * @param callable $callback
+     * @return mixed
+     * @throws \Throwable
+     */
+    protected function runWithDbReconnect(callable $callback)
+    {
+        try {
+            return $callback();
+        } catch (\Throwable $e) {
+            if (!$this->isMysqlConnectionLost($e)) {
+                throw $e;
+            }
+            Yii::warning('检测到 MySQL 连接断开,尝试重连并重试一次: ' . $e->getMessage(), __METHOD__);
+            noticeUtil::push('检测到 MySQL 连接断开,尝试重连并重试一次: ' . $e->getMessage(), __METHOD__);
+            Yii::$app->db->close();
+            Yii::$app->db->open();
+            return $callback();
+        }
+    }
+
+    /**
+     * 判断是否属于 MySQL 连接丢失错误
+     *
+     * @param \Throwable $e
+     * @return bool
+     */
+    protected function isMysqlConnectionLost(\Throwable $e)
+    {
+        $message = $e->getMessage();
+        if (stripos($message, 'server has gone away') !== false) {
+            return true;
+        }
+        if (stripos($message, 'Lost connection to MySQL server') !== false) {
+            return true;
+        }
+        if (stripos($message, 'SQLSTATE[HY000] [2006]') !== false) {
+            return true;
+        }
+        if (stripos($message, 'SQLSTATE[HY000] [2013]') !== false) {
+            return true;
+        }
+        return false;
+    }
+}

+ 93 - 0
common/components/rabbitmq/cancelLimitBuyConsumer.php

@@ -0,0 +1,93 @@
+<?php
+/**
+ * 取消限购消费者
+ * 处理取消限购操作
+ */
+
+namespace common\components\rabbitmq;
+
+use bizGhs\product\classes\ProductClass;
+use common\components\noticeUtil;
+use mikemadisonweb\rabbitmq\components\ConsumerInterface;
+use PhpAmqpLib\Message\AMQPMessage;
+
+class cancelLimitBuyConsumer extends baseConsumer
+{
+    /**
+     * 执行消费者逻辑
+     * 
+     * @param AMQPMessage $msg 消息对象
+     * @return string 消息处理结果
+     * 
+     * ConsumerInterface::MSG_ACK - 确认消息(标记为已处理)并从队列中删除
+     * ConsumerInterface::MSG_REJECT - 拒绝并从队列中删除消息
+     * ConsumerInterface::MSG_REQUEUE - 拒绝并重新入队消息
+     */
+    public function execute(AMQPMessage $msg)
+    {
+        try {
+            $this->ensureDbConnection();
+            // 反序列化消息体
+            $data = unserialize($msg->body);
+            if (!is_array($data)) {
+                noticeUtil::push("取消限购的消费者报错:Invalid notify message format: {$msg->body}", '15280215347');
+                return ConsumerInterface::MSG_REJECT;
+            }
+            print_r($data);
+            // 根据操作类型分发处理
+            $type = $data['type'] ?? null;
+            $result = $this->runWithDbReconnect(function () use ($type, $data) {
+                switch ($type) {
+                    case 'limit_buy_clear':
+                        return $this->clearOrderItemLimitBuy($data);
+                    default:
+                        noticeUtil::push("取消限购的消费者报错,未知 type: {$type}");
+                        return false;
+                }
+            });
+            if ($result) {
+                return ConsumerInterface::MSG_ACK;
+            } else {
+                noticeUtil::push("取消限购的消费者报错:Stock message processing failed");
+                return ConsumerInterface::MSG_REQUEUE;
+            }
+        } catch (\Exception $e) {
+            noticeUtil::push("取消限购的消费者报错:" . $e->getMessage());
+            //return ConsumerInterface::MSG_REQUEUE;
+            return ConsumerInterface::MSG_ACK;
+        }
+    }
+
+    /**
+     * 清空订单项限购值
+     *
+     * @param array $data
+     * @return bool
+     */
+    private function clearOrderItemLimitBuy($data)
+    {
+        $productId = intval($data['productId'] ?? 0);
+        if ($productId <= 0) {
+            noticeUtil::push('取消限购的消费者报错:limit_buy_clear 缺少 productId', '15280215347');
+            return true;
+        }
+
+        $clearAt = intval($data['clearAt'] ?? 0);
+        if ($clearAt <= 0) {
+            noticeUtil::push('取消限购的消费者报错:limit_buy_clear 缺少 clearAt', '15280215347');
+            return true;
+        }
+
+        // 旧消息直接忽略,避免“先到期的旧消息”提前清空
+        if (!ProductClass::checkLimitBuyClearMessage($productId, $clearAt)) {
+            return true;
+        }
+
+        $result = ProductClass::clearLimitBuyByProductId($productId);
+        if ($result) {
+            ProductClass::clearLimitBuyClearMark($productId);
+        }
+        return $result;
+    }
+
+}

+ 18 - 18
common/components/rabbitmq/customConsumer.php

@@ -12,7 +12,7 @@ use common\components\noticeUtil;
 use mikemadisonweb\rabbitmq\components\ConsumerInterface;
 use PhpAmqpLib\Message\AMQPMessage;
 
-class customConsumer implements ConsumerInterface
+class customConsumer extends baseConsumer
 {
     /**
      * 执行消费者逻辑
@@ -27,6 +27,7 @@ class customConsumer implements ConsumerInterface
     public function execute(AMQPMessage $msg)
     {
         try {
+            $this->ensureDbConnection();
             // 反序列化消息体
             $data = unserialize($msg->body);
             if (!is_array($data)) {
@@ -36,23 +37,22 @@ class customConsumer implements ConsumerInterface
             print_r($data);
             // 根据操作类型分发处理
             $type = $data['type'] ?? null;
-            switch ($type) {
-                case 'add_custom':
-                    //添加新客户
-                    $result = CustomClass::generateCustom($data);
-                    break;
-                case 'pull_custom_from_other_shop':
-                    //从分店拉取客户
-                    $result = CustomClass::pullOtherShopCustom($data);
-                    break;
-                case 'hd_change_custom_expense_level':
-                    //客户消费等级变更
-                    $result = HdCustomClass::updateCustomExpenseLevel($data);
-                    break;
-                default:
-                    noticeUtil::push("客户的消费者报错,不存在的类型: {$type}");
-                    $result = false;
-            }
+            $result = $this->runWithDbReconnect(function () use ($type, $data) {
+                switch ($type) {
+                    case 'add_custom':
+                        //添加新客户
+                        return CustomClass::generateCustom($data);
+                    case 'pull_custom_from_other_shop':
+                        //从分店拉取客户
+                        return CustomClass::pullOtherShopCustom($data);
+                    case 'hd_change_custom_expense_level':
+                        //客户消费等级变更
+                        return HdCustomClass::updateCustomExpenseLevel($data);
+                    default:
+                        noticeUtil::push("客户的消费者报错,不存在的类型: {$type}");
+                        return false;
+                }
+            });
             if ($result) {
                 return ConsumerInterface::MSG_ACK;
             } else {

+ 24 - 26
common/components/rabbitmq/notifyConsumer.php

@@ -16,7 +16,7 @@ use common\components\push;
 use mikemadisonweb\rabbitmq\components\ConsumerInterface;
 use PhpAmqpLib\Message\AMQPMessage;
 
-class notifyConsumer implements ConsumerInterface
+class notifyConsumer extends baseConsumer
 {
     /**
      * 执行消费者逻辑
@@ -31,6 +31,7 @@ class notifyConsumer implements ConsumerInterface
     public function execute(AMQPMessage $msg)
     {
         try {
+            $this->ensureDbConnection();
             // 反序列化消息体
             $data = unserialize($msg->body);
             if (!is_array($data)) {
@@ -40,31 +41,28 @@ class notifyConsumer implements ConsumerInterface
             print_r($data);
             // 根据通知类型分发处理
             $type = $data['type'] ?? null;
-            switch ($type) {
-                case 'ghs_new_order_notify':
-                    //供货商的新订单通知
-                    $result = $this->ghsNewOrderNotify($data);
-                    break;
-                case 'hd_new_order_notify':
-                    //花店的新订单通知
-                    $result = $this->hdNewOrderNotify($data);
-                    break;
-                case 'hd_new_cg_notify':
-                    //花店的新采购单通知
-                    $result = $this->hdNewCgNotify($data);
-                    break;
-                case 'ghs_pt_error':
-                    //供货商跑腿异常通知
-                    $result = $this->ghsPtErrorAction($data);
-                    break;
-                case 'hd_pt_error':
-                    //花店跑腿异常通知
-                    $result = $this->hdPtErrorAction($data);
-                    break;
-                default:
-                    noticeUtil::push("通知的消费者提示:Unknown notify type: {$type}", '15280215347');
-                    $result = false;
-            }
+            $result = $this->runWithDbReconnect(function () use ($type, $data) {
+                switch ($type) {
+                    case 'ghs_new_order_notify':
+                        //供货商的新订单通知
+                        return $this->ghsNewOrderNotify($data);
+                    case 'hd_new_order_notify':
+                        //花店的新订单通知
+                        return $this->hdNewOrderNotify($data);
+                    case 'hd_new_cg_notify':
+                        //花店的新采购单通知
+                        return $this->hdNewCgNotify($data);
+                    case 'ghs_pt_error':
+                        //供货商跑腿异常通知
+                        return $this->ghsPtErrorAction($data);
+                    case 'hd_pt_error':
+                        //花店跑腿异常通知
+                        return $this->hdPtErrorAction($data);
+                    default:
+                        noticeUtil::push("通知的消费者提示:Unknown notify type: {$type}", '15280215347');
+                        return false;
+                }
+            });
             if ($result) {
                 return ConsumerInterface::MSG_ACK;
             } else {

+ 15 - 14
common/components/rabbitmq/ptConsumer.php

@@ -8,7 +8,7 @@ use common\components\noticeUtil;
 use mikemadisonweb\rabbitmq\components\ConsumerInterface;
 use PhpAmqpLib\Message\AMQPMessage;
 
-class ptConsumer implements ConsumerInterface
+class ptConsumer extends baseConsumer
 {
     /**
      * 执行消费者逻辑
@@ -23,6 +23,7 @@ class ptConsumer implements ConsumerInterface
     public function execute(AMQPMessage $msg)
     {
         try {
+            $this->ensureDbConnection();
             // 反序列化消息体
             $data = unserialize($msg->body);
             if (!is_array($data)) {
@@ -33,19 +34,19 @@ class ptConsumer implements ConsumerInterface
 
             // 根据操作类型分发处理
             $type = $data['type'] ?? null;
-            switch ($type) {
-                case 'hd_pt_create_order':
-                    //花店跑腿开始下单
-                    $result = \bizHd\express\classes\HdDeliveryOrderClass::beginCreateOrder($data);
-                    break;
-                case 'ghs_pt_create_order':
-                    //供货商跑腿开始下单
-                    $result = \bizGhs\express\classes\GhsDeliveryOrderClass::beginCreateOrder($data);
-                    break;
-                default:
-                    noticeUtil::push("跑腿发单的消费者报错,不存在的类型: {$type}");
-                    $result = false;
-            }
+            $result = $this->runWithDbReconnect(function () use ($type, $data) {
+                switch ($type) {
+                    case 'hd_pt_create_order':
+                        //花店跑腿开始下单
+                        return \bizHd\express\classes\HdDeliveryOrderClass::beginCreateOrder($data);
+                    case 'ghs_pt_create_order':
+                        //供货商跑腿开始下单
+                        return \bizGhs\express\classes\GhsDeliveryOrderClass::beginCreateOrder($data);
+                    default:
+                        noticeUtil::push("跑腿发单的消费者报错,不存在的类型: {$type}");
+                        return false;
+                }
+            });
 
             if ($result == ConsumerInterface::MSG_ACK) {
                 return ConsumerInterface::MSG_ACK;

+ 92 - 5
common/components/rabbitmq/stockConsumer.php

@@ -6,12 +6,14 @@
 
 namespace common\components\rabbitmq;
 
+use bizGhs\product\classes\ProductClass;
+use bizHd\product\classes\ProductClass as hdProductClass;
 use common\components\noticeUtil;
 use mikemadisonweb\rabbitmq\components\ConsumerInterface;
 use PhpAmqpLib\Message\AMQPMessage;
 use Yii;
 
-class stockConsumer implements ConsumerInterface
+class stockConsumer extends baseConsumer
 {
     /**
      * 执行消费者逻辑
@@ -26,20 +28,30 @@ class stockConsumer implements ConsumerInterface
     public function execute(AMQPMessage $msg)
     {
         try {
+            $this->ensureDbConnection();
             // 反序列化消息体
             $data = unserialize($msg->body);
             if (!is_array($data)) {
-                noticeUtil::push("库存的消费者报错:Invalid notify message format: {$msg->body}", '15280215347');
+                noticeUtil::push("库存的消费者报错:Invalid notify message format: {$msg->body}");
                 return ConsumerInterface::MSG_REJECT;
             }
             print_r($data);
             // 根据操作类型分发处理
             $type = $data['type'] ?? null;
+            if ($type == 'limit_buy_clear') {
+                Yii::info('限购延迟消息开始消费: ' . json_encode($data, JSON_UNESCAPED_UNICODE), __METHOD__);
+                Yii::getLogger()->flush(true);
+            }
             switch ($type) {
                 case 'add':
                     $result = true;
                     echo '持久化OK---';
-                    print_r($data);
+                    break;
+                case 'limit_buy_clear':
+                    echo 'limit_buy_clear---';
+                    $result = $this->runWithDbReconnect(function () use ($data) {
+                        return $this->clearOrderItemLimitBuy($data);
+                    });
                     break;
                 default:
                     noticeUtil::push("库存的消费者报错,未知 type: {$type}");
@@ -49,12 +61,87 @@ class stockConsumer implements ConsumerInterface
                 return ConsumerInterface::MSG_ACK;
             } else {
                 noticeUtil::push("库存的消费者报错:Stock message processing failed");
-                return ConsumerInterface::MSG_REQUEUE;
+                //return ConsumerInterface::MSG_REQUEUE;
+                return ConsumerInterface::MSG_ACK;
             }
         } catch (\Exception $e) {
             noticeUtil::push("库存的消费者报错:" . $e->getMessage());
-            return ConsumerInterface::MSG_REQUEUE;
+            //return ConsumerInterface::MSG_REQUEUE;
+            return ConsumerInterface::MSG_ACK;
+        }
+    }
+
+    /**
+     * 清空订单项限购值
+     *
+     * @param array $data
+     * @return bool
+     */
+    private function clearOrderItemLimitBuy($data)
+    {
+        $productId = intval($data['productId']);
+        if ($productId <= 0) {
+            noticeUtil::push('取消限购的消费者报错:limit_buy_clear 缺少 productId');
+            return true;
+        }
+
+        $clearAt = intval($data['clearAt'] ?? 0);
+        if ($clearAt <= 0) {
+            noticeUtil::push('取消限购的消费者报错:limit_buy_clear 缺少 clearAt');
+            return true;
         }
+
+        $ptType = $data['ptType'] ?? 'ghs';
+        $productClass = $ptType == 'ghs' ? ProductClass::class : hdProductClass::class;
+
+        // 旧消息直接忽略,避免“先到期的旧消息”提前清空
+        if (!$productClass::checkLimitBuyClearMessage($productId, $clearAt)) {
+            return true;
+        }
+
+        $recurring = intval($data['recurring'] ?? 0);
+        if ($recurring == 1) {
+            $result = $productClass::clearLimitBuyRecordByProductId($productId);
+            if ($result) {
+                $intervalDays = intval($data['intervalDays'] ?? 0);
+                $clearHour = intval($data['clearHour'] ?? -1);
+                if ($intervalDays <= 0 || $clearHour < 0 || $clearHour > 23) {
+                    $config = $productClass::getLimitBuyClearLoopConfigByProductId($productId);
+                    $intervalDays = intval($config['intervalDays'] ?? 0);
+                    $clearHour = intval($config['clearHour'] ?? -1);
+                }
+                if ($intervalDays > 0 && $clearHour >= 0 && $clearHour <= 23) {
+                    $productClass::createRecurringLimitBuyCache($productId, $intervalDays, $clearHour);
+                } else {
+                    $productClass::clearLimitBuyClearMark($productId);
+                    noticeUtil::push('循环限购清理成功但缺少下一次调度配置: ' . json_encode([
+                        'ptType' => $ptType,
+                        'productId' => $productId,
+                        'clearAt' => $clearAt,
+                    ], JSON_UNESCAPED_UNICODE));
+                }
+            } else {
+                noticeUtil::push('限购字段清理失败: ' . json_encode([
+                        'ptType' => $ptType,
+                        'productId' => $productId,
+                        'clearAt' => $clearAt,
+                    ], JSON_UNESCAPED_UNICODE));
+            }
+            return $result;
+        }
+
+        $result = $productClass::clearLimitBuyByProductId($productId);
+        if ($result) {
+            $productClass::clearLimitBuyClearMark($productId);
+        } else {
+            noticeUtil::push('限购字段清理失败: ' . json_encode([
+                    'ptType' => $ptType,
+                    'productId' => $productId,
+                    'clearAt' => $clearAt,
+                ], JSON_UNESCAPED_UNICODE));
+        }
+
+        return $result;
     }
 
 }

+ 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]);
     }
 
     //成功输出内容

+ 22 - 2
common/config/rabbitMQ.php

@@ -36,6 +36,14 @@ $rabbitMQ = [
             'name' => 'ptExchange',
             'type' => 'direct',
             'durable' => true,
+        ],
+        [
+            'name' => 'limitBuyDelayExchange',
+            'type' => 'x-delayed-message',
+            'durable' => true,
+            'arguments' => new \PhpAmqpLib\Wire\AMQPTable([
+                'x-delayed-type' => 'direct',
+            ]),
         ]
     ],
 
@@ -63,6 +71,11 @@ $rabbitMQ = [
             'passive' => false,
             'durable' => true,
         ],
+        [
+            'name' => 'limitBuyQueue',
+            'passive' => false,
+            'durable' => true,
+        ],
     ],
 
     /**
@@ -89,6 +102,11 @@ $rabbitMQ = [
             'queue' => 'ptQueue',
             'exchange' => 'ptExchange',
             'routing_keys' => ['ptRoute'],
+        ],
+        [
+            'queue' => 'limitBuyQueue',
+            'exchange' => 'limitBuyDelayExchange',
+            'routing_keys' => ['limitBuyDelayRoute'],
         ]
     ],
 
@@ -111,7 +129,7 @@ $rabbitMQ = [
         [
             //跑腿下单生产者
             'name' => 'ptProducer',
-        ],
+        ]
     ],
 
     /**
@@ -130,6 +148,8 @@ $rabbitMQ = [
             'name' => 'stockConsumer',
             'callbacks' => [
                 'stockQueue' => '\common\components\rabbitmq\stockConsumer',
+                //'limitBuyQueue' => '\common\components\rabbitmq\cancelLimitBuyConsumer',
+                'limitBuyQueue' => '\common\components\rabbitmq\stockConsumer',
             ]
         ],
         [
@@ -145,7 +165,7 @@ $rabbitMQ = [
             'callbacks' => [
                 'ptQueue' => '\common\components\rabbitmq\ptConsumer',
             ]
-        ],
+        ]
     ],
 ];
 return $rabbitMQ;

+ 0 - 4
console/controllers/HdOrderController.php

@@ -9,11 +9,7 @@ use common\components\noticeUtil;
 use common\components\util;
 use yii\console\Controller;
 use Yii;
-use biz\shop\classes\ShopExtClass;
-use bizHd\shop\classes\ShopClass;
 use bizHd\wx\classes\WxOpenClass;
-use common\components\payUtil;
-use biz\wx\classes\WxMessageClass;
 
 class HdOrderController extends Controller
 {

+ 1 - 2
console/controllers/PurchaseController.php

@@ -44,14 +44,13 @@ class PurchaseController extends Controller
                 $transaction = $connection->beginTransaction();
                 $id = $purchase['id'] ?? 0;
                 $orderSn = $purchase['orderSn'] ?? '';
-                try {
 
+                try {
                     $current = PurchaseClass::getById($id, true);
                     PurchaseService::expire($current);
                     $transaction->commit();
 
                     //noticeUtil::push("采购单过期未付款,库存已回滚,单号:{$orderSn}");
-
                 } catch (\Exception $e) {
                     $transaction->rollBack();
                     $msg = $e->getMessage();

+ 0 - 3
console/controllers/ShopCouponController.php

@@ -3,9 +3,6 @@
 namespace console\controllers;
 
 use biz\shop\classes\ShopCouponClass;
-use bizHd\purchase\classes\PurchaseClass;
-use bizHd\purchase\models\Purchase;
-use bizHd\purchase\services\PurchaseService;
 use common\components\noticeUtil;
 use yii\console\Controller;
 use Yii;

+ 1 - 1
vendor/mikemadisonweb/yii2-rabbitmq/Configuration.php

@@ -293,7 +293,7 @@ class Configuration extends Component
             if (!isset($exchange['type'])) {
                 throw new InvalidConfigException('Exchange type should be specified.');
             }
-            $allowed = ['direct', 'topic', 'fanout', 'headers'];
+            $allowed = ['direct', 'topic', 'fanout', 'headers', 'x-delayed-message'];
             if (!in_array($exchange['type'], $allowed, true)) {
                 $allowed = implode(', ', $allowed);
                 throw new InvalidConfigException("Unknown exchange type `{$exchange['type']}`. Allowed values are: {$allowed}");

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff