🏠

数据迁移

从文件系统到数据库的华丽转身

讲师:乌鸦哥

让我们的博客彻底"专业化"!

为什么要迁移数据?

告别"原始时代",拥抱现代化 🚀

文件存储的问题

现在的博客就像"手工作坊" 🔨

  • 文章都写在JSON文件里
  • 想改内容要修改代码
  • 数据分散,难以管理
  • 无法实现动态功能
  • 扩展性很差

数据库存储的优势

升级到"现代化工厂" 🏭

  • 动态管理 - 随时增删改查
  • 快速查询 - 复杂条件搜索
  • 灵活排序 - 按时间、热度等排序
  • 多用户支持 - 并发操作无压力
  • 数据安全 - 事务保证一致性

迁移策略

制定我们的"搬家计划" 📦

迁移步骤

就像搬家一样,要有条不紊

  1. 数据备份 - 先把原来的数据保存好
  2. 数据清理 - 整理和标准化数据格式
  3. 批量导入 - 通过管理后台批量添加
  4. 前端改造 - 修改数据获取方式
  5. 测试验证 - 确保功能正常
  6. 上线切换 - 正式启用新系统

第一步:准备现有数据

整理我们的"旧家当" 📂

查看现有数据结构


// data/posts.json (示例)
[
  {
    "id": 1,
    "title": "我的第一篇博客",
    "content": "这是我第一次写博客...",
    "author": "张三",
    "createTime": "2024-01-01",
    "tags": ["生活", "随想"]
  },
  {
    "id": 2,
    "title": "学习前端开发",
    "content": "今天学习了Vue.js...",
    "author": "李四", 
    "createTime": "2024-01-05",
    "tags": ["技术", "前端"]
  }
]
            

这些数据需要"搬进"数据库

数据清理和转换


// utils/dataTransform.js
export function transformPostData(fileData) {
  return fileData.map(post => ({
    title: post.title.trim(),
    content: post.content.trim(),
    author: post.author || '未知作者',
    createdAt: new Date(post.createTime),
    status: 'published',
    tags: Array.isArray(post.tags) ? post.tags.join(',') : '',
    // 添加缺失的字段
    slug: generateSlug(post.title),
    excerpt: generateExcerpt(post.content),
    viewCount: 0
  }))
}

function generateSlug(title) {
  return title.toLowerCase()
    .replace(/[^a-z0-9一-龥]/g, '-')
    .replace(/-+/g, '-')
}

function generateExcerpt(content, length = 150) {
  return content.substring(0, length) + '...'
}
            

第二步:批量导入数据

通过管理后台"搬家" 🚚

创建数据导入页面


<!-- pages/admin/import.vue -->
<template>
  <div class="max-w-4xl mx-auto p-6">
    <h1 class="text-3xl font-bold mb-8">数据导入</h1>
    
    <div class="bg-yellow-50 border border-yellow-200 rounded p-4 mb-6">
      <h3 class="font-bold text-yellow-800">⚠️ 注意</h3>
      <p class="text-yellow-700">此操作将导入所有历史文章到数据库,请确保数据备份完成</p>
    </div>
    
    <div class="space-y-4">
      <button 
        @click="importPosts"
        :disabled="importing"
        class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 disabled:opacity-50"
      >
        {{ importing ? '导入中...' : '开始导入文章' }}
      </button>
      
      <div v-if="importResult" class="mt-4 p-4 rounded-lg"
           :class="importResult.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'">
        {{ importResult.message }}
      </div>
    </div>
  </div>
</template>
            

实现导入逻辑


// pages/admin/import.vue 的 script 部分
<script setup>
const importing = ref(false)
const importResult = ref(null)

const importPosts = async () => {
  importing.value = true
  importResult.value = null
  
  try {
    const response = await $fetch('/api/admin/import-posts', {
      method: 'POST'
    })
    
    importResult.value = {
      success: true,
      message: `成功导入 ${response.count} 篇文章!`
    }
    
  } catch (error) {
    importResult.value = {
      success: false,
      message: '导入失败:' + error.message
    }
  } finally {
    importing.value = false
  }
}
</script>
            

创建导入接口


// server/api/admin/import-posts.post.js
import fs from 'fs'
import path from 'path'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export default defineEventHandler(async (event) => {
  try {
    // 验证管理员权限
    const user = await getCurrentUser(event)
    if (!user || user.role !== 'admin') {
      throw createError({
        statusCode: 403,
        statusMessage: '需要管理员权限'
      })
    }
    
    // 读取JSON文件
    const dataPath = path.join(process.cwd(), 'data/posts.json')
    const fileData = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
    
    // 转换数据格式
    const transformedData = transformPostData(fileData)
    
    // 批量插入数据库
    const results = await prisma.post.createMany({
      data: transformedData,
      skipDuplicates: true // 跳过重复数据
    })
    
    return {
      success: true,
      count: results.count,
      message: `成功导入 ${results.count} 篇文章`
    }
    
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '导入失败:' + error.message
    })
  }
})
            

第三步:修改前端页面

让页面从数据库"取货" 🛒

修改首页文章列表


<!-- pages/index.vue -->
<script setup>
// ❌ 旧方式:从文件读取
// import postsData from '~/data/posts.json'

// ✅ 新方式:从数据库获取
const { data: response } = await useFetch('/api/posts', {
  query: {
    limit: 6,
    status: 'published'
  }
})

const posts = computed(() => response.value?.posts || [])
</script>

<template>
  <div class="container mx-auto px-4 py-8">
    <h1 class="text-4xl font-bold text-center mb-8">最新文章</h1>
    
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      <article 
        v-for="post in posts" 
        :key="post.id"
        class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
      >
        <div class="p-6">
          <h2 class="text-xl font-bold mb-2">
            <NuxtLink :to="`/posts/${post.id}`" class="hover:text-blue-600">
              {{ post.title }}
            </NuxtLink>
          </h2>
          
          <p class="text-gray-600 mb-4">
            {{ post.excerpt || post.content.substring(0, 150) + '...' }}
          </p>
          
          <div class="flex justify-between items-center text-sm text-gray-500">
            <span>作者:{{ post.author?.name || post.author }}</span>
            <span>{{ formatDate(post.createdAt) }}</span>
          </div>
        </div>
      </article>
    </div>
  </div>
</template>
            

修改文章详情页


<!-- pages/posts/[id].vue -->
<script setup>
const route = useRoute()
const postId = route.params.id

// ❌ 旧方式:从文件查找
// const post = postsData.find(p => p.id === parseInt(postId))

// ✅ 新方式:从数据库获取
const { data: post } = await useFetch(`/api/posts/${postId}`)

if (!post.value) {
  throw createError({
    statusCode: 404,
    statusMessage: '文章不存在'
  })
}

// 增加浏览量
await $fetch(`/api/posts/${postId}/view`, { method: 'POST' })
</script>

<template>
  <article class="max-w-4xl mx-auto px-4 py-8">
    <header class="mb-8">
      <h1 class="text-4xl font-bold mb-4">{{ post.title }}</h1>
      
      <div class="flex items-center justify-between text-gray-600 border-b pb-4">
        <div class="flex items-center space-x-4">
          <span>作者:{{ post.author?.name || post.author }}</span>
          <span>发布时间:{{ formatDate(post.createdAt) }}</span>
        </div>
        <span>浏览量:{{ post.viewCount }}</span>
      </div>
    </header>
    
    <div class="prose prose-lg max-w-none">
      <div v-html="formatContent(post.content)"></div>
    </div>
    
    <footer class="mt-8 pt-4 border-t">
      <div v-if="post.tags" class="flex flex-wrap gap-2">
        <span 
          v-for="tag in post.tags.split(',')" 
          :key="tag"
          class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm"
        >
          #{{ tag.trim() }}
        </span>
      </div>
    </footer>
  </article>
</template>
            

第四步:完善功能

添加现代化博客必备功能 ✨

添加浏览量统计


// server/api/posts/[id]/view.post.js
export default defineEventHandler(async (event) => {
  try {
    const postId = parseInt(getRouterParam(event, 'id'))
    
    // 更新浏览量
    await prisma.post.update({
      where: { id: postId },
      data: {
        viewCount: {
          increment: 1
        }
      }
    })
    
    return { success: true }
    
  } catch (error) {
    // 静默处理错误,不影响用户体验
    console.error('更新浏览量失败:', error)
    return { success: false }
  }
})
            

添加搜索功能


// server/api/posts/search.get.js
export default defineEventHandler(async (event) => {
  try {
    const query = getQuery(event)
    const keyword = query.q?.trim()
    
    if (!keyword) {
      return { posts: [], total: 0 }
    }
    
    const posts = await prisma.post.findMany({
      where: {
        AND: [
          { status: 'published' },
          {
            OR: [
              { title: { contains: keyword } },
              { content: { contains: keyword } },
              { tags: { contains: keyword } }
            ]
          }
        ]
      },
      orderBy: { createdAt: 'desc' },
      include: {
        author: {
          select: { name: true, avatar: true }
        }
      }
    })
    
    return {
      posts,
      total: posts.length,
      keyword
    }
    
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '搜索失败'
    })
  }
})
            

添加分类和标签


// server/api/posts/categories.get.js
export default defineEventHandler(async (event) => {
  try {
    // 获取所有标签并统计使用次数
    const posts = await prisma.post.findMany({
      where: { status: 'published' },
      select: { tags: true }
    })
    
    const tagCount = {}
    
    posts.forEach(post => {
      if (post.tags) {
        post.tags.split(',').forEach(tag => {
          const trimmedTag = tag.trim()
          if (trimmedTag) {
            tagCount[trimmedTag] = (tagCount[trimmedTag] || 0) + 1
          }
        })
      }
    })
    
    // 按使用次数排序
    const sortedTags = Object.entries(tagCount)
      .sort(([,a], [,b]) => b - a)
      .map(([tag, count]) => ({ tag, count }))
    
    return { tags: sortedTags }
    
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '获取标签失败'
    })
  }
})
            

完整的博客系统

我们的"作品"完成了! 🎊

系统架构图

🎨 前端层 (Nuxt.js)

  • • 首页 - 文章列表展示
  • • 详情页 - 文章内容阅读
  • • 管理后台 - 内容管理
  • • 搜索页 - 内容搜索

🔧 后端层 (API Routes)

  • • 文章CRUD - 增删改查
  • • 用户认证 - 登录授权
  • • 搜索接口 - 全文搜索
  • • 统计接口 - 数据分析

💾 数据层 (Database)

  • • 文章表 - 存储文章内容
  • • 用户表 - 存储用户信息
  • • 关系维护 - 数据一致性

功能特色

👤 用户功能

  • • 浏览文章列表
  • • 阅读文章详情
  • • 搜索文章内容
  • • 按标签分类
  • • 响应式设计

👨‍💼 管理功能

  • • 发布新文章
  • • 编辑已有文章
  • • 批量数据导入
  • • 权限控制
  • • 数据统计

技术栈总结

前端技术

  • • Nuxt.js 3
  • • Vue.js 3
  • • Tailwind CSS
  • • TypeScript

后端技术

  • • Node.js
  • • Nitro Engine
  • • Prisma ORM
  • • SQLite

其他工具

  • • JWT 认证
  • • File Upload
  • • SEO 优化
  • • PWA 支持

🎉 恭喜完成!

你已经完成了一个完整的全栈博客系统

✅ 你学会了什么:

  • 前端页面开发和组件化
  • 后端API设计和开发
  • 数据库设计和操作
  • 用户认证和权限管理
  • 数据迁移和系统整合
  • 全栈项目的完整开发流程

下一步:可以继续添加评论、点赞、分享等高级功能! 🚀