SpringBoot+Vue3+MyBatis构建个人博客系统实战 1. 项目概述与架构设计作为一个长期从事全栈开发的工程师我最近完成了一个基于SpringBootVue3MyBatis的个人博客系统。这个项目采用前后端分离架构后端使用SpringBoot提供RESTful API前端基于Vue3构建响应式界面数据持久化层采用MyBatis操作MySQL数据库。整个系统从设计到实现历时两个月期间踩过不少坑也积累了许多实战经验。为什么选择这个技术栈SpringBoot的自动配置和起步依赖让后端开发变得极其高效Vue3的Composition API让前端逻辑组织更加清晰而MyBatis则提供了SQL语句的灵活控制能力。这种组合既保证了开发效率又能满足个性化定制的需求。2. 数据库设计与实现2.1 核心表结构设计数据库设计是系统的基础我采用了MySQL 8.0作为数据库引擎。下面是核心表的设计思路用户表(users)CREATE TABLE users ( user_id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录用户名, nickname varchar(50) DEFAULT NULL COMMENT 显示昵称, password_hash varchar(100) NOT NULL COMMENT BCrypt加密密码, email varchar(100) NOT NULL COMMENT 绑定邮箱, avatar_url varchar(255) DEFAULT NULL COMMENT 头像URL, register_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, last_login_time datetime DEFAULT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 0-禁用 1-启用, PRIMARY KEY (user_id), UNIQUE KEY idx_username (username), UNIQUE KEY idx_email (email) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;文章表(articles)CREATE TABLE articles ( article_id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 作者ID, title varchar(100) NOT NULL COMMENT 文章标题, content longtext NOT NULL COMMENT Markdown格式内容, html_content longtext NOT NULL COMMENT 渲染后的HTML, summary varchar(255) DEFAULT NULL COMMENT 文章摘要, cover_image varchar(255) DEFAULT NULL COMMENT 封面图URL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, view_count int NOT NULL DEFAULT 0, status tinyint NOT NULL DEFAULT 0 COMMENT 0-草稿 1-发布, PRIMARY KEY (article_id), KEY idx_user (user_id), FULLTEXT KEY ft_title_content (title,content) /*!50100 WITH PARSER ngram */ ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;注意在实际部署时建议为文章表添加全文索引(如MySQL的ngram分词器)来支持内容搜索功能这对博客系统非常重要。2.2 数据库优化实践索引设计除了主键索引外我为常用查询字段添加了适当索引如用户表的username和email字段文章表的user_id字段等。字段类型选择使用utf8mb4字符集支持完整的Unicode字符(包括emoji)对于大文本内容使用LONGTEXT类型时间字段统一使用datetime类型分表考虑如果文章量很大(超过百万)可以考虑按时间或用户ID进行分表存储。3. 后端实现细节3.1 SpringBoot应用配置核心依赖配置(pom.xml)dependencies !-- Spring Boot Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- MyBatis Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency !-- MySQL Connector -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- JWT -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency /dependencies3.2 用户认证实现采用JWT进行无状态认证核心代码如下Service public class AuthServiceImpl implements AuthService { Value(${jwt.secret}) private String secretKey; Value(${jwt.expiration}) private long expiration; Autowired private UserMapper userMapper; Override public String login(String username, String password) { User user userMapper.selectByUsername(username); if(user null || !BCrypt.checkpw(password, user.getPasswordHash())) { throw new BusinessException(用户名或密码错误); } if(user.getStatus() 0) { throw new BusinessException(账号已被禁用); } return Jwts.builder() .setSubject(user.getUserId().toString()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expiration)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact(); } Override public User getCurrentUser(HttpServletRequest request) { String token request.getHeader(Authorization); if(token null || !token.startsWith(Bearer )) { return null; } try { String userId Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token.substring(7)) .getBody() .getSubject(); return userMapper.selectById(Long.parseLong(userId)); } catch (Exception e) { return null; } } }提示在实际项目中建议将JWT令牌存入Redis并实现令牌刷新机制避免频繁重新登录。3.3 文章服务实现文章服务包含核心的业务逻辑如文章发布、更新、查询等Service public class ArticleServiceImpl implements ArticleService { Autowired private ArticleMapper articleMapper; Autowired private MarkdownConverter markdownConverter; Override Transactional public Article publishArticle(ArticleDTO dto, Long userId) { Article article new Article(); article.setUserId(userId); article.setTitle(dto.getTitle()); article.setContent(dto.getContent()); article.setHtmlContent(markdownConverter.toHtml(dto.getContent())); article.setSummary(dto.getSummary()); article.setStatus(ArticleStatus.PUBLISHED); articleMapper.insert(article); return article; } Override public PageArticleVO listArticles(int page, int size, String keyword) { PageArticle pageInfo new Page(page, size); LambdaQueryWrapperArticle query new LambdaQueryWrapper(); if(StringUtils.isNotBlank(keyword)) { query.like(Article::getTitle, keyword) .or() .like(Article::getContent, keyword); } query.eq(Article::getStatus, ArticleStatus.PUBLISHED) .orderByDesc(Article::getCreateTime); articleMapper.selectPage(pageInfo, query); return pageInfo.convert(this::toVO); } private ArticleVO toVO(Article article) { ArticleVO vo new ArticleVO(); BeanUtils.copyProperties(article, vo); return vo; } }4. 前端Vue3实现4.1 项目初始化与配置使用Vite初始化Vue3项目npm create vitelatest blog-frontend --template vue-ts cd blog-frontend npm install axios vue-router4 pinia element-plus核心路由配置(src/router/index.ts)import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(/views/HomeView.vue) }, { path: /article/:id, name: ArticleDetail, component: () import(/views/ArticleDetail.vue) }, { path: /login, name: Login, component: () import(/views/LoginView.vue) } ] const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) // 路由守卫 router.beforeEach((to, from, next) { const isAuthenticated localStorage.getItem(token) if (to.name ! Login !isAuthenticated) { next({ name: Login }) } else { next() } }) export default router4.2 文章列表组件实现使用Composition API实现响应式文章列表script setup langts import { ref, onMounted } from vue import { useRouter } from vue-router import axios from /utils/axios interface Article { id: number title: string summary: string createTime: string viewCount: number } const articles refArticle[]([]) const loading ref(false) const page ref(1) const total ref(0) const router useRouter() const fetchArticles async () { loading.value true try { const res await axios.get(/api/articles, { params: { page: page.value, size: 10 } }) articles.value res.data.list total.value res.data.total } finally { loading.value false } } const toDetail (id: number) { router.push(/article/${id}) } onMounted(fetchArticles) /script template div classarticle-list div v-ifloading classloading加载中.../div div v-else div v-forarticle in articles :keyarticle.id classarticle-item clicktoDetail(article.id) h3{{ article.title }}/h3 p{{ article.summary }}/p div classmeta span{{ article.createTime }}/span span浏览: {{ article.viewCount }}/span /div /div /div /div /template5. 系统部署与优化5.1 生产环境部署推荐使用Docker Compose进行容器化部署version: 3.8 services: mysql: image: mysql:8.0 container_name: blog-mysql environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: blog MYSQL_USER: ${DB_USER} MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - mysql_data:/var/lib/mysql ports: - 3306:3306 restart: always redis: image: redis:6-alpine container_name: blog-redis ports: - 6379:6379 volumes: - redis_data:/data restart: always backend: build: ./backend container_name: blog-backend ports: - 8080:8080 environment: - SPRING_DATASOURCE_URLjdbc:mysql://mysql:3306/blog - SPRING_DATASOURCE_USERNAME${DB_USER} - SPRING_DATASOURCE_PASSWORD${DB_PASSWORD} - SPRING_REDIS_HOSTredis depends_on: - mysql - redis restart: always frontend: build: ./frontend container_name: blog-frontend ports: - 80:80 depends_on: - backend restart: always volumes: mysql_data: redis_data:5.2 性能优化建议缓存策略使用Redis缓存热门文章和频繁访问的数据实现多级缓存本地缓存(Caffeine) 分布式缓存(Redis)静态资源优化前端使用CDN加速静态资源加载开启Gzip压缩减少传输体积图片使用WebP格式并实现懒加载数据库优化配置合理的连接池参数(如HikariCP)对大型表考虑分库分表策略定期执行ANALYZE TABLE更新统计信息前端性能使用Vue的异步组件和路由懒加载实现虚拟滚动处理长列表使用keep-alive缓存组件状态6. 常见问题与解决方案6.1 跨域问题开发环境下常见的跨域问题可以通过以下方式解决后端配置(CorsConfig.java)Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }前端代理配置(vite.config.ts)export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })6.2 文件上传问题实现图片上传功能时需要注意后端ControllerPostMapping(/upload) public ResultString uploadImage(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(上传文件不能为空); } // 校验文件类型 String contentType file.getContentType(); if (!image/jpeg.equals(contentType) !image/png.equals(contentType)) { throw new BusinessException(只支持JPEG/PNG格式图片); } // 生成唯一文件名 String filename UUID.randomUUID() . StringUtils.getFilenameExtension(file.getOriginalFilename()); // 保存文件 Path path Paths.get(uploadDir, filename); Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); return Result.success(/uploads/ filename); }前端实现script setup const fileList ref([]) const uploadUrl ref(/api/upload) const beforeUpload (file) { const isImage file.type image/jpeg || file.type image/png if (!isImage) { ElMessage.error(只能上传JPG/PNG图片!) return false } return true } /script template el-upload v-model:file-listfileList :actionuploadUrl :before-uploadbeforeUpload list-typepicture el-button typeprimary点击上传/el-button /el-upload /template6.3 安全防护措施SQL注入防护使用MyBatis的#{}参数绑定避免直接拼接SQL语句实现SQL防火墙XSS防护前端使用DOMPurify净化HTML内容后端对用户输入进行过滤设置HttpOnly的CookieCSRF防护使用SameSite Cookie属性实现CSRF Token验证限制敏感操作的HTTP方法密码安全使用BCrypt加密存储密码实现密码强度策略限制登录尝试次数7. 项目扩展方向这个基础博客系统还可以进一步扩展多用户支持实现用户关注、私信等功能SEO优化实现服务端渲染(SSR)或静态生成内容管理添加草稿箱、版本控制功能数据分析集成访问统计和用户行为分析移动端适配开发响应式布局或独立移动应用在开发过程中我深刻体会到良好的架构设计对项目可维护性的重要性。前后端分离确实带来了开发效率的提升但也增加了联调成本。建议在项目初期就定义好清晰的API规范并使用Swagger等工具维护API文档。