时间和时区设计:UTC、时间戳在各端应用方式

最后更新: 2026-01-07

時間不會為任何人停止……即便是你。

数据库的时间存储

数据库应储存UTC是黄金标准,最佳实践。

核心原则:存储绝对时间,而非相对时间

  • UTC(协调世界时) 是一个全球统一的、不受夏令时影响的绝对时间点。2025-10-27 11:00:00 UTC 在地球上任何地方,指向的都是同一个时刻。
  • 本地时间 是相对的。2025-10-27 11:00:00 这个字符串本身是模糊的。是北京时间的11点,还是纽约时间的11点?两者相差12或13个小时。如果纽约在11月第一个周日结束夏令时,这个时间点还会“跳变”。
  • 数据库的职责 是忠实地、无歧义地记录事件发生的 那个瞬间。

你现在看到的时间格式大概是这样的:

Plain
12025-10-27T11:00:00.634Z

其中:

  • z表示UTC(零时区)
  • 不依赖服务器部署地区
  • 不受夏令时影响
  • 跨时区数据可直接对比

时间戳

对于时间戳通常有两总理解:

A、不带时间的时间戳

例如 MySQL 的 TIMESTAMP 类型(注意:MySQL 的 TIMESTAMP 实际是带时区的,但这里容易混淆)或 DATETIME,PostgreSQL 的 timestamp(不带时区)。

  • 它只存了YYYY-MM-DD HH:MM:SS,没有时区信息

B、带时区的时间戳

例如 PostgreSQL 的 timestamptz,SQL Server 的 datetimeoffset,Oracle 的 TIMESTAMP WITH TIME ZONE

  • 工作原理:当你存入一个带时区的时间(如 2025-10-27 11:00:00+08:00)时,数据库通常会将其内部转换为 UTC 存储。当你查询时,它会根据你客户端的时区设置,转换回对应的本地时间显示。
  • 存储带时区的时间戳,其底层存储的就是 UTC。UTC 是它的“存储格式”,带时区信息是它的“输入/输出接口”。

两种“时间戳”

我们常说的时间戳不是那种1767752710到秒或是毫秒的么,为什么2025-10-27 11:00:00+08:00也是时间戳?这其实是两种类型:

类型正式名称常见叫法例子特点
Unix时间戳Unix时间/Epoch时间时间戳、时间戳数字、秒数1767752710整数,表示从1970-01-01 00:00:00 UTC开始的秒数
ISO 8601时间戳日期时间字符串时间戳、ISO时间戳、时间字符串2025-10-27 11:00:00+08:00 字符串,包含完整的日期时间和时区信息

语境决定含义

  • 开发/系统层面:说“时间戳”通常指 1767752710
  • 数据库/SQL层面:说“时间戳”通常指带时区的日期时间类型
  • 日志/API文档:说“时间戳”通常指 2025-10-27T11:00:00Z

无论哪种形式,数据库最终存储的都是一个绝对时间点

时间戳转换

我的用户在UTC+8,我需要在接口层转换成UTC+8吗?

并不推荐,如果在接口转成UTC+8,就会失去时区的中立性,而多时区的用户也会变得难以支持。

那么我应该在什么时候做时区转换呢?

前端,仅有一个场景:展示给用户

比如:

TypeScript
1new Date("2025-12-30T09:43:39.634Z").toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })

或者使用dayjs:

TypeScript
1dayjs.utc(time).local().format("YYYY-MM-DD HH:mm:ss")

什么时候需要考虑用户时区?

⚠️当业务中出现自然日语义时,例如:

  • 今天/昨天
  • 本周/本月

这些概念,会依赖用户所在时区,当遇到这一类情况时,依旧靠前端解决,使用用户时区计算时间边界,传递时间戳:

TypeScript
1const tz = "Asia/Shanghai" 2 3const start = dayjs().tz(tz).startOf("day").valueOf() 4const end = dayjs().tz(tz).endOf("day").valueOf() 5 6api.get("/consume", { start, end })

这样做优点在于,后端逻辑极简,完全不用考虑时间上的转换,不考虑用户在哪,更保证了时间的绝对。