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,180 @@
<?php
namespace App\Services;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class GmailParserService
{
protected array $parserMatrix = [
[
'keywords' => ['國泰世華銀行消費彙整通知'],
'method' => 'parseCathaySummary'
],
[
'keywords' => ['永豐銀行信用卡消費通知'],
'method' => 'parseSinoPacNotification'
],
[
'keywords' => ['電子發票', '載具', '發票開立通知'],
'method' => 'parseElectronicInvoice'
],
[
'keywords' => ['消費通知', '刷卡', '卡片消費'],
'method' => 'parseCreditCardNotification'
],
];
public function parse(string $subject, string $body): ?array
{
// 🔄 遍歷矩陣,尋找第一個命中的規則
foreach ($this->parserMatrix as $rule) {
if (Str::contains($subject, $rule['keywords'])) {
$method = $rule['method'];
// 動態呼叫對應的方法 (例如 $this->parseCathaySummary($body) )
return $this->{$method}($body);
}
}
Log::info("【Parser】忽略信件主旨未命中任何對照規則: [{$subject}]");
return null;
}
/**
* 🟢 國泰世華消費彙整信件專屬解析
*/
private function parseCathaySummary(string $body): ?array
{
$cleanBody = preg_replace('/\s+/', ' ', $body);
if (preg_match('/(\d{4}\/\d{2}\/\d{2})/u', $cleanBody, $dateMatches)) {
$date = Carbon::createFromFormat('Y/m/d', $dateMatches[1])->format('Y-m-d');
} else {
$date = now()->format('Y-m-d');
}
$pattern = '/NT\$\s*([0-9,]+)\s*<\/td>\s*<td[^>]*>\s*([^<\s&]+)\s*<\/td>/u';
if (!preg_match_all($pattern, $cleanBody, $matches, PREG_SET_ORDER)) {
Log::error("【Parser】國泰信件未命中消費明細。");
return null;
}
$results = [];
foreach ($matches as $match) {
$sellerName = $this->convertFullWidthToHalfWidth(trim($match[2]));
$results[] = [
'amount' => floatval(str_replace(',', '', $match[1])),
'item_name' => "信用卡消費: {$sellerName}",
'seller_name' => $sellerName,
'date' => $date,
'category_id' => null,
];
}
return $results;
}
/**
* 永豐銀行信用卡消費通知(單筆制)
*/
private function parseSinoPacNotification(string $body): ?array
{
$cleanBody = preg_replace('/\s+/', ' ', $body);
$amount = 0;
$sellerName = '永豐刷卡消費';
if (preg_match('/(?:新臺幣|TWD)\s*([0-9,]+)/u', $cleanBody, $amountMatches)) {
$amount = floatval(str_replace(',', '', $amountMatches[1]));
}
if (preg_match('/特約商店[:]\s*([^<\s]+)/u', $cleanBody, $merchantMatches)) {
$sellerName = trim($merchantMatches[1]);
}
if ($amount === 0) return null;
if (preg_match('/(\d{4}\/\d{2}\/\d{2})/u', $cleanBody, $dateMatches)) {
$date = Carbon::createFromFormat('Y/m/d', $dateMatches[1])->format('Y-m-d');
} else {
$date = now()->format('Y-m-d');
}
$sellerName = $this->convertFullWidthToHalfWidth($sellerName);
return [
'amount' => $amount,
'item_name' => "信用卡消費: {$sellerName}",
'seller_name' => $sellerName,
'date' => $date,
'category_id' => null,
];
}
/**
* 解析電子發票信件
*/
private function parseElectronicInvoice(string $body): ?array
{
$amount = 0;
$sellerName = '未知商家';
if (preg_match('/(總計|金額|總金額)[:]\s*?\$?([0-9,]+)/u', $body, $matches)) {
$amount = floatval(str_replace(',', '', $matches[2]));
}
if (preg_match('/(賣方|開立店家|商店名稱)[:]\s*?([^\n<\s]+)/u', $body, $matches)) {
$sellerName = trim($matches[2]);
}
if ($amount === 0) return null;
return [
'amount' => $amount,
'item_name' => '電子發票消費',
'seller_name' => $sellerName,
'date' => now()->format('Y-m-d'),
'category_id' => null,
];
}
/**
* 解析信用卡消費通知(泛用型備援)
*/
private function parseCreditCardNotification(string $body): ?array
{
$amount = 0;
$sellerName = '一般刷卡消費';
if (preg_match('/(NT\$|元|TWD)\s*?([0-9,]+)/u', $body, $matches)) {
$amount = floatval(str_replace(',', '', $matches[2]));
} elseif (preg_match('/([0-9,]+)\s*?(元)/u', $body, $matches)) {
$amount = floatval(str_replace(',', '', $matches[1]));
}
if (preg_match('/於\s*?([^\n\](]+?)\s*?消費/u', $body, $matches)) {
$sellerName = trim($matches[1]);
} elseif (preg_match('/(特約商店|消費商店|商店)[:]\s*?([^\n<\s]+)/u', $body, $matches)) {
$sellerName = trim($matches[2]);
}
if ($amount === 0) return null;
return [
'amount' => $amount,
'item_name' => "刷卡消費: {$sellerName}",
'seller_name' => $sellerName,
'date' => now()->format('Y-m-d'),
'category_id' => null,
];
}
private function convertFullWidthToHalfWidth(string $str): string
{
$str = str_replace('&nbsp;', '', $str);
return mb_convert_kana($str, 'asKV', 'UTF-8');
}
}