每年毕设选题的名单发下来,“基于SpringBoot的个人博客系统的设计与实现”这个题目几乎雷打不动地出现在好几个班里。不少同学一看就觉得“这题太简单了,不就是个写文章看文章的东西吗”,但等真开工才发现,光是技术选型、表结构设计、Markdown渲染、部署上线这几个环节,就够折腾几个星期的。我带过的几个学弟学妹选这个题,有人踩了SpringBoot版本过高的坑,有人被MyBatis Plus分页插件坑了一整天,也有人论文写得干巴巴、答辩被问得下不来台。
这篇文章我把从选题、定技术栈、建表、写核心功能,到部署排错、写论文、准备答辩的完整链路梳理一遍。内容全部按我实际做过的版本和帮别人改过的代码来写,该给配置给配置,该给代码给代码,遇到过的坑也直接摊开讲。如果你正准备选这个题,或者已经做到一半卡住了,这篇可以作为一条主线参考。
1. 选题逻辑与技术栈定版:为什么“博客系统”并不好糊弄
1.1 博客系统的功能边界其实比想象中宽
先聊选题。很多人觉得博客系统简单,是因为只看到了前台展示文章和后台发布文章这两个页面。实际上一个完整的个人博客系统,至少要覆盖四条业务链路:
- 内容生产链路:后台登录、写Markdown、传封面图、发布或存草稿、编辑和删除文章。
- 内容消费链路:前台首页分页展示、文章详情、按分类筛选、按标签聚合、按时间归档、关键词搜索。
- 用户互动链路:注册、登录、评论、回复评论、评论审核或删除。
- 系统治理链路:分类和标签管理、个人信息维护、访问统计、异常处理和日志记录。
把这几条链路全做完,你会发现它其实是一个“内容管理系统CMS”的完整缩影。毕设导师看重的是你有没有完整的软件工程思维,而不是功能列表有多长。所以这个题目真正考察的点是:你是否能把一个Web项目从零设计到落地,并且在论文里讲清楚每一个设计决策的理由。
1.2 SpringBoot版本、配套组件与项目结构定版
技术选型上,SpringBoot是绝对的主流,但版本选择很容易踩坑。这两年最大的坑就是SpringBoot 3.x和2.x的差异:3.x要求JDK 17起步,而且把包名从javax.*换成了jakarta.*。如果你照着网上大量基于2.x的教程写代码,在3.x下编译会直接报找不到包。我当时做这套系统时用的是SpringBoot 2.7.x + JDK 8,原因很简单:毕业设计阶段,稳定和资料好查比版本新更重要。如果你现在新开项目,我建议直接选SpringBoot 2.7.18这种2.x的最终维护版本,网上教程、博客、开源项目几乎全部兼容,遇到问题搜起来最快。
配套组件我推荐这样定:
- MyBatis Plus:大幅减少单表CRUD代码,分页查询、条件构造器这些直接省掉很多样板代码。
- MySQL 8.0:主流、稳定,毕业设计用完全够。
- Redis:我建议做成可选项。如果做用户登录Token缓存、文章访问计数,可以加;如果担心部署复杂度,就不加。我最后是加了Redis做Token黑名单和阅读量统计的,答辩的时候这也算一个亮点。
- 前端:两种方案,一种是用Thymeleaf服务端渲染,简单直接;另一种是前后端分离,前端用Vue3 + Element Plus。大多数学校毕业设计更接受前后端分离,因为工作量更大、结构更清晰。我当时选的是SpringBoot提供纯RESTful API + Vue3前端,正好贴合现在主流开发模式。
项目结构上,我建议按功能分包,而不是按层分包。很多同学喜欢建controller、service、mapper三个大包然后把所有类堆进去,前期还行,后期找代码非常痛苦。按功能分包的意思是,一个模块一个包,比如user包里面放UserController、UserService、UserMapper、UserEntity,这样别人看你项目结构,一眼就能看懂这个系统的模块划分。我的实际目录结构大致是:
src/main/java/com/example/blog/ ├── BlogApplication.java ├── common/ # 统一返回结果、异常处理、常量 ├── config/ # MyBatis Plus、Redis、跨域、拦截器配置 ├── security/ # JWT工具、拦截器 ├── module/ │ ├── user/ # 用户模块 │ ├── article/ # 文章模块 │ ├── category/ # 分类模块 │ ├── tag/ # 标签模块 │ ├── comment/ # 评论模块 │ └── statistics/ # 访问统计模块1.3 把功能清单分成“必做项”和“加分项”
毕业设计最怕的是需求失控。我见过一个学弟,一开始想做博客,然后想加上在线聊天,又想加上音乐播放器,最后还打算做App端,结果两个月过去核心功能都没写完。我的建议是把功能清单严格分成两层:
必做项,这些缺了系统就不完整:
- 用户注册、登录、退出登录。
- 文章的发布、编辑、删除、草稿管理。
- 前台的文章列表展示和详情页。
- 评论发表和列表展示。
- 分类、标签的维护和展示。
加分项,这些用来拉开差距:
- 文章搜索,可以先做MySQL的
LIKE查询,后面有余力再考虑全文索引。 - 按月归档,前台按时间轴展示文章。
- 阅读量统计,用Redis自增实现。
- 个人简介、友链页面、站点公告。
- 管理员对评论的删除功能。
功能清单确定后,第一件事不是写代码,而是建数据模型。
2. 数据模型先行:五张表加一张中间表的建模过程
2.1 从页面反推实体:先列功能再画ER图
我建模的习惯是“从页面反推”。先把需要做的页面列出来,比如注册登录页、文章列表页、文章详情页、后台文章编辑页、后台分类管理页,然后从每个页面里抽出数据字段。
博客系统最核心的实体是文章(Article)。围绕文章,用户发表文章,文章属于一个分类,文章可以有多个标签,用户可以评论文章。这样就得到5张核心表:user、article、category、tag、comment。因为一篇文章可以打多个标签,一个标签也对应多篇文章,所以文章和标签是多对多关系,需要一张中间表article_tag。
数据模型是后面一切功能的骨架,如果这一步歪了,后面每个功能都会别扭。比如有人图省事,把分类直接设计成文章表里的一个category_name字符串字段,结果后来想把分类改名,必须写一条UPDATE article SET category_name = '新名字' WHERE category_name = '旧名字',还要担心改漏。用独立的分类表加外键关联,才是正常的做法。
2.2 核心表结构与索引设计
下面是我实际用的建表语句,字段不多,但每个都是必须的。先看文章表:
CREATE TABLE `article` ( `id` bigint NOT NULL AUTO_INCREMENT, `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 NOT NULL COMMENT 'Markdown原文', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图URL', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-草稿 1-已发布', `create_time` datetime NOT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_status_time` (`status`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个细节值得说。第一,content字段我推荐存Markdown原文,而不是存渲染好的HTML。原因是前端编辑时需要用原文来回显,如果只存HTML,后续想改内容很麻烦;如果只存原文,展示时再渲染就可以。HTML可以下前端渲染,也可以后端渲染后临时生成,不需要落库。第二,索引idx_status_time是前台列表页最常用的查询条件:只查已发布文章,按时间倒序。没有这个联合索引,数据量到几千篇的时候,列表页就会开始变慢。
用户表:
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `nickname` varchar(50) DEFAULT NULL, `avatar` varchar(500) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `role` tinyint NOT NULL DEFAULT '1' COMMENT '1-普通用户 2-管理员', `create_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;密码必须加密存储,明文是绝对不能接受的。我用的是Spring Security里的BCryptPasswordEncoder,即使两张表数据泄露,密码也无法直接还原。
分类和标签表都很简单:id、name、create_time,顶多加一个sort_order排序字段。分类和文章是一对多,文章表里存category_id;标签表通过中间表关联:
CREATE TABLE `article_tag` ( `article_id` bigint NOT NULL, `tag_id` bigint NOT NULL, PRIMARY KEY (`article_id`, `tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;联合主键直接把数据重复问题堵死了:同一篇文章不能重复打同一个标签。很多新手用自增id做中间表主键,其实完全没有必要,联合主键就够了。
评论表相对复杂一点,要考虑层级:
CREATE TABLE `comment` ( `id` bigint NOT NULL AUTO_INCREMENT, `article_id` bigint NOT NULL COMMENT '所属文章ID', `user_id` bigint NOT NULL COMMENT '评论用户ID', `parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父评论ID,0表示根评论', `content` varchar(1000) NOT NULL, `create_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_article_time` (`article_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;parent_id用于实现“楼中楼”回复效果,为0时表示一级评论。有了这个字段,前端可以递归渲染嵌套评论,结构上比平铺评论灵活得多。
2.3 两个我踩过的字段设计坑
第一个坑:软删除和唯一索引冲突。我一开始给用户表加了deleted字段做逻辑删除,又给username加了唯一索引。结果用户注销后再注册同名账号,插入直接报唯一索引冲突,因为老记录还在表里。后来我改成了两个方案:要么所有表都统一带deleted并且在唯一索引里把这个字段加上;要么干脆不用通用软删除,只有需要审计的记录用专门的逻辑删除字段。毕设系统我最后选择了物理删除,因为这个系统数据量不大,不存在大数据场景下的误删恢复需求,简即是优。
第二个坑:评论内容长度。我一开始把评论内容设计成varchar(255),觉得够用。后来有人测试时复制了一段稍微长一点的文字,直接报Data too long for column,前端弹出一大串异常信息,特别难看。后来改成varchar(1000),阈值高了很多,同时前端也加了500字符的长度限制。这个经历给我的教训是:字段长度一定要结合业务预估,宁宽勿窄,因为改表结构成本远比改一个字段长度要高。
3. 功能落地的四条主线:登录、发文章、看文章、评论
数据模型确定后,代码实现阶段我建议按“登录鉴权→文章管理→前台展示→评论互动”四条主线推进。每完成一条主线,系统都是可以运行验证的,不会等到最后一天才把所有代码拼在一起,结果一堆冲突。
3.1 登录鉴权用JWT加拦截器,理由和写法
登录方案现在主流是JWT,而不是传统的Session。毕业设计里用JWT有个明显的好处:论文和答辩时有东西可讲,比如无状态、鉴权流程、Token过期策略。我用的流程是:用户登录成功后,后端生成一个Token返回给前端,前端存在LocalStorage里,每次请求在Header里带上Authorization: Bearer <token>;后端写一个拦截器,拦截除登录、注册、文章列表、文章详情、评论列表以外的接口,校验Token有效性。
拦截器的核心代码大概是这样的:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { String realToken = token.substring(7); // 校验签名和过期时间,解析出用户ID放入request Long userId = JwtUtil.parseUserId(realToken); if (userId != null) { request.setAttribute("userId", userId); return true; } } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } }写这个拦截器时有三个细节需要特别注意。第一,跨域预检请求OPTIONS必须先放行,否则前端调用接口时会被CORS拦截,排查起来非常头疼。第二,JWT的密钥不能用默认值,至少从配置文件里读取,长度不要少于32个字符。第三,给Token加过期时间,我一般设置24小时,前端在收到401响应后自动跳转登录页。
3.2 文章发布的Markdown链路:编辑、存储、渲染、防XSS
编辑器的选择上,我尝试过两种方案:前端集成一个开源的Markdown编辑器,比如Editor.md或者Vditor,直接编辑和预览;后端只接文章内容和标题的保存接口。另一个方案是后端引入Markdown解析库,前端提交原文,后端渲染成HTML再返回给前端展示。
我最终用的是前端编辑、前端渲染混合的方式。编辑时用Vditor,它自带预览、代码高亮、图片上传,体验很完整。详情页展示时,如果前端有现成的Markdown渲染库,就直接在浏览器里渲染;如果是Thymeleaf服务端渲染,就需要后端引入commonmark-java这类解析库,把Markdown转成HTML再和页面一起返回。两种都可以,只要记住一条硬原则:用户输入的Markdown原文里可能包含JavaScript代码,渲染前必须做HTML转义和脚本过滤,否则就是存储型XSS漏洞,任何人发一篇文章就能在别人浏览器里执行任意脚本。
我在项目里的做法是,定义一个MarkdownUtil工具类,渲染时先把代码块里的内容原样保留,再把其他部分的HTML标签做白名单过滤,只允许p、code、pre、blockquote、a、ul、ol、li、h1到h6、img这些常规标签。编译HTML时所有<script>、<iframe>、on*事件属性一律剔除。这样虽然偶尔会让某些嵌入内容失效,但安全性优先,这是毕设系统可以接受的代价。
3.3 前台展示:分页、标签云与按月归档
前台首页的列表接口,用MyBatis Plus的分页功能就够了。先配置分页插件拦截器:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); // 单页最多50条,防止有人恶意调大页码参数拖垮数据库 paginationInterceptor.setMaxLimit(50L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }然后在Service里用LambdaQueryWrapper写查询条件:
Page<Article> page = new Page<>(current, size); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Article::getStatus, 1) .eq(categoryId != null, Article::getCategoryId, categoryId) .orderByDesc(Article::getCreateTime);这段代码看起来简单,但有几个点容易出错。eq(condition, column, value)三个参数的重载,第一个参数是布尔条件,只有传入true时才拼接SQL,这样“分类筛选”在不传分类时不会生成多余的WHERE条件。这是MyBatis Plus用得好的关键技巧。
标签云的数据聚合其实很简单,就是把关联文章数量统计出来:
SELECT t.id, t.name, COUNT(at.article_id) AS article_count FROM tag t LEFT JOIN article_tag at ON t.id = at.tag_id GROUP BY t.id, t.name按月归档则是另一句常用SQL:
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS count FROM article WHERE status = 1 GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month DESC这两条SQL建议直接写进Mapper的XML里而不是用MyBatis Plus的封装方法,因为涉及聚合和别名,用封装方法反而绕。
3.4 评论模块的隐藏需求:嵌套、防刷、可控制
评论模块看起来简单,实际在测试阶段最容易出问题。我遇到过的场景按频次排序是:乱码、XSS、重复提交、被刷评论。
乱码问题主要靠全链路UTF-8解决:数据库表utf8mb4、数据库连接URL加characterEncoding=utf8、前端页面<meta charset="utf-8">、后端接收参数时确保SpringBoot的server.servlet.encoding.force=true。这四层缺一,就可能出现中文变问号的情况。
XSS问题还是在渲染层过滤,评论内容和文章内容一视同仁。重复提交问题,我用的方案是前端提交后按钮置灰,后端再对同一用户同一文章的评论做短时间频率限制,比如同一个IP一分钟内最多评论5次。这块可以用一个简单的内存Map记录请求时间戳,也可以引入Redis的INCR加过期时间。毕设环节用内存Map就够演示了,但如果想体现技术水平,用Redis做限流更值得写进论文。
评论的嵌套展示,后端返回的列表需要带parentId,前端递归渲染。这里有一个小坑:评论列表接口不要一次性把整篇文章所有评论全部返回,数据量大时会很卡。我采用的方式是前端分页加载,默认加载20条,点“加载更多”再取20条。如果一个评论下面有嵌套回复,回复也走分页。
4. 部署和运行阶段翻车复盘:四个细节结束了我的“本地能跑”
前面功能写完了,项目在本地跑得很欢,但到了部署到云服务器或者换一台电脑运行时,问题一个一个蹦出来。这一节我把自己实际排查过的四个问题复盘一遍,每个都给出现象、排查链路和最终修复方案。
4.1 时间凭空少了8小时:时区问题的排查全过程
现象很经典:本地Windows上运行,接口返回的文章发布时间是正确的2025-06-18 10:30:00;部署到Linux服务器后,同一个接口返回的时间变成了2025-06-18 02:30:00,整整少了8小时。
排查过程是这样:先怀疑数据库存储的时间不对,直接在服务器上执行SELECT create_time FROM article,发现数据库里存的就是10:30:00,说明插入时没问题。再用Postman调接口看返回JSON,发现返回的是02:30:00,问题出现在Java程序读取数据库到JSON输出的环节。后来发现是MySQL驱动连接URL里没有指定时区,服务器系统默认时区是UTC,MySQL驱动按UTC解析日期,再转成默认时区输出,就产生了8小时偏移。
修复方式是在数据库连接URL里显式指定时区:
spring: datasource: url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8同时Jackson的time-zone也写成GMT+8,保证JSON序列化时时间格式统一。这里建议任何环境都显式配置,不要依赖服务器默认时区,否则换一台服务器就出一次事。
4.2 MyBatis Plus分页不生效:插件配置被忽略
另一个高频问题:写好了Page查询,控制台打了SQL日志,发现SQL里竟然没有LIMIT,而且page.getRecords()返回了全部数据,total值也不对。
一开始我以为是分页代码写错了,反复检查Page参数都没问题。后来翻官方文档才想起来:MyBatis Plus的分页功能不是内置的,必须显式注册PaginationInnerInterceptor。如果漏了这段配置,所有分页查询都会变成全表查询。更隐蔽的是,如果项目中同时配置了多个MybatisPlusInterceptor的Bean,或者有人手动在XML里写了Interceptor,插件也可能被覆盖,导致分页失效。
我的排查建议是:先看启动日志里有没有PaginationInnerInterceptor相关初始化信息;再看控制台打印的SQL末尾有没有LIMIT关键词;最后确认MyBatis Plus的版本和SpringBoot版本是否兼容。特别提醒一下,网上很多旧教程用的是3.4.0以前的写法,比如PaginationInterceptor,这个类在新版本已经被移除,改用MybatisPlusInterceptor加PaginationInnerInterceptor的组合,版本不匹配也会出现分页失效。
4.3 上线后刷新404:前端路由与静态资源映射
前端用的是Vue3的vue-router,默认是HTML5 History模式。本地开发时一切正常,前端打包成dist后放进SpringBoot的static目录,结果首页能打开,点击进入/article/1后按F5刷新,直接404。
排查逻辑是这样的:SpringBoot处理请求时,如果没有匹配的Controller接口,就会返回404,而Vue Router的History模式依赖服务器把所有未知路径重新指向index.html,由前端路由接管。所以问题根源是SpringBoot没有做这个“回落”配置。
修复方法是在SpringBoot里加一个路由映射,把所有非API路径转发到index.html:
@Controller public class PageForwardController { @RequestMapping(value = "/{path:[^\\.]*}") public String forward() { return "forward:/index.html"; } }注意[^\\.]*这个正则,它的意思是路径中不包含点号的请求全部转发到index.html,这样/article/1会转过去,而/js/app.js这类静态资源路径不会被误处理。
4.4 数据库字段名消失:Linux下MySQL的大小写敏感
这是最诡异的一次:本地Windows上项目跑得好好的,部署到Linux服务器后,一个查询接口直接报“Unknown column 'create_time' in 'where clause'”。
我先检查了实体类字段名和数据库列名,完全对得上。然后到服务器上执行SHOW CREATE TABLE article,发现数据库表里的列名居然也叫create_time,没问题。再检查Mapper XML里的SQL,也没问题。最后才反应过来,问题可能出在MyBatis Plus自动生成的SQL上:我在实体类里用@TableField("create_time")和@TableName("article")做映射,本地MySQL在Windows上表名和列名的大小写不敏感,而Linux上的MySQL默认是大小写敏感,如果建表时表名是大写,或者实体类注解的表名和实际表名差一个字母的大小写,就找不到了。
解决方案是统一规范:数据库表名、列名全部用snake_case小写,实体类字段用camelCase,注解里明确写@TableName("article")。同时服务器上的MySQL配置lower_case_table_names=1,让表名强制小写。这一步做完,问题就再也没有出现过。
这个问题给我的教训很实在:“本地能跑”和“部署能跑”是两回事,所有路径、时区、大小写、编码问题,只在Linux环境或换一台机器时才会暴露。
5. 论文和答辩怎么准备:把“常规系统”讲出差异点
代码写完了,系统跑起来了,但对毕业设计来说,论文和答辩往往才是决定成绩的关键一步。很多学生代码做得不错,却栽在论文写得像产品说明书,或者答辩时只会演示页面不会讲设计思路。
5.1 论文里最能体现工作量的是这三章
论文的摘要和绪论部分容易写成空话,比如“随着互联网的发展,博客已经成为人们分享知识的重要平台”。这种话可以写,但不能整章都是。导师最关注的其实是三块内容:
第一,需求分析。不要只罗列功能需求,要写清楚每类用户的典型使用场景。比如普通游客浏览文章、注册用户评论、管理员管理所有内容。每一类场景对应哪些用例,用例之间怎么关联。
第二,系统设计。包括架构设计、功能模块设计、数据库设计。数据库设计部分不要只贴建表语句,要解释为什么这样设计:哪些表是一对多,哪些是多对多,为什么要用中间表,为什么给某个字段加索引,索引解决了什么问题。
第三,系统测试。这一章最容易被当成凑字数,但其实写好了是最有说服力的。建议做两层测试:一层是功能测试,用表格列出测试用例,包含正常输入、边界输入、异常输入三类,写明预期结果和实际结果;另一层是接口测试,可以用Postman或Apifox跑一遍核心接口,把响应状态码和返回结构贴进去。有真实验证结果做支撑,论文的严谨性会明显提升。
5.2 系统测试部分:用功能用例表和边界用例征服老师
功能用例表不用面面俱到,挑十几个核心用例就够了。我建议的表格结构是这样:
| 用例编号 | 测试模块 | 操作步骤 | 输入数据 | 预期结果 | 实际结果 |
|---|---|---|---|---|---|
| TC-01 | 用户注册 | 打开注册页,填写表单后提交 | 新用户名、合法邮箱 | 注册成功并跳转登录页 | 与预期一致 |
| TC-02 | 用户注册 | 填写已存在的用户名提交 | 已占用用户名 | 提示“用户名已存在” | 与预期一致 |
| TC-03 | 文章发布 | 后台填写标题正文后保存草稿 | 标题为空、正文有内容 | 提示“标题不能为空” | 与预期一致 |
| TC-04 | 评论 | 对已发布文章发表评论 | 评论内容500字 | 评论成功并显示在列表 | 与预期一致 |
我实践下来,老师对这个表格的容忍度很高,只要格式清晰、结果真实,他们不会追问太多。怕就怕为了凑表数把明显没测过的用例也写上去,一问就露馅,得不偿失。
5.3 答辩高概率追问的四个问题与应对话术
根据我和学弟学妹们的经验,答辩现场提问集中在四个方向:
第一,“你这个系统跟现有的开源博客系统有什么本质区别?”这个问题的正确回答思路不是强调功能差异,而是强调技术实践。你可以说:“区别不在功能,而在实现方式。开源博客系统大多是部署即用的产品,而我在这个项目里完整走了一遍需求分析、数据库设计、接口设计、前后端开发和部署的流程,重点是自己实现了JWT鉴权、Markdown内容处理和全链路的异常处理。”这些是你在项目里真实做的事情。
第二,“并发量大了怎么办?”千万不要说“我的系统高性能高并发”。你得先承认当前系统以功能完整性为目标,然后说清楚如果要做升级应该怎么做:数据库加索引、引入Redis做缓存、把静态资源放到CDN、前端做懒加载、按需引入消息队列。能说出这几个方向,说明你是理解瓶颈在哪里的。
第三,“这个SQL为什么这样写?”只要你把数据库设计章节讲清楚,这个一般不难。关键是答辩前把项目里自己写的复杂SQL都原文讲一遍,把每个字段的作用说清楚。
第四,“部署方案是什么?”要能讲清楚用了哪台服务器、用的什么系统、Java环境怎么装的、MySQL版本是多少、用不用Nginx、域名和端口怎么配的。哪怕你只是在本机跑通,也要清楚这些环节。
答辩还有个实用技巧:提前准备两套账号。一套管理员账号,一套普通用户账号,用户名密码写在一张小纸条上放在电脑旁边。演示的时候不要临场注册、临场登录,那些流程容易出意外。直接进系统,按“首页展示→文章详情→评论→登录后台→发一篇文章→前台看到文章”的路径走一遍,这条路你自己先跑十遍,保证每个点击都顺畅,比准备任何漂亮话都有用。
说回到题目本身,“基于SpringBoot的个人博客系统的设计与实现”能做的深度和广度都超出很多人预期。把基础功能做扎实,把部署踩过的坑记录成测试章节,把版本选择和数据表设计背后的理由想清楚,这套系统就不仅仅是“又一个博客”,而是你对SpringBoot整体理解的一份完整交付物。希望这份流程能帮你少走一些我走过的弯路。