Bläddra i källkod

限购清除在定时清除的基础上,增加循环清除:
1. 前端传 limitBuyClearIntervalDays 和 cleartAt 时启用循环清空;我会兼容 clearAt 作为别名,避免后续拼写修正导致接口不兼容。
2. limitBuyClearIntervalDays 必须是正整数,cleartAt/clearAt 必须是 0-23 的整点小时。
3. 首次执行按你确认的规则计算:从当前时间先加 limitBuyClearIntervalDays 天,再落到所选小时;若落点不大于当前时间,再补一个周期。
4. 循环到点后只清空 Redis 中该商品的已购买数量,不把 xhGhsItem.limitBuy 改为 0。

shizhongqi 3 månader sedan
förälder
incheckning
3a7f5013d9

+ 156 - 10
biz-ghs/product/classes/ProductClass.php

@@ -48,6 +48,7 @@ class ProductClass extends BaseClass
 
     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()
@@ -1957,16 +1958,26 @@ class ProductClass extends BaseClass
         self::updateById($id, $upData);
 
         //创建限购缓存
-        if ($data['limitBuy'] > 0 && isset($data['limitBuyClearTime']) && $data['limitBuyClearTime'] != '') {
-            $limitBuyClearTime = strtotime($data['limitBuyClearTime']);
-            if ($limitBuyClearTime > time()) {
+        $limitBuy = $data['limitBuy'] ?? 0;
+        if ($limitBuy > 0) {
+            $limitBuyClearLoopConfig = self::getLimitBuyClearLoopConfig($data);
+            if (!empty($limitBuyClearLoopConfig)) {
                 if ($type == 'ghs') {
-                    self::createLimitBuyCache($id, $limitBuyClearTime - time());
+                    self::createRecurringLimitBuyCache($id, $limitBuyClearLoopConfig['intervalDays'], $limitBuyClearLoopConfig['clearHour']);
                 } else {
-                    \bizHd\product\classes\ProductClass::createLimitBuyCache($id, $limitBuyClearTime - time());
+                    \bizHd\product\classes\ProductClass::createRecurringLimitBuyCache($id, $limitBuyClearLoopConfig['intervalDays'], $limitBuyClearLoopConfig['clearHour']);
+                }
+            } elseif (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 {
-                util::fail('清空时间必须大于当前时间');
             }
         }
         
@@ -2691,11 +2702,19 @@ class ProductClass extends BaseClass
         return $product['mainId'] ?? 0;
     }
 
-    // 创建限购缓存(有过期时间)
-    public static function createLimitBuyCache($productId, $seconds = 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)) {
@@ -2707,12 +2726,27 @@ class ProductClass extends BaseClass
             //删除缓存
             Yii::$app->redis->executeCommand('HDEL', [$limitKey, 0]);
         }
+
         Yii::$app->redis->executeCommand('HSET', [$limitKey, 0, 0]);
         if ($seconds > 0) {
-            $clearAt = time() + intval($seconds);
+            $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',
@@ -2720,6 +2754,11 @@ class ProductClass extends BaseClass
                 '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', [
@@ -2735,10 +2774,104 @@ class ProductClass extends BaseClass
                 '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)
+    {
+        $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];
+    }
+
+    // 创建循环清空限购缓存
+    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 clearLimitBuyClearLoopConfig($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+        return true;
     }
 
     /**
@@ -2868,6 +3001,7 @@ class ProductClass extends BaseClass
             }
         }
         self::clearLimitBuyClearMark($productId);
+        self::clearLimitBuyClearLoopConfig($productId);
     }
 
     // 校验清空限购消息是否是当前有效版本
@@ -2915,6 +3049,18 @@ 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;

+ 110 - 4
biz-hd/product/classes/ProductClass.php

@@ -16,6 +16,7 @@ class ProductClass extends BaseClass
 {
     const LIMIT_BUY_KEY = 'hd_limit_buy:';
     const CLEAR_MARK_KEY = 'hd_limit_buy_clear_at:';
+    const CLEAR_LOOP_KEY = 'hd_limit_buy_clear_loop:';
     const LIMIT_BUY_ROLLBACK_SNAPSHOT_KEY = 'hd_limitBuyRollbackSnapshot'; //记录限购旧值快照缓存键
 
     public static $baseFile = '\bizHd\product\models\Product';
@@ -488,6 +489,7 @@ class ProductClass extends BaseClass
             }
         }
         self::clearLimitBuyClearMark($productId);
+        self::clearLimitBuyClearLoopConfig($productId);
     }
 
     public static function getHasLimitBuyList($product)
@@ -545,6 +547,18 @@ 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 clearLimitBuyClearMark($productId)
     {
@@ -557,11 +571,19 @@ class ProductClass extends BaseClass
         return true;
     }
 
-    // 创建限购缓存(有过期时间)
-    public static function createLimitBuyCache($productId, $seconds = 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)) {
@@ -573,11 +595,25 @@ class ProductClass extends BaseClass
             //删除缓存
             Yii::$app->redis->executeCommand('HDEL', [$limitKey, 0]);
         }
+
         Yii::$app->redis->executeCommand('HSET', [$limitKey, 0, 0]);
         if ($seconds > 0) {
-            $clearAt = time() + intval($seconds);
+            $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 = [
@@ -586,6 +622,11 @@ class ProductClass extends BaseClass
                 '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', [
@@ -599,11 +640,76 @@ class ProductClass extends BaseClass
                     'type' => 'limit_buy_clear',
                     'ptType' => 'hd',
                     'productId' => intval($productId),
-                    'clearAt' => intval($clearAt)
+                    '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 clearLimitBuyClearLoopConfig($productId)
+    {
+        $productId = intval($productId);
+        if ($productId <= 0) {
+            return false;
+        }
+        $clearLoopKey = self::CLEAR_LOOP_KEY . $productId;
+        Yii::$app->redis->executeCommand('DEL', [$clearLoopKey]);
+        return true;
     }
 
     /**

+ 1 - 1
common/components/rabbitmq/cancelLimitBuyConsumer.php

@@ -84,7 +84,7 @@ class cancelLimitBuyConsumer extends baseConsumer
             return true;
         }
 
-        $result = OrderItemClass::clearLimitBuyByProductId($productId);
+        $result = ProductClass::clearLimitBuyByProductId($productId);
         if ($result) {
             ProductClass::clearLimitBuyClearMark($productId);
         }

+ 34 - 12
common/components/rabbitmq/stockConsumer.php

@@ -91,32 +91,54 @@ class stockConsumer extends baseConsumer
             return true;
         }
 
+        $ptType = $data['ptType'] ?? 'ghs';
+        $productClass = $ptType == 'ghs' ? ProductClass::class : hdProductClass::class;
+
         // 旧消息直接忽略,避免“先到期的旧消息”提前清空
-        if (!ProductClass::checkLimitBuyClearMessage($productId, $clearAt)) {
+        if (!$productClass::checkLimitBuyClearMessage($productId, $clearAt)) {
             return true;
         }
 
-        $ptType = $data['ptType'];
-        if ($ptType == 'ghs') {
-            $result = ProductClass::clearLimitBuyByProductId($productId);
+        $recurring = intval($data['recurring'] ?? 0);
+        if ($recurring == 1) {
+            $result = $productClass::clearLimitBuyRecordByProductId($productId);
             if ($result) {
-                ProductClass::clearLimitBuyClearMark($productId);
-            } else {
-                noticeUtil::push('限购字段清理失败: ' . json_encode([
+                $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 {
-            $result = hdProductClass::clearLimitBuyByProductId($productId);
-            if ($result) {
-                hdProductClass::clearLimitBuyClearMark($productId);
+                }
             } 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;