如何在支持时区的数据库中存储生日?

How to store birthdays in a database with timezone support?

我正在使用 JavaScript 和 PostgreSQL 数据库,我想存储生日并在他们自己的时区中午 12 点通知用户,目前我正在将日期从他们的时区转换为我的本地服务器时间并检查每个查看日期和时间是否匹配的小时

import { parseFromTimeZone } from "date-fns-timezone";

const userInput = "08-11" // day/month
const timeZone = "Europe/Amsterdam"
const date = parseFromTimeZone(`2000-${userInput} 00:00:00`, { timeZone });

// This is what I store in my database
const dateToStore = date.toISOString().slice("2000:".length).split(":")[0];

// This is what I run every hour
await Birthday.find({
  where: {
    date: new Date().toISOString().slice("year:".length).split(":")[0],
  },
});

问题是这个解决方案不是很动态,因为如果我迁移我的服务器它就会中断,我的问题是:

我有 date-fns 可用,但我不介意使用其他库

我推荐一个包含三个字段的 account table 的解决方案:

  1. birthday,属于 Postgres 类型 date
  2. timezone,Postgres 类型 text。在这里你可以存储类似 Europe/Amsterdam 的东西,重要的是它是 Postgres 和你的日期库都可以识别为时区的东西。
  3. last_birthday_wish_sent_at 类型 timestamptz(shorthand 对应 timestamp with time zone,它在内部将所有内容存储为 UTC)。

我已将生日日期与其时区分离,因为请记住,用户的生日在世界任何地方始终是同一天,即使他们四处走动。因此,如果我在阿姆斯特丹的生日是 8 月 11 日,那么如果我搬到 San F运行cisco,生日仍然是 8 月 11 日。单独存储这些组件将允许您在它们移动时重新配置它们的时区。

我 运行 在每个小时的第 0 分钟执行一个 cron,运行 逻辑是这样的(伪代码,抱歉):

for timezone in all timezones:
     if > 12 PM in timezone:
         for account in accounts in timezone:
             if birthday <= today AND (last_birthday_wish_sent_at IS NULL OR last_birthday_wish_sent_at < now() - '1 year):
                 send birthday wish
                 set last_birthday_wish_sent_at = now()

last_birthday_wish_sent_at 的目的是让您可以编写一个更笨和更灵活的算法(即即使 cron 失败一小时,生日祝福仍然会发送),但仍然确保永远不会任意年份双送生日祝福

将其建模为单独的 table 可能更安全,您可以在其中跟踪您曾经发送过的每个生日祝福以及您发送的用户和年份。这消除了跨年度边界的任何时间错误的可能性。

您希望将上述伪代码中的帐户选择和过滤建模为 SQL,这样您就不会返回比必要的更大的结果集。类似于:

SELECT *
FROM account
WHERE timezone IN ('Europe/Amsterdam', ...)
    -- note: actual date comparison a little more complicated than this
    -- because you should make sure to compare the month and day components
    -- only (probably with the `EXTRACT` function)
    AND birthday <= NOW()
    AND (
        last_birthday_wish_sent IS NULL
        OR last_birthday_wish_sent < NOW() - '1 year'::interval
    );

并确保在 timezonebirthdaylast_birthday_wish_sent 上有适当的索引。

您还可以加强时区检查的逻辑:它总是在某个地方变成下午 12 点,但它完全可以预测table 发生这种情况的地点,因此没有必要每次都检查每个时区。您还可以将其推送到 Postgres 并将整个选择逻辑打包到一个查询中。