Files
finance_app/app/Jobs/FetchAndParseGmailInvoices.php
henry yo 85972f950c feat: 前端分離+功能api化
1. 新增資產管理CRUD、googleOAuth登入
2. 更改原記帳controller
2026-06-25 14:19:07 +08:00

141 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
use App\Models\Expense;
use App\Models\CategoryRule; // 💡 假設這是你存自動分類規則的 Model
use App\Services\GmailParserService;
use Google\Client as GoogleClient;
use Google\Service\Gmail;
use Google\Service\Gmail\ModifyMessageRequest;
use Illuminate\Support\Facades\Log;
class FetchAndParseGmailInvoices implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
/**
* 這裡我們可以在初始化時傳入指定 User或者在 handle 裡跑全部 User
*/
public function __construct(User $user)
{
$this->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 '';
}
}