diff --git a/app/Http/Controllers/Site/ReportController.php b/app/Http/Controllers/Site/ReportController.php index 5136cdf..cf33829 100644 --- a/app/Http/Controllers/Site/ReportController.php +++ b/app/Http/Controllers/Site/ReportController.php @@ -82,8 +82,7 @@ class ReportController extends Controller return ['sell' => 0, 'buy' => 0]; } - $price = Price::orderByDesc('id')->first(); - $priceValue = $price ? (float) $price->price : 0.0; + $priceValue = (float) app(ProductDisplayPriceService::class)->resolveHistoricalBasePrice(); if ($priceValue <= 0) { return ['sell' => 0, 'buy' => 0]; @@ -406,8 +405,9 @@ class ReportController extends Controller } if ($priceValue <= 0) { - $lastPrice = Price::latest()->first(); - $storedPrice = $lastPrice ? (float) $lastPrice->price : 0.0; + // آخرین قیمت همین provider (نه هر ردیفی از جدول prices — ممکن است + // از یک تیک provider دیگر مانده باشد و مظنه را ده‌ها میلیون پرت کند) + $storedPrice = (float) app(ProductDisplayPriceService::class)->resolveHistoricalBasePrice(); // fallback فقط با show_last_price تصمیم‌گیری می‌شود؛ force_refresh (تیک کران) نباید آن را override کند. // آستانهٔ ثابت قبلی (۳۰ میلیون) برای طلا بود و محصولات ارزان‌تر مثل کاتد مس/نقره را اشتباهاً «بدون مظنه» می‌کرد. @@ -448,7 +448,9 @@ class ReportController extends Controller $buy = $display['buy']; $priceLast = $display['priceLast']; - if (!$closed && $priceValue > 0) { + // سقف/کف روز فقط از مظنهٔ زنده؛ fallback دیتابیس قیمت دیشب را + // به‌عنوان «قیمت امروز» ثبت می‌کرد و بازهٔ روز را خراب می‌کرد. + if (!$closed && $priceValue > 0 && $priceSource !== 'database_last') { $config->trackDailyPriceExtreme((float) $priceValue); } diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index fa8a14f..94a6c2e 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -5,9 +5,11 @@ namespace App\Services; use App\Models\Cnt; use App\Models\Price; use App\Models\PriceCandle; +use App\Models\PriceSyncLog; use App\Models\Product; use App\Models\Setting; use Carbon\Carbon; +use Illuminate\Support\Facades\Cache; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; class ConfigService @@ -37,13 +39,65 @@ class ConfigService } /** - * آخرین قیمت ذخیره‌شده در جدول prices. + * آخرین قیمت مرجع ثبت‌شده از همین provider (price_sync_logs). + * + * جدول prices منبع قیمت را نگه نمی‌دارد؛ یک تیک از provider دیگر + * (مثلاً montzar روی فروشگاهی که تابلوی zaarbox دارد) تا روزها + * «آخرین قیمت» می‌ماند و مظنه پرت نشان داده می‌شود. */ - public function referenceQuotePrice(): float + public function lastProviderQuotePrice(): int { - $last = Price::query()->orderByDesc('id')->value('price'); + $provider = $this->priceProvider(); - return $last !== null ? (float) $last : 0.0; + return (int) Cache::remember( + 'price:last_provider_quote:' . $provider, + 10, + function () use ($provider) { + try { + return (int) PriceSyncLog::query() + ->where('source', $provider) + ->where('liquid_gold_price', '>', 0) + ->orderByDesc('id') + ->value('liquid_gold_price'); + } catch (\Throwable $e) { + return 0; + } + } + ); + } + + /** + * کمینه/بیشینهٔ قیمت مرجع امروز — فقط ردیف‌های همین provider. + * + * @return array{min: int, max: int} + */ + public function providerTodayQuoteExtremes(): array + { + $provider = $this->priceProvider(); + + $row = Cache::remember( + 'price:provider_today_extremes:' . $provider, + 10, + function () use ($provider) { + try { + $stats = PriceSyncLog::query() + ->where('source', $provider) + ->where('liquid_gold_price', '>', 0) + ->whereDate('created_at', now()->toDateString()) + ->selectRaw('MIN(liquid_gold_price) as min_price, MAX(liquid_gold_price) as max_price') + ->first(); + + return [ + 'min' => (int) ($stats->min_price ?? 0), + 'max' => (int) ($stats->max_price ?? 0), + ]; + } catch (\Throwable $e) { + return ['min' => 0, 'max' => 0]; + } + } + ); + + return is_array($row) ? $row : ['min' => 0, 'max' => 0]; } /** مظنهٔ مثبت قابل استفاده است — محدودیت انحراف نسبت به DB برداشته شد. */ @@ -542,18 +596,24 @@ class ConfigService $currentUp = (float) ($upSetting->value ?: 0); $currentDown = (float) ($downSetting->value ?: 0); - $anchor = $currentUp > 0 ? $currentUp : $this->referenceQuotePrice(); - if ($anchor > 0 && ! $this->isPlausibleQuotePrice((float) $priceInt, $anchor)) { - return; - } - if ($extremeDate !== $today) { + // سقف و کف روز باید از یک منبع باشند؛ وگرنه تیک provider دیگر + // بازهٔ جعلی می‌سازد (مثلاً سقف تابلو و کف montzar). + $provider = $this->priceProvider(); + $extremeSource = trim((string) $this->getString('price_extreme_source', '')); + + if ($extremeDate !== $today || ($extremeSource !== '' && $extremeSource !== $provider)) { $this->UpdateConfig('up_price', $priceInt); $this->UpdateConfig('down_price', $priceInt); $this->UpdateConfig('price_extreme_date', $today); + $this->UpdateConfig('price_extreme_source', $provider); return; } + if ($extremeSource === '') { + $this->UpdateConfig('price_extreme_source', $provider); + } + if ($currentUp <= 0 || $priceInt > $currentUp) { $this->UpdateConfig('up_price', $priceInt); } @@ -569,6 +629,12 @@ class ConfigService */ public function resolvedDailyBaseExtremes(): array { + // تاریخچهٔ برچسب‌دار همین provider — سقف/کف بدون قاطی شدن با تیک منبع دیگر + $fromLog = $this->providerTodayQuoteExtremes(); + if ($fromLog['max'] > 0 && $fromLog['min'] > 0) { + return ['up' => $fromLog['max'], 'down' => $fromLog['min']]; + } + $up = $this->getInt('up_price', 0); $down = $this->getInt('down_price', 0); @@ -604,21 +670,8 @@ class ConfigService [$up, $down] = [$down, $up]; } - // کف روز آلوده به مظنهٔ اشتباه provider (مثلاً ۱۶۸M) را دور بریز - if ($up > 0 && $down > 0 && ! $this->isPlausibleQuotePrice((float) $down, (float) $up)) { - $fromTable = $this->filteredTodayMinMax(); - if ($fromTable['min'] > 0 && $this->isPlausibleQuotePrice((float) $fromTable['min'], (float) $up)) { - $down = $fromTable['min']; - } else { - $down = $up; - } - if ($this->getInt('down_price', 0) !== $down) { - $this->UpdateConfig('down_price', $down); - } - } - $storedDown = $this->getInt('down_price', 0); - if ($down > 0 && $down < $storedDown && $this->isPlausibleQuotePrice((float) $down, (float) max($up, 1))) { + if ($down > 0 && $down < $storedDown) { $this->UpdateConfig('down_price', $down); } diff --git a/app/Services/LivePriceCacheService.php b/app/Services/LivePriceCacheService.php index 0a3bda4..16903df 100644 --- a/app/Services/LivePriceCacheService.php +++ b/app/Services/LivePriceCacheService.php @@ -2,7 +2,6 @@ namespace App\Services; -use App\Models\Price; use Illuminate\Support\Facades\Cache; class LivePriceCacheService @@ -114,8 +113,7 @@ class LivePriceCacheService { $config = app(ConfigService::class); $display = app(ProductDisplayPriceService::class); - $last = Price::orderByDesc('id')->first(); - $base = $last ? (float) $last->price : 0.0; + $base = (float) $display->resolveHistoricalBasePrice(); $provider = $config->priceProvider(); $montzarSource = 0; diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index 589f327..ac95814 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -281,8 +281,19 @@ class ProductDisplayPriceService return $historical; } + /** + * آخرین قیمت پایهٔ ذخیره‌شده. + * + * اول تاریخچهٔ برچسب‌دار همین provider (price_sync_logs)؛ جدول prices منبع را + * ذخیره نمی‌کند و یک تیک اشتباه از provider دیگر «آخرین قیمت» را پرت می‌کند. + */ public function resolveHistoricalBasePrice(): int { + $fromProviderLog = app(ConfigService::class)->lastProviderQuotePrice(); + if ($fromProviderLog > 0) { + return $fromProviderLog; + } + $last = Price::orderByDesc('id')->first(); if (!$last || (float) $last->price <= 0) { return 0; @@ -562,10 +573,7 @@ class ProductDisplayPriceService } if ($price <= 0 && $config->getBool('show_last_price', false)) { - $last = Price::orderByDesc('id')->first(); - if ($last && (float) $last->price > 0) { - $price = (int) $last->price; - } + $price = $this->resolveHistoricalBasePrice(); } $diffBuy = (int) $config->displayBuyPriceDiffRial(); diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index b0a4450..7fbda47 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -2334,7 +2334,7 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN ( -- تنظیمات پیشرفته (can_edit=0) — فقط در پنل پیشرفته؛ ادمین دیپلوی ویرایش می‌کند UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( - 'up_price', 'down_price', 'price_extreme_date', + 'up_price', 'down_price', 'price_extreme_date', 'price_extreme_source', 'main_product', 'price_provider', 'zaarbox_api_url', 'montzar_base_url', 'montzar_diff', 'montzar_closed', 'montzar_order_response', 'montzar_token', 'montzar_live_price', 'montzar_last_product_log', @@ -2513,7 +2513,7 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN ( 'do_send_liman' ); UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( - 'up_price', 'down_price', 'price_extreme_date', + 'up_price', 'down_price', 'price_extreme_date', 'price_extreme_source', 'main_product', 'price_provider', 'zaarbox_api_url', 'montzar_base_url', 'montzar_diff', 'montzar_closed', 'montzar_order_response', 'montzar_token', 'montzar_live_price', 'montzar_last_product_log',