From a04dc6e57d825de2312d2cc4a2dc0ca32fd6c7dd Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 7 Jul 2026 19:24:46 +0330 Subject: [PATCH] fix channel price bug 2 --- app/Services/PriceChannelBroadcastService.php | 117 +++++++++++------- 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/app/Services/PriceChannelBroadcastService.php b/app/Services/PriceChannelBroadcastService.php index 09be510..2412efc 100644 --- a/app/Services/PriceChannelBroadcastService.php +++ b/app/Services/PriceChannelBroadcastService.php @@ -22,6 +22,8 @@ class PriceChannelBroadcastService private const SIGNATURE_CACHE_KEY = 'price_channel:last_signature'; + private const SEND_LOCK_KEY = 'price_channel:send_lock'; + private const SUPPORTED_MESSENGERS = ['bale', 'telegram', 'rubika']; /** وزن هر مثقال به گرم — ضریب تبدیل قیمت هر گرم به هر مثقال (مثقال = گرم × این عدد) */ @@ -48,60 +50,70 @@ class PriceChannelBroadcastService return; } - // شرط ۱ (ارزان): از آخرین «ارسال واقعی» حداقل price_channel_min_seconds گذشته باشد. - // چون فقط روی ارسال واقعی markSent می‌کنیم، یک بررسیِ بی‌ارسال تایمر را ریست نمی‌کند - // و ارسالِ بعدی را عقب نمی‌اندازد. - if (! $this->cooldownElapsed()) { + // قفل — دو فراخوان هم‌زمان (مثلاً دو force_refresh پشت‌سرهم) را سریالی می‌کند تا + // هر دو قبل از ذخیرهٔ markSent/امضا، cooldown را «گذشته» و امضا را «قدیمی» نبینند و + // پیام تکراری نفرستند. اگر قفل دست کسی است، همین حالا بی‌ارسال برمی‌گردیم. + $lock = Cache::lock(self::SEND_LOCK_KEY, 30); + if (! $lock->get()) { return; } - $products = $this->eligibleProducts(); - if ($products === []) { - Log::debug('PriceChannel: محصول واجد شرایطی نیست (active/قیمت/ساعت کاری)'); - return; + try { + // شرط ۱: از آخرین «ارسال واقعی» حداقل price_channel_min_seconds گذشته باشد. + // چون فقط روی ارسال واقعی markSent می‌کنیم، یک بررسیِ بی‌ارسال تایمر را ریست نمی‌کند. + if (! $this->cooldownElapsed()) { + return; + } + + $products = $this->eligibleProducts(); + if ($products === []) { + Log::debug('PriceChannel: محصول واجد شرایطی نیست (active/قیمت/ساعت کاری)'); + return; + } + + // شرط ۲: قیمتِ نمایشی نسبت به آخرین ارسال واقعاً تغییر کرده باشد؛ وگرنه چیزی نفرست + // و تایمر را هم دست نزن (تا سرِ اولین تغییرِ بعدی بلافاصله بفرستد). + $signature = $this->priceSignature($products); + if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) { + return; + } + + $message = $this->buildMessage($products); + if ($message === '') { + return; + } + + $results = []; + foreach ($targets as $messenger => $target) { + $results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message); + } + + // فقط بعد از ارسال واقعی: تایمر cooldown و امضای قیمت به‌روزرسانی می‌شوند + $this->markSent(); + Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay()); + + Log::info('PriceChannel: قیمت به کانال ارسال شد', [ + 'targets' => array_keys($targets), + 'products' => count($products), + 'results' => $results, + ]); + } finally { + $lock->release(); } - - // شرط ۲: قیمت نسبت به آخرین ارسال واقعاً تغییر کرده باشد؛ وگرنه چیزی نفرست - // و تایمر را هم دست نزن (تا سرِ اولین تغییرِ بعدی بلافاصله بفرستد). - $signature = $this->priceSignature($products); - if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) { - return; - } - - $message = $this->buildMessage($products); - if ($message === '') { - return; - } - - $results = []; - foreach ($targets as $messenger => $target) { - $results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message); - } - - // فقط بعد از ارسال واقعی: تایمر cooldown و امضای قیمت به‌روزرسانی می‌شوند - $this->markSent(); - Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay()); - - Log::info('PriceChannel: قیمت به کانال ارسال شد', [ - 'targets' => array_keys($targets), - 'products' => count($products), - 'results' => $results, - ]); } catch (\Throwable $e) { Log::warning('PriceChannelBroadcast failed', ['message' => $e->getMessage()]); } } - /** امضای قیمت محصولات واجد شرایط — برای تشخیص تغییر قیمت */ + /** + * امضای قیمت — روی «بدنهٔ نمایشیِ» پیام (بدون هدر/ساعت) گرفته می‌شود تا دقیقاً همان چیزی + * که کاربر می‌بیند مبنای تشخیص تغییر باشد؛ اختلاف چند ریالیِ زیرِ آستانهٔ نمایش پیام تکراری نمی‌سازد. + * + * @param array $products + */ private function priceSignature(array $products): string { - $parts = []; - foreach ($products as $product) { - $parts[] = $product->id . ':' . (int) ($product->price ?? 0) - . ':' . ((int) ($product->open_buy)) . ((int) ($product->open_sell)); - } - - return md5(implode('|', $parts)); + return md5($this->buildPriceBody($products)); } /** @@ -232,6 +244,23 @@ class PriceChannelBroadcastService public function buildMessage(?array $products = null): string { $products = $products ?? $this->eligibleProducts(); + $body = $this->buildPriceBody($products); + if ($body === '') { + return ''; + } + + return trim($this->header() . "\n\n" . $body); + } + + /** + * بدنهٔ قیمت (بدون هدر/ساعت) — همان چیزی که «تغییر قیمت» را می‌سازد. + * امضای تشخیص تغییر روی همین بدنه‌ی نمایشیِ رُندشده گرفته می‌شود تا اختلاف چند ریالی + * که به یک عدد نمایشیِ یکسان می‌رسد، پیام تکراری نفرستد. + * + * @param array $products + */ + private function buildPriceBody(array $products): string + { if ($products === []) { return ''; } @@ -242,8 +271,6 @@ class PriceChannelBroadcastService $mesghal = $this->changeMesghal(); $lines = []; - $lines[] = $this->header(); - $lines[] = ''; foreach ($products as $product) { $base = (float) ($product->price ?? 0);