user = $user; } public function handle(GmailParserService $parserService) { // 1. 檢查使用者憑證 if (!$this->user->google_refresh_token) { Log::warning("User ID {$this->user->id} 試圖執行抓信任務,但缺乏 refresh_token。"); return; } try { // 2. 初始化 Google Client 並刷新 Token $client = new GoogleClient(); $client->setClientId(config('services.google.client_id')); $client->setClientSecret(config('services.google.client_secret')); $client->refreshToken($this->user->google_refresh_token); $gmailService = new Gmail($client); // 3. 搜尋帶有專屬標籤且「未讀」的信件 $searchQuery = 'label:FinBuddy-Card is:unread'; $response = $gmailService->users_messages->listUsersMessages('me', [ 'q' => $searchQuery, 'maxResults' => 20 // 每次最多處理 20 筆,防爆 ]); $messages = $response->getMessages(); if (empty($messages)) { Log::info("User ID {$this->user->id}: 沒發現符合條件的未讀刷卡信件。"); return; } // 4. 逐封拆解信件 foreach ($messages as $msgItem) { $msgId = $msgItem->getId(); $message = $gmailService->users_messages->get('me', $msgId, ['format' => 'full']); $payload = $message->getPayload(); // 抓取主旨 $subject = ''; foreach ($payload->getHeaders() as $header) { if ($header->getName() === 'Subject') { $subject = $header->getValue(); break; } } // 遞迴拿到內文 $body = $this->getDecodedBody($payload); // 5. 丟給 Parser 進行 Regex 解析 $parsedData = $parserService->parse($subject, $body); if ($parsedData) { // 🟢 智慧分類:根據商家名稱反查資料庫裡的自動分類規則 // 假設你的規則表有關鍵字比對 (例如: keyword = 'PCHOME', category_id = 3) $matchedRule = CategoryRule::where('user_id', $this->user->id) ->get() ->first(function ($rule) use ($parsedData) { return Str::contains(strtoupper($parsedData['seller_name']), strtoupper($rule->keyword)); }); $categoryId = $matchedRule ? $matchedRule->category_id : null; // 6. 寫入消費紀錄(我們已經補上了 $fillable,絕對暢行無阻!) Expense::create([ 'user_id' => $this->user->id, 'amount' => $parsedData['amount'], 'item_name' => $parsedData['item_name'], 'seller_name' => $parsedData['seller_name'], 'date' => $parsedData['date'], 'category_id' => $categoryId, 'note' => 'Gmail背景自動記帳', ]); Log::info("User ID {$this->user->id}: 成功自動記帳一筆金額 {$parsedData['amount']},商家: {$parsedData['seller_name']}"); } // 7. 🟢 防重複防線:記帳成功後,立刻把信件在 Gmail 標記為「已讀」 $mods = new ModifyMessageRequest(); $mods->setRemoveLabelIds(['UNREAD']); // 移除未讀標籤 = 標記已讀 $gmailService->users_messages->modify('me', $msgId, $mods); } } catch (\Exception $e) { Log::error("User ID {$this->user->id} Gmail 自動抓帳 Job 發生崩潰: " . $e->getMessage()); } } /** * 遞迴解析內文的輔助方法 */ private function getDecodedBody($part) { $body = $part->getBody(); if ($body && $body->getData()) { $sanitizedData = str_replace(['-', '_'], ['+', '/'], $body->getData()); return base64_decode($sanitizedData); } $parts = $part->getParts(); if ($parts) { foreach ($parts as $subPart) { $result = $this->getDecodedBody($subPart); if ($result) return $result; } } return ''; } }