数据库
PostgreSQL + Drizzle ORM 配置与管理
数据库
Sistine Starter 使用 PostgreSQL 作为主数据库,配合 Drizzle ORM 提供类型安全的数据库操作。本指南将帮助你配置数据库连接、理解表结构和执行数据库迁移。
为什么选择 PostgreSQL?
PostgreSQL 是一个强大的开源关系型数据库,具有以下优势:
- 可靠性: 经过多年生产环境验证
- 功能丰富: 支持 JSON、全文搜索、地理数据等
- ACID 事务: 确保数据一致性
- 可扩展性: 适合从小型应用到大型系统
- 云原生: 各大云平台都提供托管服务
选择数据库提供商
Neon (推荐)
Neon 是无服务器 PostgreSQL,按需付费,启动速度快。
优势:
- 免费额度慷慨(500MB 存储 + 100 小时计算时间)
- 自动扩缩容
- 分支功能(可为每个 PR 创建数据库分支)
- 启动时间 < 1 秒
设置步骤:
- 访问 neon.tech 并注册
- 创建新项目
- 复制连接字符串(包含
?sslmode=require
) - 粘贴到
.env.local
的DATABASE_URL
配置数据库连接
在 .env.local
中配置数据库连接字符串:
# PostgreSQL 连接字符串
DATABASE_URL="postgresql://user:password@host:5432/database?sslmode=require"
连接字符串格式说明:
postgresql://[用户名]:[密码]@[主机]:[端口]/[数据库名]?[参数]
参数 | 说明 | 示例 |
---|---|---|
用户名 | 数据库用户 | postgres |
密码 | 用户密码 | your_password |
主机 | 数据库服务器地址 | ep-xxx.us-east-2.aws.neon.tech |
端口 | 端口号(默认 5432) | 5432 |
数据库名 | 数据库名称 | sistine |
参数 | 额外参数 | sslmode=require |
重要提示:
- ✅ 生产环境必须使用
sslmode=require
确保安全连接 - ✅ 不要将
.env.local
提交到 Git - ✅ 开发和生产使用不同的数据库
Drizzle ORM 简介
Drizzle 是一个现代化的 TypeScript ORM,具有以下特点:
- 类型安全: 完整的 TypeScript 类型推断
- SQL-like: 接近原生 SQL 的查询语法
- 零运行时开销: 编译时类型检查,运行时无额外负担
- 自动迁移: 支持自动生成迁移文件
基础使用示例
import { db } from "@/lib/db";
import { user } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
// 查询用户
const users = await db.select().from(user).where(eq(user.email, "user@example.com"));
// 插入用户
await db.insert(user).values({
id: "uuid",
name: "张三",
email: "zhangsan@example.com",
emailVerified: false,
credits: 300,
});
// 更新用户积分
await db.update(user)
.set({ credits: 500 })
.where(eq(user.id, "uuid"));
// 删除用户
await db.delete(user).where(eq(user.id, "uuid"));
事务操作
import { db } from "@/lib/db";
import { user, creditLedger } from "@/lib/db/schema";
import { sql } from "drizzle-orm";
// 扣除积分(事务性操作)
await db.transaction(async (tx) => {
// 扣除用户积分
await tx.update(user)
.set({ credits: sql`${user.credits} - 10` })
.where(eq(user.id, userId));
// 记录到账本
await tx.insert(creditLedger).values({
id: randomUUID(),
userId,
delta: -10,
reason: "chat_usage",
});
});
数据库迁移命令
Sistine Starter 提供以下命令管理数据库 Schema:
生成迁移文件
当你修改了 lib/db/schema.ts
后,生成迁移 SQL 文件:
pnpm db:generate
迁移文件会生成在 drizzle/
目录下。
执行迁移
将迁移文件应用到数据库:
pnpm db:migrate
适用场景: 生产环境部署,确保版本可追溯。
推送 Schema(开发环境)
直接同步 Schema 到数据库,无需生成迁移文件:
pnpm db:push
适用场景: 本地开发快速迭代。
警告: db:push
可能会删除数据,生产环境禁用!
Drizzle Studio(可视化管理)
启动浏览器版数据库管理界面:
pnpm db:studio
访问 https://local.drizzle.studio
查看和编辑数据。
核心表结构
用户与认证表
user (用户表)
存储用户的基本信息和积分余额。
字段 | 类型 | 说明 |
---|---|---|
id | text | 用户唯一 ID (主键) |
name | text | 用户名称 |
email | text | 邮箱地址(唯一) |
emailVerified | boolean | 邮箱是否已验证 |
image | text | 头像 URL |
credits | integer | 当前可用积分 |
role | text | 用户角色 (admin / user ) |
planKey | text | 当前订阅计划 ID |
banned | boolean | 是否被封禁 |
banReason | text | 封禁原因 |
banExpires | timestamp | 封禁到期时间 |
createdAt | timestamp | 注册时间 |
updatedAt | timestamp | 更新时间 |
session (会话表)
存储用户登录会话。
字段 | 类型 | 说明 |
---|---|---|
id | text | 会话 ID (主键) |
token | text | 会话 Token (唯一) |
userId | text | 关联用户 ID |
expiresAt | timestamp | 过期时间 |
ipAddress | text | 登录 IP 地址 |
userAgent | text | 浏览器 User Agent |
createdAt | timestamp | 创建时间 |
updatedAt | timestamp | 更新时间 |
account (账户表)
存储 OAuth 提供商的账户信息。
字段 | 类型 | 说明 |
---|---|---|
id | text | 账户 ID (主键) |
userId | text | 关联用户 ID |
providerId | text | 提供商 ID (google / email ) |
accountId | text | 提供商账户 ID |
accessToken | text | 访问 Token |
refreshToken | text | 刷新 Token |
password | text | 密码哈希(邮箱登录) |
createdAt | timestamp | 创建时间 |
支付与订阅表
payment (支付记录表)
存储所有支付记录(一次性购买和订阅续费)。
字段 | 类型 | 说明 |
---|---|---|
id | text | 支付记录 ID (主键) |
provider | varchar(32) | 支付提供商 (creem ) |
providerPaymentId | text | 提供商支付 ID (唯一,用于幂等性) |
userId | text | 用户 ID |
amountCents | integer | 金额(美分) |
currency | varchar(8) | 货币代码 (usd ) |
status | varchar(32) | 支付状态 |
type | varchar(32) | 支付类型 (one_time / subscription ) |
planKey | varchar(64) | 计划 ID |
creditsGranted | integer | 已发放的积分数 |
raw | text | 原始 webhook 数据(JSON) |
createdAt | timestamp | 创建时间 |
subscription (订阅表)
存储活跃的订阅记录。
字段 | 类型 | 说明 |
---|---|---|
id | text | 订阅 ID (主键) |
provider | varchar(32) | 支付提供商 (creem ) |
providerSubId | text | 提供商订阅 ID (唯一) |
userId | text | 用户 ID |
planKey | varchar(64) | 计划 ID (starter_monthly 等) |
status | varchar(32) | 订阅状态 (active / canceled / expired ) |
currentPeriodEnd | timestamp | 当前计费周期结束时间 |
raw | text | 原始数据(JSON) |
createdAt | timestamp | 创建时间 |
updatedAt | timestamp | 更新时间 |
creditLedger (积分账本表)
记录所有积分变动,支持审计。
字段 | 类型 | 说明 |
---|---|---|
id | text | 记录 ID (主键) |
userId | text | 用户 ID |
delta | integer | 积分变化量(正数=增加,负数=扣除) |
reason | varchar(64) | 变动原因 (chat_usage , subscription_cycle , one_time_pack , registration_bonus , admin_adjustment 等) |
paymentId | text | 关联的支付记录 ID (可选) |
createdAt | timestamp | 创建时间 |
示例记录:
delta: 300, reason: 'registration_bonus' → 注册赠送
delta: -10, reason: 'chat_usage' → 对话消耗
delta: 1000, reason: 'subscription_cycle', paymentId: 'xxx' → 订阅发放
subscriptionCreditSchedule (订阅积分调度表)
管理年付订阅的分期积分发放。
字段 | 类型 | 说明 |
---|---|---|
id | text | 调度 ID (主键) |
subscriptionId | text | 订阅 ID (唯一) |
userId | text | 用户 ID |
planKey | varchar(64) | 计划 ID |
creditsPerGrant | integer | 每次发放积分数 |
intervalMonths | integer | 发放间隔(月) |
grantsRemaining | integer | 剩余发放次数 |
totalCreditsRemaining | integer | 剩余总积分 |
nextGrantAt | timestamp | 下次发放时间(已索引) |
createdAt | timestamp | 创建时间 |
updatedAt | timestamp | 更新时间 |
示例: 年付计划 12000 积分,分 12 个月发放
creditsPerGrant: 1000
intervalMonths: 1
grantsRemaining: 11 (已发放 1 次)
totalCreditsRemaining: 11000
nextGrantAt: 2025-11-15
AI 功能表
chatSession (对话会话表)
字段 | 类型 | 说明 |
---|---|---|
id | text | 会话 ID (主键) |
userId | text | 用户 ID |
title | text | 会话标题 |
model | varchar(48) | 使用的模型 |
totalMessages | integer | 消息总数 |
totalCreditsUsed | integer | 累计消耗积分 |
lastMessageAt | timestamp | 最后一条消息时间 |
createdAt | timestamp | 创建时间 |
updatedAt | timestamp | 更新时间 |
chatMessage (对话消息表)
字段 | 类型 | 说明 |
---|---|---|
id | text | 消息 ID (主键) |
sessionId | text | 会话 ID |
role | varchar(16) | 角色 (user / assistant / system ) |
content | text | 消息内容 |
creditsUsed | integer | 本条消息消耗积分 |
createdAt | timestamp | 创建时间 |
generationHistory (生成历史表)
存储图像和视频生成记录。
字段 | 类型 | 说明 |
---|---|---|
id | text | 记录 ID (主键) |
userId | text | 用户 ID |
type | varchar(16) | 生成类型 (image / video ) |
prompt | text | 生成提示词 |
imageUrl | text | 输入图片 URL (图生图/图生视频) |
resultUrl | text | 生成结果 URL |
taskId | text | 异步任务 ID (视频生成) |
status | varchar(16) | 状态 (pending , processing , completed , failed ) |
creditsUsed | integer | 消耗积分 |
metadata | text | 额外数据(JSON) |
error | text | 错误信息 |
createdAt | timestamp | 创建时间 |
updatedAt | timestamp | 更新时间 |
其他表
passwordResetToken (密码重置 Token 表)
字段 | 类型 | 说明 |
---|---|---|
id | text | 记录 ID (主键) |
userId | text | 用户 ID |
token | text | 重置 Token (唯一) |
expiresAt | timestamp | 过期时间 |
createdAt | timestamp | 创建时间 |
newsletterSubscription (Newsletter 订阅表)
字段 | 类型 | 说明 |
---|---|---|
id | text | 订阅 ID (主键) |
email | text | 邮箱地址(唯一) |
userId | text | 关联用户 ID (可选) |
status | varchar(16) | 状态 (active / unsubscribed ) |
unsubscribeToken | text | 取消订阅 Token (唯一) |
subscribedAt | timestamp | 订阅时间 |
unsubscribedAt | timestamp | 取消订阅时间 |
updatedAt | timestamp | 更新时间 |
数据库连接配置
连接文件
lib/db/index.ts
:
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString, { prepare: false });
export const db = drizzle(client, { schema });
Schema 定义
完整的 Schema 定义在 lib/db/schema.ts
。
常见问题
如何查看数据库中的数据?
方法 1: Drizzle Studio
pnpm db:studio
方法 2: Neon 的 GUI
- Neon: 项目页面 > Tables
方法 3: psql 命令行
psql "postgresql://user:password@host/db?sslmode=require"
如何重置数据库?
警告: 这会删除所有数据!
# 方法 1: 删除并重新推送
pnpm db:push --force
# 方法 2: 手动删除所有表,然后重新迁移
pnpm db:migrate
如何备份数据库?
使用 pg_dump:
pg_dump "postgresql://user:password@host/db?sslmode=require" > backup.sql
恢复:
psql "postgresql://user:password@host/db?sslmode=require" < backup.sql
迁移失败怎么办?
- 查看错误信息: 通常会提示冲突的列或表
- 手动修复: 使用 Drizzle Studio 或 psql 手动调整
- 重新生成迁移: 删除冲突的迁移文件,重新
db:generate
如何添加新表?
- 在
lib/db/schema.ts
中定义新表:
export const myNewTable = pgTable("my_new_table", {
id: text("id").primaryKey(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
- 生成迁移:
pnpm db:generate
- 执行迁移:
pnpm db:migrate
如何优化数据库性能?
- 添加索引: 在频繁查询的列上添加索引
export const myTable = pgTable("my_table", {
// ...
}, (table) => ({
emailIdx: index("my_table_email_idx").on(table.email),
}));
- 使用连接池: 生产环境配置连接池 (Neon 已内置)
- 分页查询: 避免一次性加载大量数据
- 定期清理: 删除过期的 session 和 token
开发环境和生产环境如何分离?
-
使用不同的数据库:
.env.local
→ 开发数据库- 生产环境变量 → 生产数据库
-
使用数据库分支(Neon):
- 主分支: 生产数据
- 开发分支: 开发数据