copy(); $latestYear = $latestMonth->year; // 1. 本月基礎統計 (用一個查詢同時撈出「總金額」與「總筆數」) $monthStats = Expense::where('user_id', $userId) ->whereYear('date', $latestYear) ->whereMonth('date', $latestMonth) ->selectRaw('SUM(amount) as total_amount, COUNT(id) as total_count') ->first(); if (!$monthStats || $monthStats->total_amount == 0) { // 改用 first() 確保能抓到單筆資料 $latestExpense = Expense::where('user_id', $userId)->latest('date')->first(); if ($latestExpense) { $targetDate = Carbon::parse($latestExpense->date); $latestYear = $targetDate->year; $latestMonth = $targetDate->month; // 重新撈取該月份的統計資料 $monthStats = Expense::where('user_id', $userId) ->whereYear('date', $latestYear) ->whereMonth('date', $latestMonth) ->selectRaw('SUM(amount) as total_amount, COUNT(id) as total_count') ->first(); } } $monthlyTotal = (float) ($monthStats->total_amount ?? 0); $monthlyCount = (int) ($monthStats->total_count ?? 0); // 2. 本月分類統計(圓餅圖)- 保持集合操作即可 $categoryStats = Expense::with('category') ->where('user_id', $userId) ->whereYear('date', $latestYear) ->whereMonth('date', $latestMonth) ->get() ->groupBy(fn($e) => $e->category?->name ?? '未分類') ->map(fn($group, $name) => [ 'name' => $name, 'total' => (float) $group->sum('amount'), 'color' => $group->first()->category?->color ?? '#6B7280', ]) ->values(); // 3. 近 6 個月趨勢優化:一次性撈出半年內的所有資料,避免 6 次 SQL 查詢 $sixMonthsAgo = $now->copy()->subMonths(5)->startOfMonth(); $endOfCurrentMonth = $now->copy()->endOfMonth(); // 建立近 6 個月的初始月份清單(例如 ['2026/01' => 0.0, '2026/02' => 0.0 ... ]) $monthlyTrendData = collect(range(5, 0))->mapWithKeys(function ($i) use ($now) { $monthStr = $now->copy()->subMonths($i)->format('Y/m'); return [$monthStr => 0.0]; })->toArray(); // ➔ (A) 撈取一般消費 (未開啟分攤) $isPostgres = DB::connection()->getDriverName() === 'pgsql'; $dateGroupRaw = $isPostgres ? "to_char(date, 'YYYY/MM')" : "DATE_FORMAT(date, '%Y/%m')"; $normalExpenses = Expense::where('user_id', $userId) ->where('is_amortized', false) ->where('date', '>=', $sixMonthsAgo) ->selectRaw("{$dateGroupRaw} as month, SUM(amount) as total") ->groupBy('month') ->get(); foreach ($normalExpenses as $expense) { if (array_key_exists($expense->month, $monthlyTrendData)) { $monthlyTrendData[$expense->month] += (float) $expense->total; } } // ➔ (B) 智慧分攤消費 (處理跨月、跨年、半年繳大魔王 🎯) // 撈出所有歸屬期間有交集到這 6 個月範圍內的分攤費用 $amortizedExpenses = Expense::where('user_id', $userId) ->where('is_amortized', true) ->where(function ($query) use ($sixMonthsAgo, $endOfCurrentMonth) { $query->where('period_start', '<=', $endOfCurrentMonth) ->where('period_end', '>=', $sixMonthsAgo); }) ->get(); foreach ($amortizedExpenses as $expense) { $start = Carbon::parse($expense->period_start)->startOfMonth(); $end = Carbon::parse($expense->period_end)->endOfMonth(); // 計算這筆費用的總跨月份數 (至少為 1 個月) $totalMonths = max(1, $start->diffInMonths($end) + 1); // 算出每月平均攤提金額 $monthlyShare = (float) $expense->amount / $totalMonths; // 跑這 6 個月的迴圈,看有沒有落在這筆費用的歸屬區間內 foreach ($monthlyTrendData as $monthStr => &$currentTotal) { $currentMonthObj = Carbon::createFromFormat('Y/m', $monthStr)->startOfMonth(); // 如果圖表上的這個月份,剛好在費用的歸屬起訖之內,就把每月平均金額灌進去! if ($currentMonthObj->betweenIncluded($start, $end)) { $currentTotal += $monthlyShare; } } } // 轉回前端圖表要的格式 $monthlyTrend = collect($monthlyTrendData)->map(function ($total, $monthStr) { return [ 'month' => $monthStr, 'total' => round($total, 2), // 四捨五入到小數點兩位 ]; })->values(); return response()->json([ 'monthlyTotal' => $monthlyTotal, 'monthlyCount' => $monthlyCount, 'categoryStats' => $categoryStats, 'monthlyTrend' => $monthlyTrend, ]); } }