前端与数据库之间如何正确管理日期和时区
我似乎找不到一个对这个问题的明确方案,选项很多,优缺点也各不相同。
我有一个用Flutter编写的医疗应用,用来记录执行某个治疗/操作的日期和时间。显示的日期需要始终以“墙钟时间”呈现——也就是说如果某次操作在下午4点进行,第二天时区因夏令时变化,前端展示的历史记录需要显示当日的4 点。
我读到的建议大多如下:
- 在前端把本地日期时间转换为UTC,并通过网络发送。
- 在数据库中将日期以UTC存储。
- 将日期以UTC的形式返回给前端,在前端再转换成本地时间
这会导致时间随时区变化而改变——也就是说某天会显示为2 点,夏令时变化后,历史记录将显示为1 点。
其他选项建议存储:
- 以UTC存储日期/时间/时区(在C#中使用datetimeoffset)
- 本地日期/时间
- 时区(即Australia/Sydney)
我不理解的是,为什么不直接把执行该手术/治疗时的本地日期/时间/时区(使用datetimeoffset)存入数据库,然后在前端始终显示这个?很多帖子都建议不要这样做,但我不能理解原因。
另外,我还需要能够对在某一天执行的所有手术/治疗进行报表,因此仅使用UTC的日期/时间在这种场景下行不通(我需要知道用户所在的时区,以判断该操作是在第12日还是第13日,例如)。
解决方案
你的直觉是对的。所谓“只存UTC”的建议,是面向只关心时间瞬点的应用——这类应用占多数,因此成了所谓的“标准”建议。你的应用关心的是墙钟时间。
下面这样的方案可以同时满足两方面的需求:
procedure_at TIMESTAMPTZ NOT NULL, -- UTC, for ordering and comparisons
timezone TEXT NOT NULL -- "Australia/Sydney"
UTC给你一个统一的时间线,用于排序和跨时区比较(如果你需要的话),而IANA时区名称让你把时间转换回该日期的正确墙钟时间。时区数据库应包含历史规则,因此它应用的是当时生效的偏移量,而不是今天的偏移量。
要报出“12日所有的操作”,请把 procedure_at 转换到存储的时区,并按本地日期进行筛选。
你的 DateTimeOffset 方法也可以工作,因为你直接得到墙钟时间,并且可以从偏移量推导出UTC。你只是失去了地点名称,因此 +11:00 可能是Sydney或 Solomon Islands。对于一个地点上下文可能很重要的医疗应用,我会偏向使用IANA名称。我认为现在大多数平台对它的支持都相当好。