feat: 前端分離+功能api化

1. 新增資產管理CRUD、googleOAuth登入
2. 更改原記帳controller
This commit is contained in:
2026-06-25 14:19:07 +08:00
parent 7bb931c97b
commit 85972f950c
70 changed files with 4754 additions and 1520 deletions

View File

@@ -0,0 +1,140 @@
<?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 '';
}
}