架构设计与技术选型

从本章开始,我们将构建一个完整的 AI 视频平台——用户可以通过文本描述生成 AI 视频、上传和管理视频、在线播放、搜索浏览,管理员可以审核内容和查看数据看板。这不是一个玩具项目,而是一个覆盖前后端全链路的生产级系统。本章从需求分析到架构设计,完整呈现一个 Nuxt4 全栈项目的技术决策过程。

1. 需求分析

1.1 产品定位

AI 视频平台的核心价值:让普通用户通过文本描述生成短视频。围绕这个核心,衍生出完整的视频生命周期管理。

1.2 核心功能矩阵

模块用户端功能管理端功能
AI 生成文本→视频、风格选择、生成进度、历史记录模型配置、配额管理
视频管理上传、编辑信息、删除、封面设置内容审核、批量操作
播放体验HLS 自适应码率、弹幕、进度记忆、画中画播放质量监控
用户系统注册登录、个人主页、收藏、关注用户管理、封禁
搜索发现全文搜索、标签筛选、推荐流搜索热词、推荐策略
数据看板播放量、点赞数全站数据、趋势分析

1.3 非功能性需求

维度目标关键技术
性能LCP < 2.5s, FID < 100msSSR + 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 60sSEO 重要,内容可缓存
视频详情页SSRSEO 关键页,需要 OG 标签
搜索页SSRSEO 需要,搜索结果动态
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 技术栈清单

层级技术版本用途
框架Nuxt44.x全栈框架
UINuxt UI + Tailwind v43.x / 4.x组件库 + 样式
状态Pinia3.x全局状态管理
ORMDrizzle0.35+数据库操作
数据库PostgreSQL16主数据库
缓存Redis7.x会话 + 限流 + 任务队列
对象存储S3 / Cloudflare R2视频 + 图片存储
认证nuxt-auth-utilsSession 认证
校验Zod3.x输入校验
测试Vitest + Playwright单元 + E2E
部署Docker + Node.js容器化部署
监控Sentry错误监控

6.2 关键架构决策

决策点选择否决方案理由
前后端架构Nuxt4 全栈前后端分离BFF 内置,减少一层服务
Monorepopnpm workspaceTurborepo够用、简单,不需要远程缓存
数据库PostgreSQLMySQL / MongoDBJSONB 支持、全文搜索、性能
ORMDrizzlePrisma零运行时、类型推导更强
对象存储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 一行配置
  • 技术决策:每个选型都有明确的否决方案和理由,避免"因为流行所以用"