架构设计与技术选型
从本章开始,我们将构建一个完整的 AI 视频平台——用户可以通过文本描述生成 AI 视频、上传和管理视频、在线播放、搜索浏览,管理员可以审核内容和查看数据看板。这不是一个玩具项目,而是一个覆盖前后端全链路的生产级系统。本章从需求分析到架构设计,完整呈现一个 Nuxt4 全栈项目的技术决策过程。
1. 需求分析
1.1 产品定位
AI 视频平台的核心价值:让普通用户通过文本描述生成短视频。围绕这个核心,衍生出完整的视频生命周期管理。
1.2 核心功能矩阵
| 模块 | 用户端功能 | 管理端功能 |
|---|---|---|
| AI 生成 | 文本→视频、风格选择、生成进度、历史记录 | 模型配置、配额管理 |
| 视频管理 | 上传、编辑信息、删除、封面设置 | 内容审核、批量操作 |
| 播放体验 | HLS 自适应码率、弹幕、进度记忆、画中画 | 播放质量监控 |
| 用户系统 | 注册登录、个人主页、收藏、关注 | 用户管理、封禁 |
| 搜索发现 | 全文搜索、标签筛选、推荐流 | 搜索热词、推荐策略 |
| 数据看板 | 播放量、点赞数 | 全站数据、趋势分析 |
1.3 非功能性需求
| 维度 | 目标 | 关键技术 |
|---|---|---|
| 性能 | LCP < 2.5s, FID < 100ms | SSR + CDN + 图片优化 |
| 可用性 | 99.9% uptime | 健康检查 + 自动恢复 |
| 安全 | OWASP Top 10 防护 | CSP + CSRF + 输入校验 |
| 可扩展 | 支持新 AI 模型接入 | 适配器模式 + 插件化 |
| 国际化 | 中英双语 | @nuxtjs/i18n |
2. 整体架构
2.1 架构分层
┌──────────────────────────────────────────────┐
│ CDN (静态资源) │
├──────────────────────────────────────────────┤
│ Nuxt4 SSR Server │
│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Pages │ │Components│ │ Composables │ │
│ └─────────┘ └──────────┘ └──────────────┘ │
├──────────────────────────────────────────────┤
│ Nitro Server (BFF 层) │
│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ API 路由 │ │ Middleware│ │ Service │ │
│ └─────────┘ └──────────┘ └──────────────┘ │
├──────────────────────────────────────────────┤
│ 外部服务 │
│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Database │ │ AI API │ │ Object Store │ │
│ │(Postgres)│ │(可灵/Sora)│ │ (S3/R2) │ │
│ └─────────┘ └──────────┘ └──────────────┘ │
└──────────────────────────────────────────────┘
为什么选 Nuxt4 全栈:
- BFF 内置:Nitro Server 作为 BFF(Backend For Frontend),不需要额外维护一个 API 服务
- SSR 原生:视频平台需要 SEO(搜索引擎收录视频页),SSR 是硬需求
- 全栈类型安全:前后端共享 TypeScript 类型,API 返回值类型自动推导
- 一套部署:前端 + BFF 作为一个服务部署,减少运维复杂度
2.2 Monorepo 目录结构
ai-video-platform/
├── apps/
│ ├── web/ # 用户端 Nuxt4 应用
│ │ ├── pages/
│ │ ├── components/
│ │ ├── composables/
│ │ ├── layouts/
│ │ ├── server/ # Nitro BFF 层
│ │ │ ├── api/
│ │ │ ├── middleware/
│ │ │ └── utils/
│ │ └── nuxt.config.ts
│ └── admin/ # 管理后台 Nuxt4 应用
│ ├── pages/
│ ├── components/
│ ├── server/
│ └── nuxt.config.ts
├── packages/
│ ├── shared/ # 共享代码
│ │ ├── types/ # TypeScript 类型定义
│ │ ├── utils/ # 工具函数
│ │ └── constants/ # 常量
│ ├── ui/ # 共享 UI 组件(Nuxt Layer)
│ │ ├── components/
│ │ ├── composables/
│ │ └── nuxt.config.ts
│ └── database/ # 数据库层
│ ├── schema/ # Drizzle ORM Schema
│ ├── migrations/
│ └── seed/
├── pnpm-workspace.yaml
├── nuxt.config.ts # 共享 Nuxt 配置(如果需要)
└── package.json
架构决策:
- apps 分离:用户端和管理后台是独立应用,独立部署。管理后台不需要 SEO,可以用 SPA 模式
- packages/shared:类型定义和工具函数共享,避免重复定义
- packages/ui:以 Nuxt Layer 形式共享 UI 组件——按钮、表单、卡片等基础组件两端复用
- packages/database:数据库 Schema 和迁移独立管理,两个应用共享同一套数据库定义
2.3 pnpm Workspace 配置
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'// package.json
{
"scripts": {
"dev:web": "pnpm --filter @ai-video/web dev",
"dev:admin": "pnpm --filter @ai-video/admin dev",
"build:web": "pnpm --filter @ai-video/web build",
"build:admin": "pnpm --filter @ai-video/admin build",
"db:migrate": "pnpm --filter @ai-video/database migrate",
"db:seed": "pnpm --filter @ai-video/database seed"
}
}3. 数据库设计
3.1 技术选型:Drizzle ORM + PostgreSQL
| 选项 | 优点 | 缺点 | 选择理由 |
|---|---|---|---|
| Drizzle ORM | 类型安全、零抽象、SQL-like | 生态较新 | 与 Nuxt4 集成最好,零运行时开销 |
| Prisma | 生态成熟、文档丰富 | 运行时引擎重、冷启动慢 | 在 Serverless 场景下性能差 |
| Knex | 轻量、灵活 | 无类型推导 | 缺少类型安全 |
Drizzle 的核心优势:Schema 即类型——定义表结构时自动生成 TypeScript 类型,查询结果的类型自动推导,不需要手动维护类型定义。
3.2 核心 Schema
// packages/database/schema/users.ts
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => createId()),
email: text('email').notNull().unique(),
name: text('name').notNull(),
avatar: text('avatar'),
role: text('role', { enum: ['user', 'admin', 'moderator'] }).default('user').notNull(),
banned: boolean('banned').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
// packages/database/schema/videos.ts
export const videos = pgTable('videos', {
id: text('id').primaryKey().$defaultFn(() => createId()),
title: text('title').notNull(),
description: text('description'),
userId: text('user_id').notNull().references(() => users.id),
status: text('status', {
enum: ['draft', 'processing', 'ready', 'failed', 'reviewing', 'published', 'rejected'],
}).default('draft').notNull(),
sourceType: text('source_type', { enum: ['upload', 'ai_generated'] }).notNull(),
sourceUrl: text('source_url'), // 原始文件 URL
hlsUrl: text('hls_url'), // HLS 播放地址
thumbnailUrl: text('thumbnail_url'), // 封面图
duration: integer('duration'), // 时长(秒)
width: integer('width'),
height: integer('height'),
fileSize: bigint('file_size', { mode: 'number' }),
viewCount: integer('view_count').default(0).notNull(),
likeCount: integer('like_count').default(0).notNull(),
tags: text('tags').array(),
createdAt: timestamp('created_at').defaultNow().notNull(),
publishedAt: timestamp('published_at'),
})
// packages/database/schema/ai-tasks.ts
export const aiTasks = pgTable('ai_tasks', {
id: text('id').primaryKey().$defaultFn(() => createId()),
userId: text('user_id').notNull().references(() => users.id),
videoId: text('video_id').references(() => videos.id),
prompt: text('prompt').notNull(),
model: text('model').notNull(), // 'kling', 'sora', 'runway'
style: text('style'), // 'realistic', 'anime', 'cinematic'
status: text('status', {
enum: ['pending', 'running', 'completed', 'failed', 'cancelled'],
}).default('pending').notNull(),
progress: integer('progress').default(0), // 0-100
result: jsonb('result'), // AI API 返回的原始结果
errorMessage: text('error_message'),
retryCount: integer('retry_count').default(0),
createdAt: timestamp('created_at').defaultNow().notNull(),
completedAt: timestamp('completed_at'),
})3.3 关系定义
// packages/database/schema/relations.ts
import { relations } from 'drizzle-orm'
export const usersRelations = relations(users, ({ many }) => ({
videos: many(videos),
aiTasks: many(aiTasks),
}))
export const videosRelations = relations(videos, ({ one, many }) => ({
user: one(users, { fields: [videos.userId], references: [users.id] }),
comments: many(comments),
}))4. API 层设计
4.1 Nitro Server 路由结构
server/
├── api/
│ ├── auth/
│ │ ├── login.post.ts
│ │ ├── register.post.ts
│ │ └── me.get.ts
│ ├── videos/
│ │ ├── index.get.ts # GET /api/videos(列表)
│ │ ├── index.post.ts # POST /api/videos(创建)
│ │ ├── [id].get.ts # GET /api/videos/:id
│ │ ├── [id].patch.ts # PATCH /api/videos/:id
│ │ ├── [id].delete.ts # DELETE /api/videos/:id
│ │ └── [id]/
│ │ ├── like.post.ts # POST /api/videos/:id/like
│ │ └── comments.get.ts # GET /api/videos/:id/comments
│ ├── ai/
│ │ ├── generate.post.ts # POST /api/ai/generate
│ │ ├── tasks/
│ │ │ ├── [id].get.ts # GET /api/ai/tasks/:id
│ │ │ └── [id].delete.ts # DELETE /api/ai/tasks/:id(取消)
│ │ └── models.get.ts # GET /api/ai/models(可用模型列表)
│ └── upload/
│ ├── presign.post.ts # POST /api/upload/presign
│ └── complete.post.ts # POST /api/upload/complete
├── middleware/
│ ├── auth.ts # 认证中间件
│ ├── rate-limit.ts # 限流
│ └── request-id.ts # 请求 ID
└── utils/
├── db.ts # 数据库连接
├── ai-client.ts # AI API 客户端
└── storage.ts # 对象存储客户端
4.2 API 处理函数模式
// server/api/videos/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const { page = 1, limit = 20, tag, sort = 'latest' } = query
const db = useDB()
const where = and(
eq(videos.status, 'published'),
tag ? arrayContains(videos.tags, [tag as string]) : undefined,
)
const orderBy = sort === 'popular'
? desc(videos.viewCount)
: desc(videos.publishedAt)
const [items, [{ count }]] = await Promise.all([
db.select()
.from(videos)
.where(where)
.orderBy(orderBy)
.limit(Number(limit))
.offset((Number(page) - 1) * Number(limit)),
db.select({ count: sql<number>`count(*)` })
.from(videos)
.where(where),
])
return {
items,
pagination: {
page: Number(page),
limit: Number(limit),
total: count,
},
}
})4.3 输入校验
使用 Zod 统一校验 API 输入:
// server/api/ai/generate.post.ts
import { z } from 'zod'
const generateSchema = z.object({
prompt: z.string().min(10).max(500),
model: z.enum(['kling', 'sora', 'runway']),
style: z.enum(['realistic', 'anime', 'cinematic']).optional(),
duration: z.number().min(3).max(60).default(10),
})
export default defineEventHandler(async (event) => {
const session = await requireAuth(event)
const body = await readValidatedBody(event, generateSchema.parse)
const task = await createAiTask({
userId: session.user.id,
prompt: body.prompt,
model: body.model,
style: body.style,
})
return { taskId: task.id }
})5. 混合渲染策略
5.1 页面级渲染决策
不同页面有不同的渲染需求:
| 页面 | 渲染模式 | 理由 |
|---|---|---|
| 首页 | SSR + SWR 60s | SEO 重要,内容可缓存 |
| 视频详情页 | SSR | SEO 关键页,需要 OG 标签 |
| 搜索页 | SSR | SEO 需要,搜索结果动态 |
| AI 生成页 | CSR | 纯交互,不需要 SEO |
| 个人中心 | CSR | 私有内容,无 SEO 需求 |
| 管理后台 | SPA | 内部使用,无 SEO 需求 |
5.2 Route Rules 配置
// apps/web/nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { swr: 60 }, // 首页缓存 60s
'/videos/**': { swr: 30 }, // 视频列表缓存 30s
'/video/:id': { ssr: true }, // 视频详情 SSR
'/search': { ssr: true }, // 搜索页 SSR
'/create/**': { ssr: false }, // AI 生成页 CSR
'/dashboard/**': { ssr: false }, // 个人中心 CSR
'/api/**': { cors: true }, // API 允许跨域
},
})5.3 SEO 关键:视频详情页的 Meta
<!-- pages/video/[id].vue -->
<script setup>
const route = useRoute()
const { data: video } = await useFetch(`/api/videos/${route.params.id}`)
useSeoMeta({
title: video.value?.title,
description: video.value?.description,
ogTitle: video.value?.title,
ogDescription: video.value?.description,
ogImage: video.value?.thumbnailUrl,
ogVideo: video.value?.hlsUrl,
ogType: 'video.other',
})
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'VideoObject',
name: video.value?.title,
description: video.value?.description,
thumbnailUrl: video.value?.thumbnailUrl,
uploadDate: video.value?.createdAt,
duration: `PT${video.value?.duration}S`,
}),
},
],
})
</script>6. 技术选型总览
6.1 技术栈清单
| 层级 | 技术 | 版本 | 用途 |
|---|---|---|---|
| 框架 | Nuxt4 | 4.x | 全栈框架 |
| UI | Nuxt UI + Tailwind v4 | 3.x / 4.x | 组件库 + 样式 |
| 状态 | Pinia | 3.x | 全局状态管理 |
| ORM | Drizzle | 0.35+ | 数据库操作 |
| 数据库 | PostgreSQL | 16 | 主数据库 |
| 缓存 | Redis | 7.x | 会话 + 限流 + 任务队列 |
| 对象存储 | S3 / Cloudflare R2 | — | 视频 + 图片存储 |
| 认证 | nuxt-auth-utils | — | Session 认证 |
| 校验 | Zod | 3.x | 输入校验 |
| 测试 | Vitest + Playwright | — | 单元 + E2E |
| 部署 | Docker + Node.js | — | 容器化部署 |
| 监控 | Sentry | — | 错误监控 |
6.2 关键架构决策
| 决策点 | 选择 | 否决方案 | 理由 |
|---|---|---|---|
| 前后端架构 | Nuxt4 全栈 | 前后端分离 | BFF 内置,减少一层服务 |
| Monorepo | pnpm workspace | Turborepo | 够用、简单,不需要远程缓存 |
| 数据库 | PostgreSQL | MySQL / MongoDB | JSONB 支持、全文搜索、性能 |
| ORM | Drizzle | Prisma | 零运行时、类型推导更强 |
| 对象存储 | Presigned URL 直传 | 服务端代理 | 减轻服务端带宽压力 |
| AI 调用 | 异步任务 + 轮询 | WebSocket | 更简单、更可靠 |
| 视频播放 | HLS.js | 原生 video | 自适应码率、更好的兼容性 |
本章小结
- 架构分层:CDN → Nuxt4 SSR → Nitro BFF → 外部服务(DB/AI/Storage),全栈一体化
- Monorepo:apps/(web + admin)+ packages/(shared + ui + database),pnpm workspace 管理
- 数据库:Drizzle ORM + PostgreSQL,Schema 即类型,videos/aiTasks/users 三张核心表
- API 设计:RESTful 文件路由,Zod 输入校验,
readValidatedBody统一处理 - 混合渲染:SEO 页面 SSR/SWR,交互页面 CSR,管理后台 SPA——Route Rules 一行配置
- 技术决策:每个选型都有明确的否决方案和理由,避免"因为流行所以用"