namespace Service.Transaction; /// /// 周期性账单服务接口 /// public interface ITransactionPeriodicService { /// /// 执行周期性账单检查和生成 /// Task ExecutePeriodicBillsAsync(); /// /// 计算下次执行时间 /// DateTime? CalculateNextExecuteTime(TransactionPeriodic periodic, DateTime baseTime); } /// /// 周期性账单服务实现 /// public class TransactionPeriodicService( ITransactionPeriodicRepository periodicRepository, ITransactionRecordRepository transactionRepository, IMessageRecordRepository messageRepository, ILogger 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, "执行周期性账单检查时发生错误"); } } /// /// 判断今天是否需要执行 /// private bool ShouldExecuteToday(TransactionPeriodic bill) { if (!bill.IsEnabled) { return false; } 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 }; } /// /// 判断是否需要在本周执行 /// 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); } /// /// 判断是否需要在本月执行 /// 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); } /// /// 判断是否需要在本季度执行 /// 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; } /// /// 判断是否需要在本年执行 /// private bool ShouldExecuteYearly(string config, DateTime today) { if (string.IsNullOrWhiteSpace(config) || !int.TryParse(config, out var dayOfYear)) return false; return today.DayOfYear == dayOfYear; } /// /// 计算下次执行时间 /// 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); } }