资讯动态

SpringBoot+Vue全栈博客系统:从数据库设计到部署上线实战

发布时间:2026/10/13 18:23:48 来源:尧图企业网站定制
1. 项目整体设计与技术选型思路1.1 技术栈为什么这样选做博客系统第一反应可能觉得无非是“文章评论”的CRUD难度不大。但真把需求摊开看会发现里面藏着不少值得认真对待的点用户要能注册登录、发文、传图、管理分类标签访客要能浏览、检索、评论后台要能统计和运营。如果选传统单体JSP或者纯前端静态页面开发时省事后期维护成本却高得吓人。所以我最终选了前后端分离的方案后端用SpringBoot前端用Vue数据层用MyBatis操作MySQL。这套组合在个人项目中特别合理——SpringBoot负责快速搭建接口Vue负责交互体验MyBatis让我对SQL有完全控制权MySQL则足够承载博客场景的数据量。SpringBoot的好处不用多说内嵌Tomcat、自动配置、起步依赖几个注解就能跑起一个Web应用。和SSHStrutsSpringHibernate时代相比XML配置大幅减少调试效率提升明显。Vue这边响应式数据和组件化开发让我能把页面拆成头部、侧边栏、文章列表、评论区等多个独立模块改一处不影响全局对个人项目来说维护起来轻松很多。这里插一句题外话标题里写的“管理系统”严格来说博客面向公开访问叫“个人博客系统”可能更准确。不过从开发角度“后台前台”双端设计确实符合管理系统思路其实没毛病。1.2 项目结构与分层设计项目采用Maven多模块逻辑划分实际上单模块也能运行但我在代码组织上严格区分层级这对我后面扩展功能帮助巨大。整体分为Controller层接收前端请求做参数校验调用ServiceService层业务逻辑处理事务控制比如发布文章时同时更新分类统计Mapper层MyBatis接口负责和数据库交互Entity层数据库实体映射DTO/VO层接口传输对象和视图对象避免直接暴露实体类打个生活化的比方Controller是餐厅前台Service是后厨Mapper是食材供应商。前台只管把客人需求转给后厨后厨决定怎么做菜供应商负责提供原料。每层职责单一出了问题定位也快。前端Vue这边按视图划分组件路由用Vue Router管理状态管理用Vuex或Pinia。请求统一封装在utils/request.js里通过Axios发送HTTP请求携带JWT令牌处理响应码。页面组件和路由懒加载结合首页静态资源加载速度也能优化。1.3 开发环境与准备工作我本地的开发环境是JDK 1.8、Maven 3.6、MySQL 5.7、Node.js 14以上。IDE用IntelliJ IDEA前端开发用VS Code两者兼顾Java和Vue的调试体验。注意SpringBoot 2.x版本和3.x版本差异很大如果参考网上旧教程建议统一用2.7.x版本避免javax到jakarta命名空间变化引发的依赖问题。这个问题我在开发中遇到过多次后面会展开说。数据库用Navicat做可视化操作但建表语句我保留了一份SQL脚本方便部署到服务器时直接执行。所有表名、字段名都采用下划线风格实体类属性用驼峰风格在MyBatis配置文件里开启驼峰映射省去很多手写ResultMap的功夫。1.4 功能需求拆解与范围界定个人博客麻雀虽小五脏俱全我在做需求拆解时把功能分为两类访客可见的前台功能和管理员专属的后台功能。前台包括首页文章列表、文章详情、分类归档、标签云、搜索、评论展示和评论提交、关于我页面。后台包括登录、文章管理增删改查、分类管理、标签管理、评论管理审核/删除、个人资料设置。在完整源码中我把每一个功能都做成了独立模块接口路径符合RESTful规范。比如GET /api/article/list分页查询文章列表GET /api/article/detail/{id}查询文章详情POST /api/article/save保存文章PUT /api/article/update更新文章DELETE /api/article/delete/{id}删除文章这样的接口划分清晰前后端联调时各干各的互不干扰。后面在部署上线、二次开发时也能快速定位需要改哪里。2. 数据库设计核心表结构与字段规划2.1 博客系统需要哪几张表个人博客的数据库设计最核心的几张表我认为是用户表、文章表、评论表、分类表、标签表再加上文章标签关联表。这种设计兼顾了数据查询效率和扩展性是个很成熟的方案。看表结构对比表名核心字段用途说明userid, username, password, nickname, avatar, role用户信息区分管理员和普通用户articleid, title, summary, content, cover_image, category_id, view_count, create_time文章主体内容分类外键关联categoryid, name, description文章分类支持侧边栏归档tagid, name标签独立存储article_tagarticle_id, tag_id多对多关联表commentid, article_id, user_id, content, parent_id, status评论支持楼中楼回复用户表和文章表是核心评论表通过article_id关联文章parent_id支持嵌套回复。这里parent_id的设计很关键为0表示一级评论不为0则是对某条评论的回复。2.2 关键字段设计思路文章表的内容字段用longtext类型不要用text。富文本编辑器比如编辑器里插入大段代码或长篇图文生成的内容动辄几千字甚至几万字text类型最大只有64KB一旦文章稍长就会导致保存失败。我在早期版本里踩过这个坑后来统一改成longtext问题彻底解决。文章封面图字段cover_image存储的是图片访问URL而不是图片二进制内容。图片文件上传到服务器本地目录或对象存储数据库中只记录路径。这个设计能极大减轻数据库压力也方便后续接入CDN。用户表的密码字段我存的是BCrypt加密后的哈希值不是明文。BCrypt是自适应哈希算法每次生成的结果不同安全性远高于MD5。登录校验时用BCryptPasswordEncoder的matches方法比对即可。评论表的status字段取值0或10表示待审核1表示已通过。这是为了防止垃圾评论在未经审核的情况下直接展示在页面上。管理员在后台能看到全部评论列表审核通过后状态自动更新。2.3 索引设计重点索引是查询性能的生命线。我在这几张表里添加了如下索引文章表的category_id索引分类页查询需要按分类过滤文章文章表的create_time索引文章列表默认按时间倒序评论表的article_id索引查询某篇文章下所有评论用户表的username唯一索引保证用户名不重复登录查询也更快复合索引不是越多越好我见过有人把所有查询字段都塞进索引里结果写入变慢、占用磁盘空间MySQL优化器反而没走索引。所以在个人博客场景下单列索引就够用了。2.4 SQL脚本编写要点完整源码中我附带了一份init.sql脚本包含建库、建表、基础数据插入语句。这里分享几个脚本编写细节第一指定字符集。建库语句要带上DEFAULT CHARACTER SET utf8mb4保证中文和Emoji都能正常存储。utf8mb4是utf8的超集推荐直接使用。第二明确主键自增。id BIGINT AUTO_INCREMENT PRIMARY KEY写在每张表里保证插入时无需手动指定主键。第三加时间戳字段。每张表都加上create_time和update_time并设置默认值为CURRENT_TIMESTAMP方便后续排查问题。3. 后端接口开发实战从登录到文章的完整链路3.1 JWT登录鉴权的具体实现用户登录后后端需要签发一个令牌前端每次请求都携带这个令牌后端校验通过才允许访问受保护的接口。我选用JWTJSON Web Token方案因为它无状态、跨域友好非常适合前后端分离架构。具体做法是引入jjwt依赖在pom.xml中添加。用户登录成功后用用户的id、用户名、角色这三样信息生成JWT签名密钥放在application.yml配置文件中。令牌有效时间我设置为24小时过期后前端会收到401响应码自动跳转重新登录。服务端写好拦截器继承HandlerInterceptorAdapter重写preHandle方法。对不需要登录就能访问的接口比如文章列表、文章详情放行对后台管理接口校验JWT。SpringBoot的WebMvcConfigurer里注册拦截器并指定拦截路径为/api/admin/**。提示JWT虽然无状态但存在密钥泄露问题。不要把密钥硬编码进代码里配置在环境变量或配置文件中并通过.gitignore排除这属于基础安全意识。3.2 文章发布与保存的Service层设计文章发布是博客系统的核心操作。前端编辑器提交的内容是HTML格式的富文本后端接收后做如下处理标题非空校验长度限制在100字符以内摘要为空时从正文内容中截取前200字符作为摘要分类ID必须存在否则抛出业务异常标签列表接收后先按名称查询不存在的标签自动创建文章插入成功后再向关联表批量插入文章标签关系这里有一个工作量不大但很值得做的优化文章保存和标签处理逻辑放在一个事务里保证数据一致性。我通过Transactional(rollbackFor Exception.class)注解实现任何一个环节抛出异常整条数据回滚。用生活化的方式理解就是“要么全做要么全不做”不会出现文章保存成功但标签没关联上的尴尬情况。文章浏览量view_count字段我在SQL层面直接用UPDATE article SET view_count view_count 1 WHERE id #{id}来增加避免先查询再更新的竞态问题。3.3 MyBatis的XML与注解混合使用MyBatis有两种书写SQL的方式注解和XML。个人项目里我推荐混合使用——简单查询用注解复杂SQL和动态SQL用XML。以文章列表的分页查询为例它本身需要根据关键字、分类ID、时间范围动态拼SQL写XML非常合适select idselectArticlePage resultTypecom.blog.entity.Article SELECT a.*, c.name AS categoryName FROM article a LEFT JOIN category c ON a.category_id c.id where if testkeyword ! null and keyword ! AND (a.title LIKE CONCAT(%, #{keyword}, %) OR a.summary LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND a.category_id #{categoryId} /if /where ORDER BY a.create_time DESC /select动态SQL的where标签会自动处理AND连接避免第一个条件出现多余AND的语法错误。这个细节看起来很基础但很多新手写动态SQL时最容易在这里翻车。关联查询里注意表名别名。多表联查时如果字段名重复比如article.id和category.id必须在SELECT中明确指出别名否则MyBatis自动映射的ResultMap会拿不到正确字段值返回的实体对象对应属性为null。3.4 文件图片上传接口实现博客系统绕不开图片上传。我实现的方案是前端通过Axios以multipart/form-data格式提交文件后端接收MultipartFile对象存储到指定磁盘目录。上传接口核心逻辑检查文件大小限制在5MB以内超过则抛出异常检查文件扩展名只允许jpg、png、gif、webp为了避免文件名冲突采用UUID生成新文件名保留原扩展名图片保存到本地/upload/yyyyMMdd/目录按日期分目录存放返回图片访问URL格式为/image/yyyyMMdd/uuid.jpg为了让图片可以通过URL访问SpringBoot里做静态资源映射。配置类实现WebMvcConfigurer重写addResourceHandlers方法将/image/**映射到本地磁盘路径Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/image/**) .addResourceHandler(file: uploadPath); }注意这里file:前缀必不可少否则Spring无法识别为文件系统路径。3.5 接口统一返回格式设计所有接口返回统一格式前端处理起来方便得多。我定义了一个Result类public class Result { private Integer code; private String message; private Object data; // 省略getter/setter }请求成功code 200data为实际数据业务失败code 500message为错误描述未认证code 401前端接收到后跳转登录页全局异常处理器用RestControllerAdvice注解实现业务异常、参数校验异常、未知异常分别捕获统一封装为Result返回。这样前端只需要判断code是否为200就能决定后续逻辑代码简洁且不容易出错。4. 前端Vue实现页面搭建与前后端联调4.1 Vue项目结构与路由规划前端工程用Vue CLI创建项目结构按模块划分src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # 状态管理 ├── views/ # 页面视图 ├── App.vue # 根组件 └── main.js # 入口文件路由设计分两块前台路由和后台路由。前台路由包含首页、文章详情、分类、标签、关于等页面后台路由包含登录、文章管理、评论管理等页面。/admin路由统一挂在名为Admin的父路由下子路由通过嵌套实现。后台页面需要一个登录态检查的路由守卫用户未登录时点击后台管理自动跳转登录页。4.2 Axios请求拦截与响应处理请求封装是前端联调的基础。我在utils/request.js中创建Axios实例设置baseURL为/api并通过拦截器统一处理请求拦截器从localStorage中取出JWT令牌设置到请求头Authorization字段。响应拦截器判断响应状态码如果为200直接返回data部分如果为401清除本地登录状态并跳转登录页。业务异常code500则弹出错误提示。这种统一处理的优势很明显每个页面方法中只需关注成功的数据处理不需要重复写错误判断逻辑。4.3 首页文章列表与分页加载首页是访客最直观的页面我按经典博客布局实现顶部导航栏、中间内容区、右侧侧边栏。内容区展示文章卡片每张卡片包含封面图、标题、摘要、分类、发布时间和浏览量。分页组件用Element UI的el-pagination组件传入currentPage、pageSize、total三个属性。切换页码时触发handlePageChange方法重新请求接口。为了提升用户体验我在查询参数中加入了pageNum和pageSize后端通过PageHelper插件实现物理分页。注意PageHelper的startPage方法必须紧跟着第一条查询语句中间不能插入其他数据库操作。这个顺序问题我在实际开发中踩过坑导致分页数据不对后来阅读源码才明白是ThreadLocal机制在起作用。4.4 富文本编辑器集成与XSS防护文章编辑页集成富文本编辑器我选用的是轻量级的编辑器组件。发布文章时编辑器输出的HTML内容需要做转义处理防止XSS攻击。XSS攻击通俗说就是用户在内容里嵌入恶意脚本比如scriptalert(xss)/script其他访客浏览时脚本被执行。后端处理方案是引入Jsoup库对HTML内容进行白名单过滤只保留p、h1、h2、img、br、strong等安全标签其余全部清理干净。4.5 跨域问题与解决方案前后端分离开发中跨域是个绕不开的坎。前端运行在localhost:8080后端接口在localhost:9090浏览器的同源策略会拦截请求。我的解决方案是在后端配置全局跨域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); } }开发环境也可以通过在Vue项目的vue.config.js中配置代理解决module.exports { devServer: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }注意两者不能同时生效否则会出现每次请求都携带OPTIONS预检请求影响接口响应速度。我的方案是开发环境用代理生产环境用Nginx反向代理后端不开启全局跨域。5. 项目部署上线与环境配置实战5.1 Maven打包与Java环境配置项目开发完成后的目标是上线。完整源码中我提供了一个详细的部署文档核心步骤是后端打包在项目根目录执行mvn clean package如果跳过测试则加-DskipTests参数。打包完成后target目录下生成blog-server.jar文件。服务器环境要求JDK 1.8及以上、MySQL 5.7及以上、Maven构建用、NginxWeb服务器。常见坑服务器如果是CentOS 7默认的JDK版本可能是OpenJDK 1.7版本不支持SpringBoot 2.x需要手动安装JDK。安装方式用官方rpm包或包管理器均可更建议用二进制tar包解压后配置软链接方便版本切换。5.2 Nginx反向代理与静态资源部署前端后台打包命令为npm run build生成dist目录。我把dist下的文件上传到服务器Nginx的web根目录比如/usr/share/nginx/html/blog/。Nginx配置两个关键点一是前端页面路由使用history模式时刷新页面会出现404需要配置try_files $uri $uri/ /index.html;二是接口请求需要反向代理到后端的Java服务。server { listen 80; server_name your_domain.com; root /usr/share/nginx/html/blog; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /image/ { alias /var/blog/upload/; } }图片路径单独配置alias映射到上传目录避免把上传文件放在Nginx web根目录下便于管理和备份。5.3 MySQL初始化与远程连接服务器上创建数据库和导入SQLmysql -u root -p create database blog default character set utf8mb4; use blog; source /path/to/init.sql;如果后端服务在本地调试、需要连线上数据库通过Navicat建立SSH隧道连接。安全考虑服务器上的MySQL默认绑定127.0.0.1不允许远程直连这个设置我建议保留。13代之后我特意确认了后端application.yml中的数据库连接信息spring: datasource: url: jdbc:mysql://localhost:3306/blog?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_passwordserverTimezone不设置的话新版驱动会报日期时间区域错误这个是老生常谈的坑了。5.4 上线后的性能优化部署上线只是第一步性能优化才是拉开体验差距的关键。我做了三个层面的优化第一文章详情接口加Redis缓存。热点文章访问频繁每次都查数据库压力大。查询逻辑改为“先查Redis命中则直接返回未命中则查数据库并回填Redis设置过期时间10分钟”。第二静态资源加CDN。前端打包后的js、css文件体积不小域名解析后接入CDN各地区用户访问静态资源会快很多。后端接口动态请求不受影响。第三图片上传自动化压缩。用户上传原图可能几兆大小我用Java的图片处理库做了等比压缩宽超过1200px则压缩到1200px质量参数设为0.8清晰度影响小体积下降明显。6. 常见问题排查与开发经验记录6.1 依赖版本冲突问题SpringBoot项目最大的依赖坑发生在引入第三方组件时版本冲突频发。我遇到过com.fasterxml.jackson依赖冲突导致JSON序列化失败前端拿到的数据一直为空。排查方法很简单执行mvn dependency:tree查看依赖树找出不同版本冲突的位置在pom.xml中使用dependencyManagement统一管理版本。例如dependencyManagement dependencies dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.13.4/version /dependency /dependencies /dependencyManagement6.2 MyBatis返回结果为null的排查思路文章列表接口返回时前端发现createTime字段为空。排查后发现原因数据库字段是create_time实体属性是createTimeMyBatis未开启驼峰映射。解决方案是在application.yml中加入mybatis: configuration: map-underscore-to-camel-case: true这个配置加上后SQL查询出的列名下划线会自动映射到实体类驼峰属性不用每个字段手写resultMap。如果是多表关联查询可以在XML的resultMap中手动指定字段映射兜底保证数据正确。6.3 JWT令牌在拦截器中的校验拦截器校验JWT的核心逻辑Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { Result result Result.error(401, 未登录或token已过期); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(JSON.toJSONString(result)); return false; } try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token.replace(Bearer , )) .getBody(); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // token验证失败 ... return false; } }关键词是“OPTIONS请求直接放行”。浏览器跨域请求时会先发一个预检请求如果拦截器拦住了前端真正请求发不出去页面表现为一直转圈。6.4 富文本内容保存不完整的坑有次测试发现文章发布后详情页内容被截断了。定位后发现是数据库字段类型的问题正文字段在初始化脚本中被定义成varchar(255)这个长度根本不够存储大段富文本内容。修改为longtext后问题消失。这里提醒大家初始化脚本建表时正文和摘要字段类型要慎重选择。varchar(255)适合短文本tinytext限制255字节text限制64KBmediumtext限制16MBlongtext限制4GB按需使用。6.5 分页查询数据总数不准后台文章管理分页时发现总条数和真实数据不一致。原因PageHelper的startPage方法执行后如果紧接着执行了多条SQL查询分页信息会被第二次查询消费掉导致数据错乱。解决办法startPage()后只执行一条查询或者把查询结果封装到PageInfo对象中用PageInfo.getTotal()获取总数。最好统一把分页查询的返回类型定义为PageResult自定义对象隔离对PageHelper的依赖。7. 完整源码的使用方式与二次开发建议7.1 如何运行这份源码拿到源码后有四个步骤要完成导入数据库执行init.sql自动完成建库建表和基础数据配置后端修改application.yml中数据库密码、JWT密钥、上传路径启动后端mvn spring-boot:run或直接运行主类启动前端npm install安装依赖npm run serve启动开发服务整个流程走完打开浏览器访问http://localhost:8080就能看到博客前台首页。7.2 源码中的关键目录说明源码结构我刻意做了整理方便阅读src/main/java后端源码按包分好controller、service、mapper、entity、config、common、utilssrc/main/resources下是MyBatis映射文件和配置文件frontend目录存放Vue前端工程doc目录下是部署文档和init.sql初始化脚本建议拿到源码的读者不要急着删减代码先按我上面的结构通读一遍。从Controller层看接口入口再到Service层看业务逻辑最后到Mapper层看SQL语句整体脉络就清晰了。7.3 可以怎么扩展这个系统个人博客系统是个很好的练手项目扩展空间很大。我自己整理过几个扩展方向Redis缓存接入热文章、分类列表、评论数等场景优先加Redis缓存搜索功能升级SQL的LIKE查询在数据量大后性能下降可以接入Elasticsearch或使用MySQL全文索引多用户支持当前系统仅支持管理员发文可以扩展普通用户投稿、审核发布流程附件资源管理增加文件库功能支持上传附件、按类型过滤、统计下载次数邮件通知用户注册后发送激活邮件、评论被回复时发送提醒扩展时优先保持分层结构不变新增模块和原模块之间通过Service接口交互不要直接在Controller中直接操作Mapper这一点对以后继续学习SpringCloud微服务拆分也有帮助。8. 写在最后一点开发心得我在开发这个博客系统时最大的体会是技术选型不重要重要的是把功能和数据流理清楚。SpringBoot和Vue都是当前生态很成熟的方案社区资料丰富遇到问题搜索引擎都能解决。反而是细节问题比如字符编码、分页插件顺序、拦截器放行范围这些最容易让人卡住。如果读者是初学者建议按照我提供的源码和部署文档走一遍不要只看不练。把项目跑起来后试着改一个功能模块比如把文章列表改成瀑布流布局或者把评论改成支持Markdown语法这样能从理解项目变成驾驭项目。最后分享一个小技巧每次改完代码先用Maven的clean清理再重新打包避免旧版本class文件残留导致线上跑的还是旧逻辑。开发时养成随手提交Git的好习惯每个阶段的功能点做一次提交回过头排查Bug时定位会精准很多。项目本身不复杂但认真走完从设计到部署的全流程积累的经验对后续做更大型的项目帮助很大。希望大家也能从这个博客系统项目中收获自己的成长。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑