diff --git a/app/Console/Commands/PriceChannelTest.php b/app/Console/Commands/PriceChannelTest.php index 9a7155a..dfbda74 100644 --- a/app/Console/Commands/PriceChannelTest.php +++ b/app/Console/Commands/PriceChannelTest.php @@ -20,7 +20,7 @@ class PriceChannelTest extends Command $this->line('قابلیت روشن (price_channel_enabled): ' . ($info['enabled'] ? 'بله' : 'خیر')); $this->line('پیام‌رسان‌های انتخاب‌شده: ' . (implode(', ', $info['selected_messengers']) ?: '— (خالی)')); $this->line('حداقل فاصله ارسال (ثانیه): ' . $info['min_seconds']); - $this->line('فاصله cooldown گذشته؟ ' . ($info['cooldown_elapsed'] ? 'بله' : 'خیر (هنوز زوده)')); + $this->line('فاصله cooldown گذشته؟ ' . ($info['cooldown_elapsed'] ? 'بله' : 'خیر (هنوز ' . ($info['cooldown_seconds_remaining'] ?? 0) . ' ثانیه مانده)')); $this->line('منبع قالب: ' . ($info['template_source'] === 'db' ? 'تنظیمات ادمین' : 'پیش‌فرض داخلی')); if (! empty($info['live_trade_buy_rial'])) { $this->line('قیمت فروش اپ (buy API): ' . number_format((int) $info['live_trade_buy_rial']) . ' ریال'); diff --git a/app/Services/PriceChannelBroadcastService.php b/app/Services/PriceChannelBroadcastService.php index 09b82fb..381f5cc 100644 --- a/app/Services/PriceChannelBroadcastService.php +++ b/app/Services/PriceChannelBroadcastService.php @@ -76,9 +76,8 @@ TXT; } try { - // شرط ۱: از آخرین «ارسال واقعی» حداقل price_channel_min_seconds گذشته باشد. - // چون فقط روی ارسال واقعی markSent می‌کنیم، یک بررسیِ بی‌ارسال تایمر را ریست نمی‌کند. - if (! $this->cooldownElapsed()) { + // شرط ۱: حداقل price_channel_min_seconds از آخرین ارسال گذشته باشد (اتمی — جلوی race و چند worker) + if (! $this->acquireSendCooldown()) { return; } @@ -111,8 +110,7 @@ TXT; $results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message); } - // فقط بعد از ارسال واقعی: تایمر cooldown و امضای قیمت به‌روزرسانی می‌شوند - $this->markSent(); + // فقط بعد از ارسال واقعی: امضا به‌روزرسانی می‌شود (cooldown در acquireSendCooldown ست شد) Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay()); Log::info('PriceChannel: قیمت به کانال ارسال شد', [ @@ -176,6 +174,11 @@ TXT; return; } + if (! $this->acquireSendCooldown()) { + Log::debug('PriceChannel: قالب عوض شد ولی cooldown هنوز تمام نشده'); + return; + } + $message = $this->buildMessage($products, $boardProduct); if ($message === '') { return; @@ -187,10 +190,9 @@ TXT; } $signature = $this->broadcastSignature(); - $this->markSent(); Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay()); - Log::info('PriceChannel: قالب پیام تغییر کرد — ارسال فوری به کانال', [ + Log::info('PriceChannel: قالب پیام تغییر کرد — ارسال به کانال', [ 'targets' => array_keys($targets), 'results' => $results, ]); @@ -221,10 +223,8 @@ TXT; } if ($message !== '' && $targets !== []) { - $this->markSent(); - if ($this->resolveLiveTradePrices() !== null) { - Cache::put(self::SIGNATURE_CACHE_KEY, $this->broadcastSignature(), now()->addDay()); - } + $this->acquireSendCooldown(true); + Cache::put(self::SIGNATURE_CACHE_KEY, $this->broadcastSignature(), now()->addDay()); } return [ @@ -266,6 +266,7 @@ TXT; 'selected_messengers' => $selected, 'min_seconds' => $this->config->getInt('price_channel_min_seconds', 120), 'cooldown_elapsed' => $this->cooldownElapsed(), + 'cooldown_seconds_remaining' => $this->cooldownSecondsRemaining(), 'eligible_products' => count($this->eligibleProducts()), 'messengers' => $messengers, 'active_targets' => array_keys($this->resolveActiveTargets()), @@ -718,25 +719,39 @@ TXT; ]); } - /** حداقل فاصله‌ی مجاز بین دو ارسال — جلوگیری از سیل پیام در هر تیک ۱۰ثانیه‌ای */ - private function cooldownElapsed(): bool + /** حداقل فاصلهٔ مجاز بین دو ارسال — Cache::add اتمی (TTL = min_seconds) */ + private function acquireSendCooldown(bool $force = false): bool { $minSeconds = max(0, $this->config->getInt('price_channel_min_seconds', 120)); - if ($minSeconds === 0) { + if ($minSeconds === 0 || $force) { + if ($minSeconds > 0) { + Cache::put(self::COOLDOWN_CACHE_KEY, now()->timestamp, $minSeconds); + } + return true; } - $lastSent = Cache::get(self::COOLDOWN_CACHE_KEY); - if ($lastSent === null) { - return true; - } - - return now()->timestamp - (int) $lastSent >= $minSeconds; + return Cache::add(self::COOLDOWN_CACHE_KEY, now()->timestamp, $minSeconds); } - private function markSent(): void + private function cooldownElapsed(): bool { - Cache::put(self::COOLDOWN_CACHE_KEY, now()->timestamp, now()->addDay()); + return $this->cooldownSecondsRemaining() === 0; + } + + private function cooldownSecondsRemaining(): int + { + $minSeconds = max(0, $this->config->getInt('price_channel_min_seconds', 120)); + if ($minSeconds === 0 || ! Cache::has(self::COOLDOWN_CACHE_KEY)) { + return 0; + } + + $lastSent = (int) Cache::get(self::COOLDOWN_CACHE_KEY, 0); + if ($lastSent <= 0) { + return 0; + } + + return max(0, $minSeconds - (now()->timestamp - $lastSent)); } /** @return array */ diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 967cabd..048e833 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -42,7 +42,22 @@ SET @invite_link_desc = 'متن پیام اشتراک‌گذاری دعوت در SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.'; SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. فقط متغیر ٪نام یا %نام = نام صاحب حساب (deposit_account_holder). پیش‌فرض: «شماره حساب».'; -SET @price_channel_message_desc = 'قالب متن پیام ارسالی به کانال. متغیرهای قیمت تابلو: ٪گرم_خرید ٪گرم_فروش ٪مثقال_خرید ٪مثقال_فروش (یا با %). زمان: ٪تاریخ ٪ساعت. اپ: ٪اسم_اپ یا ٪نام (نام فارسی)؛ ٪عنوان = خط «📊 قیمت لحظه‌ای + نام». تماس جدا: ٪آیدی ٪شماره ٪لینک. اختیاری: ٪محصولات (لیست همه محصولات)؛ ٪فوتر (ترکیب آیدی+شماره+لینک). فاصله بعد از ٪ اختیاری است. خالی = قالب پیش‌فرض.'; +SET @price_channel_message_desc = CONCAT( + 'متن پیامی که با تغییر قیمت به کانال فرستاده می‌شود. هر متغیر را دقیقاً همین‌طور بنویسید (با ٪ یا %).', CHAR(10), + CHAR(10), + 'قیمت تابلو — مثل اپ:', CHAR(10), + '• %گرم_فروش = عدد بزرگ صفحهٔ اصلی اپ (فروش هر گرم)', CHAR(10), + '• %گرم_خرید = خرید هر گرم', CHAR(10), + '• %مثقال_فروش = فروش هر مثقال', CHAR(10), + '• %مثقال_خرید = خرید هر مثقال', CHAR(10), + CHAR(10), + 'زمان: %تاریخ (شمسی) • %ساعت', CHAR(10), + 'نام: %نام (فقط نام فروشگاه) • %عنوان (خط آماده 📊 قیمت لحظه‌ای + نام)', CHAR(10), + 'تماس: %شماره • %آیدی • %لینک (از فیلدهای پایین همین بخش)', CHAR(10), + 'اختیاری: %محصولات • %فوتر', CHAR(10), + CHAR(10), + 'خالی = قالب پیش‌فرض.' +); SET @price_channel_default_text = CONCAT( 'سلام 👋', CHAR(10), '%عنوان', CHAR(10), diff --git a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql index df3e2ab..7926d68 100644 --- a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql +++ b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql @@ -41,7 +41,22 @@ SET @invite_link_desc = 'متن پیام اشتراک‌گذاری دعوت در SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.'; SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. فقط متغیر ٪نام یا %نام = نام صاحب حساب (deposit_account_holder). پیش‌فرض: «شماره حساب».'; -SET @price_channel_message_desc = 'قالب متن پیام ارسالی به کانال. متغیرهای قیمت تابلو: ٪گرم_خرید ٪گرم_فروش ٪مثقال_خرید ٪مثقال_فروش (یا با %). زمان: ٪تاریخ ٪ساعت. اپ: ٪اسم_اپ یا ٪نام (نام فارسی)؛ ٪عنوان = خط «📊 قیمت لحظه‌ای + نام». تماس جدا: ٪آیدی ٪شماره ٪لینک. اختیاری: ٪محصولات (لیست همه محصولات)؛ ٪فوتر (ترکیب آیدی+شماره+لینک). فاصله بعد از ٪ اختیاری است. خالی = قالب پیش‌فرض.'; +SET @price_channel_message_desc = CONCAT( + 'متن پیامی که با تغییر قیمت به کانال فرستاده می‌شود. هر متغیر را دقیقاً همین‌طور بنویسید (با ٪ یا %).', CHAR(10), + CHAR(10), + 'قیمت تابلو — مثل اپ:', CHAR(10), + '• %گرم_فروش = عدد بزرگ صفحهٔ اصلی اپ (فروش هر گرم)', CHAR(10), + '• %گرم_خرید = خرید هر گرم', CHAR(10), + '• %مثقال_فروش = فروش هر مثقال', CHAR(10), + '• %مثقال_خرید = خرید هر مثقال', CHAR(10), + CHAR(10), + 'زمان: %تاریخ (شمسی) • %ساعت', CHAR(10), + 'نام: %نام (فقط نام فروشگاه) • %عنوان (خط آماده 📊 قیمت لحظه‌ای + نام)', CHAR(10), + 'تماس: %شماره • %آیدی • %لینک (از فیلدهای پایین همین بخش)', CHAR(10), + 'اختیاری: %محصولات • %فوتر', CHAR(10), + CHAR(10), + 'خالی = قالب پیش‌فرض.' +); SET @price_channel_default_text = CONCAT( 'سلام 👋', CHAR(10), '%عنوان', CHAR(10), diff --git a/database/sql/price_channel_settings.sql b/database/sql/price_channel_settings.sql index 37dff5b..91c9d5e 100644 --- a/database/sql/price_channel_settings.sql +++ b/database/sql/price_channel_settings.sql @@ -11,7 +11,22 @@ START TRANSACTION; SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; -SET @price_channel_message_desc = 'قالب متن پیام ارسالی به کانال. متغیرهای قیمت تابلو: ٪گرم_خرید ٪گرم_فروش ٪مثقال_خرید ٪مثقال_فروش (یا با %). زمان: ٪تاریخ ٪ساعت. اپ: ٪اسم_اپ یا ٪نام (نام فارسی)؛ ٪عنوان = خط «📊 قیمت لحظه‌ای + نام». تماس جدا: ٪آیدی ٪شماره ٪لینک. اختیاری: ٪محصولات (لیست همه محصولات)؛ ٪فوتر (ترکیب آیدی+شماره+لینک). فاصله بعد از ٪ اختیاری است. خالی = قالب پیش‌فرض.'; +SET @price_channel_message_desc = CONCAT( + 'متن پیامی که با تغییر قیمت به کانال فرستاده می‌شود. هر متغیر را دقیقاً همین‌طور بنویسید (با ٪ یا %).', CHAR(10), + CHAR(10), + 'قیمت تابلو — مثل اپ:', CHAR(10), + '• %گرم_فروش = عدد بزرگ صفحهٔ اصلی اپ (فروش هر گرم)', CHAR(10), + '• %گرم_خرید = خرید هر گرم', CHAR(10), + '• %مثقال_فروش = فروش هر مثقال', CHAR(10), + '• %مثقال_خرید = خرید هر مثقال', CHAR(10), + CHAR(10), + 'زمان: %تاریخ (شمسی) • %ساعت', CHAR(10), + 'نام: %نام (فقط نام فروشگاه) • %عنوان (خط آماده 📊 قیمت لحظه‌ای + نام)', CHAR(10), + 'تماس: %شماره • %آیدی • %لینک (از فیلدهای پایین همین بخش)', CHAR(10), + 'اختیاری: %محصولات • %فوتر', CHAR(10), + CHAR(10), + 'خالی = قالب پیش‌فرض.' +); SET @price_channel_default_text = CONCAT( 'سلام 👋', CHAR(10), '%عنوان', CHAR(10),