Sistine Starter

数据库

PostgreSQL + Drizzle ORM 配置与管理

数据库

Sistine Starter 使用 PostgreSQL 作为主数据库,配合 Drizzle ORM 提供类型安全的数据库操作。本指南将帮助你配置数据库连接、理解表结构和执行数据库迁移。

为什么选择 PostgreSQL?

PostgreSQL 是一个强大的开源关系型数据库,具有以下优势:

  • 可靠性: 经过多年生产环境验证
  • 功能丰富: 支持 JSON、全文搜索、地理数据等
  • ACID 事务: 确保数据一致性
  • 可扩展性: 适合从小型应用到大型系统
  • 云原生: 各大云平台都提供托管服务

选择数据库提供商

Neon (推荐)

Neon 是无服务器 PostgreSQL,按需付费,启动速度快。

优势:

  • 免费额度慷慨(500MB 存储 + 100 小时计算时间)
  • 自动扩缩容
  • 分支功能(可为每个 PR 创建数据库分支)
  • 启动时间 < 1 秒

设置步骤:

  1. 访问 neon.tech 并注册
  2. 创建新项目
  3. 复制连接字符串(包含 ?sslmode=require)
  4. 粘贴到 .env.localDATABASE_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 (用户表)

存储用户的基本信息和积分余额。

字段类型说明
idtext用户唯一 ID (主键)
nametext用户名称
emailtext邮箱地址(唯一)
emailVerifiedboolean邮箱是否已验证
imagetext头像 URL
creditsinteger当前可用积分
roletext用户角色 (admin / user)
planKeytext当前订阅计划 ID
bannedboolean是否被封禁
banReasontext封禁原因
banExpirestimestamp封禁到期时间
createdAttimestamp注册时间
updatedAttimestamp更新时间

session (会话表)

存储用户登录会话。

字段类型说明
idtext会话 ID (主键)
tokentext会话 Token (唯一)
userIdtext关联用户 ID
expiresAttimestamp过期时间
ipAddresstext登录 IP 地址
userAgenttext浏览器 User Agent
createdAttimestamp创建时间
updatedAttimestamp更新时间

account (账户表)

存储 OAuth 提供商的账户信息。

字段类型说明
idtext账户 ID (主键)
userIdtext关联用户 ID
providerIdtext提供商 ID (google / email)
accountIdtext提供商账户 ID
accessTokentext访问 Token
refreshTokentext刷新 Token
passwordtext密码哈希(邮箱登录)
createdAttimestamp创建时间

支付与订阅表

payment (支付记录表)

存储所有支付记录(一次性购买和订阅续费)。

字段类型说明
idtext支付记录 ID (主键)
providervarchar(32)支付提供商 (creem)
providerPaymentIdtext提供商支付 ID (唯一,用于幂等性)
userIdtext用户 ID
amountCentsinteger金额(美分)
currencyvarchar(8)货币代码 (usd)
statusvarchar(32)支付状态
typevarchar(32)支付类型 (one_time / subscription)
planKeyvarchar(64)计划 ID
creditsGrantedinteger已发放的积分数
rawtext原始 webhook 数据(JSON)
createdAttimestamp创建时间

subscription (订阅表)

存储活跃的订阅记录。

字段类型说明
idtext订阅 ID (主键)
providervarchar(32)支付提供商 (creem)
providerSubIdtext提供商订阅 ID (唯一)
userIdtext用户 ID
planKeyvarchar(64)计划 ID (starter_monthly 等)
statusvarchar(32)订阅状态 (active / canceled / expired)
currentPeriodEndtimestamp当前计费周期结束时间
rawtext原始数据(JSON)
createdAttimestamp创建时间
updatedAttimestamp更新时间

creditLedger (积分账本表)

记录所有积分变动,支持审计。

字段类型说明
idtext记录 ID (主键)
userIdtext用户 ID
deltainteger积分变化量(正数=增加,负数=扣除)
reasonvarchar(64)变动原因 (chat_usage, subscription_cycle, one_time_pack, registration_bonus, admin_adjustment 等)
paymentIdtext关联的支付记录 ID (可选)
createdAttimestamp创建时间

示例记录:

delta: 300, reason: 'registration_bonus' → 注册赠送
delta: -10, reason: 'chat_usage' → 对话消耗
delta: 1000, reason: 'subscription_cycle', paymentId: 'xxx' → 订阅发放

subscriptionCreditSchedule (订阅积分调度表)

管理年付订阅的分期积分发放。

字段类型说明
idtext调度 ID (主键)
subscriptionIdtext订阅 ID (唯一)
userIdtext用户 ID
planKeyvarchar(64)计划 ID
creditsPerGrantinteger每次发放积分数
intervalMonthsinteger发放间隔(月)
grantsRemaininginteger剩余发放次数
totalCreditsRemaininginteger剩余总积分
nextGrantAttimestamp下次发放时间(已索引)
createdAttimestamp创建时间
updatedAttimestamp更新时间

示例: 年付计划 12000 积分,分 12 个月发放

creditsPerGrant: 1000
intervalMonths: 1
grantsRemaining: 11 (已发放 1 次)
totalCreditsRemaining: 11000
nextGrantAt: 2025-11-15

AI 功能表

chatSession (对话会话表)

字段类型说明
idtext会话 ID (主键)
userIdtext用户 ID
titletext会话标题
modelvarchar(48)使用的模型
totalMessagesinteger消息总数
totalCreditsUsedinteger累计消耗积分
lastMessageAttimestamp最后一条消息时间
createdAttimestamp创建时间
updatedAttimestamp更新时间

chatMessage (对话消息表)

字段类型说明
idtext消息 ID (主键)
sessionIdtext会话 ID
rolevarchar(16)角色 (user / assistant / system)
contenttext消息内容
creditsUsedinteger本条消息消耗积分
createdAttimestamp创建时间

generationHistory (生成历史表)

存储图像和视频生成记录。

字段类型说明
idtext记录 ID (主键)
userIdtext用户 ID
typevarchar(16)生成类型 (image / video)
prompttext生成提示词
imageUrltext输入图片 URL (图生图/图生视频)
resultUrltext生成结果 URL
taskIdtext异步任务 ID (视频生成)
statusvarchar(16)状态 (pending, processing, completed, failed)
creditsUsedinteger消耗积分
metadatatext额外数据(JSON)
errortext错误信息
createdAttimestamp创建时间
updatedAttimestamp更新时间

其他表

passwordResetToken (密码重置 Token 表)

字段类型说明
idtext记录 ID (主键)
userIdtext用户 ID
tokentext重置 Token (唯一)
expiresAttimestamp过期时间
createdAttimestamp创建时间

newsletterSubscription (Newsletter 订阅表)

字段类型说明
idtext订阅 ID (主键)
emailtext邮箱地址(唯一)
userIdtext关联用户 ID (可选)
statusvarchar(16)状态 (active / unsubscribed)
unsubscribeTokentext取消订阅 Token (唯一)
subscribedAttimestamp订阅时间
unsubscribedAttimestamp取消订阅时间
updatedAttimestamp更新时间

数据库连接配置

连接文件

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

迁移失败怎么办?

  1. 查看错误信息: 通常会提示冲突的列或表
  2. 手动修复: 使用 Drizzle Studio 或 psql 手动调整
  3. 重新生成迁移: 删除冲突的迁移文件,重新 db:generate

如何添加新表?

  1. 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(),
});
  1. 生成迁移:
pnpm db:generate
  1. 执行迁移:
pnpm db:migrate

如何优化数据库性能?

  1. 添加索引: 在频繁查询的列上添加索引
export const myTable = pgTable("my_table", {
  // ...
}, (table) => ({
  emailIdx: index("my_table_email_idx").on(table.email),
}));
  1. 使用连接池: 生产环境配置连接池 (Neon 已内置)
  2. 分页查询: 避免一次性加载大量数据
  3. 定期清理: 删除过期的 session 和 token

开发环境和生产环境如何分离?

  1. 使用不同的数据库:

    • .env.local → 开发数据库
    • 生产环境变量 → 生产数据库
  2. 使用数据库分支(Neon):

    • 主分支: 生产数据
    • 开发分支: 开发数据

下一步