fix
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 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s

This commit is contained in:
SunCheng
2026-01-30 10:41:19 +08:00
parent d9703d31ae
commit 704f58b1a1
46 changed files with 6074 additions and 301 deletions

View File

@@ -21,7 +21,7 @@ public class BudgetStatsService(
{
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{
logger.LogInformation("开始计算分类统计信息: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
logger.LogInformation("开始计算分类统计信息: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
category, referenceDate);
var result = new BudgetCategoryStats();
@@ -31,20 +31,20 @@ public class BudgetStatsService(
// 获取月度统计
logger.LogDebug("开始计算月度统计");
result.Month = await CalculateMonthlyCategoryStatsAsync(category, referenceDate);
logger.LogInformation("月度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%",
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}%",
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}",
logger.LogError(ex, "计算分类统计信息时发生错误: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
category, referenceDate);
throw;
}
@@ -54,7 +54,7 @@ public class BudgetStatsService(
private async Task<BudgetStatsDto> CalculateMonthlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{
logger.LogDebug("开始计算月度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM}",
logger.LogDebug("开始计算月度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM}",
category, referenceDate);
var result = new BudgetStatsDto
@@ -94,7 +94,7 @@ public class BudgetStatsService(
// 使用 GetAllBudgetsWithArchiveAsync 中已经计算好的 Current 值
var totalCurrent = budgets.Sum(b => b.Current);
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", totalCurrent, budgets.Count);
var transactionType = category switch
{
BudgetCategory.Expense => TransactionType.Expense,
@@ -129,7 +129,7 @@ public class BudgetStatsService(
decimal accumulated = 0;
var daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month);
logger.LogDebug("本月天数: {DaysInMonth}", daysInMonth);
for (var i = 1; i <= daysInMonth; i++)
{
var currentDate = new DateTime(startDate.Year, startDate.Month, i);
@@ -143,21 +143,21 @@ public class BudgetStatsService(
if (dailyStats.TryGetValue(currentDate.Date, out var amount))
{
accumulated += amount;
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 金额={Amount}, 累计={Accumulated}",
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 金额={Amount}, 累计={Accumulated}",
currentDate, amount, accumulated);
}
else
{
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 无交易数据,累计={Accumulated}",
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 无交易数据,累计={Accumulated}",
currentDate, accumulated);
}
// 对每一天的累计值应用硬性预算调整
var adjustedAccumulated = accumulated;
if (transactionType == TransactionType.Expense)
{
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
// 检查是否为当前月份
var isCurrentMonth = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
if (isCurrentMonth && currentDate.Date <= now.Date)
@@ -166,31 +166,31 @@ public class BudgetStatsService(
// totalMandatoryVirtual是所有硬性预算的虚拟消耗
// 但如果硬性预算有实际交易accumulated中已经包含了会重复
// 所以需要accumulated + (totalMandatoryVirtual - 硬性预算的实际交易部分)
// 更简单的理解:
// - 如果某个硬性预算本月完全没有交易记录它的虚拟值应该加到accumulated上
// - 如果某个硬性预算有部分交易记录,应该补齐到虚拟值
// - 实现:取 max(accumulated, totalMandatoryVirtual) 是不对的
// - 正确accumulated + 硬性预算中没有实际交易的那部分的虚拟值
// 由于无法精确区分,采用近似方案:
// 计算所有硬性预算的Current总和这个值已经包含了虚拟消耗在CalculateCurrentAmountAsync中处理
// 计算非硬性预算的交易累计这部分在accumulated中
// 但accumulated是所有交易的累计包括硬性预算的实际交易
// 最终简化方案:
// dailyStats包含所有实际交易包括硬性预算的实际交易
// 对于没有实际交易的硬性预算它们的虚拟消耗没有在dailyStats中
// 所以adjustedAccumulated = accumulated + 没有实际交易的硬性预算的虚拟消耗
// 实用方法:每个硬性预算,取 max(它在dailyStats中的累计, 虚拟值)
// 但我们无法从dailyStats中提取单个预算的数据
// 终极简化如果硬性预算的Current值等于虚拟值说明没有实际交易
// 这种情况下accumulated中不包含这部分需要加上虚拟值
// 如果Current值大于虚拟值说明有实际交易accumulated中已包含不需要调整
decimal mandatoryAdjustment = 0;
foreach (var budget in mandatoryBudgets)
{
@@ -205,16 +205,16 @@ public class BudgetStatsService(
var dayOfYear = currentDate.DayOfYear;
dailyVirtual = budget.Limit * dayOfYear / daysInYear;
}
// 如果budget.Current约等于整月的虚拟值说明没有实际交易
// 但Current是整个月的dailyVirtual是到当前天的
// 需要判断该预算是否有实际交易记录
// 简化假设如果硬性预算的Current等于虚拟值误差<1元就没有实际交易
var 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);
if (Math.Abs(budget.Current - monthlyVirtual) < 1)
{
// 没有实际交易,需要添加虚拟消耗
@@ -223,14 +223,14 @@ public class BudgetStatsService(
currentDate, budget.Name, dailyVirtual);
}
}
adjustedAccumulated += mandatoryAdjustment;
}
}
result.Trend.Add(adjustedAccumulated);
}
logger.LogDebug("趋势图数据计算完成(已应用硬性预算调整)");
}
else
@@ -265,7 +265,7 @@ public class BudgetStatsService(
logger.LogInformation("【使用率】{Current}元 ÷ {Limit}元 × 100% = {Rate:F2}%", result.Current, totalLimit, result.Rate);
// 5. 生成预算明细汇总日志
var budgetDetails = budgets.Select(b =>
var budgetDetails = budgets.Select(b =>
{
var limit = CalculateBudgetLimit(b, BudgetPeriodType.Month, referenceDate);
var prefix = b.IsArchive ? $"({b.ArchiveMonth}月归档)" : "";
@@ -285,7 +285,7 @@ public class BudgetStatsService(
private async Task<BudgetStatsDto> CalculateYearlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{
logger.LogDebug("开始计算年度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy}",
logger.LogDebug("开始计算年度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy}",
category, referenceDate);
var result = new BudgetStatsDto
@@ -338,7 +338,7 @@ public class BudgetStatsService(
// 计算当前实际值,考虑硬性预算的特殊逻辑
var totalCurrent = budgets.Sum(b => b.Current);
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)",
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)",
totalCurrent, budgets.Count);
var now = dateTimeProvider.Now;
@@ -380,21 +380,21 @@ public class BudgetStatsService(
if (dailyStats.TryGetValue(currentMonthDate, out var amount))
{
accumulated += amount;
logger.LogTrace("月份 {Month:yyyy-MM}: 金额={Amount}, 累计={Accumulated}",
logger.LogTrace("月份 {Month:yyyy-MM}: 金额={Amount}, 累计={Accumulated}",
currentMonthDate, amount, accumulated);
}
else
{
logger.LogTrace("月份 {Month:yyyy-MM}: 无交易数据,累计={Accumulated}",
logger.LogTrace("月份 {Month:yyyy-MM}: 无交易数据,累计={Accumulated}",
currentMonthDate, accumulated);
}
// 对每个月的累计值应用硬性预算调整
var adjustedAccumulated = accumulated;
if (transactionType == TransactionType.Expense)
{
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
// 检查是否为当前年份
var isCurrentYear = referenceDate.Year == now.Year;
if (isCurrentYear && currentMonthDate <= now)
@@ -403,7 +403,7 @@ public class BudgetStatsService(
foreach (var budget in mandatoryBudgets)
{
decimal monthlyVirtual = 0;
if (budget.Type == BudgetPeriodType.Month)
{
// 月度硬性预算:如果该月已完成,累加整月;如果是当前月,按天数比例
@@ -424,12 +424,12 @@ public class BudgetStatsService(
var dayOfYear = i < now.Month ? lastDayOfMonth.DayOfYear : now.DayOfYear;
monthlyVirtual = budget.Limit * dayOfYear / daysInYear;
}
// 判断该硬性预算是否有实际交易
var 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);
if (Math.Abs(budget.Current - yearlyVirtual) < 1)
{
// 没有实际交易,需要添加虚拟消耗
@@ -438,11 +438,11 @@ public class BudgetStatsService(
currentMonthDate, budget.Name, monthlyVirtual);
}
}
adjustedAccumulated += mandatoryAdjustment;
}
}
result.Trend.Add(adjustedAccumulated);
}
@@ -480,7 +480,7 @@ public class BudgetStatsService(
logger.LogInformation("【使用率】{Current}元 ÷ {Limit}元 × 100% = {Rate:F2}%", result.Current, totalLimit, result.Rate);
// 5. 生成预算明细汇总日志
var budgetDetails = budgets.Select(b =>
var budgetDetails = budgets.Select(b =>
{
var limit = CalculateBudgetLimit(b, BudgetPeriodType.Year, referenceDate);
var prefix = b.IsArchive ? "(归档)" : b.RemainingMonths > 0 ? $"(剩余{b.RemainingMonths}月)" : "";
@@ -503,7 +503,7 @@ public class BudgetStatsService(
BudgetPeriodType statType,
DateTime referenceDate)
{
logger.LogDebug("开始获取预算数据: Category={Category}, StatType={StatType}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
logger.LogDebug("开始获取预算数据: Category={Category}, StatType={StatType}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
category, statType, referenceDate);
var result = new List<BudgetStatsItem>();
@@ -516,7 +516,7 @@ public class BudgetStatsService(
if (statType == BudgetPeriodType.Year)
{
logger.LogDebug("年度统计:开始获取整年的预算数据");
// 获取当前有效的预算(用于当前月及未来月)
var currentBudgets = await budgetRepository.GetAllAsync();
var currentBudgetsDict = currentBudgets
@@ -526,7 +526,7 @@ public class BudgetStatsService(
// 用于跟踪已处理的预算ID避免重复
var processedBudgetIds = new HashSet<long>();
// 1. 处理历史归档月份1月到当前月-1
if (referenceDate.Year == now.Year && now.Month > 1)
{
@@ -585,7 +585,7 @@ public class BudgetStatsService(
}
}
}
// 2. 处理当前月及未来月(使用当前预算)
logger.LogDebug("开始处理当前及未来月份预算");
foreach (var budget in currentBudgetsDict.Values)
@@ -675,7 +675,7 @@ public class BudgetStatsService(
// 获取归档数据
logger.LogDebug("开始获取归档数据: Year={Year}, Month={Month}", year, month);
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
if (archive != null)
{
var itemCount = archive.Content.Count();
@@ -714,7 +714,7 @@ public class BudgetStatsService(
var budgets = await budgetRepository.GetAllAsync();
var budgetCount = budgets.Count();
logger.LogDebug("获取到 {BudgetCount} 个预算", budgetCount);
foreach (var budget in budgets)
{
if (budget.Category == category && ShouldIncludeBudget(budget, statType))
@@ -845,7 +845,7 @@ public class BudgetStatsService(
logger.LogInformation("预算 {BudgetName} 限额计算: 原始预算={OriginalLimit}, 计算后限额={CalculatedLimit}, 算法: {Algorithm}",
budget.Name, budget.Limit, itemLimit, algorithmDescription);
return itemLimit;
}
@@ -888,13 +888,13 @@ public class BudgetStatsService(
private decimal ApplyMandatoryBudgetAdjustment(List<BudgetStatsItem> budgets, decimal currentTotal, DateTime referenceDate, BudgetPeriodType statType)
{
logger.LogDebug("开始应用硬性预算调整: 当前总计={CurrentTotal}, 统计类型={StatType}, 参考日期={ReferenceDate:yyyy-MM-dd}",
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);
var mandatoryIndex = 0;
@@ -906,14 +906,14 @@ public class BudgetStatsService(
if (statType == BudgetPeriodType.Month)
{
isCurrentPeriod = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查月度周期: 参考月份={RefMonth}, 当前月份={CurrentMonth}, 是否为当前周期={IsCurrent}",
mandatoryIndex, mandatoryBudgets.Count, budget.Name,
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查月度周期: 参考月份={RefMonth}, 当前月份={CurrentMonth}, 是否为当前周期={IsCurrent}",
mandatoryIndex, mandatoryBudgets.Count, budget.Name,
referenceDate.ToString("yyyy-MM"), now.ToString("yyyy-MM"), isCurrentPeriod);
}
else // Year
{
isCurrentPeriod = referenceDate.Year == now.Year;
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查年度周期: 参考年份={RefYear}, 当前年份={CurrentYear}, 是否为当前周期={IsCurrent}",
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查年度周期: 参考年份={RefYear}, 当前年份={CurrentYear}, 是否为当前周期={IsCurrent}",
mandatoryIndex, mandatoryBudgets.Count, budget.Name,
referenceDate.Year, now.Year, isCurrentPeriod);
}
@@ -923,7 +923,7 @@ public class BudgetStatsService(
// 计算硬性预算的应累加值
decimal mandatoryAccumulation = 0;
var accumulationAlgorithm = "";
if (budget.Type == BudgetPeriodType.Month)
{
// 月度硬性预算按天数比例累加
@@ -931,7 +931,7 @@ public class BudgetStatsService(
var daysElapsed = now.Day;
mandatoryAccumulation = budget.Limit * daysElapsed / daysInMonth;
accumulationAlgorithm = $"月度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInMonth} = {mandatoryAccumulation:F2}";
logger.LogDebug("月度硬性预算 {BudgetName}: 限额={Limit}, 本月天数={DaysInMonth}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
logger.LogDebug("月度硬性预算 {BudgetName}: 限额={Limit}, 本月天数={DaysInMonth}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
budget.Name, budget.Limit, daysInMonth, daysElapsed, mandatoryAccumulation);
}
else if (budget.Type == BudgetPeriodType.Year)
@@ -941,7 +941,7 @@ public class BudgetStatsService(
var daysElapsed = now.DayOfYear;
mandatoryAccumulation = budget.Limit * daysElapsed / daysInYear;
accumulationAlgorithm = $"年度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInYear} = {mandatoryAccumulation:F2}";
logger.LogDebug("年度硬性预算 {BudgetName}: 限额={Limit}, 本年天数={DaysInYear}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
logger.LogDebug("年度硬性预算 {BudgetName}: 限额={Limit}, 本年天数={DaysInYear}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
budget.Name, budget.Limit, daysInYear, daysElapsed, mandatoryAccumulation);
}
@@ -958,7 +958,7 @@ public class BudgetStatsService(
}
else
{
logger.LogDebug("硬性预算 {BudgetName} 未触发调整: 当前总计={CurrentTotal} >= 应累加值={MandatoryAccumulation}",
logger.LogDebug("硬性预算 {BudgetName} 未触发调整: 当前总计={CurrentTotal} >= 应累加值={MandatoryAccumulation}",
budget.Name, adjustedTotal, mandatoryAccumulation);
}
}
@@ -1029,7 +1029,7 @@ public class BudgetStatsService(
var budgetLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Month, referenceDate);
var typeLabel = budget.IsMandatoryExpense ? "硬性" : "普通";
var archiveLabel = budget.IsArchive ? $" ({budget.ArchiveMonth}月归档)" : "";
description.AppendLine($"""
<tr>
<td>{budget.Name}{archiveLabel}</td>
@@ -1138,8 +1138,8 @@ public class BudgetStatsService(
{
var budgetLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Year, referenceDate);
var typeStr = budget.IsCurrentMonth ? "当前月" : "未来月";
var calcStr = budget.IsCurrentMonth
? $"1月×{budget.Limit:N0}"
var calcStr = budget.IsCurrentMonth
? $"1月×{budget.Limit:N0}"
: $"{budget.RemainingMonths}月×{budget.Limit:N0}";
description.AppendLine($"""
@@ -1195,7 +1195,7 @@ public class BudgetStatsService(
description.AppendLine($"<h3>计算公式</h3>");
description.AppendLine($"<p><strong>年度预算额度合计:</strong>");
var limitParts = new List<string>();
// 归档月度预算部分
foreach (var group in archivedMonthlyBudgets.GroupBy(b => b.Id))
{
@@ -1204,7 +1204,7 @@ public class BudgetStatsService(
var groupTotalLimit = first.Limit * count;
limitParts.Add($"{first.Name}(归档{count}月×{first.Limit:N0}={groupTotalLimit:N0})");
}
// 当前月度预算部分
foreach (var budget in currentMonthlyBudgets)
{
@@ -1218,18 +1218,18 @@ public class BudgetStatsService(
limitParts.Add($"{budget.Name}(剩余{budget.RemainingMonths}月×{budget.Limit:N0}={budgetLimit:N0})");
}
}
// 年度预算部分
foreach (var budget in archivedYearlyBudgets.Concat(currentYearlyBudgets))
{
limitParts.Add($"{budget.Name}({budget.Limit:N0})");
}
description.AppendLine($"{string.Join(" + ", limitParts)} = <span class='{(category == BudgetCategory.Expense ? "expense-value" : "income-value")}'><strong>{totalLimit:N0}</strong></span></p>");
description.AppendLine($"<p><strong>实际{categoryName}合计:</strong>");
var currentParts = new List<string>();
// 归档月度预算的实际值
foreach (var group in archivedMonthlyBudgets.GroupBy(b => b.Id))
{
@@ -1237,13 +1237,13 @@ public class BudgetStatsService(
var groupTotalCurrent = group.Sum(b => b.Current);
currentParts.Add($"{first.Name}(归档{groupTotalCurrent:N1})");
}
// 年度预算的实际值
foreach (var budget in archivedYearlyBudgets.Concat(currentYearlyBudgets))
{
currentParts.Add($"{budget.Name}({budget.Current:N1})");
}
description.AppendLine($"{string.Join(" + ", currentParts)} = <span class='{(category == BudgetCategory.Expense ? "expense-value" : "income-value")}'><strong>{totalCurrent:N1}</strong></span></p>");
var rate = totalLimit > 0 ? totalCurrent / totalLimit * 100 : 0;