【问题标题】:How do I properly store UTC date in database from user date and timezone input?如何根据用户日期和时区输入将 UTC 日期正确存储在数据库中?
【发布时间】:2014-11-01 20:27:38
【问题描述】:

我的 javascript 应用程序必须创建事件,这些事件将作为 UTC 存储在数据库中,以便之后可以在 3 个不同的时区中显示。

我发现难以弄清楚的棘手部分是,在创建事件时,用户必须选择一个时区和日期。

这个想法是: - 用户从带有时区的附加下拉列表中选择日期 + 所需时区。 - 我在数据库中存储为 UTC - 所有用户都可以看到 3 个不同时区的日期。

您可能会问,当使用日期选择器选择日期时,为什么有人需要额外的下拉列表来选择另一个时区,默认情况下已经包含一个时区。

示例:假设 Jim,美国公民,他一生都在使用 EDT 华盛顿时间计划活动;他正在中国访问,进入一家中国网吧,并想使用这个应用程序计划一个活动。日期选择器将选择当地时区,即中国标准时间。但 Jim 想通过 EDT 进行计划,并确保该应用程序能够正确处理所有事情。

因此,他必须从额外的下拉列表中专门选择所需的时区。

所以我的问题是。 因为我允许用户选择所需的时区,我是否首先必须将用户输入的日期转换为那个时区,然后再将其转换为 UTC,然后才存储它? 还是在将事件保存在数据库中时,我根本对时区转换不感兴趣?

那么哪一步是正确的: - 获取本地日期+选定的时区 - 将本地日期转换为用户选择的时区 - 将日期转换为 UTC - 存储到数据库 - 阅读时,使用选定的时区转换为 3 个时区

或 - 获取本地日期+选定的时区 - 将日期转换为 UTC,忽略时区 - 存储到数据库 - 阅读时,使用选定的时区转换为 3 个时区


稍后编辑 - 我在 meteor 中执行此操作,因此是 javascript 服务器端。 DB 是 mongodb,因此出于性能原因,日期必须保存为 JS 日期对象(当然是 utc 格式)。


第二次编辑

以下是我尝试过的实现(它不起作用,例如当我输入 KST 上午 07:00 的事件日期时,当输出从数据库读回的最终结果并在该 KST 时区中转换回时显示除了07:00 AM)

一切从这里开始 - 这是一个服务器端方法,它从日期选择器中读取日期、从时间选择器中读取时间以及从下拉列表中读取时区:

var pStartDate = GetDateAndTimeFromPostData(eventAttributes.startDate, eventAttributes.startTime, eventAttributes.timezone);

这里我尝试从不同的控件(datepicker、timepicker、timezone ddl)构建选定的日期:

function GetDateAndTimeFromPostData(dt, tm, timezone)
    {
        var t = tm.split(":");
        var hour =  t[0];
        var min = t[1];

        var finalDate = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), hour, min);
        var utcConverted =  ConvertUserTimezoneToServerTimezone(finalDate, timezone);

        return utcConverted;
    }

我在这里尝试时区转换:

function ConvertUserTimezoneToServerTimezone(dateToConvert, tz)
    {
        var userTimezonedDate;

        switch(tz)
        {
            case "EDT":
            {
                userTimezonedDate = moment.tz(dateToConvert, "America/New_York");
                break;
            }
            case "CEST":
            {
                userTimezonedDate = moment.tz(dateToConvert, "Europe/Berlin");
                break;
            }
            case "KST":
            {
                userTimezonedDate =  moment.tz(dateToConvert, "Asia/Seoul");
                break;
            }
            case "CST":
            {
                userTimezonedDate = moment.tz(dateToConvert, "Asia/Shanghai");
                break;
            }
        }

        var utcDateFromUserTimezonedDate = userTimezonedDate.utc().toDate();
        return utcDateFromUserTimezonedDate; 
    }

错误已经在上面的代码中,因为 utc 日期没有保存为 KST,而是保存为 GMT(我的本地时区)。

作为旁注,我不太了解时刻时区如何转换;当我访问时区网站时,我会在 chrome 开发工具中这样写:

var x = new Date();
var y = moment.tz(x, "Asia/Seoul");
y.utc().toDate()

我实际上希望返回一个显示 KST 的日期对象,对吗?但它显示 GMT+2,我的本地 tz。

2014 年 9 月 8 日星期一 23:44:05 GMT+0200(中欧夏令时间)

我也尝试过向后考虑,例如存储的日期对象应该是什么样子,但这也令人困惑 - 是否应该将其保存为选定的时区?例如2014 年 9 月 8 日星期一 23:44:05 GMT+0900 (KST) 如果不是,我存储的日期对象应该是什么样子?

再次,我使用带有流星和 mongoDB 作为数据库的 javascript 服务器端。

非常感谢,

【问题讨论】:

  • 这在很大程度上取决于您要在数据库中存储的格式......
  • 只需使用不显示本地时区但可配置为任何时区的日期选择器,然后生成 UTC 时间戳。
  • @Amc_rtty 你在这里最终得到了什么解决方案?
  • @geoboy 采用了 Matt Johnson 的解决方案,见下文

标签: javascript datetime timezone


【解决方案1】:

您提供的选择都不合适。

  • 如果您允许用户在美国东部时间安排活动,那么他将输入该时区的时间。他目前在中国的事实是无关紧要的。所以从他当地的中国标准时间转换为 UTC 并不是你想做的事情。

  • 由于您要存储未来事件,因此存储 UTC 值并不是最好的建议。您应该存储原始输入的值和原始选择的时区。您也可以存储 UTC 时间,但您应该随时准备重新计算它。

    这很重要,因为时区规则可能会在输入事件的时间和事件的实际时间之间发生变化。例如,用户可能不是在美国,而是在俄罗斯。 There are changes coming this year (2014),并且需要更新您的时区数据。如果事件是在您应用数据更新之前安排的,那么您将使用 old 规则来计算 UTC 值。如果事件发生在此更改之后(例如,2014 年 11 月),那么一旦您应用更新,事件时间就会错误地更改。

如果你想在 JavaScript 中完成这一切,有several libraries for working with time zones in JavaScript。但是,我主要在后端使用 JavaScript 时建议这样做,例如使用 Node.js 应用程序。

使用服务器端代码通常更容易解决此类问题。您可能应该调查有哪些选项可用于以您在后端代码中使用的语言处理时区。

为了进一步阅读,我已经写了好几次了:

关于您的编辑 - 对于您提供的代码,我可以提供以下建议:

  • 切勿尝试将时区缩写映射到特定时区。正如see in this list on Wikipedia 所言,歧义太多了。即使在您自己的 4 个时区列表中也存在一些问题。具体来说,EDT 仅用于America/New_York 的一年中的部分时间 - 一年中的另一部分是 EST。虽然你有 Asia/Shanghai 的 CST,但它也可以申请 America/Chicago 和其他几个地方。 (CST 有 5 种不同的含义。)

  • 除了时区缩写的下拉列表,还有其他一些选项:

    • 如果您只需要处理几个时区,您可以提供一个下拉菜单。只需在值中使用时区 id,并在文本中使用时区的全名。例如:

      <select name="tz">
          <option value="America/New_York">Eastern Time (North America)</option>
          <option value="Europe/Berlin">Central European Time</option>
          <option value="Asia/Seoul">Korean Standard Time</option>
          <option value="Asia/Shanghai">China Standard Time</option>
      </select>
      
    • 如果您想列出世界上的所有时区,您可能会发现放入一个下拉列表太长。在这种情况下,请提供 两个 下拉列表。第一个将选择一个国家,然后第二个将选择所选国家内的时区。由于许多国家/地区只有一个时区,因此某些用户根本不必从第二个列表中进行选择。

    • 如果您想要一种更具交互性的方法,请考虑使用基于地图的时区选择器控件,例如 this onethis one

  • 在您的 GetDateAndTimeFromPostData 函数中,您构造了一个 Date 对象。您需要记住,Date 对象在内部始终是 UTC,但对于大多数输入和输出而言,它采用本地时区的行为。 local 是指运行代码的计算机的本地。在服务器端函数中,这将是您的服务器的时区 - 在这种情况下不合适。无需使用 Date 对象,因为您已经在使用 moment.js。

  • 确保您了解在现有矩对象上调用 .tz(zone) 与调用 moment.tz(value, zone) 之间存在区别。先验将某个时刻调整到特定时区,而后者创建一个已经在特定时区表示的新时刻。

  • 关于您的旁注,您正在违背时刻和时刻时区的目的:

    • 由于x代表当前日期和时间,那么moment.tz(x, "Asia/Seoul")就和moment().tz("Asia/Seoul")一样

    • y.utc() 正在将值转换回 UTC,因此调用 .tz(...) 开始是没有意义的

    • .toDate() 将所有内容放回 Date 对象中,该对象在内部表示 UTC,但始终使用本地时区显示其输出。

    • 所以最后,您只是将当前日期和时间作为Date 对象,所以整个事情将减少到new Date()

  • 对于 MongoDB,它的 ISODate 类型无论如何都只是存储 UTC 值。因此,对于需要转换为 UTC 的部分代码,请考虑以下内容:

    moment.tz([2014,0,1,10,0],'Asia/Seoul').toISOString()
    

    或者您可以直接传递等效的 Date 对象 - 取决于您使用 Mongo 客户端的方式:

    moment.tz([2014,0,1,10,0],'Asia/Seoul').toDate()
    
  • 请考虑一下我在最初回复中所说的话。 UTC 值将帮助您及时了解 even 应该运行的时间 - 但您不应该忘记原始输入值!如果eventAttributes.startDate 已经是Date 对象,那么它可能已经转换为UTC,因此您丢失了原始输入值。您应该确保使用代表用户提供的 ISO 字符串将值从客户端传递到服务器。例如,传递2014-12-25T12:34:00。不要尝试在此处传递偏移量,或转换为 UTC,或传递整数。除非您准确传递用户提供的内容,否则您无法存储该值。

    当您将原始输入值存储在 Mongo 中时 - 将其存储为字符串。如果您将其存储为 Date,那么 Mongo 也会将其调整为 UTC。

【讨论】:

  • 感谢您的宝贵时间;我正在使用 JS 服务器端(流星)执行此操作,并且正在使用 moment.js 和 moment 时区。此刻,我仍然对如何准确实现这一点感到有些困惑;我将用我目前得到的代码更新帖子。
  • 在最初的帖子中添加了我所有的代码 - 你能看一下吗?谢谢
  • 我已更新我的答案以解决您的代码。但是,请考虑提出更具体的个人问题(作为新问题)。像这样的长篇文章可能只对您有用,而 StackOverflow 旨在构建对其他人也有用的材料。此外,如果您花一些时间搜索和浏览各种标签,您会发现其中许多问题已经在其他帖子中得到解决。谢谢。
  • 非常感谢!当我回到家时,我会读你写的所有东西。关于您的最后一条注释,将数据库中存储为字符串而不是日期 - 之后按日期查询时不会引起问题吗? “在日期 X 和 Y 之间给我” - 如果日期存储为字符串,那会影响性能,不是吗?
  • 按照 Mongo 的建议存储您的 UTC 值,以便您查询它。将 original 输入值存储为字符串。你不需要查询它。如果/当您需要重新计算 UTC 值时,您将使用它,例如,如果您更新您的时刻时区数据以反映新的 tz 变化。
【解决方案2】:

在 JavaScript 中,您可以或者使用 UTC,使用机器上的本地时间(但不知道其时区;您最多可以确定到UTC)。

很多人尝试在 JS 中转换时区都失败了,但是所有这些方法都注定要失败,因为 DST、闰年/秒等。有太多的陷阱使实现变得极其复杂。

时间转换操作最好在后端完成。

您说您将日期存储在数据库中,所以我假设您将其转移到某个后端。

假设您的后端是某个 PHP 应用程序,您将从日期选择器和时区中收集纯日期作为简单字符串(例如 2014-09-08 15:12:00)。

在 PHP 中,您现在可以执行以下操作:

$timestamp = strtotime($_POST['date']);
$dateLocal = new \DateTime("@$timestamp", new \DateTimezone($_POST['timezone']));
$dateUtc = $dateLocal->setTimezone(new \DateTimezone('UTC'));

现在,$dateUtc 将日期作为 UTC 中坚如磐石的 DateTime 对象包含在内,您现在可以对其进行进一步处理。

顺便说一句,如果您想在执行任何其他操作之前向用户显示转换后的时区,我会将其实现为某种 AJAX 服务。如前所述,尝试在 JavaScript 本身中转换时区注定会失败。

【讨论】:

  • 不一定会失败。当然,对于任何既不是本地时间也不是 UTC 的时区,您都应该使用日期库,而不是原生的 Date 对象。
  • @Bergi:我很想知道有一个几乎完美的时间转换实现的 JS 库。在这个时候,我不会相信我过去见过的任何人。
  • github.com/mde/timezone-js 看起来相当成熟。不过我承认我没有用过。
  • 谢谢,看起来确实很有趣。
  • 这样的库有 5 个,as listed here。它们具有不同级别的功能。
猜你喜欢
  • 2011-04-17
  • 2011-09-26
  • 2018-08-23
  • 2011-02-04
  • 2011-12-24
  • 1970-01-01
  • 2014-01-16
  • 2018-09-28
  • 2020-02-26
相关资源
最近更新 更多