namespace Service; public interface IBudgetService { Task> GetListAsync(DateTime? referenceDate = null); Task GetStatisticsAsync(long id, DateTime? referenceDate = null); Task ArchiveBudgetsAsync(int year, int month); /// /// 获取指定分类的统计信息(月度和年度) /// Task GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null); } public class BudgetService( IBudgetRepository budgetRepository, IBudgetArchiveRepository budgetArchiveRepository, ITransactionRecordRepository transactionRecordRepository, IOpenAiService openAiService, IConfigService configService, IMessageRecordService messageService, ILogger logger ) : IBudgetService { public async Task> GetListAsync(DateTime? referenceDate = null) { var budgets = await budgetRepository.GetAllAsync(); var dtos = new List(); foreach (var budget in budgets) { var currentAmount = await CalculateCurrentAmountAsync(budget, referenceDate); dtos.Add(BudgetResult.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(); } public async Task GetStatisticsAsync(long id, DateTime? referenceDate = null) { if (id == -1) { return await GetVirtualSavingsDtoAsync(BudgetPeriodType.Year, referenceDate); } if (id == -2) { return await GetVirtualSavingsDtoAsync(BudgetPeriodType.Month, referenceDate); } var budget = await budgetRepository.GetByIdAsync(id); if (budget == null) { return null; } var currentAmount = await CalculateCurrentAmountAsync(budget, referenceDate); 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); var budgets = await GetListAsync(referenceDate); var addArchives = new List(); var updateArchives = new List(); foreach (var budget in budgets) { var archive = await budgetArchiveRepository.GetArchiveAsync(budget.Id, year, month); if (archive != null) { archive.RealizedAmount = budget.Current; archive.ArchiveDate = DateTime.Now; updateArchives.Add(archive); } else { archive = new BudgetArchive { BudgetId = budget.Id, BudgetType = budget.Type, Year = year, Month = month, BudgetedAmount = budget.Limit, RealizedAmount = budget.Current, ArchiveDate = DateTime.Now }; addArchives.Add(archive); } } if (addArchives.Count > 0) { if (!await budgetArchiveRepository.AddRangeAsync(addArchives)) { return "保存预算归档失败"; } } if (updateArchives.Count > 0) { if (!await budgetArchiveRepository.UpdateRangeAsync(updateArchives)) { return "更新预算归档失败"; } } _ = NotifyAsync(year, month); return string.Empty; } private async Task NotifyAsync(int year, int month) { try { var archives = await budgetArchiveRepository.GetListAsync(year, month); var budgets = await budgetRepository.GetAllAsync(); var budgetMap = budgets.ToDictionary(b => b.Id, b => b); var archiveData = archives.Select(a => { budgetMap.TryGetValue(a.BudgetId, out var br); var name = br?.Name ?? (a.BudgetId == -1 ? "年度存款" : a.BudgetId == -2 ? "月度存款" : "未知"); return new { Name = name, Type = a.BudgetType.ToString(), Limit = a.BudgetedAmount, Actual = a.RealizedAmount, Category = br?.Category.ToString() ?? (a.BudgetId < 0 ? "Savings" : "Unknown") }; }).ToList(); var yearTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync( $""" SELECT COUNT(*) AS TransactionCount, SUM(ABS(Amount)) AS TotalAmount, Type, Classify FROM TransactionRecord WHERE OccurredAt >= '{year}-01-01' AND OccurredAt < '{year + 1}-01-01' GROUP BY Type, Classify ORDER BY TotalAmount DESC """ ); var monthYear = new DateTime(year, month, 1).AddMonths(1); var monthTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync( $""" SELECT COUNT(*) AS TransactionCount, SUM(ABS(Amount)) AS TotalAmount, Type, Classify FROM TransactionRecord WHERE OccurredAt >= '{year}-{month:00}-01' AND OccurredAt < '{monthYear:yyyy-MM-dd}' GROUP BY Type, Classify ORDER BY TotalAmount DESC """ ); // 分析未被预算覆盖的分类 (仅针对支出类型 Type=0) var budgetedCategories = budgets .Where(b => !string.IsNullOrEmpty(b.SelectedCategories)) .SelectMany(b => b.SelectedCategories.Split(',')) .Distinct() .ToHashSet(); var uncovered = monthTransactions .Where(t => { var dict = (IDictionary)t; var classify = dict["Classify"]?.ToString() ?? ""; var type = Convert.ToInt32(dict["Type"]); return type == 0 && !budgetedCategories.Contains(classify); }) .ToList(); logger.LogInformation("预算执行数据{JSON}", JsonSerializer.Serialize(archiveData)); logger.LogInformation("本月消费明细{JSON}", JsonSerializer.Serialize(monthTransactions)); logger.LogInformation("全年累计消费概况{JSON}", JsonSerializer.Serialize(yearTransactions)); logger.LogInformation("未被预算覆盖的分类{JSON}", JsonSerializer.Serialize(uncovered)); var dataPrompt = $""" 报告周期:{year}年{month}月 账单数据说明:支出金额已取绝对值(TotalAmount 为正数表示支出/收入的总量)。 1. 预算执行数据(JSON): {JsonSerializer.Serialize(archiveData)} 2. 本月消费明细(按分类, JSON): {JsonSerializer.Serialize(monthTransactions)} 3. 全年累计消费概况(按分类, JSON): {JsonSerializer.Serialize(yearTransactions)} 4. 未被任何预算覆盖的支出分类(JSON): {JsonSerializer.Serialize(uncovered)} 请生成一份专业且美观的预算执行分析报告,严格遵守以下要求: 【内容要求】 1. 概览:总结本月预算达成情况。 2. 预算详情:使用 HTML 表格展示预算执行明细(预算项、预算额、实际额、使用/达成率、状态)。 3. 超支/异常预警:重点分析超支项或支出异常的分类。 4. 消费透视:针对“未被预算覆盖的支出”提供分析建议。分析这些账单产生的合理性,并评估是否需要为其中的大额或频发分类建立新预算。 5. 改进建议:基于本月整体收入支出情况,给出下月预算调整或消费改进的专业化建议。 6. 语言风格:专业、清晰、简洁,适合财务报告阅读。 【格式要求】 1. 使用HTML格式(移动端H5页面风格) 2. 生成清晰的报告标题(基于用户问题) 3. 使用表格展示统计数据(table > thead/tbody > tr > th/td) 4. 使用合适的HTML标签:h2(标题)、h3(小节)、p(段落)、table(表格)、ul/li(列表)、strong(强调) 5. 支出金额用 金额 包裹 6. 收入金额用 金额 包裹 7. 重要结论用 内容 高亮 【样式限制(重要)】 8. 不要包含 html、body、head 标签 9. 不要使用任何 style 属性或