4

有人问过,但我正在努力掌握如何在网络应用程序中处理时区的概念。我有一个跟踪项目进度的系统。我的 SQL Server 数据库中有一个 ProjectStartDate DATE。(还有一些字段和表,但让我们专注于一个)。

服务器位于美国某处。我住在澳大利亚。

调用SELECT GETDATE()返回“2013-08-11 14:40:50.630”我的系统时钟显示“2013-08-12 07:40”

在我的数据库中,所有表上都有“CreateDateTime”列。当我在我的 c# 代码中存储它时,我使用CreateDate = DateTime.UtcNow

我使用它,因为我听说使用 UTC 更好。

但是,当用户看到日历控件并且他们为项目选择开始日期时,我会存储用户选择的任何内容。没有转换......正如我所说, StartDate 是数据库中的 DATE 类型。

问题是,如果一个项目今天开始 - 我的前端说当前项目没有开始,因为服务器仍然在昨天。

我认为日期应该在我存储它们时存储。但也许我需要以某种方式获取用户时区,并将其应用到 UI 级别?

我看到的问题是:

  • 我不知道用户的时区。添加一些东西让他们选择吗?
  • 项目的状态可能在存储过程中确定,那么我什么时候可以应用转换?在 proc 中,它可能会进行检查,如果 StartDate <= DateTime.Now?

我大部分时间使用 EntityFramework 和 Linq 来获取数据。我需要一种从 SQL 意义上和 .Net 意义上插入和检索数据的策略。

我添加了代码让用户根据这个选择他们的时区:

public List<TimeZoneDto> GetTimeZones()
{
    var zones = TimeZoneInfo.GetSystemTimeZones();
    var result = zones.Select(tz => new TimeZoneDto
        {
            Id = tz.Id, 
            Description = tz.DisplayName
        }).ToList();

    return result;
}

然后将其保留在他们的个人资料中。

正如以下答案中所建议的,所有日期都存储为 UTC。

我仍然很困惑如何处理他们往返数据库和客户端的日期。这是我如何存储记录的示例:

public int SaveNonAvailibility(PersonNonAvailibilityDto n)
{
    person_non_availibility o;

    if (n.Id == 0)
    {
        o = new person_non_availibility
            {
                PersonId = n.PersonId,
                Reason = n.Reason,
                StartDate = n.StartDate,
                EndDate = n.EndDate,
                NonAvailibilityTypeId = n.NonAvailibilityTypeId,
                CreateUser = _userId,
                CreateDate = DateTime.UtcNow

            };
        _context.person_non_availibility.Add(o);
    }
    else
    {
        o = (from c in _context.person_non_availibility where c.Id == n.Id select c).FirstOrDefault();
        o.StartDate = n.StartDate;
        o.EndDate = n.EndDate;
        o.Reason = n.Reason;
        o.NonAvailibilityTypeId = n.NonAvailibilityTypeId;
        o.LastUpdateDate = DateTime.UtcNow;
        o.LastUpdateUser = _userId;
        o.Deleted = n.Deleted ? DateTime.UtcNow : (DateTime?)null;
    }
    _context.SaveChanges();
    return o.Id;
}

这种方法基本上可以节省一个人无法工作的时间。请注意我存储 LastUpdateDate 的方式。此外,“开始”和“结束”日期。这些日期是更多的“商务”日期。

在选择,然后是日期检查时,我遇到了问题。在此示例中,我正在获取基于 NOW 的个人收费率。

public decimal CurrentRate
{
    get
    {
        if (ResourceCosts != null)
        {
            var i = ResourceCosts.FirstOrDefault(t => DateTime.UtcNow <= (t.EndDate.HasValue ? t.EndDate.Value : DateTime.UtcNow) && DateTime.UtcNow >= t.StartDate);
            if (i != null) return i.Cost;
            return 0;
        }
        return 0;
    }
}

所以,我想在这里做的是,根据当前日期,我想看看他的费率(因为他的收费我们的费率可能是从 1 月 1 日到 1 月 15 日的 100 美元,然后从 16 日到 110 美元) 1 月 31 日。所以,我寻找今天适用的汇率(如果有的话)。这不适用于跨时区,可能在这里我需要根据“DateTime.UTCNow”进行一些日期操作?

请注意,我现在根据上面存储用户时区的代码知道用户时区。我可以在这里以某种方式使用它吗?也许,当用户登录时,从他的个人资料(Zimezone 信息)中获取日期信息,然后拥有一个全局共享函数,该函数根据从 UTC 日期添加或删除小时数返回用户日期时间......并使用它我在哪里做 DateTime.UTCNow?

希望有人可以指导我。

4

2 回答 2

9

您可能会发现没有一种单一的“正确”方法可以处理所有这些问题。您在问题中描述的几个不同问题有多种方法。我将尝试澄清几点。

  • 首先,永远不要试图考虑服务器上的本地时间。您的代码和数据不必根据您的部署位置而更改。您说您的服务器在美国,但需要考虑多个时区,并且许多服务器的时区设置为 UTC,无论其物理位置如何。

    您应该避免GETDATE()SYSDATETIME()在 SQL Server 中。如果您需要 SQL 中的当前时间戳,请使用GETUTCDATE()SYSUTCDATETIME()。如果由于某种原因服务器的时区对您很重要,请SYSDATETIMEOFFSET()改用。

    同样,避免DateTime.Now在任何服务器端代码中使用 .Net。使用DateTime.UtcNoworDateTimeOffset.UtcNow 作为 UTC 时间戳,或者DateTimeOffset.Now如果由于某种原因服务器的时区对您很重要,则使用。

    您可以在我的博客文章中阅读更多相关信息:反对 DateTime.Now 的案例

  • 接下来,让我们谈谈您正在使用的数据类型。dateSQL Server 中的类型只存储一个日期。而已。没有时间,没有偏移,也没有时区。一个例子是2013-08-11。当您真正指的是整个日历日期时,您应该使用它。“今天”没有全球统一的语境。相反,每个人都根据他们的时区有自己的含义。此外,并非每个日历日都是 24 小时。一天可以是 23、23.5、24、24.5 或 25 小时,具体取决于特定时区中夏令时的应用方式以及您是否正在评估 DST 转换的日期。

    在 .Net 中 - 没有Date类型,因此 SQLdate被转换为 a DateTime,时间设置为午夜 ( 00:00:00),种类设置为Unspecified. 但不要自欺欺人——时间不是突然到午夜,我们只是在时间上填零。这会导致很多错误和混乱。(如果你想避免这种情况,你可以试试Noda Time,它有一个LocalDate用于此目的的类型。)

  • 你真正需要考虑的,并且没有在你的问题中定义的是:

    项目开始的确切时间是什么时候?

    现在你只是在说2013-08-11,它并不是指特定的时刻。您是指特定时区的那一天的开始吗?或者你的意思是根据用户的时区的那一天的开始?这些可能不是一回事。除非您知道您所谈论的时间点,否则您无法与任何人的“现在”(UTC、本地或其他)进行比较。

    如果项目在全球范围内的某个确切时间开始,那么最简单的方法是存储一个datetime(或datetime2)类型,其中包含 UTC 中的那个精确时间。所以你可能会说一个项目开始于2013-08-10T14:00:00Z- 这将是澳大利亚悉尼的 8 月 11 日午夜。在 .Net 中,您将使用设置为的DateTime类型。.KindUtc

    您可以表示这一点的另一种方式是通过存储datetimeoffset具有值的类型2013-08-11T00:00:00+10:00- 这是同一时刻,但使用偏移量为您提供预转换的值。(悉尼在该日期是 UTC+10)。您将使用该DateTimeOffset类型在 .Net 中使用它。

    但是,如果项目根据用户在不同的时间开始,那么它就不是一个确切的时间点。这更像是一个“浮动”的开始。如果来自世界各地的用户被分配到同一个项目,那么一些用户可能会先于其他用户开始。如果这是您的意图,那么如果所有项目都在午夜开始,您可以只使用类型,或者如果项目可能在不同时间开始date,您可以使用datetime或 ( ) 类型。datetime2在您的 .Net 代码中,您将使用设置为的DateTime类型。.KindUnspecified

  • 关于获取用户的时区,您能做的最好的事情就是询问他们。尽管存在常见的误解 - 您不能只从浏览器中获取它。您可以从浏览器中得知它们当前的偏移量是多少。(阅读时区标签 wiki的“TimeZone != Offset”部分)。

    当询问用户他们的时区时,如果您决定使用 Windows 时区,您可以从该TimeZoneInfo.GetSystemTimeZones方法中生成一个下拉列表。.Id是您存储在数据库中的密钥,并向.DisplayName用户显示。稍后您可以使用该TimeZoneInfo.FindSystemTimeZoneById方法获取TimeZoneInfo可用于转换的对象。

    如果您想更精确,您可以使用 IANA 时区而不是 Windows 时区。为此,我建议使用基于地图的时区选择器控件,例如这个. 您还可以使用jsTimeZoneDetect来猜测控件的默认值。在服务器上,您将使用Noda Time执行时区转换。

  • 有一种方法不需要时区转换。基本上,您在 UTC 中做所有事情。这包括以 UTC 将时间传输到浏览器。然后,您可以使用 JavaScript 以UTC 格式获取用户的当前时间并与之进行比较。

    Date如果您愿意,可以使用 JavaScript 类的各种函数来执行此操作。但是您可能会发现使用诸如moment.js 之类的库会更容易。

    虽然这种方法对很多事情都是可行的,但安全性并不是其中之一。您的用户可以轻松更改其计算机的时钟以解决此问题。

  • 另一种方法是将服务器端与 UTC 进行比较。如果您的数据库中有确切的 UTC 开始时间,那么您只需检查DateTime.UtcNow您的 .Net 代码并使用它来决定要做什么。您不需要用户的时区来进行此比较,但如果您想向他们展示当地时间的含义,您将需要它。

我希望这可以消除混乱并且不会使情况变得更糟!:) 如果您还有其他问题,请编辑您的问题或在评论中提问。

更新

针对您更新的问题,我建议您尝试以下操作:

var timeZoneId = "Eastern Standard Time"; // from your user's selection
var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
var nowInTimeZone = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timeZone);
var todayInTimeZone = nowInTimeZone.Date;

var i = ResourceCosts.FirstOrDefault(t => t.StartDate <= todayInTimeZone &&
            (!t.EndDate.HasValue || t.EndDate >= todayInTimeZone));

当然,这意味着在您的StartDateEndDate字段中,您不会将它们存储为 UTC,而是存储为与用户相关的“营业日期”。这些仅在您应用时区时排列到特定时间,因此相同的 UTC 时间戳可能会落在不同的日期,具体取决于用户的时区。

此外,您正在使用完全包含的范围,这对于这些日历日期范围通常是可以的。但请确保您意识到可能存在重叠。因此,如果您有2013-01-01 - 2013-02-012013-02-01 - 2013-03-01,那么就有一天2013-02-01两个范围内。

解决此问题的一种常见方法是使用半开区间[start,end)。换句话说,start <= now && end > now. 但这在使用完整日期和时间而不是仅使用日期时更为常见。您可能不需要这样做,但您至少应该为您的特定场景考虑它。

于 2013-08-12T01:09:18.970 回答
2

您可以从浏览器获取用户的时区。这只会获得他们在计算机上设置的值。因此,他们可以利用这一点来谋取利益。例如,如果您将访问限制在当地时间午夜之前,他们可以更改时间以“提前”访问。所以记住这一点。

接下来,您要做的是将所有时间都以 UTC 格式存储在数据库中。您可以使用 GetUTCDate(),而不是使用 GetDate()。

接下来,您可以将它们的时区或它们与 UTC 的偏移量存储在数据库中。仅存储 UTC 偏移量就会遇到一些危险,因为观察夏令时的时区在一年中的不同时间会有不同的偏移量。

但是有了这两条信息,就可以计算出用户的“本地”时间。您可以将小时数添加到存储的日期,然后将其与本地服务器时间(也从 UTC 调整)进行比较。

如果您不是很在意,您可以将时间以 UTC 格式存储在数据库中,然后简单地比较服务器上的 DateTime.UTCNow 以确定它们是否应该具有访问权限。

于 2013-08-11T21:57:40.057 回答