如何优雅地处理时区

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

  1. 以 UTC 表示当前时间(使用 DateTime.UtcNow很容易解决)。

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

    这也会增加使用像 JsonResult这样的东西的难度。你不能再轻易地调用 Json(myEnumerable),它将不得不是 Json(myEnumerable.Select(transformAllDates))。也许 AutoMapper 可以在这种情况下提供帮助?

  3. 从用户获取输入(本地到 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);
}
}

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

这个问题以前优雅地解决过吗? 我是否遗漏了什么? 非常感谢您的想法和意见。

编辑: 为了消除一些疑惑,我想添加一些更多的细节。现在的问题不是 怎么做在 db 中存储 UTC 时间,而是从 UTC-> Local 和 Local-> UTC 的过程。正如@Max Zerbini 所指出的,显然将 UTC-> Local 代码放在视图中是明智的,但是使用 DateTimeExtensions真的是答案吗?当从用户获取输入时,是否有必要接受日期作为用户的本地时间(因为 JS 将使用这个时间) ,然后使用 ModelBinder转换为 UTC?用户的时区存储在数据库中,很容易检索到。

71042 次浏览

Sf4答案上的 活动部分中,用户输入事件的地址、开始日期和可选的结束日期。这些时间被转换为 SQL 服务器中的 datetimeoffset,用于说明与 UTC 的偏移量。

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

我所做的两件主要的事情对我来说是有效的。首先,始终使用 DateTimeOffset结构。它考虑到从 UTC 的偏移,如果你可以从你的客户端得到这些信息,它使你的生活更容易一点。

其次,在执行翻译时,假设您知道客户机所在的位置/时区,您可以使用 公共信息时区数据库将一个时间从 UTC 翻译成另一个时区(或者如果愿意,可以在两个时区之间进行三角测量)。Tz 数据库(有时被称为 奥尔森数据库)的伟大之处在于它考虑了整个历史时区的变化; 得到一个偏移量是你想得到偏移量的日期的函数(只要看看 2005年能源政策法更改了夏令时在美国生效的日期)。

有了数据库,您就可以使用 ZoneInfo (tz 数据库/Olson 数据库) . NET API。注意,这里没有二进制发行版,您必须下载 最新版本并自己编译它。

在撰写本文时,它正在解析最新数据发布中的所有文件(实际上,我在2011年9月25日对 Ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz文件运行了它; 在2017年3月,您可以通过 https://iana.org/time-zonesFpt.iana.org/tz/release/tzdata2017a.tar.gz获得它)。

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

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

但是,如果您的读者需要本地时间,那么使用 DateTimeOffset和扩展方法将时区转换为本地时间就可以了; SQL 数据类型 datetimeoffset将转换为。NET 的 DateTimeOffset,然后你可以得到使用 GetUniversalTime方法的通用时间。从那里开始,您只需使用 ZoneInfo类上的方法将 UTC 转换为本地时间(您需要做一些工作才能将其转换为 DateTimeOffset,但是这非常简单)。

在哪里进行转换?这是一个成本,你将不得不支付 某个地方,并没有“最佳”的方式。但是我会选择视图,将时区偏移作为视图模型的一部分。这样,如果对视图的需求发生变化,您就不必更改视图模型以适应变化。您的 JsonResult将只包含一个模型与 IEnumerable<T> 还有的偏移。

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

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

这并不是一个建议,它更多的是一种范式的共享,但是我所见过的在 web 应用程序中处理时区信息的最 有侵略性方式(这不是 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 应用程序会自动更新自己。它们存储的唯一内容是用户区域设置的日期时间格式字符串。

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

这可能是一个大锤敲碎坚果,但你可以注入一个层之间的 UI 和业务层透明地转换日期时间的本地时间对返回的对象图,以及输入日期时间参数的 UTC。

我想这可以通过 PostSharp 或某个控制反转容器来实现。

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

对于输出,创建如下的显示/编辑器模板

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

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

有关创建自定义编辑器模板的更多细节,请参见 给你给你

或者,因为您希望它既能用于输入又能用于输出,所以我建议扩展一个控件,甚至创建自己的控件。这样就可以拦截输入和输出,并根据需要转换文本/值。

如果你想沿着这条路走下去,希望这个链接 能够把你推向正确的方向。

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

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

1-我们在模型级别处理转换。因此,在模型类中,我们写:

    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; }
...
}

这里我们可以得到时区的列表,显示给用户从下拉框中选择:

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)

剪辑

感谢 马特 · 约翰逊指出了原始解决方案的弱点,并为删除原始文章感到抱歉,但得到正确的代码显示格式问题... 原来编辑器有混合“子弹”和“预编码”的问题,所以我删除了泡泡,它是好的。

我希望将日期存储为 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");
}
});
}

此格式旨在与 SQLServerDateTimeOffset 类型匹配。

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. 当我从 SQLServer 读取日期时,我正在这样做:

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;
}

在视图中,我只是显示/编辑/验证一个 DateTime。

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