更新预算归档功能,添加归档总结和更新归档总结接口,优化预算统计逻辑,调整相关样式
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 34s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 34s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -2,21 +2,23 @@
|
||||
|
||||
public interface IBudgetService
|
||||
{
|
||||
Task<List<BudgetResult>> GetListAsync(DateTime? referenceDate = null);
|
||||
|
||||
Task<BudgetResult?> GetStatisticsAsync(long id, DateTime referenceDate);
|
||||
Task<List<BudgetResult>> GetListAsync(DateTime referenceDate);
|
||||
|
||||
Task<string> ArchiveBudgetsAsync(int year, int month);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定分类的统计信息(月度和年度)
|
||||
/// </summary>
|
||||
Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null);
|
||||
Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate);
|
||||
|
||||
/// <summary>
|
||||
/// 获取未被预算覆盖的分类统计信息
|
||||
/// </summary>
|
||||
Task<List<UncoveredCategoryDetail>> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null);
|
||||
|
||||
Task<string?> GetArchiveSummaryAsync(int year, int month);
|
||||
|
||||
Task UpdateArchiveSummaryAsync(int year, int month, string? summary);
|
||||
}
|
||||
|
||||
public class BudgetService(
|
||||
@@ -29,8 +31,38 @@ public class BudgetService(
|
||||
ILogger<BudgetService> logger
|
||||
) : IBudgetService
|
||||
{
|
||||
public async Task<List<BudgetResult>> GetListAsync(DateTime? referenceDate = null)
|
||||
public async Task<List<BudgetResult>> GetListAsync(DateTime referenceDate)
|
||||
{
|
||||
var year = referenceDate.Year;
|
||||
var month = referenceDate.Month;
|
||||
|
||||
var isArchive = year < DateTime.Now.Year
|
||||
|| (year == DateTime.Now.Year && month < DateTime.Now.Month);
|
||||
|
||||
if (isArchive)
|
||||
{
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
var periodRange = GetPeriodRange(DateTime.Now, BudgetPeriodType.Month, referenceDate);
|
||||
return archive.Content.Select(c => new BudgetResult
|
||||
{
|
||||
Name = c.Name,
|
||||
Type = c.Type,
|
||||
Limit = c.Limit,
|
||||
Current = c.Actual,
|
||||
Category = c.Category,
|
||||
SelectedCategories = c.SelectedCategories,
|
||||
Description = c.Description,
|
||||
PeriodStart = periodRange.start,
|
||||
PeriodEnd = periodRange.end,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
logger.LogWarning("获取预算列表时发现归档数据缺失,Year: {Year}, Month: {Month}", year, month);
|
||||
}
|
||||
|
||||
var budgets = await budgetRepository.GetAllAsync();
|
||||
var dtos = new List<BudgetResult?>();
|
||||
|
||||
@@ -53,104 +85,17 @@ public class BudgetService(
|
||||
return dtos.Where(dto => dto != null).Cast<BudgetResult>().ToList();
|
||||
}
|
||||
|
||||
public async Task<BudgetResult?> GetStatisticsAsync(long id, DateTime referenceDate)
|
||||
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
|
||||
{
|
||||
bool isArchive = false;
|
||||
BudgetRecord? budget = null;
|
||||
if (id == -1)
|
||||
{
|
||||
if (isAcrhiveFunc(BudgetPeriodType.Year))
|
||||
{
|
||||
isArchive = true;
|
||||
budget = await BuildVirtualSavingsBudgetRecordAsync(-1, referenceDate, 0);
|
||||
}
|
||||
|
||||
}
|
||||
else if (id == -2)
|
||||
{
|
||||
if (isAcrhiveFunc(BudgetPeriodType.Month))
|
||||
{
|
||||
isArchive = true;
|
||||
budget = await BuildVirtualSavingsBudgetRecordAsync(-2, referenceDate, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
budget = await budgetRepository.GetByIdAsync(id);
|
||||
|
||||
if (budget == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
isArchive = isAcrhiveFunc(budget.Type);
|
||||
}
|
||||
|
||||
if (isArchive && budget != null)
|
||||
{
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(
|
||||
id,
|
||||
referenceDate.Year,
|
||||
referenceDate.Month);
|
||||
|
||||
if (archive != null) // 存在归档 直接读取归档数据
|
||||
{
|
||||
budget.Limit = archive.BudgetedAmount;
|
||||
return BudgetResult.FromEntity(
|
||||
budget,
|
||||
archive.RealizedAmount,
|
||||
referenceDate,
|
||||
archive.Description ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (id == -1)
|
||||
{
|
||||
return await GetVirtualSavingsDtoAsync(BudgetPeriodType.Year, referenceDate);
|
||||
}
|
||||
if (id == -2)
|
||||
{
|
||||
return await GetVirtualSavingsDtoAsync(BudgetPeriodType.Month, referenceDate);
|
||||
}
|
||||
|
||||
budget = await budgetRepository.GetByIdAsync(id);
|
||||
if (budget == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentAmount = await CalculateCurrentAmountAsync(budget, referenceDate);
|
||||
return BudgetResult.FromEntity(budget, currentAmount, referenceDate);
|
||||
|
||||
bool isAcrhiveFunc(BudgetPeriodType periodType)
|
||||
{
|
||||
if (periodType == BudgetPeriodType.Year)
|
||||
{
|
||||
return DateTime.Now.Year > referenceDate.Year;
|
||||
}
|
||||
else if (periodType == BudgetPeriodType.Month)
|
||||
{
|
||||
return DateTime.Now.Year > referenceDate.Year
|
||||
|| (DateTime.Now.Year == referenceDate.Year
|
||||
&& DateTime.Now.Month > referenceDate.Month);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null)
|
||||
{
|
||||
var budgets = (await budgetRepository.GetAllAsync()).ToList();
|
||||
var refDate = referenceDate ?? DateTime.Now;
|
||||
var budgets = await GetListAsync(referenceDate);
|
||||
|
||||
var result = new BudgetCategoryStats();
|
||||
|
||||
// 获取月度统计
|
||||
result.Month = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Month, refDate);
|
||||
result.Month = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Month, referenceDate);
|
||||
|
||||
// 获取年度统计
|
||||
result.Year = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Year, refDate);
|
||||
result.Year = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Year, referenceDate);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -190,8 +135,30 @@ public class BudgetService(
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<string?> GetArchiveSummaryAsync(int year, int month)
|
||||
{
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
|
||||
return archive?.Summary;
|
||||
}
|
||||
|
||||
public async Task UpdateArchiveSummaryAsync(int year, int month, string? summary)
|
||||
{
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
|
||||
if (archive == null)
|
||||
{
|
||||
await ArchiveBudgetsAsync(year, month);
|
||||
archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
|
||||
}
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
archive.Summary = summary;
|
||||
await budgetArchiveRepository.UpdateAsync(archive);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BudgetStatsDto> CalculateCategoryStatsAsync(
|
||||
List<BudgetRecord> budgets,
|
||||
List<BudgetResult> budgets,
|
||||
BudgetCategory category,
|
||||
BudgetPeriodType statType,
|
||||
DateTime referenceDate)
|
||||
@@ -236,7 +203,15 @@ public class BudgetService(
|
||||
totalLimit += itemLimit;
|
||||
|
||||
// 当前值累加
|
||||
var currentAmount = await CalculateCurrentAmountAsync(budget, referenceDate);
|
||||
var currentAmount = await CalculateCurrentAmountAsync(new()
|
||||
{
|
||||
Name = budget.Name,
|
||||
Type = budget.Type,
|
||||
Limit = budget.Limit,
|
||||
Category = budget.Category,
|
||||
SelectedCategories = string.Join(',', budget.SelectedCategories),
|
||||
StartDate = new DateTime(referenceDate.Year, referenceDate.Month, 1)
|
||||
}, referenceDate);
|
||||
if (budget.Type == statType)
|
||||
{
|
||||
totalCurrent += currentAmount;
|
||||
@@ -263,55 +238,49 @@ public class BudgetService(
|
||||
public async Task<string> ArchiveBudgetsAsync(int year, int month)
|
||||
{
|
||||
var referenceDate = new DateTime(year, month, 1);
|
||||
|
||||
var budgets = await GetListAsync(referenceDate);
|
||||
|
||||
var addArchives = new List<BudgetArchive>();
|
||||
var updateArchives = new List<BudgetArchive>();
|
||||
foreach (var budget in budgets)
|
||||
var content = budgets.Select(b => new BudgetArchiveContent
|
||||
{
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(budget.Id, year, month);
|
||||
Name = b.Name,
|
||||
Type = b.Type,
|
||||
Limit = b.Limit,
|
||||
Actual = b.Current,
|
||||
Category = b.Category,
|
||||
SelectedCategories = b.SelectedCategories,
|
||||
Description = b.Description
|
||||
}).ToArray();
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
archive.RealizedAmount = budget.Current;
|
||||
archive.ArchiveDate = DateTime.Now;
|
||||
archive.Description = budget.Description;
|
||||
updateArchives.Add(archive);
|
||||
}
|
||||
else
|
||||
{
|
||||
archive = new BudgetArchive
|
||||
{
|
||||
BudgetId = budget.Id,
|
||||
BudgetType = budget.Type,
|
||||
Year = year,
|
||||
Month = month,
|
||||
BudgetedAmount = budget.Limit,
|
||||
RealizedAmount = budget.Current,
|
||||
Description = budget.Description,
|
||||
ArchiveDate = DateTime.Now
|
||||
};
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
|
||||
|
||||
addArchives.Add(archive);
|
||||
}
|
||||
}
|
||||
|
||||
if (addArchives.Count > 0)
|
||||
if (archive != null)
|
||||
{
|
||||
if (!await budgetArchiveRepository.AddRangeAsync(addArchives))
|
||||
{
|
||||
return "保存预算归档失败";
|
||||
}
|
||||
}
|
||||
if (updateArchives.Count > 0)
|
||||
{
|
||||
if (!await budgetArchiveRepository.UpdateRangeAsync(updateArchives))
|
||||
archive.Content = content;
|
||||
archive.ArchiveDate = DateTime.Now;
|
||||
if (!await budgetArchiveRepository.UpdateAsync(archive))
|
||||
{
|
||||
return "更新预算归档失败";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
archive = new BudgetArchive
|
||||
{
|
||||
Year = year,
|
||||
Month = month,
|
||||
Content = content,
|
||||
ArchiveDate = DateTime.Now
|
||||
};
|
||||
|
||||
if (!await budgetArchiveRepository.AddAsync(archive))
|
||||
{
|
||||
return "保存预算归档失败";
|
||||
}
|
||||
}
|
||||
|
||||
_ = NotifyAsync(year, month);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -320,22 +289,16 @@ public class BudgetService(
|
||||
try
|
||||
{
|
||||
var archives = await budgetArchiveRepository.GetListAsync(year, month);
|
||||
var budgets = await budgetRepository.GetAllAsync();
|
||||
var budgetMap = budgets.ToDictionary(b => b.Id, b => b);
|
||||
|
||||
var archiveData = archives.Select(a =>
|
||||
var archiveData = archives.SelectMany(a => a.Content.Select(c => new
|
||||
{
|
||||
budgetMap.TryGetValue(a.BudgetId, out var br);
|
||||
var name = br?.Name ?? (a.BudgetId == -1 ? "年度存款" : a.BudgetId == -2 ? "月度存款" : "未知");
|
||||
return new
|
||||
{
|
||||
Name = name,
|
||||
Type = a.BudgetType.ToString(),
|
||||
Limit = a.BudgetedAmount,
|
||||
Actual = a.RealizedAmount,
|
||||
Category = br?.Category.ToString() ?? (a.BudgetId < 0 ? "Savings" : "Unknown")
|
||||
};
|
||||
}).ToList();
|
||||
c.Name,
|
||||
Type = c.Type.ToString(),
|
||||
c.Limit,
|
||||
c.Actual,
|
||||
Category = c.Category.ToString(),
|
||||
c.SelectedCategories
|
||||
})).ToList();
|
||||
|
||||
var yearTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync(
|
||||
$"""
|
||||
@@ -368,9 +331,9 @@ public class BudgetService(
|
||||
);
|
||||
|
||||
// 分析未被预算覆盖的分类 (仅针对支出类型 Type=0)
|
||||
var budgetedCategories = budgets
|
||||
.Where(b => !string.IsNullOrEmpty(b.SelectedCategories))
|
||||
.SelectMany(b => b.SelectedCategories.Split(','))
|
||||
var budgetedCategories = archiveData
|
||||
.SelectMany(b => b.SelectedCategories)
|
||||
.Where(c => !string.IsNullOrEmpty(c))
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
|
||||
@@ -567,10 +530,10 @@ public class BudgetService(
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th">名称</th>
|
||||
<th">金额</th>
|
||||
<th">折算</th>
|
||||
<th">合计</th>
|
||||
<th>名称</th>
|
||||
<th>金额</th>
|
||||
<th>折算</th>
|
||||
<th>合计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -23,6 +23,8 @@ public class BudgetArchiveJob(
|
||||
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var budgetService = scope.ServiceProvider.GetRequiredService<IBudgetService>();
|
||||
|
||||
// 归档月度数据
|
||||
var result = await budgetService.ArchiveBudgetsAsync(year, month);
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
|
||||
Reference in New Issue
Block a user