fixed some bugs
This commit is contained in:
@@ -21,6 +21,9 @@ class PriceChannelTest extends Command
|
||||
$this->line('پیامرسانهای انتخابشده: ' . (implode(', ', $info['selected_messengers']) ?: '— (خالی)'));
|
||||
$this->line('حداقل فاصله ارسال (ثانیه): ' . $info['min_seconds']);
|
||||
$this->line('فاصله cooldown گذشته؟ ' . ($info['cooldown_elapsed'] ? 'بله' : 'خیر (هنوز ' . ($info['cooldown_seconds_remaining'] ?? 0) . ' ثانیه مانده)'));
|
||||
if (array_key_exists('signature_changed', $info)) {
|
||||
$this->line('قیمت نسبت به آخرین ارسال کانال عوض شده؟ ' . ($info['signature_changed'] ? 'بله' : 'خیر (همین دلیل عدم ارسال)'));
|
||||
}
|
||||
$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']) . ' ریال');
|
||||
|
||||
@@ -54,6 +54,8 @@ class LivePriceCacheService
|
||||
|
||||
app(PriceBroadcastService::class)->broadcastLastIfChanged($body);
|
||||
|
||||
app(PriceChannelBroadcastService::class)->scheduleBroadcastIfEligible();
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ class PriceChannelBroadcastService
|
||||
|
||||
private const SEND_LOCK_KEY = 'price_channel:send_lock';
|
||||
|
||||
private const DEFERRED_BROADCAST_KEY = 'price_channel:deferred_broadcast';
|
||||
|
||||
private const SUPPORTED_MESSENGERS = ['bale', 'telegram', 'rubika'];
|
||||
|
||||
/** وزن هر مثقال به گرم — ضریب تبدیل قیمت هر گرم به هر مثقال (مثقال = گرم × این عدد) */
|
||||
@@ -50,6 +52,28 @@ TXT;
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* صف ارسال کانال — همان لحظهٔ تغییر قیمت اپ (مثل SSE)، نه فقط cron.
|
||||
*/
|
||||
public function scheduleBroadcastIfEligible(): void
|
||||
{
|
||||
if (! $this->config->getBool('price_channel_enabled', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dispatch = static function (): void {
|
||||
app(self::class)->broadcastIfEligible();
|
||||
};
|
||||
|
||||
if (app()->runningInConsole()) {
|
||||
$dispatch();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch($dispatch)->afterResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* نقطهی ورود — بعد از تغییر قیمت صدا زده میشود.
|
||||
* فقط وقتی قابلیت روشن است، پیامرسانی انتخاب شده و فاصلهی مجاز گذشته باشد ارسال میکند.
|
||||
@@ -64,58 +88,83 @@ TXT;
|
||||
$targets = $this->resolveActiveTargets();
|
||||
if ($targets === []) {
|
||||
Log::debug('PriceChannel: enabled ولی هیچ مقصد فعالی نیست (توکن/مقصد را چک کن)');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// قفل — دو فراخوان همزمان (مثلاً دو force_refresh پشتسرهم) را سریالی میکند تا
|
||||
// هر دو قبل از ذخیرهٔ markSent/امضا، cooldown را «گذشته» و امضا را «قدیمی» نبینند و
|
||||
// پیام تکراری نفرستند. اگر قفل دست کسی است، همین حالا بیارسال برمیگردیم.
|
||||
$lock = Cache::lock(self::SEND_LOCK_KEY, 30);
|
||||
if (! $lock->get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// شرط ۱: حداقل price_channel_min_seconds از آخرین ارسال گذشته باشد (اتمی — جلوی race و چند worker)
|
||||
if (! $this->acquireSendCooldown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$products = $this->eligibleProducts();
|
||||
$boardProduct = $this->resolveBoardProduct($products);
|
||||
if ($boardProduct === null) {
|
||||
Log::debug('PriceChannel: محصول تابلو (آبشده) واجد شرایطی نیست');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$trade = $this->resolveLiveTradePrices();
|
||||
if ($trade === null || $trade['buy'] <= 0) {
|
||||
Log::debug('PriceChannel: مظنه زنده تابلو در دسترس نیست');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// شرط ۲: قیمت تابلو یا قالب پیام نسبت به آخرین ارسال واقعاً تغییر کرده باشد
|
||||
$signature = $this->broadcastSignature();
|
||||
// اول امضا — بدون مصرف cooldown (باگ قبلی: cooldown قبل از امضا باعث میشد
|
||||
// هر ۱۲۰ ثانیه یک «اسلات» بسوزد حتی وقتی قیمت عوض نشده یا ارسال ناموفق بود)
|
||||
$signature = $this->broadcastSignature($trade);
|
||||
if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->cooldownElapsed()) {
|
||||
$this->scheduleDeferredBroadcast($this->cooldownSecondsRemaining() + 1);
|
||||
Log::debug('PriceChannel: قیمت عوض شده ولی cooldown هنوز تمام نشده — ارسال بهتأخیر افتاد', [
|
||||
'remaining' => $this->cooldownSecondsRemaining(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->buildMessage($products, $boardProduct);
|
||||
if ($message === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->acquireSendCooldown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$anyOk = false;
|
||||
foreach ($targets as $messenger => $target) {
|
||||
$results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message);
|
||||
foreach ((array) $results[$messenger] as $status) {
|
||||
if (is_string($status) && str_starts_with($status, 'ok')) {
|
||||
$anyOk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $anyOk) {
|
||||
Cache::forget(self::COOLDOWN_CACHE_KEY);
|
||||
Log::warning('PriceChannel: هیچ ارسالی موفق نبود — cooldown آزاد شد تا تلاش مجدد', [
|
||||
'results' => $results,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// فقط بعد از ارسال واقعی: امضا بهروزرسانی میشود (cooldown در acquireSendCooldown ست شد)
|
||||
Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay());
|
||||
|
||||
Log::info('PriceChannel: قیمت به کانال ارسال شد', [
|
||||
'targets' => array_keys($targets),
|
||||
'products' => count($products),
|
||||
'buy_rial' => $trade['buy'],
|
||||
'sell_rial' => $trade['sell'],
|
||||
'results' => $results,
|
||||
]);
|
||||
} finally {
|
||||
@@ -127,17 +176,22 @@ TXT;
|
||||
}
|
||||
|
||||
/**
|
||||
* امضای ارسال — قیمت تابلو + قالب پیام؛ تغییر هر کدام باید ارسال بعدی را مجاز کند.
|
||||
* امضای ارسال — buy/sell خام API (ریال) + قالب؛ دقیقتر از رشتهٔ فرمتشدهٔ نمایشی.
|
||||
*
|
||||
* @param array{buy: int, sell: int}|null $trade
|
||||
*/
|
||||
private function broadcastSignature(): string
|
||||
private function broadcastSignature(?array $trade = null): string
|
||||
{
|
||||
$prices = $this->computeDisplayPrices();
|
||||
$trade ??= $this->resolveLiveTradePrices();
|
||||
if ($trade === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$channelDiff = (int) $this->config->getInt('price_channel_diff', 0);
|
||||
|
||||
return md5(implode('|', [
|
||||
$prices['gram_buy'],
|
||||
$prices['gram_sell'],
|
||||
$prices['mesghal_buy'],
|
||||
$prices['mesghal_sell'],
|
||||
(int) $trade['buy'] + $channelDiff,
|
||||
(int) $trade['sell'] - $channelDiff,
|
||||
$this->messageTemplate(),
|
||||
]));
|
||||
}
|
||||
@@ -174,8 +228,9 @@ TXT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->acquireSendCooldown()) {
|
||||
if (! $this->cooldownElapsed()) {
|
||||
Log::debug('PriceChannel: قالب عوض شد ولی cooldown هنوز تمام نشده');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,12 +239,28 @@ TXT;
|
||||
return;
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($targets as $messenger => $target) {
|
||||
$results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message);
|
||||
if (! $this->acquireSendCooldown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$signature = $this->broadcastSignature();
|
||||
$results = [];
|
||||
$anyOk = false;
|
||||
foreach ($targets as $messenger => $target) {
|
||||
$results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message);
|
||||
foreach ((array) $results[$messenger] as $status) {
|
||||
if (is_string($status) && str_starts_with($status, 'ok')) {
|
||||
$anyOk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $anyOk) {
|
||||
Cache::forget(self::COOLDOWN_CACHE_KEY);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$signature = $this->broadcastSignature($trade);
|
||||
Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay());
|
||||
|
||||
Log::info('PriceChannel: قالب پیام تغییر کرد — ارسال به کانال', [
|
||||
@@ -267,6 +338,8 @@ TXT;
|
||||
'min_seconds' => $this->config->getInt('price_channel_min_seconds', 120),
|
||||
'cooldown_elapsed' => $this->cooldownElapsed(),
|
||||
'cooldown_seconds_remaining' => $this->cooldownSecondsRemaining(),
|
||||
'signature_changed' => $trade !== null
|
||||
&& $this->broadcastSignature($trade) !== Cache::get(self::SIGNATURE_CACHE_KEY),
|
||||
'eligible_products' => count($this->eligibleProducts()),
|
||||
'messengers' => $messengers,
|
||||
'active_targets' => array_keys($this->resolveActiveTargets()),
|
||||
@@ -719,6 +792,28 @@ TXT;
|
||||
]);
|
||||
}
|
||||
|
||||
/** وقتی قیمت عوض شده ولی cooldown مانده — یک بار بعد از اتمام cooldown دوباره تلاش کن */
|
||||
private function scheduleDeferredBroadcast(int $delaySeconds): void
|
||||
{
|
||||
$delaySeconds = max(1, min($delaySeconds, 600));
|
||||
if (! Cache::add(self::DEFERRED_BROADCAST_KEY, now()->timestamp, $delaySeconds + 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dispatch = static function (): void {
|
||||
Cache::forget(self::DEFERRED_BROADCAST_KEY);
|
||||
app(self::class)->broadcastIfEligible();
|
||||
};
|
||||
|
||||
if (app()->runningInConsole()) {
|
||||
// در tick/cron: تلاش مجدد در همان فرایند بعد از cooldown کافی نیست؛
|
||||
// tick بعدی (۱۰ ثانیه) دوباره broadcastIfEligible را صدا میزند.
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch($dispatch)->delay(now()->addSeconds($delaySeconds));
|
||||
}
|
||||
|
||||
/** حداقل فاصلهٔ مجاز بین دو ارسال — Cache::add اتمی (TTL = min_seconds) */
|
||||
private function acquireSendCooldown(bool $force = false): bool
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user