From b5d2b878decbc655a9462ebf5d69911187437aa4 Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 11 Jun 2026 19:23:16 +0330 Subject: [PATCH] fix bug --- app/Console/Commands/MigrateCommand.php | 21 +++ app/Console/Commands/SyncProductPrices.php | 4 +- .../Controllers/Admin/ProductController.php | 9 +- app/Providers/AppServiceProvider.php | 7 +- app/Services/ProductDisplayPriceService.php | 143 ++++++++++++++---- app/Services/ProductPriceSyncService.php | 34 +---- app/Services/ZaarboxPriceService.php | 104 ++++++++----- database/schema/README.md | 16 ++ ...-schema.sql => mysql-schema.sql.reference} | 0 database/sql/DEPLOY_CHECKLIST.md | 15 +- 10 files changed, 237 insertions(+), 116 deletions(-) create mode 100644 app/Console/Commands/MigrateCommand.php create mode 100644 database/schema/README.md rename database/schema/{mysql-schema.sql => mysql-schema.sql.reference} (100%) diff --git a/app/Console/Commands/MigrateCommand.php b/app/Console/Commands/MigrateCommand.php new file mode 100644 index 0000000..3e76ee1 --- /dev/null +++ b/app/Console/Commands/MigrateCommand.php @@ -0,0 +1,21 @@ +migrator->repositoryExists()) { + $this->call('migrate:install', array_filter([ + '--database' => $this->option('database'), + ])); + } + } +} diff --git a/app/Console/Commands/SyncProductPrices.php b/app/Console/Commands/SyncProductPrices.php index 81e1198..6db79a6 100644 --- a/app/Console/Commands/SyncProductPrices.php +++ b/app/Console/Commands/SyncProductPrices.php @@ -9,11 +9,11 @@ class SyncProductPrices extends Command { protected $signature = 'product:sync-prices'; - protected $description = 'Sync variable product prices from external feeds (every ~30s via scheduler)'; + protected $description = 'Fetch zaarbox price board + sync product DB (scheduler: every 30s)'; public function handle(ProductPriceSyncService $syncService): int { - // کران: همیشه فید تازه از zaarbox + // کران: GET https://price.zaarbox.ir/api/free (یا zaarbox_api_url) → کش + UPDATE محصولات $syncService->syncAll(true); return self::SUCCESS; diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index 1f82659..0d8a76a 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -152,12 +152,9 @@ class ProductController extends SuperController public function listProduct() { - // لیست کاربر: فقط zaarbox با کش — فید legacy در کران (product:sync-prices) - $syncService = app(\App\Services\ProductPriceSyncService::class); - if (str_contains(request()->path(), '/customer/')) { - $syncService->syncForProductList(); - } else { - $syncService->syncAll(false); + // ادمین: sync اختیاری از کش — کاربر: فقط enrich از کش کران (بدون HTTP / بدون UPDATE) + if (!str_contains(request()->path(), '/customer/')) { + app(\App\Services\ProductPriceSyncService::class)->syncAll(false); } $products = Product::with(['file'])->where('active', 1)->get(); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ee8ca5b..7c0ccd5 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -13,7 +13,12 @@ class AppServiceProvider extends ServiceProvider */ public function register() { - // + $this->app->extend('command.migrate', function ($command, $app) { + return new \App\Console\Commands\MigrateCommand( + $app['migrator'], + $app['events'] + ); + }); } /** diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index 7966d3f..f87c987 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -6,15 +6,19 @@ use App\Models\Price; use App\Models\Product; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Schema; /** - * قیمت نمایشی محصولات آبشده با منبع «قیمت اپ» — در DB صفر است، برای API پر می‌شود. + * قیمت نمایشی محصولات برای API — فقط از کش/تنظیمات (بدون HTTP روی list). + * + * • آبشده با «قیمت اپ» → montzar_live_price / کش /api/price/last + * • محصول با reading_method → ردیف فید zaarbox از کش کران (۳۰ث) */ class ProductDisplayPriceService { public const LIVE_PRICE_CACHE_KEY = 'live_price_api_payload'; - public const LIVE_PRICE_CACHE_SECONDS = 15; + public const LIVE_PRICE_CACHE_SECONDS = 45; public function usesAppPrice(Product $product): bool { @@ -29,44 +33,81 @@ class ProductDisplayPriceService return $this->isMeltedTradeProduct($product); } + /** + * پر کردن price روی مدل‌ها برای پاسخ JSON — بدون UPDATE دیتابیس + */ public function enrichForApi(Collection|iterable $products): void { - $needsLivePrice = false; + $collection = collect($products); + $zaarbox = app(ZaarboxPriceService::class); + $zaarboxFeed = $zaarbox->getCachedPriceListOnly(); - foreach ($products as $product) { - if ($product instanceof Product && $this->usesAppPrice($product)) { - $needsLivePrice = true; - break; + $baseAppPrice = 0; + if ($collection->contains(fn ($p) => $p instanceof Product && $this->usesAppPrice($p))) { + $baseAppPrice = $this->resolveAppBasePrice(); + } + + foreach ($collection as $product) { + if (!$product instanceof Product) { + continue; + } + + if ($this->usesAppPrice($product)) { + $this->applyAppPrice($product, $baseAppPrice); + continue; + } + + if ($product->reading_method && is_array($zaarboxFeed)) { + $this->applyReferenceFeedPrice($product, $zaarboxFeed, $zaarbox); } } + } - if (!$needsLivePrice) { - return; - } - - $basePrice = $this->resolveAppBasePrice(); + private function applyAppPrice(Product $product, int $basePrice): void + { if ($basePrice <= 0) { return; } - foreach ($products as $product) { - if (!$product instanceof Product || !$this->usesAppPrice($product)) { - continue; + $offset = Product::hasPriceOffsetColumn() + ? (float) ($product->price_offset_percent ?? 0) + : 0.0; + + $computed = ProductPriceService::adjustedTotal($basePrice, 0, $offset); + if ($computed <= 0) { + return; + } + + $product->setAttribute('price', $computed); + if (Schema::hasColumn($product->getTable(), 'price_profit')) { + $product->setAttribute('price_profit', 0); + } + } + + /** @param array> $feed */ + private function applyReferenceFeedPrice(Product $product, array $feed, ZaarboxPriceService $zaarbox): void + { + $row = $zaarbox->findFeedRow($feed, (string) $product->reading_method); + if ($row === null) { + return; + } + + $computed = $zaarbox->computeAdjustedProductPrice($product, $row); + if ($computed <= 0) { + if ( + Product::hasUseLastPriceIfZeroColumn() + && ($product->use_last_price_if_zero ?? false) + && (int) ($product->price ?? 0) > 0 + ) { + return; } - $offset = Product::hasPriceOffsetColumn() - ? (float) ($product->price_offset_percent ?? 0) - : 0.0; + return; + } - $computed = ProductPriceService::adjustedTotal($basePrice, 0, $offset); - if ($computed <= 0) { - continue; - } - - $product->setAttribute('price', $computed); - if (\Illuminate\Support\Facades\Schema::hasColumn($product->getTable(), 'price_profit')) { - $product->setAttribute('price_profit', 0); - } + $product->setAttribute('price', $computed); + if (Schema::hasColumn($product->getTable(), 'price_profit')) { + $product->setAttribute('price_profit', 0); } } @@ -83,8 +124,12 @@ class ProductDisplayPriceService public function resolveAppBasePrice(): int { - $payload = $this->livePricePayload(); + return $this->extractBaseFromPayload($this->livePricePayload()); + } + /** @param array $payload */ + private function extractBaseFromPayload(array $payload): int + { $price = (int) ($payload['price'] ?? 0); if ($price > 0) { return $price; @@ -96,7 +141,12 @@ class ProductDisplayPriceService return (int) round(($buy + $sell) / 2); } - return (int) ($payload['price_last'] ?? 0); + $priceLast = (int) ($payload['price_last'] ?? 0); + if ($priceLast > 0) { + return $priceLast; + } + + return 0; } /** @return array{price: int, buy: int, sell: int, price_last: int} */ @@ -120,8 +170,7 @@ class ProductDisplayPriceService if ($provider === 'montzar') { $price = $config->getInt('montzar_live_price', 0); } elseif ($provider === 'zaarbox') { - $zaarbox = app(ZaarboxPriceService::class)->fetchPrice(); - $price = (int) ($zaarbox['order_price'] ?? 0); + $price = $this->resolveZaarboxBoardPriceFromCache($config); } elseif ($provider === 'main_product') { $main = $config->mainProduct(); if ($main) { @@ -131,7 +180,7 @@ class ProductDisplayPriceService $price = $config->getInt('taban_gohar_live_price', 0); } - if ($price <= 0) { + if ($price <= 0 && $config->getBool('show_last_price', false)) { $last = Price::orderByDesc('id')->first(); if ($last && (float) $last->price > 0) { $price = (int) $last->price; @@ -141,12 +190,40 @@ class ProductDisplayPriceService $diffBuy = (int) $config->displayBuySpreadRial(); $diffSell = (int) $config->displaySellSpreadRial(); - return [ + $payload = [ 'price' => $price, 'buy' => $price > 0 ? $price + $diffBuy : 0, 'sell' => $price > 0 ? max(0, $price - $diffSell) : 0, 'price_last' => $price > 0 ? $price + $diffBuy : 0, ]; + + if ($this->extractBaseFromPayload($payload) > 0) { + $this->cacheLivePricePayload($payload); + } + + return $payload; + } + + private function resolveZaarboxBoardPriceFromCache(ConfigService $config): int + { + $identifier = trim($config->getString('zaarbox_price_key', '')); + if ($identifier === '') { + return 0; + } + + $feed = app(ZaarboxPriceService::class)->getCachedPriceListOnly(); + if (!is_array($feed)) { + return 0; + } + + $row = app(ZaarboxPriceService::class)->findFeedRow($feed, $identifier); + if ($row === null) { + return 0; + } + + $row = app(ZaarboxPriceService::class)->normalizeFeedRow($row); + + return (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0)); } private function isMeltedTradeProduct(Product $product): bool diff --git a/app/Services/ProductPriceSyncService.php b/app/Services/ProductPriceSyncService.php index 9ccb32b..822c5ef 100644 --- a/app/Services/ProductPriceSyncService.php +++ b/app/Services/ProductPriceSyncService.php @@ -2,7 +2,6 @@ namespace App\Services; -use App\Http\Controllers\Site\ReportController; use App\Models\Product; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; @@ -23,32 +22,6 @@ class ProductPriceSyncService ) { } - /** - * sync سبک برای /customer/product/list — فقط zaarbox با کش کوتاه. - * فید legacy (تا ۴۰ث timeout) فقط در کران اجرا می‌شود. - */ - public function syncForProductList(): bool - { - if (!$this->configService->getBool('product_update_price_on_list', true)) { - return false; - } - - if ( - !Product::hasStoredPriceColumns() - || !Product::query()->whereNotNull('reading_method')->exists() - ) { - return false; - } - - try { - return $this->zaarboxPriceService->syncProductsFromApi(false); - } catch (\Throwable $e) { - Log::warning('Zaarbox product list sync failed', ['message' => $e->getMessage()]); - - return false; - } - } - /** @param bool $forceRefresh درخواست تازه از https://price.zaarbox.ir/api/free (کران) */ public function syncAll(bool $forceRefresh = false): bool { @@ -104,11 +77,10 @@ class ProductPriceSyncService $synced = true; if ($product->bubble == 1) { - $reportController = new ReportController(); - $liquidGold = $reportController->lastPrice(); + $liquidGoldPrice = app(ProductDisplayPriceService::class)->resolveAppBasePrice(); $p = Product::find($product->id); - if ($p && ($liquidGold['price'] ?? 0) > 0) { - $p->amount = $RPrice['price'] / $liquidGold['price']; + if ($p && $liquidGoldPrice > 0) { + $p->amount = $RPrice['price'] / $liquidGoldPrice; $p->show = $show; $p->save(); } diff --git a/app/Services/ZaarboxPriceService.php b/app/Services/ZaarboxPriceService.php index 30eb944..860281a 100644 --- a/app/Services/ZaarboxPriceService.php +++ b/app/Services/ZaarboxPriceService.php @@ -19,8 +19,8 @@ class ZaarboxPriceService private const CACHE_TTL_MINUTES = 35; - /** حداکثر فاصلهٔ استفاده مجدد از فید بدون درخواست جدید (ثانیه) */ - private const PRICE_LIST_CACHE_SECONDS = 25; + /** TTL کش فید — کران هر ۳۰ث refresh می‌کند؛ list فقط از همین می‌خواند */ + private const PRICE_LIST_CACHE_SECONDS = 45; private const DEFAULT_HISTORY_ID = 'ca3800a0-1f57-3528-b963-178f9168e04f'; @@ -135,8 +135,8 @@ class ZaarboxPriceService return $empty; } - $row = $this->findRow(collect($prices), $identifier); - if (empty($row)) { + $row = $this->findFeedRow($prices, $identifier); + if ($row === null) { Log::warning('Zaarbox price row not found', [ 'identifier' => $identifier, 'api' => $this->apiUrl(), @@ -192,12 +192,26 @@ class ZaarboxPriceService $prices = $this->fetchPriceList(); if ($prices !== null) { - Cache::put(self::PRICE_LIST_CACHE_KEY, $prices, now()->addSeconds(self::PRICE_LIST_CACHE_SECONDS)); + $this->storePriceListCache($prices); } return $prices; } + /** فقط کش — برای /customer/product/list (بدون HTTP) */ + public function getCachedPriceListOnly(): ?array + { + $cached = Cache::get(self::PRICE_LIST_CACHE_KEY); + + return is_array($cached) ? $cached : null; + } + + /** @param array> $prices */ + public function storePriceListCache(array $prices): void + { + Cache::put(self::PRICE_LIST_CACHE_KEY, $prices, now()->addSeconds(self::PRICE_LIST_CACHE_SECONDS)); + } + /** * @return array */ @@ -244,8 +258,8 @@ class ZaarboxPriceService return $empty; } - $row = $this->findRow(collect($prices), $readingMethod); - if (empty($row)) { + $row = $this->findFeedRow($prices, $readingMethod); + if ($row === null) { return $empty; } @@ -283,7 +297,6 @@ class ZaarboxPriceService $config = $this->config; $mainProductId = $config->mainProductId(); $hasExtremeKeys = Setting::whereIn('key', ['up_price', 'down_price'])->count() === 2; - $collection = collect($prices); $copper = null; $dollar = null; @@ -291,8 +304,8 @@ class ZaarboxPriceService $currentTime = now()->format('H:i:s'); foreach ($products as $product) { - $RPrice = $collection->firstWhere('key', $product->reading_method); - if (empty($RPrice)) { + $RPrice = $this->findFeedRow($prices, (string) $product->reading_method); + if ($RPrice === null) { continue; } @@ -326,11 +339,11 @@ class ZaarboxPriceService } if ((int) ($product->bubble ?? 0) === 1) { - $liquidGold = app(\App\Http\Controllers\Site\ReportController::class)->lastPrice(); + $liquidGoldPrice = app(ProductDisplayPriceService::class)->resolveAppBasePrice(); $p = Product::find($product->id); - if ($p && ($liquidGold['price'] ?? 0) > 0) { + if ($p && $liquidGoldPrice > 0) { $show = ($currentTime >= $product->auto_open_at && $currentTime <= $product->auto_close_at); - $p->amount = $row['price'] / $liquidGold['price']; + $p->amount = $row['price'] / $liquidGoldPrice; $p->show = $show; $p->save(); } @@ -363,8 +376,49 @@ class ZaarboxPriceService return true; } + /** + * جستجو در فید zaarbox — key / id / name (همان reading_method ادمین) + * + * @param array> $prices + * @return array|null + */ + public function findFeedRow(array $prices, string $identifier): ?array + { + $identifier = trim($identifier); + if ($identifier === '') { + return null; + } + + $collection = collect($prices); + + foreach (['key', 'id', 'name'] as $field) { + $row = $collection->firstWhere($field, $identifier); + if (!empty($row)) { + return is_array($row) ? $row : (array) $row; + } + } + + return null; + } + + /** + * قیمت نمایشی محصول از یک ردیف فید + offset درصد — بدون نوشتن DB + * + * @param array $row + */ + public function computeAdjustedProductPrice(Product $product, array $row): int + { + $normalized = $this->normalizeFeedRow($row); + + return ProductPriceService::adjustedTotal( + (int) ($normalized['price'] ?? 0), + (int) ($normalized['profit'] ?? 0), + ProductPriceService::offsetPercent($product) + ); + } + /** @param array $row */ - private function normalizeFeedRow(array $row): array + public function normalizeFeedRow(array $row): array { if (($row['name'] ?? '') === 'گلد دات ای از آبشده - رسمی - نقد فردا') { $row['price'] = ($row['price'] ?? 0) / 4.3318; @@ -388,8 +442,8 @@ class ZaarboxPriceService return false; } - $RPrice = collect($prices)->firstWhere('key', $product->reading_method); - if (empty($RPrice)) { + $RPrice = $this->findFeedRow($prices, (string) $product->reading_method); + if ($RPrice === null) { return false; } @@ -531,22 +585,4 @@ class ZaarboxPriceService } } - /** - * @param Collection> $collection - * @return array|null - */ - private function findRow(Collection $collection, string $identifier): ?array - { - $row = $collection->firstWhere('key', $identifier); - if (!empty($row)) { - return $row; - } - - $row = $collection->firstWhere('id', $identifier); - if (!empty($row)) { - return $row; - } - - return null; - } } diff --git a/database/schema/README.md b/database/schema/README.md new file mode 100644 index 0000000..da43739 --- /dev/null +++ b/database/schema/README.md @@ -0,0 +1,16 @@ +# Schema reference — دستی import کنید + +`mysql-schema.sql.reference` فقط مرجع است. Laravel دیگر آن را با `php artisan migrate` load **نمی‌کند**. + +## DB خالی + +```bash +mysql -h HOST -P PORT -u USER -p DATABASE < database/schema/mysql-schema.sql.reference +mysql ... < database/sql/baseline_migrations.sql +php artisan migrate +mysql ... < database/sql/deploy_settings_const.sql +``` + +## DB موجود + +`mysql-schema.sql.reference` **نزنید** — فقط `baseline_migrations.sql` + `migrate`. diff --git a/database/schema/mysql-schema.sql b/database/schema/mysql-schema.sql.reference similarity index 100% rename from database/schema/mysql-schema.sql rename to database/schema/mysql-schema.sql.reference diff --git a/database/sql/DEPLOY_CHECKLIST.md b/database/sql/DEPLOY_CHECKLIST.md index 0713221..ba09120 100644 --- a/database/sql/DEPLOY_CHECKLIST.md +++ b/database/sql/DEPLOY_CHECKLIST.md @@ -1,5 +1,8 @@ # چک‌لیست Deploy — برنچ ماژولار +> **ایمنی:** `php artisan migrate` دیگر `mysql-schema.sql` را auto-load نمی‌کند (DROP TABLE نمی‌زند). +> فایل schema فقط دستی: `database/schema/mysql-schema.sql.reference` + ## قبل از شروع - [ ] بکاپ DB @@ -23,16 +26,12 @@ DB_PASSWORD=$(grep ^DB_PASSWORD= .env | cut -d= -f2- | tr -d '\r"') # ۱ baseline mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" \ < database/sql/baseline_migrations.sql - - #mysql -h 127.0.0.1 -P 3030 -u root -p soroushgold < database/sql/baseline_migrations.sql # باید ۱۱ ردیف چاپ شود (شامل 2019 و 2021) # ۲ چک DB — اگر کمتر از ۲ بود baseline قدیمی است mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" -N -e \ "SELECT COUNT(*) FROM migrations WHERE migration LIKE '2019%' OR migration LIKE '2021%';" - - #mysql -h "127.0.0.1" -P "3030" -u "root" -p"1ipqLRRMC7qS8s3WkuwXiS7P" "soroushgold" -N -e \"SELECT COUNT(*) FROM migrations WHERE migration LIKE '2019%' OR migration LIKE '2021%';" # باید: 2 @@ -45,8 +44,6 @@ php artisan migrate # ۵ settings mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" \ < database/sql/deploy_settings_const.sql - - #mysql -h "127.0.0.1" -P "3030" -u "root" -p"1ipqLRRMC7qS8s3WkuwXiS7P" "soroushgold" \ < database/sql/deploy_settings_const.sql ``` **یا:** `bash database/sql/run_deploy_existing_db.sh` @@ -87,12 +84,12 @@ mysql -h 127.0.0.1 -P 3030 -u root -p soroushgold \ ## DB خالی ```bash +mysql ... < database/schema/mysql-schema.sql.reference +mysql ... < database/sql/baseline_migrations.sql php artisan migrate mysql ... < database/sql/deploy_settings_const.sql ``` -`baseline_migrations.sql` **نزنید**. - --- ## خطاها @@ -100,7 +97,7 @@ mysql ... < database/sql/deploy_settings_const.sql | خطا | فیکس | |-----|------| | `Table already exists` | `fake_package_migrations.sql` → `migrate` | -| `Loading stored database schema` | restore بکاپ → `baseline` قبل از `migrate` | +| `Loading stored database schema` | کد قدیمی — `git pull` (دیگر رخ نمی‌دهد). اگر شد: restore بکاپ | | `2019`/`2021` No بعد از baseline | baseline قدیمی → `fake_package_migrations.sql` | | `Unknown host HOST` | از `.env` بخوان نه placeholder | | `Unknown column 'is_exist'` | `git pull` + دوباره `deploy_settings_const.sql` (نسخه جدید ستون را اضافه می‌کند) |