添加动态目标
All checks were successful
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 10s

This commit is contained in:
孙诚
2026-01-15 20:00:41 +08:00
parent caf6f3fe60
commit f4f1600782
7 changed files with 237 additions and 12 deletions

View File

@@ -19,6 +19,11 @@ public interface IBudgetService
Task<string?> GetArchiveSummaryAsync(int year, int month);
Task UpdateArchiveSummaryAsync(int year, int month, string? summary);
/// <summary>
/// 获取指定周期的存款预算信息
/// </summary>
Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type);
}
public class BudgetService(
@@ -86,6 +91,12 @@ public class BudgetService(
return dtos.Where(dto => dto != null).Cast<BudgetResult>().ToList();
}
public async Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
{
var referenceDate = new DateTime(year, month, 1);
return await GetVirtualSavingsDtoAsync(type, referenceDate);
}
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{
var budgets = await GetListAsync(referenceDate);
@@ -204,13 +215,14 @@ public class BudgetService(
totalLimit += itemLimit;
// 当前值累加
var selectedCategories = budget.SelectedCategories != null ? string.Join(',', budget.SelectedCategories) : string.Empty;
var currentAmount = await CalculateCurrentAmountAsync(new()
{
Name = budget.Name,
Type = budget.Type,
Limit = budget.Limit,
Category = budget.Category,
SelectedCategories = string.Join(',', budget.SelectedCategories),
SelectedCategories = selectedCategories,
StartDate = new DateTime(referenceDate.Year, referenceDate.Month, 1)
}, referenceDate);
if (budget.Type == statType)
@@ -242,6 +254,14 @@ public class BudgetService(
var budgets = await GetListAsync(referenceDate);
var expenseSurplus = budgets
.Where(b => b.Category == BudgetCategory.Expense && !b.NoLimit && b.Type == BudgetPeriodType.Month)
.Sum(b => b.Limit - b.Current);
var incomeSurplus = budgets
.Where(b => b.Category == BudgetCategory.Income && !b.NoLimit && b.Type == BudgetPeriodType.Month)
.Sum(b => b.Current - b.Limit);
var content = budgets.Select(b => new BudgetArchiveContent
{
Name = b.Name,
@@ -260,6 +280,8 @@ public class BudgetService(
{
archive.Content = content;
archive.ArchiveDate = DateTime.Now;
archive.ExpenseSurplus = expenseSurplus;
archive.IncomeSurplus = incomeSurplus;
if (!await budgetArchiveRepository.UpdateAsync(archive))
{
return "更新预算归档失败";
@@ -272,7 +294,9 @@ public class BudgetService(
Year = year,
Month = month,
Content = content,
ArchiveDate = DateTime.Now
ArchiveDate = DateTime.Now,
ExpenseSurplus = expenseSurplus,
IncomeSurplus = incomeSurplus
};
if (!await budgetArchiveRepository.AddAsync(archive))
@@ -578,10 +602,8 @@ public class BudgetService(
}
description.Append($"<p>收入合计: <span class='income-value'><strong>{incomeLimitAtPeriod:N0}</strong></span></p>");
// 新增:显示不记额收入明细
description.Append("<h3>不记额收入明细</h3>");
if (noLimitIncomeItems.Count == 0) description.Append("<p>无不记额收入</p>");
else
if (noLimitIncomeItems.Count > 0)
{
description.Append("""
<table>
@@ -637,11 +659,10 @@ public class BudgetService(
}
description.Append($"<p>支出合计: <span class='expense-value'><strong>{expenseLimitAtPeriod:N0}</strong></span></p>");
// 新增:显示不记额支出明细
description.Append("<h3>不记额支出明细</h3>");
if (noLimitExpenseItems.Count == 0) description.Append("<p>无不记额支出</p>");
else
if (noLimitExpenseItems.Count > 0)
{
description.Append("<h3>不记额支出明细</h3>");
description.Append("""
<table>
<thead>
@@ -663,6 +684,7 @@ public class BudgetService(
}
description.Append("</tbody></table>");
}
description.Append($"<p>不记额支出合计: <span class='expense-value'><strong>{noLimitExpenseAtPeriod:N0}</strong></span></p>");
description.Append("<h3>存款计划结论</h3>");
@@ -671,12 +693,70 @@ public class BudgetService(
var totalExpense = expenseLimitAtPeriod + noLimitExpenseAtPeriod;
description.Append($"<p>计划收入 = 预算 <span class='income-value'>{incomeLimitAtPeriod:N0}</span> + 不记额 <span class='income-value'>{noLimitIncomeAtPeriod:N0}</span> = <span class='income-value'><strong>{totalIncome:N0}</strong></span></p>");
description.Append($"<p>计划支出 = 预算 <span class='expense-value'>{expenseLimitAtPeriod:N0}</span> + 不记额 <span class='expense-value'>{noLimitExpenseAtPeriod:N0}</span> = <span class='expense-value'><strong>{totalExpense:N0}</strong></span></p>");
description.Append($"<p>最终目标:<span class='highlight'><strong>{totalIncome - totalExpense:N0}</strong></span></p>");
decimal historicalSurplus = 0;
if (periodType == BudgetPeriodType.Year)
{
var archives = await budgetArchiveRepository.GetArchivesByYearAsync(date.Year);
if (archives.Count > 0)
{
var expenseSurplus = archives.Sum(a => a.ExpenseSurplus);
var incomeSurplus = archives.Sum(a => a.IncomeSurplus);
historicalSurplus = expenseSurplus + incomeSurplus;
description.Append("<h3>历史月份盈亏</h3>");
description.Append("""
<table>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
""");
foreach (var archive in archives)
{
var monthlyTotal = archive.ExpenseSurplus + archive.IncomeSurplus;
var monthlyClass = monthlyTotal >= 0 ? "income-value" : "expense-value";
description.Append($"""
<tr>
<td>{archive.Month}月</td>
<td><span class='{(archive.ExpenseSurplus >= 0 ? "income-value" : "expense-value")}'>{archive.ExpenseSurplus:N0}</span></td>
<td><span class='{(archive.IncomeSurplus >= 0 ? "income-value" : "expense-value")}'>{archive.IncomeSurplus:N0}</span></td>
<td><span class='{monthlyClass}'>{monthlyTotal:N0}</span></td>
</tr>
""");
}
var totalClass = historicalSurplus >= 0 ? "income-value" : "expense-value";
description.Append($"""
<tr>
<td><strong>汇总</strong></td>
<td><span class='{(expenseSurplus >= 0 ? "income-value" : "expense-value")}'><strong>{expenseSurplus:N0}</strong></span></td>
<td><span class='{(incomeSurplus >= 0 ? "income-value" : "expense-value")}'><strong>{incomeSurplus:N0}</strong></span></td>
<td><span class='{totalClass}'><strong>{historicalSurplus:N0}</strong></span></td>
</tr>
</tbody></table>
""");
}
var finalGoal = totalIncome - totalExpense + historicalSurplus;
description.Append($"<p>动态目标 = 计划盈余 <span class='highlight'>{totalIncome - totalExpense:N0}</span> + 历史盈亏 <span class='highlight'>{historicalSurplus:N0}</span> = <span class='highlight'><strong>{finalGoal:N0}</strong></span></p>");
}
else
{
description.Append($"<p>最终目标:<span class='highlight'><strong>{totalIncome - totalExpense:N0}</strong></span></p>");
}
var finalLimit = periodType == BudgetPeriodType.Year ? (totalIncome - totalExpense + historicalSurplus) : (totalIncome - totalExpense);
var virtualBudget = await BuildVirtualSavingsBudgetRecordAsync(
periodType == BudgetPeriodType.Year ? -1 : -2,
date,
totalIncome - totalExpense); // 修改:使用总金额
finalLimit);
// 计算实际发生的 收入 - 支出
var current = await CalculateCurrentAmountAsync(new BudgetRecord