博客网站系统 - 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 宝塔面板部署教程
系统准备
# 更新系统 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. 配置宝塔面板
访问宝塔面板(通常是
http://服务器IP:8888)登录后按照向导安装推荐套件:
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
}部署验证
访问网站确认前端正常加载
测试API接口是否正常工作
验证WebSocket连接
检查静态资源加载
确认数据库连接正常
项目优化建议
性能优化
启用Gzip压缩
配置CDN加速静态资源
使用Redis缓存热点数据
数据库查询优化
图片懒加载和WebP格式
安全加固
定期更新系统和依赖
配置防火墙和安全组
启用HTTPS
设置强密码策略
定期备份数据
这个完整的方案提供了从技术选型、代码实现到服务器部署的全套解决方案。您可以根据实际需求调整配置和功能模块。
文章来源:本站博主原创设计
版权声明
本文仅代表作者观点,不代表京强博客立场。
本文系作者授权百度百家发表,未经许可,不得转载。

