重构存款预算
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 44s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 3s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 44s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 3s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
@@ -30,9 +30,9 @@ public class BudgetService(
|
||||
IBudgetArchiveRepository budgetArchiveRepository,
|
||||
ITransactionRecordRepository transactionRecordRepository,
|
||||
IOpenAiService openAiService,
|
||||
IConfigService configService,
|
||||
IMessageService messageService,
|
||||
ILogger<BudgetService> logger
|
||||
ILogger<BudgetService> logger,
|
||||
IBudgetSavingsService budgetSavingsService
|
||||
) : IBudgetService
|
||||
{
|
||||
public async Task<List<BudgetResult>> GetListAsync(DateTime referenceDate)
|
||||
@@ -80,11 +80,11 @@ public class BudgetService(
|
||||
}
|
||||
|
||||
// 创造虚拟的存款预算
|
||||
dtos.Add(await GetSavingsDtoAsync(
|
||||
dtos.Add(await budgetSavingsService.GetSavingsDtoAsync(
|
||||
BudgetPeriodType.Month,
|
||||
referenceDate,
|
||||
budgets));
|
||||
dtos.Add(await GetSavingsDtoAsync(
|
||||
dtos.Add(await budgetSavingsService.GetSavingsDtoAsync(
|
||||
BudgetPeriodType.Year,
|
||||
referenceDate,
|
||||
budgets));
|
||||
@@ -103,7 +103,7 @@ public class BudgetService(
|
||||
public async Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
|
||||
{
|
||||
var referenceDate = new DateTime(year, month, 1);
|
||||
return await GetSavingsDtoAsync(type, referenceDate);
|
||||
return await budgetSavingsService.GetSavingsDtoAsync(type, referenceDate);
|
||||
}
|
||||
|
||||
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
|
||||
@@ -595,484 +595,6 @@ public class BudgetService(
|
||||
|
||||
return (start, end);
|
||||
}
|
||||
|
||||
private async Task<BudgetResult?> GetSavingsDtoAsync(
|
||||
BudgetPeriodType periodType,
|
||||
DateTime? referenceDate = null,
|
||||
IEnumerable<BudgetRecord>? existingBudgets = null)
|
||||
{
|
||||
var allBudgets = existingBudgets;
|
||||
|
||||
if (existingBudgets == null)
|
||||
{
|
||||
allBudgets = await budgetRepository.GetAllAsync();
|
||||
}
|
||||
|
||||
if (allBudgets == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
allBudgets = allBudgets
|
||||
// 排序顺序 1.硬性预算 2.月度->年度 3.实际金额倒叙
|
||||
.OrderBy(b => b.IsMandatoryExpense)
|
||||
.ThenBy(b => b.Type)
|
||||
.ThenByDescending(b => b.Limit);
|
||||
|
||||
var date = referenceDate ?? DateTime.Now;
|
||||
|
||||
decimal incomeLimitAtPeriod = 0;
|
||||
decimal expenseLimitAtPeriod = 0;
|
||||
decimal noLimitIncomeAtPeriod = 0; // 新增:不记额收入汇总
|
||||
decimal noLimitExpenseAtPeriod = 0; // 新增:不记额支出汇总
|
||||
|
||||
var incomeItems = new List<(string Name, decimal Limit, decimal Factor, decimal Historical, decimal Total)>();
|
||||
var expenseItems = new List<(string Name, decimal Limit, decimal Factor, decimal Historical, decimal Total)>();
|
||||
var noLimitIncomeItems = new List<(string Name, decimal Amount)>(); // 新增
|
||||
var noLimitExpenseItems = new List<(string Name, decimal Amount)>(); // 新增
|
||||
|
||||
// 如果是年度计算,先从归档中获取所有历史数据
|
||||
Dictionary<(long Id, int Month), (decimal HistoricalLimit, BudgetCategory Category, string Name)> historicalData = new();
|
||||
|
||||
if (periodType == BudgetPeriodType.Year)
|
||||
{
|
||||
var yearArchives = await budgetArchiveRepository.GetArchivesByYearAsync(date.Year);
|
||||
|
||||
// 按预算ID和月份记录历史数据
|
||||
foreach (var archive in yearArchives)
|
||||
{
|
||||
foreach (var content in archive.Content)
|
||||
{
|
||||
// 跳过存款类预算
|
||||
if (content.Category == BudgetCategory.Savings) continue;
|
||||
|
||||
historicalData[(content.Id, archive.Month)] = (
|
||||
content.Limit,
|
||||
content.Category,
|
||||
content.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理当前预算
|
||||
var processedIds = new HashSet<long>();
|
||||
foreach (var b in allBudgets)
|
||||
{
|
||||
if (b.Category == BudgetCategory.Savings) continue;
|
||||
|
||||
processedIds.Add(b.Id);
|
||||
decimal factor;
|
||||
decimal historicalAmount = 0m;
|
||||
var historicalMonths = new List<int>();
|
||||
|
||||
if (periodType == BudgetPeriodType.Year)
|
||||
{
|
||||
if (b.Type == BudgetPeriodType.Month)
|
||||
{
|
||||
// 月度预算在年度计算时:历史归档 + 剩余月份预算
|
||||
// 收集该预算的所有历史月份数据
|
||||
foreach (var ((id, month), (limit, _, _)) in historicalData)
|
||||
{
|
||||
if (id == b.Id)
|
||||
{
|
||||
historicalAmount += limit;
|
||||
historicalMonths.Add(month);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算剩余月份数(当前月到12月)
|
||||
var remainingMonths = 12 - date.Month + 1;
|
||||
factor = remainingMonths;
|
||||
}
|
||||
else if (b.Type == BudgetPeriodType.Year)
|
||||
{
|
||||
factor = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
factor = 0;
|
||||
}
|
||||
}
|
||||
else if (periodType == BudgetPeriodType.Month)
|
||||
{
|
||||
factor = b.Type switch
|
||||
{
|
||||
BudgetPeriodType.Month => 1,
|
||||
BudgetPeriodType.Year => 0,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
factor = 0; // 其他周期暂不计算虚拟存款
|
||||
}
|
||||
|
||||
if (factor <= 0 && historicalAmount <= 0) continue;
|
||||
|
||||
// 处理不记额预算
|
||||
if (b.NoLimit)
|
||||
{
|
||||
var actualAmount = await CalculateCurrentAmountAsync(b, date);
|
||||
if (b.Category == BudgetCategory.Income)
|
||||
{
|
||||
noLimitIncomeAtPeriod += actualAmount;
|
||||
noLimitIncomeItems.Add((b.Name, actualAmount));
|
||||
}
|
||||
else if (b.Category == BudgetCategory.Expense)
|
||||
{
|
||||
noLimitExpenseAtPeriod += actualAmount;
|
||||
noLimitExpenseItems.Add((b.Name, actualAmount));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通预算:历史金额 + 当前预算折算
|
||||
var subtotal = historicalAmount + b.Limit * factor;
|
||||
var displayName = b.Name;
|
||||
|
||||
// 如果有历史月份,添加月份范围显示
|
||||
if (historicalMonths.Count > 0)
|
||||
{
|
||||
historicalMonths.Sort();
|
||||
var monthRange = historicalMonths.Count == 1
|
||||
? $"{historicalMonths[0]}月"
|
||||
: $"{historicalMonths[0]}~{historicalMonths[^1]}月";
|
||||
displayName = $"{b.Name} ({monthRange})";
|
||||
}
|
||||
|
||||
if (b.Category == BudgetCategory.Income)
|
||||
{
|
||||
incomeLimitAtPeriod += subtotal;
|
||||
incomeItems.Add((displayName, b.Limit, factor, historicalAmount, subtotal));
|
||||
}
|
||||
else if (b.Category == BudgetCategory.Expense)
|
||||
{
|
||||
expenseLimitAtPeriod += subtotal;
|
||||
expenseItems.Add((displayName, b.Limit, factor, historicalAmount, subtotal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理已删除的预算(只在归档中存在,但当前预算列表中不存在的)
|
||||
if (periodType == BudgetPeriodType.Year)
|
||||
{
|
||||
// 按预算ID分组
|
||||
var deletedBudgets = historicalData
|
||||
.Where(kvp => !processedIds.Contains(kvp.Key.Id))
|
||||
.GroupBy(kvp => kvp.Key.Id);
|
||||
|
||||
foreach (var group in deletedBudgets)
|
||||
{
|
||||
var months = group.Select(g => g.Key.Month).OrderBy(m => m).ToList();
|
||||
var totalLimit = group.Sum(g => g.Value.HistoricalLimit);
|
||||
var (_, category, name) = group.First().Value;
|
||||
|
||||
var monthRange = months.Count == 1
|
||||
? $"{months[0]}月"
|
||||
: $"{months[0]}~{months[^1]}月";
|
||||
var displayName = $"{name} ({monthRange}, 已删除)";
|
||||
|
||||
// 这是一个已被删除的预算,但有历史数据
|
||||
if (category == BudgetCategory.Income)
|
||||
{
|
||||
incomeLimitAtPeriod += totalLimit;
|
||||
incomeItems.Add((displayName, 0, months.Count, totalLimit, totalLimit));
|
||||
}
|
||||
else if (category == BudgetCategory.Expense)
|
||||
{
|
||||
expenseLimitAtPeriod += totalLimit;
|
||||
expenseItems.Add((displayName, 0, months.Count, totalLimit, totalLimit));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var description = new StringBuilder();
|
||||
description.Append("<h3>预算收入明细</h3>");
|
||||
if (incomeItems.Count == 0) description.Append("<p>无收入预算</p>");
|
||||
else
|
||||
{
|
||||
// 根据是否有历史数据决定表格列
|
||||
var hasHistoricalData = incomeItems.Any(i => i.Historical > 0);
|
||||
|
||||
if (hasHistoricalData)
|
||||
{
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>当前预算</th>
|
||||
<th>剩余月数</th>
|
||||
<th>历史归档</th>
|
||||
<th>合计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""");
|
||||
foreach (var item in incomeItems)
|
||||
{
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td>{item.Name}</td>
|
||||
<td>{item.Limit:N0}</td>
|
||||
<td>{item.Factor:0.##}</td>
|
||||
<td>{item.Historical:N0}</td>
|
||||
<td><span class='income-value'>{item.Total:N0}</span></td>
|
||||
</tr>
|
||||
""");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>金额</th>
|
||||
<th>折算</th>
|
||||
<th>合计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""");
|
||||
foreach (var item in incomeItems)
|
||||
{
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td>{item.Name}</td>
|
||||
<td>{item.Limit:N0}</td>
|
||||
<td>{item.Factor:0.##}</td>
|
||||
<td><span class='income-value'>{item.Total:N0}</span></td>
|
||||
</tr>
|
||||
""");
|
||||
}
|
||||
}
|
||||
description.Append("</tbody></table>");
|
||||
}
|
||||
description.Append($"<p>收入合计: <span class='income-value'><strong>{incomeLimitAtPeriod:N0}</strong></span></p>");
|
||||
|
||||
if (periodType == BudgetPeriodType.Year && noLimitIncomeItems.Count > 0)
|
||||
{
|
||||
description.Append("<h3>不记额收入明细</h3>");
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>预算名称</th>
|
||||
<th>实际发生</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""");
|
||||
foreach (var (name, amount) in noLimitIncomeItems)
|
||||
{
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td><span class='income-value'>{amount:N0}</span></td>
|
||||
</tr>
|
||||
""");
|
||||
}
|
||||
description.Append("</tbody></table>");
|
||||
description.Append($"<p>不记额收入合计: <span class='income-value'><strong>{noLimitIncomeAtPeriod:N0}</strong></span></p>");
|
||||
}
|
||||
|
||||
description.Append("<h3>预算支出明细</h3>");
|
||||
if (expenseItems.Count == 0) description.Append("<p>无支出预算</p>");
|
||||
else
|
||||
{
|
||||
// 根据是否有历史数据决定表格列
|
||||
var hasHistoricalData = expenseItems.Any(i => i.Historical > 0);
|
||||
|
||||
if (hasHistoricalData)
|
||||
{
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>当前预算</th>
|
||||
<th>剩余月数</th>
|
||||
<th>历史归档</th>
|
||||
<th>合计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""");
|
||||
foreach (var item in expenseItems)
|
||||
{
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td>{item.Name}</td>
|
||||
<td>{item.Limit:N0}</td>
|
||||
<td>{item.Factor:0.##}</td>
|
||||
<td>{item.Historical:N0}</td>
|
||||
<td><span class='expense-value'>{item.Total:N0}</span></td>
|
||||
</tr>
|
||||
""");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>金额</th>
|
||||
<th>折算</th>
|
||||
<th>合计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""");
|
||||
foreach (var item in expenseItems)
|
||||
{
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td>{item.Name}</td>
|
||||
<td>{item.Limit:N0}</td>
|
||||
<td>{item.Factor:0.##}</td>
|
||||
<td><span class='expense-value'>{item.Total:N0}</span></td>
|
||||
</tr>
|
||||
""");
|
||||
}
|
||||
}
|
||||
description.Append("</tbody></table>");
|
||||
}
|
||||
description.Append($"<p>支出合计: <span class='expense-value'><strong>{expenseLimitAtPeriod:N0}</strong></span></p>");
|
||||
|
||||
if (periodType == BudgetPeriodType.Year && noLimitExpenseItems.Count > 0)
|
||||
{
|
||||
description.Append("<h3>不记额支出明细</h3>");
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>预算名称</th>
|
||||
<th>实际发生</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""");
|
||||
foreach (var (name, amount) in noLimitExpenseItems)
|
||||
{
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td><span class='expense-value'>{amount:N0}</span></td>
|
||||
</tr>
|
||||
""");
|
||||
}
|
||||
description.Append("</tbody></table>");
|
||||
description.Append($"<p>不记额支出合计: <span class='expense-value'><strong>{noLimitExpenseAtPeriod:N0}</strong></span></p>");
|
||||
}
|
||||
|
||||
description.Append("<h3>存款计划结论</h3>");
|
||||
// 修改计算公式:包含不记额收入和支出
|
||||
var totalIncome = incomeLimitAtPeriod + noLimitIncomeAtPeriod;
|
||||
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='income-value'>{totalIncome:N0}</span> - 计划支出 <span class='expense-value'>{totalExpense:N0}</span> = <span class='income-value'><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='{(totalIncome - totalExpense >= 0 ? "income-value" : "expense-value")}'>{totalIncome - totalExpense:N0}</span>
|
||||
+ 年度历史盈亏 <span class='{(historicalSurplus >= 0 ? "income-value" : "expense-value")}'>{historicalSurplus:N0}</span>
|
||||
= <span class='{(finalGoal >= 0 ? "income-value" : "expense-value")}'><strong>{finalGoal:N0}</strong></span></p>
|
||||
""");
|
||||
}
|
||||
|
||||
var finalLimit = periodType == BudgetPeriodType.Year ? (totalIncome - totalExpense + historicalSurplus) : (totalIncome - totalExpense);
|
||||
|
||||
var virtualBudget = await BuildVirtualSavingsBudgetRecordAsync(
|
||||
periodType == BudgetPeriodType.Year ? -1 : -2,
|
||||
date,
|
||||
finalLimit);
|
||||
|
||||
// 计算实际发生的 收入 - 支出
|
||||
var current = await CalculateCurrentAmountAsync(new BudgetRecord
|
||||
{
|
||||
Category = virtualBudget.Category,
|
||||
Type = virtualBudget.Type,
|
||||
SelectedCategories = virtualBudget.SelectedCategories,
|
||||
StartDate = virtualBudget.StartDate,
|
||||
}, date);
|
||||
|
||||
return BudgetResult.FromEntity(virtualBudget, current, date, description.ToString());
|
||||
}
|
||||
|
||||
private async Task<BudgetRecord> BuildVirtualSavingsBudgetRecordAsync(
|
||||
long id,
|
||||
DateTime date,
|
||||
decimal limit)
|
||||
{
|
||||
var savingsCategories = await configService.GetConfigByKeyAsync<string>("SavingsCategories") ?? string.Empty;
|
||||
return new BudgetRecord
|
||||
{
|
||||
Id = id,
|
||||
Name = id == -1 ? "年度存款" : "月度存款",
|
||||
Category = BudgetCategory.Savings,
|
||||
Type = id == -1 ? BudgetPeriodType.Year : BudgetPeriodType.Month,
|
||||
Limit = limit,
|
||||
StartDate = id == -1
|
||||
? new DateTime(date.Year, 1, 1)
|
||||
: new DateTime(date.Year, date.Month, 1),
|
||||
SelectedCategories = savingsCategories
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public record BudgetResult
|
||||
|
||||
Reference in New Issue
Block a user