博客网站系统 - SpringBoot + Vue2/Vue3 全栈开发方案

作者:nctcpc 发表于:2025-11-17

博客网站系统 - SpringBoot + Vue2/Vue3 全栈开发方案


系统架构设计


技术栈选型

  • 后端: SpringBoot 2.7+、Spring Security、JWT、MyBatis Plus、WebSocket

  • 前端: Vue2/Vue3混合使用、Vue Router、Vuex/Pinia、Element Plus/Element UI

  • 数据库: MySQL 8.0、Redis

  • 部署: Ubuntu 24.04.3 LTS、宝塔面板、Docker(可选)

后端实现方案


项目结构

blog-backend/
├── src/main/java/com/blog/
│   ├── config/           # 配置类
│   ├── controller/       # 控制器
│   ├── service/          # 业务层
│   ├── mapper/           # 数据访问层
│   ├── entity/           # 实体类
│   ├── dto/              # 数据传输对象
│   ├── utils/            # 工具类
│   └── security/         # 安全配置
├── resources/
│   ├── application.yml   # 主配置文件
│   └── mapper/           # MyBatis映射文件
└── pom.xml


核心依赖配置 (pom.xml)

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
    <!-- 数据库 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
    <!-- 文件上传 -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
</dependencies>


核心配置文件 (application.yml)

server:
  port: 8080
  servlet:
    context-path: /api

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/blog_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

jwt:
  secret: your-jwt-secret-key
  expiration: 86400000


WebSocket配置类

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }
}


文章管理控制器示例

@RestController
@RequestMapping("/articles")
public class ArticleController {
    
    @Autowired
    private ArticleService articleService;
    
    @GetMapping
    public Result listArticles(@RequestParam(defaultValue = "1") Integer page,
                             @RequestParam(defaultValue = "10") Integer size) {
        Page<ArticleVO> articles = articleService.getArticles(page, size);
        return Result.success(articles);
    }
    
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public Result createArticle(@RequestBody ArticleDTO articleDTO) {
        articleService.createArticle(articleDTO);
        return Result.success("文章创建成功");
    }
    
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public Result updateArticle(@PathVariable Long id, @RequestBody ArticleDTO articleDTO) {
        articleService.updateArticle(id, articleDTO);
        return Result.success("文章更新成功");
    }
}


前端实现方案


项目结构

blog-frontend/
├── public/
├── src/
│   ├── assets/           # 静态资源
│   ├── components/       # 公共组件
│   ├── views/           # 页面组件
│   ├── router/          # 路由配置
│   ├── store/           # 状态管理
│   ├── utils/           # 工具函数
│   └── api/             # API接口
├── package.json
└── vue.config.js


package.json 核心依赖

{
  "dependencies": {
    "vue": "^3.2.0",
    "vue-router": "^4.0.0",
    "pinia": "^2.0.0",
    "axios": "^1.0.0",
    "element-plus": "^2.2.0",
    "tailwindcss": "^3.2.0",
    "sockjs-client": "^1.6.0",
    "stompjs": "^2.3.3"
  }
}


响应式布局配置 (tailwind.config.js)

module.exports = {
  content: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      screens: {
        'sm': '640px',
        'md': '768px', 
        'lg': '1024px',
        'xl': '1280px',
        '2xl': '1536px',
      }
    },
  },
  plugins: [],
}


文章列表组件示例

<template>
  <div class="container mx-auto px-4 py-8">
    <!-- 文章列表 -->
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      <article 
        v-for="article in articles" 
        :key="article.id"
        class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
      >
        <!-- 文章封面 -->
        <div class="h-48 overflow-hidden">
          <img 
            :src="article.coverImage" 
            :alt="article.title"
            class="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
          >
        </div>
        
        <!-- 文章内容 -->
        <div class="p-6">
          <h3 class="text-xl font-semibold mb-2 line-clamp-2">
            {{ article.title }}
          </h3>
          <p class="text-gray-600 mb-4 line-clamp-3">
            {{ article.summary }}
          </p>
          
          <!-- 标签 -->
          <div class="flex flex-wrap gap-2 mb-4">
            <span 
              v-for="tag in article.tags" 
              :key="tag"
              class="px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
            >
              {{ tag }}
            </span>
          </div>
          
          <!-- 文章信息 -->
          <div class="flex items-center justify-between text-sm text-gray-500">
            <span>{{ formatDate(article.createTime) }}</span>
            <span>{{ article.viewCount }} 阅读</span>
          </div>
        </div>
      </article>
    </div>
    
    <!-- 分页 -->
    <div class="mt-8 flex justify-center">
      <el-pagination
        v-model:current-page="currentPage"
        :page-size="pageSize"
        :total="total"
        layout="prev, pager, next"
        @current-change="handlePageChange"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { getArticles } from '@/api/article'

const articles = ref([])
const currentPage = ref(1)
const pageSize = ref(9)
const total = ref(0)

// 加载文章列表
const loadArticles = async () => {
  try {
    const response = await getArticles(currentPage.value, pageSize.value)
    articles.value = response.data.records
    total.value = response.data.total
  } catch (error) {
    console.error('加载文章失败:', error)
  }
}

// 分页变化
const handlePageChange = (page) => {
  currentPage.value = page
  loadArticles()
}

// 格式化日期
const formatDate = (dateString) => {
  return new Date(dateString).toLocaleDateString('zh-CN')
}

onMounted(() => {
  loadArticles()
})
</script>

<style scoped>
.line-clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.line-clamp-3 {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
</style>


聊天室组件示例

<template>
  <div class="h-screen flex flex-col bg-gray-100">
    <!-- 聊天头部 -->
    <header class="bg-white shadow-sm p-4">
      <h2 class="text-xl font-semibold">{{ currentRoom.name }}</h2>
    </header>
    
    <!-- 消息区域 -->
    <div ref="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-4">
      <div 
        v-for="message in messages" 
        :key="message.id"
        :class="['flex', message.isOwn ? 'justify-end' : 'justify-start']"
      >
        <div :class="[
          'max-w-xs lg:max-w-md px-4 py-2 rounded-lg',
          message.isOwn ? 'bg-blue-500 text-white' : 'bg-white text-gray-800'
        ]">
          <p class="text-sm">{{ message.content }}</p>
          <span class="text-xs opacity-70">{{ formatTime(message.timestamp) }}</span>
        </div>
      </div>
    </div>
    
    <!-- 输入区域 -->
    <div class="bg-white border-t p-4">
      <div class="flex space-x-2">
        <input
          v-model="newMessage"
          @keyup.enter="sendMessage"
          type="text"
          placeholder="输入消息..."
          class="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
        >
        <button
          @click="sendMessage"
          class="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 transition-colors"
        >
          发送
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { Client } from '@stomp/stompjs'
import SockJS from 'sockjs-client'

const messages = ref([])
const newMessage = ref('')
const messagesContainer = ref(null)
const stompClient = ref(null)
const currentRoom = ref({ id: 1, name: '公共聊天室' })

// 连接到WebSocket
const connect = () => {
  const socket = new SockJS('http://localhost:8080/api/ws')
  stompClient.value = new Client({
    webSocketFactory: () => socket,
    reconnectDelay: 5000,
    onConnect: () => {
      console.log('Connected to WebSocket')
      
      // 订阅聊天室消息
      stompClient.value.subscribe(`/topic/chat/${currentRoom.value.id}`, (message) => {
        const receivedMessage = JSON.parse(message.body)
        messages.value.push({
          ...receivedMessage,
          isOwn: false
        })
        scrollToBottom()
      })
    },
    onStompError: (error) => {
      console.error('WebSocket error:', error)
    }
  })
  
  stompClient.value.activate()
}

// 发送消息
const sendMessage = () => {
  if (!newMessage.value.trim()) return
  
  const message = {
    roomId: currentRoom.value.id,
    content: newMessage.value,
    timestamp: new Date().toISOString()
  }
  
  stompClient.value.publish({
    destination: '/app/chat.send',
    body: JSON.stringify(message)
  })
  
  messages.value.push({
    ...message,
    isOwn: true
  })
  
  newMessage.value = ''
  scrollToBottom()
}

// 滚动到底部
const scrollToBottom = () => {
  nextTick(() => {
    if (messagesContainer.value) {
      messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
    }
  })
}

// 格式化时间
const formatTime = (timestamp) => {
  return new Date(timestamp).toLocaleTimeString('zh-CN', {
    hour: '2-digit',
    minute: '2-digit'
  })
}

onMounted(() => {
  connect()
})

onUnmounted(() => {
  if (stompClient.value) {
    stompClient.value.deactivate()
  }
})
</script>


数据库设计


核心表结构

-- 用户表
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    avatar VARCHAR(255),
    role ENUM('ADMIN', 'USER') DEFAULT 'USER',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 文章表
CREATE TABLE articles (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200) NOT NULL,
    content LONGTEXT NOT NULL,
    summary TEXT,
    cover_image VARCHAR(255),
    category_id BIGINT,
    status ENUM('DRAFT', 'PUBLISHED') DEFAULT 'DRAFT',
    view_count INT DEFAULT 0,
    like_count INT DEFAULT 0,
    comment_count INT DEFAULT 0,
    seo_keywords VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- 分类表
CREATE TABLE categories (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    description VARCHAR(200),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 标签表
CREATE TABLE tags (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 文章标签关联表
CREATE TABLE article_tags (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    article_id BIGINT NOT NULL,
    tag_id BIGINT NOT NULL,
    FOREIGN KEY (article_id) REFERENCES articles(id),
    FOREIGN KEY (tag_id) REFERENCES tags(id)
);

-- 评论表
CREATE TABLE comments (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    article_id BIGINT NOT NULL,
    user_id BIGINT,
    content TEXT NOT NULL,
    parent_id BIGINT,
    status ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (article_id) REFERENCES articles(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (parent_id) REFERENCES comments(id)
);

-- 聊天室表
CREATE TABLE chat_rooms (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    type ENUM('PUBLIC', 'PRIVATE', 'GROUP') DEFAULT 'PUBLIC',
    created_by BIGINT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (created_by) REFERENCES users(id)
);

-- 聊天消息表
CREATE TABLE chat_messages (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    room_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    content TEXT NOT NULL,
    message_type ENUM('TEXT', 'IMAGE', 'FILE') DEFAULT 'TEXT',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (room_id) REFERENCES chat_rooms(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);


Ubuntu 24.04.3 LTS 宝塔面板部署教程

  1. 系统准备

# 更新系统
sudo apt update && sudo apt upgrade -y

# 安装必要工具
sudo apt install -y curl wget vim git


2. 安装宝塔面板

# 下载并安装宝塔面板
wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh

# 安装完成后会显示面板地址、用户名和密码,请妥善保存

3. 配置宝塔面板

  1. 访问宝塔面板(通常是 http://服务器IP:8888

  2. 登录后按照向导安装推荐套件:

    • Nginx 1.22+

    • MySQL 8.0

    • PHP 8.1(可选)

    • Redis

    • PM2 Manager(Node.js管理)

4. 环境配置

配置MySQL

# 登录MySQL
mysql -u root -p

# 创建数据库和用户
CREATE DATABASE blog_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'blog_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT ALL PRIVILEGES ON blog_db.* TO 'blog_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

配置Redis

# 编辑Redis配置
sudo vim /etc/redis/redis.conf

# 修改以下配置:
# bind 127.0.0.1
# requirepass your_redis_password
# maxmemory 256mb
# maxmemory-policy allkeys-lru

# 重启Redis
sudo systemctl restart redis

5. 部署后端应用

# 创建应用目录
sudo mkdir -p /www/wwwroot/blog/api
cd /www/wwwroot/blog/api

# 上传或克隆后端代码
git clone your-backend-repo .

# 编译项目
./mvnw clean package -Dmaven.test.skip=true

# 创建启动脚本
sudo vim /www/wwwroot/blog/start-backend.sh

启动脚本内容:

#!/bin/bash
APP_NAME="blog-backend"
APP_PORT=8080
JAR_PATH="/www/wwwroot/blog/api/target/blog-backend-1.0.0.jar"

# 停止现有应用
PID=$(ps -ef | grep $APP_NAME | grep -v grep | awk '{print $2}')
if [ -n "$PID" ]; then
    echo "Stopping $APP_NAME (PID: $PID)"
    kill -9 $PID
fi

# 启动应用
nohup java -jar $JAR_PATH --server.port=$APP_PORT > /www/wwwroot/blog/backend.log 2>&1 &
echo "Backend application started on port $APP_PORT"

6. 部署前端应用

# 创建前端目录
sudo mkdir -p /www/wwwroot/blog/frontend
cd /www/wwwroot/blog/frontend

# 上传或克隆前端代码
git clone your-frontend-repo .

# 安装依赖
npm install

# 构建生产版本
npm run build

使用PM2管理前端服务:

# 安装PM2
npm install -g pm2

# 创建PM2配置文件
vim ecosystem.config.js

PM2配置:

module.exports = {
  apps: [{
    name: 'blog-frontend',
    script: 'node_modules/@vue/cli-service/bin/vue-cli-service.js',
    args: 'serve',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    }
  }]
}
# 启动前端服务
pm2 start ecosystem.config.js
pm2 save
pm2 startup

7. 配置Nginx反向代理

在宝塔面板中创建网站:

  • 域名:your-domain.com(或使用服务器IP)

  • 根目录:/www/wwwroot/blog/frontend/dist

配置Nginx反向代理:

server {
    listen 80;
    server_name your-domain.com;
    
    # 前端静态文件
    location / {
        root /www/wwwroot/blog/frontend/dist;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    
    # 后端API代理
    location /api/ {
        proxy_pass http://localhost:8080/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    # WebSocket代理
    location /ws/ {
        proxy_pass http://localhost:8080/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

8. SSL证书配置(可选)

在宝塔面板中申请免费的Let's Encrypt SSL证书,启用HTTPS。

9. 防火墙配置

# 开启防火墙
sudo ufw enable

# 开放必要端口
sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 22

0. 系统服务配置

创建系统服务文件管理后端应用:

sudo vim /etc/systemd/system/blog-backend.service

服务文件内容:

[Unit]
Description=Blog Backend Service
After=network.target

[Service]
Type=simple
User=www
Group=www
WorkingDirectory=/www/wwwroot/blog/api
ExecStart=/usr/bin/java -jar /www/wwwroot/blog/api/target/blog-backend-1.0.0.jar
ExecReload=/bin/kill -HUP $MAINPID
Restart=always

[Install]
WantedBy=multi-user.target
# 启用服务
sudo systemctl daemon-reload
sudo systemctl enable blog-backend
sudo systemctl start blog-backend

11. 监控和维护

设置日志轮转:

sudo vim /etc/logrotate.d/blog

日志轮转配置:

/www/wwwroot/blog/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    copytruncate
}

部署验证

  1. 访问网站确认前端正常加载

  2. 测试API接口是否正常工作

  3. 验证WebSocket连接

  4. 检查静态资源加载

  5. 确认数据库连接正常

项目优化建议


性能优化

  1. 启用Gzip压缩

  2. 配置CDN加速静态资源

  3. 使用Redis缓存热点数据

  4. 数据库查询优化

  5. 图片懒加载和WebP格式

安全加固

  1. 定期更新系统和依赖

  2. 配置防火墙和安全组

  3. 启用HTTPS

  4. 设置强密码策略

  5. 定期备份数据

这个完整的方案提供了从技术选型、代码实现到服务器部署的全套解决方案。您可以根据实际需求调整配置和功能模块。


文章来源:本站博主原创设计

版权声明

本文仅代表作者观点,不代表京强博客立场。
本文系作者授权百度百家发表,未经许可,不得转载。

分享:

扫一扫在手机阅读、分享本文

请发表您的评论

您是本站第3名访客 今日有0篇新文章/评论