|null $lastPayload */ public function broadcastAfterPriceUpdate(?array $lastPayload = null, bool $force = false): void { $stream = app(PriceStreamService::class); if (! $stream->isEnabled()) { return; } if ($lastPayload === null) { $cache = app(LivePriceCacheService::class); $lastPayload = $cache->get() ?? $cache->buildFromDatabase(); } if (is_array($lastPayload) && $lastPayload !== []) { $lastPayload = app(ConfigService::class)->applyHomePriceDisplayState($lastPayload); $lastPayload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($lastPayload); } if ($force) { if (is_array($lastPayload) && $lastPayload !== []) { $stream->publish('last', $lastPayload); } $listPayload = $this->buildProductListPayload(); if ($listPayload !== null) { $stream->publish('list', $listPayload); } return; } $lastChanged = is_array($lastPayload) && $lastPayload !== [] && $this->rememberHashIfChanged($this->cacheKey(self::LAST_HASH_KEY), self::lastPriceIdentity($lastPayload)); if ($lastChanged && is_array($lastPayload)) { $stream->publish('last', $lastPayload); $this->publishListIfChanged(true); } else { $this->publishListIfChanged(); } } /** * @param array $payload * @return array */ public static function lastPriceIdentity(array $payload): array { $goldOunce = is_array($payload['gold_ounce'] ?? null) ? $payload['gold_ounce'] : null; return [ 'sell' => (int) ($payload['sell'] ?? 0), 'buy' => (int) ($payload['buy'] ?? 0), 'price_last' => (int) ($payload['price_last'] ?? 0), 'price' => (int) ($payload['price'] ?? 0), 'closed' => (bool) ($payload['closed'] ?? false), 'status' => (string) ($payload['status'] ?? ''), 'main_product' => (int) ($payload['main_product'] ?? 0), 'montzar' => (int) ($payload['montzar'] ?? 0), 'zaarbox' => (int) ($payload['zaarbox'] ?? 0), 'copper_world' => (int) ($payload['copper_world'] ?? 0), 'dollar_free' => (int) ($payload['dollar_free'] ?? 0), 'copper_ton' => (string) ($payload['copper_ton'] ?? '0'), 'copper_kilo' => (string) ($payload['copper_kilo'] ?? '0'), 'gold_ounce_price' => round((float) ($goldOunce['price'] ?? 0), 2), ]; } public function broadcastLastIfChanged(?array $lastPayload): void { $stream = app(PriceStreamService::class); if (! $stream->isEnabled()) { return; } if (! is_array($lastPayload) || $lastPayload === []) { return; } $lastPayload = app(ConfigService::class)->applyHomePriceDisplayState($lastPayload); $lastPayload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($lastPayload); // no_quote صفر را روی SSE پخش نکن — کلاینت مظنهٔ معتبر را پاک می‌کرد // در حالی که /api/price/last هنوز قیمت داشت. if (! app(ConfigService::class)->payloadHasDisplayablePrice($lastPayload) && (string) ($lastPayload['status'] ?? '') === 'no_quote' ) { return; } if (! $this->rememberHashIfChanged($this->cacheKey(self::LAST_HASH_KEY), self::lastPriceIdentity($lastPayload))) { return; } $stream->publish('last', $lastPayload); $this->publishListIfChanged(true); } public function buildListPayload(bool $allowCached = false): ?array { if ($allowCached) { $cached = Cache::get($this->cacheKey(self::LIST_SNAPSHOT_KEY)); if ( is_array($cached) && is_array($cached['body'] ?? null) && isset($cached['stored_at']) && now()->diffInSeconds($cached['stored_at']) <= self::LIST_SNAPSHOT_SECONDS ) { return $cached['body']; } } $body = $this->buildProductListPayload(); if ($body !== null && $body !== []) { Cache::put($this->cacheKey(self::LIST_SNAPSHOT_KEY), [ 'body' => $body, 'stored_at' => now(), ], now()->addSeconds(self::LIST_SNAPSHOT_SECONDS + 5)); } return $body; } public function broadcastListIfChanged(bool $force = false): bool { $stream = app(PriceStreamService::class); if (! $stream->isEnabled()) { return false; } return $this->publishListIfChanged($force); } private function publishListIfChanged(bool $force = false): bool { $listPayload = $this->buildProductListPayload(); if ($listPayload === null || $listPayload === []) { return false; } if (! $force && ! $this->rememberHashIfChanged($this->cacheKey(self::LIST_HASH_KEY), self::listIdentity($listPayload))) { return false; } if ($force) { $this->rememberHashIfChanged($this->cacheKey(self::LIST_HASH_KEY), self::listIdentity($listPayload)); } Cache::forget($this->cacheKey(self::LIST_SNAPSHOT_KEY)); app(PriceStreamService::class)->publish('list', $listPayload); return true; } /** * @param array> $listPayload * @return array> */ public static function listIdentity(array $listPayload): array { $identity = []; foreach ($listPayload as $product) { if ($product instanceof \Illuminate\Database\Eloquent\Model) { $product = $product->toArray(); } if (! is_array($product)) { continue; } $file = $product['file'] ?? null; if ($file instanceof \Illuminate\Database\Eloquent\Model) { $file = $file->toArray(); } if (! is_array($file)) { $file = []; } $identity[] = [ 'id' => (int) ($product['id'] ?? 0), 'file_id' => (int) ($product['file_id'] ?? ($file['id'] ?? 0)), 'price' => (int) ($product['price'] ?? 0), 'price_profit' => (int) ($product['price_profit'] ?? 0), 'amount' => (float) ($product['amount'] ?? 0), 'app_price' => (bool) ($product['app_price'] ?? false), 'trade_by_quantity' => (bool) ($product['trade_by_quantity'] ?? false), 'direct_price' => (int) ($product['direct_price'] ?? 0), 'reading_method' => (string) ($product['reading_method'] ?? ''), 'price_offset_percent' => (float) ($product['price_offset_percent'] ?? 0), 'show' => (bool) ($product['show'] ?? true), 'trade_open' => (bool) ($product['trade_open'] ?? $product['show'] ?? true), 'active' => (bool) ($product['active'] ?? true), 'reference_provider_active' => (bool) ($product['reference_provider_active'] ?? false), ]; } return $identity; } /** * @param array|array $payload */ private function cacheKey(string $base): string { return $base.':'.app(PriceStreamService::class)->tenantScope(); } private function rememberHashIfChanged(string $cacheKey, array $payload): bool { $hash = md5(json_encode($payload, JSON_UNESCAPED_UNICODE)); if (Cache::get($cacheKey) === $hash) { return false; } Cache::put($cacheKey, $hash, now()->addHour()); return true; } /** * @return array|null */ private function buildProductListPayload(): ?array { if (app(ConfigService::class)->isStore()) { return $this->normalizeListPayload( app(ProductController::class)->customerStoreProductListPayload() ); } $request = Request::create('/customer/product/list', 'GET'); app()->instance('request', $request); $response = app(ProductController::class)->listProduct(); $decoded = json_decode($response->getContent(), true); return is_array($decoded) ? $decoded : null; } /** * مدل/کالکشن Eloquent را به آرایهٔ خالص تبدیل می‌کند تا listIdentity و SSE * همیشه file_id و URL تصویر را ببینند (قبلاً is_array روی Model شکست می‌خورد). * * @param mixed $payload * @return array|null */ private function normalizeListPayload(mixed $payload): ?array { if ($payload === null) { return null; } if ($payload === []) { return []; } $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE); if ($encoded === false) { return null; } $decoded = json_decode($encoded, true); return is_array($decoded) ? $decoded : null; } }