diff --git a/Service/BudgetService.cs b/Service/BudgetService.cs index 39daee7..a7c99bb 100644 --- a/Service/BudgetService.cs +++ b/Service/BudgetService.cs @@ -7,6 +7,11 @@ public interface IBudgetService Task GetStatisticsAsync(long id, DateTime? referenceDate = null); Task ArchiveBudgetsAsync(int year, int month); + + /// + /// 获取指定分类的统计信息(月度和年度) + /// + Task GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null); } public class BudgetService( @@ -64,6 +69,92 @@ public class BudgetService( return BudgetResult.FromEntity(budget, currentAmount, referenceDate); } + public async Task GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null) + { + var budgets = (await budgetRepository.GetAllAsync()).ToList(); + var refDate = referenceDate ?? DateTime.Now; + + var result = new BudgetCategoryStats(); + + // 获取月度统计 + result.Month = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Month, refDate); + + // 获取年度统计 + result.Year = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Year, refDate); + + return result; + } + + private async Task CalculateCategoryStatsAsync( + List budgets, + BudgetCategory category, + BudgetPeriodType statType, + DateTime referenceDate) + { + var result = new BudgetStatsDto + { + PeriodType = statType, + Rate = 0, + Current = 0, + Limit = 0, + Count = 0 + }; + + // 获取当前分类下所有未停止的预算 + var relevant = budgets + .Where(b => b.Category == category && !b.IsStopped) + .ToList(); + + if (relevant.Count == 0) + { + return result; + } + + result.Count = relevant.Count; + decimal totalCurrent = 0; + decimal totalLimit = 0; + + foreach (var budget in relevant) + { + // 限额折算 + var itemLimit = budget.Limit; + if (statType == BudgetPeriodType.Month && budget.Type == BudgetPeriodType.Year) + { + // 月度视图下,年度预算不参与限额计算 + itemLimit = 0; + } + else if (statType == BudgetPeriodType.Year && budget.Type == BudgetPeriodType.Month) + { + // 年度视图下,月度预算折算为年度 + itemLimit = budget.Limit * 12; + } + totalLimit += itemLimit; + + // 当前值累加 + var currentAmount = await CalculateCurrentAmountAsync(budget, referenceDate); + if (budget.Type == statType) + { + totalCurrent += currentAmount; + } + else + { + // 如果周期不匹配 + if (statType == BudgetPeriodType.Year && budget.Type == BudgetPeriodType.Month) + { + // 在年度视图下,月度预算计入其当前值(作为对年度目前的贡献) + totalCurrent += currentAmount; + } + // 月度视图下,年度预算的 current 不计入 + } + } + + result.Limit = totalLimit; + result.Current = totalCurrent; + result.Rate = totalLimit > 0 ? (totalCurrent / totalLimit) * 100 : 0; + + return result; + } + public async Task ArchiveBudgetsAsync(int year, int month) { var referenceDate = new DateTime(year, month, 1); @@ -393,18 +484,22 @@ public class BudgetService( Category = BudgetCategory.Savings, Type = periodType, Limit = incomeLimitAtPeriod - expenseLimitAtPeriod, - StartDate = periodType == BudgetPeriodType.Year ? new DateTime(date.Year, 1, 1) : new DateTime(date.Year, date.Month, 1), + StartDate = periodType == BudgetPeriodType.Year + ? new DateTime(date.Year, 1, 1) + : new DateTime(date.Year, date.Month, 1), SelectedCategories = savingsCategories }; // 计算实际发生的 收入 - 支出 - var incomeHelper = new BudgetRecord { Category = BudgetCategory.Income, Type = periodType, StartDate = virtualBudget.StartDate, SelectedCategories = savingsCategories }; - var expenseHelper = new BudgetRecord { Category = BudgetCategory.Expense, Type = periodType, StartDate = virtualBudget.StartDate, SelectedCategories = savingsCategories }; + var current = await CalculateCurrentAmountAsync(new BudgetRecord + { + Category = virtualBudget.Category, + Type = virtualBudget.Type, + SelectedCategories = virtualBudget.SelectedCategories, + StartDate = virtualBudget.StartDate, + }, date); - var actualIncome = await CalculateCurrentAmountAsync(incomeHelper, date); - var actualExpense = await CalculateCurrentAmountAsync(expenseHelper, date); - - return BudgetResult.FromEntity(virtualBudget, actualIncome - actualExpense, date, description.ToString()); + return BudgetResult.FromEntity(virtualBudget, current, date, description.ToString()); } } @@ -459,3 +554,49 @@ public record BudgetResult }; } } +/// +/// 预算统计结果 DTO +/// +public class BudgetStatsDto +{ + /// + /// 统计周期类型(Month/Year) + /// + public BudgetPeriodType PeriodType { get; set; } + + /// + /// 使用率百分比(0-100) + /// + public decimal Rate { get; set; } + + /// + /// 实际金额 + /// + public decimal Current { get; set; } + + /// + /// 目标/限额金额 + /// + public decimal Limit { get; set; } + + /// + /// 预算项数量 + /// + public int Count { get; set; } +} + +/// +/// 分类统计结果 +/// +public class BudgetCategoryStats +{ + /// + /// 月度统计 + /// + public BudgetStatsDto Month { get; set; } = new(); + + /// + /// 年度统计 + /// + public BudgetStatsDto Year { get; set; } = new(); +} \ No newline at end of file diff --git a/Service/EmailServices/EmailHandleService.cs b/Service/EmailServices/EmailHandleService.cs index d1a0872..d2a2459 100644 --- a/Service/EmailServices/EmailHandleService.cs +++ b/Service/EmailServices/EmailHandleService.cs @@ -82,8 +82,6 @@ public class EmailHandleService( { logger.LogInformation("处理交易记录: 卡号 {Card}, 交易原因 {Reason}, 金额 {Amount}, 余额 {Balance}, 类型 {Type}", card, reason, amount, balance, type); - - var record = await SaveTransactionRecordAsync( card, reason, @@ -104,7 +102,9 @@ public class EmailHandleService( records.Add(record); } - _ = await AnalyzeClassifyAsync(records.ToArray()); + // var analysisResult = await AnalyzeClassifyAsync(records.ToArray()); + // TODO 不应该直接保存 应该保存在备用字段上,前端确认后再更新到正式字段 + return allSuccess; } diff --git a/Service/Jobs/BudgetArchiveJob.cs b/Service/Jobs/BudgetArchiveJob.cs index 7631496..e93a195 100644 --- a/Service/Jobs/BudgetArchiveJob.cs +++ b/Service/Jobs/BudgetArchiveJob.cs @@ -17,7 +17,7 @@ public class BudgetArchiveJob( logger.LogInformation("开始执行预算归档任务"); // 每个月1号执行,归档上个月的数据 - var targetDate = DateTime.Now.AddMonths(0); // TODO 调试期间使用 + var targetDate = DateTime.Now.AddMonths(-1); var year = targetDate.Year; var month = targetDate.Month; diff --git a/Web/src/api/budget.js b/Web/src/api/budget.js index f06e4b8..ffb15b3 100644 --- a/Web/src/api/budget.js +++ b/Web/src/api/budget.js @@ -72,6 +72,19 @@ export function toggleStopBudget(id) { }) } +/** + * 获取分类统计信息(月度和年度) + * @param {string} category 分类 (Expense/Income/Savings) + * @param {string} referenceDate 参考日期 (可选) + */ +export function getCategoryStats(category, referenceDate) { + return request({ + url: '/Budget/GetCategoryStats', + method: 'get', + params: { category, referenceDate } + }) +} + /** * 归档预算 * @param {number} year 年份 diff --git a/Web/src/components/Budget/BudgetCard.vue b/Web/src/components/Budget/BudgetCard.vue index 5c36e91..3be3ca0 100644 --- a/Web/src/components/Budget/BudgetCard.vue +++ b/Web/src/components/Budget/BudgetCard.vue @@ -1,11 +1,10 @@