ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3个人博客管理系统开发实战教程

SpringBoot+Vue3个人博客管理系统开发实战教程 毕设和简历项目别再写“单表增删改查”了我用 SpringBoot Vue3 搭了一个完整的个人博客管理系统做过毕业设计或者准备校招简历的人大概率都遇到过同一个两难太简单的项目没有表达空间做完自己也心虚太复杂的项目时间不够光是环境配置就能卡好几天。个人博客管理系统正好卡在中间——业务边界清晰、功能链路完整、从数据库设计到接口开发再到前端页面能覆盖主流 Web 开发的大部分环节。更重要的是如果用 SpringBoot Vue3 做前后端分离架构这个项目在简历上写出来面试官不会觉得是“玩具项目”因为它包含了几道真正会被追问的关卡JWT 认证怎么做、跨域怎么解决、分页查询怎么设计、前端路由守卫怎么控制访问权限。这篇文章要带你做的不是零散 demo而是一个可以直接跑、字段完整、代码可复制的个人博客管理系统。文章会从数据库设计开始逐步拆解后端接口、前端页面、联调验证、常见踩坑和工程规范。即使你现在还没有完整跑通过前后端分离项目照着做一遍整个技术链路就会清晰很多。先给一个明确判断这类项目最大的价值不是“会写增删改查”而是帮你建立前后端分离项目从数据表到 REST 接口、再到页面渲染的全局认知。本文所有代码示例都来自真实可运行的写法你只需要准备好 IDE、数据库和 Node 环境就行。1. 为什么要用 SpringBoot Vue3 做博客系统很多同学会问博客系统不是随便找个开源程序就能用吗为什么还要自己写如果你目标是“快速拥有一个博客”开源程序确实够用。但如果你目标是“完成一个能放进简历、能讲清楚技术细节的实战项目”自己从零搭建一遍的意义完全不同。面试官看简历时会关心一个问题这个项目你动手做到了哪个层面。如果你能把表结构、接口设计、登录状态存续、跨域配置、部署方式全部讲明白项目含金量自然就上去了。前后端分离是这个项目的关键架构选择。传统单体项目里后端渲染模板页面数据和页面耦合在一起每个页面改动都可能牵连后端逻辑。前后端分离之后后端只负责输出 JSON 数据前端单独负责页面渲染和用户交互两边可以并行开发也可以独立部署。技术选型上SpringBoot 目前仍然是国内企业级项目里使用率最高的 Java 框架之一。它用约定优于配置的方式解决了 Spring 早期大量 XML 配置的痛点让开发者能把注意力放在业务逻辑上。Vue3 则带来了 Composition API、更高效的响应式系统和更好的 TypeScript 支持加上 Vite 的极速冷启动开发体验比 Vue2 Webpack 时代提升了一个档次。从就业角度看SpringBoot Vue3 是当前国内中小型公司招聘中高频出现的组合。做完这个项目你既熟悉了后端接口开发也练了前端组件化开发这份经验在找实习、参加秋招时都能直接转化为面试素材。需要强调的是版本选择上不必过度纠结。网上搜到 SpringBoot 版本太高的报错多数是 JDK 和依赖版本不匹配导致。本文示例以 Java 8 Spring Boot 2.x 为基准展开如果你用 JDK 17 Spring Boot 3.x遇到依赖命名变化时按官方迁移文档调整即可核心思路完全相同。2. 个人博客管理系统的技术栈与功能规划开始写代码之前先把技术栈和功能范围列清楚。贪多嚼不烂一个适合学习和展示的博客管理系统功能边界应该是“完整但不庞杂”。2.1 技术栈总览分层技术选型作用说明前端框架Vue 3 Vite页面渲染、组件化开发、前端工程化UI 组件库Element Plus表格、表单、弹窗、消息提示等现成组件状态管理Pinia管理用户登录状态、全局配置路由Vue Router 4页面路由和导航守卫HTTP 请求Axios统一封装接口请求和响应拦截后端框架Spring Boot提供 REST API 服务ORMMyBatis-Plus简化数据库操作自带分页和逻辑删除数据库MySQL 8存储用户、文章、分类、标签、评论数据认证方案JWT BCrypt登录签发 token密码加密存储接口文档Springfox/springdoc自动生成 Swagger 接口文档需要说明的是MyBatis-Plus 并不是 SpringBoot 官方默认集成但它在国内项目中使用非常普遍。它最大的贡献是让你不用手写大量单表 CRUD 的 SQL同时把分页插件和逻辑删除做成开箱即用的能力非常适合做这类管理系统。2.2 功能模块拆解在开发之前先确定系统必须具备的功能模块再按模块拆解成一张可执行的任务清单用户模块注册、登录、退出登录、获取当前用户信息。文章模块文章发布、编辑、删除、分页查询、详情查看、浏览数更新。分类模块分类的增删改查文章可以选择所属分类。标签模块标签管理支持一篇文章多个标签。评论模块评论发表、评论列表查询、评论删除后台管理。后台管理页面文章管理表格、编辑页、分类管理页、登录页、布局框架。前台展示页面博客首页文章列表、文章详情页。这个范围既能体现增删改查基本功又能覆盖真实项目的典型场景。做的时候按“数据表 → 后端接口 → 前端页面 → 联调”的顺序推进每一步都有明确的交付物不会做到一半卡住。3. 数据库设计与初始化脚本数据库是项目的根基。很多新手习惯上来就写 Entity 类然后靠框架自动建表结果字段漏加、类型不对回头改起来非常痛苦。更稳妥的顺序是先设计表和字段关系再写初始化 SQL。3.1 核心表设计个人博客系统需要五张核心表和一张关联表表名说明sys_user用户表存储登录账号和密码category分类表文章所属分类article文章表存储标题、内容、封面、状态等tag标签表article_tag文章-标签关联表comment评论表表关系上用户和文章是一对多分类和文章是一对多文章和标签是多对多文章和评论是一对多。3.2 建库建表 SQL-- 文件路径sql/init.sql CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE blog; -- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除 0-未删除 1-已删除 ) ENGINEInnoDB COMMENT用户表; -- 分类表 CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL UNIQUE COMMENT 分类名称, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 ) ENGINEInnoDB COMMENT文章分类表; -- 文章表 CREATE TABLE article ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 文章ID, user_id BIGINT NOT NULL COMMENT 作者ID, category_id BIGINT DEFAULT NULL COMMENT 分类ID, title VARCHAR(200) NOT NULL COMMENT 标题, summary VARCHAR(500) DEFAULT NULL COMMENT 摘要, content LONGTEXT COMMENT Markdown内容, cover_image VARCHAR(255) DEFAULT NULL COMMENT 封面图, status TINYINT DEFAULT 1 COMMENT 状态 0-草稿 1-已发布, view_count INT DEFAULT 0 COMMENT 浏览数, like_count INT DEFAULT 0 COMMENT 点赞数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0, KEY idx_user_id (user_id), KEY idx_category_id (category_id), CONSTRAINT fk_article_user FOREIGN KEY (user_id) REFERENCES sys_user (id) ) ENGINEInnoDB COMMENT文章表; -- 标签表 CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 标签ID, name VARCHAR(50) NOT NULL UNIQUE COMMENT 标签名称, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 ) ENGINEInnoDB COMMENT标签表; -- 文章标签关联表 CREATE TABLE article_tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, article_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, UNIQUE KEY uk_article_tag (article_id, tag_id) ) ENGINEInnoDB COMMENT文章标签关联表; -- 评论表 CREATE TABLE comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, article_id BIGINT NOT NULL COMMENT 文章ID, user_id BIGINT NOT NULL COMMENT 评论用户ID, parent_id BIGINT DEFAULT NULL COMMENT 父评论ID0表示一级评论, content VARCHAR(1000) NOT NULL COMMENT 评论内容, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0, KEY idx_article_id (article_id) ) ENGINEInnoDB COMMENT评论表;这里有几个设计细节值得注意密码字段使用 VARCHAR(100)因为 BCrypt 加密后的字符串超过 60 个字符如果建表时只留 32 位后面插入必然报错。字符集选择 utf8mb4 而不是 utf8是为了能正常存储表情符号和生僻字这在博客评论场景很常见。关联表使用联合唯一索引防止同一篇文章重复添加同一个标签。deleted 字段是逻辑删除标记配合 MyBatis-Plus 的逻辑删除能力可以在不真正删数据的情况下实现“删除”效果。建完表之后顺手插入一条测试账号和几个分类数据方便后面联调-- 初始数据密码是 123456 的 BCrypt 加密结果 INSERT INTO sys_user (username, password, nickname) VALUES (admin, $2a$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPn.mvZKW, 管理员); INSERT INTO category (name) VALUES (Java), (前端), (数据库), (随笔);4. SpringBoot 后端环境搭建与基础配置4.1 使用 IDEA 创建项目推荐使用 IDEA 的 Spring Initializr 创建 SpringBoot 项目也可以到 Spring 官网下载初始工程后导入。创建时勾选Spring WebMySQL DriverLombok项目创建完成后再手动加入以下依赖!-- pom.xml 中追加 -- !-- MyBatis-Plus 自动配置 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- JWT 工具库 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency !-- 密码加密只引入 Spring Security Crypto 模块不引入完整 Spring Security -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId version5.7.3/version /dependency如果使用 JDK 9 以上版本jjwt 0.9.1 需要额外引入 JAXB 依赖否则运行时会抛 ClassNotFoundException。遇到这个问题时可以换成 jjwt 0.11.5 版本并调整 API或者直接引入依赖 jaxb-api。这里展示核心思路具体版本以实际项目能编译运行为准。4.2 配置 application.yml# 文件路径src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/blog?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB 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: 0map-underscore-to-camel-case 开启后数据库字段 create_time 可以自动映射到 Java 实体类属性 createTime省去大量手写 SQL 映射。log-impl 配置为 StdOutImpl 后控制台会打印 SQL 日志排错时非常有用。logic-delete-field 配合逻辑删除配置执行 delete 方法时 MyBatis-Plus 会自动改为 update 语句。5. 后端核心代码实现统一返回、实体、登录认证与文章接口5.1 统一返回结果类前后端分离项目中接口返回格式必须统一否则前端每个页面都要做特殊处理。这里定义一个通用的 Result 类// 文件路径src/main/java/com/example/blog/common/Result.java package com.example.blog.common; import lombok.Data; Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }5.2 文章实体类在 MyBatis-Plus 中使用注解声明表名、主键类型和逻辑删除字段// 文件路径src/main/java/com/example/blog/entity/Article.java package com.example.blog.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data TableName(article) public class Article { TableId(type IdType.AUTO) private Long id; private Long userId; private Long categoryId; private String title; private String summary; private String content; private String coverImage; private Integer status; private Integer viewCount; private Integer likeCount; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }需要说明的是 createTime 和 updateTime 的填充有两种方式一种依赖数据库默认值和 ON UPDATE另一种通过 MyBatis-Plus 的自动填充处理器。两种都可以推荐在数据库层做兜底代码里用 TableField 声明填充策略即可。如果不想引入额外逻辑直接在 SQL 里让数据库自动填充也不会影响功能。5.3 JWT 登录认证博客后台需要登录才能发文章这里采用 JWT 方案用户登录成功后签发 token前端每次请求在请求头携带 token后端拦截器校验 token 并解析出当前用户。先看一下登录接口的核心逻辑// 文件路径src/main/java/com/example/blog/service/impl/AuthServiceImpl.java关键代码 Override public String login(LoginDTO loginDTO) { // 1. 根据用户名查用户 LambdaQueryWrapperSysUser wrapper new LambdaQueryWrapper(); wrapper.eq(SysUser::getUsername, loginDTO.getUsername()); SysUser user userMapper.selectOne(wrapper); // 2. 校验用户是否存在、密码是否正确 if (user null || !BCryptPasswordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { throw new RuntimeException(用户名或密码错误); } // 3. 签发 JWT return JwtUtil.createToken(user.getId(), user.getUsername()); }实际项目中推荐把 BCryptPasswordEncoder 声明为一个 Spring Bean通过构造器或 Resource 注入使用。密码校验的细节在生产系统里还要考虑防止暴力破解、登录失败次数限制等问题这里先不过度展开。5.4 文章增删改查接口文章接口是系统的核心。Controller 层面保持轻量只做参数接收和结果包装业务逻辑放在 Service 层// 文件路径src/main/java/com/example/blog/controller/ArticleController.java package com.example.blog.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.blog.common.Result; import com.example.blog.entity.Article; import com.example.blog.service.ArticleService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; RestController RequestMapping(/api/articles) public class ArticleController { Resource private ArticleService articleService; /** * 分页查询文章列表 */ GetMapping public ResultPageArticle page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long categoryId) { PageArticle page new Page(pageNum, pageSize); LambdaQueryWrapperArticle wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Article::getCategoryId, categoryId) .eq(Article::getStatus, 1) .orderByDesc(Article::getCreateTime); return Result.success(articleService.page(page, wrapper)); } /** * 文章详情 */ GetMapping(/{id}) public ResultArticle detail(PathVariable Long id) { Article article articleService.getById(id); if (article null) { return Result.error(文章不存在); } return Result.success(article); } /** * 发布文章 */ PostMapping public ResultVoid save(RequestBody Article article) { article.setViewCount(0); article.setLikeCount(0); article.setStatus(1); articleService.save(article); return Result.success(null); } /** * 更新文章 */ PutMapping(/{id}) public ResultVoid update(PathVariable Long id, RequestBody Article article) { article.setId(id); articleService.updateById(article); return Result.success(null); } /** * 删除文章逻辑删除 */ DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { articleService.removeById(id); return Result.success(null); } }这里有几个值得说的细节RequestParam(defaultValue 1) 给分页参数提供了默认值前端不传页码时接口也不会报错。LambdaQueryWrapper 的条件构造器中第一个参数是布尔表达式条件不成立时整个条件自动跳过这种写法简洁又安全。删除接口调用 removeById 后因为配置了逻辑删除MyBatis-Plus 会执行 UPDATE article SET deleted 1 而不是 DELETE数据并没有真正丢失。后面如果要实现浏览量递增可以在 Service 中执行 update 语句UPDATE article SET view_count view_count 1 WHERE id ?这样避免并发修改时的覆盖问题。5.5 分页插件配置MyBatis-Plus 的分页功能需要注册分页插件否则 Page 参数不会生效// 文件路径src/main/java/com/example/blog/config/MybatisPlusConfig.java package com.example.blog.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(100L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }5.6 跨域配置前端开发服务默认运行在 http://localhost:5173后端接口运行在 http://localhost:8080两个端口不同就存在跨域问题。开发阶段推荐用 Vite 的 proxy 代理请求同时也需要在后端配置跨域兜底方便直接测试接口// 文件路径src/main/java/com/example/blog/config/CorsConfig.java package com.example.blog.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里要特别提醒allowCredentials(true) 时不能使用 allowedOrigins()必须使用 allowedOriginPatterns()否则浏览器会拒绝带 Cookie 的跨域请求。6. Vue3 前端工程搭建与核心封装后端接口就绪后开始搭建 Vue3 前端。前端需要包含登录页、后台布局、文章列表页、文章编辑页、分类管理页。6.1 使用 Vite 创建 Vue3 项目# 创建项目 npm create vitelatest blog-admin -- --template vue # 进入项目目录 cd blog-admin # 安装依赖 npm install # 安装路由、状态管理、HTTP 请求、UI 组件库 npm install vue-router4 pinia axios element-plus element-plus/icons-vue如果你在安装 Element Plus 之后发现样式未生效需要再引入样式文件// 文件路径src/main.js import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount(#app)6.2 配置 Vite 代理开发环境下通过代理解决跨域问题浏览器请求 /api 时由 Vite 转发到后端 8080 端口// 文件路径vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })6.3 封装 Axios 请求统一处理请求头 token 注入、响应状态码判断和 401 跳转// 文件路径src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理返回结构和错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录已过期请重新登录) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default request6.4 路由与导航守卫// 文件路径src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /, component: () import(../layout/AdminLayout.vue), redirect: /articles, children: [ { path: articles, component: () import(../views/article/ArticleList.vue) }, { path: articles/edit, component: () import(../views/article/ArticleEdit.vue) }, { path: articles/edit/:id, component: () import(../views/article/ArticleEdit.vue) }, { path: categories, component: () import(../views/category/CategoryList.vue) } ] } ] const router createRouter({ history: createWebHistory(), routes }) // 全局前置守卫未登录不能访问后台页面 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } }) export default router路由守卫是前端权限控制的关键一环。它不能替代后端权限校验但能在用户体验层面避免未登录用户进入管理页面后端接口层仍然需要校验 token 有效性。6.5 文章列表页使用 Element Plus 的 el-table 和 el-pagination 实现列表和分页!-- 文件路径src/views/article/ArticleList.vue -- template div el-card div styledisplay: flex; justify-content: space-between; align-items: center h2 stylemargin: 0文章管理/h2 el-button typeprimary click$router.push(/articles/edit) 写文章 /el-button /div el-table :datalist border stylemargin-top: 16px el-table-column propid labelID width70 / el-table-column proptitle label标题 min-width200 / el-table-column propcategoryId label分类ID width100 / el-table-column propviewCount label浏览量 width90 / el-table-column proplikeCount label点赞数 width90 / el-table-column propcreateTime label发布时间 width180 / el-table-column label操作 width160 fixedright template #default{ row } el-button link typeprimary click$router.push(/articles/edit/${row.id}) 编辑 /el-button el-button link typedanger clickhandleDelete(row) 删除 /el-button /template /el-table-column /el-table el-pagination v-model:current-pagepageNum :page-sizepageSize :totaltotal layoutprev, pager, next stylemargin-top: 16px; justify-content: flex-end current-changeloadList / /el-card /div /template script setup import { ref, onMounted } from vue import { ElMessage, ElMessageBox } from element-plus import request from ../../utils/request const list ref([]) const pageNum ref(1) const pageSize ref(10) const total ref(0) const loadList async () { const data await request.get(/articles, { params: { pageNum: pageNum.value, pageSize: pageSize.value } }) list.value data.records total.value data.total } const handleDelete async (row) { await ElMessageBox.confirm(确定删除《${row.title}》吗, 提示, { type: warning }) await request.delete(/articles/${row.id}) ElMessage.success(删除成功) loadList() } onMounted(() { loadList() }) /script这个页面使用 script setup 语法组合式 API 让逻辑更集中。ref 声明的 list、pageNum、total 是响应式数据onMounted 进入页面时自动加载第一页数据。6.6 文章编辑页编辑页使用表单和 Markdown 编辑器。如果不想引入额外的编辑器组件也可以直接用 el-input typetextarea 存储 Markdown 文本但从展示效果和简历亮点角度考虑推荐集成 mavon-editor。!-- 文件路径src/views/article/ArticleEdit.vue 核心代码 -- template el-card el-form refformRef :modelform label-width80px el-form-item label标题 required el-input v-modelform.title placeholder请输入文章标题 / /el-form-item el-form-item label摘要 el-input v-modelform.summary typetextarea :rows3 placeholder请输入文章摘要 / /el-form-item el-form-item label分类 el-select v-modelform.categoryId placeholder选择分类 clearable el-option v-foritem in categoryList :keyitem.id :labelitem.name :valueitem.id / /el-select /el-form-item el-form-item label内容 required mavon-editor v-modelform.content styleheight: 500px / /el-form-item el-form-item el-button typeprimary clickhandleSubmit发布/el-button el-button click$router.back()返回/el-button /el-form-item /el-form /el-card /template script setup import { ref, onMounted } from vue import { useRoute, useRouter } from vue-router import { ElMessage } from element-plus import MavonEditor from mavon-editor import mavon-editor/dist/css/index.css import request from ../../utils/request const route useRoute() const router useRouter() const articleId route.params.id const form ref({ title: , summary: , categoryId: null, content: }) const categoryList ref([]) const loadCategory async () { const data await request.get(/categories) categoryList.value data } const loadArticle async () { if (!articleId) return const data await request.get(/articles/${articleId}) form.value data } const handleSubmit async () { if (!form.value.title || !form.value.content) { ElMessage.warning(标题和内容不能为空) return } if (articleId) { await request.put(/articles/${articleId}, form.value) ElMessage.success(文章已更新) } else { await request.post(/articles, form.value) ElMessage.success(文章发布成功) } router.push(/articles) } onMounted(() { loadCategory() loadArticle() }) /scriptmavon-editor 在 Vue3 项目中如果遇到版本兼容问题可以查找它对应的 Vue3 版本或使用其他 Markdown 编辑器替代这里重点是理解表单数据的提交方式。7. 运行验证与前后端联调代码写完后按以下步骤启动并进行验证。7.1 启动后端先确认 MySQL 服务已启动并且 init.sql 已经正确执行。然后在 IDEA 中运行 BlogApplication 主类。正常启动后控制台会输出以下关键日志Tomcat started on port(s): 8080 (http) Started BlogApplication in 5.2 seconds如果 8080 端口被占用修改 application.yml 中的 server.port 即可。用 curl 快速验证文章列表接口curl http://localhost:8080/api/articles?pageNum1pageSize10预期返回 JSON 结构{ code: 200, message: 操作成功, data: { records: [], total: 0, size: 10, current: 1, pages: 0 } }如果没有数据records 会是空数组这属于正常结果。先调用登录接口获取 token再调用新增文章接口就能看到数据被写入数据库。7.2 启动前端cd blog-admin npm run dev控制台输出 Vite 启动地址默认是 http://localhost:5173。浏览器打开后先访问登录页用 admin 账号和 123456 登录。登录成功后左侧菜单进入文章管理可以看到空列表点击“写文章”发布一条数据。刷新页面后文章列表就能查询到刚才发布的内容。7.3 验证逻辑删除在文章管理页面删除一条文章然后在 Navicat 或命令行中查看 article 表会发现该行数据仍然存在只是 deleted 字段变为了 1。这说明逻辑删除生效数据库没有物理删除数据。7.4 联调失败排查顺序联调时最常见的问题是前端请求 404 或 500。如果启动失败按以下顺序排查看浏览器 DevTools 的 Network 面板确认请求地址是否正确。看后端控制台日志是否有异常堆栈。确认数据库连接配置是否正确。确认前端代理是否生效有没有走 /api 前缀。8. 常见问题与排查思路问题现象可能原因排查方式解决方案后端启动失败端口被占用8080 被其他进程占用查看启动日志的端口冲突提示修改 server.port 或结束占用进程Access denied for user root数据库账号密码错误检查 application.yml 中 datasource 配置改成正确的账号密码确认 MySQL 用户有远程访问权限前端 npm install 报错Node 版本和依赖版本不匹配查看 npm 报错信息中的版本提示升级 Node 或锁定依赖版本登录接口返回 500密码加密方式不一致查看后端异常确认是否 NoSuchAlgorithmException引入 spring-security-crypto使用 BCryptPasswordEncoder前端请求跨域报错Vite 代理未配置或后端 CORS 未生效看 Network 面板请求是否经过 /api 代理开发环境用 Vite proxy生产环境用 Nginx 转发列表接口返回 records 为空数据表没有数据或逻辑删除状态不对查询数据库确认 deleted 字段状态插入测试数据确认查询条件是否带逻辑删除过滤文章内容无法保存字段长度超限或类型不支持查看后端日志中的 SQL 异常大文本字段使用 LONGTEXT 类型页面登录后仍然跳转 /loginlocalStorage token 未写入查看登录接口返回和存储代码登录成功后统一 localStorage.setItem(token, data)这里真正容易踩坑的地方是 SpringBoot 版本和 JDK 版本的兼容问题。如果你的项目报错信息中包含 javax 包找不到、ClassNotFound多半是 JDK 版本变化导致。可以统一把 javax.servlet 依赖改为 jakarta.servletSpring Boot 3.x 默认或者保持 JDK 8 Spring Boot 2.x 的组合不动。9. 最佳实践与工程建议项目跑通只是第一步。如果你想把这套代码真正做成简历上的亮点下面这些实践建议值得认真过一遍。9.1 接口设计层面统一使用 RESTful 风格资源名用复数动作交给 HTTP 方法表达GET 查询、POST 新增、PUT 修改、DELETE 删除。统一返回结构。前端只关心 code、message、data 三个字段业务状态和 HTTP 状态尽量分开表达。对于权限不足、参数错误等情况code 可以给出更细的业务错误码。分页接口不要在大表上使用深分页。数据量到几十万条后LIMIT 100000, 10 的查询性能会显著下降可以考虑游标分页或限制最大页码。9.2 安全层面密码必须加密存储BCrypt 是目前推荐的方案之一。绝对不要明文存密码这是面试官非常容易追问的点。token 在浏览器端的存储位置需要根据安全目标权衡。演示项目用 localStorage 方便但一旦出现 XSS 漏洞攻击者可以直接读取 token。更严苛的场景下推荐使用 httpOnly Cookie 配合 CSRF 防护。后端接口必须有权限校验不能只依赖前端隐藏按钮。如果接口没做校验用户直接 curl 调用就能删除任意文章这是非常大的安全漏洞。生产环境不要把数据库密码、JWT 密钥写在 application.yml 里推荐通过环境变量或配置中心注入。9.3 工程化层面包名按功能分层还是按业务模块分层小型项目按 controller/service/mapper 分层足够清晰项目变大后更适合按业务模块划分例如 article、user、comment 各自成包。前端组件公共代码要抽取。请求封装、分页组件、图片上传组件都可以复用不要在每个页面重复写一遍 axios 配置。日志要打关键节点。登录成功失败、文章发布、删除操作都建议记录日志并包含操作者信息。使用 git 管理代码每个功能点一次提交commit message 写清楚改动内容。这个习惯对毕设指导老师检查和面试代码仓库评估都很加分。9.4 部署层面后端代码可以打成 jar 包部署前端代码通过 npm run build 生成静态文件交给 Nginx 托管。生产环境由 Nginx 统一监听 80 端口前端静态资源直接返回/api 路径转发到后端服务。这是目前最主流的前后端分离部署方式也方便水平扩展后端实例。10. 总结与后续学习方向到这里一个基于 SpringBoot Vue3 的前后端分离个人博客管理系统就跑通了。你实际动手做过的内容包括数据库表设计、MyBatis-Plus 集成与分页、JWT 登录认证、统一异常返回、前端路由守卫、Axios 封装、Element Plus 列表页和编辑页开发、前后端跨域联调以及常见排错方式。这些能力不是只看文章就能获得的每一步都需要你亲手跑一遍遇到报错、定位问题、修复验证这才是项目经验的真正来源。项目跑通之后如果还想继续提升可以沿着这几个方向深入给文章列表加 Redis 缓存降低数据库压力用阿里云 OSS 或 MinIO 实现图片上传集成 Spring Security 做更细粒度的权限控制用 Docker 打包前后端环境实现一键部署给博客系统加 Elasticsearch 搜索。下一步建议是给项目补充完善的项目文档和接口文档然后把它放到 GitHub 上。面试前准备好一段五分钟的自我介绍围绕技术选型、表设计、认证方案、部署流程来讲。这套博客管理系统虽然不是超大项目但它足够展示你对前后端分离项目完整链路的理解对毕设答辩和求职面试来说都是性价比很高的一次投入。
返回列表