using Service.Transaction; namespace Service.Budget; public interface IBudgetSavingsService { Task GetSavingsDtoAsync( BudgetPeriodType periodType, DateTime? referenceDate = null, IEnumerable? existingBudgets = null); } public class BudgetSavingsService( IBudgetRepository budgetRepository, IBudgetArchiveRepository budgetArchiveRepository, ITransactionStatisticsService transactionStatisticsService, IConfigService configService, IDateTimeProvider dateTimeProvider ) : IBudgetSavingsService { public async Task GetSavingsDtoAsync( BudgetPeriodType periodType, DateTime? referenceDate = null, IEnumerable? existingBudgets = null) { var budgets = existingBudgets; if (existingBudgets == null) { budgets = await budgetRepository.GetAllAsync(); } if (budgets == null) { throw new InvalidOperationException("No budgets found."); } budgets = budgets // 排序顺序 1.硬性预算 2.月度->年度 3.实际金额倒叙 .OrderBy(b => b.IsMandatoryExpense) .ThenBy(b => b.Type) .ThenByDescending(b => b.Limit); var year = referenceDate?.Year ?? dateTimeProvider.Now.Year; var month = referenceDate?.Month ?? dateTimeProvider.Now.Month; if (periodType == BudgetPeriodType.Month) { return await GetForMonthAsync(budgets, year, month); } else if (periodType == BudgetPeriodType.Year) { return await GetForYearAsync(budgets, year); } throw new NotSupportedException($"Period type {periodType} is not supported."); } private async Task GetForMonthAsync( IEnumerable budgets, int year, int month) { var transactionClassify = await transactionStatisticsService.GetAmountGroupByClassifyAsync( new DateTime(year, month, 1), new DateTime(year, month, 1).AddMonths(1) ); var monthlyIncomeItems = new List<(string name, decimal limit, decimal current, bool isMandatory)>(); var monthlyExpenseItems = new List<(string name, decimal limit, decimal current, bool isMandatory)>(); var monthlyBudgets = budgets .Where(b => b.Type == BudgetPeriodType.Month); foreach (var budget in monthlyBudgets) { var classifyList = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); decimal currentAmount = 0; var transactionType = budget.Category switch { BudgetCategory.Income => TransactionType.Income, BudgetCategory.Expense => TransactionType.Expense, _ => throw new NotSupportedException($"Budget category {budget.Category} is not supported.") }; foreach (var classify in classifyList) { // 获取分类+收入支出类型一致的金额 if (transactionClassify.TryGetValue((classify, transactionType), out var amount)) { currentAmount += amount; } } // 硬性预算 如果实际发生金额小于 应(总天数/实际天数)发生金额 // 直接取应发生金额(为了预算的准确性) if (budget.IsMandatoryExpense && currentAmount == 0) { currentAmount = budget.Limit / DateTime.DaysInMonth(year, month) * dateTimeProvider.Now.Day; } if (budget.Category == BudgetCategory.Income) { monthlyIncomeItems.Add(( name: budget.Name, limit: budget.Limit, current: currentAmount, isMandatory: budget.IsMandatoryExpense )); } else if (budget.Category == BudgetCategory.Expense) { monthlyExpenseItems.Add(( name: budget.Name, limit: budget.Limit, current: currentAmount, isMandatory: budget.IsMandatoryExpense )); } } var yearlyIncomeItems = new List<(string name, decimal limit, decimal current)>(); var yearlyExpenseItems = new List<(string name, decimal limit, decimal current)>(); var yearlyBudgets = budgets .Where(b => b.Type == BudgetPeriodType.Year); // 只需要考虑实际发生在本月的年度预算 因为他会影响到月度的结余情况 foreach (var budget in yearlyBudgets) { var classifyList = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); decimal currentAmount = 0; var transactionType = budget.Category switch { BudgetCategory.Income => TransactionType.Income, BudgetCategory.Expense => TransactionType.Expense, _ => throw new NotSupportedException($"Budget category {budget.Category} is not supported.") }; foreach (var classify in classifyList) { // 获取分类+收入支出类型一致的金额 if (transactionClassify.TryGetValue((classify, transactionType), out var amount)) { currentAmount += amount; } } if (currentAmount == 0) { continue; } if (budget.Category == BudgetCategory.Income) { yearlyIncomeItems.Add(( name: budget.Name, limit: budget.Limit, current: currentAmount )); } else if (budget.Category == BudgetCategory.Expense) { yearlyExpenseItems.Add(( name: budget.Name, limit: budget.Limit, current: currentAmount )); } } var description = new StringBuilder(); #region 构建月度收入支出明细表格 description.AppendLine("

月度预算收入明细

"); description.AppendLine(""" """); foreach (var item in monthlyIncomeItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 硬性收入
{item.name} {item.limit:N0} {(item.isMandatory ? "是" : "否")}
"); description.AppendLine($"""

收入合计: {monthlyIncomeItems.Sum(item => item.limit):N0}

"""); description.AppendLine("

月度预算支出明细

"); description.AppendLine(""" """); foreach (var item in monthlyExpenseItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 硬性支出
{item.name} {item.limit:N0} {(item.isMandatory ? "是" : "否")}
"); description.AppendLine($"""

支出合计: {monthlyExpenseItems.Sum(item => item.limit):N0}

"""); #endregion #region 构建发生在本月的年度预算收入支出明细表格 if (yearlyIncomeItems.Any()) { description.AppendLine("

年度收入预算(发生在本月)

"); description.AppendLine(""" """); foreach (var item in yearlyIncomeItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 本月收入
{item.name} {(item.limit == 0 ? "不限额" : item.limit.ToString("N0"))} {item.current:N0}
"); description.AppendLine($"""

收入合计: {yearlyIncomeItems.Sum(item => item.current):N0}

"""); } if (yearlyExpenseItems.Any()) { description.AppendLine("

年度支出预算(发生在本月)

"); description.AppendLine(""" """); foreach (var item in yearlyExpenseItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 本月支出
{item.name} {(item.limit == 0 ? "不限额" : item.limit.ToString("N0"))} {item.current:N0}
"); description.AppendLine($"""

支出合计: {yearlyExpenseItems.Sum(item => item.current):N0}

"""); } #endregion #region 总结 description.AppendLine("

存款计划结论

"); var plannedIncome = monthlyIncomeItems.Sum(item => item.limit == 0 ? item.current : item.limit) + yearlyIncomeItems.Sum(item => item.current); var plannedExpense = monthlyExpenseItems.Sum(item => item.limit == 0 ? item.current : item.limit) + yearlyExpenseItems.Sum(item => item.current); var expectedSavings = plannedIncome - plannedExpense; description.AppendLine($"""

计划存款: {expectedSavings:N0} =

     计划收入: {monthlyIncomeItems.Sum(item => item.limit):N0}

"""); if (yearlyIncomeItems.Count > 0) { description.AppendLine($"""

     + 本月发生的年度预算收入: {yearlyIncomeItems.Sum(item => item.current):N0}

"""); } description.AppendLine($"""

     - 计划支出: {monthlyExpenseItems.Sum(item => item.limit):N0} """); if (yearlyExpenseItems.Count > 0) { description.AppendLine($"""

     - 本月发生的年度预算支出: {yearlyExpenseItems.Sum(item => item.current):N0}

"""); } description.AppendLine($"""

"""); #endregion var savingsCategories = await configService.GetConfigByKeyAsync("SavingsCategories") ?? string.Empty; var currentActual = 0m; if (!string.IsNullOrEmpty(savingsCategories)) { var cats = new HashSet(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries)); foreach (var kvp in transactionClassify) { if (cats.Contains(kvp.Key.Item1)) { currentActual += kvp.Value; } } } var record = new BudgetRecord { Id = -2, Name = "月度存款计划", Type = BudgetPeriodType.Month, Limit = expectedSavings, Category = BudgetCategory.Savings, SelectedCategories = savingsCategories, StartDate = new DateTime(year, month, 1), NoLimit = false, IsMandatoryExpense = false, CreateTime = dateTimeProvider.Now, UpdateTime = dateTimeProvider.Now }; // 生成明细数据 var referenceDate = new DateTime(year, month, dateTimeProvider.Now.Day); var details = GenerateMonthlyDetails( monthlyIncomeItems, monthlyExpenseItems, yearlyIncomeItems, yearlyExpenseItems, referenceDate ); var result = BudgetResult.FromEntity( record, currentActual, new DateTime(year, month, 1), description.ToString() ); result.Details = details; return result; } private async Task GetForYearAsync( IEnumerable budgets, int year) { // 因为非当前月份的读取归档数据,这边依然是读取当前月份的数据 var currentMonth = dateTimeProvider.Now.Month; var transactionClassify = await transactionStatisticsService.GetAmountGroupByClassifyAsync( new DateTime(year, currentMonth, 1), new DateTime(year, currentMonth, 1).AddMonths(1) ); var currentMonthlyIncomeItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>(); var currentYearlyIncomeItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>(); var currentMonthlyExpenseItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>(); var currentYearlyExpenseItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>(); // 归档的预算收入支出明细 var archiveIncomeItems = new List<(long id, string name, int[] months, decimal limit, decimal current)>(); var archiveExpenseItems = new List<(long id, string name, int[] months, decimal limit, decimal current)>(); var archiveSavingsItems = new List<(long id, string name, int[] months, decimal limit, decimal current)>(); // 获取归档数据 var archives = await budgetArchiveRepository.GetArchivesByYearAsync(year); var archiveBudgetGroups = archives .SelectMany(a => a.Content.Select(x => (a.Month, Archive: x))) .Where(b => b.Archive.Type == BudgetPeriodType.Month) // 因为本来就是当前年度预算的生成 ,归档无需关心年度, 以最新地为准即可 .GroupBy(b => (b.Archive.Id, b.Archive.Limit)); foreach (var archiveBudgetGroup in archiveBudgetGroups) { var (_, archive) = archiveBudgetGroup.First(); var archiveItems = archive.Category switch { BudgetCategory.Income => archiveIncomeItems, BudgetCategory.Expense => archiveExpenseItems, BudgetCategory.Savings => archiveSavingsItems, _ => throw new NotSupportedException($"Category {archive.Category} is not supported.") }; archiveItems.Add(( id: archiveBudgetGroup.Key.Id, name: archive.Name, months: archiveBudgetGroup.Select(x => x.Month).OrderBy(m => m).ToArray(), limit: archiveBudgetGroup.Key.Limit, current: archiveBudgetGroup.Sum(x => x.Archive.Actual) )); } // 处理当月最新地没有归档的预算 foreach (var budget in budgets) { var currentAmount = 0m; var classifyList = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); var transactionType = budget.Category switch { BudgetCategory.Income => TransactionType.Income, BudgetCategory.Expense => TransactionType.Expense, _ => throw new NotSupportedException($"Budget category {budget.Category} is not supported.") }; foreach (var classify in classifyList) { // 获取分类+收入支出类型一致的金额 if (transactionClassify.TryGetValue((classify, transactionType), out var amount)) { currentAmount += amount; } } // 硬性预算 如果实际发生金额小于 应(总天数/实际天数)发生金额 // 直接取应发生金额(为了预算的准确性) if (budget.IsMandatoryExpense && currentAmount == 0) { currentAmount = budget.IsMandatoryExpense && currentAmount == 0 ? budget.Limit / (DateTime.IsLeapYear(year) ? 366 : 365) * dateTimeProvider.Now.DayOfYear : budget.Limit / DateTime.DaysInMonth(year, currentMonth) * dateTimeProvider.Now.Day; } AddOrIncCurrentItem( budget.Id, budget.Type, budget.Category, budget.Name, budget.Limit, budget.Type == BudgetPeriodType.Year ? 1 : 12 - currentMonth + 1, currentAmount, budget.IsMandatoryExpense ); } var description = new StringBuilder(); #region 构建归档收入明细表格 var archiveIncomeDiff = 0m; if (archiveIncomeItems.Any()) { description.AppendLine("

已归档收入明细

"); description.AppendLine(""" """); // 已归档的收入 foreach (var (_, name, months, limit, current) in archiveIncomeItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 合计 实际
{name} {(limit == 0 ? "不限额" : limit.ToString("N0"))} {FormatMonths(months)} {limit * months.Length:N0} {current:N0}
"); archiveIncomeDiff = archiveIncomeItems.Sum(i => i.current) - archiveIncomeItems.Sum(i => i.limit * i.months.Length); description.AppendLine($"""

已归档收入总结: {(archiveIncomeDiff > 0 ? "超额收入" : "未达预期")}: {archiveIncomeDiff:N0} = {archiveIncomeItems.Sum(i => i.limit * i.months.Length):N0} - 实际收入合计: {archiveIncomeItems.Sum(i => i.current):N0}

"""); } #endregion #region 构建年度预算收入明细表格 description.AppendLine("

预算收入明细

"); description.AppendLine(""" """); // 当前预算 foreach (var (_, name, limit, factor, _, _) in currentMonthlyIncomeItems) { description.AppendLine($""" """); } // 年预算 foreach (var (_, name, limit, _, _, _) in currentYearlyIncomeItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 月/年 合计
{name} {(limit == 0 ? "不限额" : limit.ToString("N0"))} {FormatMonthsByFactor(factor)} {(limit == 0 ? "不限额" : (limit * factor).ToString("N0"))}
{name} {(limit == 0 ? "不限额" : limit.ToString("N0"))} {year}年 {(limit == 0 ? "不限额" : limit.ToString("N0"))}
"); description.AppendLine($"""

预算收入合计: {currentMonthlyIncomeItems.Sum(i => i.limit * i.factor) + currentYearlyIncomeItems.Sum(i => i.limit):N0}

"""); #endregion #region 构建年度归档支出明细表格 var archiveExpenseDiff = 0m; if (archiveExpenseItems.Any()) { description.AppendLine("

已归档支出明细

"); description.AppendLine(""" """); // 已归档的支出 foreach (var (_, name, months, limit, current) in archiveExpenseItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 合计 实际
{name} {(limit == 0 ? "不限额" : limit.ToString("N0"))} {FormatMonths(months)} {limit * months.Length:N0} {current:N0}
"); archiveExpenseDiff = archiveExpenseItems.Sum(i => i.limit * i.months.Length) - archiveExpenseItems.Sum(i => i.current); description.AppendLine($"""

已归档支出总结: {(archiveExpenseDiff > 0 ? "节省支出" : "超支")}: {archiveExpenseDiff:N0} = {archiveExpenseItems.Sum(i => i.limit * i.months.Length):N0} - 实际支出合计: {archiveExpenseItems.Sum(i => i.current):N0}

"""); } #endregion #region 构建归档存款明细表格 var archiveSavingsDiff = 0m; if (archiveSavingsItems.Any()) { description.AppendLine("

已归档存款明细

"); description.AppendLine(""" """); // 已归档的存款 foreach (var (_, name, months, limit, current) in archiveSavingsItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 合计 实际
{name} {(limit == 0 ? "不限额" : limit.ToString("N0"))} {FormatMonths(months)} {limit * months.Length:N0} {current:N0}
"); archiveSavingsDiff = archiveSavingsItems.Sum(i => i.current) - archiveSavingsItems.Sum(i => i.limit * i.months.Length); description.AppendLine($"""

已归档存款总结: {(archiveSavingsDiff > 0 ? "超额存款" : "未达预期")}: {archiveSavingsDiff:N0} = 实际存款合计: {archiveSavingsItems.Sum(i => i.current):N0} - 预算存款合计: {archiveSavingsItems.Sum(i => i.limit * i.months.Length):N0}

"""); } #endregion #region 构建当前年度预算支出明细表格 description.AppendLine("

预算支出明细

"); description.AppendLine(""" """); // 未来月预算 foreach (var (_, name, limit, factor, _, _) in currentMonthlyExpenseItems) { description.AppendLine($""" """); } // 年预算 foreach (var (_, name, limit, _, _, _) in currentYearlyExpenseItems) { description.AppendLine($""" """); } description.AppendLine("
名称 预算 月/年 合计
{name} {(limit == 0 ? "不限额" : limit.ToString("N0"))} {FormatMonthsByFactor(factor)} {(limit == 0 ? "不限额" : (limit * factor).ToString("N0"))}
{name} {(limit == 0 ? "不限额" : limit.ToString("N0"))} {year}年 {(limit == 0 ? "不限额" : limit.ToString("N0"))}
"); // 合计 description.AppendLine($"""

支出预算合计: {currentMonthlyExpenseItems.Sum(i => i.limit * i.factor) + currentYearlyExpenseItems.Sum(i => i.limit):N0}

"""); #endregion #region 总结 var archiveIncomeBudget = archiveIncomeItems.Sum(i => i.limit * i.months.Length); var archiveExpenseBudget = archiveExpenseItems.Sum(i => i.limit * i.months.Length); // 如果有归档存款数据,直接使用;否则用收入-支出计算 var archiveSavings = archiveSavingsItems.Any() ? archiveSavingsItems.Sum(i => i.current) : archiveIncomeBudget - archiveExpenseBudget + archiveIncomeDiff + archiveExpenseDiff; var expectedIncome = currentMonthlyIncomeItems.Sum(i => i.limit == 0 ? i.current : i.limit * i.factor) + currentYearlyIncomeItems.Sum(i => i.isMandatory || i.limit == 0 ? i.current : i.limit); var expectedExpense = currentMonthlyExpenseItems.Sum(i => i.limit == 0 ? i.current : i.limit * i.factor) + currentYearlyExpenseItems.Sum(i => i.isMandatory || i.limit == 0 ? i.current : i.limit); var expectedSavings = expectedIncome - expectedExpense; description.AppendLine("

存款计划结论

"); description.AppendLine($"""

归档存款: {archiveSavings:N0} = 归档收入: {archiveIncomeBudget:N0} - 归档支出: {archiveExpenseBudget:N0} {(archiveIncomeDiff >= 0 ? " + 超额收入" : " - 未达预期收入")}: {(archiveIncomeDiff >= 0 ? archiveIncomeDiff : -archiveIncomeDiff):N0} {(archiveExpenseDiff >= 0 ? " + 节省支出" : " - 超额支出")}: {(archiveExpenseDiff >= 0 ? archiveExpenseDiff : -archiveExpenseDiff):N0}

预计存款: {expectedSavings:N0} = 预计收入: {expectedIncome:N0} - 预计支出: {expectedExpense:N0}

存档总结: {archiveSavings + expectedSavings:N0} = 预计存款: {expectedSavings:N0} {(archiveSavings > 0 ? "+" : "-")} 归档存款: {Math.Abs(archiveSavings):N0}

"""); #endregion var savingsCategories = await configService.GetConfigByKeyAsync("SavingsCategories") ?? string.Empty; var currentActual = 0m; // 1. 先累加已归档月份的存款金额 if (archiveSavingsItems.Any()) { currentActual += archiveSavingsItems.Sum(i => i.current); } // 2. 再累加当前月的存款金额 if (!string.IsNullOrEmpty(savingsCategories)) { var cats = new HashSet(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries)); foreach (var kvp in transactionClassify) { if (cats.Contains(kvp.Key.Item1)) { currentActual += kvp.Value; } } } var record = new BudgetRecord { Id = -1, Name = "年度存款计划", Type = BudgetPeriodType.Year, Limit = archiveSavings + expectedSavings, Category = BudgetCategory.Savings, SelectedCategories = savingsCategories, StartDate = new DateTime(year, 1, 1), NoLimit = false, IsMandatoryExpense = false, CreateTime = dateTimeProvider.Now, UpdateTime = dateTimeProvider.Now }; return BudgetResult.FromEntity( record, currentActual, new DateTime(year, 1, 1), description.ToString() ); void AddOrIncCurrentItem( long id, BudgetPeriodType periodType, BudgetCategory category, string name, decimal limit, int factor, decimal incAmount, bool isMandatory) { var current = (periodType, category) switch { (BudgetPeriodType.Month, BudgetCategory.Income) => currentMonthlyIncomeItems, (BudgetPeriodType.Month, BudgetCategory.Expense) => currentMonthlyExpenseItems, (BudgetPeriodType.Year, BudgetCategory.Income) => currentYearlyIncomeItems, (BudgetPeriodType.Year, BudgetCategory.Expense) => currentYearlyExpenseItems, _ => throw new NotSupportedException($"Category {category} is not supported.") }; if (current.Any(i => i.id == id)) { var existing = current.First(i => i.id == id); current.Remove(existing); current.Add((id, existing.name, existing.limit, existing.factor + factor, existing.current + incAmount, isMandatory)); } else { current.Add((id, name, limit, factor, incAmount, isMandatory)); } } string FormatMonthsByFactor(int factor) { var months = factor == 12 ? Enumerable.Range(1, 12).ToArray() : Enumerable.Range(dateTimeProvider.Now.Month, factor).ToArray(); return FormatMonths(months.ToArray()); } string FormatMonths(int[] months) { // 如果是连续的月份 则简化显示 1~3 Array.Sort(months); if (months.Length >= 2) { var isContinuous = true; for (var i = 1; i < months.Length; i++) { if (months[i] != months[i - 1] + 1) { isContinuous = false; break; } } if (isContinuous) { return $"{months.First()}~{months.Last()}月"; } } return string.Join(", ", months) + "月"; } } /// /// 计算月度计划存款 /// 公式:收入预算 + 发生在本月的年度收入 - 支出预算 - 发生在本月的年度支出 /// public static decimal CalculateMonthlyPlannedSavings( decimal monthlyIncomeBudget, decimal yearlyIncomeInThisMonth, decimal monthlyExpenseBudget, decimal yearlyExpenseInThisMonth) { return monthlyIncomeBudget + yearlyIncomeInThisMonth - monthlyExpenseBudget - yearlyExpenseInThisMonth; } /// /// 计算年度计划存款 /// 公式:归档月已实收 + 未来月收入预算 - 归档月已实支 - 未来月支出预算 /// public static decimal CalculateYearlyPlannedSavings( decimal archivedIncome, decimal futureIncomeBudget, decimal archivedExpense, decimal futureExpenseBudget) { return archivedIncome + futureIncomeBudget - archivedExpense - futureExpenseBudget; } /// /// 生成月度存款明细数据 /// private SavingsDetail GenerateMonthlyDetails( List<(string name, decimal limit, decimal current, bool isMandatory)> monthlyIncomeItems, List<(string name, decimal limit, decimal current, bool isMandatory)> monthlyExpenseItems, List<(string name, decimal limit, decimal current)> yearlyIncomeItems, List<(string name, decimal limit, decimal current)> yearlyExpenseItems, DateTime referenceDate) { var incomeDetails = new List(); var expenseDetails = new List(); // 处理月度收入 foreach (var item in monthlyIncomeItems) { var effectiveAmount = BudgetItemCalculator.CalculateEffectiveAmount( BudgetCategory.Income, item.limit, item.current, item.isMandatory, isArchived: false, referenceDate, BudgetPeriodType.Month ); var note = BudgetItemCalculator.GenerateCalculationNote( BudgetCategory.Income, item.limit, item.current, effectiveAmount, item.isMandatory, isArchived: false ); incomeDetails.Add(new BudgetDetailItem { Id = 0, // 临时ID Name = item.name, Type = BudgetPeriodType.Month, BudgetLimit = item.limit, ActualAmount = item.current, EffectiveAmount = effectiveAmount, CalculationNote = note, IsOverBudget = item.current > 0 && item.current < item.limit, IsArchived = false }); } // 处理月度支出 foreach (var item in monthlyExpenseItems) { var effectiveAmount = BudgetItemCalculator.CalculateEffectiveAmount( BudgetCategory.Expense, item.limit, item.current, item.isMandatory, isArchived: false, referenceDate, BudgetPeriodType.Month ); var note = BudgetItemCalculator.GenerateCalculationNote( BudgetCategory.Expense, item.limit, item.current, effectiveAmount, item.isMandatory, isArchived: false ); expenseDetails.Add(new BudgetDetailItem { Id = 0, Name = item.name, Type = BudgetPeriodType.Month, BudgetLimit = item.limit, ActualAmount = item.current, EffectiveAmount = effectiveAmount, CalculationNote = note, IsOverBudget = item.current > item.limit, IsArchived = false }); } // 处理年度收入(发生在本月的) foreach (var item in yearlyIncomeItems) { incomeDetails.Add(new BudgetDetailItem { Id = 0, Name = item.name, Type = BudgetPeriodType.Year, BudgetLimit = item.limit, ActualAmount = item.current, EffectiveAmount = item.current, // 年度预算发生在本月的直接用实际值 CalculationNote = "使用实际", IsOverBudget = false, IsArchived = false }); } // 处理年度支出(发生在本月的) foreach (var item in yearlyExpenseItems) { expenseDetails.Add(new BudgetDetailItem { Id = 0, Name = item.name, Type = BudgetPeriodType.Year, BudgetLimit = item.limit, ActualAmount = item.current, EffectiveAmount = item.current, CalculationNote = "使用实际", IsOverBudget = item.current > item.limit, IsArchived = false }); } // 计算汇总 var totalIncome = incomeDetails.Sum(i => i.EffectiveAmount); var totalExpense = expenseDetails.Sum(e => e.EffectiveAmount); var plannedSavings = totalIncome - totalExpense; var formula = $"收入 {totalIncome:N0} - 支出 {totalExpense:N0} = {plannedSavings:N0}"; return new SavingsDetail { IncomeItems = incomeDetails, ExpenseItems = expenseDetails, Summary = new SavingsCalculationSummary { TotalIncomeBudget = totalIncome, TotalExpenseBudget = totalExpense, PlannedSavings = plannedSavings, CalculationFormula = formula } }; } }