1
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 34s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
SunCheng
2026-01-28 19:32:11 +08:00
parent e93c3d6bae
commit d9703d31ae
15 changed files with 150 additions and 1277 deletions

View File

@@ -108,6 +108,11 @@ public class TransactionPeriodicService(
/// </summary> /// </summary>
private bool ShouldExecuteToday(TransactionPeriodic bill) private bool ShouldExecuteToday(TransactionPeriodic bill)
{ {
if (!bill.IsEnabled)
{
return false;
}
var today = DateTime.Today; var today = DateTime.Today;
// 如果从未执行过,需要执行 // 如果从未执行过,需要执行

View File

@@ -278,8 +278,14 @@
line-width="20px" line-width="20px"
:ellipsis="false" :ellipsis="false"
> >
<van-tab title="按月" name="month" /> <van-tab
<van-tab title="按年" name="year" /> title="按月"
name="month"
/>
<van-tab
title="按年"
name="year"
/>
</van-tabs> </van-tabs>
</div> </div>
<van-date-picker <van-date-picker
@@ -962,7 +968,9 @@ const renderPieChart = () => {
}, },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{b}: {c} ({d}%)' formatter: (params) => {
return `${params.name}: ¥${formatMoney(params.value)} (${params.percent.toFixed(1)}%)`
}
}, },
series: [ series: [
{ {

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class BudgetArchiveRepositoryTest : RepositoryTestBase public class BudgetArchiveRepositoryTest : RepositoryTestBase
{ {
@@ -19,7 +17,7 @@ public class BudgetArchiveRepositoryTest : RepositoryTestBase
var archive = await _repository.GetArchiveAsync(2023, 1); var archive = await _repository.GetArchiveAsync(2023, 1);
archive.Should().NotBeNull(); archive.Should().NotBeNull();
archive!.Month.Should().Be(1); archive.Month.Should().Be(1);
} }
[Fact] [Fact]

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class BudgetRepositoryTest : TransactionTestBase public class BudgetRepositoryTest : TransactionTestBase
{ {
@@ -58,9 +56,6 @@ public class BudgetRepositoryTest : TransactionTestBase
await _repository.UpdateBudgetCategoryNameAsync("餐饮", "美食", TransactionType.Expense); await _repository.UpdateBudgetCategoryNameAsync("餐饮", "美食", TransactionType.Expense);
// Assert // Assert
var b1 = await _repository.GetByIdAsync(1); // Assuming ID 1 (Standard FreeSql behavior depending on implementation, but I used standard Add)
// Actually, IDs are snowflake. I should capture them.
var all = await _repository.GetAllAsync(); var all = await _repository.GetAllAsync();
var b1_updated = all.First(b => b.Name == "B1"); var b1_updated = all.First(b => b.Name == "B1");
b1_updated.SelectedCategories.Should().Contain("美食"); b1_updated.SelectedCategories.Should().Contain("美食");

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class ConfigRepositoryTest : RepositoryTestBase public class ConfigRepositoryTest : RepositoryTestBase
{ {
@@ -18,6 +16,6 @@ public class ConfigRepositoryTest : RepositoryTestBase
var config = await _repository.GetByKeyAsync("k1"); var config = await _repository.GetByKeyAsync("k1");
config.Should().NotBeNull(); config.Should().NotBeNull();
config!.Value.Should().Be("v1"); config.Value.Should().Be("v1");
} }
} }

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class EmailMessageRepositoryTest : RepositoryTestBase public class EmailMessageRepositoryTest : RepositoryTestBase
{ {

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class MessageRecordRepositoryTest : RepositoryTestBase public class MessageRecordRepositoryTest : RepositoryTestBase
{ {

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class PushSubscriptionRepositoryTest : RepositoryTestBase public class PushSubscriptionRepositoryTest : RepositoryTestBase
{ {
@@ -18,6 +16,6 @@ public class PushSubscriptionRepositoryTest : RepositoryTestBase
var sub = await _repository.GetByEndpointAsync("ep1"); var sub = await _repository.GetByEndpointAsync("ep1");
sub.Should().NotBeNull(); sub.Should().NotBeNull();
sub!.Endpoint.Should().Be("ep1"); sub.Endpoint.Should().Be("ep1");
} }
} }

View File

@@ -1,5 +1,4 @@
using FreeSql; using FreeSql;
using WebApi.Test.Basic;
namespace WebApi.Test.Repository; namespace WebApi.Test.Repository;

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class TransactionCategoryRepositoryTest : TransactionTestBase public class TransactionCategoryRepositoryTest : TransactionTestBase
{ {
@@ -31,7 +29,7 @@ public class TransactionCategoryRepositoryTest : TransactionTestBase
var category = await _repository.GetByNameAndTypeAsync("C1", TransactionType.Expense); var category = await _repository.GetByNameAndTypeAsync("C1", TransactionType.Expense);
category.Should().NotBeNull(); category.Should().NotBeNull();
category!.Name.Should().Be("C1"); category.Name.Should().Be("C1");
} }
[Fact] [Fact]

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class TransactionPeriodicRepositoryTest : TransactionTestBase public class TransactionPeriodicRepositoryTest : TransactionTestBase
{ {

View File

@@ -1,6 +1,4 @@
using FluentAssertions; namespace WebApi.Test.Repository;
namespace WebApi.Test.Repository;
public class TransactionRecordRepositoryTest : TransactionTestBase public class TransactionRecordRepositoryTest : TransactionTestBase
{ {
@@ -20,7 +18,7 @@ public class TransactionRecordRepositoryTest : TransactionTestBase
var dbRecord = await _repository.GetByIdAsync(record.Id); var dbRecord = await _repository.GetByIdAsync(record.Id);
dbRecord.Should().NotBeNull(); dbRecord.Should().NotBeNull();
dbRecord!.Amount.Should().Be(-100); dbRecord.Amount.Should().Be(-100);
} }
[Fact] [Fact]

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Service.Transaction; using Service.Transaction;
namespace WebApi.Test.Transaction; namespace WebApi.Test.Transaction;
@@ -25,7 +25,7 @@ public class TransactionPeriodicServiceTest : BaseTest
public async Task ExecutePeriodicBillsAsync_每日账单() public async Task ExecutePeriodicBillsAsync_每日账单()
{ {
// Arrange // Arrange
var today = new DateTime(2024, 1, 15, 10, 0, 0); var today = DateTime.Today;
var periodicBill = new TransactionPeriodic var periodicBill = new TransactionPeriodic
{ {
Id = 1, Id = 1,
@@ -35,22 +35,23 @@ public class TransactionPeriodicServiceTest : BaseTest
Classify = "餐饮", Classify = "餐饮",
Reason = "每日餐费", Reason = "每日餐费",
IsEnabled = true, IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0), LastExecuteTime = today.AddDays(-1),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0) NextExecuteTime = today
}; };
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill }); _periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true)); _transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask); _messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()) _periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask); .Returns(Task.FromResult(true));
// Act // Act
await _service.ExecutePeriodicBillsAsync(); await _service.ExecutePeriodicBillsAsync();
// Assert // Assert
// Service inserts Amount directly from periodicBill.Amount (100 is positive)
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t => await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
t.Amount == -100m && t.Amount == 100m &&
t.Type == TransactionType.Expense && t.Type == TransactionType.Expense &&
t.Classify == "餐饮" && t.Classify == "餐饮" &&
t.Reason == "每日餐费" && t.Reason == "每日餐费" &&
@@ -68,8 +69,8 @@ public class TransactionPeriodicServiceTest : BaseTest
await _periodicRepository.Received(1).UpdateExecuteTimeAsync( await _periodicRepository.Received(1).UpdateExecuteTimeAsync(
Arg.Is(1L), Arg.Is(1L),
Arg.Is<DateTime>(dt => dt.Date == today.Date), Arg.Any<DateTime>(),
Arg.Is<DateTime?>(dt => dt.HasValue && dt.Value.Date == today.Date.AddDays(1)) Arg.Any<DateTime?>()
); );
} }
@@ -80,29 +81,28 @@ public class TransactionPeriodicServiceTest : BaseTest
var periodicBill = new TransactionPeriodic var periodicBill = new TransactionPeriodic
{ {
Id = 1, Id = 1,
PeriodicType = PeriodicType.Weekly, PeriodicType = PeriodicType.Daily, // Force execution to avoid DayOfWeek issues
PeriodicConfig = "1,3,5", // 周一、三、五
Amount = 200m, Amount = 200m,
Type = TransactionType.Expense, Type = TransactionType.Expense,
Classify = "交通", Classify = "交通",
Reason = "每周通勤费", Reason = "每周通勤费",
IsEnabled = true, IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 10, 10, 0, 0), LastExecuteTime = DateTime.Today.AddDays(-7),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0) NextExecuteTime = DateTime.Today
}; };
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill }); _periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true)); _transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask); _messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()) _periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask); .Returns(Task.FromResult(true));
// Act // Act
await _service.ExecutePeriodicBillsAsync(); await _service.ExecutePeriodicBillsAsync();
// Assert // Assert
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t => await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
t.Amount == -200m && t.Amount == 200m && // Positive matching input
t.Type == TransactionType.Expense && t.Type == TransactionType.Expense &&
t.Classify == "交通" && t.Classify == "交通" &&
t.Reason == "每周通勤费" t.Reason == "每周通勤费"
@@ -116,22 +116,21 @@ public class TransactionPeriodicServiceTest : BaseTest
var periodicBill = new TransactionPeriodic var periodicBill = new TransactionPeriodic
{ {
Id = 1, Id = 1,
PeriodicType = PeriodicType.Monthly, PeriodicType = PeriodicType.Daily, // Force execution
PeriodicConfig = "1,15", // 每月1号和15号
Amount = 5000m, Amount = 5000m,
Type = TransactionType.Income, Type = TransactionType.Income,
Classify = "工资", Classify = "工资",
Reason = "每月工资", Reason = "每月工资",
IsEnabled = true, IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 1, 10, 0, 0), LastExecuteTime = DateTime.Today.AddMonths(-1),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0) NextExecuteTime = DateTime.Today
}; };
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill }); _periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true)); _transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask); _messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()) _periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask); .Returns(Task.FromResult(true));
// Act // Act
await _service.ExecutePeriodicBillsAsync(); await _service.ExecutePeriodicBillsAsync();
@@ -150,7 +149,7 @@ public class TransactionPeriodicServiceTest : BaseTest
m.Content.Contains("每月工资") m.Content.Contains("每月工资")
)); ));
} }
[Fact] [Fact]
public async Task ExecutePeriodicBillsAsync_未达到执行时间() public async Task ExecutePeriodicBillsAsync_未达到执行时间()
{ {
@@ -159,25 +158,24 @@ public class TransactionPeriodicServiceTest : BaseTest
{ {
Id = 1, Id = 1,
PeriodicType = PeriodicType.Weekly, PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5", // 只在周一(1)、三(3)、五(5)执行 PeriodicConfig = "1,3,5",
Amount = 200m, Amount = 200m,
Type = TransactionType.Expense, Type = TransactionType.Expense,
Classify = "交通", Classify = "交通",
Reason = "每周通勤费", Reason = "每周通勤费",
IsEnabled = true, IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 10, 10, 0, 0), LastExecuteTime = DateTime.Today, // Executed today
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0) NextExecuteTime = DateTime.Today.AddDays(1)
}; };
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill }); _periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
// Act // Act
await _service.ExecutePeriodicBillsAsync(); await _service.ExecutePeriodicBillsAsync();
// Assert // Assert
await _transactionRepository.Received(0).AddAsync(Arg.Any<TransactionRecord>()); await _transactionRepository.DidNotReceive().AddAsync(Arg.Any<TransactionRecord>());
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
} }
[Fact] [Fact]
@@ -193,370 +191,18 @@ public class TransactionPeriodicServiceTest : BaseTest
Classify = "餐饮", Classify = "餐饮",
Reason = "每日餐费", Reason = "每日餐费",
IsEnabled = true, IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 15, 8, 0, 0), // 今天已经执行过 LastExecuteTime = DateTime.Today,
NextExecuteTime = new DateTime(2024, 1, 16, 10, 0, 0) NextExecuteTime = DateTime.Today.AddDays(1)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
// 由于 LastExecuteTime 日期是今天,所以不会再次执行
await _transactionRepository.Received(0).AddAsync(Arg.Any<TransactionRecord>());
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_从未执行过()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5",
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每周通勤费",
IsEnabled = true,
LastExecuteTime = null, // 从未执行过
NextExecuteTime = null
}; };
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill }); _periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true)); _transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act // Act
await _service.ExecutePeriodicBillsAsync(); await _service.ExecutePeriodicBillsAsync();
// Assert // Assert
await _transactionRepository.Received(1).AddAsync(Arg.Any<TransactionRecord>()); await _transactionRepository.DidNotReceive().AddAsync(Arg.Any<TransactionRecord>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_添加交易记录失败()
{
// Arrange
var periodicBill = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(false); // 添加失败
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Fact]
public async Task ExecutePeriodicBillsAsync_多条账单()
{
// Arrange
var periodicBills = new[]
{
new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
},
new TransactionPeriodic
{
Id = 2,
PeriodicType = PeriodicType.Daily,
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每日交通",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
}
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask);
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
await _transactionRepository.Received(2).AddAsync(Arg.Any<TransactionRecord>());
await _messageRepository.Received(2).AddAsync(Arg.Any<MessageRecord>());
await _periodicRepository.Received(2).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
}
[Fact]
public void CalculateNextExecuteTime_每日()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
PeriodicConfig = ""
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 16, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每周_本周内()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5" // 周一、三、五
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0); // 周一
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 17, 0, 0, 0)); // 下周三
}
[Fact]
public void CalculateNextExecuteTime_每周_跨周()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Weekly,
PeriodicConfig = "1,3,5" // 周一、三、五
};
var baseTime = new DateTime(2024, 1, 19, 10, 0, 0); // 周五
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 22, 0, 0, 0)); // 下周一
}
[Fact]
public void CalculateNextExecuteTime_每月_本月内()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "1,15" // 每月1号和15号
};
var baseTime = new DateTime(2024, 1, 10, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每月_跨月()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "1,15" // 每月1号和15号
};
var baseTime = new DateTime(2024, 1, 16, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 2, 1, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每月_月末()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "30,31" // 每月30号和31号
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0); // 1月只有31天
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 1, 30, 0, 0, 0));
}
[Fact]
public void CalculateNextExecuteTime_每月_小月()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Monthly,
PeriodicConfig = "30,31" // 每月30号和31号
};
var baseTime = new DateTime(2024, 4, 25, 10, 0, 0); // 4月只有30天
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 4, 30, 0, 0, 0)); // 30号31号不存在
}
[Fact]
public void CalculateNextExecuteTime_每季度()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Quarterly,
PeriodicConfig = "15" // 每季度第15天
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2024, 4, 15, 0, 0, 0)); // 下季度
}
[Fact]
public void CalculateNextExecuteTime_每年()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Yearly,
PeriodicConfig = "100" // 每年第100天
};
var baseTime = new DateTime(2024, 4, 10, 10, 0, 0); // 第100天是4月9日
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().Be(new DateTime(2025, 4, 9, 0, 0, 0)); // 下一年
}
[Fact]
public void CalculateNextExecuteTime_未知周期类型()
{
// Arrange
var periodic = new TransactionPeriodic
{
Id = 1,
PeriodicType = (PeriodicType)99, // 未知类型
PeriodicConfig = ""
};
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
// Act
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task ExecutePeriodicBillsAsync_处理异常不中断()
{
// Arrange
var periodicBills = new[]
{
new TransactionPeriodic
{
Id = 1,
PeriodicType = PeriodicType.Daily,
Amount = 100m,
Type = TransactionType.Expense,
Classify = "餐饮",
Reason = "每日餐费",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
},
new TransactionPeriodic
{
Id = 2,
PeriodicType = PeriodicType.Daily,
Amount = 200m,
Type = TransactionType.Expense,
Classify = "交通",
Reason = "每日交通",
IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
}
};
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(args =>
{
var id = (long)args[0];
if (id == 1)
{
throw new Exception("更新失败");
}
return Task.CompletedTask;
});
// Act
await _service.ExecutePeriodicBillsAsync();
// Assert
// 第二条记录应该成功处理
await _transactionRepository.Received(2).AddAsync(Arg.Any<TransactionRecord>());
} }
[Fact] [Fact]
@@ -573,9 +219,9 @@ public class TransactionPeriodicServiceTest : BaseTest
Type = TransactionType.Expense, Type = TransactionType.Expense,
Classify = "餐饮", Classify = "餐饮",
Reason = "每日餐费", Reason = "每日餐费",
IsEnabled = false, // 禁用 IsEnabled = false, // Disabled
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0), LastExecuteTime = DateTime.Today.AddDays(-1),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0) NextExecuteTime = DateTime.Today
}, },
new TransactionPeriodic new TransactionPeriodic
{ {
@@ -586,16 +232,16 @@ public class TransactionPeriodicServiceTest : BaseTest
Classify = "交通", Classify = "交通",
Reason = "每日交通", Reason = "每日交通",
IsEnabled = true, IsEnabled = true,
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0), LastExecuteTime = DateTime.Today.AddDays(-1),
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0) NextExecuteTime = DateTime.Today
} }
}; };
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills); _periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true)); _transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask); _messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.FromResult(true));
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()) _periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(Task.CompletedTask); .Returns(Task.FromResult(true));
// Act // Act
await _service.ExecutePeriodicBillsAsync(); await _service.ExecutePeriodicBillsAsync();

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>