symbol = $symbol; $this->type = $type; $this->defaultName = $defaultName; } public function handle(): void { try { Log::info("🔄 開始從持倉清單發動爬蟲: [{$this->symbol}] {$this->defaultName}"); $isRealPrice = true; $latestPrice = 0; $latestPriceDate = null; $name = $this->defaultName; $changeAmount = 0; $changeRate = 0; $error = null; // 股票去爬證交所 if ($this->type === 'stock') { Log::info("開始抓取{$this->type}:{$this->symbol}價格"); $response = Http::timeout(10)->get("https://mis.twse.com.tw/stock/api/getStockInfo.jsp?ex_ch=tse_{$this->symbol}.tw"); if ($response->successful()) { $json = $response->json(); $info = $json['msgArray'][0] ?? null; if (isset($info['^']) && !Carbon::parse($info['^'])->isToday()) { $isRealPrice = false; } if ($info) { $latestPrice = floatval($info['z'] ?? $info['o'] ?? 0); $latestPriceDate = isset($info['d']) ? Carbon::parse($info['d'])->format('Y-m-d') : now()->format('Y-m-d'); $name = $info['n'] ?? $this->defaultName; // 忘了備份時用的 // $latestPriceDate = isset($info['d']) ? Carbon::parse($info['d'], 'Asia/Taipei')->subDay()->format('Y-m-d') : now()->subDay()->format('Y-m-d'); if (isset($info['y']) && $latestPrice > 0) { $changeAmount = floatval($latestPrice - $info['y']); $changeRate = ($changeAmount / $info['y']) * 100; } } } } elseif ($this->type === 'fund') { Log::info("開始抓取{$this->type}:{$this->symbol}價格"); // 去鉅亨網抓 $response = Http::timeout(10) ->withHeaders([ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer' => 'https://www.anuefund.com/', 'Accept' => 'application/json, text/plain, */*', ]) ->get("https://www.anuefund.com/anuefundApi/FundDetail/FundInfo", [ 'fundDetailEnum' => 'FundINFO', 'FundID' => $this->symbol ]); if ($response->successful()) { $json = $response->json(); $headerInfo = $json['data']['hearder'] ?? null; if (isset($headerInfo['navDate']) && !Carbon::parse($headerInfo['navDate'])->isToday()) { //不是當天資料不更新 $isRealPrice = false; } if ($headerInfo) { $latestPriceDate = isset($headerInfo['navDate']) ? Carbon::parse($headerInfo['navDate'], 'Asia/Taipei')->format('Y-m-d') : now()->format('Y-m-d'); // 忘了備份時用的 // $latestPriceDate = isset($headerInfo['navDate']) ? Carbon::parse($headerInfo['navDate'], 'Asia/Taipei')->subDay()->format('Y-m-d') : now()->subDay()->format('Y-m-d'); $latestPrice = floatval($headerInfo['nav'] ?? 0); if ($latestPriceDate !== now()->format('Y-m-d')) { //不是當天資料不更新 $isRealPrice = false; $latestPrice = 0; } $name = $headerInfo['fundName'] ?? $this->defaultName; $changeAmount = floatval($headerInfo['upDown'] ?? 0); $changeRate = floatval($headerInfo['upDownRate'] ?? 0); } } Log::info("抓取{$this->type}:{$this->symbol}資料結束"); } // 爬蟲成功拿到有效股價,幫新表補資料 if ($latestPrice > 0 && $isRealPrice) { Log::info("開始寫入Security和SecurityPriceHistories表,標的: [{$this->symbol}],價格: {$latestPrice}---"); $security = Security::updateOrCreate( ['code' => $this->symbol], [ 'type' => $this->type, 'name' => $name, 'latest_price' => $latestPrice, 'latest_price_at' => $latestPriceDate, 'change_amount' => $changeAmount, 'change_rate' => $changeRate, 'source' => 'twse', ] ); SecurityPriceHistories::updateOrCreate( [ 'security_id' => $security->id, 'price_date' => $latestPriceDate, // 記錄今天的日期 ], [ 'price' => $latestPrice, 'change_amount' => $changeAmount, 'change_rate' => $changeRate, 'created_at' => now(), 'updated_at' => now(), ] ); Log::info("✅ Security & SecurityPriceHistories 標的 [{$this->symbol}] 補齊並更新成功: {$latestPrice}"); } elseif (!$isRealPrice) { Log::info("ℹ️ 標的 [{$this->symbol}] 抓到的價格是非即時的,不更新價格資訊。"); } else { Log::warning("⚠️ 標的 [{$this->symbol}] 未能從證交所抓到有效價格,{$error}"); } } catch (\Exception $e) { Log::error("❌ 更新標的 [{$this->symbol}] 價格時發生異常: " . $e->getMessage()); $this->release(60); } } }