From 92d01f96da96bb1b919a3836d6d5f7ff1e285442 Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 4 Jun 2026 14:40:02 +0330 Subject: [PATCH] zarbox added --- .../Controllers/Site/ReportController.php | 42 +++- app/Services/ConfigService.php | 41 ++- app/Services/EtehadiePriceService.php | 31 +++ app/Services/ZaarboxPriceService.php | 233 ++++++------------ config/price_providers.php | 4 +- database/sql/goldali_compat_patch.sql | 10 +- database/sql/goldali_database_settings.sql | 3 +- database/sql/goldali_zaarbox_price.sql | 25 ++ 8 files changed, 208 insertions(+), 181 deletions(-) create mode 100644 database/sql/goldali_zaarbox_price.sql diff --git a/app/Http/Controllers/Site/ReportController.php b/app/Http/Controllers/Site/ReportController.php index 6da70f6..52179e7 100644 --- a/app/Http/Controllers/Site/ReportController.php +++ b/app/Http/Controllers/Site/ReportController.php @@ -10,6 +10,8 @@ use App\Models\Setting; use App\Models\ConfigStatic; use App\Models\Customer; use App\Services\MontzarService; +use App\Services\EtehadiePriceService; +use App\Services\ZaarboxPriceService; use App\Models\Price; use App\Models\PriceCandle; use App\Models\PriceTrigger; @@ -140,6 +142,7 @@ class ReportController extends Controller $closed = false; $montzar_price = 0; + $zaarbox_price = 0; $tgn = 0; $pr_price = 0; @@ -165,6 +168,7 @@ class ReportController extends Controller 'main_product' => 0, 'taban_gohar' => 0, 'montzar' => 0, + 'zaarbox' => 0, 'closed' => true, 'time' => now(), ]); @@ -305,19 +309,31 @@ class ReportController extends Controller } } + /* + |-------------------------------------------------------------------------- + | Zaarbox — price_provider=zaarbox + settings.zaarbox_price_key + |-------------------------------------------------------------------------- + */ + if ($priceProvider === 'zaarbox' && !$closed) { + $zaarbox = app(ZaarboxPriceService::class)->fetchPrice(); + $zaarbox_price = (int) ($zaarbox['order_price'] ?? 0); + } + /* |-------------------------------------------------------------------------- | Main Product — settings.main_product سپس const |-------------------------------------------------------------------------- */ - $main_product_id = $config->mainProductId(); - if ($main_product_id && !$closed) { - if ($priceProvider === 'main_product' || $toBool($main_product_price)) { - $pr_product = Product::find($main_product_id); - if ($pr_product) { - $pr_price = (float) $pr_product['price'] + (float) $pr_product['price_profit']; - } + $pr_product = null; + if ($priceProvider === 'main_product' || $toBool($main_product_price)) { + $pr_product = $config->mainProduct(); + } + if ($pr_product && !$closed) { + if ($priceProvider === 'main_product') { + app(EtehadiePriceService::class)->syncProductFromApi($pr_product->fresh()); + $pr_product->refresh(); } + $pr_price = (float) $pr_product->price + (float) ($pr_product->price_profit ?? 0); } /* @@ -349,7 +365,10 @@ class ReportController extends Controller | انتخاب قیمت اصلی (همان price_provider) |-------------------------------------------------------------------------- */ - if ($priceProvider === 'montzar' && ($montzar_price ?? 0) !== 0 && !$closed) { + if ($priceProvider === 'zaarbox' && $zaarbox_price > 0 && !$closed) { + $priceValue = $zaarbox_price; + $priceSource = 'zaarbox'; + } elseif ($priceProvider === 'montzar' && ($montzar_price ?? 0) !== 0 && !$closed) { $priceValue = (int) $montzar_price; $priceSource = 'montzar'; } elseif ($priceProvider === 'taban_gohar' && $toBool($taban_gohar_price) && ($tgn ?? 0) !== 0 && !$closed) { @@ -373,6 +392,7 @@ class ReportController extends Controller 'main_product' => $pr_price ?? 0, 'taban_gohar' => $tgn ?? 0, 'montzar' => $montzar_price ?? 0, + 'zaarbox' => $zaarbox_price ?? 0, 'closed' => true, 'time' => now(), ]); @@ -389,6 +409,7 @@ class ReportController extends Controller 'main_product' => $pr_price ?? 0, 'taban_gohar' => $tgn ?? 0, 'montzar' => $montzar_price ?? 0, + 'zaarbox' => $zaarbox_price ?? 0, 'closed' => true, 'time' => now(), ]); @@ -478,9 +499,9 @@ class ReportController extends Controller } /* - | main_product / گلد علی: سفارش از Price::latest() — نمایش buy/sell همان منبع. + | main_product / zaarbox: سفارش از Price::latest() — نمایش buy/sell همان منبع. */ - if ($priceProvider === 'main_product' && !$closed) { + if (in_array($priceProvider, ['main_product', 'zaarbox'], true) && !$closed) { $storedPrice = $lastPrice ?? Price::latest()->first(); if ($storedPrice && (float) $storedPrice->price > 0) { $base = (float) $storedPrice->price; @@ -502,6 +523,7 @@ class ReportController extends Controller 'main_product' => $pr_price ?? 0, 'taban_gohar' => $tgn ?? 0, 'montzar' => $montzar_price ?? 0, + 'zaarbox' => $zaarbox_price ?? 0, 'closed' => false, 'time' => now(), ]); diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 34708a0..aff0797 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -3,11 +3,12 @@ namespace App\Services; use App\Models\Cnt; +use App\Models\Product; use App\Models\Setting; class ConfigService { - public const PRICE_PROVIDERS = ['montzar', 'etehadie', 'taban_gohar', 'main_product']; + public const PRICE_PROVIDERS = ['montzar', 'etehadie', 'taban_gohar', 'main_product', 'zaarbox']; public function Config($name) { @@ -82,18 +83,35 @@ class ConfigService } /** - * شناسه محصول آبشده پیش‌فرض: اول settings، بعد جدول const. + * شناسه محصول آبشده: اول settings، بعد const. + * مقدار settings می‌تواند id عددی (مثل 13) یا reading_method (مثل estjt_...) باشد. */ public function mainProductId(): ?int { - $fromSettings = $this->getString('main_product', ''); - if ($fromSettings !== '' && is_numeric($fromSettings)) { - return (int) $fromSettings; - } + $product = $this->mainProduct(); - $fromConst = Cnt::where('key', 'main_product')->value('value'); - if ($fromConst !== null && $fromConst !== '' && is_numeric($fromConst)) { - return (int) $fromConst; + return $product?->id; + } + + public function mainProduct(): ?Product + { + foreach ([$this->getString('main_product', ''), (string) (Cnt::where('key', 'main_product')->value('value') ?? '')] as $raw) { + $raw = trim($raw); + if ($raw === '') { + continue; + } + + if (is_numeric($raw)) { + $product = Product::find((int) $raw); + if ($product) { + return $product; + } + } + + $byMethod = Product::where('reading_method', $raw)->first(); + if ($byMethod) { + return $byMethod; + } } return null; @@ -122,6 +140,11 @@ class ConfigService return $this->priceProvider() === 'main_product'; } + public function usesZaarboxPricing(): bool + { + return $this->priceProvider() === 'zaarbox'; + } + public function UpdateConfig($name, $value) { return Setting::updateOrCreate( diff --git a/app/Services/EtehadiePriceService.php b/app/Services/EtehadiePriceService.php index f3c29bb..e7203e4 100644 --- a/app/Services/EtehadiePriceService.php +++ b/app/Services/EtehadiePriceService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\Product; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -142,4 +143,34 @@ class EtehadiePriceService return true; } + + /** به‌روز قیمت یک محصول از API با reading_method */ + public function syncProductFromApi(Product $product): bool + { + if (!$product->reading_method) { + return false; + } + + $prices = $this->fetchPriceList(); + if ($prices === null) { + return false; + } + + $RPrice = collect($prices)->firstWhere('key', $product->reading_method); + if (empty($RPrice)) { + return false; + } + + if (($RPrice['name'] ?? '') === 'گلد دات ای از آبشده - رسمی - نقد فردا') { + $RPrice['price'] = ($RPrice['price'] ?? 0) / 4.3318; + } + + ProductPriceService::syncFromReference( + $product, + (int) $RPrice['price'], + (int) ($RPrice['profit'] ?? 0) + ); + + return true; + } } diff --git a/app/Services/ZaarboxPriceService.php b/app/Services/ZaarboxPriceService.php index 031349f..350a02c 100644 --- a/app/Services/ZaarboxPriceService.php +++ b/app/Services/ZaarboxPriceService.php @@ -2,184 +2,99 @@ namespace App\Services; -use Carbon\Carbon; -use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ZaarboxPriceService { - private const CACHE_KEY_PREFIX = 'zaarbox_gold_prices'; - - private const CACHE_TTL_MINUTES = 35; - - private const HISTORY_ID = 'ca3800a0-1f57-3528-b963-178f9168e04f'; - - private const PERIODS = [ - 'DAILY' => ['hours' => 24, 'intervalMs' => 900000], - 'WEEKLY' => ['hours' => 168, 'intervalMs' => 7200000], - 'MONTHLY' => ['hours' => 720, 'intervalMs' => 28800000], - ]; - - private function config(): ConfigService + public function __construct(private ConfigService $config) { - return app(ConfigService::class); } - private function historyId(): string + public function isEnabled(): bool { - $chartId = $this->config()->Config('chart_id'); - - return $chartId ?: self::HISTORY_ID; + return $this->config->usesZaarboxPricing(); } - private function cacheKey(): string + public function apiUrl(): string { - return self::CACHE_KEY_PREFIX . ':' . $this->historyId(); + $url = trim((string) $this->config->getString( + 'zaarbox_api_url', + (string) config('price_providers.zaarbox_api_url', 'https://price.zaarbox.ir/api/free') + )); + + return $url !== '' ? $url : 'https://price.zaarbox.ir/api/free'; } - public function getAllPrices(): array + /** کلید یا id ردیف در JSON تابلو (settings: zaarbox_price_key) */ + public function priceIdentifier(): string { - $cached = Cache::get($this->cacheKey()); + return trim((string) $this->config->getString( + 'zaarbox_price_key', + (string) config('price_providers.zaarbox_price_key', '') + )); + } - if (is_array($cached) && isset($cached['prices'])) { - return $cached['prices']; + /** + * @return array{price: int, order_price: int, raw: array|null} + */ + public function fetchPrice(): array + { + $empty = ['price' => 0, 'order_price' => 0, 'raw' => null]; + $identifier = $this->priceIdentifier(); + if ($identifier === '') { + return $empty; } - return $this->refreshCache(); - } - - public function refreshCache(): array - { - $prices = $this->fetchAllPrices(); - - Cache::put($this->cacheKey(), [ - 'prices' => $prices, - 'refreshed_at' => now()->toIso8601String(), - ], now()->addMinutes(self::CACHE_TTL_MINUTES)); - - return $prices; - } - - private function fetchAllPrices(): array - { - $now = Carbon::now('UTC'); - $prices = []; - - $responses = Http::pool(function ($pool) use ($now) { - foreach (self::PERIODS as $key => $config) { - $start = $now->copy()->subHours($config['hours']); - $pool->as($key)->withHeaders([ - 'Referer' => 'https://tablo.montest.ir/', - 'User-Agent' => 'Mozilla/5.0', - ])->get($this->historyUrl($start, $now)); - } - }); - - foreach (self::PERIODS as $key => $config) { - $response = $responses[$key] ?? null; - $start = $now->copy()->subHours($config['hours']); - - if (!$response || !$response->successful()) { - Log::warning('Zaarbox price history fetch failed', [ - 'period' => $key, - 'status' => $response?->status(), - ]); - $prices[$key] = []; - continue; - } - - $history = $response->json('history') ?? []; - $prices[$key] = $this->mapHistory( - $history, - $this->toMillis($start), - $this->toMillis($now), - $config['intervalMs'] - ); - } - - return $prices; - } - - private function historyUrl(Carbon $start, Carbon $end): string - { - $id = $this->historyId(); - - return sprintf( - 'https://price.zaarbox.ir/api/history/%s?start=%s&end=%s&limit=1000000000', - $id, - $this->toIso8601($start), - $this->toIso8601($end) - ); - } - - private function toIso8601(Carbon $date): string - { - return $date->format('Y-m-d\TH:i:s') . '.' . str_pad((string) $date->millisecond, 3, '0', STR_PAD_LEFT) . 'Z'; - } - - private function toMillis(Carbon $date): int - { - return ($date->getTimestamp() * 1000) + $date->millisecond; - } - - private function mapHistory(array $history, int $startMs, int $endMs, int $intervalMs): array - { - $points = []; - - foreach ($history as $entry) { - if (!isset($entry['time'], $entry['price'])) { - continue; - } - - $timestamp = $this->parseTimestampMs($entry['time']); - if ($timestamp === null) { - continue; - } - - $points[] = [ - 'timestamp' => $timestamp, - 'price' => (int) $entry['price'], - ]; - } - - usort($points, fn ($a, $b) => $a['timestamp'] <=> $b['timestamp']); - - $firstBucket = (int) (ceil($startMs / $intervalMs) * $intervalMs); - if ($firstBucket < $startMs) { - $firstBucket += $intervalMs; - } - - $result = []; - $index = 0; - $lastPoint = null; - $count = count($points); - - for ($bucket = $firstBucket; $bucket <= $endMs; $bucket += $intervalMs) { - while ($index < $count && $points[$index]['timestamp'] <= $bucket) { - $lastPoint = $points[$index]; - $index++; - } - - if ($lastPoint === null) { - continue; - } - - $result[] = [ - 'value' => intdiv($lastPoint['price'], 1000), - 'timestamp' => $bucket, - ]; - } - - return $result; - } - - private function parseTimestampMs(string $time): ?int - { try { - return $this->toMillis(Carbon::parse($time)->utc()); - } catch (\Throwable) { - return null; + $prices = Http::withoutVerifying() + ->timeout(20) + ->get($this->apiUrl()) + ->json(); + + if (!is_array($prices)) { + return $empty; + } + + $row = $this->findRow(collect($prices), $identifier); + if (empty($row)) { + Log::warning('Zaarbox price row not found', [ + 'identifier' => $identifier, + 'api' => $this->apiUrl(), + ]); + + return $empty; + } + + $base = (float) ($row['price'] ?? 0); + $profit = (float) ($row['profit'] ?? 0); + $orderPrice = (int) round($base + $profit); + + return ['price' => $orderPrice, 'order_price' => $orderPrice, 'raw' => $row]; + } catch (\Throwable $e) { + Log::warning('Zaarbox price fetch failed', ['message' => $e->getMessage()]); + + return $empty; } } + + /** + * @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/config/price_providers.php b/config/price_providers.php index 03784ed..abdfc24 100644 --- a/config/price_providers.php +++ b/config/price_providers.php @@ -4,7 +4,7 @@ * پیش‌فرض وقتی کلید در جدول settings نیست. * * منبع قیمت: یک کلید → price_provider - * montzar | etehadie | taban_gohar | main_product + * montzar | etehadie | taban_gohar | main_product | zaarbox * * سازگاری قدیمی (بدون price_provider): * montzar_price=true → montzar @@ -14,6 +14,8 @@ return [ 'price_provider' => 'montzar', 'etehadie_api_url' => 'http://109.122.246.48:4949/api/free', + 'zaarbox_api_url' => 'https://price.zaarbox.ir/api/free', + 'zaarbox_price_key' => '', 'price_method' => '', 'register_jibit_verify' => false, 'register_referral_sms_to_referrer' => false, diff --git a/database/sql/goldali_compat_patch.sql b/database/sql/goldali_compat_patch.sql index 45c1d16..bfcb90e 100644 --- a/database/sql/goldali_compat_patch.sql +++ b/database/sql/goldali_compat_patch.sql @@ -3,7 +3,8 @@ START TRANSACTION; --- قیمت: محصول اصلی (نه اتحادیه) +-- قیمت از تابلو Zaarbox (یا main_product — یکی را انتخاب کنید) +-- UPDATE `settings` SET `value` = 'zaarbox' WHERE `key` = 'price_provider'; UPDATE `settings` SET `value` = 'main_product' WHERE `key` = 'price_provider'; INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'price_provider', 'main_product', 'منبع قیمت', 1, 1, 'main_product برای گلد علی' @@ -13,6 +14,13 @@ SELECT 'main_product', '13', 'شناسه محصول آبشده', 1, 1, 'اول s WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_product' LIMIT 1); UPDATE `settings` SET `value` = '13' WHERE `key` = 'main_product'; +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) +SELECT 'zaarbox_api_url', 'https://price.zaarbox.ir/api/free', 'آدرس API تابلو Zaarbox', 1, 1, NULL +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zaarbox_api_url' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) +SELECT 'zaarbox_price_key', 'estjt_41670cdf0270a874b68a0a95a235f9a1', 'key یا id ردیف در JSON', 1, 1, 'مثلاً estjt_... یا UUID' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zaarbox_price_key' LIMIT 1); + -- درگاه (اگر قبلاً نزده‌اید) INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'payping_start_url', 'https://payment.goldaliii.com/payment/start', 'آدرس شروع درگاه', 1, 1, NULL diff --git a/database/sql/goldali_database_settings.sql b/database/sql/goldali_database_settings.sql index 8e3f818..ed13469 100644 --- a/database/sql/goldali_database_settings.sql +++ b/database/sql/goldali_database_settings.sql @@ -15,8 +15,9 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'price_provider', 'main_product', 'منبع قیمت', 1, 1, 'montzar|main_product|taban_gohar' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'price_provider' LIMIT 1); +-- مقدار عددی id محصول (۱۳) — نه UUID تابلو؛ یا reading_method مثل estjt_41670... INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) -SELECT 'main_product', '13', 'شناسه محصول آبشده پیش‌فرض', 1, 1, 'اولویت بر const' +SELECT 'main_product', '13', 'محصول آبشده (id یا reading_method)', 1, 1, 'مثلاً 13 — نه UUID API' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_product' LIMIT 1); UPDATE `settings` SET `value` = '13' WHERE `key` = 'main_product'; diff --git a/database/sql/goldali_zaarbox_price.sql b/database/sql/goldali_zaarbox_price.sql new file mode 100644 index 0000000..e92abcc --- /dev/null +++ b/database/sql/goldali_zaarbox_price.sql @@ -0,0 +1,25 @@ +-- گلد علی — قیمت از API تابلو Zaarbox (price_provider=zaarbox) +-- قبل از اجرا: بکاپ + USE دیتابیس + +START TRANSACTION; + +UPDATE `settings` SET `value` = 'zaarbox' WHERE `key` = 'price_provider'; + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) +SELECT 'price_provider', 'zaarbox', 'منبع قیمت', 1, 1, 'zaarbox | main_product | montzar' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'price_provider' LIMIT 1); + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) +SELECT 'zaarbox_api_url', 'https://price.zaarbox.ir/api/free', 'آدرس API تابلو', 1, 1, NULL +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zaarbox_api_url' LIMIT 1); +UPDATE `settings` SET `value` = 'https://price.zaarbox.ir/api/free' WHERE `key` = 'zaarbox_api_url'; + +-- key ردیف در JSON (یا UUID فیلد id) — از Network تابلو کپی کنید +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) +SELECT 'zaarbox_price_key', 'estjt_41670cdf0270a874b68a0a95a235f9a1', 'شناسه ردیف تابلو', 1, 1, 'key یا id' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'zaarbox_price_key' LIMIT 1); +UPDATE `settings` SET `value` = 'estjt_41670cdf0270a874b68a0a95a235f9a1' WHERE `key` = 'zaarbox_price_key'; + +COMMIT; + +-- بعد: GET /api/price/last