如何优雅地处理时区

Posted

技术标签:

【中文标题】如何优雅地处理时区【英文标题】:How to elegantly deal with timezones 【发布时间】:2011-11-26 12:04:52 【问题描述】:

我的网站与使用该应用程序的用户位于不同的时区。除此之外,用户可以有一个特定的时区。我想知道其他 SO 用户和应用程序如何处理这个问题?最明显的部分是在数据库内部,日期/时间以 UTC 格式存储。在服务器上时,所有日期/时间都应以 UTC 处理。但是,我看到了三个我正在努力克服的问题:

    以 UTC 格式获取当前时间(使用 DateTime.UtcNow 轻松解决)。

    从数据库中提取日期/时间并将其显示给用户。可能有很多次调用来在不同的视图上打印日期。我正在考虑可以解决此问题的视图和控制器之间的某个层。或者在DateTime 上有一个自定义扩展方法(见下文)。主要的缺点是在视图中使用日期时间的每个位置,都必须调用扩展方法!

    这也会增加使用 JsonResult 之类的东西的难度。你不能再轻易地打电话给Json(myEnumerable),它必须是Json(myEnumerable.Select(transformAllDates))。也许 AutoMapper 可以在这种情况下提供帮助?

    从用户那里获取输入(本地到 UTC)。例如,发布带有日期的表单需要将日期转换为 UTC 之前。首先想到的是创建一个自定义ModelBinder

这是我想在视图中使用的扩展:

public static class DateTimeExtensions

    public static DateTime UtcToLocal(this DateTime source, 
        TimeZoneInfo localTimeZone)
    
        return TimeZoneInfo.ConvertTimeFromUtc(source, localTimeZone);
    

    public static DateTime LocalToUtc(this DateTime source, 
        TimeZoneInfo localTimeZone)
    
        source = DateTime.SpecifyKind(source, DateTimeKind.Unspecified);
        return TimeZoneInfo.ConvertTimeToUtc(source, localTimeZone);
    

考虑到现在很多应用程序都是基于云的,其中服务器的本地时间可能与预期的时区大不相同,我认为处理时区将是一件很常见的事情。

这个问题以前有没有被优雅地解决过?有什么我想念的吗?非常感谢您的想法和想法。

编辑:为了消除一些混乱,我想添加更多细节。现在的问题不是 如何 在数据库中存储 UTC 时间,而是更多关于从 UTC->Local 和 Local->UTC 的过程。正如@Max Zerbini 指出的那样,将UTC->Local 代码放在视图中显然很聪明,但是使用DateTimeExtensions 真的是答案吗?从用户那里获取输入时,接受日期作为用户的本地时间(因为那是 JS 将使用的时间)然后使用 ModelBinder 转换为 UTC 是否有意义?用户的时区存储在数据库中,很容易检索。

【问题讨论】:

您可能首先阅读了这篇出色的帖子...Daylight saving time and Timezone best practices @dodgy_coder - 这一直是时区的绝佳资源链接。但是,它并没有真正解决我的任何问题(特别是与 MVC 有关)。不过还是谢谢。 code.google.com/p/noda-time 可能有用 好奇您选择了哪种解决方案。我自己也面临着类似的决定。好问题。 @Sean - 迄今为止的解决方案并不优雅(这就是我尚未接受答案的原因)。打印日期/时间并使用 ModelBinder 手动将其转换回需要大量手动开销。 【参考方案1】:

这并不是一个建议,它更多地分享了一个范例,而是我见过的在网络应用程序中处理时区信息的最激进方式(这不是 ASP.NET 独有的) MVC) 如下:

服务器上的所有日期时间都是 UTC。 这意味着使用,就像你说的,DateTime.UtcNow

尝试尽可能少地信任客户端将日期传递给服务器。例如,如果您需要“现在”,请不要在客户端创建日期然后将其传递给服务器。在 GET 中创建一个日期并将其传递给 ViewModel 或在 POST 上执行 DateTime.UtcNow

到目前为止,票价相当标准,但这就是事情变得“有趣”的地方。

如果您必须接受来自客户端的日期,请使用 javascript 确保您发布到服务器的数据采用 UTC。客户端知道它所在的时区,因此它可以以合理的精度将时间转换为 UTC。

在渲染视图时,他们使用 html5 <time> 元素,他们永远不会直接在 ViewModel 中渲染日期时间。它被实现为HtmlHelper 扩展,类似于Html.Time(Model.when)。它将呈现<time datetime='[utctime]' data-date-format='[datetimeformat]'></time>

然后他们将使用 javascript 将 UTC 时间转换为客户端本地时间。该脚本将查找所有 <time> 元素并使用 date-format 数据属性来格式化日期并填充元素的内容。

这样他们就不必跟踪、存储或管理客户的时区。服务器不关心客户端所在的时区,也不必进行任何时区转换。它只是吐出UTC,让客户将其转换为合理的东西。这对浏览器来说很容易,因为它知道它所在的时区。如果客户端更改了他/她的时区,Web 应用程序将自动更新自身。他们存储的唯一内容是用户语言环境的日期时间格式字符串。

我并不是说这是最好的方法,但这是一种我以前从未见过的不同方法。也许你会从中得到一些有趣的想法。

【讨论】:

感谢您的回复。从用户获取日期输入(例如安排约会)时,此输入是否假定在他们当前的时区中?您如何看待 ModelBinder 在进入操作之前进行此转换(请参阅我的 cmets 到 @casperOne。另外,<time> 的想法非常酷。唯一的缺点是这意味着我需要为这些查询整个 DOM元素,仅用于转换日期并不是很好(禁用 JS 怎么办?)。再次感谢! 通常,是的,假设它会在本地时区。但他们指出,无论何时从用户那里收集时间,在发送到服务器之前,都会使用 javascript 将其转换为 UTC。他们的解决方案非常重 JS,并且在 JS 关闭时并没有很好地降级。我还没有考虑够多,看看是否有一个聪明的。但就像我说的那样,也许它会给你一些想法。 我一直从事另一个需要本地日期的项目,这就是我使用的方法。我正在使用moment.js 进行日期/时间格式化......这很甜蜜。谢谢! 难道没有一种简单的方法可以在应用程序的 app.config / web.cofig 中定义一个应用程序范围的时区,并使其将所有 DateTime.Now 值转换为 DateTime.UtcNow 吗? (我想避免公司的程序员仍然会错误地使用 DateTime.Now 的情况) 我可以看到这种方法的一个缺点是,当您将时间用于其他事情时,创建 pdf、电子邮件等,这些没有时间元素,因此您仍然必须转换那些手动。否则,非常整洁的解决方案【参考方案2】:

经过多次反馈,这是我认为干净简单的最终解决方案,涵盖了夏令时问题。

1 - 我们在模型级别处理转换。所以,在 Model 类中,我们写:

    public class Quote
    
        ...
        public DateTime DateCreated
        
            get  return CRM.Global.ToLocalTime(_DateCreated); 
            set  _DateCreated = value.ToUniversalTime(); 
        
        private DateTime _DateCreated  get; set; 
        ...
    

2 - 在全局帮助器中,我们将自定义函数设为“ToLocalTime”:

    public static DateTime ToLocalTime(DateTime utcDate)
    
        var localTimeZoneId = "China Standard Time";
        var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneId);
        var localTime = TimeZoneInfo.ConvertTimeFromUtc(utcDate, localTimeZone);
        return localTime;
    

3 - 我们可以通过在每个用户配置文件中保存时区 ID 来进一步改进这一点,这样我们就可以从用户类中检索而不是使用常量“中国标准时间”:

public class Contact

    ...
    public string TimeZone  get; set; 
    ...

4 - 在这里我们可以获取要显示给用户以从下拉框中选择的时区列表:

public class ListHelper

    public IEnumerable<SelectListItem> GetTimeZoneList()
    
        var list = from tz in TimeZoneInfo.GetSystemTimeZones()
                   select new SelectListItem  Value = tz.Id, Text = tz.DisplayName ;

        return list;
    

所以,现在在中国上午 9:25,网站托管在美国,日期以 UTC 格式保存在数据库中,这是最终结果:

5/9/2013 6:25:58 PM (Server - in USA) 
5/10/2013 1:25:58 AM (Database - Converted UTC)
5/10/2013 9:25:58 AM (Local - in China)

编辑

感谢Matt Johnson 指出原始解决方案的薄弱环节,并很抱歉删除原始帖子,但在获取正确的代码显示格式时遇到问题...结果编辑器在将“项目符号”与“预代码”混合时遇到问题",所以我去掉了公牛,没关系。

【讨论】:

如果有人没有n层架构或共享Web库,这似乎是可行的。我们如何将时区 ID 共享到数据层。【参考方案3】:

在sf4answers 上的events section 中,用户输入事件的地址、开始日期和可选的结束日期。这些时间在 SQL Server 中被转换为一个datetimeoffset,用于说明与 UTC 的偏移量。

这与您面临的问题相同(尽管您采用了不同的方法,因为您使用的是DateTime.UtcNow);你有一个位置,你需要将时间从一个时区转换到另一个时区。

我做了两件对我有用的主要事情。首先,始终使用DateTimeOffset structure。它考虑了与 UTC 的偏移量,如果您可以从客户那里获得该信息,它会让您的生活更轻松一些。

其次,在执行转换时,假设您知道客户端所在的位置/时区,您可以使用 public info time zone database 将时间从 UTC 转换到另一个时区(或者,如果您愿意,可以在两个时区)。 tz 数据库(有时称为Olson database)的伟大之处在于它考虑了历史上时区的变化;获取偏移量是您想要获取偏移量的日期的函数(只需查看 Energy Policy Act of 2005 与 changed the dates when daylight savings time goes into effect in the US)。

有了数据库,您可以使用ZoneInfo (tz database / Olson database) .NET API。请注意,没有二进制发行版,您必须下载 latest version 并自行编译。

在撰写本文时,它目前解析最新数据分发中的所有文件(我实际上是在 2011 年 9 月 25 日针对 ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz 文件运行它;在 2017 年 3 月,您可以通过 @ 获得它987654333@ 或来自ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz)。

因此,在 sf4answers 上,获取地址后,将其地理编码为纬度/经度组合,然后发送到第三方 Web 服务以获取与 tz 数据库中的条目对应的时区。从那里开始,开始和结束时间被转换为具有适当 UTC 偏移量的 DateTimeOffset 实例,然后存储在数据库中。

至于在 SO 和网站上处理它,这取决于受众和您要展示的内容。如果您注意到,大多数社交网站(以及 SO,以及 sf4answers 上的事件部分)以相对时间显示事件,或者,如果使用绝对值,通常是 UTC。

但是,如果您的受众期望当地时间,那么使用 DateTimeOffset 以及将时区转换为的扩展方法就可以了; SQL 数据类型datetimeoffset 将转换为.NET DateTimeOffset,然后您可以获得使用GetUniversalTime method 的通用时间。从那里,您只需使用 ZoneInfo 类上的方法将 UTC 转换为本地时间(您必须做一些工作才能将其转换为 DateTimeOffset,但这很简单)。

在哪里进行转换?这是您必须在某处支付的费用,而且没有“最佳”方式。不过,我会选择视图,将时区偏移作为呈现给视图的视图模型的一部分。这样,如果视图的需求发生变化,您不必更改视图模型以适应变化。您的JsonResult 将只包含一个带有IEnumerable&lt;T&gt; 偏移量的模型。

在输入端,使用模型绑定器?我会说绝对不可能。您不能保证所有日期(现在或将来)必须以这种方式进行转换,它应该是控制器的显式函数来执行此操作。同样,如果需求发生变化,您不必调整一个或多个ModelBinder 实例来调整您的业务逻辑;它业务逻辑,这意味着它应该在控制器中。

【讨论】:

感谢您的详细回复。我希望我不会显得粗鲁,但这并没有真正回答我对 ASP.NET MVC 的任何担忧:用户输入怎么样(使用自定义模型绑定器就足够了)?最重要的是,显示这些日期(在用户的本地时区)怎么样?我觉得扩展方法会给我的观点增加很多分量,这似乎是不必要的。 @TheCloudlessSky:请参阅我编辑后的回复的最后两段。就个人而言,我认为在哪里进行转换的细节是次要的。主要问题是日期时间数据的实际转换和存储(顺便说一句,我不能足够强调在 SQL Server 中使用 datetimeoffset 和在 .NET 中使用 DateTimeOffset,它们确实极大地简化了事情)我相信。由于上述原因,NET 根本无法充分处理。如果您在 2003 年输入了纽约市的日期,然后您希望将其转换为 2011 年在洛杉矶的日期,那么 .NET 在这种情况下会失败。 不使用模型绑定器(或动作之前的任何层)的问题是,每个控制器在使用时变得非常难以测试日期(它们都会依赖日期转换)。这将允许以 UTC 编写单元测试日期。我的应用程序有与个人资料相关联的用户(想想与他们的实践相关的医生/秘书)。配置文件包含时区信息。因此,在绑定过程中获取当前用户配置文件的时区和转换非常容易。你有其他反对这一点的论据吗?感谢您的意见! 反对的理由是您的测试不能准确地反映测试用例。您的输入不会采用 UTC,因此您的测试用例不应该被精心设计来使用它。它应该使用带有位置和所有位置的真实日期(尽管如果您使用DateTimeOffset,您将大大减轻这种情况,IMO)。 是的 - 但这意味着 每个 处理日期的测试都必须考虑到这一点。每个处理日期时间的操作总是会转换为UTC。对我来说,这是模型绑定器的主要候选者,然后测试模型绑定器【参考方案4】:

这只是我的看法,我认为 MVC 应用程序应该很好地将数据表示问题与数据模型管理分开。数据库可以在本地服务器时间存储数据,但表示层有责任使用本地用户时区呈现日期时间。在我看来,这与不同国家的 I18N 和数字格式相同。 在您的情况下,您的应用程序应检测用户的Culture 和时区,并更改视图以显示不同的文本、数字和日期时间表示,但存储的数据可以具有相同的格式。

【讨论】:

感谢您的回复。是的,这就是我基本描述的内容,我只是想知道如何使用 MVC 可以优雅地实现这一点。该应用程序将用户的时区存储为他们创建帐户的一部分(他们选择他们的时区)。问题再次出现在如何在每个视图中呈现日期(希望)不必用我为DateTimeExtensions 创建的方法乱扔它们。 有很多方法可以做到这一点,一种是按照您的建议使用辅助方法,另一种可能更复杂但更优雅的是使用过滤器来处理请求和响应并转换日期时间。另一种方法是开发自定义 Display 属性来注释视图模型的 DateTime 类型的字段。 这些是我一直在寻找的东西。如果您查看我的 OP,我还提到使用 AutoMapper 进行一些处理之前进入视图/json结果但在操作执行之后。【参考方案5】:

对于输出,创建一个像这样的显示/编辑器模板

@inherits System.Web.Mvc.WebViewPage<System.DateTime>
@Html.Label(Model.ToLocalTime().ToLongTimeString()))

如果您只希望某些模型使用这些模板,则可以根据模型上的属性绑定它们。

有关创建自定义编辑器模板的更多详细信息,请参阅 here 和 here。

另外,由于您希望它同时用于输入和输出,我建议您扩展一个控件,甚至创建您自己的控件。这样您就可以拦截输入和输出,并根据需要转换文本/值。

如果你想走这条路,This link 有望将你推向正确的方向。

无论哪种方式,如果您想要一个优雅的解决方案,这将是一些工作。从好的方面来说,一旦你完成了它,你就可以将它保存在你的代码库中以备将来使用!

【讨论】:

我认为自定义显示/编辑器模板和活页夹是最优雅的解决方案,因为一旦实施它就会“正常工作”。没有开发人员需要知道任何特别的信息才能使日期正常工作,只需使用DisplayForEditorFor,它每次都可以工作。 +1 这不是渲染应用服务器的时间而不是浏览器的时间吗?【参考方案6】:

这可能是一把大锤来解决问题,但您可以在 UI 和业务层之间注入一个层,该层透明地将日期时间转换为返回的对象图上的本地时间,以及输入日期时间参数上的 UTC。

我想这可以使用 PostSharp 或一些控制容器的反转来实现。

就个人而言,我会在 UI 中显式转换您的日期时间...

【讨论】:

【参考方案7】:

我想将日期存储为 DateTimeOffset 以便我可以维护写入数据库的用户的时区偏移量。但是,我只想在应用程序本身内部使用 DateTime。

所以,本地时区输入,本地时区输出。无论用户在何人/何地/何时查看数据,对于观察者来说都是本地时间 - 更改存储为 UTC + 本地偏移量。

我是这样实现的。

1. 首先,我需要获取 Web 客户端的本地时区偏移量并将该值存储在 Web 服务器上:

// Sets a session variable for local time offset from UTC
function SetTimeZone() 
    var now = new Date();
    var offset = now.getTimezoneOffset() / 60;
    var sign = offset > 0 ? "-" : "+";
    var offset = "0" + offset;
    offset = sign + offset + ":00";
    $.ajax(
        type: "post",
        url: prefixWithSitePathRoot("/Home/SetTimeZone"),
        data:  OffSet: offset ,
        datatype: "json",
        traditional: true,
        success: function (data) 
            var data = data;
        ,
        error: function (XMLHttpRequest, textStatus, errorThrown) 
            alert("SetTimeZone failed");
        
    );

该格式旨在与 SQL Server DateTimeOffset 类型相匹配。

SetTimeZone - 只设置 Session 变量的值。当用户登录时,我将此值合并到用户配置文件缓存中。

2. 当用户向数据库提交更改时,我通过实用程序类过滤 DateTime 值:

cmdADO.Parameters.AddWithValue("@AwardDate", (object)Utility.ConvertLocal2UTC(theContract.AwardDate, theContract.TimeOffset) ?? DBNull.Value);

方法:

public static DateTimeOffset? ConvertLocal2UTC(DateTime? theDateTime, string TimeZoneOffset)

    DateTimeOffset? DtOffset = null;
    if (null != theDateTime)
    
        TimeSpan AmountOfTime;
        TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime);
        DateTime datetime = Convert.ToDateTime(theDateTime);
        DateTime datetimeUTC = datetime.ToUniversalTime();

        DtOffset = new DateTimeOffset(datetimeUTC.Ticks, AmountOfTime);
    
    return DtOffset;

3. 当我从 SQL Server 读取日期时,我正在这样做:

theContract.AwardDate = theRow.IsNull("AwardDate") ? new Nullable<DateTime>() : DateTimeOffset.Parse(Convert.ToString(theRow["AwardDate"])).DateTime;

在控制器中,我修改了日期时间以匹配观察者的本地时间。 (我相信有人可以通过扩展或其他东西做得更好):

theContract.AwardDate = Utilities.ConvertUTC2Local(theContract.AwardDate, CachedCurrentUser.TimeZoneOffset);

方法:

public static DateTime? ConvertUTC2Local(DateTime? theDateTime, string TimeZoneOffset)

    if (null != theDateTime)
    
        TimeSpan AmountOfTime;
        TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime);
        DateTime datetime = Convert.ToDateTime(theDateTime);
        datetime = datetime.Add(AmountOfTime);
        theDateTime = new DateTime(datetime.Ticks, DateTimeKind.Utc);
    
    return theDateTime;

在视图中,我只是在显示/编辑/验证日期时间。

我希望这对有类似需求的人有所帮助。

【讨论】:

以上是关于如何优雅地处理时区的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot:如何优雅地处理全局异常?

如何优雅地处理重复(并发)请求?

过时的实现如何优雅地处理新版本的 OpenType 字体?

如何优雅地处理 SIGTERM 信号?

C++如何优雅地处理不被继承的友谊

如何优雅地处理 Spring Security 中未由 ControllerAdvice 处理的异常?