新增定时账单功能
This commit is contained in:
185
Service/Jobs/EmailSyncJob.cs
Normal file
185
Service/Jobs/EmailSyncJob.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using MimeKit;
|
||||
using Quartz;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 邮件同步定时任务
|
||||
/// </summary>
|
||||
public class EmailSyncJob(
|
||||
IOptions<EmailSettings> emailSettings,
|
||||
IServiceProvider serviceProvider,
|
||||
IEmailHandleService emailHandleService,
|
||||
ILogger<EmailSyncJob> logger) : IJob
|
||||
{
|
||||
private readonly Dictionary<string, IEmailFetchService> _emailFetchServices = new();
|
||||
private bool _isInitialized;
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("开始执行邮件同步任务");
|
||||
|
||||
// 如果未初始化,先初始化连接
|
||||
if (!_isInitialized)
|
||||
{
|
||||
await InitializeConnectionsAsync();
|
||||
}
|
||||
|
||||
// 执行邮件同步
|
||||
await FetchAndPostCmbTransactionsAsync();
|
||||
|
||||
logger.LogInformation("邮件同步任务执行完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "邮件同步任务执行出错");
|
||||
throw; // 让 Quartz 知道任务失败
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化所有邮箱连接
|
||||
/// </summary>
|
||||
private async Task InitializeConnectionsAsync()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
logger.LogWarning("连接已初始化,跳过重复初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (emailSettings.Value.SmtpList.Length == 0)
|
||||
{
|
||||
logger.LogWarning("未配置邮箱账户,无法初始化连接");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("开始初始化 {EmailCount} 个邮箱连接...", emailSettings.Value.SmtpList.Length);
|
||||
|
||||
// 并行初始化所有邮箱连接
|
||||
var tasks = emailSettings.Value.SmtpList.Select(async emailConfig =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var emailFetchService = ActivatorUtilities.CreateInstance<EmailFetchService>(serviceProvider);
|
||||
var success = await emailFetchService.ConnectAsync(
|
||||
emailConfig.ImapHost,
|
||||
emailConfig.ImapPort,
|
||||
emailConfig.UseSsl,
|
||||
emailConfig.Email,
|
||||
emailConfig.Password);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_emailFetchServices[emailConfig.Email] = emailFetchService;
|
||||
logger.LogInformation("邮箱 {Email} 连接建立成功", emailConfig.Email);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("邮箱 {Email} 连接建立失败", emailConfig.Email);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "初始化邮箱 {Email} 连接时出错", emailConfig.Email);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
_isInitialized = true;
|
||||
logger.LogInformation("所有邮箱连接初始化完成,成功连接 {Count} 个邮箱", _emailFetchServices.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "初始化邮箱连接失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抓取并处理招商银行邮件交易
|
||||
/// </summary>
|
||||
private async Task FetchAndPostCmbTransactionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_emailFetchServices.Count == 0)
|
||||
{
|
||||
logger.LogWarning("没有可用的邮箱连接,跳过抓取");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("开始抓取 {EmailCount} 个邮箱的邮件", _emailFetchServices.Count);
|
||||
|
||||
// 并行处理多个邮箱
|
||||
var tasks = _emailFetchServices.Select(async kvp =>
|
||||
{
|
||||
var email = kvp.Key;
|
||||
var emailFetchService = kvp.Value;
|
||||
|
||||
try
|
||||
{
|
||||
// 获取未读邮件
|
||||
var unreadMessages = await emailFetchService.FetchUnreadMessagesAsync();
|
||||
logger.LogInformation("邮箱 {Email} 获取到 {MessageCount} 封未读邮件", email, unreadMessages.Count);
|
||||
|
||||
foreach (var (message, uid) in unreadMessages)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogDebug("邮件信息 - 发送者: {From}, 主题: {Subject}, 接收时间: {Date}",
|
||||
message.From, message.Subject, message.Date);
|
||||
logger.LogDebug("邮件内容预览: {Preview}", GetEmailBodyPreview(message));
|
||||
|
||||
if (await emailHandleService.HandleEmailAsync(
|
||||
email,
|
||||
message.From.ToString(),
|
||||
message.Subject,
|
||||
message.Date.DateTime,
|
||||
message.TextBody ?? message.HtmlBody ?? string.Empty
|
||||
) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3)))
|
||||
{
|
||||
#if DEBUG
|
||||
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
||||
#else
|
||||
// 标记邮件为已读
|
||||
await emailFetchService.MarkAsReadAsync(uid);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "处理邮件时出错");
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("邮箱 {Email} 邮件抓取完成", email);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "邮箱 {Email} 邮件抓取失败", email);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
logger.LogInformation("所有邮箱邮件抓取完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "抓取邮件异常");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取邮件内容预览
|
||||
/// </summary>
|
||||
private static string GetEmailBodyPreview(MimeMessage message)
|
||||
{
|
||||
var body = message.HtmlBody ?? message.TextBody ?? string.Empty;
|
||||
var preview = body.Length > 100 ? body.Substring(0, 100) + "..." : body;
|
||||
return preview.Replace("\n", " ").Replace("\r", "");
|
||||
}
|
||||
}
|
||||
34
Service/Jobs/PeriodicBillJob.cs
Normal file
34
Service/Jobs/PeriodicBillJob.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Quartz;
|
||||
|
||||
namespace Service.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 周期性账单定时任务
|
||||
/// </summary>
|
||||
[DisallowConcurrentExecution] // 防止并发执行
|
||||
public class PeriodicBillJob(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<PeriodicBillJob> logger) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("开始执行周期性账单检查任务");
|
||||
|
||||
// 执行周期性账单检查
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
{
|
||||
var periodicService = scope.ServiceProvider.GetRequiredService<ITransactionPeriodicService>();
|
||||
await periodicService.ExecutePeriodicBillsAsync();
|
||||
}
|
||||
|
||||
logger.LogInformation("周期性账单检查任务执行完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "周期性账单检查任务执行出错");
|
||||
throw; // 让 Quartz 知道任务失败
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Service/PeriodicBillBackgroundService.cs
Normal file
61
Service/PeriodicBillBackgroundService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Service;
|
||||
|
||||
/// <summary>
|
||||
/// 周期性账单后台服务
|
||||
/// </summary>
|
||||
public class PeriodicBillBackgroundService(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<PeriodicBillBackgroundService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("周期性账单后台服务已启动");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
|
||||
// 计算下次执行时间(每天早上6点)
|
||||
var nextRun = now.Date.AddHours(6);
|
||||
if (now >= nextRun)
|
||||
{
|
||||
nextRun = nextRun.AddDays(1);
|
||||
}
|
||||
|
||||
var delay = nextRun - now;
|
||||
logger.LogInformation("下次执行周期性账单检查时间: {NextRun}, 延迟: {Delay}",
|
||||
nextRun.ToString("yyyy-MM-dd HH:mm:ss"), delay);
|
||||
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
|
||||
if (stoppingToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// 执行周期性账单检查
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
{
|
||||
var periodicService = scope.ServiceProvider.GetRequiredService<ITransactionPeriodicService>();
|
||||
await periodicService.ExecutePeriodicBillsAsync();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogInformation("周期性账单后台服务已取消");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "周期性账单后台服务执行出错");
|
||||
// 出错后等待1小时再重试
|
||||
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("周期性账单后台服务已停止");
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,15 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="CsvHelper" />
|
||||
<PackageReference Include="EPPlus" />
|
||||
<PackageReference Include="HtmlAgilityPack" />
|
||||
<PackageReference Include="Quartz" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
312
Service/TransactionPeriodicService.cs
Normal file
312
Service/TransactionPeriodicService.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
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 >= 0 && d <= 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 >= 1 && d <= 31)
|
||||
.ToList();
|
||||
|
||||
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 >= 0 && d <= 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 >= 1 && d <= 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user