diff --git a/app/Console/Commands/RefreshZaarboxPrices.php b/app/Console/Commands/RefreshZaarboxPrices.php new file mode 100644 index 0000000..d76c541 --- /dev/null +++ b/app/Console/Commands/RefreshZaarboxPrices.php @@ -0,0 +1,27 @@ +refreshCache(); + + $this->info(sprintf( + 'Zaarbox prices cached: DAILY=%d, WEEKLY=%d, MONTHLY=%d', + count($prices['DAILY'] ?? []), + count($prices['WEEKLY'] ?? []), + count($prices['MONTHLY'] ?? []), + )); + + return self::SUCCESS; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 48004ee..1e080be 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -34,6 +34,11 @@ class Kernel extends ConsoleKernel } } })->everyMinute()->name('update_products_every_20s')->withoutOverlapping(); + + $schedule->command('prices:refresh-zaarbox') + ->everyThirtyMinutes() + ->name('refresh_zaarbox_prices') + ->withoutOverlapping(); } /** diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index 2a43637..09a0a8a 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -12,6 +12,7 @@ use App\Models\Withdraw; use App\Models\BankAccount; use App\Models\Price; use App\Http\Controllers\Admin\TahesabController; +use App\Services\ZaarboxPriceService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; @@ -44,15 +45,15 @@ class WalletController extends Controller return response()->json($wallet); } - public function priceData(Request $request) + public function priceData(Request $request, ZaarboxPriceService $zaarboxPriceService) { - return $response = Http::get('https://milli.gold/api/v1/public/milli-price/widget/external'); - $prise = Price::with('order'); - TableEngine::create($prise, $request) - ->initSearch(['price']) - ->select(['*']) - ->export($prise); - return response()->json($prise); + return response()->json([ + 'code' => 0, + 'message' => 'عملیات با موفقیت انجام شد.', + 'data' => [ + 'prices' => $zaarboxPriceService->getAllPrices(), + ], + ]); } public function transactionTable(Request $request) diff --git a/app/Services/ZaarboxPriceService.php b/app/Services/ZaarboxPriceService.php new file mode 100644 index 0000000..5500bc6 --- /dev/null +++ b/app/Services/ZaarboxPriceService.php @@ -0,0 +1,178 @@ + ['hours' => 24, 'intervalMs' => 900000], + 'WEEKLY' => ['hours' => 168, 'intervalMs' => 7200000], + 'MONTHLY' => ['hours' => 720, 'intervalMs' => 28800000], + ]; + + private function config(): ConfigService + { + return app(ConfigService::class); + } + + private function historyId(): string + { + return $this->config()->Config('zaarbox_gold_history_id') ?: self::HISTORY_ID; + } + + public function getAllPrices(): array + { + $cached = Cache::get(self::CACHE_KEY); + + if (is_array($cached) && isset($cached['prices'])) { + return $cached['prices']; + } + + return $this->refreshCache(); + } + + public function refreshCache(): array + { + $prices = $this->fetchAllPrices(); + + Cache::put(self::CACHE_KEY, [ + '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' => $lastPoint['timestamp'], + ]; + } + + return $result; + } + + private function parseTimestampMs(string $time): ?int + { + try { + return $this->toMillis(Carbon::parse($time)->utc()); + } catch (\Throwable) { + return null; + } + } +}