Skip to main content

从 DataTime 类型修改开始

目前的所有时间字段都是这样定义的:

created_at DateTime @default(now())

这是一种不推荐的写法(尤其当你的应用有全球用户、需要处理不同时区时)。

为什么需要修改?

  • Prisma 默认把 DateTime 映射为 PostgreSQL 的 TIMESTAMP不带时区,timestamp without time zone)。
  • 这会导致时区混乱、夏令时问题、不同服务器/客户端时间偏移等 Bug。
  • 最佳实践是统一使用 TIMESTAMPTZ(带时区的时间戳),它在数据库内部始终以 UTC 存储,是处理多时区应用的标准做法。

推荐的修改方式

将所有时间字段修改为以下形式:

// 创建时间
created_at DateTime @default(now()) @db.Timestamptz(6)

// 更新时间(推荐使用 @updatedAt 自动更新)
updated_at DateTime @updatedAt @db.Timestamptz(6)

// 其他业务时间字段(例如事件开始时间、截止时间等)
event_time DateTime? @db.Timestamptz(6)
published_at DateTime? @db.Timestamptz(6)

完整修改建议示例:

model User {
id Int @id @default(autoincrement())
name String?
email String @unique
timezone String @default("UTC") @db.Text

created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @updatedAt @db.Timestamptz(6)

// 其他模型类似处理
}

修改后的步骤

  1. 修改 schema.prisma 文件中所有 DateTime 字段,加上 @db.Timestamptz(6)

  2. 生成迁移

    npx prisma migrate dev --name add_timestamptz

    (或者如果你是第一次大改,可以用 prisma db push 测试)

  3. 重新生成 Prisma Client

    npx prisma generate

注意事项

  • 现有数据:修改后,如果表中已有数据,Prisma 迁移通常能自动处理(因为只是类型转换),但建议在生产环境前先在测试库验证。
  • 精度(6) 表示微秒精度,是目前最常用的设置。如果你对精度要求不高,也可以用 (3)(毫秒)。
  • @updatedAt:强烈推荐对 updated_at 使用 @updatedAt,它会自动在更新记录时填充当前时间。
  • 默认值@default(now()) 仍然保留,它在数据库层面会使用当前 UTC 时间。

总结推荐

  • 所有时间戳字段 → 统一改成 DateTime @default(now()) @db.Timestamptz(6)(或 @updatedAt)。
  • 存入数据库时永远是 UTC
  • 显示给用户时,再结合 user.timezone 使用 AT TIME ZONE 转换为本地时间。

Prisma 时区字段

Prisma 中定义用户的时区字段(IANA 时区名称,如 Asia/ShanghaiAmerica/New_York),推荐写法如下:

推荐定义方式(Prisma Schema)

model User {
id Int @id @default(autoincrement())
name String?
email String @unique

/// 用户的 IANA 时区
timezone String @default("UTC") @db.Text

// 或者限制长度(更节省空间)
// timezone String @default("UTC") @db.VarChar(64)

createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
}

说明:

  • 类型使用 String:Prisma 中对应 PostgreSQL 的字符串类型。
  • @default("UTC"):强烈推荐设置默认值为 'UTC',避免空值导致的问题。
  • @db.Text:映射到 PostgreSQL 的 TEXT 类型(推荐,无长度限制,简单清晰)。
  • @db.VarChar(64):如果你想更严格控制长度也可以使用(时区名称最长通常不会超过 50 个字符)。

进阶:添加数据库约束(强烈建议)

Prisma 本身不支持直接写 CHECK 约束,但你可以在迁移后手动在 PostgreSQL 中添加,或者通过 Prisma 的 @@map + raw SQL 方式处理。

推荐做法

  1. 先用上面的 schema 生成迁移。
  2. 然后在数据库中手动添加校验约束:
ALTER TABLE "User" 
ADD CONSTRAINT valid_timezone
CHECK (timezone IN (SELECT name FROM pg_timezone_names));

或者允许为空时使用 UTC:

ALTER TABLE "User" 
ADD CONSTRAINT valid_timezone
CHECK (timezone IS NULL OR timezone IN (SELECT name FROM pg_timezone_names));

在代码中使用示例(SvelteKit + Prisma)

保存用户时区:

// +server.ts 或 actions
import { prisma } from '$lib/prisma';

async function saveUserTimezone(userId: number, clientTimezone: string) {
await prisma.user.update({
where: { id: userId },
data: {
timezone: clientTimezone // 来自浏览器 Intl.DateTimeFormat().resolvedOptions().timeZone
}
});
}

查询时按用户时区转换时间:

const events = await prisma.$queryRaw`
SELECT
e.title,
e.start_time AT TIME ZONE u.timezone AS local_start_time,
u.timezone
FROM "Event" e
JOIN "User" u ON e.user_id = u.id
WHERE e.user_id = ${userId};
`;

完整建议总结

  • 时区字段 → String @default("UTC") @db.Text(最常用)
  • 所有时间字段强烈推荐使用 @db.Timestamptz(6)(带时区的时间戳)
  • 在应用层始终存 UTC,只在展示层用 AT TIME ZONE 转换为用户时区
  • 时区字段建议加上数据库 CHECK 约束,保证数据有效性

需要我帮你生成完整的 schema.prisma 片段,或者包含 Prisma Client 查询转换时间的示例代码吗?

PostgreSQL 时区字段

在 PostgreSQL 中保存用户的时区信息(例如 Asia/ShanghaiAmerica/New_York 等 IANA 时区名称),最推荐的数据类型是 TEXTVARCHAR

推荐方案

CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
timezone TEXT NOT NULL DEFAULT 'UTC', -- 推荐
-- 或者
-- timezone VARCHAR(64) NOT NULL DEFAULT 'UTC',

created_at TIMESTAMPTZ DEFAULT NOW()
);

为什么使用 TEXT / VARCHAR

  • PostgreSQL 没有专用的 “timezone” 数据类型。
  • 用户时区来自浏览器 Intl.DateTimeFormat().resolvedOptions().timeZone,返回的是标准的 IANA/Olson 时区名称(如 Asia/Shanghai)。
  • TEXT 可以完整、精确地保存这些字符串,且长度通常在 20~50 个字符以内,VARCHAR(64)VARCHAR(100) 也足够。
  • 官方和社区的共识(StackOverflow、PostgreSQL 文档相关讨论)都是:存储时区名称时用 text

进阶:添加约束(强烈推荐)

为了保证数据质量,可以加上 CHECK 约束,确保存入的值是 PostgreSQL 真正支持的有效时区:

ALTER TABLE users 
ADD CONSTRAINT valid_timezone
CHECK (timezone IN (SELECT name FROM pg_timezone_names));

或者更灵活一点:

ALTER TABLE users 
ADD CONSTRAINT valid_timezone
CHECK (timezone IS NULL OR timezone IN (SELECT name FROM pg_timezone_names));

pg_timezone_names 是 PostgreSQL 内置视图,包含所有可用的 IANA 时区。

实际使用示例

  1. 插入/更新用户时区(从 SvelteKit 后端接收):
-- 在 +server.ts 或 action 中执行
await db.query(
`UPDATE users SET timezone = $1 WHERE id = $2`,
[userTimezone, userId]
);
  1. 查询时按用户时区显示时间(非常实用):
SELECT 
event_name,
start_time AT TIME ZONE u.timezone AS local_time,
u.timezone
FROM events e
JOIN users u ON e.user_id = u.id
WHERE e.user_id = 123;

AT TIME ZONE 是 PostgreSQL 中非常强大的操作符,能把服务器存储的 TIMESTAMPTZ(内部是 UTC)转换为用户本地时间。

其他备选方案(不推荐作为主选)

数据类型用途缺点适用场景
TEXT / VARCHAR存储 IANA 时区名称无(推荐)绝大多数情况
INTERVAL仅存储 UTC 偏移(如 +08:00)无法处理夏令时、历史规则变化简单固定偏移场景
VARCHAR(10)存储缩写(如 CST)歧义严重(多个地区用相同缩写)不推荐

最佳实践总结

  • 时区列用 TEXT,默认值设为 'UTC'
  • 加上 CHECK 约束验证有效性。
  • 所有事件/时间相关字段优先使用 TIMESTAMPTZ(存 UTC)。
  • 在应用层或查询时用 AT TIME ZONE user.timezone 转换为用户本地时间。

你当前的表结构大概是什么样的?需要我帮你写完整的建表语句 + SvelteKit 服务端保存代码吗?