Files
EmailBill/Application/BudgetApplication.cs
SunCheng a88556c784 fix
2026-02-15 10:10:28 +08:00

307 lines
9.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Service.Budget;
namespace Application;
/// <summary>
/// 预算应用服务接口
/// </summary>
public interface IBudgetApplication
{
/// <summary>
/// 获取预算列表
/// </summary>
Task<List<BudgetResponse>> GetListAsync(DateTime referenceDate);
/// <summary>
/// 获取分类统计信息(月度和年度)
/// </summary>
Task<BudgetCategoryStatsResponse> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate);
/// <summary>
/// 获取未被预算覆盖的分类统计信息
/// </summary>
Task<List<UncoveredCategoryResponse>> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null);
/// <summary>
/// 获取归档总结
/// </summary>
Task<string?> GetArchiveSummaryAsync(DateTime referenceDate);
/// <summary>
/// 获取指定周期的存款预算信息
/// </summary>
Task<BudgetResponse?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type);
/// <summary>
/// 删除预算
/// </summary>
Task DeleteByIdAsync(long id);
/// <summary>
/// 创建预算
/// </summary>
Task<long> CreateAsync(CreateBudgetRequest request);
/// <summary>
/// 更新预算
/// </summary>
Task UpdateAsync(UpdateBudgetRequest request);
}
/// <summary>
/// 预算应用服务实现
/// </summary>
public class BudgetApplication(
IBudgetService budgetService,
IBudgetRepository budgetRepository
) : IBudgetApplication
{
public async Task<List<BudgetResponse>> GetListAsync(DateTime referenceDate)
{
var results = await budgetService.GetListAsync(referenceDate);
// 排序: 刚性支出优先 → 按分类 → 按类型 → 按使用率 → 按名称
return results
.OrderByDescending(b => b.IsMandatoryExpense)
.ThenBy(b => b.Category)
.ThenBy(b => b.Type)
.ThenByDescending(b => b.Limit > 0 ? b.Current / b.Limit : 0)
.ThenBy(b => b.Name)
.Select(MapToResponse)
.ToList();
}
public async Task<BudgetCategoryStatsResponse> GetCategoryStatsAsync(
BudgetCategory category,
DateTime referenceDate)
{
var result = await budgetService.GetCategoryStatsAsync(category, referenceDate);
return new BudgetCategoryStatsResponse
{
Month = new BudgetStatsDetail
{
Limit = result.Month.Limit,
Current = result.Month.Current,
Remaining = result.Month.Limit - result.Month.Current,
UsagePercentage = result.Month.Rate,
Trend = result.Month.Trend,
Description = result.Month.Description
},
Year = new BudgetStatsDetail
{
Limit = result.Year.Limit,
Current = result.Year.Current,
Remaining = result.Year.Limit - result.Year.Current,
UsagePercentage = result.Year.Rate,
Trend = result.Year.Trend,
Description = result.Year.Description
}
};
}
public async Task<List<UncoveredCategoryResponse>> GetUncoveredCategoriesAsync(
BudgetCategory category,
DateTime? referenceDate = null)
{
var results = await budgetService.GetUncoveredCategoriesAsync(category, referenceDate);
return results.Select(r => new UncoveredCategoryResponse
{
Category = r.Category,
Amount = r.TotalAmount,
Count = r.TransactionCount
}).ToList();
}
public async Task<string?> GetArchiveSummaryAsync(DateTime referenceDate)
{
return await budgetService.GetArchiveSummaryAsync(referenceDate.Year, referenceDate.Month);
}
public async Task<BudgetResponse?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
{
var result = await budgetService.GetSavingsBudgetAsync(year, month, type);
return result == null ? null : MapToResponse(result);
}
public async Task DeleteByIdAsync(long id)
{
var success = await budgetRepository.DeleteAsync(id);
if (!success)
{
throw new BusinessException("删除预算失败,记录不存在");
}
}
public async Task<long> CreateAsync(CreateBudgetRequest request)
{
// 业务验证
await ValidateCreateRequestAsync(request);
// 不记额预算的金额强制设为0
var limit = request.NoLimit ? 0 : request.Limit;
var budget = new BudgetRecord
{
Name = request.Name,
Type = request.Type,
Limit = limit,
Category = request.Category,
SelectedCategories = string.Join(",", request.SelectedCategories),
StartDate = request.StartDate ?? DateTime.Now,
NoLimit = request.NoLimit,
IsMandatoryExpense = request.IsMandatoryExpense
};
// 验证分类冲突
await ValidateBudgetCategoriesAsync(budget);
var success = await budgetRepository.AddAsync(budget);
if (!success)
{
throw new BusinessException("创建预算失败");
}
return budget.Id;
}
public async Task UpdateAsync(UpdateBudgetRequest request)
{
var budget = await budgetRepository.GetByIdAsync(request.Id);
if (budget == null)
{
throw new NotFoundException("预算不存在");
}
// 业务验证
await ValidateUpdateRequestAsync(request);
// 不记额预算的金额强制设为0
var limit = request.NoLimit ? 0 : request.Limit;
budget.Name = request.Name;
budget.Type = request.Type;
budget.Limit = limit;
budget.Category = request.Category;
budget.SelectedCategories = string.Join(",", request.SelectedCategories);
budget.NoLimit = request.NoLimit;
budget.IsMandatoryExpense = request.IsMandatoryExpense;
if (request.StartDate.HasValue)
{
budget.StartDate = request.StartDate.Value;
}
// 验证分类冲突
await ValidateBudgetCategoriesAsync(budget);
var success = await budgetRepository.UpdateAsync(budget);
if (!success)
{
throw new BusinessException("更新预算失败");
}
}
#region Private Methods
/// <summary>
/// 映射到响应DTO
/// </summary>
private static BudgetResponse MapToResponse(BudgetResult result)
{
// 解析StartDate字符串为DateTime
DateTime.TryParse(result.StartDate, out var startDate);
return new BudgetResponse
{
Id = result.Id,
Name = result.Name,
Type = result.Type,
Limit = result.Limit,
Current = result.Current,
Category = result.Category,
SelectedCategories = result.SelectedCategories,
StartDate = startDate,
NoLimit = result.NoLimit,
IsMandatoryExpense = result.IsMandatoryExpense,
UsagePercentage = result.Limit > 0 ? result.Current / result.Limit * 100 : 0
};
}
/// <summary>
/// 验证创建请求
/// </summary>
private static Task ValidateCreateRequestAsync(CreateBudgetRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
{
throw new ValidationException("预算名称不能为空");
}
if (!request.NoLimit && request.Limit <= 0)
{
throw new ValidationException("预算金额必须大于0");
}
if (request.SelectedCategories.Length == 0)
{
throw new ValidationException("请至少选择一个分类");
}
return Task.CompletedTask;
}
/// <summary>
/// 验证更新请求
/// </summary>
private static Task ValidateUpdateRequestAsync(UpdateBudgetRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
{
throw new ValidationException("预算名称不能为空");
}
if (!request.NoLimit && request.Limit <= 0)
{
throw new ValidationException("预算金额必须大于0");
}
if (request.SelectedCategories.Length == 0)
{
throw new ValidationException("请至少选择一个分类");
}
return Task.CompletedTask;
}
/// <summary>
/// 验证预算分类从Controller迁移的业务逻辑
/// </summary>
private async Task ValidateBudgetCategoriesAsync(BudgetRecord record)
{
// 验证不记额预算必须是年度预算
if (record.NoLimit && record.Type != BudgetPeriodType.Year)
{
throw new ValidationException("不记额预算只能设置为年度预算。");
}
var allBudgets = await budgetRepository.GetAllAsync();
var recordSelectedCategories = record.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var budget in allBudgets)
{
var selectedCategories = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
if (budget.Id != record.Id)
{
if (budget.Category == record.Category &&
recordSelectedCategories.Intersect(selectedCategories).Any())
{
throw new ValidationException($"和 {budget.Name} 存在分类冲突,请调整相关分类。");
}
}
}
}
#endregion
}