数据库设计模板

好的数据库设计模板能帮你快速搭建产品骨架。当你面对一个 AI SaaS 产品从零开始的阶段,最让人头疼的往往不是前端页面怎么写,而是数据库到底该建哪些表、字段怎么设计、表之间怎么关联。本文提供一套经过实战验证的数据库设计模板,覆盖用户、产品、订单、AI 调用、额度、权限等核心场景,附带完整的 Prisma Schema 代码,可以直接拿来用。

数据库设计模板总览

一个典型的 AI SaaS 产品,数据库通常包含以下几类核心表:

分类表名用途优先级
用户域User用户账号与基础信息P0 必须
用户域AccountOAuth 第三方登录绑定P1 推荐
用户域Session用户登录会话管理P1 推荐
产品域Product产品 / 套餐定义P0 必须
产品域Feature产品功能点定义P2 按需
交易域Order订单记录P0 必须
交易域Payment支付流水P1 推荐
AI 域AiCallAI 模型调用记录P0 必须
AI 域AiUsageAI 用量聚合统计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() 而不是自增整数,避免暴露用户数量,也对分布式场景友好
  • localetimezone 是出海产品必备字段,直接影响邮件发送时间、界面语言等
  • 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")
}

userAgentipAddress 用于安全审计,比如用户在后台看到「当前登录设备」列表。出海产品需要注意 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,方便支付时查询
  • priceDecimal 而不是 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")
}

设计要点:

  • promptTokenscompletionTokenstotalTokens 三个字段分开存,因为不同模型的计费规则不同,有的按输入 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 中间表角色和权限的多对多关联

索引设计建议

索引是数据库性能的关键。建少了查询慢,建多了写入慢且浪费存储空间。下面是核心表的索引建议:

表名索引字段索引类型原因
Useremail唯一索引登录查询,必须唯一
User(email)普通索引如果邮箱查询频繁,可以额外加普通索引加速
Account(provider, providerAccountId)联合唯一索引OAuth 回调时查询绑定
AccountuserId普通索引查询用户绑定的所有账号
Order(userId, status)联合索引用户查询自己的订单列表,常按状态过滤
OrdercreatedAt普通索引后台按时间范围查询订单
AiCall(userId, createdAt)联合索引用户查询自己的调用记录,按时间排序
AiCall(model, createdAt)联合索引后台统计各模型的调用量趋势
AiCallcreatedAt普通索引按时间范围清理历史数据
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 字段存储用户语言偏好(如 enzh-CNja
  • 多语言内容:产品名称、描述等如果需要多语言,有两种做法——一是在 Product 表加 name_enname_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 数据库表关联关系图

流程图画布 · 115%
Mermaid 流程图加载中...

数据库设计检查清单

在动手建表之前,逐项对照检查:

  1. 用户表是否覆盖了认证方式:邮箱注册、OAuth 登录是否需要同时支持?Account 表的结构是否与你的 Auth 方案(NextAuth / Clerk / 自建)对齐?
  2. 金额字段是否使用了 Decimal:所有涉及货币的字段是否都是 Decimal 类型而不是 Float?小数位是否足够(通常 2 位,AI 成本建议 6 位)?
  3. AI 调用表是否记录了足够的计量信息promptTokenscompletionTokensmodel 是否都有?能否支撑按模型、按用户的成本核算?
  4. 额度表是否有变动流水:余额不能只存一个数字,每一笔变动都应在 CreditTransaction 中留痕,方便对账和排查?
  5. 索引是否覆盖了高频查询场景:用户查自己的订单、查自己的调用记录、后台按时间范围统计,这些查询的字段是否都建了索引?
  6. 软删除还是硬删除:用户注销、订单取消是直接从数据库删除,还是标记状态?出海产品建议用软删除(status = DELETED),保留数据用于合规审计。
  7. 时区是否统一:所有 DateTime 字段是否统一用 UTC 存储?展示层是否根据用户 timezone 做了转换?
  8. 多语言是否预留了空间:用户表是否有 locale 字段?产品表的多语言内容是用字段后缀还是翻译表?
  9. 外键的级联删除是否考虑清楚:用户删除后,关联的订单、AI 调用记录是否也要删除?通常业务数据(订单、调用记录)不应该级联删除,只标记关联用户已注销。
  10. 是否有审计字段createdAtupdatedAt 是否每张表都有?关键的变更操作(如额度调整、权限修改)是否有操作日志表?
  11. JSON 字段是否滥用featuresmetadata 这类 JSON 字段适合存储结构不固定的数据,但如果某个字段需要被频繁查询或作为条件过滤,应该拆成独立列。
  12. 表名和字段名的命名规范是否统一:项目内是否统一使用 camelCase(Prisma 默认)或 snake_case(数据库层面通过 @@map 映射)?

参考资料

  1. Prisma 官方文档 - Models - Prisma 数据模型定义的官方指南,涵盖标量类型、枚举、属性、函数等基础概念。
  2. ER Flow - Database Design Patterns for SaaS Applications - SaaS 应用的数据库设计模式总结,包含租户隔离、多态关联等常见模式。
  3. Microsoft Azure - 多租户 SaaS 软件数据库租用模式 - 微软官方文档,详细介绍多租户数据库的三种隔离方案及其适用场景。
  4. 腾讯云 - SaaS 产品的数据库架构设计 - 租户数据隔离策略的实践总结,覆盖物理隔离、表前缀、字段隔离等方案。
  5. GitHub - Ready-To-Deploy-SaaS-Template - 一个生产就绪的 SaaS 模板,包含完整的 Prisma Schema,可作为起步参考。
  6. MakerKit - Prisma 7 SaaS Starter Kit - Prisma 7 的 SaaS 入门套件,包含 Schema 设计、关系模式和查询优化的实战指导。
  7. Devart - Database Design Best Practices - 数据库设计最佳实践,涵盖规范化、索引策略、主外键约束等核心原则。
  8. 火山引擎 - AI SaaS 产品搭建 SOP - AI SaaS 产品从 0 到 1 的搭建经验分享,涉及数据库、支付、认证等模块的配置。