24.02-租户模型
要点
- 租户是 AI SaaS 的核心概念,每个租户是一个独立的组织/公司
- 租户模型需要支持:注册、配置、隔离、计费
- 数据隔离从共享 Schema +
tenant_id起步,按需升级 - 租户配置使用 KV 存储,支持热更新
内容
1. 租户的定义
在 AI SaaS 中,租户(Tenant)通常对应一个组织或公司。一个租户包含:
- 多个用户:租户内的成员
- 独立的配额:token、存储、请求次数
- 独立的配置:模型偏好、Prompt 模板、知识库
- 独立的计费:订阅计划、账单、发票
租户和用户的区别:
| 概念 | 含义 | 示例 |
|---|---|---|
| 租户 | 组织/公司 | Acme Corp |
| 用户 | 租户内的个人 | [email protected], [email protected] |
2. 租户数据模型
2.1 数据库 Schema
-- 租户表
CREATE TABLE tenants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL, -- 租户名称
slug TEXT NOT NULL UNIQUE, -- URL 友好标识(如 acme-corp)
plan TEXT NOT NULL DEFAULT 'free', -- 订阅计划
status TEXT NOT NULL DEFAULT 'active', -- active/suspended/cancelled
settings TEXT, -- JSON 配置
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
-- 租户配置表(可选,也可以放在 settings 字段)
CREATE TABLE tenant_configs (
tenant_id TEXT PRIMARY KEY,
preferred_model TEXT DEFAULT 'gpt-3.5-turbo',
max_tokens_per_request INTEGER DEFAULT 4000,
enable_rag INTEGER DEFAULT 1,
custom_domain TEXT,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
)2.2 TypeScript 类型
// src/types/tenant.ts
export interface Tenant {
id: string
name: string
slug: string
plan: 'free' | 'pro' | 'enterprise'
status: 'active' | 'suspended' | 'cancelled'
settings: TenantSettings
createdAt: number
updatedAt: number
}
export interface TenantSettings {
preferredModel: string
maxTokensPerRequest: number
enableRag: boolean
customDomain?: string
}
export interface TenantConfig {
tenantId: string
preferredModel: string
maxTokensPerRequest: number
enableRag: boolean
customDomain?: string
}3. 租户注册
3.1 注册流程
租户注册通常包含以下步骤:
- 用户填写组织信息(名称、邮箱)
- 系统创建租户记录
- 创建第一个用户(管理员)
- 初始化租户配置
- 发送欢迎邮件
3.2 注册接口
// src/routes/tenant.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { generateId } from '../lib/utils'
const app = new Hono()
const RegisterSchema = z.object({
tenantName: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8),
})
app.post('/api/tenant/register', zValidator('json', RegisterSchema), async (c) => {
const { tenantName, email, password } = c.req.valid('json')
// 1. 生成 slug
const slug = generateSlug(tenantName)
// 2. 检查 slug 是否已存在
const existing = await c.env.DB.prepare(
'SELECT id FROM tenants WHERE slug = ?'
).bind(slug).first()
if (existing) {
return c.json({ error: '租户名称已存在' }, 409)
}
// 3. 创建租户
const tenantId = generateId()
const now = Date.now()
await c.env.DB.prepare(`
INSERT INTO tenants (id, name, slug, plan, status, settings, created_at, updated_at)
VALUES (?, ?, ?, 'free', 'active', ?, ?, ?)
`).bind(
tenantId,
tenantName,
slug,
JSON.stringify({
preferredModel: 'gpt-3.5-turbo',
maxTokensPerRequest: 4000,
enableRag: true,
}),
now,
now
).run()
// 4. 创建管理员用户
const userId = generateId()
const passwordHash = await hashPassword(password)
await c.env.DB.prepare(`
INSERT INTO users (id, tenant_id, email, password_hash, role, created_at)
VALUES (?, ?, ?, ?, 'admin', ?)
`).bind(userId, tenantId, email, passwordHash, now).run()
// 5. 初始化租户配置到 KV(用于快速读取)
await c.env.TENANT_CONFIG.put(
`config:${tenantId}`,
JSON.stringify({
tenantId,
preferredModel: 'gpt-3.5-turbo',
maxTokensPerRequest: 4000,
enableRag: true,
})
)
// 6. 发送欢迎邮件(异步)
c.executionCtx.waitUntil(sendWelcomeEmail(email, tenantName))
return c.json({
tenantId,
userId,
slug,
}, 201)
})
function generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}4. 租户配置管理
4.1 配置存储策略
租户配置有两种存储方式:
方式 1:数据库存储
-- 配置存储在 tenants 表的 settings 字段
SELECT settings FROM tenants WHERE id = ?优点:事务支持,查询灵活 缺点:每次都需要查询数据库
方式 2:KV 缓存
// 配置存储在 KV 中
const config = await c.env.TENANT_CONFIG.get(`config:${tenantId}`, 'json')优点:毫秒级读取,适合频繁访问 缺点:需要处理缓存一致性
推荐方案:数据库存储 + KV 缓存
// 读取配置
async function getTenantConfig(tenantId: string, env: Env): Promise<TenantConfig> {
// 1. 先读 KV 缓存
const cached = await env.TENANT_CONFIG.get(`config:${tenantId}`, 'json')
if (cached) return cached
// 2. 缓存未命中,读数据库
const tenant = await env.DB.prepare(
'SELECT settings FROM tenants WHERE id = ?'
).bind(tenantId).first()
if (!tenant) {
throw new Error('Tenant not found')
}
const config = JSON.parse(tenant.settings)
// 3. 写入 KV 缓存(5 分钟过期)
await env.TENANT_CONFIG.put(
`config:${tenantId}`,
JSON.stringify(config),
{ expirationTtl: 300 }
)
return config
}
// 更新配置
async function updateTenantConfig(
tenantId: string,
updates: Partial<TenantConfig>,
env: Env
): Promise<void> {
// 1. 更新数据库
const tenant = await env.DB.prepare(
'SELECT settings FROM tenants WHERE id = ?'
).bind(tenantId).first()
const current = JSON.parse(tenant.settings)
const updated = { ...current, ...updates }
await env.DB.prepare(
'UPDATE tenants SET settings = ?, updated_at = ? WHERE id = ?'
).bind(JSON.stringify(updated), Date.now(), tenantId).run()
// 2. 更新 KV 缓存
await env.TENANT_CONFIG.put(
`config:${tenantId}`,
JSON.stringify(updated),
{ expirationTtl: 300 }
)
}4.2 配置更新接口
// src/routes/tenant.ts
app.put('/api/tenant/config', async (c) => {
const tenantId = c.get('tenantId')
const body = await c.req.json()
// 验证配置
const configSchema = z.object({
preferredModel: z.string().optional(),
maxTokensPerRequest: z.number().min(100).max(100000).optional(),
enableRag: z.boolean().optional(),
})
const updates = configSchema.parse(body)
// 更新配置
await updateTenantConfig(tenantId, updates, c.env)
return c.json({ success: true })
})5. 租户隔离
5.1 中间件实现租户隔离
每个请求都需要提取 tenantId,并确保数据隔离:
// src/middleware/tenant.ts
import { Context, Next } from 'hono'
import { verify } from 'hono/jwt'
export async function tenantMiddleware(c: Context, next: Next) {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401)
}
const token = authHeader.slice(7)
try {
const payload = await verify(token, c.env.JWT_SECRET)
const userId = payload.userId
const tenantId = payload.tenantId
// 验证租户状态
const tenant = await c.env.DB.prepare(
'SELECT status FROM tenants WHERE id = ?'
).bind(tenantId).first()
if (!tenant) {
return c.json({ error: 'Tenant not found' }, 404)
}
if (tenant.status !== 'active') {
return c.json({ error: 'Tenant is suspended' }, 403)
}
// 存储到上下文
c.set('userId', userId)
c.set('tenantId', tenantId)
await next()
} catch (error) {
return c.json({ error: 'Invalid token' }, 401)
}
}5.2 数据查询必须带上 tenant_id
// ❌ 错误:没有 tenant_id 过滤
const users = await c.env.DB.prepare(
'SELECT * FROM users WHERE email = ?'
).bind(email).all()
// ✅ 正确:带上 tenant_id
const tenantId = c.get('tenantId')
const users = await c.env.DB.prepare(
'SELECT * FROM users WHERE tenant_id = ? AND email = ?'
).bind(tenantId, email).all()5.3 封装租户感知的数据库操作
// src/lib/db.ts
export class TenantDB {
constructor(private db: D1Database, private tenantId: string) {}
async queryUsers(email?: string) {
if (email) {
return this.db.prepare(
'SELECT * FROM users WHERE tenant_id = ? AND email = ?'
).bind(this.tenantId, email).all()
}
return this.db.prepare(
'SELECT * FROM users WHERE tenant_id = ?'
).bind(this.tenantId).all()
}
async createUser(userData: CreateUserInput) {
const userId = generateId()
const now = Date.now()
await this.db.prepare(`
INSERT INTO users (id, tenant_id, email, password_hash, role, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`).bind(
userId,
this.tenantId,
userData.email,
userData.passwordHash,
userData.role,
now
).run()
return { id: userId, ...userData, createdAt: now }
}
}
// 使用
const tenantId = c.get('tenantId')
const db = new TenantDB(c.env.DB, tenantId)
const users = await db.queryUsers()6. 租户状态管理
租户有多种状态:
| 状态 | 含义 | 允许的操作 |
|---|---|---|
| active | 正常 | 所有操作 |
| suspended | 暂停(欠费/违规) | 只读,不能创建新资源 |
| cancelled | 已取消 | 只能导出数据 |
// 检查租户状态
async function checkTenantStatus(tenantId: string, env: Env) {
const tenant = await env.DB.prepare(
'SELECT status FROM tenants WHERE id = ?'
).bind(tenantId).first()
if (!tenant) {
throw new Error('Tenant not found')
}
return tenant.status
}
// 暂停租户(欠费)
async function suspendTenant(tenantId: string, env: Env) {
await env.DB.prepare(
'UPDATE tenants SET status = ?, updated_at = ? WHERE id = ?'
).bind('suspended', Date.now(), tenantId).run()
// 清除 KV 缓存
await env.TENANT_CONFIG.delete(`config:${tenantId}`)
}
// 恢复租户
async function activateTenant(tenantId: string, env: Env) {
await env.DB.prepare(
'UPDATE tenants SET status = ?, updated_at = ? WHERE id = ?'
).bind('active', Date.now(), tenantId).run()
// 重新加载配置到 KV
const config = await getTenantConfig(tenantId, env)
await env.TENANT_CONFIG.put(
`config:${tenantId}`,
JSON.stringify(config),
{ expirationTtl: 300 }
)
}7. 实战:完整的租户管理
// src/routes/tenant.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { generateId } from '../lib/utils'
import { hashPassword } from '../lib/auth'
import { getTenantConfig, updateTenantConfig } from '../lib/tenant-config'
const app = new Hono()
// 注册
app.post('/register', zValidator('json', z.object({
tenantName: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8),
})), async (c) => {
const { tenantName, email, password } = c.req.valid('json')
const slug = generateSlug(tenantName)
// 检查 slug
const existing = await c.env.DB.prepare(
'SELECT id FROM tenants WHERE slug = ?'
).bind(slug).first()
if (existing) {
return c.json({ error: '租户名称已存在' }, 409)
}
// 创建租户
const tenantId = generateId()
const userId = generateId()
const now = Date.now()
const settings = {
preferredModel: 'gpt-3.5-turbo',
maxTokensPerRequest: 4000,
enableRag: true,
}
await c.env.DB.batch([
c.env.DB.prepare(`
INSERT INTO tenants (id, name, slug, plan, status, settings, created_at, updated_at)
VALUES (?, ?, ?, 'free', 'active', ?, ?, ?)
`).bind(tenantId, tenantName, slug, JSON.stringify(settings), now, now),
c.env.DB.prepare(`
INSERT INTO users (id, tenant_id, email, password_hash, role, created_at)
VALUES (?, ?, ?, ?, 'admin', ?)
`).bind(userId, tenantId, email, await hashPassword(password), now),
])
// 初始化 KV 缓存
await c.env.TENANT_CONFIG.put(
`config:${tenantId}`,
JSON.stringify({ tenantId, ...settings }),
{ expirationTtl: 300 }
)
return c.json({ tenantId, userId, slug }, 201)
})
// 获取当前租户信息
app.get('/info', async (c) => {
const tenantId = c.get('tenantId')
const tenant = await c.env.DB.prepare(`
SELECT id, name, slug, plan, status, settings, created_at
FROM tenants WHERE id = ?
`).bind(tenantId).first()
if (!tenant) {
return c.json({ error: 'Tenant not found' }, 404)
}
return c.json({
...tenant,
settings: JSON.parse(tenant.settings),
})
})
// 更新配置
app.put('/config', zValidator('json', z.object({
preferredModel: z.string().optional(),
maxTokensPerRequest: z.number().min(100).max(100000).optional(),
enableRag: z.boolean().optional(),
})), async (c) => {
const tenantId = c.get('tenantId')
const updates = c.req.valid('json')
await updateTenantConfig(tenantId, updates, c.env)
return c.json({ success: true })
})
// 获取配置
app.get('/config', async (c) => {
const tenantId = c.get('tenantId')
const config = await getTenantConfig(tenantId, c.env)
return c.json(config)
})
export default app
function generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}8. 小结
租户模型的关键点:
- 租户定义:租户是组织/公司,包含多个用户
- 数据模型:tenants 表 + tenant_configs 表(或 settings 字段)
- 注册流程:创建租户 → 创建管理员 → 初始化配置 → 发送邮件
- 配置管理:数据库存储 + KV 缓存,支持热更新
- 数据隔离:所有查询必须带上
tenant_id,中间件统一处理 - 状态管理:active/suspended/cancelled,不同状态允许不同操作
一句话带走:
租户是 AI SaaS 的核心概念。从共享 Schema +
tenant_id起步,用 KV 缓存配置,中间件统一处理租户隔离,按需升级到独立数据库。