All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
351 lines
11 KiB
C#
351 lines
11 KiB
C#
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,
|
||
Details = result.Details != null ? MapToSavingsDetailDto(result.Details) : null
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 映射存款明细数据到DTO
|
||
/// </summary>
|
||
private static SavingsDetailDto MapToSavingsDetailDto(Service.Budget.SavingsDetail details)
|
||
{
|
||
return new SavingsDetailDto
|
||
{
|
||
IncomeItems = details.IncomeItems.Select(item => new BudgetDetailItemDto
|
||
{
|
||
Id = item.Id,
|
||
Name = item.Name,
|
||
Type = item.Type,
|
||
BudgetLimit = item.BudgetLimit,
|
||
ActualAmount = item.ActualAmount,
|
||
EffectiveAmount = item.EffectiveAmount,
|
||
CalculationNote = item.CalculationNote,
|
||
IsOverBudget = item.IsOverBudget,
|
||
IsArchived = item.IsArchived,
|
||
ArchivedMonths = item.ArchivedMonths
|
||
}).ToList(),
|
||
ExpenseItems = details.ExpenseItems.Select(item => new BudgetDetailItemDto
|
||
{
|
||
Id = item.Id,
|
||
Name = item.Name,
|
||
Type = item.Type,
|
||
BudgetLimit = item.BudgetLimit,
|
||
ActualAmount = item.ActualAmount,
|
||
EffectiveAmount = item.EffectiveAmount,
|
||
CalculationNote = item.CalculationNote,
|
||
IsOverBudget = item.IsOverBudget,
|
||
IsArchived = item.IsArchived,
|
||
ArchivedMonths = item.ArchivedMonths
|
||
}).ToList(),
|
||
Summary = new SavingsCalculationSummaryDto
|
||
{
|
||
TotalIncomeBudget = details.Summary.TotalIncomeBudget,
|
||
TotalExpenseBudget = details.Summary.TotalExpenseBudget,
|
||
PlannedSavings = details.Summary.PlannedSavings,
|
||
CalculationFormula = details.Summary.CalculationFormula
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <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
|
||
}
|