940 lines
45 KiB
C#
940 lines
45 KiB
C#
|
|
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; } // 剩余月份数,用于年度统计时的月度预算折算
|
|||
|
|
}
|
|||
|
|
}
|