fix some handling 5

This commit is contained in:
2026-07-08 13:40:43 +03:30
parent 771080d68c
commit 4af6689c11
5 changed files with 107 additions and 24 deletions
@@ -22,6 +22,12 @@ class PriceChannelTest extends Command
$this->line('حداقل فاصله ارسال (ثانیه): ' . $info['min_seconds']); $this->line('حداقل فاصله ارسال (ثانیه): ' . $info['min_seconds']);
$this->line('فاصله cooldown گذشته؟ ' . ($info['cooldown_elapsed'] ? 'بله' : 'خیر (هنوز زوده)')); $this->line('فاصله cooldown گذشته؟ ' . ($info['cooldown_elapsed'] ? 'بله' : 'خیر (هنوز زوده)'));
$this->line('منبع قالب: ' . ($info['template_source'] === 'db' ? 'تنظیمات ادمین' : 'پیش‌فرض داخلی')); $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']) . ' ریال');
$this->line('قیمت خرید اپ (sell API): ' . number_format((int) $info['live_trade_sell_rial']) . ' ریال');
$this->line('فروش گرم کانال: ' . ($info['channel_gram_sell'] ?: '—'));
$this->line('خرید گرم کانال: ' . ($info['channel_gram_buy'] ?: '—'));
}
$this->line('تعداد محصولات واجد شرایط: ' . $info['eligible_products']); $this->line('تعداد محصولات واجد شرایط: ' . $info['eligible_products']);
$this->line('مقصدهای فعال (آماده ارسال): ' . (implode(', ', $info['active_targets']) ?: '— (هیچ)')); $this->line('مقصدهای فعال (آماده ارسال): ' . (implode(', ', $info['active_targets']) ?: '— (هیچ)'));
+98 -21
View File
@@ -15,7 +15,7 @@ use Illuminate\Support\Facades\Log;
* اصول (طبق rule ساعت کاری): * اصول (طبق rule ساعت کاری):
* • هیچ تنظیمی را بازنویسی نمی‌کند — فقط می‌خواند. * • هیچ تنظیمی را بازنویسی نمی‌کند — فقط می‌خواند.
* • فقط پیام‌رسان‌هایی که ادمین انتخاب کرده و توکن + مقصد دارند ارسال می‌شود. * • فقط پیام‌رسان‌هایی که ادمین انتخاب کرده و توکن + مقصد دارند ارسال می‌شود.
* • قیمت هر محصول از قیمت پایه ذخیره‌شده + کارمزد نمایش همان اپ ساخته می‌شود. * • قیمت تابلو همان buy/sell کش‌شدهٔ /api/price/last است (مثل صفحهٔ اصلی اپ).
*/ */
class PriceChannelBroadcastService class PriceChannelBroadcastService
{ {
@@ -84,13 +84,19 @@ TXT;
$products = $this->eligibleProducts(); $products = $this->eligibleProducts();
$boardProduct = $this->resolveBoardProduct($products); $boardProduct = $this->resolveBoardProduct($products);
if ($boardProduct === null || (int) ($boardProduct->price ?? 0) <= 0) { if ($boardProduct === null) {
Log::debug('PriceChannel: محصول تابلو (آبشده) واجد شرایطی نیست'); Log::debug('PriceChannel: محصول تابلو (آبشده) واجد شرایطی نیست');
return; return;
} }
$trade = $this->resolveLiveTradePrices();
if ($trade === null || $trade['buy'] <= 0) {
Log::debug('PriceChannel: مظنه زنده تابلو در دسترس نیست');
return;
}
// شرط ۲: قیمت تابلو یا قالب پیام نسبت به آخرین ارسال واقعاً تغییر کرده باشد // شرط ۲: قیمت تابلو یا قالب پیام نسبت به آخرین ارسال واقعاً تغییر کرده باشد
$signature = $this->broadcastSignature($boardProduct); $signature = $this->broadcastSignature();
if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) { if ($signature === Cache::get(self::SIGNATURE_CACHE_KEY)) {
return; return;
} }
@@ -125,9 +131,9 @@ TXT;
/** /**
* امضای ارسال — قیمت تابلو + قالب پیام؛ تغییر هر کدام باید ارسال بعدی را مجاز کند. * امضای ارسال — قیمت تابلو + قالب پیام؛ تغییر هر کدام باید ارسال بعدی را مجاز کند.
*/ */
private function broadcastSignature(Product $boardProduct): string private function broadcastSignature(): string
{ {
$prices = $this->computeDisplayPrices($boardProduct); $prices = $this->computeDisplayPrices();
return md5(implode('|', [ return md5(implode('|', [
$prices['gram_buy'], $prices['gram_buy'],
@@ -161,7 +167,12 @@ TXT;
try { try {
$products = $this->eligibleProducts(); $products = $this->eligibleProducts();
$boardProduct = $this->resolveBoardProduct($products); $boardProduct = $this->resolveBoardProduct($products);
if ($boardProduct === null || (int) ($boardProduct->price ?? 0) <= 0) { if ($boardProduct === null) {
return;
}
$trade = $this->resolveLiveTradePrices();
if ($trade === null || $trade['buy'] <= 0) {
return; return;
} }
@@ -175,7 +186,7 @@ TXT;
$results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message); $results[$messenger] = $this->dispatchToMessenger($messenger, $target['token'], $target['chat_ids'], $message);
} }
$signature = $this->broadcastSignature($boardProduct); $signature = $this->broadcastSignature();
$this->markSent(); $this->markSent();
Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay()); Cache::put(self::SIGNATURE_CACHE_KEY, $signature, now()->addDay());
@@ -211,10 +222,8 @@ TXT;
if ($message !== '' && $targets !== []) { if ($message !== '' && $targets !== []) {
$this->markSent(); $this->markSent();
$products = $this->eligibleProducts(); if ($this->resolveLiveTradePrices() !== null) {
$boardProduct = $this->resolveBoardProduct($products); Cache::put(self::SIGNATURE_CACHE_KEY, $this->broadcastSignature(), now()->addDay());
if ($boardProduct !== null) {
Cache::put(self::SIGNATURE_CACHE_KEY, $this->broadcastSignature($boardProduct), now()->addDay());
} }
} }
@@ -249,6 +258,8 @@ TXT;
$message = $this->buildMessage(); $message = $this->buildMessage();
$fromConfig = trim((string) $this->config->getString('price_channel_message_text', '')); $fromConfig = trim((string) $this->config->getString('price_channel_message_text', ''));
$trade = $this->resolveLiveTradePrices();
$display = $trade !== null ? $this->computeDisplayPrices() : null;
return [ return [
'enabled' => $this->config->getBool('price_channel_enabled', false), 'enabled' => $this->config->getBool('price_channel_enabled', false),
@@ -259,6 +270,10 @@ TXT;
'messengers' => $messengers, 'messengers' => $messengers,
'active_targets' => array_keys($this->resolveActiveTargets()), 'active_targets' => array_keys($this->resolveActiveTargets()),
'template_source' => $fromConfig !== '' ? 'db' : 'default', 'template_source' => $fromConfig !== '' ? 'db' : 'default',
'live_trade_buy_rial' => $trade['buy'] ?? 0,
'live_trade_sell_rial' => $trade['sell'] ?? 0,
'channel_gram_sell' => $display['gram_sell'] ?? '',
'channel_gram_buy' => $display['gram_buy'] ?? '',
'message_preview' => $message, 'message_preview' => $message,
]; ];
} }
@@ -323,7 +338,12 @@ TXT;
{ {
$products = $products ?? $this->eligibleProducts(); $products = $products ?? $this->eligibleProducts();
$boardProduct = $boardProduct ?? $this->resolveBoardProduct($products); $boardProduct = $boardProduct ?? $this->resolveBoardProduct($products);
if ($boardProduct === null || (int) ($boardProduct->price ?? 0) <= 0) { if ($boardProduct === null) {
return '';
}
$trade = $this->resolveLiveTradePrices();
if ($trade === null || $trade['buy'] <= 0) {
return ''; return '';
} }
@@ -349,7 +369,7 @@ TXT;
{ {
$name = trim((string) $this->config->getString('name_fa', '')); $name = trim((string) $this->config->getString('name_fa', ''));
$title = $name !== '' ? "📊 قیمت لحظه‌ای {$name}" : '📊 قیمت لحظه‌ای'; $title = $name !== '' ? "📊 قیمت لحظه‌ای {$name}" : '📊 قیمت لحظه‌ای';
$prices = $this->computeDisplayPrices($boardProduct); $prices = $this->computeDisplayPrices();
$message = $this->applyTemplatePlaceholders($template, [ $message = $this->applyTemplatePlaceholders($template, [
'مثقال_فروش' => $prices['mesghal_sell'], 'مثقال_فروش' => $prices['mesghal_sell'],
@@ -416,21 +436,78 @@ TXT;
} }
/** /**
* قیمت‌های نمایشی تابلو (گرم + مثقال) — همان منطق اپ + price_channel_diff. * همان buy/sell اپ — کش live_price_api_payload پرشده از /api/price/last.
*
* @return array{buy: int, sell: int}|null
*/
private function resolveLiveTradePrices(): ?array
{
$display = app(ProductDisplayPriceService::class);
$display->warmLivePriceCache();
$cached = Cache::get(ProductDisplayPriceService::LIVE_PRICE_CACHE_KEY);
if (is_array($cached)) {
$buy = (int) ($cached['buy'] ?? 0);
$sell = (int) ($cached['sell'] ?? 0);
if ($buy <= 0) {
$buy = (int) ($cached['price_last'] ?? 0);
}
if ($buy > 0 && $sell > 0) {
return ['buy' => $buy, 'sell' => $sell];
}
}
$base = $display->resolveAppBasePrice();
if ($base <= 0) {
return null;
}
$provider = $this->config->priceProvider();
$priceSource = in_array($provider, ['montzar', 'zaarbox', 'main_product'], true) ? $provider : null;
$flags = $provider === 'montzar' ? $this->config->montzarOpenFlags() : [];
$trade = $this->config->buildTradeDisplayPrices(
(float) $base,
$priceSource,
[
'montzar_open_buy' => $flags['open_buy'] ?? true,
'montzar_open_sell' => $flags['open_sell'] ?? true,
]
);
if ($trade['buy'] <= 0 && $trade['sell'] <= 0) {
return null;
}
return [
'buy' => (int) $trade['buy'],
'sell' => (int) $trade['sell'],
];
}
/**
* قیمت‌های نمایشی تابلو — همان منبع صفحهٔ اصلی اپ + price_channel_diff.
* *
* @return array{gram_buy: string, gram_sell: string, mesghal_buy: string, mesghal_sell: string} * @return array{gram_buy: string, gram_sell: string, mesghal_buy: string, mesghal_sell: string}
*/ */
private function computeDisplayPrices(Product $product): array private function computeDisplayPrices(): array
{ {
$channelDiff = (float) $this->config->getInt('price_channel_diff', 0); $channelDiff = (float) $this->config->getInt('price_channel_diff', 0);
$diffBuy = (float) $this->config->displayBuyPriceDiffRial();
$diffSell = (float) $this->config->displaySellPriceDiffRial();
$mesghal = $this->changeMesghal(); $mesghal = $this->changeMesghal();
$base = (float) ($product->price ?? 0); $trade = $this->resolveLiveTradePrices();
// 🔴 خرید (مشتری می‌فروشد، پایین‌تر) — 🟢 فروش (مشتری می‌خرد، بالاتر) if ($trade === null) {
$gramBuy = $base - $diffSell - $channelDiff; return [
$gramSell = $base + $diffBuy + $channelDiff; 'gram_buy' => '',
'gram_sell' => '',
'mesghal_buy' => '',
'mesghal_sell' => '',
];
}
// sell در API = خرید مشتری (پایین) — buy در API = فروش مشتری (بالا) — مثل CurrentPrice.vue
$gramBuy = (float) $trade['sell'] - $channelDiff;
$gramSell = (float) $trade['buy'] + $channelDiff;
return [ return [
'gram_buy' => $this->formatPrice($gramBuy), 'gram_buy' => $this->formatPrice($gramBuy),
+1 -1
View File
@@ -40,7 +40,7 @@ SET @invite_link_default_text = CONCAT(
SET @invite_link_desc = 'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی استفاده می‌کند.'; SET @invite_link_desc = 'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی استفاده می‌کند.';
SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.'; SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.';
SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. همان متغیرهای ٪نام، ٪کارت و ٪شبا. پیش‌فرض: «شماره حساب».'; SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. فقط متغیر ٪نام یا %نام = نام صاحب حساب (deposit_account_holder). پیش‌فرض: «شماره حساب».';
SET @price_channel_message_desc = 'قالب متن پیام ارسالی به کانال. متغیرهای قیمت تابلو: ٪گرم_خرید ٪گرم_فروش ٪مثقال_خرید ٪مثقال_فروش (یا با %). زمان: ٪تاریخ ٪ساعت. اپ: ٪اسم_اپ یا ٪نام (نام فارسی)؛ ٪عنوان = خط «📊 قیمت لحظه‌ای + نام». تماس جدا: ٪آیدی ٪شماره ٪لینک. اختیاری: ٪محصولات (لیست همه محصولات)؛ ٪فوتر (ترکیب آیدی+شماره+لینک). فاصله بعد از ٪ اختیاری است. خالی = قالب پیش‌فرض.'; SET @price_channel_message_desc = 'قالب متن پیام ارسالی به کانال. متغیرهای قیمت تابلو: ٪گرم_خرید ٪گرم_فروش ٪مثقال_خرید ٪مثقال_فروش (یا با %). زمان: ٪تاریخ ٪ساعت. اپ: ٪اسم_اپ یا ٪نام (نام فارسی)؛ ٪عنوان = خط «📊 قیمت لحظه‌ای + نام». تماس جدا: ٪آیدی ٪شماره ٪لینک. اختیاری: ٪محصولات (لیست همه محصولات)؛ ٪فوتر (ترکیب آیدی+شماره+لینک). فاصله بعد از ٪ اختیاری است. خالی = قالب پیش‌فرض.';
SET @price_channel_default_text = CONCAT( SET @price_channel_default_text = CONCAT(
@@ -39,7 +39,7 @@ SET @invite_link_default_text = CONCAT(
SET @invite_link_desc = 'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی استفاده می‌کند.'; SET @invite_link_desc = 'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی استفاده می‌کند.';
SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.'; SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.';
SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. همان متغیرهای ٪نام، ٪کارت و ٪شبا. پیش‌فرض: «شماره حساب».'; SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. فقط متغیر ٪نام یا %نام = نام صاحب حساب (deposit_account_holder). پیش‌فرض: «شماره حساب».';
SET @price_channel_message_desc = 'قالب متن پیام ارسالی به کانال. متغیرهای قیمت تابلو: ٪گرم_خرید ٪گرم_فروش ٪مثقال_خرید ٪مثقال_فروش (یا با %). زمان: ٪تاریخ ٪ساعت. اپ: ٪اسم_اپ یا ٪نام (نام فارسی)؛ ٪عنوان = خط «📊 قیمت لحظه‌ای + نام». تماس جدا: ٪آیدی ٪شماره ٪لینک. اختیاری: ٪محصولات (لیست همه محصولات)؛ ٪فوتر (ترکیب آیدی+شماره+لینک). فاصله بعد از ٪ اختیاری است. خالی = قالب پیش‌فرض.'; SET @price_channel_message_desc = 'قالب متن پیام ارسالی به کانال. متغیرهای قیمت تابلو: ٪گرم_خرید ٪گرم_فروش ٪مثقال_خرید ٪مثقال_فروش (یا با %). زمان: ٪تاریخ ٪ساعت. اپ: ٪اسم_اپ یا ٪نام (نام فارسی)؛ ٪عنوان = خط «📊 قیمت لحظه‌ای + نام». تماس جدا: ٪آیدی ٪شماره ٪لینک. اختیاری: ٪محصولات (لیست همه محصولات)؛ ٪فوتر (ترکیب آیدی+شماره+لینک). فاصله بعد از ٪ اختیاری است. خالی = قالب پیش‌فرض.';
SET @price_channel_default_text = CONCAT( SET @price_channel_default_text = CONCAT(
+1 -1
View File
@@ -6,7 +6,7 @@ START TRANSACTION;
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.'; SET @deposit_guide_desc = 'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیش‌فرض اپ استفاده می‌شود.';
SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. همان متغیرهای ٪نام، ٪کارت و ٪شبا. پیش‌فرض: «شماره حساب».'; SET @deposit_support_msg_desc = 'متن پیام هنگام کلیک روی «پیام به پشتیبانی» و «پیام به پشتیبانی (sms)» در مودال واریز. فقط متغیر ٪نام یا %نام = نام صاحب حساب (deposit_account_holder). پیش‌فرض: «شماره حساب».';
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
SELECT 'deposit_account_holder', '', 'نام صاحب حساب واریز', 1, 1, SELECT 'deposit_account_holder', '', 'نام صاحب حساب واریز', 1, 1,