2.6 ABP公共结构 - 时间与时区设置

2.6.1 简介

虽然有些应用的需求是单时区,然而另一些是需要对不同的时区进行处理的。为了满足这样的需求以及对时间的处理。ABP提供了处理时间操作的通用基础设施。

2.6.2 Clock

Clock 这个类是主要用来处理 DateTime 的值。它具有以下静态属性和方法:

  • Now :根据当前设置的提供器来获取当前时间

  • Kind :取得当前提供器的 DateTimeKind

  • SupportsMultipleTimezone :取得一个值用来判断该应用当前所使用的提供器是否支持多时区转换(只有ClockProviders.Utc才支持多时区之间的转换)

  • Normalize 对给定的时间使用当前提供器来进行转换

所以我们不应该使用 DateTime.Now,而是使用 Clock.Now,如下所示:

DateTime now = Clock.Now;

ABP中内置了3种Provider,如下所示:

  • ClockProviders.Unspecified (UnspecifiedClockProvider):这是默认的provider并且它的表现行为就像 DateTime.Now

  • ClockProviders.Utc (UtcClockProvider): 它使用UTC时间,Clock.Now 等效于 DateTime.UtcNow。Normalize方法会将给定的时间转换为UTC时间并且设置它的Kind为 DateTimeKind.Utc。它支持多时区操作。

  • ClockProviders.Local (LocalClockProvider): 程序宿主的计算机时间。Normalize方法会将给定的时间转换为本地时间并且设置它的Kind为 DateTimeKind.Local

为了支持多时区转换,你可以设置Clock.Provider为:

Clock.Provider = ClockProviders.Utc;

对于上面的设置,我们通常是在应用程序主入口就设置好了。例如:main函数,web应用的Application_Start函数。

2.6.3 Client Side

我们可以在客户端脚本中使用 abp.clock, 当你在服务器端设置好 Clock.Provider,ABP 会自动的在客户端设置好 abp.clockprovider。ABP创建了一个脚本对象:abp.timing.timeZoneInfo 它包含了当前用户所设置的时区信息。这个信息包含了Windows和IANA时区的id和一些额外的windows时区信息,详细请查看源码 TimingScriptManager 的GetUsersTimezoneScriptsAsync函数。使用这些信息可以将服务器的UTC时间转换为客户端需要显示的时间。

注意:在客户端进行时间转换,首先你得设置你的应用默认为Utc,并且每个用户可以设置自己的时区,这个默认是使用SettingManager来设置的。然后你可以使用monent的timezone插件将服务器端时间转换为本地时间。首先全局设置:moment.tz.setDefault(abp.timing.timeZoneInfo.iana.timeZoneId); 然后你通过动态API,或者WebAPI取得JSON后,将JSON中的时间如此转换: moment(item.creationTime).format('LLL'),或者可以这样: abp.timing.convertToUserTimezone(dateTime).format(); 前提是你要使用abp.moment.js。作者的文档写的也不是很好,这是我开发过程中结合源码补充的。如果要为每个用户设置不同的时区,最好是将时区信息保存到用户表,登录的时候保存到Claim中。那么在MVC中转换的时候我们就可以用到 TimezoneHelper.ConvertFromUtc 。详细可以见提问:https://github.com/aspnetboilerplate/aspnetboilerplate/issues/1320。

如果使用用户表来保存每个用户的时区,最好是自定义一个AbpSession:

    public interface ICustomAbpSession : IAbpSession
    {
        string TimezoneId { get; }

        string ImpersonatorTimezoneId { get; }
        
        string GetUsersTimezoneScript();
    }

    public class CustomAbpSession : ClaimsAbpSession, ICustomAbpSession
    {
        public CustomAbpSession(IMultiTenancyConfig multiTenancy) : base(multiTenancy)
        {

        }

        public virtual string TimezoneId
        {
            get
            {
                var timezoneIdClaim = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == CustomAbpClaimTypes.TimezoneId);
                return string.IsNullOrEmpty(timezoneIdClaim?.Value) ? null : timezoneIdClaim.Value;
            }
        }

        public virtual string ImpersonatorTimezoneId
        {
            get
            {
                var impersonatorTimezoneIdClaim = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == CustomAbpClaimTypes.ImpersonatorTimezoneId);
                return string.IsNullOrEmpty(impersonatorTimezoneIdClaim?.Value) ? null : impersonatorTimezoneIdClaim.Value;
            }
        }
        
        //使用这个代码来重置 TimingScriptManager.cs 的GetUsersTimezoneScriptsAsync函数取得的脚本。
        //当然需要在_Layout.cshtml 中调用这个方法,如果不想这样写,也可以继承ITimingScriptManager接口,重新实现它。
        public virtual string GetUsersTimezoneScript()
        {
            if (!Clock.SupportsMultipleTimezone)
                return string.Empty;

            var timezoneId = TimezoneId;
            var timezone = TimeZoneInfo.FindSystemTimeZoneById(timezoneId);          

            var timezoneInfo = " {" +
                                  "        windows: {" +
                                  "            timeZoneId: '" + timezoneId + "'," +
                                  "            baseUtcOffsetInMilliseconds: '" + timezone.BaseUtcOffset.TotalMilliseconds + "'," +
                                  "            currentUtcOffsetInMilliseconds: '" + timezone.GetUtcOffset(Clock.Now).TotalMilliseconds + "'," +
                                  "            isDaylightSavingTimeNow: '" + timezone.IsDaylightSavingTime(Clock.Now) + "'" +
                                  "        }," +
                                  "        iana: {" +
                                  "            timeZoneId:'" + TimezoneHelper.WindowsToIana(timezoneId) + "'" +
                                  "        }," +
                                  "    }";

            return " abp.timing.timeZoneInfo = " + timezoneInfo;
        }
    }

2.6.4 时区

ABP定义了一个 Abp.Timing.TimeZone (常量:TimingSettingNames.TimeZone) 配置名,用来存储Host,Tenant和User所选择的时区。ABP假定设定的时区是一个有效的 Windows timezone name。ABP也定义了一个时区映射文件,将Windows时区转换为 IANA 时区,这是因为有些通用库所使用的是 IANA timezone id。为了支持多时区,必须使用 UtcClockProvider。如果使用 UtcClockProvider,那么所有的时间值将会以UTC时间进行存储,并且以UTC时间发送到客户端。那么在客户端我们能将UTC时间转换为客户所设置的时区。

注意: 我遇到过这样的问题,在Windows Server 2012 如果系统的时区是协调世界时(Coordinated Universal Time)。并且默认用户时区是系统时区时,TimeZoneInfo.FindSystemTimeZoneById(timezoneId),ABP中取得IANA时区会报异常。

2.6.5 Binders and Converters

  • ABP能自动normalize来自如MVC,WebAPI以及ASP.NET Core应用的时间,这是基于当前的ClockProvider的。

  • ABP能基于当前的ClockProvider自动normalize来自数据库的时间,当 [EntityFramework](9.1ABP基础设施层-集成Entity Framework.md) 以及 NHibernate 模块被使用的时候。

如果 UtcClockProvider 被使用,所有的时间都会作为UTC时间存储在数据库。并且所有来自客户端的时间都会被当做UTC时间来接收除非被明确的指定为其他时区。