feat: 添加预算统计服务增强和日志系统改进
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
1. 新增 BudgetStatsService:将预算统计逻辑从 BudgetService 中提取为独立服务,支持月度和年度统计,包含归档数据支持和硬性预算调整算法 2. 日志系统增强:添加请求ID追踪功能,支持通过请求ID查询关联日志,新增类名筛选功能 3. 日志解析优化:修复类名解析逻辑,正确提取 SourceContext 中的类名信息 4. 代码清理:移除不需要的方法名相关代码,简化日志筛选逻辑
This commit is contained in:
519
WebApi.Test/Budget/BudgetStatsTest.cs
Normal file
519
WebApi.Test/Budget/BudgetStatsTest.cs
Normal file
@@ -0,0 +1,519 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace WebApi.Test.Budget;
|
||||
|
||||
public class BudgetStatsTest : BaseTest
|
||||
{
|
||||
private readonly IBudgetRepository _budgetRepository = Substitute.For<IBudgetRepository>();
|
||||
private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For<IBudgetArchiveRepository>();
|
||||
private readonly ITransactionRecordRepository _transactionsRepository = Substitute.For<ITransactionRecordRepository>();
|
||||
private readonly IOpenAiService _openAiService = Substitute.For<IOpenAiService>();
|
||||
private readonly IMessageService _messageService = Substitute.For<IMessageService>();
|
||||
private readonly ILogger<BudgetService> _logger = Substitute.For<ILogger<BudgetService>>();
|
||||
private readonly IBudgetSavingsService _budgetSavingsService = Substitute.For<IBudgetSavingsService>();
|
||||
private readonly IDateTimeProvider _dateTimeProvider = Substitute.For<IDateTimeProvider>();
|
||||
private readonly IBudgetStatsService _budgetStatsService;
|
||||
private readonly BudgetService _service;
|
||||
|
||||
public BudgetStatsTest()
|
||||
{
|
||||
_dateTimeProvider.Now.Returns(new DateTime(2024, 1, 15));
|
||||
|
||||
_budgetStatsService = new BudgetStatsService(
|
||||
_budgetRepository,
|
||||
_budgetArchiveRepository,
|
||||
_transactionsRepository,
|
||||
_dateTimeProvider,
|
||||
Substitute.For<ILogger<BudgetStatsService>>()
|
||||
);
|
||||
|
||||
_service = new BudgetService(
|
||||
_budgetRepository,
|
||||
_budgetArchiveRepository,
|
||||
_transactionsRepository,
|
||||
_openAiService,
|
||||
_messageService,
|
||||
_logger,
|
||||
_budgetSavingsService,
|
||||
_dateTimeProvider,
|
||||
_budgetStatsService
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_月度_Test()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2024, 1, 15);
|
||||
var budgets = new List<BudgetRecord>
|
||||
{
|
||||
new() { Id = 1, Name = "吃喝", Limit = 2000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" },
|
||||
new() { Id = 2, Name = "交通", Limit = 500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "交通" }
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(args =>
|
||||
{
|
||||
var b = (BudgetRecord)args[0];
|
||||
return b.Name == "吃喝" ? 1200m : 300m;
|
||||
});
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
{ new DateTime(2024, 1, 15), 1500m } // 1月15日累计1500(吃喝1200+交通300)
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Assert
|
||||
result.Month.Limit.Should().Be(2500); // 吃喝2000 + 交通500
|
||||
result.Month.Current.Should().Be(1500); // 吃喝1200 + 交通300
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_月度_硬性收支_Test()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2024, 1, 15);
|
||||
var budgets = new List<BudgetRecord>
|
||||
{
|
||||
new() { Id = 1, Name = "房租", Limit = 3100, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, IsMandatoryExpense = true }
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(0m); // 实际支出的金额为0
|
||||
|
||||
_dateTimeProvider.Now.Returns(referenceDate);
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||||
.Returns(new Dictionary<DateTime, decimal>());
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Assert
|
||||
// 1月有31天,15号经过了15天
|
||||
// 3100 * 15 / 31 = 1500
|
||||
result.Month.Limit.Should().Be(3100);
|
||||
result.Month.Current.Should().Be(1500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_年度_1月_Test()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2024, 1, 15);
|
||||
var budgets = new List<BudgetRecord>
|
||||
{
|
||||
new() { Id = 2, Name = "月度吃饭", Limit = 3000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮" },
|
||||
new() { Id = 1, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, SelectedCategories = "旅游" }
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
|
||||
// 月度统计使用趋势统计数据(只包含月度预算的分类)
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 31),
|
||||
TransactionType.Expense,
|
||||
Arg.Is<List<string>>(list => list.Count == 1 && list.Contains("餐饮")), // 只包含月度预算的分类
|
||||
false)
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
{ new DateTime(2024, 1, 15), 800m } // 1月15日月度吃饭累计800
|
||||
});
|
||||
|
||||
// 年度统计使用GetCurrentAmountAsync
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(args =>
|
||||
{
|
||||
var b = (BudgetRecord)args[0];
|
||||
var startDate = (DateTime)args[1];
|
||||
|
||||
// 年度范围查询 - 年度旅游
|
||||
if (startDate.Month == 1 && startDate.Day == 1)
|
||||
{
|
||||
return b.Name == "年度旅游" ? 2000m : 0m;
|
||||
}
|
||||
return 0m;
|
||||
});
|
||||
|
||||
// 年度趋势统计(包含所有分类)
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 12 && d.Day == 31),
|
||||
TransactionType.Expense,
|
||||
Arg.Is<List<string>>(list => list.Count == 2), // 包含所有分类:餐饮、旅游
|
||||
true)
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
{ new DateTime(2024, 1, 1), 2800m } // 1月累计:月度吃饭800 + 年度旅游2000 = 2800
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Assert
|
||||
// 月度统计中:只包含月度预算
|
||||
result.Month.Limit.Should().Be(3000); // 月度吃饭3000
|
||||
result.Month.Current.Should().Be(800); // 月度吃饭已用800
|
||||
result.Month.Count.Should().Be(1); // 只包含1个月度预算
|
||||
|
||||
// 年度统计中:包含所有预算(月度预算按剩余月份折算)
|
||||
// 1月时剩余月份 = 12 - 1 + 1 = 12个月
|
||||
result.Year.Limit.Should().Be(12000 + (3000 * 12)); // 年度旅游12000 + 月度吃饭折算年度(3000*12=36000) = 48000
|
||||
result.Year.Current.Should().Be(2000 + 800); // 年度旅游2000 + 月度吃饭800 = 2800
|
||||
result.Year.Count.Should().Be(2); // 包含2个预算(1个月度+1个年度)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_年度_1月_硬性收支_Test()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2024, 1, 1); // 元旦
|
||||
var budgets = new List<BudgetRecord>
|
||||
{
|
||||
new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true }
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()).Returns(0m);
|
||||
_dateTimeProvider.Now.Returns(referenceDate);
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||||
.Returns(new Dictionary<DateTime, decimal>());
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Assert
|
||||
// 2024是闰年,366天。1月1号是第1天。
|
||||
// 3660 * 1 / 366 = 10
|
||||
result.Year.Limit.Should().Be(3660);
|
||||
result.Year.Current.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_年度_3月_硬性收支_Test()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2024, 3, 31); // 3月最后一天
|
||||
var budgets = new List<BudgetRecord>
|
||||
{
|
||||
new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true }
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()).Returns(0m);
|
||||
_dateTimeProvider.Now.Returns(referenceDate);
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||||
.Returns(new Dictionary<DateTime, decimal>());
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Assert
|
||||
// 2024是闰年。1月(31) + 2月(29) + 3月(31) = 91天
|
||||
// 3660 * 91 / 366 = 910
|
||||
result.Year.Limit.Should().Be(3660);
|
||||
result.Year.Current.Should().Be(910);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_月度_发生年度收支_Test()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2024, 1, 15);
|
||||
|
||||
// 设置预算:包含月度预算和年度预算
|
||||
var budgets = new List<BudgetRecord>
|
||||
{
|
||||
// 月度预算:吃喝
|
||||
new() { Id = 1, Name = "吃喝", Limit = 2000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" },
|
||||
|
||||
// 月度预算:交通
|
||||
new() { Id = 2, Name = "交通", Limit = 500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "交通" },
|
||||
|
||||
// 年度预算:年度旅游(当前月度发生了相关支出)
|
||||
new() { Id = 3, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, SelectedCategories = "旅游,度假" },
|
||||
|
||||
// 年度预算:年度奖金(当前月度发生了相关收入)
|
||||
new() { Id = 4, Name = "年度奖金", Limit = 50000, Category = BudgetCategory.Income, Type = BudgetPeriodType.Year, SelectedCategories = "奖金,年终奖" }
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
|
||||
// 设置月度预算的当前金额
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(args =>
|
||||
{
|
||||
var b = (BudgetRecord)args[0];
|
||||
return b.Name switch
|
||||
{
|
||||
"吃喝" => 1200m, // 月度预算:已用1200元
|
||||
"交通" => 300m, // 月度预算:已用300元
|
||||
"年度旅游" => 2000m, // 年度预算:1月份已用2000元
|
||||
"年度奖金" => 10000m, // 年度预算:1月份已收10000元
|
||||
_ => 0m
|
||||
};
|
||||
});
|
||||
|
||||
// 设置月度趋势统计数据:只包含月度预算相关的分类(餐饮、零食、交通)
|
||||
// 注意:不应包含年度预算的分类(旅游、度假、奖金、年终奖)
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||||
TransactionType.Expense,
|
||||
Arg.Is<List<string>>(list => list.Count == 3 && list.Contains("餐饮") && list.Contains("零食") && list.Contains("交通")),
|
||||
false)
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
{ new DateTime(2024, 1, 15), 1500m } // 1月15日累计1500(吃喝1200+交通300,不包含年度旅游2000)
|
||||
});
|
||||
|
||||
// 设置年度趋势统计数据:包含所有预算相关的分类
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 12 && d.Day == 31),
|
||||
TransactionType.Expense,
|
||||
Arg.Is<List<string>>(list => list.Count == 5), // 餐饮、零食、交通、旅游、度假
|
||||
true)
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
{ new DateTime(2024, 1, 1), 3500m } // 1月累计3500(吃喝1200+交通300+年度旅游2000)
|
||||
});
|
||||
|
||||
// 设置收入相关的趋势统计数据
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||||
TransactionType.Income,
|
||||
Arg.Any<List<string>>(),
|
||||
false)
|
||||
.Returns(new Dictionary<DateTime, decimal>()); // 月度收入为空
|
||||
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 12 && d.Day == 31),
|
||||
TransactionType.Income,
|
||||
Arg.Any<List<string>>(),
|
||||
true)
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
{ new DateTime(2024, 1, 1), 10000m } // 年度奖金10000
|
||||
});
|
||||
|
||||
// Act - 测试支出统计
|
||||
var expenseResult = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Act - 测试收入统计
|
||||
var incomeResult = await _service.GetCategoryStatsAsync(BudgetCategory.Income, referenceDate);
|
||||
|
||||
// Assert - 支出月度统计:只包含月度预算,不包含年度预算
|
||||
expenseResult.Month.Limit.Should().Be(2500); // 吃喝2000 + 交通500
|
||||
expenseResult.Month.Current.Should().Be(1500); // 吃喝1200 + 交通300(不包含年度旅游2000)
|
||||
expenseResult.Month.Count.Should().Be(2); // 只包含2个月度预算
|
||||
expenseResult.Month.Rate.Should().Be(1500m / 2500m * 100); // 60%
|
||||
|
||||
// Assert - 支出年度统计:包含所有预算(月度+年度)
|
||||
// 1月时剩余月份 = 12 - 1 + 1 = 12个月
|
||||
expenseResult.Year.Limit.Should().Be(12000 + (2500 * 12)); // 年度旅游12000 + 月度预算折算为年度(2500*12)
|
||||
expenseResult.Year.Current.Should().Be(3500); // 吃喝1200 + 交通300 + 年度旅游2000
|
||||
expenseResult.Year.Count.Should().Be(3); // 包含3个预算(2个月度+1个年度)
|
||||
|
||||
// Assert - 收入月度统计:只包含月度预算(这里没有月度收入预算,所以应该为0)
|
||||
incomeResult.Month.Limit.Should().Be(0); // 没有月度收入预算
|
||||
incomeResult.Month.Current.Should().Be(0); // 没有月度收入预算(不包含年度奖金10000)
|
||||
incomeResult.Month.Count.Should().Be(0); // 没有月度收入预算
|
||||
|
||||
// Assert - 收入年度统计:包含所有预算(只有年度收入预算)
|
||||
incomeResult.Year.Limit.Should().Be(50000); // 年度奖金50000
|
||||
incomeResult.Year.Current.Should().Be(10000); // 年度奖金已收10000
|
||||
incomeResult.Year.Count.Should().Be(1); // 包含1个年度收入预算
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_年度_3月_2月预算变更_Test()
|
||||
{
|
||||
// Arrange
|
||||
// 测试场景:2024年3月查看年度预算统计,其中2月份发生了预算变更(吃喝预算从2000增加到2500)
|
||||
var referenceDate = new DateTime(2024, 3, 15);
|
||||
|
||||
// 设置当前时间,确保3月被认为是当前月份
|
||||
_dateTimeProvider.Now.Returns(new DateTime(2024, 3, 15));
|
||||
|
||||
// 当前3月份有效的预算
|
||||
var currentBudgets = new List<BudgetRecord>
|
||||
{
|
||||
new() { Id = 1, Name = "吃喝", Limit = 2500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" }, // 2月预算变更后
|
||||
new() { Id = 2, Name = "交通", Limit = 500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "交通" },
|
||||
new() { Id = 3, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, SelectedCategories = "旅游,度假" }
|
||||
};
|
||||
|
||||
// 2月份的归档数据(预算变更前)
|
||||
var febArchive = new BudgetArchive
|
||||
{
|
||||
Year = 2024,
|
||||
Month = 2,
|
||||
Content = new[]
|
||||
{
|
||||
new BudgetArchiveContent
|
||||
{
|
||||
Id = 1,
|
||||
Name = "吃喝",
|
||||
Limit = 2000, // 2月份时预算还是2000
|
||||
Actual = 1800, // 2月份实际花费1800
|
||||
Category = BudgetCategory.Expense,
|
||||
Type = BudgetPeriodType.Month,
|
||||
SelectedCategories = new[] { "餐饮", "零食" }
|
||||
},
|
||||
new BudgetArchiveContent
|
||||
{
|
||||
Id = 2,
|
||||
Name = "交通",
|
||||
Limit = 500,
|
||||
Actual = 300,
|
||||
Category = BudgetCategory.Expense,
|
||||
Type = BudgetPeriodType.Month,
|
||||
SelectedCategories = new[] { "交通" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 1月份的归档数据
|
||||
var janArchive = new BudgetArchive
|
||||
{
|
||||
Year = 2024,
|
||||
Month = 1,
|
||||
Content = new[]
|
||||
{
|
||||
new BudgetArchiveContent
|
||||
{
|
||||
Id = 1,
|
||||
Name = "吃喝",
|
||||
Limit = 2000, // 1月份预算也是2000
|
||||
Actual = 1500, // 1月份实际花费1500
|
||||
Category = BudgetCategory.Expense,
|
||||
Type = BudgetPeriodType.Month,
|
||||
SelectedCategories = new[] { "餐饮", "零食" }
|
||||
},
|
||||
new BudgetArchiveContent
|
||||
{
|
||||
Id = 2,
|
||||
Name = "交通",
|
||||
Limit = 500,
|
||||
Actual = 250,
|
||||
Category = BudgetCategory.Expense,
|
||||
Type = BudgetPeriodType.Month,
|
||||
SelectedCategories = new[] { "交通" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 设置仓储响应
|
||||
_budgetRepository.GetAllAsync().Returns(currentBudgets);
|
||||
|
||||
// 设置归档仓储响应
|
||||
_budgetArchiveRepository.GetArchiveAsync(2024, 2).Returns(febArchive);
|
||||
_budgetArchiveRepository.GetArchiveAsync(2024, 1).Returns(janArchive);
|
||||
|
||||
// 设置月度预算的当前金额查询(仅用于3月份)
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Is<DateTime>(d => d.Month == 3), Arg.Is<DateTime>(d => d.Month == 3))
|
||||
.Returns(args =>
|
||||
{
|
||||
var b = (BudgetRecord)args[0];
|
||||
return b.Name switch
|
||||
{
|
||||
"吃喝" => 800m, // 3月份已花费800
|
||||
"交通" => 200m, // 3月份已花费200
|
||||
_ => 0m
|
||||
};
|
||||
});
|
||||
|
||||
// 年度旅游的年度金额查询
|
||||
_budgetRepository.GetCurrentAmountAsync(
|
||||
Arg.Is<BudgetRecord>(b => b.Id == 3),
|
||||
Arg.Is<DateTime>(d => d.Month == 1),
|
||||
Arg.Is<DateTime>(d => d.Month == 12))
|
||||
.Returns(2500m); // 年度旅游1-3月已花费2500
|
||||
|
||||
// 设置趋势统计数据查询(用于月度统计)
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Month == 3),
|
||||
Arg.Is<DateTime>(d => d.Month == 3),
|
||||
Arg.Any<TransactionType>(),
|
||||
Arg.Any<List<string>>(),
|
||||
Arg.Any<bool>())
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
{ new DateTime(2024, 3, 15), 1000m } // 3月15日累计1000(吃喝800+交通200)
|
||||
});
|
||||
|
||||
// 年度趋势统计数据查询
|
||||
// 注意:年度统计使用GetFilteredTrendStatisticsAsync获取趋势数据
|
||||
// 需要返回所有分类的累计金额,包括年度旅游的2500
|
||||
_transactionsRepository.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Month == 1),
|
||||
Arg.Is<DateTime>(d => d.Month == 12),
|
||||
Arg.Any<TransactionType>(),
|
||||
Arg.Any<List<string>>(),
|
||||
Arg.Is<bool>(b => b == true))
|
||||
.Returns(new Dictionary<DateTime, decimal>
|
||||
{
|
||||
// 3月累计:月度预算1000 + 年度旅游2500 = 3500
|
||||
{ new DateTime(2024, 3, 1), 3500m }
|
||||
});
|
||||
|
||||
// Act
|
||||
// 直接测试BudgetStatsService,而不是通过BudgetService
|
||||
var budgetStatsService = new BudgetStatsService(
|
||||
_budgetRepository,
|
||||
_budgetArchiveRepository,
|
||||
_transactionsRepository,
|
||||
_dateTimeProvider,
|
||||
Substitute.For<ILogger<BudgetStatsService>>()
|
||||
);
|
||||
|
||||
var result = await budgetStatsService.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Assert - 月度统计(3月份)
|
||||
// 月度统计应该只包含月度预算,使用当前预算限额
|
||||
result.Month.Limit.Should().Be(3000); // 吃喝2500 + 交通500(使用变更后的预算)
|
||||
result.Month.Current.Should().Be(1000); // 吃喝800 + 交通200
|
||||
result.Month.Count.Should().Be(2); // 包含2个月度预算
|
||||
|
||||
// Assert - 年度统计(需要考虑预算变更和剩余月份)
|
||||
// 新逻辑:
|
||||
// 1. 对于归档数据,直接使用归档的限额,不折算
|
||||
// 2. 对于当前及未来月份,使用当前预算 × 剩余月份
|
||||
//
|
||||
// 预期年度限额计算:
|
||||
// 1月归档:吃喝2000 + 交通500 = 2500
|
||||
// 2月归档:吃喝2000 + 交通500 = 2500
|
||||
// 3-12月剩余(12 - 3 + 1 = 10个月):吃喝2500×10 + 交通500×10 = 30000
|
||||
// 年度旅游:12000
|
||||
// 总计:2500 + 2500 + 30000 + 12000 = 47000
|
||||
result.Year.Limit.Should().Be(47000);
|
||||
|
||||
// 预期年度实际金额:
|
||||
// 根据趋势统计数据,3月累计: 月度预算1000 + 年度旅游2500 = 3500
|
||||
result.Year.Current.Should().Be(3500);
|
||||
|
||||
// 应该包含:
|
||||
// - 1月归档的月度预算:吃喝、1个
|
||||
// - 1月归档的月度预算:交通、1个
|
||||
// - 2月归档的月度预算:吃喝、1个
|
||||
// - 2月归档的月度预算:交通、1个
|
||||
// - 3-12月的月度预算:吃喝、1个
|
||||
// - 3-12月的月度预算:交通、1个
|
||||
// - 年度旅游:1个
|
||||
// 总计:7个
|
||||
result.Year.Count.Should().Be(7);
|
||||
|
||||
// 验证使用率计算正确
|
||||
result.Month.Rate.Should().BeApproximately(1000m / 3000m * 100, 0.01m);
|
||||
result.Year.Rate.Should().BeApproximately(3500m / 47000m * 100, 0.01m);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user