每年到这个时间点,总有一批计算机专业的同学开始为毕业设计选题发愁。如果你正在找SpringBoot相关的项目,又希望难度适中、功能清晰、演示效果好,个人博客系统是个非常经典的选择。这个题目在我接触过的毕设项目里出现频率相当高,但恰恰因为做的人多,很多人交出来的东西要么是照搬教程的拼接代码,要么是功能堆砌但说不清原理,答辩时一问就露馅。这篇文章我会以一个完整项目的视角,拆解“基于SpringBoot的在线博客内容发布与管理平台”——也就是常说的基于Java的个人网络日志系统——从需求边界、数据库设计、核心功能实现,到安全处理、部署上线和答辩准备,把每一个关键决策背后的原因也一并讲清楚。
1. 项目定位:先想清楚这个系统到底要解决什么问题
1.1 为什么个人博客适合当毕设,而不是盲选商城或管理系统
个人博客系统在毕业设计里的优势非常明显。首先是业务模型完整但不复杂,它天然包含内容发布、内容展示、用户管理、评论互动这几个核心场景,能够把Java后端开发里最常用的技术点全部覆盖到。其次是边界清晰,不会被“需求蔓延”拖垮——比起电商系统动辄订单、支付、库存、物流,博客系统的核心链路短得多,更适合在有限时间内做出完成度和质量。
我在帮人评估这个题目时经常说一句话:毕设项目的价值不在于功能多少,而在于你能否把每个功能背后的“为什么”讲清楚。SpringBoot个人博客系统刚好满足这个要求。它既有技术深度可以挖掘,比如SpringMVC的请求处理流程、事务控制、安全框架的集成;又有业务细节可以展开,比如Markdown内容怎么存储、标签云怎么统计、文章归档怎么做。这些点随便拎一个出来,都足够在答辩时展示你的思考过程。
1.2 功能边界:个人博客不等于多用户博客平台
标题里“个人博客系统”“个人网络日志系统”这两个关键词已经限定了项目属性:这是一个面向单用户的、以内容发布和管理为核心的平台。用户角色建议就分两类——管理员和访客,不要自己给自己加戏去搞什么用户注册、关注、私信之类的东西,那已经不是“个人博客”的范畴。
我见过不少同学一开始就想做多用户博客平台,理由是“功能多显得工作量足”。这个思路在毕设里非常危险:每多一类角色,意味着权限模型、数据隔离、交互逻辑都要跟着变,很容易在中期陷入改需求泥潭。真正合理的做法是围绕“内容发布与管理”做深做透,把管理员写文章、管理分类标签、审核评论这个闭环做好,把访客浏览文章、搜索、按标签和归档筛选这个体验做好,就已经是一个完成度很高的设计了。
1.3 模块划分:前后台分离但数据同一套
整个系统可以划分成两个大的功能面:前台展示模块和后台管理模块。前台面向访客,主要包含文章列表、文章详情、分类页、标签页、时间归档页、搜索页;后台面向管理员,包含登录认证、文章维护、分类维护、标签维护、评论管理。
两个模块共用同一套数据库和Service层,只是在Controller和页面模板上有不同的路由分组。建议后台路径统一加/admin前缀,方便在拦截器里做权限控制,这个细节在配置拦截器那一节我会专门讲。
2. 技术选型与架构分工:SpringBoot之外的配套决策
2.1 SpringBoot在这个项目里到底省了多少事
曾经我们用SSH或SSM写一个博客系统,光配置文件就能折磨人好几天:Spring的XML配置、MyBatis的映射配置、事务配置、视图解析器配置、数据源配置……每一样都得手工维护,出了问题还不好排查。SpringBoot用自动配置把这些常规操作全部接管了,你只需要在pom.xml里引入依赖,写好application.yml,项目就能跑起来。
SpringBoot对毕设项目的意义,不光是开发速度快,更重要的是它把Spring家族的核心思想封装成了一个“约定优于配置”的框架形态。你在答辩时可以讲清楚这样一个点:SpringBoot的自动配置本质上是@EnableAutoConfiguration在起作用,它会根据classpath里的依赖和已有的Bean,自动去装配当前场景需要的组件。这句话一出口,对面老师就知道你不是只会抄代码的人。
技术栈建议选型如下:
- 开发框架:SpringBoot 2.7.x(稳定,资料多,JDK8/11都兼容)
- 持久层:MyBatis-Plus 3.5.x(自带单表CRUD、分页、条件构造器)
- 数据库:MySQL 8.x(或者5.7,看本地环境)
- 模板渲染:Thymeleaf 3.0.x(服务端渲染,配合Bootstrap实现页面)
- 安全组件:Spring Security 或 自研拦截器(二选一,后面会对比)
- Markdown渲染:commonmark-java 或 flexmark
- 数据库连接池:Druid 或 HikariCP(推荐后者,SpringBoot默认)
2.2 持久层到底选JPA还是MyBatis-Plus,我的建议
这两个方案在毕设里都有大量用户,但侧重点完全不同。Spring Data JPA的特点是实体映射自动化,写一个BlogRepository接口就能继承一堆现成方法,适合纯CRUD的场景。但它的缺点是复杂查询需要学JPQL或 Specifications,对于习惯了SQL逻辑的人来说反而别扭。
MyBatis-Plus则保留了你手写SQL的能力,同时提供了BaseMapper,单表增删改查基本不用写SQL,标注了@TableField和@TableName之后,实体和表字段的映射关系一目了然。多个项目的实操体感下来,MyBatis-Plus在中文技术圈的资料数量、问题排查效率上都更占优,作为毕设项目维维护起来也更顺手。
如果让我给一个建议:选MyBatis-Plus。你只需要注意一点,application.yml里要配置逻辑删除字段的全局值,才能让删除操作变成逻辑删除而不是物理删除,这个细节我在第六节会展开。
2.3 页面渲染选Thymeleaf而不是前后端分离的理由
我知道现在很多同学一听到“前后端分离”就觉得高级,Vue + SpringBoot显得技术栈新。但这里有个现实问题:毕设项目需要在一场答辩里完整演示,前后端分离意味着你需要同时部署后端服务和前端静态资源,要处理跨域,要把联调的时间成本算进去。如果你的前端基础一般,调试一个跨域问题的时间可能比写完整个后台还长。
Thymeleaf作为服务端渲染模板,直接由SpringBoot返回HTML页面,数据和页面在同一个应用里交付,部署时一个jar包就全部搞定。这种方式非常适合个人博客这种以内容展示为主、表单交互相对简单的项目。而且它有一个很大的优势,在浏览器里右键查看源码能让老师直观看到后台传入的数据渲染结果,比看一个空壳HTML壳再等Ajax加载更有说服力。
当然,如果你想体现自己对前后端分离的理解,那完全可以采用Vue3 + Vite + SpringBoot的架构,后台管理部分做成轻量SPA。这个选择没有对错,关键是和你的能力水平匹配。我的建议是:如果前端没有太大把握,就用Thymeleaf把精力省下来,放到数据库设计和安全实现上。
3. 数据库设计:五张核心表如何撑起一个博客系统
3.1 核心表结构与字段设计的完整思路
个人博客系统的核心业务表不需要太多,我建议控制在五到六张:用户表、文章表、分类表、标签表、文章标签关联表、评论表。额外的统计表可以按需再加,但上面这六张已经能覆盖核心功能。
下面给出建表SQL,这是我在多个项目中验证过的基础版本,字段命名统一采用下划线风格,和Java实体类通过MyBatis-Plus的驼峰映射自动对应:
-- 用户表 CREATE TABLE `sys_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '密码,BCrypt密文', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像地址', `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB COMMENT='管理员用户表';-- 文章表 CREATE TABLE `blog_article` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '文章ID', `title` VARCHAR(200) NOT NULL COMMENT '标题', `summary` VARCHAR(500) DEFAULT NULL COMMENT '摘要', `content` MEDIUMTEXT NOT NULL COMMENT 'Markdown原文', `content_html` MEDIUMTEXT COMMENT '渲染后的HTML', `cover_image` VARCHAR(255) DEFAULT NULL COMMENT '封面图', `category_id` BIGINT DEFAULT NULL COMMENT '分类ID', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0草稿,1发布,2删除', `view_count` INT NOT NULL DEFAULT 0 COMMENT '浏览量', `is_top` TINYINT NOT NULL DEFAULT 0 COMMENT '是否置顶', `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`), KEY `idx_created_time` (`created_time`) ) ENGINE=InnoDB COMMENT='文章表';-- 分类表 CREATE TABLE `blog_category` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB COMMENT='分类表'; -- 标签表 CREATE TABLE `blog_tag` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB COMMENT='标签表'; -- 文章标签关联表 CREATE TABLE `blog_article_tag` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `article_id` BIGINT NOT NULL, `tag_id` BIGINT NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_article_tag` (`article_id`, `tag_id`) ) ENGINE=InnoDB COMMENT='文章标签关联表';3.2 文章和标签的多对多关系为什么用关联表
一篇文章可以有多个标签,一个标签下有多篇文章,这是典型的多对多关系。正确做法是中间加一张关联表,把多对多拆成文章和关联表的一对多、标签和关联表的一对多。
这个设计的优点很实在:一是查询某篇文章的标签只需以article_id过滤关联表,查询某个标签下的文章只需以tag_id倒查关联表,两边都很快;二是关联表上可以扩展字段,比如以后想记录每个标签在文章中的排序,直接加一列sort_order就行,不破坏现有结构。我在建表时还加了一个唯一约束uk_article_tag,防止同一条关联记录重复插入,这在并发请求下能起到兜底作用。
3.3 评论区表设计需要注意的层级问题
评论表如果只想做“文章底部留言”,那字段非常简单:评论ID、文章ID、评论内容、评论时间、评论人昵称即可。但如果你想做楼中楼回复,就得考虑parent_id这个字段,让每条评论可以指向它的父评论。
CREATE TABLE `blog_comment` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `article_id` BIGINT NOT NULL, `parent_id` BIGINT DEFAULT NULL COMMENT '父评论ID,顶级评论为NULL', `nickname` VARCHAR(50) NOT NULL, `email` VARCHAR(100) DEFAULT NULL, `content` VARCHAR(1000) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核,1正常,2隐藏', `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_article_id` (`article_id`) ) ENGINE=InnoDB COMMENT='评论表';这里有个实际经验要提醒:很多同学在后台管理评论时会遇到一个问题——“删掉父评论时,子评论怎么办?”如果直接物理删除父评论,前端页面会出现孤儿子评论。我建议在后台既展示父评论又展示子评论,删除时做逻辑删除而不是物理删除,只把状态改掉,同时在前端把被删除的评论显示成“该评论已隐藏”,这样数据结构始终是完整的。
3.4 索引和公共字段的最佳实践
看表结构可以发现每张表都有created_time和updated_time,这两个字段用MySQL默认值自动维护,Java实体里不需要额外赋值,非常省事。
索引方面我的建议是不要贪多。文章表的category_id、created_time是高频查询条件,建索引没问题;status字段区分度不高,单独建索引意义不大;view_count用于按浏览量排序时建索引有效,但数据量小实测区别不大。记住一个原则:索引是给高频查询建的,不是给所有字段建的,建多了反而拖慢写入速度。
4. 核心模块实现:文章从输入到展示的完整链路
4.1 后台写文章时的内容存储方案
这是整个项目最核心的一个设计决策:文章到底怎么存?我强烈建议数据库中同时保留content(Markdown原文)和content_html(渲染后的HTML)。为什么?
你把Markdown原文存下来,是为了以后编辑文章时能还原到编辑器里;渲染成HTML存下来,是为了前台展示时不需要每次都做一遍Markdown转HTML的运算。两者各司其职,属于用空间换时间的典型做法。业务量小可能体现不明显,但在文章多、流量大的场景下,这种冗余是值得的。
上传或编辑文章时的Service层调用链大致是这样:
@Override @Transactional public Long saveArticle(ArticleDTO dto) { // 1. 把DTO转成实体 BlogArticle article = new BlogArticle(); article.setTitle(dto.getTitle()); article.setSummary(dto.getSummary()); article.setContent(dto.getContent()); // 2. Markdown渲染成HTML保存(commonmark-java) Parser parser = Parser.builder().build(); HtmlRenderer renderer = HtmlRenderer.builder().build(); Document document = parser.parse(dto.getContent()); article.setContentHtml(renderer.render(document)); // 3. 保存文章主体 articleMapper.insert(article); // 4. 维护文章标签关联表 articleTagService.updateArticleTags(article.getId(), dto.getTagIds()); return article.getId(); }关注两点:一是@Transactional保证了如果标签关联写入失败,文章主体不会残留;二是渲染HTML放在Service层而不是前端模板里做,职责更清晰。
4.2 文章摘要到底是手动填还是自动生成
很多博客系统的摘要功能是第一版粗糙做出来的——直接截取正文前两百个字符。这个方案有个很大的问题:如果正文开头是Markdown语法标记,比如、# 标题、代码块,截取出来的纯文本会非常难看,还可能出现半个标签。
我建议后台编辑页面提供“自动生成摘要”按钮,逻辑是先把Markdown原文中的图片语法、代码块、标题符号全部去掉,再去除空白符,最后按纯文本长度截取,同时允许管理员手动微调。自动生成可以用正则表达式实现,核心代码如下:
public static String generateSummary(String markdown, int length) { // 1. 去除图片和链接语法 String text = markdown.replaceAll("!\\[(.*?)\\]\\(.*?\\)", ""); text = text.replaceAll("\\[(.*?)\\]\\(.*?\\)", "$1"); // 2. 去除代码块标记 text = text.replaceAll("```.*?```", " ", Pattern.DOTALL); // 3. 去除标题标记 text = text.replaceAll("^#{1,6}\\s*", "", Pattern.MULTILINE); // 4. 去掉空白 text = text.replaceAll("\\s+", " ").trim(); return text.length() > length ? text.substring(0, length) + "..." : text; }这个工具方法放在独立类里,不依赖Spring容器,所有地方都能直接调用。
4.3 前台文章列表的浏览量统计,怎么避免无脑++的问题
浏览量是个很容易被忽视但坑很多的字段。最朴素的实现是每次用户访问详情就直接执行UPDATE article SET view_count = view_count + 1 WHERE id = ?,但这样有两个问题:一是刷新一次页面浏览量就加一次,数据失真;二是每个请求都触发一次数据库更新,文章热点高时对数据库压力不小。
我的做法是做一个逻辑判断:如果当前会话里已经访问过这篇文章,就不再加。简单用Redis或Session存一个article:viewed:{articleId}的标识就能实现。如果不想引入Redis,Session里的Set集合也够用。数据一致性要求低的时候,甚至可以做一个定时批量刷新的方案:先用内存Map计数,每五分钟批量更新一次。毕设项目用Session方案即可,但要能在答辩时说清楚量产场景的改进方向。
4.4 搜索功能的实现,别一上来就搞大招
很多同学的搜索模块做法是调Elasticsearch,听着高大上,但个人博客的数据量根本用不上分布式搜索引擎,给自己增加部署负担和故障点不说,还很不好讲清楚原理。
更务实的选择是MySQL的LIKE模糊搜索:
SELECT * FROM blog_article WHERE status = 1 AND (title LIKE CONCAT('%', #{keyword}, '%') OR summary LIKE CONCAT('%', #{keyword}, '%')) ORDER BY created_time DESC配合MyBatis-Plus的LambdaQueryWrapper写法,代码很简洁:
LambdaQueryWrapper<BlogArticle> wrapper = Wrappers.lambdaQuery(); wrapper.eq(BlogArticle::getStatus, 1) .and(w -> w.like(BlogArticle::getTitle, keyword) .or().like(BlogArticle::getSummary, keyword));搜索框建议放在页面导航栏常驻位置,返回结果页展示“搜索到xx篇文章”的统计信息。有些同学还会做搜索关键词的同名高亮,这是一个额外加分项,实现思路是在渲染列表标题时把关键词替换成带<mark>标签的内容。
5. 安全与异常处理:登录校验、密码加密、注入防护
5.1 密码为什么不能明文存,BCrypt又是怎么工作的
后台登录是个人博客系统的入口,密码存储绝对不能明文。曾经有同学把管理员密码明文写在数据库里,答辩时老师瞄一眼就不愿意继续问了,这属于基础安全素养问题。
推荐使用Spring Security自带的BCryptPasswordEncoder做密码加密。BCrypt是一种加盐的哈希算法,每次对同一个密码计算都会得到不同的结果,因为内部会生成随机盐。校验时matches(明文, 密文)方法从密文中提取盐,再做一次计算比对。它的好处是防彩虹表、防暴力破解,而且计算成本可控地高,拖慢攻击者的尝试速度。
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }更完整的做法是引入Spring Security做过滤链,把/admin/**路径的请求全部拦截并做登录认证。但Spring Security的配置项对非安全方向的初学者来说有学习成本,如果时间紧张,我建议用Spring Boot的拦截器方案替代,通过实现HandlerInterceptor接口完成同样的登录校验,配合自定义注解使用,理解门槛低很多。
5.2 用一个拦截器完成后台登录校验
用拦截器做登录校验的思路很简单:定义一个拦截器,检查当前Session中是否存在“已登录用户”的标识对象。如果没有,就跳转到登录页。注册拦截器时,排除掉登录页、登录接口和静态资源。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是跨域预检请求,直接放行 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { // 判断是否为ajax请求 String requestedWith = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestedWith)) { response.setStatus(401); } else { response.sendRedirect("/admin/login"); } return false; } return true; } }注册这个拦截器时只需要拦截/admin/**路径,放行/admin/login和静态资源路径。
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/login", "/admin/doLogin"); }5.3 SQL注入、XSS等常见安全问题的实操防护
SQL注入方面,最有效的手段就是参数化查询。MyBatis和MyBatis-Plus的#{}本身就是预编译占位符,相对${}拼接要安全得多。我的铁律是:代码中永远不允许出现直接用${}拼接变量的SQL,除非用在动态表名等绝对可信位置。这个规则写进团队的代码规范里,比事后漏洞扫描还靠谱。
XSS方面,有两个入口需要处理:一是文章内容,因为管理员本身是可信的,但博客一旦开放评论,评论内容就变成不可信的;二是全文搜索的回显参数,搜索词带<script>标签时,模板输出会造成XSS。Thymeleaf默认会对th:text做HTML转义,所以只要不在模板里用th:utext输出用户输入的内容,问题基本可控。评论内容我一般是在写入数据库前做一层过滤,把<script>、onerror等危险标签和属性剥掉。
6. 高频踩坑实录:三个典型问题与完整排查链路
6.1 数据库时间比北京时间差8小时,排查全过程
这是SpringBoot项目里几乎人人都碰上过的问题。现象很直观:后台列表显示的发布时间比真实时间少了8小时,但数据库里直接查询时间是正确的。
排查思路从下往上走。先确认数据库连接配置里是否设置了serverTimezone,如果用的连接串是jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai,数据库层面正确。再看application.yml里的Jackson配置是否设置了时区,SpringBoot默认使用UTC处理时间序列化,需要显式指定:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8最后看MySQL驱动版本,8.x版本对时间类型的处理方式和5.x有区别。我排查这个问题的经验顺序是:先浏览器Network看请求响应里的时间字符串,确定时间是在后端序列化时丢的还是前端解析时丢的;再在实体字段上打断点确认Java对象里的时间值;最后定位到Jackson配置缺失就解决了。整个过程十分钟左右,关键是不要上来就改数据库。
6.2 文章详情接口JSON序列化报错:懒加载字段意外触发查询
这个问题在MyBatis-Plus和关联实体一起使用时很典型。后台文章列表接口返回实体对象时,文章关联了分类对象和标签集合,通过BlogArticle实体的getCategory()方法触发懒加载查询,结果在序列化阶段抛出了LazyInitializationException或者陷入循环引用死循环。
当时的报错堆栈指向的是jackson-databind的JsonSerializer,一开始我还以为序列化配置写错了,后来才发现是实体之间的关系没有处理干净。排查方法很直接:看一下实体中关联类型的注解,分类字段标记了@TableField(exist = false),然后通过@TableField或独立查询去替换关联属性,也就是前端需要展示什么,Service层就显式查询什么,不依赖ORM的自动关联。
这个问题的正确处理是避免在实体里定义复杂的对象关联。文章实体只保存categoryId和tagIds,需要展示分类名和标签名时,在Service层查出来后放进VO对象里返回。这种“实体薄、VO厚”的分层设计,职责清晰,完全不依赖MyBatis-Plus的关联映射,也避免循环引用问题。
6.3 批量插入标签关联数据时唯一索引冲突
在保存文章标签时,最直接的做法是先把文章原来的标签关联全删掉,再重新插入新的。这个逻辑在单请求下没问题,但并发场景下会出现一个问题:如果两篇文章同时关联同一个标签,而你的插入逻辑先不做存在性检查,就可能撞上唯一索引报错。
当时的报错是Duplicate entry 'xxx' for key 'uk_article_tag'。排查之后发现是我的关联表逻辑写反了:我以为唯一键是“一篇文章同一个标签不能重复”,数据库的uk_article_tag定义成(article_id, tag_id)确实符合这个约束,但问题是标签表的uk_name约束在先,如果标签名在数据库表里已存在,插入重复标签名时就会触发冲突。
解决方案是把标签的写入改成“找到就复用,找不到就插入”的语义。例如:
BlogTag tag = tagMapper.selectOne( Wrappers.lambdaQuery(BlogTag.class).eq(BlogTag::getName, tagName)); if (tag == null) { tag = new BlogTag(); tag.setName(tagName); tagMapper.insert(tag); }这样连接表插入时永远使用的是数据库里真实存在的标签主键。同理,批量插入关联记录时如果用insertBatch,也要保证同一条数据不会重复出现在集合里。
7. 从开发到部署:本地跑通和上线的完整链路
7.1 Maven打包前后分别需要调整的配置项
项目开发到一定阶段,需要在本地打得一个可执行的jar包。pom.xml需要确认的是spring-boot-maven-plugin存在,这样打成的是一个可运行的fat jar。打包时使用Maven的package命令即可,但要注意几个点。
我的application.yml习惯于区分环境:application-dev.yml对应本地开发,数据库指向本地MySQL;application-prod.yml对应生产,数据库指向云服务器,并配置不同的日志级别。打包时通过--spring.profiles.active=prod参数指定。这个配置习惯虽然简单,但能避免很多“本地能跑服务器跑不了”的问题。
同时还要注意文件上传的路径打包问题。博客系统必然涉及图片上传功能,本地可以写到D:/upload,但服务器上不能写Windows路径。建议把上传路径配置在application.yml里,通过@Value("${file.upload-path:./upload}")注入,这样在任何机器上都能按相对路径创建目录。
7.2 一台云服务器从零部署的实测步骤
部署这一步,很多同学会紧张,担心Linux命令不熟悉。我给出一个最简单、不需要Docker的部署方案,在CentOS或Ubuntu上都能操作:
首先安装Java运行时:
sudo apt update sudo apt install -y openjdk-11-jre-headless java -version然后把jar包上传到服务器:
scp blog-system-1.0.0.jar root@your-server:/opt/blog/最后运行并设置为后台服务:
cd /opt/blog nohup java -jar blog-system-1.0.0.jar --spring.profiles.active=prod > app.log 2>&1 &如果想让它开机自启,可以写一个systemd服务文件。这个步骤在答辩时可以大大方方地说“我使用systemd管理服务进程”,显得比单纯nohup更专业。
数据库的初始化建议使用SQL脚本导入,不要在服务器上手动建表。把本地导出的schema.sql上传到服务器执行:
mysql -u root -p blog_db < schema.sql初始化时别忘了先创建一个独立的数据库账号,只授予该数据库的增删改查权限,不要用MySQL的root账号直接跑业务应用。这是一条非常基本但非常有含金量的安全实践经验。
7.3 答辩演示的项目规划:不要上来就直接展示页面
关于答辩环节,我的建议是演示分为三个递进层次,每一层都要提前想好说辞:
第一个层次先展示项目的核心动画,让老师快速了解功能边界,包括首页、文章详情、分类、标签、搜索、后台管理列表。这一步一定提前录好备用的演示视频,防止现场网络问题。
第二个层次切入核心模块代码,分别讲解三层架构的调用链:Controller接收参数、Service处理业务逻辑、Mapper与数据库交互。选一个完整功能讲清楚即可,比如文章发布的完整链路。
第三个层次展示你的思考亮点,也就是这篇博文里提到的那些细节:为什么用逻辑删除、为什么存Markdown和HTML双份、为什么浏览量的统计要做会话去重。任何一个点都可能成为老师深入追问的突破口,这里答得越扎实,答辩评分越稳。
我在以往带项目的经历里发现一个共通的规律:毕业设计翻车最多的,往往不是技术水平最差的那批人,而是“做完就交”的那批人。功能照搬教程跑通了,但问一句为什么这样设计就答不上来。所以如果你是第一次用SpringBoot写完整项目,建议你在编码结束后,专门花两个晚上把整条链路从头到尾自己口述一遍,讲给同学听也行,对着录音讲也行。讲不通的地方,往往就是答辩时会被问住的地方,先把它补上,比什么准备都管用。