10784 字
22 分钟
Spring Boot + Vue.js 全栈开发实战:从零搭建企业级博客系统
Spring Boot + Vue.js 全栈开发实战:从零搭建企业级博客系统
问题引入
在现代Web开发中,全栈开发已经成为主流趋势。作为一个开发者,掌握前后端分离的开发模式至关重要。本文将以企业级博客系统为例,详细介绍如何使用Spring Boot和Vue.js技术栈进行全栈开发。
技术栈选择
后端技术栈
- Spring Boot 2.7+: 快速构建企业级应用
- Spring Security: 用户认证与授权
- MyBatis Plus: 数据库操作简化
- MySQL 8.0: 关系型数据库
- Redis: 缓存与会话管理
前端技术栈
- Vue 3 + TypeScript: 现代化前端框架
- Element Plus: UI组件库
- Axios: HTTP客户端
- Vue Router: 路由管理
- Pinia: 状态管理
核心功能实现
1. 用户认证系统
后端实现
// 用户实体类
@Data
@TableName("sys_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String email;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// JWT工具类
@Component
public class JwtUtils {
private static final String SECRET_KEY = "your-secret-key";
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000; // 24小时
public static String generateToken(User user) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + EXPIRE_TIME);
return Jwts.builder()
.setSubject(user.getUsername())
.claim("userId", user.getId())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
// 认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
User user = userService.findByUsername(request.getUsername());
if (user == null || !userService.checkPassword(request.getPassword(), user.getPassword())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("message", "用户名或密码错误"));
}
String token = JwtUtils.generateToken(user);
return ResponseEntity.ok(Map.of(
"token", token,
"user", Map.of(
"id", user.getId(),
"username", user.getUsername(),
"email", user.getEmail()
)
));
}
}
前端实现
// 登录组件
<template>
<div class="login-container">
<el-form :model="loginForm" :rules="rules" ref="loginForm">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { ElMessage } from 'element-plus';
const router = useRouter();
const userStore = useUserStore();
const loginForm = reactive({
username: '',
password: ''
});
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
};
const handleLogin = async () => {
try {
const response = await userStore.login(loginForm);
if (response.code === 200) {
ElMessage.success('登录成功');
router.push('/');
} else {
ElMessage.error(response.message || '登录失败');
}
} catch (error) {
ElMessage.error('登录异常');
}
};
</script>
2. 文章管理功能
数据库设计
-- 文章表
CREATE TABLE article (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
summary VARCHAR(500),
content TEXT NOT NULL,
category_id BIGINT,
status TINYINT DEFAULT 1,
is_top TINYINT DEFAULT 0,
is_original TINYINT DEFAULT 1,
view_count INT DEFAULT 0,
create_by VARCHAR(50),
create_time DATETIME,
update_by VARCHAR(50),
update_time DATETIME,
FOREIGN KEY (category_id) REFERENCES category(id)
);
-- 分类表
CREATE TABLE category (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100),
description VARCHAR(500),
article_count INT DEFAULT 0
);
-- 标签表
CREATE TABLE tag (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100),
color VARCHAR(20)
);
-- 文章标签关联表
CREATE TABLE article_tag (
article_id BIGINT,
tag_id BIGINT,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES article(id),
FOREIGN KEY (tag_id) REFERENCES tag(id)
);
后端实现
// 文章服务类
@Service
public class ArticleService {
@Autowired
private ArticleMapper articleMapper;
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private TagMapper tagMapper;
// 创建文章
@Transactional
public Article createArticle(ArticleDTO articleDTO) {
// 验证分类
Category category = categoryMapper.selectById(articleDTO.getCategoryId());
if (category == null) {
throw new BusinessException("分类不存在");
}
// 创建文章
Article article = new Article();
article.setTitle(articleDTO.getTitle());
article.setSummary(articleDTO.getSummary());
article.setContent(articleDTO.getContent());
article.setCategoryId(articleDTO.getCategoryId());
article.setStatus(articleDTO.getStatus());
article.setIsTop(articleDTO.getIsTop());
article.setIsOriginal(articleDTO.getIsOriginal());
article.setCreateBy(SecurityUtils.getCurrentUsername());
article.setCreateTime(LocalDateTime.now());
articleMapper.insert(article);
// 处理标签
if (articleDTO.getTagIds() != null && !articleDTO.getTagIds().isEmpty()) {
articleDTO.getTagIds().forEach(tagId -> {
articleMapper.insertArticleTag(article.getId(), tagId);
});
}
// 更新分类文章数量
categoryMapper.incrementArticleCount(articleDTO.getCategoryId());
return article;
}
// 获取文章列表
public Page<ArticleVO> getArticleList(Pageable pageable, Long categoryId, Long tagId) {
Page<Article> page = new Page<>(pageable.getPageNumber(), pageable.getPageSize());
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Article::getStatus, 1);
if (categoryId != null) {
wrapper.eq(Article::getCategoryId, categoryId);
}
if (tagId != null) {
wrapper.inSql(Article::getId,
"SELECT article_id FROM article_tag WHERE tag_id = " + tagId);
}
wrapper.orderByDesc(Article::getIsTop, Article::getCreateTime);
Page<Article> articlePage = articleMapper.selectPage(page, wrapper);
// 转换为VO
return articlePage.convert(this::convertToVO);
}
private ArticleVO convertToVO(Article article) {
ArticleVO vo = new ArticleVO();
BeanUtils.copyProperties(article, vo);
// 获取分类信息
Category category = categoryMapper.selectById(article.getCategoryId());
if (category != null) {
vo.setCategoryName(category.getName());
}
// 获取标签信息
List<Tag> tags = tagMapper.selectByArticleId(article.getId());
vo.setTagNames(tags.stream().map(Tag::getName).collect(Collectors.toList()));
return vo;
}
}
前端实现
// 文章列表组件
<template>
<div class="article-list">
<div v-for="article in articles" :key="article.id" class="article-item">
<h3 class="article-title">
<router-link :to="'/posts/' + article.id">{{ article.title }}</router-link>
</h3>
<p class="article-summary">{{ article.summary }}</p>
<div class="article-meta">
<span class="category">{{ article.categoryName }}</span>
<span class="tags">
<el-tag v-for="tag in article.tagNames" :key="tag" size="small">{{ tag }}</el-tag>
</span>
<span class="time">{{ formatDate(article.createTime) }}</span>
</div>
</div>
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { articleApi } from '@/api/article';
const articles = ref([]);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const fetchArticles = async () => {
try {
const response = await articleApi.getList({
pageNum: currentPage.value,
pageSize: pageSize.value
});
articles.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取文章列表失败:', error);
}
};
const handlePageChange = (page: number) => {
currentPage.value = page;
fetchArticles();
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN');
};
onMounted(() => {
fetchArticles();
});
</script>
3. 部署与优化
Docker部署
# 后端Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/blog-backend.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# 前端Dockerfile
FROM node:16-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
性能优化建议
数据库优化
- 添加索引:文章表的title、create_time字段
- 使用读写分离
- 缓存热点数据
前端优化
- 路由懒加载
- 图片懒加载
- 使用CDN加速静态资源
后端优化
- 使用Redis缓存文章列表
- 异步处理文章访问统计
- 使用消息队列处理耗时操作
实际应用场景
这套博客系统可以应用于:
- 个人技术博客
- 团队知识库
- 企业内部文档系统
- 技术社区平台
面试要点
Spring Security如何实现JWT认证?
- 使用过滤器拦截请求
- 解析JWT token
- 验证token有效性
- 设置SecurityContext
Vue 3组合式API的优势?
- 逻辑复用更灵活
- 代码组织更清晰
- 类型推导更好
- 性能优化更易实现
如何设计数据库表结构?
- 遵循第三范式
- 合理使用索引
- 考虑数据增长
- 预留扩展字段
总结
本文详细介绍了使用Spring Boot和Vue.js开发企业级博客系统的完整流程,从技术选型、核心功能实现到部署优化,涵盖了全栈开发的各个方面。通过这个项目,开发者可以掌握前后端分离的开发模式,为实际项目开发打下坚实基础。
Spring Boot + Vue.js 全栈开发实战:从零搭建企业级博客系统
https://www.zztzz.com.cn/posts/67/ 部分信息可能已经过时









