简介:一份基于Springboot的学生读书笔记共享系统设计与实现资料包,面向Java课程设计、毕业设计及Springboot初学者。内容涵盖完整源码与配套论文,论文按绪论、技术介绍、需求分析、系统设计、系统实现、系统测试等章节展开,清晰呈现从需求到实现的完整开发脉络。系统选用Java、MySQL、Tomcat与Springboot框架,实现管理员后台、前台首页、用户读书笔记共享等典型功能模块。压缩包共784个文件,体积19.18MB,以js、vue、java、css、html、png等类型为主,其中前端vue与js文件承载页面交互,java文件处理后端逻辑,sql脚本提供数据库初始化,doc/docx为论文文档,并附带bat运行脚本便于本地启动。已有32人学习下载,适合需要快速搭建同类课设项目、参考论文结构或学习Springboot+Vue前后端整合的读者。
1. 学生读书笔记共享系统:SpringBoot 单体项目为什么仍是毕设与接私活的首选
拿到「基于 SpringBoot 学生读书笔记共享设计与实现」这个标题,很多人的第一反应是“又一个图书管理系统换皮”。但把「共享」两个字拆开看,它比常见的单机图书借阅多了两条核心链路:笔记的上传 / 展示 / 检索,以及多用户之间的可见性控制。这正是 SpringBoot 单体应用最舒服的射程范围——既不需要微服务那套注册中心、消息队列来撑场面,也不会因为业务太简单导致论文没东西可写。我见过太多人一上来就纠结要不要上 Redis、要不要拆前后端分离,最后卡在环境搭建上;而这个项目用一套 SpringBoot + MyBatis Plus + Thymeleaf(或 Vue 分离)就能跑通全部需求,源码包里的论文还能直接对应到数据库设计、权限模型和测试章节。
这个标题的受众也很明确:计算机相关专业做毕业设计的学生、准备 Java 课程设计的初学者,以及接外包时碰到“读书笔记共享”这类需求的全栈开发者。它的价值不在于算法多难,而在于把 CRUD 做扎实——用户注册登录、笔记的增删改查、按分类 / 关键字搜索、点赞收藏、个人笔记与公共笔记的可见性切换,再加上 JWT 或 Session 的权限控制。把这些点理顺,系统就能稳定跑起来,论文也有真实的数据流和界面截图可写。下面我会按一条可复现的路线拆解:先讲清楚设计上的四个关键选型,再给出数据库与项目骨架的最小实现,接着处理文件上传与全文检索这两个最容易翻车的模块,最后把部署和避坑清单一次性讲透。
2. 先想清楚四件事:共享边界、权限模型、富文本存储、检索方式
2.1 共享边界:公开、私有与关注可见,别把权限设计成布尔值
读书笔记共享系统最容易被做“浅”的地方,就是给笔记表加一个is_public字段,然后查询时where is_public = 1。这种做法在演示时没有问题,但论文的“需求分析”和“系统设计”章节会显得非常单薄。更合理的设计是三级可见性:公开(所有人可见)、私有(仅自己可见)、好友/关注可见(仅关注者可见)。三级权限在数据库里可以用一个tinyint字段visibility表示,但在查询层需要分别处理:公开笔记直接查,私有笔记追加user_id = 当前用户,关注可见则要通过关注关系表先查出被关注者的 ID 集合,再作为in条件拼入查询。
常见做法是在 Service 层封装一个NoteQuery对象,里面携带当前用户 ID、目标用户 ID、关键字、分类 ID、分页参数,然后由 SQL 动态拼接条件。用 MyBatis Plus 时,我一般直接用 LambdaQueryWrapper,配合and嵌套来处理权限条件。这里有个参数细节:查询公开笔记时不要带user_id条件,否则会漏掉其他用户的公开内容;而查询“我的笔记”时才需要eq(user_id, currentUserId)。把这两个条件分开封装成两个方法,比在同一个方法里用 if 判断更清晰,也方便论文里写“模块划分”。
2.2 权限控制:用 Sa-Token 还是 JWT + 拦截器
SpringBoot 做登录认证,绕不开 Session、JWT、Sa-Token 这几个选择。对于这个项目的规模,我的建议是直接上 Sa-Token——它比 Shiro 轻,比手写 JWT 拦截器省事,而且自带的StpUtil.getLoginId()能直接拿到当前登录用户 ID,省去从 Token 里解析用户信息的样板代码。源码包里如果是老项目,很可能是 Spring Security + JWT 的组合;但如果你是自己从零写,Sa-Token 的学习成本和出 bug 的概率都低得多。
使用 Sa-Token 时,核心配置就三件事:引入依赖、配置拦截器或过滤器、在登录接口调用StpUtil.login(userId)。需要注意的一个坑是Token 生效范围:默认是单点登录模式,同一账号在多处登录会互相踢下线;做读书笔记这类系统,用户很可能在手机和电脑同时使用,需要在配置里打开is-concurrent: true,并设置is-share: false,这样每个设备各自持有 Token,互不影响。不用 Sa-Token 的话,也可以继续用 Shiro,但 Shiro 在 SpringBoot 3.x 下的整合比较折腾,而 Sa-Token 对 SpringBoot 2.x / 3.x 都跟进得很快,这也是我近年更推荐它的原因。
2.3 富文本笔记的存储:Markdown 源码入库,渲染交给前端
读书笔记和普通文章的区别在于:它既有结构化元数据(书名、作者、页码、分类),又有非结构化的笔记正文。正文如果是纯文本,系统体验会很差;如果直接粘贴 Word 里的富文本,又容易带进大量垃圾标签和样式。最常见的方案是前端用 Markdown 编辑器(如 md-editor-v3 或 wangEditor 的 Markdown 模式),后端只保存 Markdown 原文,展示时前端用 marked / markdown-it 渲染成 HTML。这样数据库里存的是一段普通文本,占用空间小,也方便做关键词搜索。
有个参数需要提前定好:笔记正文的字段类型用text而不是varchar。MySQL 的varchar最长 65535 字节,但这是按字符集换算的,utf8mb4 下实际最多存 16383 个汉字左右。一篇读书笔记加上摘录和感想很容易超过这个长度,届时 Hibernate 建表会直接报错。用text类型(64KB)或longtext(4GB)就不会有瓶颈。如果你用 MyBatis Plus,实体类上要加@TableField(typeHandler = JacksonTypeHandler.class)之类的注解来处理特殊类型,不过对于纯 Markdown 字符串,普通String字段直接映射就行,不需要 typeHandler。
2.4 检索方案:先做 LIKE,再考虑全文索引,不要一上来就上 ES
热搜词里频繁出现“springboot 整合”“源码”这类字眼,很多人会误以为做一个带搜索功能的系统必须上 Elasticsearch。实际上,当你的数据量在十万条以内时,MySQL 的 LIKE 查询配合合理索引完全够用。读书笔记共享系统的数据量级——就算一个高校所有学生都用,笔记总数也很难超过几十万条。在这个量级下,WHERE title LIKE '%关键字%' OR content LIKE '%关键字%'的查询虽然会全表扫描,但单表几十万数据、平均笔记长度 1KB 的场景下,响应时间通常仍在几百毫秒以内,对毕业设计演示毫无压力。
如果论文想强调“检索性能优化”,可以做一层轻量级方案:给 title 字段加前缀索引,给 content 字段建 FULLTEXT 索引(MyISAM 或 InnoDB 的 ngram 解析器)。MySQL 5.7+ 的 InnoDB 支持中文全文索引,建表时用FULLTEXT KEY ft_content (content),查询时用MATCH(content) AGAINST('关键字' IN NATURAL LANGUAGE MODE)。这里有个坑:ngram 默认分词长度是 2,搜索单个汉字时查不到东西,需要配置ngram_token_size=2(但最小支持 1,配置 1 后单个字也能搜)。更省事的做法是直接用 LIKE 糊一层,然后在校验阶段通过explain看执行计划,用数据说明“现阶段 LIKE 满足需求,未来数据量增大后可平滑切换 ES”——这个表述在论文里比“我用了 ES”更容易自圆其说,因为 ES 的部署和维护成本在毕设答辩时反而会成为被追问的重灾区。
3. 从零搭出可运行的共享系统:数据库脚本和 SpringBoot 项目骨架
3.1 数据库设计:五张表覆盖全部核心业务
这个系统的实体并不复杂,我按“用户—笔记—互动—关注”四个维度拆成五张表:user(用户)、note(笔记)、category(分类)、note_like(点赞)、user_follow(关注)。如果要做收藏功能,再加一张note_favorite,但为了演示和论文篇幅,点赞和收藏通常二选一,我建议保留点赞,因为点赞的计数更新逻辑更适合在论文里写“并发控制”。
下面是最小可运行的建表脚本,我直接把它放进源码包的sql/init.sql里。注意note表里的visibility字段,0=私有,1=公开,2=关注可见;status字段预留审核能力,默认 1 表示已发布,0 表示草稿。有些二手源码会把草稿直接删掉,但保留草稿功能会让论文的“用例图”多一个角色动作,建议不删。
CREATE DATABASE IF NOT EXISTS reading_notes DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE reading_notes; DROP TABLE IF EXISTS note; DROP TABLE IF EXISTS user_follow; DROP TABLE IF EXISTS note_like; DROP TABLE IF EXISTS category; DROP TABLE IF EXISTS user; CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `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 '头像URL', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `category` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '分类名,如:文学、技术、历史', `sort` INT DEFAULT 0 COMMENT '排序权重', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='笔记分类表'; CREATE TABLE `note` ( `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 '标题', `content` LONGTEXT NOT NULL COMMENT 'Markdown正文', `book_name` VARCHAR(100) DEFAULT NULL COMMENT '关联书名', `visibility` TINYINT NOT NULL DEFAULT 1 COMMENT '0私有 1公开 2关注可见', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0草稿 1已发布', `like_count` INT NOT NULL DEFAULT 0 COMMENT '点赞数,冗余字段', `view_count` INT NOT NULL DEFAULT 0 COMMENT '浏览数', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_category_id` (`category_id`), KEY `idx_create_time` (`create_time`), FULLTEXT KEY `ft_title_content` (`title`, `content`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='读书笔记表';这段脚本有四个参数细节值得说明。第一,密码字段长度设为 100,因为 BCrypt 加密后的字符串长度是 60,但有些实现会带前缀或 salt 信息,留出余量可以避免后面改动字段;第二,title和content一起建了全文索引,如果后续觉得搜索不准,可以拆成两个单独的 FULLTEXT 索引,分别做标题权重和正文权重;第三,like_count和view_count是典型的冗余计数,每次点赞 / 访问时先更新计数,再插入明细记录,而不是实时count(*);第四,所有外键都没有在数据库层面声明,只建了普通索引——这是企业开发里的常见习惯,让外键约束交给应用层处理,避免删除用户时被外键拦住,论文里也可以写“出于性能与扩展性考虑,物理外键不建,逻辑外键由 Service 层维护”。
另外还需要note_like和user_follow两张表,这里就不展开全部脚本了,核心思路是:note_like以(user_id, note_id)建联合唯一索引,防止重复点赞;user_follow以(user_id, follow_user_id)建联合唯一索引,防止重复关注。两张表的主键都用自增BIGINT,不要用联合主键,因为在 MyBatis Plus 中联合主键需要额外配置,自增单主键最省事。
3.2 SpringBoot 工程骨架:包结构、依赖版本与启动类
拿到一个“源码+论文”的压缩包,第一步不是急着跑起来,而是先看pom.xml里的 SpringBoot 版本。这里有一个高频坑:很多老项目的源码是基于 SpringBoot 2.3/2.4 写的,但你在新电脑上装的是 JDK 17+,直接运行会报UnsupportedClassVersionError或者Caused by: java.lang.IllegalArgumentException。常见的做法是把 SpringBoot 版本升到 2.7.18(最后一个 2.x 版本,兼容 JDK 8~17)或 3.1.x(要求 JDK 17+)。如果你用的是 IDEA 自带的 Spring Initializr 生成的 JDK 21 项目,再落回 SpringBoot 2.7 会非常痛苦,不如直接选 SpringBoot 3.1 + 对应的 MyBatis Plus 3.5.3+。
一个合理的包结构如下,它和论文里的“系统设计—架构设计”章节能直接对应:
com.example.readingnotes ├── ReadingNotesApplication.java // 启动类 ├── controller // 控制层:AuthController, NoteController, UserController │ ├── dto // 请求体与响应体对象 │ └── vo // 视图对象(含分页信息) ├── service // 业务层接口与实现 │ └── impl ├── mapper // MyBatis Plus 的 Mapper 接口 ├── entity // 数据库实体类 ├── config // Sa-Token、CORS、WebMvc 配置 ├── common // 统一返回结果 Result、异常处理、常量 └── utils // 工具类:JWT、MD5、字符串处理启动类没有任何特殊之处,标准的三件套注解:@SpringBootApplication、@MapperScan("com.example.readingnotes.mapper")、public static void main。但有一点需要注意:如果你的 application.yml 里没有配置spring.datasource.url,启动时会因为无法创建数据源而直接退出。很多二手源码会自带一个application-druid.yml或application-dev.yml,运行前要确认spring.profiles.active指向了正确的环境。用 H2 内存库做演示也是一种常见做法,但论文里如果截图是 H2 的 URL,答辩时容易被追问“为什么不用 MySQL”,所以我建议一律改成 MySQL,并附上sql/init.sql。
3.3 统一返回结果与全局异常:让前后端联调少吵三次架
这个项目的接口返回格式如果不统一,Postman 调试时会很崩溃。我一般定义一个Result<T>泛型类,包含code、message、data三个字段,成功时code=200,业务失败时code=400或code=401,未登录时code=401。Controller 里所有接口都返回Result<?>,配合@RestControllerAdvice做全局异常处理,把MethodArgumentNotValidException(参数校验失败)、BusinessException(自定义业务异常)、Exception(兜底)分别映射到对应的 code。这样前端拿到非 200 的 code 后弹出 message 即可,完全不用关心 HTTP 状态码。
一个常用的参数校验技巧是:在 DTO 字段上直接标注@NotBlank(message = "标题不能为空")、@Size(max = 200),SpringBoot 在进 Controller 方法前就会自动校验,出错后由全局异常处理器统一返回。这比在 Service 里写几十行 if 判断干净得多,论文里也可以写“通过注解式校验减少业务代码冗余”。
4. 上传与分享链路:笔记发布、全文检索、点赞关注的实现细节
4.1 发布笔记的最小可用逻辑:草稿、状态与标记清除
发布笔记这个动作,在NoteController里对应一个POST /api/note接口。请求体里带上title、content、categoryId、visibility、bookName,如果status=0则是保存草稿,status=1则是直接发布。在NoteServiceImpl里,核心逻辑是:判断当前登录用户与笔记作者是否一致、设置userId、调用save或saveOrUpdate。这里有个容易漏掉的点:修改笔记时不能把create_time重置,如果你是先查出来再 set 新值,那么继续沿用原对象的创建时间;如果直接用前端传回的对象去 update,很可能把createTime覆盖成当前时间。
@Override public Long createNote(NoteCreateRequest request) { Note note = new Note(); note.setUserId(StpUtil.getLoginIdAsLong()); note.setTitle(request.getTitle()); note.setContent(request.getContent()); note.setCategoryId(request.getCategoryId()); note.setBookName(request.getBookName()); note.setVisibility(request.getVisibility()); note.setStatus(request.getStatus()); note.setLikeCount(0); note.setViewCount(0); noteMapper.insert(note); return note.getId(); }这段代码的逻辑是:从 Sa-Token 会话里拿到当前登录用户 ID,然后逐字段装配新笔记。注意categoryId可能是 null,此时分类接口要容错——查询笔记列表时,如果categoryId为空则不做过滤,而不是查出category_id IS NULL的数据。分页查询用 MyBatis Plus 的Page<Note>对象,构造LambdaQueryWrapper<Note>时,先eq(Note::getStatus, 1),再根据权限条件决定and嵌套里的过滤条件。一个可行的写法是:如果当前用户已登录,查询条件为(visibility=1 OR (visibility=2 AND user_id IN (select follow_user_id from user_follow where user_id=当前用户)) OR user_id=当前用户);如果未登录,直接eq(visibility, 1)。把未登录场景单独处理,是因为未登录用户不应该触发关注表查询,这样也能减少一次无谓的数据库访问。
4.2 点赞计数与并发:用乐观锁还是直接 UPDATE
点赞功能看着简单,但并发下容易出数据不一致。比如两个人同时点赞,like_count先读后写就可能丢更新。最简单的修复是直接用一条 SQL 完成自增,不要先查询再更新。在 MyBatis Plus 里可以自定义 SQL,也可以直接用UpdateWrapper:
boolean success = likeService.saveOrUpdate(...); // 先插入点赞记录,通过唯一索引保证不重复 if (success) { noteMapper.update(null, new LambdaUpdateWrapper<Note>() .eq(Note::getId, noteId) .setSql("like_count = like_count + 1")); }这里的关键是setSql("like_count = like_count + 1"),它让数据库在原子层面完成自增,避免并发覆盖。如果论文里想提“乐观锁”,可以再给note表加一个version字段,并在实体类上用@Version注解,MyBatis Plus 更新时会自动带上WHERE version = ?。但对于点赞这种高频操作,乐观锁失败重试的成本反而比直接自增高,所以我更推荐上面这种 SQL 自增的方式,并在论文的“性能优化”部分说明理由。
取消点赞时,逻辑是对称的:先删除note_like记录,再执行like_count = like_count - 1。这里有一个隐藏很深的坑:如果用户从未点赞,却调用了取消点赞接口,like_count会被误减成负数。解决方法是先查note_like是否存在,存在才执行更新;或者用UPDATE note SET like_count = IF(like_count > 0, like_count - 1, 0) WHERE id = ?兜底。作为有经验的开发者,我建议两层都做:应用层保证业务不越权,SQL 层保证数据不为负。
4.3 全文检索与搜索历史:从 LIKE 到 FULLTEXT 的平滑切换
前面已经说过,默认直接 LIKE 是最省心的。但既然建表脚本里加了 FULLTEXT 索引,我在这里给出一个可以二选一的查询封装。默认情况下,Service 里的searchNotes(keyword)方法是这样的:
// LIKE 版本:兼容任何 MySQL 版本 List<Note> notes = noteMapper.selectList(new LambdaQueryWrapper<Note>() .like(Note::getTitle, keyword) .or() .like(Note::getContent, keyword) .eq(Note::getStatus, 1) .last("LIMIT 20"));如果数据量变大或者答辩时想展示“全文索引”,可以把上面的条件替换成apply("MATCH(title, content) AGAINST({0})", keyword)。这里有个需要提醒的点:AGAINST 的{0}参数必须用 MyBatis Plus 的 apply 参数占位符,不能直接字符串拼接,否则会报 SQL 注入风险提示。FullText 的搜索默认是自然语言模式,搜索“Java 编程”会被拆成“Java”和“编程”两个词;想要更精确的短语匹配,可以用布尔模式IN BOOLEAN MODE,给关键词加上引号。实际测试下来,“读书笔记”这种四个汉字的关键词,在 ngram 默认 2 字分词下,搜“读书笔”也能出结果,但搜“读”就查不到——如果论文是在 Linux 环境部署,我一般建议直接把ngram_token_size调成 1,牺牲一点索引体积换取单字可搜,用户体验更贴近手机输入法。
搜索历史这个功能属于可选项,做起来也不难:在user_search_log表里记录user_id, keyword, search_time,查询搜索热词时按关键字分组统计。论文里的“数据分析”或“系统亮点”部分可以拿它充一个章节,但实现成本很低,属于典型的“花小钱装大逼”功能。
4.4 文件的落地与访问:为什么推荐本地存储而不是 OSS
如果共享系统允许上传封面图、附件(如 PDF 摘录),就会遇到文件存储问题。常见的做法有三种:存 MySQL BLOB、存服务器本地、存云 OSS。对于毕设和课程设计,本地存储是最稳妥的——因为 OSS 需要开通阿里云 / 腾讯云账号、配 Bucket 权限、写回调,一旦你给的源码包里没有真实的 AccessKey,答辩演示时一传文件就报错,直接翻车。本地存储只需要一个upload/目录,并通过配置给 SpringBoot 添加一个静态资源映射。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadPath + "/"); } }对应地,在application.yml里有一行file.upload-path: /Users/你的用户名/reading-notes-upload,上传接口用MultipartFile接收文件,FileUtils.copyInputStreamToFile写入磁盘,然后返回/files/时间戳_原文件名给前端拼完整 URL。参数里有一个坑:Linux 部署时路径最后必须带斜杠,否则file:协议映射会失败,404 找不到资源。Windows 下开发用D:/upload/也可以,但注意 SpringBoot 的file:前缀在 Windows 上写file:D:/upload/是合法的,写file:D:\upload\则可能因为反斜杠转义出问题。
关于文件大小的限制,SpringBoot 默认单个文件最大 1MB,如果不改配置,传一张手机截图就报MaxUploadSizeExceededException。在 yml 里加上spring.servlet.multipart.max-file-size: 10MB和max-request-size: 10MB即可。这个参数卡住过很多人,而且报错信息还不直观。
5. 避坑指南:源码包导入、版本地狱与部署现场的 5 个血泪教训
5.1 “SpringBoot 版本太高”导致的启动失败,先查 JDK 再看依赖
热搜词里反复出现“springboot版本太高”“springboot 版本”,这几乎是源码类项目最常见的翻车点。现象是:导入源码后 IDEA 里一切正常,一运行就报Unable to start ServletWebServerApplicationContext或Failed to configure a DataSource。前者通常是 Jackson 或 Tomcat 版本与 SpringBoot 不兼容,后者则是没配数据源。解决路径是:先看pom.xml的parent版本,看 IDEA 里 Project Structure 的 SDK 是否匹配,再按版本号去查对应的 MyBatis Plus 和 Sa-Token 依赖。SpringBoot 3.0 以后不能用javax.*包,要用jakarta.*,如果你的源码里大量出现import javax.servlet.http.HttpServletRequest且编译不过,只能二选一:降回 SpringBoot 2.7,或全局替换javax为jakarta。多数情况下,把项目直接降到 2.7.18 是最省时间的选择。
5.2 数据库初始化“成功”但登录表里没数据:字符集与 SQL 版本不一致
很多源码包里的init.sql是作者从 Navicat 导出的,里面可能带SET NAMES utf8mb4或SOURCE命令。直接在命令行手动执行时,如果终端字符集是 utf8,而表定义是 utf8mb4,中文分类名容易变成乱码。解决方法是:执行前先SET NAMES utf8mb4;,然后用source命令导入时也会带乱码风险,建议用 IDEA Database 工具或 Navicat 直接运行整个 SQL 文件,这两个工具会正确识别文件编码。另外,如果 SQL 文件里有DROP TABLE IF EXISTS,在已有数据的库里跑会清库,操作前一定确认这不是你的生产库——毕设阶段无所谓,如果是在公司环境演示,别手滑。
5.3 部署到云服务器后图片裂了:SpringBoot 静态资源与 Nginx 的归属问题
如果是单机部署,SpringBoot 的静态资源映射能正常取到本地文件。但很多人会用宝塔或 Nginx 做反向代理,这时/files/**的请求会被 Nginx 拦截并转发到 SpringBoot 的 8080 端口。如果 Nginx 配置了location / { proxy_pass http://127.0.0.1:8080; },一般情况下没问题;但如果 Nginx 对静态文件设置了try_files或expires,或者root指向了别的目录,你的图片就会 404。我的建议是:部署阶段不要把/files/交给 Nginx,直接在 Nginx 里写一个location /files/ { alias /www/wwwroot/reading-notes-upload/; },让 Nginx 直接读磁盘,SpringBoot 只负责上传写文件。这样不仅访问速度快,还能避免 SpringBoot 容器重启时临时文件丢失。
已有的图片还是裂,多半是上传时文件名带了中文或空格,返回给前端的 URL 没有做 URL 编码,浏览器请求时解析失败。解决方法是上传时统一重命名为yyyyMMddHHmmss_随机数.扩展名,丢弃原始文件名。论文里可以把这写成一个“文件安全命名策略”,也算一个能讲的亮点。
5.4 全文索引不生效:MySQL 表引擎与 ngram 配置的连环坑
如果你选择了 FULLTEXT 方案,有一个很容易被忽略的事实:MySQL 5.7 默认全文索引只支持英文分词,中文要用 ngram 插件。检查办法是执行SHOW VARIABLES LIKE 'ngram_token_size';,如果结果为 2,你可以改配置文件my.cnf里的ngram_token_size=1后重启 MySQL。但改这个参数会让 MySQL 重建所有全文索引,如果表很大,启动会变慢。另一个隐蔽问题是:LIKE 和 FULLTEXT 两条查询路径对空格和标点的处理逻辑不同,同一关键词在搜索结果页和首页“热门笔记”里可能数量不一致,测试时注意对账。
如果项目在 Windows 本地跑,MySQL 服务是mysql命令启动的,修改my.ini后需要重启服务;如果在 Docker 里跑 MySQL,需要挂载配置文件并重建容器。很多人把ngram_token_size写在了[mysqld]下面,但 MySQL 8.0 里这个参数必须在[mysqld]之前加载,顺序错了直接启动失败。这个坑我踩过一次,当时手动执行 SQL 建表一直提示“token not found”,后来才发现是配置放在[client]组里导致不生效。
5.5 论文写的和代码对不上:页面跳转方式与前后端分离的错位
部分源码包里早期版本是 Thymeleaf 模板渲染,后期作者把前端改成 Vue 了,但论文截图还停留在模板版。答辩时老师对比截图和现场 URL,发现前端框架对不上,会直接被质疑“是不是别人的项目”。解决方法是在代码里保留两种入口或至少保证截图里的页面能被访问到:如果源码是前后端分离的,你可以在resources/static下放一个index.html,里面用 axios 调用后端 API,让评委通过http://localhost:8080/直接看到主要页面。无需上线前端工程,只需把 Vue 构建后的dist内容复制到static目录,同时确保接口是相对路径或带正确前缀。这是毕设演示最稳的做法:一套代码直接跑通,不再依赖npm run dev进程。
6. 进阶与验证:写自动化测试、打印执行计划并给论文补上性能数据
如果你不想让这个项目停在“能跑”的层面,我建议花半天时间补三样东西:接口冒烟测试、SQL 执行计划分析、压力测试报告。这三样是论文“系统测试”章节的硬货,也是答辩时老师最喜欢问的部分。先加一个 SpringBoot 自带的测试类,用 MockMvc 测登录、发布笔记、搜索三个主流程:
@SpringBootTest @AutoConfigureMockMvc public class NoteApiTest { @Autowired private MockMvc mockMvc; @Test public void testPublishNote() throws Exception { // 先登录拿到 token,再发布笔记 MvcResult loginResult = mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"test\",\"password\":\"123456\"}")) .andExpect(status().isOk()) .andReturn(); String token = JsonPath.read(loginResult.getResponse().getContentAsString(), "$.data.tokenName"); String tokenValue = JsonPath.read(loginResult.getResponse().getContentAsString(), "$.data.tokenValue"); mockMvc.perform(post("/api/note") .header(token, tokenValue) .contentType(MediaType.APPLICATION_JSON) .content("{\"title\":\"《活着》读后感\",\"content\":\"# 第一章 感悟\",\"visibility\":1,\"status\":1}")) .andExpect(jsonPath("$.code").value(200)); } }这段测试代码的注意点是:Sa-Token 的请求头名称默认是satoken,如果你在配置里改过token-name,这里的 header 名也要跟着改。很多源码里的 Token 头是Authorization,用 Sa-Token 的话可以直接在拦截器里配置别名。
看着测试通过还不够,我还会直接在 MySQL 里执行项目生成的 SQL 语句,前面加EXPLAIN,重点看type和rows两列。如果type=ALL且rows>10000,说明没走索引。对于公开笔记列表的分页查询,这个 SQL 一般是这样:
EXPLAIN SELECT * FROM note WHERE status = 1 AND visibility = 1 ORDER BY create_time DESC LIMIT 10;这里最容易忽略的优化点:排序字段create_time如果没有索引,MySQL 会先查出全量数据再用 filesort 排序,数据量大时很慢。解决方法是建一个联合索引(status, visibility, create_time),让查询走的索引天然有序,Extra里不再出现Using filesort。这个优化在论文里可以写满半页,老师一听就明白你真的理解索引。
压力测试我一般用 JMeter,不开 GUI 而是用命令行跑,这样给出的数据更可信。测试内容就是 100 个线程循环 10 次请求/api/note/public列表接口,观察Throughput和Error%。如果错误率大于 1%,九成是数据库连接池默认配置太小——SpringBoot 默认 HikariCP 最大连接数是 10,并发一高就Connection is not available。把spring.datasource.hikari.maximum-pool-size调到 50 一般就能解决,这又是一个能在测试章节里写“优化前后对比”的素材。
最后一句来自我的习惯:每个项目交付前,我会重新解压一次源码包,按 README 从零走一遍部署流程,确保我写出的启动步骤不是靠我电脑里的残留环境。很多“源码能跑”的假象,都是因为在原项目目录里跑过太多次,环境变量、本地数据库都留着旧配置。重新解压后再踩一遍坑,才算真正交付。希望帮到你。
本文还有配套的精品资源,点击获取