2026-01-28 11:19:23 +08:00
|
|
|
|
using Service.Transaction;
|
|
|
|
|
|
|
2026-01-19 13:39:59 +08:00
|
|
|
|
namespace Service.Budget;
|
|
|
|
|
|
|
|
|
|
|
|
public interface IBudgetSavingsService
|
|
|
|
|
|
{
|
|
|
|
|
|
Task<BudgetResult> GetSavingsDtoAsync(
|
|
|
|
|
|
BudgetPeriodType periodType,
|
|
|
|
|
|
DateTime? referenceDate = null,
|
|
|
|
|
|
IEnumerable<BudgetRecord>? existingBudgets = null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class BudgetSavingsService(
|
|
|
|
|
|
IBudgetRepository budgetRepository,
|
2026-01-20 19:11:05 +08:00
|
|
|
|
IBudgetArchiveRepository budgetArchiveRepository,
|
2026-01-28 10:58:15 +08:00
|
|
|
|
ITransactionStatisticsService transactionStatisticsService,
|
2026-01-20 19:11:05 +08:00
|
|
|
|
IConfigService configService,
|
|
|
|
|
|
IDateTimeProvider dateTimeProvider
|
2026-01-19 13:39:59 +08:00
|
|
|
|
) : IBudgetSavingsService
|
|
|
|
|
|
{
|
|
|
|
|
|
public async Task<BudgetResult> GetSavingsDtoAsync(
|
|
|
|
|
|
BudgetPeriodType periodType,
|
|
|
|
|
|
DateTime? referenceDate = null,
|
|
|
|
|
|
IEnumerable<BudgetRecord>? existingBudgets = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var budgets = existingBudgets;
|
|
|
|
|
|
|
|
|
|
|
|
if (existingBudgets == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
budgets = await budgetRepository.GetAllAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (budgets == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException("No budgets found.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
budgets = budgets
|
|
|
|
|
|
// 排序顺序 1.硬性预算 2.月度->年度 3.实际金额倒叙
|
|
|
|
|
|
.OrderBy(b => b.IsMandatoryExpense)
|
|
|
|
|
|
.ThenBy(b => b.Type)
|
|
|
|
|
|
.ThenByDescending(b => b.Limit);
|
|
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
var year = referenceDate?.Year ?? dateTimeProvider.Now.Year;
|
|
|
|
|
|
var month = referenceDate?.Month ?? dateTimeProvider.Now.Month;
|
|
|
|
|
|
|
|
|
|
|
|
if (periodType == BudgetPeriodType.Month)
|
2026-01-19 13:39:59 +08:00
|
|
|
|
{
|
2026-01-20 19:11:05 +08:00
|
|
|
|
return await GetForMonthAsync(budgets, year, month);
|
2026-01-19 13:39:59 +08:00
|
|
|
|
}
|
2026-01-20 19:11:05 +08:00
|
|
|
|
else if (periodType == BudgetPeriodType.Year)
|
2026-01-19 13:39:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
return await GetForYearAsync(budgets, year);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new NotSupportedException($"Period type {periodType} is not supported.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<BudgetResult> GetForMonthAsync(
|
|
|
|
|
|
IEnumerable<BudgetRecord> budgets,
|
2026-01-20 19:11:05 +08:00
|
|
|
|
int year,
|
2026-01-19 13:39:59 +08:00
|
|
|
|
int month)
|
|
|
|
|
|
{
|
2026-01-28 10:58:15 +08:00
|
|
|
|
var transactionClassify = await transactionStatisticsService.GetAmountGroupByClassifyAsync(
|
2026-01-20 19:11:05 +08:00
|
|
|
|
new DateTime(year, month, 1),
|
|
|
|
|
|
new DateTime(year, month, 1).AddMonths(1)
|
|
|
|
|
|
);
|
2026-01-19 13:39:59 +08:00
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
var monthlyIncomeItems = new List<(string name, decimal limit, decimal current, bool isMandatory)>();
|
|
|
|
|
|
var monthlyExpenseItems = new List<(string name, decimal limit, decimal current, bool isMandatory)>();
|
2026-01-19 13:39:59 +08:00
|
|
|
|
var monthlyBudgets = budgets
|
|
|
|
|
|
.Where(b => b.Type == BudgetPeriodType.Month);
|
2026-01-20 19:11:05 +08:00
|
|
|
|
foreach (var budget in monthlyBudgets)
|
|
|
|
|
|
{
|
|
|
|
|
|
var classifyList = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
|
|
|
|
|
|
decimal currentAmount = 0;
|
|
|
|
|
|
var transactionType = budget.Category switch
|
|
|
|
|
|
{
|
|
|
|
|
|
BudgetCategory.Income => TransactionType.Income,
|
|
|
|
|
|
BudgetCategory.Expense => TransactionType.Expense,
|
|
|
|
|
|
_ => throw new NotSupportedException($"Budget category {budget.Category} is not supported.")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var classify in classifyList)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取分类+收入支出类型一致的金额
|
|
|
|
|
|
if (transactionClassify.TryGetValue((classify, transactionType), out var amount))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentAmount += amount;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 硬性预算 如果实际发生金额小于 应(总天数/实际天数)发生金额
|
|
|
|
|
|
// 直接取应发生金额(为了预算的准确性)
|
|
|
|
|
|
if (budget.IsMandatoryExpense && currentAmount == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
currentAmount = budget.Limit / DateTime.DaysInMonth(year, month) * dateTimeProvider.Now.Day;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (budget.Category == BudgetCategory.Income)
|
|
|
|
|
|
{
|
|
|
|
|
|
monthlyIncomeItems.Add((
|
|
|
|
|
|
name: budget.Name,
|
|
|
|
|
|
limit: budget.Limit,
|
|
|
|
|
|
current: currentAmount,
|
|
|
|
|
|
isMandatory: budget.IsMandatoryExpense
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (budget.Category == BudgetCategory.Expense)
|
|
|
|
|
|
{
|
|
|
|
|
|
monthlyExpenseItems.Add((
|
|
|
|
|
|
name: budget.Name,
|
|
|
|
|
|
limit: budget.Limit,
|
|
|
|
|
|
current: currentAmount,
|
|
|
|
|
|
isMandatory: budget.IsMandatoryExpense
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var yearlyIncomeItems = new List<(string name, decimal limit, decimal current)>();
|
|
|
|
|
|
var yearlyExpenseItems = new List<(string name, decimal limit, decimal current)>();
|
2026-01-19 13:39:59 +08:00
|
|
|
|
var yearlyBudgets = budgets
|
|
|
|
|
|
.Where(b => b.Type == BudgetPeriodType.Year);
|
2026-01-20 19:11:05 +08:00
|
|
|
|
// 只需要考虑实际发生在本月的年度预算 因为他会影响到月度的结余情况
|
|
|
|
|
|
foreach (var budget in yearlyBudgets)
|
|
|
|
|
|
{
|
|
|
|
|
|
var classifyList = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
|
|
|
|
|
|
decimal currentAmount = 0;
|
|
|
|
|
|
var transactionType = budget.Category switch
|
|
|
|
|
|
{
|
|
|
|
|
|
BudgetCategory.Income => TransactionType.Income,
|
|
|
|
|
|
BudgetCategory.Expense => TransactionType.Expense,
|
|
|
|
|
|
_ => throw new NotSupportedException($"Budget category {budget.Category} is not supported.")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var classify in classifyList)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取分类+收入支出类型一致的金额
|
|
|
|
|
|
if (transactionClassify.TryGetValue((classify, transactionType), out var amount))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentAmount += amount;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentAmount == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (budget.Category == BudgetCategory.Income)
|
|
|
|
|
|
{
|
|
|
|
|
|
yearlyIncomeItems.Add((
|
|
|
|
|
|
name: budget.Name,
|
|
|
|
|
|
limit: budget.Limit,
|
|
|
|
|
|
current: currentAmount
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (budget.Category == BudgetCategory.Expense)
|
|
|
|
|
|
{
|
|
|
|
|
|
yearlyExpenseItems.Add((
|
|
|
|
|
|
name: budget.Name,
|
|
|
|
|
|
limit: budget.Limit,
|
|
|
|
|
|
current: currentAmount
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var description = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
|
|
#region 构建月度收入支出明细表格
|
|
|
|
|
|
description.AppendLine("<h3>月度预算收入明细</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>硬性收入</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in monthlyIncomeItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{item.name}</td>
|
|
|
|
|
|
<td>{item.limit:N0}</td>
|
|
|
|
|
|
<td>{(item.isMandatory ? "是" : "否")}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
收入合计:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{monthlyIncomeItems.Sum(item => item.limit):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
|
|
|
|
description.AppendLine("<h3>月度预算支出明细</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>硬性支出</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
foreach (var item in monthlyExpenseItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{item.name}</td>
|
|
|
|
|
|
<td>{item.limit:N0}</td>
|
|
|
|
|
|
<td>{(item.isMandatory ? "是" : "否")}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
支出合计:
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>{monthlyExpenseItems.Sum(item => item.limit):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
#endregion
|
2026-01-19 13:39:59 +08:00
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
#region 构建发生在本月的年度预算收入支出明细表格
|
|
|
|
|
|
if (yearlyIncomeItems.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine("<h3>年度收入预算(发生在本月)</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>本月收入</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in yearlyIncomeItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{item.name}</td>
|
|
|
|
|
|
<td>{(item.limit == 0 ? "不限额" : item.limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td><span class='income-value'>{item.current:N0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
收入合计:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{yearlyIncomeItems.Sum(item => item.current):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (yearlyExpenseItems.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine("<h3>年度支出预算(发生在本月)</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>本月支出</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
foreach (var item in yearlyExpenseItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{item.name}</td>
|
|
|
|
|
|
<td>{(item.limit == 0 ? "不限额" : item.limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td><span class='expense-value'>{item.current:N0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
支出合计:
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>{yearlyExpenseItems.Sum(item => item.current):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 总结
|
|
|
|
|
|
|
|
|
|
|
|
description.AppendLine("<h3>存款计划结论</h3>");
|
|
|
|
|
|
var plannedIncome = monthlyIncomeItems.Sum(item => item.limit == 0 ? item.current : item.limit) + yearlyIncomeItems.Sum(item => item.current);
|
|
|
|
|
|
var plannedExpense = monthlyExpenseItems.Sum(item => item.limit == 0 ? item.current : item.limit) + yearlyExpenseItems.Sum(item => item.current);
|
|
|
|
|
|
var expectedSavings = plannedIncome - plannedExpense;
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
计划存款:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{expectedSavings:N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
=
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
|
|
|
|
|
|
计划收入:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{monthlyIncomeItems.Sum(item => item.limit):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
if (yearlyIncomeItems.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
|
|
|
|
|
|
+ 本月发生的年度预算收入:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{yearlyIncomeItems.Sum(item => item.current):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
|
|
|
|
|
|
- 计划支出:
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>{monthlyExpenseItems.Sum(item => item.limit):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
""");
|
|
|
|
|
|
if (yearlyExpenseItems.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
|
|
|
|
|
|
- 本月发生的年度预算支出:
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>{yearlyExpenseItems.Sum(item => item.current):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
#endregion
|
2026-01-19 13:39:59 +08:00
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
var savingsCategories = await configService.GetConfigByKeyAsync<string>("SavingsCategories") ?? string.Empty;
|
2026-01-20 19:26:36 +08:00
|
|
|
|
var currentActual = 0m;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(savingsCategories))
|
|
|
|
|
|
{
|
2026-01-30 10:41:19 +08:00
|
|
|
|
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
|
|
|
|
|
foreach (var kvp in transactionClassify)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cats.Contains(kvp.Key.Item1))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentActual += kvp.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-20 19:26:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
var record = new BudgetRecord
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = -2,
|
|
|
|
|
|
Name = "月度存款计划",
|
|
|
|
|
|
Type = BudgetPeriodType.Month,
|
|
|
|
|
|
Limit = expectedSavings,
|
|
|
|
|
|
Category = BudgetCategory.Savings,
|
|
|
|
|
|
SelectedCategories = savingsCategories,
|
|
|
|
|
|
StartDate = new DateTime(year, month, 1),
|
|
|
|
|
|
NoLimit = false,
|
|
|
|
|
|
IsMandatoryExpense = false,
|
|
|
|
|
|
CreateTime = dateTimeProvider.Now,
|
|
|
|
|
|
UpdateTime = dateTimeProvider.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return BudgetResult.FromEntity(
|
|
|
|
|
|
record,
|
2026-01-20 19:26:36 +08:00
|
|
|
|
currentActual,
|
2026-01-20 19:11:05 +08:00
|
|
|
|
new DateTime(year, month, 1),
|
|
|
|
|
|
description.ToString()
|
|
|
|
|
|
);
|
2026-01-19 13:39:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<BudgetResult> GetForYearAsync(
|
|
|
|
|
|
IEnumerable<BudgetRecord> budgets,
|
|
|
|
|
|
int year)
|
|
|
|
|
|
{
|
2026-01-20 19:11:05 +08:00
|
|
|
|
// 因为非当前月份的读取归档数据,这边依然是读取当前月份的数据
|
|
|
|
|
|
var currentMonth = dateTimeProvider.Now.Month;
|
2026-01-28 10:58:15 +08:00
|
|
|
|
var transactionClassify = await transactionStatisticsService.GetAmountGroupByClassifyAsync(
|
2026-01-20 19:11:05 +08:00
|
|
|
|
new DateTime(year, currentMonth, 1),
|
|
|
|
|
|
new DateTime(year, currentMonth, 1).AddMonths(1)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
var currentMonthlyIncomeItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>();
|
|
|
|
|
|
var currentYearlyIncomeItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>();
|
|
|
|
|
|
var currentMonthlyExpenseItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>();
|
|
|
|
|
|
var currentYearlyExpenseItems = new List<(long id, string name, decimal limit, int factor, decimal current, bool isMandatory)>();
|
|
|
|
|
|
// 归档的预算收入支出明细
|
|
|
|
|
|
var archiveIncomeItems = new List<(long id, string name, int[] months, decimal limit, decimal current)>();
|
|
|
|
|
|
var archiveExpenseItems = new List<(long id, string name, int[] months, decimal limit, decimal current)>();
|
2026-02-01 10:27:04 +08:00
|
|
|
|
var archiveSavingsItems = new List<(long id, string name, int[] months, decimal limit, decimal current)>();
|
2026-01-20 19:11:05 +08:00
|
|
|
|
// 获取归档数据
|
|
|
|
|
|
var archives = await budgetArchiveRepository.GetArchivesByYearAsync(year);
|
|
|
|
|
|
var archiveBudgetGroups = archives
|
|
|
|
|
|
.SelectMany(a => a.Content.Select(x => (a.Month, Archive: x)))
|
|
|
|
|
|
.Where(b => b.Archive.Type == BudgetPeriodType.Month) // 因为本来就是当前年度预算的生成 ,归档无需关心年度, 以最新地为准即可
|
|
|
|
|
|
.GroupBy(b => (b.Archive.Id, b.Archive.Limit));
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var archiveBudgetGroup in archiveBudgetGroups)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (_, archive) = archiveBudgetGroup.First();
|
|
|
|
|
|
var archiveItems = archive.Category switch
|
|
|
|
|
|
{
|
|
|
|
|
|
BudgetCategory.Income => archiveIncomeItems,
|
|
|
|
|
|
BudgetCategory.Expense => archiveExpenseItems,
|
2026-02-01 10:27:04 +08:00
|
|
|
|
BudgetCategory.Savings => archiveSavingsItems,
|
2026-01-20 19:11:05 +08:00
|
|
|
|
_ => throw new NotSupportedException($"Category {archive.Category} is not supported.")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
archiveItems.Add((
|
|
|
|
|
|
id: archiveBudgetGroup.Key.Id,
|
|
|
|
|
|
name: archive.Name,
|
|
|
|
|
|
months: archiveBudgetGroup.Select(x => x.Month).OrderBy(m => m).ToArray(),
|
|
|
|
|
|
limit: archiveBudgetGroup.Key.Limit,
|
|
|
|
|
|
current: archiveBudgetGroup.Sum(x => x.Archive.Actual)
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理当月最新地没有归档的预算
|
|
|
|
|
|
foreach (var budget in budgets)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentAmount = 0m;
|
|
|
|
|
|
|
|
|
|
|
|
var classifyList = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
|
|
|
|
|
|
var transactionType = budget.Category switch
|
|
|
|
|
|
{
|
|
|
|
|
|
BudgetCategory.Income => TransactionType.Income,
|
|
|
|
|
|
BudgetCategory.Expense => TransactionType.Expense,
|
|
|
|
|
|
_ => throw new NotSupportedException($"Budget category {budget.Category} is not supported.")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var classify in classifyList)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取分类+收入支出类型一致的金额
|
|
|
|
|
|
if (transactionClassify.TryGetValue((classify, transactionType), out var amount))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentAmount += amount;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 硬性预算 如果实际发生金额小于 应(总天数/实际天数)发生金额
|
|
|
|
|
|
// 直接取应发生金额(为了预算的准确性)
|
|
|
|
|
|
if (budget.IsMandatoryExpense && currentAmount == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
currentAmount = budget.IsMandatoryExpense && currentAmount == 0
|
|
|
|
|
|
? budget.Limit / (DateTime.IsLeapYear(year) ? 366 : 365) * dateTimeProvider.Now.DayOfYear
|
|
|
|
|
|
: budget.Limit / DateTime.DaysInMonth(year, currentMonth) * dateTimeProvider.Now.Day;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
AddOrIncCurrentItem(
|
|
|
|
|
|
budget.Id,
|
|
|
|
|
|
budget.Type,
|
|
|
|
|
|
budget.Category,
|
|
|
|
|
|
budget.Name,
|
|
|
|
|
|
budget.Limit,
|
|
|
|
|
|
budget.Type == BudgetPeriodType.Year
|
|
|
|
|
|
? 1
|
|
|
|
|
|
: 12 - currentMonth + 1,
|
|
|
|
|
|
currentAmount,
|
|
|
|
|
|
budget.IsMandatoryExpense
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var description = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
|
|
#region 构建归档收入明细表格
|
|
|
|
|
|
var archiveIncomeDiff = 0m;
|
|
|
|
|
|
if (archiveIncomeItems.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine("<h3>已归档收入明细</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>月</th>
|
|
|
|
|
|
<th>合计</th>
|
|
|
|
|
|
<th>实际</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
// 已归档的收入
|
|
|
|
|
|
foreach (var (_, name, months, limit, current) in archiveIncomeItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{name}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td>{FormatMonths(months)}</td>
|
|
|
|
|
|
<td>{limit * months.Length:N0}</td>
|
|
|
|
|
|
<td><span class='income-value'>{current:N0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
|
|
|
|
|
archiveIncomeDiff = archiveIncomeItems.Sum(i => i.current) - archiveIncomeItems.Sum(i => i.limit * i.months.Length);
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
2026-01-20 19:13:33 +08:00
|
|
|
|
<span class="highlight">已归档收入总结: </span>
|
2026-01-20 19:11:05 +08:00
|
|
|
|
{(archiveIncomeDiff > 0 ? "超额收入" : "未达预期")}:
|
|
|
|
|
|
<span class='{(archiveIncomeDiff > 0 ? "income-value" : "expense-value")}'>
|
|
|
|
|
|
<strong>{archiveIncomeDiff:N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
=
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{archiveIncomeItems.Sum(i => i.limit * i.months.Length):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
-
|
|
|
|
|
|
实际收入合计:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{archiveIncomeItems.Sum(i => i.current):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 构建年度预算收入明细表格
|
|
|
|
|
|
description.AppendLine("<h3>预算收入明细</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>月/年</th>
|
|
|
|
|
|
<th>合计</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
|
|
|
|
// 当前预算
|
|
|
|
|
|
foreach (var (_, name, limit, factor, _, _) in currentMonthlyIncomeItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{name}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td>{FormatMonthsByFactor(factor)}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : (limit * factor).ToString("N0"))}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 年预算
|
|
|
|
|
|
foreach (var (_, name, limit, _, _, _) in currentYearlyIncomeItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{name}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td>{year}年</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
2026-01-30 10:41:19 +08:00
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
预算收入合计:
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>
|
2026-01-30 10:41:19 +08:00
|
|
|
|
{currentMonthlyIncomeItems.Sum(i => i.limit * i.factor)
|
|
|
|
|
|
+ currentYearlyIncomeItems.Sum(i => i.limit):N0}
|
2026-01-20 19:11:05 +08:00
|
|
|
|
</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 构建年度归档支出明细表格
|
|
|
|
|
|
var archiveExpenseDiff = 0m;
|
|
|
|
|
|
if (archiveExpenseItems.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine("<h3>已归档支出明细</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>月</th>
|
|
|
|
|
|
<th>合计</th>
|
|
|
|
|
|
<th>实际</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
|
|
|
|
// 已归档的支出
|
|
|
|
|
|
foreach (var (_, name, months, limit, current) in archiveExpenseItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{name}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td>{FormatMonths(months)}</td>
|
|
|
|
|
|
<td>{limit * months.Length:N0}</td>
|
|
|
|
|
|
<td><span class='expense-value'>{current:N0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
2026-01-30 10:41:19 +08:00
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
archiveExpenseDiff = archiveExpenseItems.Sum(i => i.limit * i.months.Length) - archiveExpenseItems.Sum(i => i.current);
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
2026-01-20 19:13:33 +08:00
|
|
|
|
<span class="highlight">已归档支出总结: </span>
|
2026-01-20 19:11:05 +08:00
|
|
|
|
{(archiveExpenseDiff > 0 ? "节省支出" : "超支")}:
|
|
|
|
|
|
<span class='{(archiveExpenseDiff > 0 ? "income-value" : "expense-value")}'>
|
|
|
|
|
|
<strong>{archiveExpenseDiff:N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
=
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>{archiveExpenseItems.Sum(i => i.limit * i.months.Length):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
- 实际支出合计:
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>{archiveExpenseItems.Sum(i => i.current):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
2026-02-01 10:27:04 +08:00
|
|
|
|
#region 构建归档存款明细表格
|
|
|
|
|
|
var archiveSavingsDiff = 0m;
|
|
|
|
|
|
if (archiveSavingsItems.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine("<h3>已归档存款明细</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>月</th>
|
|
|
|
|
|
<th>合计</th>
|
|
|
|
|
|
<th>实际</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
|
|
|
|
// 已归档的存款
|
|
|
|
|
|
foreach (var (_, name, months, limit, current) in archiveSavingsItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{name}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td>{FormatMonths(months)}</td>
|
|
|
|
|
|
<td>{limit * months.Length:N0}</td>
|
|
|
|
|
|
<td><span class='income-value'>{current:N0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
2026-01-20 19:11:05 +08:00
|
|
|
|
|
2026-02-01 10:27:04 +08:00
|
|
|
|
archiveSavingsDiff = archiveSavingsItems.Sum(i => i.current) - archiveSavingsItems.Sum(i => i.limit * i.months.Length);
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<span class="highlight">已归档存款总结: </span>
|
|
|
|
|
|
{(archiveSavingsDiff > 0 ? "超额存款" : "未达预期")}:
|
|
|
|
|
|
<span class='{(archiveSavingsDiff > 0 ? "income-value" : "expense-value")}'>
|
|
|
|
|
|
<strong>{archiveSavingsDiff:N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
=
|
|
|
|
|
|
实际存款合计:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{archiveSavingsItems.Sum(i => i.current):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
-
|
|
|
|
|
|
预算存款合计:
|
|
|
|
|
|
<span class='income-value'>
|
|
|
|
|
|
<strong>{archiveSavingsItems.Sum(i => i.limit * i.months.Length):N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
2026-01-20 19:11:05 +08:00
|
|
|
|
#region 构建当前年度预算支出明细表格
|
|
|
|
|
|
description.AppendLine("<h3>预算支出明细</h3>");
|
|
|
|
|
|
description.AppendLine("""
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>预算</th>
|
|
|
|
|
|
<th>月/年</th>
|
|
|
|
|
|
<th>合计</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
""");
|
|
|
|
|
|
|
|
|
|
|
|
// 未来月预算
|
|
|
|
|
|
foreach (var (_, name, limit, factor, _, _) in currentMonthlyExpenseItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{name}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td>{FormatMonthsByFactor(factor)}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : (limit * factor).ToString("N0"))}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 年预算
|
|
|
|
|
|
foreach (var (_, name, limit, _, _, _) in currentYearlyExpenseItems)
|
|
|
|
|
|
{
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>{name}</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
<td>{year}年</td>
|
|
|
|
|
|
<td>{(limit == 0 ? "不限额" : limit.ToString("N0"))}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
""");
|
|
|
|
|
|
}
|
|
|
|
|
|
description.AppendLine("</tbody></table>");
|
|
|
|
|
|
|
|
|
|
|
|
// 合计
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
支出预算合计:
|
|
|
|
|
|
<span class='expense-value'>
|
|
|
|
|
|
<strong>
|
2026-01-30 10:41:19 +08:00
|
|
|
|
{currentMonthlyExpenseItems.Sum(i => i.limit * i.factor)
|
|
|
|
|
|
+ currentYearlyExpenseItems.Sum(i => i.limit):N0}
|
2026-01-20 19:11:05 +08:00
|
|
|
|
</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 总结
|
|
|
|
|
|
var archiveIncomeBudget = archiveIncomeItems.Sum(i => i.limit * i.months.Length);
|
|
|
|
|
|
var archiveExpenseBudget = archiveExpenseItems.Sum(i => i.limit * i.months.Length);
|
2026-02-01 10:27:04 +08:00
|
|
|
|
// 如果有归档存款数据,直接使用;否则用收入-支出计算
|
|
|
|
|
|
var archiveSavings = archiveSavingsItems.Any()
|
|
|
|
|
|
? archiveSavingsItems.Sum(i => i.current)
|
|
|
|
|
|
: archiveIncomeBudget - archiveExpenseBudget + archiveIncomeDiff + archiveExpenseDiff;
|
2026-01-20 19:11:05 +08:00
|
|
|
|
|
|
|
|
|
|
var expectedIncome = currentMonthlyIncomeItems.Sum(i => i.limit == 0 ? i.current : i.limit * i.factor) + currentYearlyIncomeItems.Sum(i => i.isMandatory || i.limit == 0 ? i.current : i.limit);
|
|
|
|
|
|
var expectedExpense = currentMonthlyExpenseItems.Sum(i => i.limit == 0 ? i.current : i.limit * i.factor) + currentYearlyExpenseItems.Sum(i => i.isMandatory || i.limit == 0 ? i.current : i.limit);
|
|
|
|
|
|
var expectedSavings = expectedIncome - expectedExpense;
|
|
|
|
|
|
|
|
|
|
|
|
description.AppendLine("<h3>存款计划结论</h3>");
|
|
|
|
|
|
description.AppendLine($"""
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>归档存款:</strong>
|
|
|
|
|
|
<span class='income-value'><strong>{archiveSavings:N0}</strong></span>
|
|
|
|
|
|
=
|
|
|
|
|
|
归档收入: <span class='income-value'>{archiveIncomeBudget:N0}</span>
|
|
|
|
|
|
-
|
|
|
|
|
|
归档支出: <span class='expense-value'>{archiveExpenseBudget:N0}</span>
|
|
|
|
|
|
{(archiveIncomeDiff >= 0 ? " + 超额收入" : " - 未达预期收入")}: <span class='{(archiveIncomeDiff >= 0 ? "income-value" : "expense-value")}'>{(archiveIncomeDiff >= 0 ? archiveIncomeDiff : -archiveIncomeDiff):N0}</span>
|
|
|
|
|
|
{(archiveExpenseDiff >= 0 ? " + 节省支出" : " - 超额支出")}: <span class='{(archiveExpenseDiff >= 0 ? "income-value" : "expense-value")}'>{(archiveExpenseDiff >= 0 ? archiveExpenseDiff : -archiveExpenseDiff):N0}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>预计存款:</strong>
|
|
|
|
|
|
<span class='income-value'><strong>{expectedSavings:N0}</strong></span>
|
|
|
|
|
|
=
|
|
|
|
|
|
预计收入: <span class='income-value'>{expectedIncome:N0}</span>
|
|
|
|
|
|
-
|
|
|
|
|
|
预计支出: <span class='expense-value'>{expectedExpense:N0}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>存档总结:</strong>
|
|
|
|
|
|
<span class='{(archiveSavings + expectedSavings > 0 ? "income-value" : "expense-value")}'>
|
|
|
|
|
|
<strong>{archiveSavings + expectedSavings:N0}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
=
|
|
|
|
|
|
预计存款:
|
|
|
|
|
|
<span class='income-value'>{expectedSavings:N0}</span>
|
|
|
|
|
|
{(archiveSavings > 0 ? "+" : "-")}
|
|
|
|
|
|
归档存款:
|
|
|
|
|
|
<span class='{(archiveSavings > 0 ? "income-value" : "expense-value")}'>{Math.Abs(archiveSavings):N0}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
""");
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
var savingsCategories = await configService.GetConfigByKeyAsync<string>("SavingsCategories") ?? string.Empty;
|
2026-01-30 10:41:19 +08:00
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
var currentActual = 0m;
|
2026-02-11 13:00:01 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 先累加已归档月份的存款金额
|
|
|
|
|
|
if (archiveSavingsItems.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
currentActual += archiveSavingsItems.Sum(i => i.current);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 再累加当前月的存款金额
|
2026-01-20 19:11:05 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(savingsCategories))
|
|
|
|
|
|
{
|
2026-01-30 10:41:19 +08:00
|
|
|
|
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
|
|
|
|
|
foreach (var kvp in transactionClassify)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cats.Contains(kvp.Key.Item1))
|
|
|
|
|
|
{
|
|
|
|
|
|
currentActual += kvp.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-20 19:11:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 13:00:01 +08:00
|
|
|
|
|
2026-01-20 19:11:05 +08:00
|
|
|
|
var record = new BudgetRecord
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = -1,
|
|
|
|
|
|
Name = "年度存款计划",
|
|
|
|
|
|
Type = BudgetPeriodType.Year,
|
|
|
|
|
|
Limit = archiveSavings + expectedSavings,
|
|
|
|
|
|
Category = BudgetCategory.Savings,
|
|
|
|
|
|
SelectedCategories = savingsCategories,
|
|
|
|
|
|
StartDate = new DateTime(year, 1, 1),
|
|
|
|
|
|
NoLimit = false,
|
|
|
|
|
|
IsMandatoryExpense = false,
|
|
|
|
|
|
CreateTime = dateTimeProvider.Now,
|
|
|
|
|
|
UpdateTime = dateTimeProvider.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return BudgetResult.FromEntity(
|
|
|
|
|
|
record,
|
|
|
|
|
|
currentActual,
|
|
|
|
|
|
new DateTime(year, 1, 1),
|
|
|
|
|
|
description.ToString()
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
void AddOrIncCurrentItem(
|
|
|
|
|
|
long id,
|
|
|
|
|
|
BudgetPeriodType periodType,
|
|
|
|
|
|
BudgetCategory category,
|
|
|
|
|
|
string name,
|
|
|
|
|
|
decimal limit,
|
|
|
|
|
|
int factor,
|
|
|
|
|
|
decimal incAmount,
|
|
|
|
|
|
bool isMandatory)
|
|
|
|
|
|
{
|
|
|
|
|
|
var current = (periodType, category) switch
|
|
|
|
|
|
{
|
|
|
|
|
|
(BudgetPeriodType.Month, BudgetCategory.Income) => currentMonthlyIncomeItems,
|
|
|
|
|
|
(BudgetPeriodType.Month, BudgetCategory.Expense) => currentMonthlyExpenseItems,
|
|
|
|
|
|
(BudgetPeriodType.Year, BudgetCategory.Income) => currentYearlyIncomeItems,
|
|
|
|
|
|
(BudgetPeriodType.Year, BudgetCategory.Expense) => currentYearlyExpenseItems,
|
|
|
|
|
|
_ => throw new NotSupportedException($"Category {category} is not supported.")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (current.Any(i => i.id == id))
|
|
|
|
|
|
{
|
|
|
|
|
|
var existing = current.First(i => i.id == id);
|
|
|
|
|
|
current.Remove(existing);
|
|
|
|
|
|
current.Add((id, existing.name, existing.limit, existing.factor + factor, existing.current + incAmount, isMandatory));
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
current.Add((id, name, limit, factor, incAmount, isMandatory));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string FormatMonthsByFactor(int factor)
|
|
|
|
|
|
{
|
|
|
|
|
|
var months = factor == 12
|
|
|
|
|
|
? Enumerable.Range(1, 12).ToArray()
|
|
|
|
|
|
: Enumerable.Range(dateTimeProvider.Now.Month, factor).ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
return FormatMonths(months.ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string FormatMonths(int[] months)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 如果是连续的月份 则简化显示 1~3
|
|
|
|
|
|
Array.Sort(months);
|
|
|
|
|
|
if (months.Length >= 2)
|
|
|
|
|
|
{
|
2026-01-28 17:00:58 +08:00
|
|
|
|
var isContinuous = true;
|
|
|
|
|
|
for (var i = 1; i < months.Length; i++)
|
2026-01-20 19:11:05 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (months[i] != months[i - 1] + 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
isContinuous = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isContinuous)
|
|
|
|
|
|
{
|
|
|
|
|
|
return $"{months.First()}~{months.Last()}月";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return string.Join(", ", months) + "月";
|
|
|
|
|
|
}
|
2026-01-19 13:39:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|