namespace WebApi.Controllers; [ApiController] [Route("api/[controller]/[action]")] public class BudgetController( IBudgetService budgetService, IConfigService configService, ILogger logger) : ControllerBase { /// /// 获取预算列表 /// [HttpGet] public async Task>> GetListAsync([FromQuery] DateTime? referenceDate = null) { try { var budgets = await budgetService.GetAllAsync(); var dtos = new List(); foreach (var budget in budgets) { var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate); dtos.Add(BudgetDto.FromEntity(budget, currentAmount, referenceDate)); } // 创造虚拟的存款预算 dtos.Add(await GetVirtualSavingsDtoAsync( BudgetPeriodType.Month, referenceDate, budgets)); dtos.Add(await GetVirtualSavingsDtoAsync( BudgetPeriodType.Year, referenceDate, budgets)); return dtos.Where(dto => dto != null).Cast().ToList().Ok(); } catch (Exception ex) { logger.LogError(ex, "获取预算列表失败"); return $"获取预算列表失败: {ex.Message}".Fail>(); } } /// /// 获取单个预算统计信息 /// [HttpGet] public async Task> GetStatisticsAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null) { try { if (id == -1) { return (await GetVirtualSavingsDtoAsync(BudgetPeriodType.Year, referenceDate))!.Ok(); } if (id == -2) { return (await GetVirtualSavingsDtoAsync(BudgetPeriodType.Month, referenceDate))!.Ok(); } var budget = await budgetService.GetByIdAsync(id); if (budget == null) return "预算不存在".Fail(); var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate); return BudgetDto.FromEntity(budget, currentAmount, referenceDate).Ok(); } catch (Exception ex) { logger.LogError(ex, "获取预算统计失败, Id: {Id}", id); return $"获取预算统计失败: {ex.Message}".Fail(); } } /// /// 删除预算 /// [HttpDelete("{id}")] public async Task DeleteByIdAsync(long id) { try { var success = await budgetService.DeleteAsync(id); return success ? BaseResponse.Done() : "删除预算失败".Fail(); } catch (Exception ex) { logger.LogError(ex, "删除预算失败, Id: {Id}", id); return $"删除预算失败: {ex.Message}".Fail(); } } /// /// 创建预算 /// [HttpPost] public async Task> CreateAsync([FromBody] CreateBudgetDto dto) { try { var budget = new BudgetRecord { Name = dto.Name, Type = dto.Type, Limit = dto.Limit, Category = dto.Category, SelectedCategories = dto.SelectedCategories != null ? string.Join(",", dto.SelectedCategories) : string.Empty, StartDate = dto.StartDate ?? DateTime.Now }; var varidationError = await ValidateBudgetSelectedCategoriesAsync(budget); if (!string.IsNullOrEmpty(varidationError)) { return varidationError.Fail(); } var success = await budgetService.AddAsync(budget); if (success) { return budget.Id.Ok(); } return "创建预算失败".Fail(); } catch (Exception ex) { logger.LogError(ex, "创建预算失败"); return $"创建预算失败: {ex.Message}".Fail(); } } /// /// 更新预算 /// [HttpPost] public async Task UpdateAsync([FromBody] UpdateBudgetDto dto) { try { var budget = await budgetService.GetByIdAsync(dto.Id); if (budget == null) return "预算不存在".Fail(); budget.Name = dto.Name; budget.Type = dto.Type; budget.Limit = dto.Limit; budget.Category = dto.Category; budget.SelectedCategories = dto.SelectedCategories != null ? string.Join(",", dto.SelectedCategories) : string.Empty; budget.IsStopped = dto.IsStopped; if (dto.StartDate.HasValue) { budget.StartDate = dto.StartDate.Value; } var varidationError = await ValidateBudgetSelectedCategoriesAsync(budget); if (!string.IsNullOrEmpty(varidationError)) { return varidationError.Fail(); } var success = await budgetService.UpdateAsync(budget); return success ? BaseResponse.Done() : "更新预算失败".Fail(); } catch (Exception ex) { logger.LogError(ex, "更新预算失败, Id: {Id}", dto.Id); return $"更新预算失败: {ex.Message}".Fail(); } } private async Task GetVirtualSavingsDtoAsync( BudgetPeriodType periodType, DateTime? referenceDate = null, List? existingBudgets = null) { var allBudgets = existingBudgets; if(existingBudgets == null) { allBudgets = await budgetService.GetAllAsync(); } if(allBudgets == null) { return null; } var date = referenceDate ?? DateTime.Now; decimal incomeLimitAtPeriod = 0; decimal expenseLimitAtPeriod = 0; var incomeItems = new List<(string Name, decimal Limit, decimal Factor, decimal Total)>(); var expenseItems = new List<(string Name, decimal Limit, decimal Factor, decimal Total)>(); foreach (var b in allBudgets) { if (b.IsStopped || b.Category == BudgetCategory.Savings) continue; // 折算系数:根据当前请求的 periodType (Year 或 Month),将预算 b 的 Limit 折算过来 decimal factor = 1.0m; if (periodType == BudgetPeriodType.Year) { factor = b.Type switch { BudgetPeriodType.Month => 12, BudgetPeriodType.Year => 1, _ => 0 }; } else if (periodType == BudgetPeriodType.Month) { factor = b.Type switch { BudgetPeriodType.Month => 1, BudgetPeriodType.Year => 0, _ => 0 }; } else { factor = 0; // 其他周期暂不计算虚拟存款 } if (factor <= 0) continue; var subtotal = b.Limit * factor; if (b.Category == BudgetCategory.Income) { incomeLimitAtPeriod += subtotal; incomeItems.Add((b.Name, b.Limit, factor, subtotal)); } else if (b.Category == BudgetCategory.Expense) { expenseLimitAtPeriod += subtotal; expenseItems.Add((b.Name, b.Limit, factor, subtotal)); } } var description = new StringBuilder(); description.Append("

预算收入明细

"); if (incomeItems.Count == 0) description.Append("

无收入预算

"); else { description.Append(""); foreach (var item in incomeItems) { description.Append($""); } description.Append("
名称金额折算合计
{item.Name}{item.Limit:N0}x{item.Factor:0.##}{item.Total:N0}
"); } description.Append($"

收入合计: {incomeLimitAtPeriod:N0}

"); description.Append("

预算支出明细

"); if (expenseItems.Count == 0) description.Append("

无支出预算

"); else { description.Append(""); foreach (var item in expenseItems) { description.Append($""); } description.Append("
名称金额折算合计
{item.Name}{item.Limit:N0}x{item.Factor:0.##}{item.Total:N0}
"); } description.Append($"

支出合计: {expenseLimitAtPeriod:N0}

"); description.Append("

存款计划结论

"); description.Append($"

计划存款 = 收入 {incomeLimitAtPeriod:N0} - 支出 {expenseLimitAtPeriod:N0}

"); description.Append($"

最终目标:{incomeLimitAtPeriod - expenseLimitAtPeriod:N0}

"); var savingsCategories = await configService.GetConfigByKeyAsync("SavingsCategories") ?? string.Empty; var virtualBudget = new BudgetRecord { Id = periodType == BudgetPeriodType.Year ? -1 : -2, Name = periodType == BudgetPeriodType.Year ? "年度存款" : "月度存款", 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), 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 actualIncome = await budgetService.CalculateCurrentAmountAsync(incomeHelper, date); var actualExpense = await budgetService.CalculateCurrentAmountAsync(expenseHelper, date); return BudgetDto.FromEntity(virtualBudget, actualIncome - actualExpense, date, description.ToString()); } private async Task ValidateBudgetSelectedCategoriesAsync(BudgetRecord record) { var allBudgets = await budgetService.GetAllAsync(); var recordSelectedCategories = record.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var budget in allBudgets) { var selectedCategories = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); if (budget.Id != record.Id) { if (budget.Category == record.Category && recordSelectedCategories.Intersect(selectedCategories).Any()) { return $"和 {budget.Name} 存在分类冲突,请调整相关分类。"; } } } return string.Empty; } /// /// 切换预算暂停状态 /// [HttpPost] public async Task ToggleStopAsync([FromQuery] long id) { try { var success = await budgetService.ToggleStopAsync(id); return success ? BaseResponse.Done() : "操作失败".Fail(); } catch (Exception ex) { logger.LogError(ex, "切换预算状态失败, Id: {Id}", id); return $"操作失败: {ex.Message}".Fail(); } } }