fixed some bugs
This commit is contained in:
@@ -21,6 +21,9 @@ class PriceChannelTest extends Command
|
|||||||
$this->line('پیامرسانهای انتخابشده: ' . (implode(', ', $info['selected_messengers']) ?: '— (خالی)'));
|
$this->line('پیامرسانهای انتخابشده: ' . (implode(', ', $info['selected_messengers']) ?: '— (خالی)'));
|
||||||
$this->line('حداقل فاصله ارسال (ثانیه): ' . $info['min_seconds']);
|
$this->line('حداقل فاصله ارسال (ثانیه): ' . $info['min_seconds']);
|
||||||
$this->line('فاصله cooldown گذشته؟ ' . ($info['cooldown_elapsed'] ? 'بله' : 'خیر (هنوز ' . ($info['cooldown_seconds_remaining'] ?? 0) . ' ثانیه مانده)'));
|
$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' ? 'تنظیمات ادمین' : 'پیشفرض داخلی'));
|
$this->line('منبع قالب: ' . ($info['template_source'] === 'db' ? 'تنظیمات ادمین' : 'پیشفرض داخلی'));
|
||||||
if (! empty($info['live_trade_buy_rial'])) {
|
if (! empty($info['live_trade_buy_rial'])) {
|
||||||
$this->line('قیمت فروش اپ (buy API): ' . number_format((int) $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(PriceBroadcastService::class)->broadcastLastIfChanged($body);
|
||||||
|
|
||||||
|
app(PriceChannelBroadcastService::class)->scheduleBroadcastIfEligible();
|
||||||
|
|
||||||
return $body;
|
return $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class PriceChannelBroadcastService
|
|||||||
|
|
||||||
private const SEND_LOCK_KEY = 'price_channel:send_lock';
|
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'];
|
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();
|
$targets = $this->resolveActiveTargets();
|
||||||
if ($targets === []) {
|
if ($targets === []) {
|
||||||
Log::debug('PriceChannel: enabled ولی هیچ مقصد فعالی نیست (توکن/مقصد را چک کن)');
|
Log::debug('PriceChannel: enabled ولی هیچ مقصد فعالی نیست (توکن/مقصد را چک کن)');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// قفل — دو فراخوان همزمان (مثلاً دو force_refresh پشتسرهم) را سریالی میکند تا
|
|
||||||
// هر دو قبل از ذخیرهٔ markSent/امضا، cooldown را «گذشته» و امضا را «قدیمی» نبینند و
|
|
||||||
// پیام تکراری نفرستند. اگر قفل دست کسی است، همین حالا بیارسال برمیگردیم.
|
|
||||||
$lock = Cache::lock(self::SEND_LOCK_KEY, 30);
|
$lock = Cache::lock(self::SEND_LOCK_KEY, 30);
|
||||||
if (! $lock->get()) {
|
if (! $lock->get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// شرط ۱: حداقل price_channel_min_seconds از آخرین ارسال گذشته باشد (اتمی — جلوی race و چند worker)
|
|
||||||
if (! $this->acquireSendCooldown()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$products = $this->eligibleProducts();
|
$products = $this->eligibleProducts();
|
||||||
$boardProduct = $this->resolveBoardProduct($products);
|
$boardProduct = $this->resolveBoardProduct($products);
|
||||||
if ($boardProduct === null) {
|
if ($boardProduct === null) {
|
||||||
Log::debug('PriceChannel: محصول تابلو (آبشده) واجد شرایطی نیست');
|
Log::debug('PriceChannel: محصول تابلو (آبشده) واجد شرایطی نیست');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$trade = $this->resolveLiveTradePrices();
|
$trade = $this->resolveLiveTradePrices();
|
||||||
if ($trade === null || $trade['buy'] <= 0) {
|
if ($trade === null || $trade['buy'] <= 0) {
|
||||||
Log::debug('PriceChannel: مظنه زنده تابلو در دسترس نیست');
|
Log::debug('PriceChannel: مظنه زنده تابلو در دسترس نیست');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// شرط ۲: قیمت تابلو یا قالب پیام نسبت به آخرین ارسال واقعاً تغییر کرده باشد
|
// اول امضا — بدون مصرف cooldown (باگ قبلی: cooldown قبل از امضا باعث میشد
|
||||||
$signature = $this->broadcastSignature();
|
// هر ۱۲۰ ثانیه یک «اسلات» بسوزد حتی وقتی قیمت عوض نشده یا ارسال ناموفق بود)
|
||||||
|
$signature = $this->broadcastSignature($trade);
|
||||||
if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) {
|
if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->cooldownElapsed()) {
|
||||||
|
$this->scheduleDeferredBroadcast($this->cooldownSecondsRemaining() + 1);
|
||||||
|
Log::debug('PriceChannel: قیمت عوض شده ولی cooldown هنوز تمام نشده — ارسال بهتأخیر افتاد', [
|
||||||
|
'remaining' => $this->cooldownSecondsRemaining(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$message = $this->buildMessage($products, $boardProduct);
|
$message = $this->buildMessage($products, $boardProduct);
|
||||||
if ($message === '') {
|
if ($message === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->acquireSendCooldown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$results = [];
|
$results = [];
|
||||||
|
$anyOk = false;
|
||||||
foreach ($targets as $messenger => $target) {
|
foreach ($targets as $messenger => $target) {
|
||||||
$results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message);
|
$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());
|
Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay());
|
||||||
|
|
||||||
Log::info('PriceChannel: قیمت به کانال ارسال شد', [
|
Log::info('PriceChannel: قیمت به کانال ارسال شد', [
|
||||||
'targets' => array_keys($targets),
|
'targets' => array_keys($targets),
|
||||||
'products' => count($products),
|
'products' => count($products),
|
||||||
|
'buy_rial' => $trade['buy'],
|
||||||
|
'sell_rial' => $trade['sell'],
|
||||||
'results' => $results,
|
'results' => $results,
|
||||||
]);
|
]);
|
||||||
} finally {
|
} 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('|', [
|
return md5(implode('|', [
|
||||||
$prices['gram_buy'],
|
(int) $trade['buy'] + $channelDiff,
|
||||||
$prices['gram_sell'],
|
(int) $trade['sell'] - $channelDiff,
|
||||||
$prices['mesghal_buy'],
|
|
||||||
$prices['mesghal_sell'],
|
|
||||||
$this->messageTemplate(),
|
$this->messageTemplate(),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
@@ -174,8 +228,9 @@ TXT;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->acquireSendCooldown()) {
|
if (! $this->cooldownElapsed()) {
|
||||||
Log::debug('PriceChannel: قالب عوض شد ولی cooldown هنوز تمام نشده');
|
Log::debug('PriceChannel: قالب عوض شد ولی cooldown هنوز تمام نشده');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,12 +239,28 @@ TXT;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = [];
|
if (! $this->acquireSendCooldown()) {
|
||||||
foreach ($targets as $messenger => $target) {
|
return;
|
||||||
$results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$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());
|
Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay());
|
||||||
|
|
||||||
Log::info('PriceChannel: قالب پیام تغییر کرد — ارسال به کانال', [
|
Log::info('PriceChannel: قالب پیام تغییر کرد — ارسال به کانال', [
|
||||||
@@ -267,6 +338,8 @@ TXT;
|
|||||||
'min_seconds' => $this->config->getInt('price_channel_min_seconds', 120),
|
'min_seconds' => $this->config->getInt('price_channel_min_seconds', 120),
|
||||||
'cooldown_elapsed' => $this->cooldownElapsed(),
|
'cooldown_elapsed' => $this->cooldownElapsed(),
|
||||||
'cooldown_seconds_remaining' => $this->cooldownSecondsRemaining(),
|
'cooldown_seconds_remaining' => $this->cooldownSecondsRemaining(),
|
||||||
|
'signature_changed' => $trade !== null
|
||||||
|
&& $this->broadcastSignature($trade) !== Cache::get(self::SIGNATURE_CACHE_KEY),
|
||||||
'eligible_products' => count($this->eligibleProducts()),
|
'eligible_products' => count($this->eligibleProducts()),
|
||||||
'messengers' => $messengers,
|
'messengers' => $messengers,
|
||||||
'active_targets' => array_keys($this->resolveActiveTargets()),
|
'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) */
|
/** حداقل فاصلهٔ مجاز بین دو ارسال — Cache::add اتمی (TTL = min_seconds) */
|
||||||
private function acquireSendCooldown(bool $force = false): bool
|
private function acquireSendCooldown(bool $force = false): bool
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user