1. 核心原则
系统内部(数据库、后端代码、API 传输)统一使用 UTC,只有在数据真正呈现在用户眼前时,才转换成用户的当地时间。
2. 数据库设计
✅ 推荐方案:使用 timestamptz
在建表时,时间字段必须使用 timestamp with time zone (简写为 timestamptz)。
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW() -- ✅ 正确
);
- 原理:Postgres 在存储
timestamptz时,会自动将输入的任何时区时间转换为 UTC 格式存储。它内部不保存时区名(如 ‘Asia/Shanghai’),只保存 UTC 时间点。 - 优势:
- 语义明确:告诉所有连接数据库的客户端(代码、BI 工具、SQL 客户端),这是一个绝对时间点。
- 计算强大:支持直接在 SQL 中跨时区查询(如
AT TIME ZONE语法)。 - 防呆:无论误传了哪个时区的时间,存入后在物理时间轴上都是同一个点。
❌ 反模式:使用 timestamp (without time zone) 存 UTC
不要为了“只存 UTC”而使用 timestamp without time zone。
- 风险:这会让数据变成“裸奔”状态(没有元数据)。第三方工具(如 BI 报表)或不同的数据库驱动会根据自己的配置猜测这个时间的含义,极易导致 8 小时偏差的 Bug。
3. 数据流转架构
阶段一:数据写入 (Write)
- 前端 (浏览器/App):
- 用户选择/产生时间(本地时间)。
- 提交前将时间转换为 UTC 的 ISO 8601 字符串。
- 例如:
"2023-12-18T10:00:00Z"(带Z代表 UTC)。
- 后端 (API):
- 透传该时间对象。
- 数据库 (Postgres):
- 接收到带
Z的时间字符串。 - 字段为
timestamptz。 - 处理:由于输入已是 UTC,且存储目标也是 UTC,数据库不进行任何时区加减计算,直接落库。
- 效率最高,路径最短。
- 接收到带
阶段二:数据读取 (Read)
- 数据库配置:
- 后端服务连接 Postgres 时,Session 时区设置为 UTC (
SET TIME ZONE 'UTC')。
- 后端服务连接 Postgres 时,Session 时区设置为 UTC (
- 数据库 (Postgres):
- 读取数据,原样吐出 UTC 时间。
- 后端 (API):
- 序列化为 JSON,格式统一为
"2023-12-18T10:00:00Z"。
- 序列化为 JSON,格式统一为
- 前端 (浏览器/App):
- 接收 UTC 字符串。
- 根据用户浏览器当前时区(或用户设置的时区)进行格式化展示。
- 显示结果:
2023-12-18 18:00:00(如果是北京用户)。
4. 为什么这个方案是最好的?
- 数据库算力节省:前端直接提交 UTC,Postgres 仅做存储,不做时区偏移计算。
- 避免配置漂移:不依赖服务器(OS)时区,也不依赖 Postgres 配置文件(
postgresql.conf)里的时区。所有时间自带上下文。