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,162 @@
<?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 Illuminate\Support\Str;
use App\Models\User;
use App\Models\Expense;
use App\Services\GmailParserService;
use Google\Client as GoogleClient;
use Google\Service\Gmail;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class FetchAndParseGmailInvoices implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function handle(GmailParserService $parserService)
{
Log::info("====== [DEBUG START] 開始執行 Gmail 抓信任務 ======");
if (!$this->user->google_refresh_token) {
Log::error("❌ 失敗User ID {$this->user->id} 缺乏 google_refresh_token");
return;
}
try {
Log::info("🔄 正在初始化 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);
$afterDate = Carbon::now()->subDays(3)->format('Y/m/d');
$searchQuery = "from:cathaybk.com.tw subject:消費彙整通知 after:{$afterDate}";
Log::info("🔍 正在向 Google API 發送搜尋 Query: [{$searchQuery}]");
$response = $gmailService->users_messages->listUsersMessages('me', [
'q' => $searchQuery,
'maxResults' => 10
]);
$messages = $response->getMessages();
if (empty($messages)) {
Log::warning("⚠️ 警告Google API 回傳空陣列!");
return;
}
Log::info("🎯 成功拿到信件清單!總共發現 " . count($messages) . " 封潛在信件,開始逐封處理...");
foreach ($messages as $index => $msgItem) {
$msgId = $msgItem->getId();
Log::info("--- [處理第 " . ($index + 1) . " 封信] ID: {$msgId} ---");
$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;
}
}
Log::info("📬 信件主旨: [{$subject}]");
$body = $this->getDecodedBody($payload);
Log::info("📄 內文解碼完成,準備丟入 Parser...");
// 1. 執行解析(現在國泰會回報陣列,其餘單筆通知我們手動把它包成陣列)
$parsedResult = $parserService->parse($subject, $body);
if (!$parsedResult) {
Log::error("❌ 失敗GmailParserService 無法解析此信件內容");
continue;
}
// 💡 智慧防呆:如果回傳的是單筆消費(一維陣列),我們把它包成二維陣列,這樣下面統一用 foreach 處理
$expensesList = isset($parsedResult['amount']) ? [$parsedResult] : $parsedResult;
Log::info("✨ Parser 解析成功!共發現 " . count($expensesList) . " 筆刷卡消費項目,開始遍歷寫入...");
// 🔄 遍歷這封信裡面的每一筆刷卡紀錄
foreach ($expensesList as $parsedData) {
// 🟢 2. 智慧資產帳戶對照
$matchedAccountRule = \App\Models\AccountRule::where('user_id', $this->user->id)
->get()
->first(function ($rule) use ($subject) {
return Str::contains($subject, $rule->keyword);
});
$accountId = $matchedAccountRule ? $matchedAccountRule->account_id : null;
// 🟢 3. 智慧消費分類對照
$matchedCategoryRule = \App\Models\CategoryRule::where('user_id', $this->user->id)
->get()
->first(function ($rule) use ($parsedData) {
return Str::contains(strtoupper($parsedData['seller_name']), strtoupper($rule->keyword));
});
$categoryId = $matchedCategoryRule ? $matchedCategoryRule->category_id : null;
// 4. 💾 嘗試寫入資料庫
$expense = Expense::firstOrCreate(
[
'user_id' => $this->user->id,
'amount' => $parsedData['amount'],
'seller_name' => $parsedData['seller_name'],
'date' => $parsedData['date'],
],
[
'item_name' => $parsedData['item_name'],
'account_id' => $accountId,
'category_id' => $categoryId,
'note' => 'Gmail自動同步(多筆完全體)',
]
);
if ($expense->wasRecentlyCreated) {
Log::info("✅ 成功!建立全新消費: [{$parsedData['seller_name']}] 額度: [{$parsedData['amount']}], ID: {$expense->id}");
} else {
Log::info(" 提示:[{$parsedData['seller_name']}] 額度: [{$parsedData['amount']}] 已存在,跳過。");
}
}
}
Log::info("====== [DEBUG END] Gmail 抓信任務順利結束 ======");
} catch (\Exception $e) {
Log::error("💥 崩潰handle 內發生致命錯誤: " . $e->getMessage());
}
}
private function getDecodedBody($part)
{
$body = $part->getBody();
if ($body && $body->getData()) {
return base64_decode(str_replace(['-', '_'], ['+', '/'], $body->getData()));
}
$parts = $part->getParts();
if ($parts) {
foreach ($parts as $subPart) {
$result = $this->getDecodedBody($subPart);
if ($result) return $result;
}
}
return '';
}
}