且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

根据时区安排在一天中的特定时间进行hangfire作业

更新时间:2023-02-16 13:00:56

您的答案非常接近.有几个问题:

Your answer was pretty close. There were a few problems:

  • 您假设给定时区中的今天与UTC中的今天是同一日期.根据时区,这些日期可能会有所不同.例如,2019年10月18日世界标准时间凌晨1点,美国中部时间2019年10月17日下午8:00.

  • You were assuming that today in a given time zone was the same date as today in UTC. Depending on time zone, these could be different days. For example, 1 AM UTC on 2019-10-18, is 8:00 PM in US Central Time on 2019-10-17.

如果您围绕今天是否发生过"进行设计,则可能会跳过合法事件.相反,想一想下一个未来发生的事情"要容易得多.

If you design around "has it happened yet today", you'll potentially skip over legitimate occurrences. Instead, it's much easier to just think about "what is the next future occurrence".

您没有采取任何措施来处理无效或含糊的当地时间,例如DST的开始或结束以及标准时间的变化.这对于重复发生的事件很重要.

You weren't doing anything to handle invalid or ambiguous local times, such as occur with the start or end of DST and with changes in standard time. This is important for recurring events.

继续执行代码:

// Get the current UTC time just once at the start
var utcNow = DateTimeOffset.UtcNow;

foreach (var schedule in schedules)
{
    // schedule notification only if not already scheduled in the future
    if (schedule.LastScheduledDateTime == null || schedule.LastScheduledDateTime.Value < utcNow)
    {
        // Get the time zone for this schedule
        var tz = TimeZoneInfo.FindSystemTimeZoneById(schedule.User.TimeZone);

        // Decide the next time to run within the given zone's local time
        var nextDateTime = nowInZone.TimeOfDay <= schedule.PreferredTime
            ? nowInZone.Date.Add(schedule.PreferredTime)
            : nowInZone.Date.AddDays(1).Add(schedule.PreferredTime);

        // Get the point in time for the next scheduled future occurrence
        var nextOccurrence = nextDateTime.ToDateTimeOffset(tz);

        // Do the scheduling
        BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.CompanyUserID), nextOccurrence);

        // Update the schedule
        schedule.LastScheduledDateTime = nextOccurrence;
    }
}

如果您将LastScheduledDateTime设置为DateTimeOffset?而不是DateTime?,我认为您会发现代码和数据更加清晰.上面的代码假定了这一点.如果您不想 ,则可以将最后一行更改为:

I think you'll find that your code and data are much clearer if you make your LastScheduledDateTime a DateTimeOffset? instead of a DateTime?. The above code assumes that. If you don't want to, then you can change that last line to:

        schedule.LastScheduledDateTime = nextOccurrence.UtcDateTime;

还请注意ToDateTimeOffset的使用,这是一种扩展方法.将其放在某个地方的静态类中.其目的是在考虑特定时区的情况下从DateTime创建DateTimeOffset.在处理模棱两可和无效的本地时间时,它会应用典型的调度问题. (如果您想了解更多信息,我上次在其他Stack Overflow答案中对此发表了文章.)以下是实现: /p>

Also note the use of ToDateTimeOffset, which is an extension method. Place it in a static class somewhere. Its purpose is to create a DateTimeOffset from a DateTime taking a specific time zone into account. It applies typical scheduling concerns when dealing with ambiguous and invalid local times. (I last posted about it in this other Stack Overflow answer if you want to read more.) Here is the implementation:

public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)
{
    if (dt.Kind != DateTimeKind.Unspecified)
    {
        // Handle UTC or Local kinds (regular and hidden 4th kind)
        DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
        return TimeZoneInfo.ConvertTime(dto, tz);
    }

    if (tz.IsAmbiguousTime(dt))
    {
        // Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
        TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
        TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
        return new DateTimeOffset(dt, offset);
    }

    if (tz.IsInvalidTime(dt))
    {
        // Advance by the gap, and return with the daylight offset  (2:30 ET becomes 3:30 EDT)
        TimeSpan[] offsets = { tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) };
        TimeSpan gap = offsets[1] - offsets[0];
        return new DateTimeOffset(dt.Add(gap), offsets[1]);
    }

    // Simple case
    return new DateTimeOffset(dt, tz.GetUtcOffset(dt));
}

(对于您而言,这种类型始终是未指定的,因此您可以根据需要删除该第一项检查,但是我更喜欢在其他用途​​的情况下保持其全部功能.)

(In your case, the kind is always unspecified, so you could remove that first check if you want to, but I prefer to keep it fully functional in case of other usage.)

顺便说一句,您不需要if (!schedules.HasAny()) { return; }检查.实体框架已经在SaveChangesAsync期间测试了更改,如果没有更改,则不执行任何操作.

Incidentally, you don't need the if (!schedules.HasAny()) { return; } check. Entity Framework already tests for changes during SaveChangesAsync, and does nothing if there aren't any.