320 lines
11 KiB
C#
320 lines
11 KiB
C#
namespace Service;
|
|
|
|
/// <summary>
|
|
/// 周期性账单服务接口
|
|
/// </summary>
|
|
public interface ITransactionPeriodicService
|
|
{
|
|
/// <summary>
|
|
/// 执行周期性账单检查和生成
|
|
/// </summary>
|
|
Task ExecutePeriodicBillsAsync();
|
|
|
|
/// <summary>
|
|
/// 计算下次执行时间
|
|
/// </summary>
|
|
DateTime? CalculateNextExecuteTime(TransactionPeriodic periodic, DateTime baseTime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 周期性账单服务实现
|
|
/// </summary>
|
|
public class TransactionPeriodicService(
|
|
ITransactionPeriodicRepository periodicRepository,
|
|
ITransactionRecordRepository transactionRepository,
|
|
IMessageRecordRepository messageRepository,
|
|
ILogger<TransactionPeriodicService> logger
|
|
) : ITransactionPeriodicService
|
|
{
|
|
public async Task ExecutePeriodicBillsAsync()
|
|
{
|
|
try
|
|
{
|
|
logger.LogInformation("开始执行周期性账单检查...");
|
|
|
|
var pendingBills = await periodicRepository.GetPendingPeriodicBillsAsync();
|
|
var billsList = pendingBills.ToList();
|
|
|
|
logger.LogInformation("找到 {Count} 条需要执行的周期性账单", billsList.Count);
|
|
|
|
foreach (var bill in billsList)
|
|
{
|
|
try
|
|
{
|
|
// 检查是否满足执行条件
|
|
if (!ShouldExecuteToday(bill))
|
|
{
|
|
logger.LogInformation("周期性账单 {Id} 今天不需要执行", bill.Id);
|
|
continue;
|
|
}
|
|
|
|
// 创建交易记录
|
|
var transaction = new TransactionRecord
|
|
{
|
|
Amount = bill.Amount,
|
|
Type = bill.Type,
|
|
Classify = bill.Classify,
|
|
Reason = bill.Reason,
|
|
OccurredAt = DateTime.Now,
|
|
Card = "周期性账单",
|
|
ImportFrom = "周期性账单自动生成"
|
|
};
|
|
|
|
var success = await transactionRepository.AddAsync(transaction);
|
|
|
|
if (success)
|
|
{
|
|
logger.LogInformation("成功创建周期性账单交易记录: {Reason}, 金额: {Amount}",
|
|
bill.Reason, bill.Amount);
|
|
|
|
// 创建未读消息
|
|
var message = new MessageRecord
|
|
{
|
|
Title = "周期性账单提醒",
|
|
Content = $"已自动生成{(bill.Type == TransactionType.Expense ? "支出" : "收入")}账单:{bill.Reason},金额:{bill.Amount:F2}元",
|
|
IsRead = false
|
|
};
|
|
await messageRepository.AddAsync(message);
|
|
|
|
// 更新执行时间
|
|
var now = DateTime.Now;
|
|
var nextTime = CalculateNextExecuteTime(bill, now);
|
|
await periodicRepository.UpdateExecuteTimeAsync(bill.Id, now, nextTime);
|
|
|
|
logger.LogInformation("周期性账单 {Id} 下次执行时间: {NextTime}",
|
|
bill.Id, nextTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "无");
|
|
}
|
|
else
|
|
{
|
|
logger.LogWarning("创建周期性账单交易记录失败: {BillId}", bill.Id);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "处理周期性账单 {BillId} 时出错", bill.Id);
|
|
}
|
|
}
|
|
|
|
logger.LogInformation("周期性账单检查执行完成");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "执行周期性账单检查时发生错误");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断今天是否需要执行
|
|
/// </summary>
|
|
private bool ShouldExecuteToday(TransactionPeriodic bill)
|
|
{
|
|
var today = DateTime.Today;
|
|
|
|
// 如果从未执行过,需要执行
|
|
if (bill.LastExecuteTime == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// 如果今天已经执行过,不需要再执行
|
|
if (bill.LastExecuteTime.Value.Date == today)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return bill.PeriodicType switch
|
|
{
|
|
PeriodicType.Daily => true, // 每天都执行
|
|
PeriodicType.Weekly => ShouldExecuteWeekly(bill.PeriodicConfig, today),
|
|
PeriodicType.Monthly => ShouldExecuteMonthly(bill.PeriodicConfig, today),
|
|
PeriodicType.Quarterly => ShouldExecuteQuarterly(bill.PeriodicConfig, today),
|
|
PeriodicType.Yearly => ShouldExecuteYearly(bill.PeriodicConfig, today),
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断是否需要在本周执行
|
|
/// </summary>
|
|
private bool ShouldExecuteWeekly(string config, DateTime today)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config))
|
|
return false;
|
|
|
|
var dayOfWeek = (int)today.DayOfWeek; // 0=Sunday, 1=Monday, ..., 6=Saturday
|
|
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1)
|
|
.Where(d => d is >= 0 and <= 6)
|
|
.ToList();
|
|
|
|
return executeDays.Contains(dayOfWeek);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断是否需要在本月执行
|
|
/// </summary>
|
|
private bool ShouldExecuteMonthly(string config, DateTime today)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config))
|
|
return false;
|
|
|
|
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1)
|
|
.Where(d => d is >= 1 and <= 31)
|
|
.ToList();
|
|
|
|
// 如果当前为月末,且配置中有大于当月天数的日期,则也执行
|
|
var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month);
|
|
if (today.Day == daysInMonth && executeDays.Any(d => d > daysInMonth))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return executeDays.Contains(today.Day);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断是否需要在本季度执行
|
|
/// </summary>
|
|
private bool ShouldExecuteQuarterly(string config, DateTime today)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config) || !int.TryParse(config, out var dayOfQuarter))
|
|
return false;
|
|
|
|
// 计算当前是本季度的第几天
|
|
var quarterStartMonth = (today.Month - 1) / 3 * 3 + 1;
|
|
var quarterStart = new DateTime(today.Year, quarterStartMonth, 1);
|
|
var daysSinceQuarterStart = (today - quarterStart).Days + 1;
|
|
|
|
return daysSinceQuarterStart == dayOfQuarter;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 判断是否需要在本年执行
|
|
/// </summary>
|
|
private bool ShouldExecuteYearly(string config, DateTime today)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config) || !int.TryParse(config, out var dayOfYear))
|
|
return false;
|
|
|
|
return today.DayOfYear == dayOfYear;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算下次执行时间
|
|
/// </summary>
|
|
public DateTime? CalculateNextExecuteTime(TransactionPeriodic periodic, DateTime baseTime)
|
|
{
|
|
return periodic.PeriodicType switch
|
|
{
|
|
PeriodicType.Daily => baseTime.Date.AddDays(1),
|
|
PeriodicType.Weekly => CalculateNextWeekly(periodic.PeriodicConfig, baseTime),
|
|
PeriodicType.Monthly => CalculateNextMonthly(periodic.PeriodicConfig, baseTime),
|
|
PeriodicType.Quarterly => CalculateNextQuarterly(periodic.PeriodicConfig, baseTime),
|
|
PeriodicType.Yearly => CalculateNextYearly(periodic.PeriodicConfig, baseTime),
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private DateTime? CalculateNextWeekly(string config, DateTime baseTime)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config))
|
|
return null;
|
|
|
|
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1)
|
|
.Where(d => d is >= 0 and <= 6)
|
|
.OrderBy(d => d)
|
|
.ToList();
|
|
|
|
if (!executeDays.Any())
|
|
return null;
|
|
|
|
var currentDayOfWeek = (int)baseTime.DayOfWeek;
|
|
|
|
// 找下一个执行日
|
|
var nextDay = executeDays.FirstOrDefault(d => d > currentDayOfWeek);
|
|
if (nextDay > 0)
|
|
{
|
|
var daysToAdd = nextDay - currentDayOfWeek;
|
|
return baseTime.Date.AddDays(daysToAdd);
|
|
}
|
|
|
|
// 下周的第一个执行日
|
|
var firstDay = executeDays.First();
|
|
var daysUntilNextWeek = 7 - currentDayOfWeek + firstDay;
|
|
return baseTime.Date.AddDays(daysUntilNextWeek);
|
|
}
|
|
|
|
private DateTime? CalculateNextMonthly(string config, DateTime baseTime)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config))
|
|
return null;
|
|
|
|
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1)
|
|
.Where(d => d is >= 1 and <= 31)
|
|
.OrderBy(d => d)
|
|
.ToList();
|
|
|
|
if (!executeDays.Any())
|
|
return null;
|
|
|
|
// 找本月的下一个执行日
|
|
var nextDay = executeDays.FirstOrDefault(d => d > baseTime.Day);
|
|
if (nextDay > 0)
|
|
{
|
|
var daysInMonth = DateTime.DaysInMonth(baseTime.Year, baseTime.Month);
|
|
if (nextDay <= daysInMonth)
|
|
{
|
|
return new DateTime(baseTime.Year, baseTime.Month, nextDay);
|
|
}
|
|
}
|
|
|
|
// 下个月的第一个执行日
|
|
var nextMonth = baseTime.AddMonths(1);
|
|
var firstDay = executeDays.First();
|
|
var daysInNextMonth = DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month);
|
|
var actualDay = Math.Min(firstDay, daysInNextMonth);
|
|
return new DateTime(nextMonth.Year, nextMonth.Month, actualDay);
|
|
}
|
|
|
|
private DateTime? CalculateNextQuarterly(string config, DateTime baseTime)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config) || !int.TryParse(config, out var dayOfQuarter))
|
|
return null;
|
|
|
|
// 计算下一个季度的开始时间
|
|
var currentQuarterStartMonth = ((baseTime.Month - 1) / 3) * 3 + 1;
|
|
var nextQuarterStartMonth = currentQuarterStartMonth + 3;
|
|
var nextQuarterYear = baseTime.Year;
|
|
|
|
if (nextQuarterStartMonth > 12)
|
|
{
|
|
nextQuarterStartMonth = 1;
|
|
nextQuarterYear++;
|
|
}
|
|
|
|
var nextQuarterStart = new DateTime(nextQuarterYear, nextQuarterStartMonth, 1);
|
|
return nextQuarterStart.AddDays(dayOfQuarter - 1);
|
|
}
|
|
|
|
private DateTime? CalculateNextYearly(string config, DateTime baseTime)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(config) || !int.TryParse(config, out var dayOfYear))
|
|
return null;
|
|
|
|
var nextYear = baseTime.Year;
|
|
if (baseTime.DayOfYear >= dayOfYear)
|
|
{
|
|
nextYear++;
|
|
}
|
|
|
|
// 处理闰年情况
|
|
var daysInYear = DateTime.IsLeapYear(nextYear) ? 366 : 365;
|
|
var actualDay = Math.Min(dayOfYear, daysInYear);
|
|
|
|
return new DateTime(nextYear, 1, 1).AddDays(actualDay - 1);
|
|
}
|
|
}
|