数据库设计模板
好的数据库设计模板能帮你快速搭建产品骨架。当你面对一个 AI SaaS 产品从零开始的阶段,最让人头疼的往往不是前端页面怎么写,而是数据库到底该建哪些表、字段怎么设计、表之间怎么关联。本文提供一套经过实战验证的数据库设计模板,覆盖用户、产品、订单、AI 调用、额度、权限等核心场景,附带完整的 Prisma Schema 代码,可以直接拿来用。
数据库设计模板总览
一个典型的 AI SaaS 产品,数据库通常包含以下几类核心表:
| 分类 | 表名 | 用途 | 优先级 |
|---|---|---|---|
| 用户域 | User | 用户账号与基础信息 | P0 必须 |
| 用户域 | Account | OAuth 第三方登录绑定 | P1 推荐 |
| 用户域 | Session | 用户登录会话管理 | P1 推荐 |
| 产品域 | Product | 产品 / 套餐定义 | P0 必须 |
| 产品域 | Feature | 产品功能点定义 | P2 按需 |
| 交易域 | Order | 订单记录 | P0 必须 |
| 交易域 | Payment | 支付流水 | P1 推荐 |
| AI 域 | AiCall | AI 模型调用记录 | P0 必须 |
| AI 域 | AiUsage | AI 用量聚合统计 | P2 按需 |
| 额度域 | Credit | 用户额度 / 积分账户 | P0 必须 |
| 额度域 | CreditTransaction | 额度变动流水 | P1 推荐 |
| 权限域 | Role | 角色定义 | P1 推荐 |
| 权限域 | Permission | 权限定义 | P2 按需 |
| 权限域 | UserRole | 用户-角色关联 | P1 推荐 |
这张清单不是绝对的,你可以根据产品规模做增减。但核心思路是:用户域管人、产品域管卖什么、交易域管收钱、AI 域管消耗、额度域管余额、权限域管控。六个域各司其职,边界清晰。
各核心表的完整设计
下面用 Prisma Schema 语法给出每张表的完整定义。选择 Prisma 是因为它在 Next.js 生态中使用广泛,类型推导友好,迁移工具也成熟。如果你用的是 Drizzle、TypeORM 或 SQL 直写,字段设计思路是通用的,语法层面做对应翻译即可。
用户表 User
用户表是所有 SaaS 产品的起点。AI SaaS 产品通常需要支持邮箱注册和 OAuth 社交登录两种方式。
model User {
id String @id @default(cuid())
email String @unique
emailVerified DateTime?
name String?
image String?
passwordHash String? // 邮箱注册时的密码哈希
locale String @default("en") // 用户语言偏好
timezone String @default("UTC")
role Role @default(USER)
status UserStatus @default(ACTIVE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关联
accounts Account[]
sessions Session[]
orders Order[]
credits Credit[]
aiCalls AiCall[]
@@index([email])
@@map("users")
}
enum Role {
USER
ADMIN
}
enum UserStatus {
ACTIVE
SUSPENDED
DELETED
}设计要点:
id使用cuid()而不是自增整数,避免暴露用户数量,也对分布式场景友好locale和timezone是出海产品必备字段,直接影响邮件发送时间、界面语言等status用枚举而不是布尔值,方便后续扩展「停用」「待验证」等中间状态
OAuth 账号表 Account
model Account {
id String @id @default(cuid())
userId String
type String // "oauth2" | "oidc" | "email"
provider String // "google" | "github" 等
providerAccountId String
refreshToken String?
accessToken String?
expiresAt Int?
tokenType String?
scope String?
idToken String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
@@map("accounts")
}这张表遵循了 NextAuth / Auth.js 的标准结构。如果你的认证方案用了 NextAuth,直接套用即可。核心点是 provider + providerAccountId 的联合唯一索引,确保同一个 OAuth 提供商的同一个账号不会重复绑定。
会话表 Session
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
userAgent String?
ipAddress String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("sessions")
}userAgent 和 ipAddress 用于安全审计,比如用户在后台看到「当前登录设备」列表。出海产品需要注意 GDPR 合规,IP 地址属于个人数据,存储时要考虑数据保护要求。
产品表 Product
产品表定义你要卖的套餐或商品。AI SaaS 产品通常按「套餐 + 用量」的模式收费。
model Product {
id String @id @default(cuid())
name String
slug String @unique
description String?
price Decimal @db.Decimal(10, 2)
currency String @default("USD")
billingCycle String @default("monthly") // "monthly" | "yearly" | "one_time"
stripePriceId String? // Stripe 价格 ID
features Json // 套餐包含的功能配额
isActive Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
@@index([slug])
@@index([isActive, sortOrder])
@@map("products")
}设计要点:
slug用于 URL 路由,比如/pricing/pro-plan,用唯一索引保证不重复features用 JSON 字段存储套餐的功能配额,比如{ "ai_calls": 1000, "image_gen": 100, "max_projects": 5 },不同产品的功能差异很大,用 JSON 比拆表更灵活stripePriceId直接关联 Stripe 的价格 ID,方便支付时查询price用Decimal而不是Float,避免浮点精度问题。涉及到钱的字段,永远用 Decimal
订单表 Order
model Order {
id String @id @default(cuid())
orderNo String @unique // 业务订单号
userId String
productId String
status OrderStatus @default(PENDING)
amount Decimal @db.Decimal(10, 2)
currency String @default("USD")
paidAt DateTime?
cancelledAt DateTime?
metadata Json? // 扩展信息
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
product Product @relation(fields: [productId], references: [id])
payments Payment[]
@@index([userId, status])
@@index([orderNo])
@@index([createdAt])
@@map("orders")
}
enum OrderStatus {
PENDING
PAID
CANCELLED
REFUNDED
FAILED
}orderNo 是面向用户的业务订单号,通常用日期 + 随机数生成,比如 ORD20260701-A8F3K2。它与数据库主键 id 分离,好处是订单号可以按业务规则自定义格式,而 id 保持技术层面的简洁。
支付流水表 Payment
model Payment {
id String @id @default(cuid())
orderId String
provider String @default("stripe") // "stripe" | "paypal"
providerPaymentId String @unique // 第三方支付流水号
amount Decimal @db.Decimal(10, 2)
currency String @default("USD")
status PaymentStatus @default(PENDING)
rawResponse Json? // 原始回调数据,便于排查
createdAt DateTime @default(now())
order Order @relation(fields: [orderId], references: [id])
@@index([orderId])
@@index([providerPaymentId])
@@map("payments")
}
enum PaymentStatus {
PENDING
SUCCEEDED
FAILED
REFUNDED
}一个订单可能有多条支付流水(比如用户第一次支付失败后重试),所以 Order 和 Payment 是一对多关系。rawResponse 保存支付回调的原始数据,在出现支付纠纷时非常有用。
AI 调用表 AiCall
这是 AI SaaS 产品区别于普通 SaaS 的核心表。每一次 AI 模型调用都应该被记录,用于计费、限流和数据分析。
model AiCall {
id String @id @default(cuid())
userId String
model String // "gpt-4o" | "claude-sonnet-4-20250514" | "dall-e-3" 等
endpoint String // "chat/completions" | "images/generations" 等
promptTokens Int @default(0)
completionTokens Int @default(0)
totalTokens Int @default(0)
cost Decimal? @db.Decimal(10, 6) // 本次调用的成本(美元)
latency Int? // 响应耗时(毫秒)
statusCode Int? // HTTP 状态码
errorMessage String?
metadata Json? // 请求参数摘要,如 temperature、max_tokens 等
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId, createdAt])
@@index([model, createdAt])
@@index([createdAt])
@@map("ai_calls")
}设计要点:
promptTokens、completionTokens、totalTokens三个字段分开存,因为不同模型的计费规则不同,有的按输入 token 计费,有的按输出 token 计费cost用 6 位小数的 Decimal,因为单次调用的成本可能很小(比如 $0.000123)latency记录响应耗时,用于性能监控和慢查询告警metadata是 JSON 字段,不同 AI 模型的参数差异很大,用 JSON 比加列更灵活
额度表 Credit
额度表管理用户的「余额」,是计费系统的核心。
model Credit {
id String @id @default(cuid())
userId String
balance Int @default(0) // 当前可用额度
totalAdded Int @default(0) // 累计充值
totalUsed Int @default(0) // 累计消耗
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
transactions CreditTransaction[]
@@unique([userId]) // 一个用户只有一条额度记录
@@map("credits")
}
model CreditTransaction {
id String @id @default(cuid())
creditId String
type CreditTransType // "recharge" | "consume" | "refund" | "expire" | "adjust"
amount Int // 正数表示增加,负数表示减少
balanceAfter Int // 变动后的余额
reference String? // 关联的业务 ID,如订单号、AI 调用 ID
description String?
createdAt DateTime @default(now())
credit Credit @relation(fields: [creditId], references: [id])
@@index([creditId, createdAt])
@@map("credit_transactions")
}
enum CreditTransType {
RECHARGE
CONSUME
REFUND
EXPIRE
ADJUST
}设计要点:
Credit表只存当前余额和累计值,具体每一笔变动在CreditTransaction中记录balanceAfter记录每次变动后的余额快照,方便对账和排查余额异常type用枚举覆盖所有变动场景:充值、消费、退款、过期、人工调整- 一个用户只有一条额度记录(
@@unique([userId])),通过事务保证余额变动的原子性
角色与权限表
对于多数 AI SaaS 产品,初期只需区分「普通用户」和「管理员」两个角色,用 User 表的 role 枚举就够了。当产品规模扩大,需要更细粒度的权限控制时,再引入独立的角色权限表:
model Role {
id String @id @default(cuid())
name String @unique // "admin" | "editor" | "viewer"
description String?
createdAt DateTime @default(now())
permissions RolePermission[]
@@map("roles")
}
model Permission {
id String @id @default(cuid())
resource String // "project" | "ai_call" | "billing"
action String // "create" | "read" | "update" | "delete"
roles RolePermission[]
@@unique([resource, action])
@@map("permissions")
}
model RolePermission {
roleId String
permissionId String
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@id([roleId, permissionId])
@@map("role_permissions")
}采用 resource + action 的权限模型(类似 RBAC),比简单的「功能开关」更灵活。比如 project:delete 表示删除项目的权限,billing:read 表示查看账单的权限。
表的关联关系设计
上面所有表通过外键关联,形成一个完整的数据库结构。关联关系的设计遵循几个原则:
一对一关系:用户与额度账户。一个用户对应一条 Credit 记录,用 @@unique([userId]) 约束。
一对多关系:用户与订单、用户与 AI 调用、订单与支付流水、额度与额度流水。这是最常见的关联方式,外键放在「多」的一方。
多对多关系:角色与权限。一个角色有多个权限,一个权限也可以属于多个角色,通过中间表 RolePermission 关联。
用一张表梳理清楚:
| 关联关系 | 类型 | 外键位置 | 说明 |
|---|---|---|---|
| User → Account | 一对多 | Account.userId | 一个用户可以绑定多个 OAuth 账号 |
| User → Session | 一对多 | Session.userId | 一个用户可以有多个登录会话 |
| User → Order | 一对多 | Order.userId | 一个用户可以有多个订单 |
| User → AiCall | 一对多 | AiCall.userId | 一个用户有多次 AI 调用 |
| User → Credit | 一对一 | Credit.userId (unique) | 一个用户一条额度记录 |
| Product → Order | 一对多 | Order.productId | 一个产品可以有多个订单 |
| Order → Payment | 一对多 | Payment.orderId | 一个订单可以有多条支付流水 |
| Credit → CreditTransaction | 一对多 | CreditTransaction.creditId | 一条额度记录有多笔变动 |
| Role ↔ Permission | 多对多 | RolePermission 中间表 | 角色和权限的多对多关联 |
索引设计建议
索引是数据库性能的关键。建少了查询慢,建多了写入慢且浪费存储空间。下面是核心表的索引建议:
| 表名 | 索引字段 | 索引类型 | 原因 |
|---|---|---|---|
| User | email | 唯一索引 | 登录查询,必须唯一 |
| User | (email) | 普通索引 | 如果邮箱查询频繁,可以额外加普通索引加速 |
| Account | (provider, providerAccountId) | 联合唯一索引 | OAuth 回调时查询绑定 |
| Account | userId | 普通索引 | 查询用户绑定的所有账号 |
| Order | (userId, status) | 联合索引 | 用户查询自己的订单列表,常按状态过滤 |
| Order | createdAt | 普通索引 | 后台按时间范围查询订单 |
| AiCall | (userId, createdAt) | 联合索引 | 用户查询自己的调用记录,按时间排序 |
| AiCall | (model, createdAt) | 联合索引 | 后台统计各模型的调用量趋势 |
| AiCall | createdAt | 普通索引 | 按时间范围清理历史数据 |
| CreditTransaction | (creditId, createdAt) | 联合索引 | 查询某用户的额度变动流水 |
索引设计有几个通用原则:
- 查询驱动:先搞清楚哪些查询最频繁、最慢,再针对性建索引,不要凭直觉建
- 联合索引注意顺序:把等值查询的字段放前面,范围查询的字段放后面。比如
(userId, createdAt)中,userId是等值匹配,createdAt是范围查询 - 不要过度索引:每个索引都会拖慢写入速度。对于
AiCall这种高频写入的表,索引数量要控制 - 定期审查:用数据库的慢查询日志和索引使用情况统计,清理无用的索引
扩展性考虑
多租户设计
如果你的 AI SaaS 产品面向 B2B 场景(比如企业内部使用),需要支持多租户。多租户有三种常见的数据库方案:
| 方案 | 实现方式 | 隔离级别 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 每个租户一个数据库实例 | 最高 | 高 | 大客户、合规要求严格 |
| Schema 隔离 | 同一数据库,每个租户一个 Schema | 中等 | 中 | 中等规模 B2B |
| 共享表 | 所有租户共用表,通过 tenant_id 字段区分 | 最低 | 低 | B2C 或小规模 B2B |
对于大多数 AI 出海产品,初期建议用共享表方案,在每张业务表上加 organizationId 字段:
model Organization {
id String @id @default(cuid())
name String
slug String @unique
plan String @default("free")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("organizations")
}
// 在业务表上加 organizationId
model Project {
id String @id @default(cuid())
organizationId String
name String
// ... 其他字段
organization Organization @relation(fields: [organizationId], references: [id])
@@index([organizationId])
@@map("projects")
}国际化考虑
出海产品的数据库设计需要为国际化留好扩展空间:
- 用户偏好:User 表的
locale字段存储用户语言偏好(如en、zh-CN、ja) - 多语言内容:产品名称、描述等如果需要多语言,有两种做法——一是在 Product 表加
name_en、name_zh等字段(简单直接),二是建独立的翻译表(灵活但复杂) - 货币处理:
currency字段用 ISO 4217 标准(USD、EUR、JPY),金额一律用最小单位(分)或 Decimal 存储,不要直接用 Float - 时区处理:所有时间字段统一用 UTC 存储(
DateTime默认就是 UTC),在展示层根据用户的timezone做转换
案例:AI 写作工具的数据库设计
假设你正在做一个 AI 写作工具(类似 Jasper、Copy.ai),核心功能是用户输入提示词,AI 生成营销文案。数据库设计的关键差异点:
// 写作工具特有的「项目」和「文档」概念
model Project {
id String @id @default(cuid())
userId String
name String // 项目名称,如「我的博客文章」
type String @default("article") // "article" | "ad_copy" | "email" | "social"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
documents Document[]
@@index([userId, type])
@@map("projects")
}
model Document {
id String @id @default(cuid())
projectId String
title String
content String? // 生成的文案内容
status String @default("draft") // "draft" | "published" | "archived"
wordCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id])
generations Generation[]
@@index([projectId, status])
@@map("documents")
}
model Generation {
id String @id @default(cuid())
documentId String
userId String
prompt String // 用户输入的提示词
result String // AI 生成的结果
model String // 使用的模型
tokensUsed Int
createdAt DateTime @default(now())
document Document @relation(fields: [documentId], references: [id])
@@index([documentId, createdAt])
@@map("generations")
}写作工具的特点是需要「项目 → 文档 → 生成记录」的三层结构。用户在一个项目下创建多篇文档,每篇文档可能经历多次 AI 生成。Generation 表记录每次生成的 prompt 和结果,方便用户回溯和对比。
案例:AI 图片生成工具的数据库设计
假设你做一个 AI 图片生成工具(类似 Midjourney、DALL-E 产品),核心差异在于:
model ImageGeneration {
id String @id @default(cuid())
userId String
prompt String // 文字描述
negativePrompt String? // 反向提示词
model String @default("dall-e-3")
imageUrl String? // 生成的图片 URL
imageWidth Int?
imageHeight Int?
style String? // "vivid" | "natural"
quality String? // "standard" | "hd"
status String @default("processing") // "processing" | "completed" | "failed"
tokensUsed Int @default(0)
cost Decimal? @db.Decimal(10, 6)
createdAt DateTime @default(now())
@@index([userId, createdAt])
@@index([status])
@@map("image_generations")
}
model ImageCollection {
id String @id @default(cuid())
userId String
name String // 图集名称
createdAt DateTime @default(now())
images ImageCollectionItem[]
@@index([userId])
@@map("image_collections")
}
model ImageCollectionItem {
id String @id @default(cuid())
collectionId String
imageGenerationId String
collection ImageCollection @relation(fields: [collectionId], references: [id])
imageGeneration ImageGeneration @relation(fields: [imageGenerationId], references: [id])
@@unique([collectionId, imageGenerationId])
@@map("image_collection_items")
}图片生成工具与写作工具的核心差异:
| 对比维度 | AI 写作工具 | AI 图片生成工具 |
|---|---|---|
| 核心产出 | 文本文档 | 图片文件 |
| 内容存储 | content 字段存文本 | imageUrl 存 OSS 地址 |
| 组织方式 | 项目 → 文档 | 图集 → 图片 |
| 输入参数 | 提示词、语言、长度 | 提示词、反向提示词、尺寸、风格 |
| 生成耗时 | 通常 2-10 秒 | 可能 10-60 秒 |
| 存储成本 | 文本,存储成本低 | 图片,存储成本高,需要考虑 CDN 和过期清理 |
核心表字段设计对比
不同产品类型对核心表字段的需求有差异,以下做横向对比:
| 字段 | AI 写作工具 | AI 图片工具 | AI 视频工具 | 通用 SaaS |
|---|---|---|---|---|
prompt | 必须 | 必须 | 必须 | 按需 |
tokens | 必须 | 必须 | 必须 | 必须 |
latency | 推荐 | 推荐 | 推荐 | 推荐 |
imageUrl | 不需要 | 必须 | 不需要 | 不需要 |
videoUrl | 不需要 | 不需要 | 必须 | 不需要 |
content | 必须 | 不需要 | 不需要 | 按需 |
cost | 推荐 | 推荐 | 推荐 | 推荐 |
status | 推荐 | 必须 | 必须 | 推荐 |
Mermaid 数据库表关联关系图
数据库设计检查清单
在动手建表之前,逐项对照检查:
- 用户表是否覆盖了认证方式:邮箱注册、OAuth 登录是否需要同时支持?Account 表的结构是否与你的 Auth 方案(NextAuth / Clerk / 自建)对齐?
- 金额字段是否使用了 Decimal:所有涉及货币的字段是否都是
Decimal类型而不是Float?小数位是否足够(通常 2 位,AI 成本建议 6 位)? - AI 调用表是否记录了足够的计量信息:
promptTokens、completionTokens、model是否都有?能否支撑按模型、按用户的成本核算? - 额度表是否有变动流水:余额不能只存一个数字,每一笔变动都应在
CreditTransaction中留痕,方便对账和排查? - 索引是否覆盖了高频查询场景:用户查自己的订单、查自己的调用记录、后台按时间范围统计,这些查询的字段是否都建了索引?
- 软删除还是硬删除:用户注销、订单取消是直接从数据库删除,还是标记状态?出海产品建议用软删除(
status = DELETED),保留数据用于合规审计。 - 时区是否统一:所有
DateTime字段是否统一用 UTC 存储?展示层是否根据用户timezone做了转换? - 多语言是否预留了空间:用户表是否有
locale字段?产品表的多语言内容是用字段后缀还是翻译表? - 外键的级联删除是否考虑清楚:用户删除后,关联的订单、AI 调用记录是否也要删除?通常业务数据(订单、调用记录)不应该级联删除,只标记关联用户已注销。
- 是否有审计字段:
createdAt、updatedAt是否每张表都有?关键的变更操作(如额度调整、权限修改)是否有操作日志表? - JSON 字段是否滥用:
features、metadata这类 JSON 字段适合存储结构不固定的数据,但如果某个字段需要被频繁查询或作为条件过滤,应该拆成独立列。 - 表名和字段名的命名规范是否统一:项目内是否统一使用
camelCase(Prisma 默认)或snake_case(数据库层面通过@@map映射)?
参考资料
- Prisma 官方文档 - Models - Prisma 数据模型定义的官方指南,涵盖标量类型、枚举、属性、函数等基础概念。
- ER Flow - Database Design Patterns for SaaS Applications - SaaS 应用的数据库设计模式总结,包含租户隔离、多态关联等常见模式。
- Microsoft Azure - 多租户 SaaS 软件数据库租用模式 - 微软官方文档,详细介绍多租户数据库的三种隔离方案及其适用场景。
- 腾讯云 - SaaS 产品的数据库架构设计 - 租户数据隔离策略的实践总结,覆盖物理隔离、表前缀、字段隔离等方案。
- GitHub - Ready-To-Deploy-SaaS-Template - 一个生产就绪的 SaaS 模板,包含完整的 Prisma Schema,可作为起步参考。
- MakerKit - Prisma 7 SaaS Starter Kit - Prisma 7 的 SaaS 入门套件,包含 Schema 设计、关系模式和查询优化的实战指导。
- Devart - Database Design Best Practices - 数据库设计最佳实践,涵盖规范化、索引策略、主外键约束等核心原则。
- 火山引擎 - AI SaaS 产品搭建 SOP - AI SaaS 产品从 0 到 1 的搭建经验分享,涉及数据库、支付、认证等模块的配置。