有人问过这个问题,但我正在努力掌握如何在web应用程序中处理时区的概念。我有一个跟踪项目进度的系统。我有一个ProjectStartDate日期在我的SQL Server数据库。(还有更多的字段和表,但我们只关注一个)。
服务器位于美国的某个地方。我住在澳大利亚。
调用SELECT GETDATE()
返回"2013-08-11 14:40:50.630"我的系统时钟显示"2013-08-12 07:40">
在我的数据库中,我在所有表上都有'CreateDateTime'列。当我在c#代码中存储时,我使用CreateDate = DateTime.UtcNow
我使用它,因为我听说使用UTC更好。
但是,当用户看到一个日历控件,并且他们为一个项目选择一个开始日期时,我将存储用户所选择的内容。没有转换…我说过,StartDate是数据库中的DATE类型。
问题是,如果一个项目今天开始-我的前端说当前项目是没有开始的,因为服务器还在昨天。
我认为日期应该像我存储它们一样存储。但也许我需要获取用户时区,并应用到UI层面?
我看到的问题是:
- 我不知道用户的时区。添加一些允许他们选择的东西?
- 项目的状态可能在存储过程中确定,因此何时可以应用转换?在这个过程中,它可能会做一个检查,如果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的方式。此外,"开始"one_answers"结束"日期。这些日期更多的是"商务"日期。
选择,然后日期检查,是我有问题的地方。在这个例子中,我得到了一个基于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美元,然后从1月16日到1月31日是110美元),所以我寻找今天适用的费率(如果有的话)。这不能跨时区工作,它可能在这里,我需要做一些日期操作基于'DateTime.UTCNow'?
注意,我现在根据上面存储用户时区的代码知道了用户的时区。我可以在这里用它吗?也许,当用户登录时,从他的配置文件(Zimezone信息)中获取日期信息,然后有一个全局共享函数,返回用户datetime,基于从UTC日期添加或删除小时…并在我做datetime。utcnow时使用它?
希望有人能指点一下。
您可能会发现没有一种"正确"的方法来处理所有这些。你在问题中描述的几个不同问题有多种解决方法。我将试图澄清几点。
-
首先,不要试图考虑服务器上的本地时间。您的代码和数据不应该根据部署位置而改变。您说您的服务器在美国,但是要考虑多个时区,并且许多服务器将其时区设置为UTC,而不管其物理位置如何。
您应该避免在SQL Server中使用
同样地,避免在. net中的任何服务器端代码中使用GETDATE()
或SYSDATETIME()
。如果您需要SQL中的当前时间戳,请使用GETUTCDATE()
或SYSUTCDATETIME()
。如果由于某种原因服务器的时区对您很重要,请使用SYSDATETIMEOFFSET()
。DateTime.Now
。使用DateTime.UtcNow
或DateTimeOffset.UtcNow
作为UTC时间戳,或者如果由于某些原因服务器的时区对你很重要,使用DateTimeOffset.Now
。 你可以在我的博客文章《反对日期时间的案例》中了解更多。现在 -
接下来,让我们讨论一下您正在使用的数据类型。SQL Server中的
date
类型只存储一个日期。就是这样。没有时间,没有偏移,没有时区。2013-08-11
就是一个例子。当你真正指的是整个日历日期时,应该使用它。"今天"在世界范围内没有统一的背景。相反,每个人都根据自己的时区有自己的意思。此外,并非每个日历日都是24小时。一天可能是23、23.5、24、24.5或25小时,这取决于夏令时在特定时区的应用方式,以及您是否正在评估夏令时转换的日期。在。net中没有
Date
类型,所以SQL的date
被转换为DateTime
,时间设置为午夜(00:00:00
),类型设置为Unspecified
。但是不要欺骗自己——时间并不是突然变成了午夜,我们只是在为时间填上0。这可能会导致很多错误和混乱。(如果你想避免这种情况,你可以尝试Noda Time,它有一个LocalDate
类型用于此目的。) -
你真正需要考虑的,但在你的问题中没有定义的是:
什么确切的时刻开始一个项目?
现在你只是在说
2013-08-11
,这并不是指一个特定的时刻。您是指某特定时区当天的开始吗?或者您指的是根据用户的时区的那天的开始?这可能不是一回事。你不能拿别人的"现在"(utc、本地或其他)来比较,除非你知道你在谈论的是什么时刻。如果项目在世界范围内的一个精确时刻开始,那么最简单的事情就是存储包含UTC精确时间的
datetime
(或datetime2
)类型。所以你可能会说一个项目开始于2013-08-10T14:00:00Z
,也就是8月11日在澳大利亚悉尼的午夜。在。net中,您将使用DateTime
类型,并将.Kind
设置为Utc
。另一种表示方式是存储一个
datetimeoffset
类型,它的值为2013-08-11T00:00:00+10:00
——它在时间上是相同的,但是使用偏移量给你一个预转换的值。(悉尼在那天是UTC+10)。您可以在。net中使用DateTimeOffset
类型来处理此操作。但是如果项目根据用户的不同在不同的时间开始,那么它就不是一个确切的时间点。这更像是一个"浮动"的开始。如果来自世界各地的用户被分配到同一个项目,那么一些用户可能会在其他用户之前开始。如果这是您的意图,那么如果所有项目都在午夜开始,那么您可以使用
date
类型,或者如果项目可能在不同时间开始,您可以使用datetime
或(datetime2
)类型。在。net代码中,您将使用DateTime
类型,并将.Kind
设置为Unspecified
。 -
关于获取用户的时区,你能做的最好的事情就是询问他们。尽管存在普遍的误解——你不能仅仅从浏览器中获取它。你能从浏览器得知的只是它们的当前偏移量是多少。(请阅读时区标签wiki的"TimeZone != Offset"部分)。
当询问用户的时区时,如果您决定使用Windows时区,您可以从
TimeZoneInfo.GetSystemTimeZones
方法生成一个下拉列表。.Id
是存储在数据库中的键,并向用户显示.DisplayName
。稍后,您可以使用TimeZoneInfo.FindSystemTimeZoneById
方法来获得一个TimeZoneInfo
对象,您可以将其用于转换。如果您想要更精确,您可以使用IANA时区而不是Windows时区。为此,我建议使用基于地图的时区选择器控件,比如这个。您还可以使用jsTimeZoneDetect来猜测控件的默认值。在服务器上,您将使用Noda Time执行时区转换。
-
有一种方法不需要时区转换。基本上,您在UTC中执行所有操作。这包括将时间以UTC格式传输到浏览器。然后你可以使用JavaScript获取用户的当前时间在UTC,并与之比较。
如果您愿意,可以使用JavaScript
Date
类的各种函数来完成此操作。但是你可能会发现使用像moment.js这样的库更容易。虽然这种方法对许多事情都是可行的,但安全性不是其中之一。你的用户可以很容易地改变他们的计算机时钟来解决这个问题。
-
另一种方法是比较服务器端与UTC。如果你的数据库中有确切的UTC开始时间,那么你可以在。net代码中检查
DateTime.UtcNow
,并使用它来决定要做什么。您不需要用户的时区来进行这种比较,但如果您想向他们显示其在当地时间中的含义,则需要它。
我希望这澄清了混乱,没有使情况变得更糟!:)如果您有其他问题,请编辑您的问题或在评论中提出。
关于你最新的问题,我建议你试试以下方法:
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));
当然,这意味着在您的StartDate
和EndDate
字段中,您不会将这些存储为UTC -而是作为与用户相关的"业务日期"。当你应用一个时区时,这些时间只对应于一个特定的时刻,所以相同的UTC时间戳可能落在不同的日期,这取决于用户所在的时区。
此外,您正在使用完全包含的范围,这对于这类日历日期范围通常是可以的。但要确保你意识到可能会有重叠。因此,如果您有2013-01-01 - 2013-02-01
和2013-02-01 - 2013-03-01
,那么有一天2013-02-01
都在范围内。
解决这个问题的一个常见方法是使用半开间隔[start,end)
。换句话说,start <= now && end > now
。但是,当使用完整的日期和时间而不是仅仅使用日期时,这种情况更常见。您可能不需要这样做,但您至少应该为您的特定场景考虑一下。
您可以从浏览器获取用户的时区。这将只获得他们在计算机上设置的值。所以他们可以利用这一点。例如,如果您将访问限制在当地时间午夜之前,他们可以更改时间以获得"早期"访问。所以请记住这一点。
接下来你要做的是在数据库中存储UTC格式的所有时间。你可以用GetUTCDate()代替GetDate()。
接下来,您可以存储它们的时区,或者它们在数据库中与UTC的偏移量。仅仅存储UTC偏移量会有一些危险,因为遵守夏令时的时区在一年中的不同时间会有不同的偏移量。
但是有了这两条信息,你就可以计算出用户的"本地"时间。您可以将小时数添加到存储的日期,然后将其与本地服务器时间(也从UTC调整)进行比较。
如果你真的不关心那么多,你可以在数据库中存储UTC时间,然后简单地比较DateTime。