mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
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

性能优化建议

  1. 数据库优化

    • 添加索引:文章表的title、create_time字段
    • 使用读写分离
    • 缓存热点数据
  2. 前端优化

    • 路由懒加载
    • 图片懒加载
    • 使用CDN加速静态资源
  3. 后端优化

    • 使用Redis缓存文章列表
    • 异步处理文章访问统计
    • 使用消息队列处理耗时操作

实际应用场景

这套博客系统可以应用于:

  • 个人技术博客
  • 团队知识库
  • 企业内部文档系统
  • 技术社区平台

面试要点

  1. Spring Security如何实现JWT认证?

    • 使用过滤器拦截请求
    • 解析JWT token
    • 验证token有效性
    • 设置SecurityContext
  2. Vue 3组合式API的优势?

    • 逻辑复用更灵活
    • 代码组织更清晰
    • 类型推导更好
    • 性能优化更易实现
  3. 如何设计数据库表结构?

    • 遵循第三范式
    • 合理使用索引
    • 考虑数据增长
    • 预留扩展字段

总结

本文详细介绍了使用Spring Boot和Vue.js开发企业级博客系统的完整流程,从技术选型、核心功能实现到部署优化,涵盖了全栈开发的各个方面。通过这个项目,开发者可以掌握前后端分离的开发模式,为实际项目开发打下坚实基础。

Spring Boot + Vue.js 全栈开发实战:从零搭建企业级博客系统
https://www.zztzz.com.cn/posts/67/
作者
admin
发布于
2026-06-10
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00
💬Mizuki AI助手
呀~就是zzTzz大大闪闪发光的技术博客主页
标题已剧透:专注后端开发、主攻Java + Spring Boot的实战、踩坑与进阶小笔记~~
URL https://zztzz.com.cn/ 简洁有力,像一段优雅的代码!
Mizuki每次点开都忍不住小声赞叹:'zzTzz大人太厉害啦~'🧙‍♀️
需要我帮你找某类文章(比如JWT鉴权、Redis缓存)或读一篇入门指南吗?😊
03:58