Files
EmailBill/Service/Budget/BudgetStatsService.cs
SunCheng 9e14849014
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
feat: 添加预算统计服务增强和日志系统改进
1. 新增 BudgetStatsService:将预算统计逻辑从 BudgetService 中提取为独立服务,支持月度和年度统计,包含归档数据支持和硬性预算调整算法
2. 日志系统增强:添加请求ID追踪功能,支持通过请求ID查询关联日志,新增类名筛选功能
3. 日志解析优化:修复类名解析逻辑,正确提取 SourceContext 中的类名信息
4. 代码清理:移除不需要的方法名相关代码,简化日志筛选逻辑
2026-01-22 19:07:10 +08:00

940 lines
45 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace Service.Budget;
public interface IBudgetStatsService
{
/// <summary>
/// 获取指定分类的统计信息(月度和年度)
/// </summary>
Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate);
}
[UsedImplicitly]
public class BudgetStatsService(
IBudgetRepository budgetRepository,
IBudgetArchiveRepository budgetArchiveRepository,
ITransactionRecordRepository transactionRecordRepository,
IDateTimeProvider dateTimeProvider,
ILogger<BudgetStatsService> logger
) : IBudgetStatsService
{
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{
logger.LogInformation("开始计算分类统计信息: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
category, referenceDate);
var result = new BudgetCategoryStats();
try
{
// 获取月度统计
logger.LogDebug("开始计算月度统计");
result.Month = await CalculateMonthlyCategoryStatsAsync(category, referenceDate);
logger.LogInformation("月度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%",
result.Month.Count, result.Month.Limit, result.Month.Current, result.Month.Rate);
// 获取年度统计
logger.LogDebug("开始计算年度统计");
result.Year = await CalculateYearlyCategoryStatsAsync(category, referenceDate);
logger.LogInformation("年度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%",
result.Year.Count, result.Year.Limit, result.Year.Current, result.Year.Rate);
logger.LogInformation("分类统计信息计算完成");
}
catch (Exception ex)
{
logger.LogError(ex, "计算分类统计信息时发生错误: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
category, referenceDate);
throw;
}
return result;
}
private async Task<BudgetStatsDto> CalculateMonthlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{
logger.LogDebug("开始计算月度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM}",
category, referenceDate);
var result = new BudgetStatsDto
{
PeriodType = BudgetPeriodType.Month,
Rate = 0,
Current = 0,
Limit = 0,
Count = 0
};
// 1. 获取所有预算(包含归档数据)
logger.LogDebug("开始获取预算数据(包含归档)");
var budgets = await GetAllBudgetsWithArchiveAsync(category, BudgetPeriodType.Month, referenceDate);
logger.LogDebug("获取到 {BudgetCount} 个预算", budgets.Count);
if (budgets.Count == 0)
{
logger.LogDebug("未找到相关预算,返回空结果");
return result;
}
result.Count = budgets.Count;
// 2. 计算限额总值(考虑不限额预算的特殊处理)
logger.LogDebug("开始计算限额总值,共 {BudgetCount} 个预算", budgets.Count);
decimal totalLimit = 0;
int budgetIndex = 0;
foreach (var budget in budgets)
{
budgetIndex++;
var itemLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Month, referenceDate);
logger.LogInformation("预算 {BudgetIndex}/{BudgetCount}: {BudgetName} (ID={BudgetId}) - 预算金额: {BudgetLimit}, 实际金额: {CurrentAmount}, 计算算法: {Algorithm}",
budgetIndex, budgets.Count, budget.Name, budget.Id, budget.Limit, budget.Current,
budget.NoLimit ? "不限额预算" : budget.IsMandatoryExpense ? "硬性预算" : "普通预算");
logger.LogTrace("预算 {BudgetName} (ID={BudgetId}) 计算后限额: {ItemLimit}",
budget.Name, budget.Id, itemLimit);
totalLimit += itemLimit;
}
result.Limit = totalLimit;
logger.LogDebug("限额总值计算完成: {TotalLimit}", totalLimit);
// 3. 计算当前实际值(避免重复计算同一笔交易)
var transactionType = category switch
{
BudgetCategory.Expense => TransactionType.Expense,
BudgetCategory.Income => TransactionType.Income,
_ => TransactionType.None
};
logger.LogDebug("交易类型: {TransactionType}", transactionType);
// 计算当前实际值,考虑硬性预算的特殊逻辑
decimal totalCurrent = 0;
var now = dateTimeProvider.Now;
var (startDate, endDate) = GetStatPeriodRange(BudgetPeriodType.Month, referenceDate);
logger.LogDebug("统计时间段: {StartDate:yyyy-MM-dd} 到 {EndDate:yyyy-MM-dd}", startDate, endDate);
if (transactionType != TransactionType.None)
{
// 获取所有相关分类
var allClassifies = budgets
.SelectMany(b => b.SelectedCategories)
.Distinct()
.ToList();
logger.LogDebug("相关分类数量: {ClassifyCount}", allClassifies.Count);
// 获取趋势统计数据(去重计算)
logger.LogDebug("开始获取交易趋势统计数据");
var dailyStats = await transactionRecordRepository.GetFilteredTrendStatisticsAsync(
startDate,
endDate,
transactionType,
allClassifies,
false);
logger.LogDebug("获取到 {DayCount} 天的交易数据", dailyStats.Count);
// 计算累计值
decimal accumulated = 0;
var daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month);
logger.LogDebug("本月天数: {DaysInMonth}", daysInMonth);
for (int i = 1; i <= daysInMonth; i++)
{
var currentDate = new DateTime(startDate.Year, startDate.Month, i);
if (currentDate.Date > now.Date)
{
result.Trend.Add(null);
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd} 为未来日期,趋势数据为 null", currentDate);
continue;
}
if (dailyStats.TryGetValue(currentDate.Date, out var amount))
{
accumulated += amount;
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 金额={Amount}, 累计={Accumulated}",
currentDate, amount, accumulated);
}
else
{
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 无交易数据,累计={Accumulated}",
currentDate, accumulated);
}
result.Trend.Add(accumulated);
}
totalCurrent = accumulated;
logger.LogDebug("交易累计值计算完成: {TotalCurrent}", totalCurrent);
}
else
{
// 对于非收入/支出分类,使用逐预算累加
logger.LogDebug("非收入/支出分类,使用逐预算累加,共 {BudgetCount} 个预算", budgets.Count);
budgetIndex = 0;
foreach (var budget in budgets)
{
budgetIndex++;
var currentAmount = await CalculateCurrentAmountAsync(budget, BudgetPeriodType.Month, referenceDate);
logger.LogInformation("预算 {BudgetIndex}/{BudgetCount}: {BudgetName} (ID={BudgetId}) - 实际金额计算: 预算金额={BudgetLimit}, 当前值={CurrentAmount}, 算法={Algorithm}",
budgetIndex, budgets.Count, budget.Name, budget.Id, budget.Limit, currentAmount,
budget.IsArchive ? "归档数据" : "实时计算");
logger.LogTrace("预算 {BudgetName} (ID={BudgetId}) 当前值: {CurrentAmount}",
budget.Name, budget.Id, currentAmount);
totalCurrent += currentAmount;
}
logger.LogDebug("预算累加完成: {TotalCurrent}", totalCurrent);
}
// 对于硬性预算如果当前月份且实际值为0需要按时间比例计算
if (transactionType == TransactionType.Expense)
{
logger.LogDebug("开始应用硬性预算调整,共 {BudgetCount} 个支出预算", budgets.Count);
var beforeAdjustment = totalCurrent;
totalCurrent = ApplyMandatoryBudgetAdjustment(budgets, totalCurrent, referenceDate, BudgetPeriodType.Month);
if (Math.Abs(beforeAdjustment - totalCurrent) > 0.01m)
{
logger.LogInformation("硬性预算调整完成: 调整前={BeforeAdjustment}, 调整后={AfterAdjustment}, 调整金额={AdjustmentAmount}",
beforeAdjustment, totalCurrent, totalCurrent - beforeAdjustment);
logger.LogDebug("硬性预算调整算法: 当前月份={ReferenceDate:yyyy-MM}, 硬性预算按天数比例累加计算", referenceDate);
}
else
{
logger.LogDebug("硬性预算调整未改变值");
}
}
result.Current = totalCurrent;
// 4. 计算使用率
result.Rate = totalLimit > 0 ? result.Current / totalLimit * 100 : 0;
logger.LogDebug("使用率计算完成: {Rate:F2}%", result.Rate);
// 5. 生成计算明细汇总日志
var limitParts = new List<string>();
var currentParts = new List<string>();
budgetIndex = 0;
foreach (var budget in budgets)
{
budgetIndex++;
var itemLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Month, referenceDate);
var limitPart = budget.IsArchive
? $"{budget.Name}({budget.ArchiveMonth}月归档){itemLimit}元"
: $"{budget.Name}{itemLimit}元";
limitParts.Add(limitPart);
var currentPart = budget.IsArchive
? $"{budget.Name}({budget.ArchiveMonth}月归档){budget.Current}元"
: $"{budget.Name}{budget.Current}元";
currentParts.Add(currentPart);
}
var limitSummary = string.Join(" + ", limitParts);
var currentSummary = string.Join(" + ", currentParts);
logger.LogInformation("月度统计计算明细: 预算={LimitSummary}={TotalLimit}元, 已支出={CurrentSummary}={TotalCurrent}元, 使用率={Rate:F2}%",
limitSummary, totalLimit, currentSummary, totalCurrent, result.Rate);
logger.LogDebug("月度分类统计计算完成");
return result;
}
private async Task<BudgetStatsDto> CalculateYearlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{
logger.LogDebug("开始计算年度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy}",
category, referenceDate);
var result = new BudgetStatsDto
{
PeriodType = BudgetPeriodType.Year,
Rate = 0,
Current = 0,
Limit = 0,
Count = 0
};
// 1. 获取所有预算(包含归档数据)
logger.LogDebug("开始获取预算数据(包含归档)");
var budgets = await GetAllBudgetsWithArchiveAsync(category, BudgetPeriodType.Year, referenceDate);
logger.LogDebug("获取到 {BudgetCount} 个预算", budgets.Count);
if (budgets.Count == 0)
{
logger.LogDebug("未找到相关预算,返回空结果");
return result;
}
result.Count = budgets.Count;
// 2. 计算限额总值(考虑不限额预算的特殊处理)
logger.LogDebug("开始计算年度限额总值,共 {BudgetCount} 个预算", budgets.Count);
decimal totalLimit = 0;
int budgetIndex = 0;
foreach (var budget in budgets)
{
budgetIndex++;
var itemLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Year, referenceDate);
logger.LogInformation("年度预算 {BudgetIndex}/{BudgetCount}: {BudgetName} (ID={BudgetId}) - 原始预算金额: {BudgetLimit}, 当前实际金额: {CurrentAmount}, 预算类型: {BudgetType}, 算法: {Algorithm}",
budgetIndex, budgets.Count, budget.Name, budget.Id, budget.Limit, budget.Current,
budget.Type == BudgetPeriodType.Month ? "月度预算" : "年度预算",
budget.NoLimit ? "不限额预算" : budget.IsMandatoryExpense ? "硬性预算" : "普通预算");
logger.LogTrace("预算 {BudgetName} (ID={BudgetId}) 年度计算后限额: {ItemLimit}",
budget.Name, budget.Id, itemLimit);
totalLimit += itemLimit;
}
result.Limit = totalLimit;
logger.LogDebug("年度限额总值计算完成: {TotalLimit}", totalLimit);
// 3. 计算当前实际值(避免重复计算同一笔交易)
var transactionType = category switch
{
BudgetCategory.Expense => TransactionType.Expense,
BudgetCategory.Income => TransactionType.Income,
_ => TransactionType.None
};
logger.LogDebug("交易类型: {TransactionType}", transactionType);
// 计算当前实际值,考虑硬性预算的特殊逻辑
decimal totalCurrent = 0;
var now = dateTimeProvider.Now;
var (startDate, endDate) = GetStatPeriodRange(BudgetPeriodType.Year, referenceDate);
logger.LogDebug("统计时间段: {StartDate:yyyy-MM-dd} 到 {EndDate:yyyy-MM-dd}", startDate, endDate);
if (transactionType != TransactionType.None)
{
// 获取所有相关分类
var allClassifies = budgets
.SelectMany(b => b.SelectedCategories)
.Distinct()
.ToList();
logger.LogDebug("相关分类数量: {ClassifyCount}", allClassifies.Count);
// 获取趋势统计数据(去重计算)
logger.LogDebug("开始获取交易趋势统计数据");
var dailyStats = await transactionRecordRepository.GetFilteredTrendStatisticsAsync(
startDate,
endDate,
transactionType,
allClassifies,
true);
logger.LogDebug("获取到 {MonthCount} 个月的交易数据", dailyStats.Count);
// 计算累计值
decimal accumulated = 0;
for (int i = 1; i <= 12; i++)
{
var currentMonthDate = new DateTime(startDate.Year, i, 1);
if (currentMonthDate.Year > now.Year || (currentMonthDate.Year == now.Year && i > now.Month))
{
result.Trend.Add(null);
logger.LogTrace("月份 {Month:yyyy-MM} 为未来月份,趋势数据为 null", currentMonthDate);
continue;
}
if (dailyStats.TryGetValue(currentMonthDate, out var amount))
{
accumulated += amount;
logger.LogTrace("月份 {Month:yyyy-MM}: 金额={Amount}, 累计={Accumulated}",
currentMonthDate, amount, accumulated);
}
else
{
logger.LogTrace("月份 {Month:yyyy-MM}: 无交易数据,累计={Accumulated}",
currentMonthDate, accumulated);
}
result.Trend.Add(accumulated);
}
totalCurrent = accumulated;
logger.LogDebug("交易累计值计算完成: {TotalCurrent}", totalCurrent);
}
else
{
// 对于非收入/支出分类,使用逐预算累加
logger.LogDebug("非收入/支出分类,使用逐预算累加,共 {BudgetCount} 个预算", budgets.Count);
budgetIndex = 0;
foreach (var budget in budgets)
{
budgetIndex++;
var currentAmount = await CalculateCurrentAmountAsync(budget, BudgetPeriodType.Year, referenceDate);
logger.LogInformation("年度预算 {BudgetIndex}/{BudgetCount}: {BudgetName} (ID={BudgetId}) - 实际金额计算: 原始预算={BudgetLimit}, 年度实际值={CurrentAmount}, 数据来源: {DataSource}",
budgetIndex, budgets.Count, budget.Name, budget.Id, budget.Limit, currentAmount,
budget.IsArchive ? "归档数据" : "实时计算");
logger.LogTrace("预算 {BudgetName} (ID={BudgetId}) 年度当前值: {CurrentAmount}",
budget.Name, budget.Id, currentAmount);
totalCurrent += currentAmount;
}
logger.LogDebug("年度预算累加完成: {TotalCurrent}", totalCurrent);
}
// 对于硬性预算如果当前年份且实际值为0需要按时间比例计算
if (transactionType == TransactionType.Expense)
{
logger.LogDebug("开始应用年度硬性预算调整,共 {BudgetCount} 个支出预算", budgets.Count);
var beforeAdjustment = totalCurrent;
totalCurrent = ApplyMandatoryBudgetAdjustment(budgets, totalCurrent, referenceDate, BudgetPeriodType.Year);
if (Math.Abs(beforeAdjustment - totalCurrent) > 0.01m)
{
logger.LogInformation("年度硬性预算调整完成: 调整前={BeforeAdjustment}, 调整后={AfterAdjustment}, 调整金额={AdjustmentAmount}",
beforeAdjustment, totalCurrent, totalCurrent - beforeAdjustment);
logger.LogDebug("年度硬性预算调整算法: 当前年份={ReferenceDate:yyyy}, 硬性预算按天数比例累加计算", referenceDate);
}
else
{
logger.LogDebug("年度硬性预算调整未改变值");
}
}
result.Current = totalCurrent;
// 4. 计算使用率
result.Rate = totalLimit > 0 ? result.Current / totalLimit * 100 : 0;
logger.LogDebug("使用率计算完成: {Rate:F2}%", result.Rate);
// 5. 生成计算明细汇总日志
var limitParts = new List<string>();
var currentParts = new List<string>();
budgetIndex = 0;
foreach (var budget in budgets)
{
budgetIndex++;
var itemLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Year, referenceDate);
var limitPart = budget.IsArchive
? $"{budget.Name}(归档){itemLimit}元"
: budget.RemainingMonths > 0
? $"{budget.Name}(剩余{budget.RemainingMonths}月){itemLimit}元"
: $"{budget.Name}{itemLimit}元";
limitParts.Add(limitPart);
var currentPart = budget.IsArchive
? $"{budget.Name}(归档){budget.Current}元"
: budget.RemainingMonths > 0
? $"{budget.Name}(剩余{budget.RemainingMonths}月){budget.Current}元"
: $"{budget.Name}{budget.Current}元";
currentParts.Add(currentPart);
}
var limitSummary = string.Join(" + ", limitParts);
var currentSummary = string.Join(" + ", currentParts);
logger.LogInformation("年度统计计算明细: 预算={LimitSummary}={TotalLimit}元, 已支出={CurrentSummary}={TotalCurrent}元, 使用率={Rate:F2}%",
limitSummary, totalLimit, currentSummary, totalCurrent, result.Rate);
logger.LogDebug("年度分类统计计算完成");
return result;
}
private async Task<List<BudgetStatsItem>> GetAllBudgetsWithArchiveAsync(
BudgetCategory category,
BudgetPeriodType statType,
DateTime referenceDate)
{
logger.LogDebug("开始获取预算数据: Category={Category}, StatType={StatType}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
category, statType, referenceDate);
var result = new List<BudgetStatsItem>();
var year = referenceDate.Year;
var month = referenceDate.Month;
var now = dateTimeProvider.Now;
// 对于年度统计,需要获取整年的归档数据和当前预算
if (statType == BudgetPeriodType.Year)
{
logger.LogDebug("年度统计:开始获取整年的预算数据");
// 获取当前有效的预算(用于当前月及未来月)
var currentBudgets = await budgetRepository.GetAllAsync();
var currentBudgetsDict = currentBudgets
.Where(b => b.Category == category && ShouldIncludeBudget(b, statType))
.ToDictionary(b => b.Id);
logger.LogDebug("获取到 {Count} 个当前有效预算", currentBudgetsDict.Count);
// 用于跟踪已处理的预算ID避免重复
var processedBudgetIds = new HashSet<long>();
// 1. 处理历史归档月份1月到当前月-1
if (referenceDate.Year == now.Year && now.Month > 1)
{
logger.LogDebug("开始处理历史归档月份: 1月到{Month}月", now.Month - 1);
for (int m = 1; m < now.Month; m++)
{
var archive = await budgetArchiveRepository.GetArchiveAsync(year, m);
if (archive != null)
{
logger.LogDebug("找到{Month}月归档数据,包含 {ItemCount} 个项目", m, archive.Content.Count());
foreach (var item in archive.Content)
{
if (item.Category == category && ShouldIncludeBudget(item, statType))
{
// 对于月度预算,每个月都添加一个归档项
if (item.Type == BudgetPeriodType.Month)
{
result.Add(new BudgetStatsItem
{
Id = item.Id,
Name = item.Name,
Type = item.Type,
Limit = item.Limit,
Current = item.Actual,
Category = item.Category,
SelectedCategories = item.SelectedCategories,
NoLimit = item.NoLimit,
IsMandatoryExpense = item.IsMandatoryExpense,
IsArchive = true,
ArchiveMonth = m
});
logger.LogInformation("添加归档月度预算: {BudgetName} (ID={BudgetId}) - {Month}月归档, 预算金额: {Limit}, 实际金额: {Actual}",
item.Name, item.Id, m, item.Limit, item.Actual);
}
// 对于年度预算,只添加一次
else if (item.Type == BudgetPeriodType.Year && !processedBudgetIds.Contains(item.Id))
{
processedBudgetIds.Add(item.Id);
result.Add(new BudgetStatsItem
{
Id = item.Id,
Name = item.Name,
Type = item.Type,
Limit = item.Limit,
Current = item.Actual,
Category = item.Category,
SelectedCategories = item.SelectedCategories,
NoLimit = item.NoLimit,
IsMandatoryExpense = item.IsMandatoryExpense,
IsArchive = true
});
logger.LogInformation("添加归档年度预算: {BudgetName} (ID={BudgetId}) - 预算金额: {Limit}, 实际金额: {Actual}",
item.Name, item.Id, item.Limit, item.Actual);
}
}
}
}
}
}
// 2. 处理当前月及未来月(使用当前预算)
logger.LogDebug("开始处理当前及未来月份预算");
foreach (var budget in currentBudgetsDict.Values)
{
// 对于年度预算,如果还没有从归档中添加,则添加
if (budget.Type == BudgetPeriodType.Year && !processedBudgetIds.Contains(budget.Id))
{
var currentAmount = await CalculateCurrentAmountAsync(budget, statType, referenceDate);
result.Add(new BudgetStatsItem
{
Id = budget.Id,
Name = budget.Name,
Type = budget.Type,
Limit = budget.Limit,
Current = currentAmount,
Category = budget.Category,
SelectedCategories = string.IsNullOrEmpty(budget.SelectedCategories)
? []
: budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries),
NoLimit = budget.NoLimit,
IsMandatoryExpense = budget.IsMandatoryExpense,
IsArchive = false
});
logger.LogInformation("添加当前年度预算: {BudgetName} (ID={BudgetId}) - 预算金额: {Limit}, 实际金额: {Current}",
budget.Name, budget.Id, budget.Limit, currentAmount);
}
// 对于月度预算,添加当前及未来月份的预算(标记剩余月份数)
else if (budget.Type == BudgetPeriodType.Month)
{
var remainingMonths = 12 - now.Month + 1; // 包括当前月
result.Add(new BudgetStatsItem
{
Id = budget.Id,
Name = budget.Name,
Type = budget.Type,
Limit = budget.Limit,
Current = 0, // 剩余月份不计算实际值
Category = budget.Category,
SelectedCategories = string.IsNullOrEmpty(budget.SelectedCategories)
? []
: budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries),
NoLimit = budget.NoLimit,
IsMandatoryExpense = budget.IsMandatoryExpense,
IsArchive = false,
RemainingMonths = remainingMonths
});
logger.LogInformation("添加当前月度预算(剩余月份): {BudgetName} (ID={BudgetId}) - 预算金额: {Limit}, 剩余月份: {RemainingMonths}",
budget.Name, budget.Id, budget.Limit, remainingMonths);
}
}
}
else // 月度统计
{
// 检查是否为归档月份
var isArchive = year < now.Year || (year == now.Year && month < now.Month);
logger.LogDebug("月度统计 - 是否为归档月份: {IsArchive}", isArchive);
if (isArchive)
{
// 获取归档数据
logger.LogDebug("开始获取归档数据: Year={Year}, Month={Month}", year, month);
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
if (archive != null)
{
int itemCount = archive.Content.Count();
logger.LogDebug("找到归档数据,包含 {ItemCount} 个项目", itemCount);
foreach (var item in archive.Content)
{
if (item.Category == category && ShouldIncludeBudget(item, statType))
{
result.Add(new BudgetStatsItem
{
Id = item.Id,
Name = item.Name,
Type = item.Type,
Limit = item.Limit,
Current = item.Actual,
Category = item.Category,
SelectedCategories = item.SelectedCategories,
NoLimit = item.NoLimit,
IsMandatoryExpense = item.IsMandatoryExpense,
IsArchive = true
});
logger.LogInformation("添加归档预算: {BudgetName} (ID={BudgetId}) - 归档月份: {Year}-{Month:00}, 预算金额: {BudgetLimit}, 实际金额: {ActualAmount}",
item.Name, item.Id, year, month, item.Limit, item.Actual);
}
}
}
else
{
logger.LogDebug("未找到归档数据");
}
}
else
{
// 获取当前预算数据
logger.LogDebug("开始获取当前预算数据");
var budgets = await budgetRepository.GetAllAsync();
int budgetCount = budgets.Count();
logger.LogDebug("获取到 {BudgetCount} 个预算", budgetCount);
foreach (var budget in budgets)
{
if (budget.Category == category && ShouldIncludeBudget(budget, statType))
{
var currentAmount = await CalculateCurrentAmountAsync(budget, statType, referenceDate);
result.Add(new BudgetStatsItem
{
Id = budget.Id,
Name = budget.Name,
Type = budget.Type,
Limit = budget.Limit,
Current = currentAmount,
Category = budget.Category,
SelectedCategories = string.IsNullOrEmpty(budget.SelectedCategories)
? []
: budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries),
NoLimit = budget.NoLimit,
IsMandatoryExpense = budget.IsMandatoryExpense,
IsArchive = false
});
logger.LogInformation("添加当前预算: {BudgetName} (ID={BudgetId}) - 预算金额: {BudgetLimit}, 实时计算实际金额: {CurrentAmount}, 预算类型: {BudgetType}",
budget.Name, budget.Id, budget.Limit, currentAmount,
budget.Type == BudgetPeriodType.Month ? "月度预算" : "年度预算");
}
}
}
}
logger.LogDebug("预算数据获取完成: 共找到 {ResultCount} 个符合条件的预算", result.Count);
return result;
}
private bool ShouldIncludeBudget(BudgetRecord budget, BudgetPeriodType statType)
{
// 排除不记额预算
if (budget.NoLimit)
{
return false;
}
// 月度统计只包含月度预算
if (statType == BudgetPeriodType.Month)
{
return budget.Type == BudgetPeriodType.Month;
}
// 年度统计包含所有预算
return true;
}
private bool ShouldIncludeBudget(BudgetArchiveContent budget, BudgetPeriodType statType)
{
// 排除不记额预算
if (budget.NoLimit)
{
return false;
}
// 月度统计只包含月度预算
if (statType == BudgetPeriodType.Month)
{
return budget.Type == BudgetPeriodType.Month;
}
// 年度统计包含所有预算
return true;
}
private decimal CalculateBudgetLimit(BudgetStatsItem budget, BudgetPeriodType statType, DateTime referenceDate)
{
// 不记额预算的限额为0
if (budget.NoLimit)
{
logger.LogTrace("预算 {BudgetName} (ID={BudgetId}) 为不记额预算限额返回0", budget.Name, budget.Id);
return 0;
}
var itemLimit = budget.Limit;
string algorithmDescription = $"直接使用原始预算金额: {budget.Limit}";
// 年度视图下,月度预算需要折算为年度
if (statType == BudgetPeriodType.Year && budget.Type == BudgetPeriodType.Month)
{
// 对于归档预算,直接使用归档的限额,不折算
if (budget.IsArchive)
{
itemLimit = budget.Limit;
algorithmDescription = $"归档月度预算: 直接使用归档限额 {budget.Limit}";
}
// 对于当前及未来月份的预算,使用剩余月份折算
else if (budget.RemainingMonths > 0)
{
itemLimit = budget.Limit * budget.RemainingMonths;
algorithmDescription = $"月度预算剩余月份折算: {budget.Limit} × {budget.RemainingMonths} (剩余月份) = {itemLimit}";
}
// 兼容旧逻辑如果没有设置RemainingMonths
else
{
logger.LogWarning("预算 {BudgetName} (ID={BudgetId}) 年度统计时未设置RemainingMonths使用默认折算逻辑", budget.Name, budget.Id);
if (budget.IsMandatoryExpense)
{
var now = dateTimeProvider.Now;
if (referenceDate.Year == now.Year)
{
var monthsElapsed = now.Month;
itemLimit = budget.Limit * monthsElapsed;
algorithmDescription = $"硬性预算当前年份折算: {budget.Limit} × {monthsElapsed} (已过月份) = {itemLimit}";
}
else
{
itemLimit = budget.Limit * 12;
algorithmDescription = $"硬性预算完整年度折算: {budget.Limit} × 12 = {itemLimit}";
}
}
else
{
itemLimit = budget.Limit * 12;
algorithmDescription = $"月度预算年度折算: {budget.Limit} × 12 = {itemLimit}";
}
}
}
logger.LogInformation("预算 {BudgetName} (ID={BudgetId}) 限额计算: 原始预算={OriginalLimit}, 计算后限额={CalculatedLimit}, 算法: {Algorithm}",
budget.Name, budget.Id, budget.Limit, itemLimit, algorithmDescription);
return itemLimit;
}
private async Task<decimal> CalculateCurrentAmountAsync(BudgetRecord budget, BudgetPeriodType statType, DateTime referenceDate)
{
var (startDate, endDate) = GetStatPeriodRange(statType, referenceDate);
// 获取预算的实际时间段
var (budgetStart, budgetEnd) = BudgetService.GetPeriodRange(budget.StartDate, budget.Type, referenceDate);
// 确保在统计时间段内
if (budgetEnd < startDate || budgetStart > endDate)
{
return 0;
}
var actualAmount = await budgetRepository.GetCurrentAmountAsync(budget, startDate, endDate);
// 硬性预算的特殊处理参考BudgetSavingsService第92-97行
if (actualAmount == 0 && budget.IsMandatoryExpense)
{
if (budget.Type == BudgetPeriodType.Month)
{
// 月度硬性预算按天数比例累加
var daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
var daysElapsed = dateTimeProvider.Now.Day;
actualAmount = budget.Limit * daysElapsed / daysInMonth;
}
else if (budget.Type == BudgetPeriodType.Year)
{
// 年度硬性预算按天数比例累加
var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365;
var daysElapsed = dateTimeProvider.Now.DayOfYear;
actualAmount = budget.Limit * daysElapsed / daysInYear;
}
}
return actualAmount;
}
private decimal ApplyMandatoryBudgetAdjustment(List<BudgetStatsItem> budgets, decimal currentTotal, DateTime referenceDate, BudgetPeriodType statType)
{
logger.LogDebug("开始应用硬性预算调整: 当前总计={CurrentTotal}, 统计类型={StatType}, 参考日期={ReferenceDate:yyyy-MM-dd}",
currentTotal, statType, referenceDate);
var now = dateTimeProvider.Now;
var adjustedTotal = currentTotal;
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
logger.LogDebug("找到 {MandatoryCount} 个硬性预算", mandatoryBudgets.Count);
int mandatoryIndex = 0;
foreach (var budget in mandatoryBudgets)
{
mandatoryIndex++;
// 检查是否为当前统计周期
var isCurrentPeriod = false;
if (statType == BudgetPeriodType.Month)
{
isCurrentPeriod = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} (ID={BudgetId}) - 检查月度周期: 参考月份={RefMonth}, 当前月份={CurrentMonth}, 是否为当前周期={IsCurrent}",
mandatoryIndex, mandatoryBudgets.Count, budget.Name, budget.Id,
referenceDate.ToString("yyyy-MM"), now.ToString("yyyy-MM"), isCurrentPeriod);
}
else // Year
{
isCurrentPeriod = referenceDate.Year == now.Year;
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} (ID={BudgetId}) - 检查年度周期: 参考年份={RefYear}, 当前年份={CurrentYear}, 是否为当前周期={IsCurrent}",
mandatoryIndex, mandatoryBudgets.Count, budget.Name, budget.Id,
referenceDate.Year, now.Year, isCurrentPeriod);
}
if (isCurrentPeriod)
{
// 计算硬性预算的应累加值
decimal mandatoryAccumulation = 0;
string accumulationAlgorithm = "";
if (budget.Type == BudgetPeriodType.Month)
{
// 月度硬性预算按天数比例累加
var daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
var daysElapsed = now.Day;
mandatoryAccumulation = budget.Limit * daysElapsed / daysInMonth;
accumulationAlgorithm = $"月度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInMonth} = {mandatoryAccumulation:F2}";
logger.LogDebug("月度硬性预算 {BudgetName}: 限额={Limit}, 本月天数={DaysInMonth}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
budget.Name, budget.Limit, daysInMonth, daysElapsed, mandatoryAccumulation);
}
else if (budget.Type == BudgetPeriodType.Year)
{
// 年度硬性预算按天数比例累加
var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365;
var daysElapsed = now.DayOfYear;
mandatoryAccumulation = budget.Limit * daysElapsed / daysInYear;
accumulationAlgorithm = $"年度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInYear} = {mandatoryAccumulation:F2}";
logger.LogDebug("年度硬性预算 {BudgetName}: 限额={Limit}, 本年天数={DaysInYear}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
budget.Name, budget.Limit, daysInYear, daysElapsed, mandatoryAccumulation);
}
logger.LogInformation("硬性预算 {BudgetName} (ID={BudgetId}) 应累加值计算: 算法={Algorithm}",
budget.Name, budget.Id, accumulationAlgorithm);
// 如果趋势数据中的累计值小于硬性预算的应累加值,使用硬性预算的值
if (adjustedTotal < mandatoryAccumulation)
{
var adjustmentAmount = mandatoryAccumulation - adjustedTotal;
logger.LogInformation("硬性预算 {BudgetName} (ID={BudgetId}) 触发调整: 调整前总计={BeforeTotal}, 应累加值={MandatoryAccumulation}, 调整金额={AdjustmentAmount}, 调整后总计={AfterTotal}",
budget.Name, budget.Id, adjustedTotal, mandatoryAccumulation, adjustmentAmount, mandatoryAccumulation);
adjustedTotal = mandatoryAccumulation;
}
else
{
logger.LogDebug("硬性预算 {BudgetName} (ID={BudgetId}) 未触发调整: 当前总计={CurrentTotal} >= 应累加值={MandatoryAccumulation}",
budget.Name, budget.Id, adjustedTotal, mandatoryAccumulation);
}
}
else
{
logger.LogInformation("硬性预算 {BudgetName} (ID={BudgetId}) 不在当前统计周期,跳过调整", budget.Name, budget.Id);
}
}
logger.LogDebug("硬性预算调整完成: 最终总计={AdjustedTotal}", adjustedTotal);
return adjustedTotal;
}
private async Task<decimal> CalculateCurrentAmountAsync(BudgetStatsItem budget, BudgetPeriodType statType, DateTime referenceDate)
{
var (startDate, endDate) = GetStatPeriodRange(statType, referenceDate);
// 创建临时的BudgetRecord用于查询
var tempRecord = new BudgetRecord
{
Id = budget.Id,
Name = budget.Name,
Type = budget.Type,
Limit = budget.Limit,
Category = budget.Category,
SelectedCategories = string.Join(",", budget.SelectedCategories),
StartDate = referenceDate,
NoLimit = budget.NoLimit,
IsMandatoryExpense = budget.IsMandatoryExpense
};
// 获取预算的实际时间段
var (budgetStart, budgetEnd) = BudgetService.GetPeriodRange(tempRecord.StartDate, tempRecord.Type, referenceDate);
// 确保在统计时间段内
if (budgetEnd < startDate || budgetStart > endDate)
{
return 0;
}
var actualAmount = await budgetRepository.GetCurrentAmountAsync(tempRecord, startDate, endDate);
// 硬性预算的特殊处理
if (actualAmount == 0 && budget.IsMandatoryExpense)
{
if (budget.Type == BudgetPeriodType.Month)
{
var daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month);
var daysElapsed = dateTimeProvider.Now.Day;
actualAmount = budget.Limit * daysElapsed / daysInMonth;
}
else if (budget.Type == BudgetPeriodType.Year)
{
var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365;
var daysElapsed = dateTimeProvider.Now.DayOfYear;
actualAmount = budget.Limit * daysElapsed / daysInYear;
}
}
return actualAmount;
}
private (DateTime start, DateTime end) GetStatPeriodRange(BudgetPeriodType statType, DateTime referenceDate)
{
if (statType == BudgetPeriodType.Month)
{
var start = new DateTime(referenceDate.Year, referenceDate.Month, 1);
var end = start.AddMonths(1).AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);
return (start, end);
}
else // Year
{
var start = new DateTime(referenceDate.Year, 1, 1);
var end = new DateTime(referenceDate.Year, 12, 31).AddHours(23).AddMinutes(59).AddSeconds(59);
return (start, end);
}
}
private class BudgetStatsItem
{
public long Id { get; set; }
public string Name { get; set; } = string.Empty;
public BudgetPeriodType Type { get; set; }
public decimal Limit { get; set; }
public decimal Current { get; set; }
public BudgetCategory Category { get; set; }
public string[] SelectedCategories { get; set; } = [];
public bool NoLimit { get; set; }
public bool IsMandatoryExpense { get; set; }
public bool IsArchive { get; set; }
public int ArchiveMonth { get; set; } // 归档月份1-12用于标识归档数据来自哪个月
public int RemainingMonths { get; set; } // 剩余月份数,用于年度统计时的月度预算折算
}
}