diff --git a/app/Services/ZaarboxPriceService.php b/app/Services/ZaarboxPriceService.php index 350a02c..2e04f01 100644 --- a/app/Services/ZaarboxPriceService.php +++ b/app/Services/ZaarboxPriceService.php @@ -2,12 +2,26 @@ namespace App\Services; +use Carbon\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ZaarboxPriceService { + public const CACHE_KEY = 'zaarbox_gold_prices'; + + private const CACHE_TTL_MINUTES = 35; + + private const DEFAULT_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], + ]; + public function __construct(private ConfigService $config) { } @@ -36,6 +50,46 @@ class ZaarboxPriceService )); } + /** UUID سری زمانی نمودار در API تاریخچه Zaarbox */ + public function historyId(): string + { + $id = trim((string) $this->config->getString( + 'zaarbox_gold_history_id', + (string) config('price_providers.zaarbox_gold_history_id', self::DEFAULT_HISTORY_ID) + )); + + return $id !== '' ? $id : self::DEFAULT_HISTORY_ID; + } + + /** + * @return array{DAILY: array, WEEKLY: array, MONTHLY: array} + */ + public function getAllPrices(): array + { + $cached = Cache::get(self::CACHE_KEY); + + if (is_array($cached) && isset($cached['prices'])) { + return $cached['prices']; + } + + return $this->refreshCache(); + } + + /** + * @return array{DAILY: array, WEEKLY: array, MONTHLY: array} + */ + public function refreshCache(): array + { + $prices = $this->fetchChartPrices(); + + Cache::put(self::CACHE_KEY, [ + 'prices' => $prices, + 'refreshed_at' => now()->toIso8601String(), + ], now()->addMinutes(self::CACHE_TTL_MINUTES)); + + return $prices; + } + /** * @return array{price: int, order_price: int, raw: array|null} */ @@ -79,6 +133,133 @@ class ZaarboxPriceService } } + /** + * @return array{DAILY: array, WEEKLY: array, MONTHLY: array} + */ + private function fetchChartPrices(): 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 + { + return sprintf( + 'https://price.zaarbox.ir/api/history/%s?start=%s&end=%s&limit=1000000000', + $this->historyId(), + $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; + } + + /** + * @param array> $history + * @return array + */ + 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' => $lastPoint['timestamp'], + ]; + } + + return $result; + } + + private function parseTimestampMs(string $time): ?int + { + try { + return $this->toMillis(Carbon::parse($time)->utc()); + } catch (\Throwable) { + return null; + } + } + /** * @param Collection> $collection * @return array|null diff --git a/config/price_providers.php b/config/price_providers.php index abdfc24..60acc5d 100644 --- a/config/price_providers.php +++ b/config/price_providers.php @@ -16,6 +16,8 @@ return [ 'etehadie_api_url' => 'http://109.122.246.48:4949/api/free', 'zaarbox_api_url' => 'https://price.zaarbox.ir/api/free', 'zaarbox_price_key' => '', + /** UUID سری زمانی نمودار — GET /api/history/{id} */ + 'zaarbox_gold_history_id' => 'ca3800a0-1f57-3528-b963-178f9168e04f', 'price_method' => '', 'register_jibit_verify' => false, 'register_referral_sms_to_referrer' => false,