测试覆盖率
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 27s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 27s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
@@ -82,10 +82,8 @@ public class BudgetStatsService(
|
||||
// 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);
|
||||
totalLimit += itemLimit;
|
||||
}
|
||||
@@ -94,7 +92,7 @@ public class BudgetStatsService(
|
||||
|
||||
// 3. 计算当前实际值
|
||||
// 使用 GetAllBudgetsWithArchiveAsync 中已经计算好的 Current 值
|
||||
decimal totalCurrent = budgets.Sum(b => b.Current);
|
||||
var totalCurrent = budgets.Sum(b => b.Current);
|
||||
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", totalCurrent, budgets.Count);
|
||||
|
||||
var transactionType = category switch
|
||||
@@ -124,8 +122,7 @@ public class BudgetStatsService(
|
||||
startDate,
|
||||
endDate,
|
||||
transactionType,
|
||||
allClassifies,
|
||||
false);
|
||||
allClassifies);
|
||||
logger.LogDebug("获取到 {DayCount} 天的交易数据", dailyStats.Count);
|
||||
|
||||
// 计算累计值(用于趋势图)
|
||||
@@ -133,7 +130,7 @@ public class BudgetStatsService(
|
||||
var daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month);
|
||||
logger.LogDebug("本月天数: {DaysInMonth}", daysInMonth);
|
||||
|
||||
for (int i = 1; i <= daysInMonth; i++)
|
||||
for (var i = 1; i <= daysInMonth; i++)
|
||||
{
|
||||
var currentDate = new DateTime(startDate.Year, startDate.Month, i);
|
||||
if (currentDate.Date > now.Date)
|
||||
@@ -162,68 +159,9 @@ public class BudgetStatsService(
|
||||
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
|
||||
|
||||
// 检查是否为当前月份
|
||||
bool isCurrentMonth = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
|
||||
var isCurrentMonth = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
|
||||
if (isCurrentMonth && currentDate.Date <= now.Date)
|
||||
{
|
||||
// 对于每个硬性预算,计算其虚拟消耗并累加
|
||||
foreach (var budget in mandatoryBudgets)
|
||||
{
|
||||
decimal mandatoryDailyAmount = 0;
|
||||
|
||||
if (budget.Type == BudgetPeriodType.Month)
|
||||
{
|
||||
// 月度硬性预算按当天的天数比例
|
||||
mandatoryDailyAmount = budget.Limit * i / daysInMonth;
|
||||
}
|
||||
else if (budget.Type == BudgetPeriodType.Year)
|
||||
{
|
||||
// 年度硬性预算按当天的天数比例
|
||||
var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365;
|
||||
var dayOfYear = currentDate.DayOfYear;
|
||||
mandatoryDailyAmount = budget.Limit * dayOfYear / daysInYear;
|
||||
}
|
||||
|
||||
// 检查该硬性预算当天是否有实际交易记录
|
||||
// 如果budget.Current为0或很小,说明没有实际交易,需要加上虚拟消耗
|
||||
// 简化处理:直接检查如果这个硬性预算的实际值小于应有值,就补上差额
|
||||
var expectedTotal = mandatoryDailyAmount;
|
||||
|
||||
// 获取这个硬性预算对应的实际交易累计(从accumulated中无法单独提取)
|
||||
// 简化方案:直接添加硬性预算的虚拟值,让其累加到实际支出上
|
||||
// 但这样会重复计算有交易记录的硬性预算
|
||||
|
||||
// 更好的方案:只在硬性预算没有实际交易时才添加虚拟值
|
||||
// 由于budget.Current已经包含了虚拟消耗(在CalculateCurrentAmountAsync中处理)
|
||||
// 我们需要知道是否有实际交易
|
||||
|
||||
// 最简单的方案:如果budget.Current等于虚拟值,说明没有实际交易,累加虚拟值
|
||||
// 但这在趋势计算中无法判断每一天的情况
|
||||
|
||||
// 实际上,正确的做法是:
|
||||
// 1. dailyStats 只包含实际交易
|
||||
// 2. 对于硬性预算,如果它没有实际交易,需要补充虚拟消耗
|
||||
// 3. 判断方法:比较当天该预算应有的虚拟值和实际累计值
|
||||
|
||||
// 由于我们无法在这里区分某个特定预算的交易,
|
||||
// 使用简化方案:总的实际交易 + 总的硬性预算虚拟消耗的差额
|
||||
}
|
||||
|
||||
// 简化实现:计算所有硬性预算的总虚拟消耗
|
||||
decimal totalMandatoryVirtual = 0;
|
||||
foreach (var budget in mandatoryBudgets)
|
||||
{
|
||||
if (budget.Type == BudgetPeriodType.Month)
|
||||
{
|
||||
totalMandatoryVirtual += budget.Limit * i / daysInMonth;
|
||||
}
|
||||
else if (budget.Type == BudgetPeriodType.Year)
|
||||
{
|
||||
var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365;
|
||||
var dayOfYear = currentDate.DayOfYear;
|
||||
totalMandatoryVirtual += budget.Limit * dayOfYear / daysInYear;
|
||||
}
|
||||
}
|
||||
|
||||
// 关键:accumulated是所有预算的实际交易累计(不包含虚拟消耗)
|
||||
// totalMandatoryVirtual是所有硬性预算的虚拟消耗
|
||||
// 但如果硬性预算有实际交易,accumulated中已经包含了,会重复
|
||||
@@ -237,10 +175,7 @@ public class BudgetStatsService(
|
||||
|
||||
// 由于无法精确区分,采用近似方案:
|
||||
// 计算所有硬性预算的Current总和,这个值已经包含了虚拟消耗(在CalculateCurrentAmountAsync中处理)
|
||||
decimal totalMandatoryCurrent = budgets
|
||||
.Where(b => b.IsMandatoryExpense)
|
||||
.Sum(b => b.Current);
|
||||
|
||||
|
||||
// 计算非硬性预算的交易累计(这部分在accumulated中)
|
||||
// 但accumulated是所有交易的累计,包括硬性预算的实际交易
|
||||
|
||||
@@ -276,7 +211,7 @@ public class BudgetStatsService(
|
||||
// 需要判断该预算是否有实际交易记录
|
||||
// 简化:假设如果硬性预算的Current等于虚拟值(误差<1元),就没有实际交易
|
||||
|
||||
decimal monthlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
var monthlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
? budget.Limit * now.Day / daysInMonth
|
||||
: budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365);
|
||||
|
||||
@@ -378,7 +313,7 @@ public class BudgetStatsService(
|
||||
// 2. 计算限额总值(考虑不限额预算的特殊处理)
|
||||
logger.LogDebug("开始计算年度限额总值,共 {BudgetCount} 个预算", budgets.Count);
|
||||
decimal totalLimit = 0;
|
||||
int budgetIndex = 0;
|
||||
var budgetIndex = 0;
|
||||
foreach (var budget in budgets)
|
||||
{
|
||||
budgetIndex++;
|
||||
@@ -402,7 +337,7 @@ public class BudgetStatsService(
|
||||
logger.LogDebug("交易类型: {TransactionType}", transactionType);
|
||||
|
||||
// 计算当前实际值,考虑硬性预算的特殊逻辑
|
||||
decimal totalCurrent = budgets.Sum(b => b.Current);
|
||||
var totalCurrent = budgets.Sum(b => b.Current);
|
||||
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)",
|
||||
totalCurrent, budgets.Count);
|
||||
|
||||
@@ -431,7 +366,7 @@ public class BudgetStatsService(
|
||||
|
||||
// 计算累计值(用于趋势图)
|
||||
decimal accumulated = 0;
|
||||
for (int i = 1; i <= 12; i++)
|
||||
for (var i = 1; i <= 12; i++)
|
||||
{
|
||||
var currentMonthDate = new DateTime(startDate.Year, i, 1);
|
||||
|
||||
@@ -461,7 +396,7 @@ public class BudgetStatsService(
|
||||
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
|
||||
|
||||
// 检查是否为当前年份
|
||||
bool isCurrentYear = referenceDate.Year == now.Year;
|
||||
var isCurrentYear = referenceDate.Year == now.Year;
|
||||
if (isCurrentYear && currentMonthDate <= now)
|
||||
{
|
||||
decimal mandatoryAdjustment = 0;
|
||||
@@ -491,7 +426,7 @@ public class BudgetStatsService(
|
||||
}
|
||||
|
||||
// 判断该硬性预算是否有实际交易
|
||||
decimal yearlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
var yearlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
? budget.Limit * now.Month + (budget.Limit * now.Day / DateTime.DaysInMonth(now.Year, now.Month)) - budget.Limit
|
||||
: budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365);
|
||||
|
||||
@@ -596,7 +531,7 @@ public class BudgetStatsService(
|
||||
if (referenceDate.Year == now.Year && now.Month > 1)
|
||||
{
|
||||
logger.LogDebug("开始处理历史归档月份: 1月到{Month}月", now.Month - 1);
|
||||
for (int m = 1; m < now.Month; m++)
|
||||
for (var m = 1; m < now.Month; m++)
|
||||
{
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(year, m);
|
||||
if (archive != null)
|
||||
@@ -627,9 +562,8 @@ public class BudgetStatsService(
|
||||
item.Name, m, item.Limit, item.Actual);
|
||||
}
|
||||
// 对于年度预算,只添加一次
|
||||
else if (item.Type == BudgetPeriodType.Year && !processedBudgetIds.Contains(item.Id))
|
||||
else if (item.Type == BudgetPeriodType.Year && processedBudgetIds.Add(item.Id))
|
||||
{
|
||||
processedBudgetIds.Add(item.Id);
|
||||
result.Add(new BudgetStatsItem
|
||||
{
|
||||
Id = item.Id,
|
||||
@@ -707,7 +641,6 @@ public class BudgetStatsService(
|
||||
var remainingMonths = 12 - now.Month;
|
||||
if (remainingMonths > 0)
|
||||
{
|
||||
var futureLimit = budget.Limit * remainingMonths;
|
||||
result.Add(new BudgetStatsItem
|
||||
{
|
||||
Id = budget.Id,
|
||||
@@ -745,7 +678,7 @@ public class BudgetStatsService(
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
int itemCount = archive.Content.Count();
|
||||
var itemCount = archive.Content.Count();
|
||||
logger.LogDebug("找到归档数据,包含 {ItemCount} 个项目", itemCount);
|
||||
foreach (var item in archive.Content)
|
||||
{
|
||||
@@ -779,7 +712,7 @@ public class BudgetStatsService(
|
||||
// 获取当前预算数据
|
||||
logger.LogDebug("开始获取当前预算数据");
|
||||
var budgets = await budgetRepository.GetAllAsync();
|
||||
int budgetCount = budgets.Count();
|
||||
var budgetCount = budgets.Count();
|
||||
logger.LogDebug("获取到 {BudgetCount} 个预算", budgetCount);
|
||||
|
||||
foreach (var budget in budgets)
|
||||
@@ -860,7 +793,7 @@ public class BudgetStatsService(
|
||||
}
|
||||
|
||||
var itemLimit = budget.Limit;
|
||||
string algorithmDescription = $"直接使用原始预算金额: {budget.Limit}";
|
||||
var algorithmDescription = $"直接使用原始预算金额: {budget.Limit}";
|
||||
|
||||
// 年度视图下,月度预算需要折算为年度
|
||||
if (statType == BudgetPeriodType.Year && budget.Type == BudgetPeriodType.Month)
|
||||
@@ -964,12 +897,12 @@ public class BudgetStatsService(
|
||||
|
||||
logger.LogDebug("找到 {MandatoryCount} 个硬性预算", mandatoryBudgets.Count);
|
||||
|
||||
int mandatoryIndex = 0;
|
||||
var mandatoryIndex = 0;
|
||||
foreach (var budget in mandatoryBudgets)
|
||||
{
|
||||
mandatoryIndex++;
|
||||
// 检查是否为当前统计周期
|
||||
var isCurrentPeriod = false;
|
||||
bool isCurrentPeriod;
|
||||
if (statType == BudgetPeriodType.Month)
|
||||
{
|
||||
isCurrentPeriod = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
|
||||
@@ -989,7 +922,7 @@ public class BudgetStatsService(
|
||||
{
|
||||
// 计算硬性预算的应累加值
|
||||
decimal mandatoryAccumulation = 0;
|
||||
string accumulationAlgorithm = "";
|
||||
var accumulationAlgorithm = "";
|
||||
|
||||
if (budget.Type == BudgetPeriodType.Month)
|
||||
{
|
||||
@@ -1039,55 +972,6 @@ public class BudgetStatsService(
|
||||
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)
|
||||
@@ -1104,21 +988,21 @@ public class BudgetStatsService(
|
||||
}
|
||||
}
|
||||
|
||||
private class BudgetStatsItem
|
||||
private record 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 long Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public BudgetPeriodType Type { get; init; }
|
||||
public decimal Limit { get; init; }
|
||||
public decimal Current { get; init; }
|
||||
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; } // 剩余月份数,用于年度统计时的月度预算折算
|
||||
public bool IsCurrentMonth { get; set; } // 标记是否为当前月的预算(用于年度统计中月度预算的计算)
|
||||
public string[] SelectedCategories { get; init; } = [];
|
||||
public bool NoLimit { get; init; }
|
||||
public bool IsMandatoryExpense { get; init; }
|
||||
public bool IsArchive { get; init; }
|
||||
public int ArchiveMonth { get; init; } // 归档月份(1-12),用于标识归档数据来自哪个月
|
||||
public int RemainingMonths { get; init; } // 剩余月份数,用于年度统计时的月度预算折算
|
||||
public bool IsCurrentMonth { get; init; } // 标记是否为当前月的预算(用于年度统计中月度预算的计算)
|
||||
}
|
||||
|
||||
private string GenerateMonthlyDescription(List<BudgetStatsItem> budgets, decimal totalLimit, decimal totalCurrent, DateTime referenceDate, BudgetCategory category)
|
||||
@@ -1189,10 +1073,10 @@ public class BudgetStatsService(
|
||||
var categoryName = category == BudgetCategory.Expense ? "支出" : "收入";
|
||||
|
||||
// 分组:归档的月度预算、归档的年度预算、当前月度预算(剩余月份)、当前年度预算
|
||||
var archivedMonthlyBudgets = budgets.Where(b => b.IsArchive && b.Type == BudgetPeriodType.Month).ToList();
|
||||
var archivedYearlyBudgets = budgets.Where(b => b.IsArchive && b.Type == BudgetPeriodType.Year).ToList();
|
||||
var currentMonthlyBudgets = budgets.Where(b => !b.IsArchive && b.Type == BudgetPeriodType.Month).ToList();
|
||||
var currentYearlyBudgets = budgets.Where(b => !b.IsArchive && b.Type == BudgetPeriodType.Year).ToList();
|
||||
var archivedMonthlyBudgets = budgets.Where(b => b is { IsArchive: true, Type: BudgetPeriodType.Month }).ToList();
|
||||
var archivedYearlyBudgets = budgets.Where(b => b is { IsArchive: true, Type: BudgetPeriodType.Year }).ToList();
|
||||
var currentMonthlyBudgets = budgets.Where(b => b is { IsArchive: false, Type: BudgetPeriodType.Month }).ToList();
|
||||
var currentYearlyBudgets = budgets.Where(b => b is { IsArchive: false, Type: BudgetPeriodType.Year }).ToList();
|
||||
|
||||
// 归档月度预算明细
|
||||
if (archivedMonthlyBudgets.Any())
|
||||
@@ -1375,8 +1259,8 @@ public class BudgetStatsService(
|
||||
|
||||
// 如果是连续的月份,简化显示为 1~3月
|
||||
Array.Sort(months);
|
||||
bool isContinuous = true;
|
||||
for (int i = 1; i < months.Length; i++)
|
||||
var isContinuous = true;
|
||||
for (var i = 1; i < months.Length; i++)
|
||||
{
|
||||
if (months[i] != months[i - 1] + 1)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user