简介本资源为基于SpringBoot的学生读书笔记共享系统完整课设项目面向计算机相关专业学生与Java初学者可用于课程设计、毕业设计参考或SpringBoot入门实战。项目采用Java语言开发后端基于SpringBoot框架数据存储使用MySQL前端页面配合Vue与HTML实现涵盖管理员、前台首页与用户三大功能模块并附有完整论文文档从绪论、技术介绍、需求分析到系统设计、实现与测试逐章展开包含实体ER图与数据表设计。压缩包共784个文件以js、svg、java、vue、css、html等源码与页面资源为主另含sql建库脚本、bat启动脚本及docx论文整体约19.18MB目录结构清晰便于按模块查阅与二次开发。目前已有32人学习下载适合需要完整赛题方案、论文写作参考与项目排错思路的读者。1. 从一份 7z 压缩包说起学生读书笔记共享系统到底解决什么问题很多计算机专业的同学在做课程设计或毕业设计时都会拿到类似「基于 Spring Boot 学生读书笔记共享设计与实现」这样的题目附带一个 7z 压缩包里面是源码加论文。第一反应往往是这不就是个增删改查的博客吗但真正动手做起来你会发现它比想象中要复杂——读书笔记共享涉及用户权限隔离、笔记的公开/私密可见性、书籍信息与笔记的关联、以及共享场景下的检索和推荐。它要解决的核心问题是让一群学生把自己的读书笔记结构化地存起来并且能按书籍、标签、用户三个维度互相查看和复用。这套系统适合谁一是正在做 Java 课程设计、需要一套完整可运行项目参考的在校生二是想用 Spring Boot 练手一个「有业务逻辑但不至于太庞大」的全栈项目的开发者。它不像电商系统那样涉及复杂的订单和支付也不像社交平台那样需要处理海量消息但它的业务边界清晰、数据模型直观非常适合用来理解 Spring Boot 分层架构、MyBatis 持久化和前后端分离的完整链路。接下来我会按「数据模型怎么设计 → 后端怎么搭 → 前端怎么接 → 坑在哪」的顺序把这条路走一遍。2. 数据模型与权限设计笔记共享系统的骨架怎么搭2.1 四张核心表撑起整个业务读书笔记共享系统的数据模型并不复杂但每张表的字段设计直接决定了后续功能能不能顺利扩展。我一般会先画清楚实体关系一个用户可以有多个书架一个书架里有多本书一本书对应多条笔记一条笔记可以打多个标签。落到数据库层面核心表大概是这四张表名关键字段说明userid, username, password, nickname, avatar, role用户表role 区分学生和管理员bookid, title, author, isbn, cover_url, category书籍信息表isbn 用于去重noteid, user_id, book_id, title, content, visibility, create_time笔记表visibility 控制公开/私密tagid, name标签表配合 note_tag 关联表使用这里有个容易忽略的点note 表的 visibility 字段。很多同学一开始只做「公开」和「私密」两种状态但实际使用中会出现「仅同班同学可见」这种需求。我的建议是先用枚举值 0/1/2 分别代表私密、公开、好友可见后续要扩展只需要加一个 note_visibility_rule 表即可不用改主表结构。书籍表里的 isbn 字段也值得多说一句。学生在录入书籍时手动输入书名很容易出现同一本书多个条目。用 ISBN 做唯一索引配合前端调用公开的图书 API 自动填充书名和作者能大幅减少脏数据。如果 ISBN 为空再回退到「书名作者」的联合唯一索引。2.2 权限控制不是加个拦截器就完事Spring Boot 项目里做权限最常见的做法是 Spring Security 加 JWT。但这套系统有个特殊之处笔记的可见性是行级权限不是简单的接口级权限。也就是说同一个/api/note/{id}接口A 用户请求自己的私密笔记应该返回数据B 用户请求同一条笔记应该返回 403。这种行级权限用拦截器很难优雅处理我的做法是在 Service 层做校验// NoteServiceImpl.java public NoteVO getNoteById(Long noteId, Long currentUserId) { Note note noteMapper.selectById(noteId); if (note null) { throw new BusinessException(笔记不存在); } // 行级权限判断私密笔记只有作者本人可见 if (note.getVisibility() 0 !note.getUserId().equals(currentUserId)) { throw new BusinessException(403, 无权查看该笔记); } // 好友可见的笔记需要查关系表 if (note.getVisibility() 2 !isFriend(note.getUserId(), currentUserId)) { throw new BusinessException(403, 无权查看该笔记); } return convertToVO(note); }这段代码的逻辑说明先查笔记是否存在再根据 visibility 字段做分支判断。参数方面noteId 是路径参数currentUserId 从 JWT token 中解析后通过 ThreadLocal 或方法参数传入。注意这里没有用 Spring Security 的 PreAuthorize因为行级权限依赖具体数据注解表达式写起来很别扭不如在 Service 层显式判断来得清晰。提示JWT 的 payload 里只放 userId 和 role不要放权限列表。权限列表一旦变化旧 token 就无法及时失效行级权限判断必须每次查库。2.3 笔记与标签的多对多关系怎么落标签功能看起来简单但查询「带有某个标签的所有公开笔记」时如果 SQL 写不好很容易出现 N1 问题。我的做法是建一张 note_tag 关联表然后在 MyBatis 的 XML 里用一次 JOIN 查询搞定!-- NoteMapper.xml -- select idselectPublicNotesByTag resultMapNoteVOMap SELECT n.*, GROUP_CONCAT(t.name) AS tag_names FROM note n LEFT JOIN note_tag nt ON n.id nt.note_id LEFT JOIN tag t ON nt.tag_id t.id WHERE n.visibility 1 AND n.id IN (SELECT note_id FROM note_tag WHERE tag_id #{tagId}) GROUP BY n.id ORDER BY n.create_time DESC LIMIT #{offset}, #{pageSize} /select逻辑说明先用子查询筛出带有目标标签的笔记 ID再回表关联标签名用 GROUP_CONCAT 把多个标签拼成一个字符串返回给前端。参数 tagId 是标签主键offset 和 pageSize 控制分页。这里有个坑GROUP_CONCAT 默认长度限制是 1024 字节如果一条笔记标签特别多会被截断。解决办法是在 MySQL 配置里调大 group_concat_max_len或者在 Java 层再查一次标签。3. 后端接口与业务逻辑Spring Boot 分层怎么切才不乱3.1 Controller 层只做参数校验和路由很多初学者会把业务逻辑写在 Controller 里导致一个方法几百行。我的习惯是 Controller 只做三件事接收参数、校验参数、调用 Service 返回结果。参数校验用 Jakarta Validation 注解比如NotBlank、Size配合全局异常处理器统一返回错误信息。// NoteController.java RestController RequestMapping(/api/note) public class NoteController { Autowired private NoteService noteService; PostMapping public ResultNoteVO createNote(Valid RequestBody NoteCreateDTO dto) { Long userId UserContext.getCurrentUserId(); NoteVO vo noteService.createNote(dto, userId); return Result.success(vo); } GetMapping(/public) public ResultPageResultNoteVO listPublicNotes( RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) Long tagId) { return Result.success(noteService.listPublicNotes(page, size, tagId)); } }逻辑说明createNote 接口从 UserContext 中取当前用户 ID这个 UserContext 是在 JWT 拦截器里设置的 ThreadLocal。listPublicNotes 接口支持分页和标签筛选page 和 size 有默认值tagId 可选。参数校验失败会抛出 MethodArgumentNotValidException由全局异常处理器捕获后返回统一格式。注意ThreadLocal 用完一定要在拦截器的 afterCompletion 里 remove否则线程池复用时会串数据这是血泪教训。3.2 Service 层的事务边界要卡准读书笔记系统里涉及多表写入的操作有两个创建笔记时同时写 note 表和 note_tag 表删除笔记时同时删两张表。这两个操作必须在一个事务里。Spring Boot 里加Transactional注解即可但要注意默认只对 RuntimeException 回滚如果抛的是 checked exception需要手动指定 rollbackFor。// NoteServiceImpl.java Transactional(rollbackFor Exception.class) public NoteVO createNote(NoteCreateDTO dto, Long userId) { Note note new Note(); note.setUserId(userId); note.setBookId(dto.getBookId()); note.setTitle(dto.getTitle()); note.setContent(dto.getContent()); note.setVisibility(dto.getVisibility()); note.setCreateTime(LocalDateTime.now()); noteMapper.insert(note); // 批量插入标签关联 if (CollectionUtils.isNotEmpty(dto.getTagIds())) { ListNoteTag noteTags dto.getTagIds().stream() .map(tagId - new NoteTag(note.getId(), tagId)) .collect(Collectors.toList()); noteTagMapper.batchInsert(noteTags); } return convertToVO(note); }逻辑说明先插入笔记主记录MyBatis 的 useGeneratedKeys 会把自增 ID 回填到 note 对象里然后用这个 ID 构建标签关联列表批量插入。参数 dto 里的 tagIds 是前端传来的标签 ID 数组可能为空所以加了判空。rollbackFor 设为 Exception.class 是为了确保任何异常都回滚避免出现笔记插入了但标签没插入的脏数据。3.3 分页查询用 PageHelper 还是手写 LIMIT这是被问得最多的问题之一。PageHelper 的好处是代码简洁一行PageHelper.startPage(page, size)就能搞定但它对 SQL 有侵入性复杂查询时容易出问题。我的建议是简单单表查询用 PageHelper多表 JOIN 的复杂查询手写 LIMIT。// 手写分页的 Service 方法 public PageResultNoteVO listPublicNotes(Integer page, Integer size, Long tagId) { int offset (page - 1) * size; ListNoteVO list noteMapper.selectPublicNotesByTag(tagId, offset, size); long total noteMapper.countPublicNotesByTag(tagId); return new PageResult(list, total, page, size); }逻辑说明offset 根据页码和每页条数计算传给 MyBatis 的 LIMIT 子句。total 单独查一次 COUNT虽然多了一次查询但 SQL 可控不会出现 PageHelper 在某些复杂场景下 count 语句生成错误的问题。参数 page 从 1 开始size 建议限制在 50 以内防止前端传个 10000 把数据库拖垮。4. 前端对接与部署Vue 打包放进 Spring Boot 的完整链路4.1 前后端分离还是合并部署课程设计阶段我强烈建议用前后端分离开发、合并部署的方式。开发时 Vue 跑在 5173 端口通过 Vite 的 proxy 转发 API 请求到 Spring Boot 的 8080 端口这样前后端可以独立调试。部署时把 Vue 打包产物放到 Spring Boot 的src/main/resources/static目录下打成一个 jar 包用户只需要java -jar就能跑起来。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })逻辑说明所有以 /api 开头的请求都会被代理到后端 8080 端口changeOrigin 设为 true 是为了让后端收到的 Host 头是 localhost:8080 而不是 localhost:5173避免某些安全校验失败。开发阶段这样配置就够了不需要处理跨域。4.2 打包合并时的静态资源路径问题Vue 打包后默认的 publicPath 是/如果 Spring Boot 的 context-path 不是根路径就会出现 404。解决办法是在 vite.config.js 里设置base: ./让资源用相对路径引用。然后 Spring Boot 这边需要加一个配置类把前端路由的 404 请求转发到 index.html// WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/**) .addResourceLocations(classpath:/static/); } Override public void addViewControllers(ViewControllerRegistry registry) { // 把非 API 请求都转发到 index.html支持 Vue Router 的 history 模式 registry.addViewController(/{path:[^\\.]*}) .setViewName(forward:/index.html); } }逻辑说明addResourceHandlers 把静态资源映射到 classpath:/static/ 目录addViewControllers 用正则匹配不含点号的路径转发到 index.html。这样用户在浏览器里直接访问/note/123这种前端路由时不会报 404。参数方面正则[^\\.]*表示不包含点号的任意字符确保 .js、.css 等静态文件不会被错误转发。提示如果用了 Vue Router 的 hash 模式这一步可以省略但 URL 里会带 #不太好看。history 模式需要后端配合这是常见做法。4.3 文件上传与头像存储读书笔记系统里用户头像和笔记封面图需要上传。课程设计阶段不建议接对象存储直接用本地磁盘存数据库里存相对路径即可。Spring Boot 处理 MultipartFile 的默认大小限制是 1MB需要在 application.yml 里调大spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB上传接口的写法PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(文件为空); } String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); String newName UUID.randomUUID().toString() suffix; Path path Paths.get(uploadDir, newName); Files.createDirectories(path.getParent()); file.transferTo(path.toFile()); return Result.success(/upload/ newName); }逻辑说明用 UUID 重命名文件避免同名覆盖保留原始后缀。uploadDir 从配置文件读取一般是./upload/。返回的路径是相对路径前端拼接域名后访问。参数方面max-file-size 控制单个文件大小max-request-size 控制整个请求的大小如果一次上传多个文件后者要设得更大。5. 避坑与排查这套系统最容易翻车的五个地方5.1 启动报错「Table ‘xxx’ doesn‘t exist」现象Spring Boot 启动成功但访问接口时报错提示某张表不存在。原因JPA 或 MyBatis-Plus 的自动建表没有生效或者数据库连接指向了错误的库。解决检查 application.yml 里的spring.datasource.url是否指向了正确的数据库名然后手动执行 schema.sql 建表。如果用的是 MyBatis-Plus确认TableName注解里的表名和数据库一致。5.2 中文乱码从数据库一路乱到前端现象笔记内容里的中文在数据库里显示正常但接口返回给前端后变成问号。原因数据库连接 URL 没有指定字符集或者 Spring Boot 的 HTTP 编码配置不对。解决在 JDBC URL 后面加?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时在 application.yml 里配置server.servlet.encoding.charsetUTF-8和forcetrue。5.3 JWT token 过期后前端没有跳转登录页现象token 过期后接口返回 401但前端页面卡在原地用户不知道发生了什么。原因前端的 axios 拦截器没有处理 401 状态码。解决在 axios 的 response 拦截器里判断 status 是否为 401如果是就清除本地 token 并跳转到登录页。注意不要在所有 401 场景都跳转比如登录接口本身返回 401 时不应该跳转否则会死循环。5.4 分页查询第二页数据重复现象第一页和第二页有重复数据或者某些数据被跳过。原因ORDER BY 的字段有相同值MySQL 的 LIMIT 在排序不稳定时返回顺序不确定。解决在 ORDER BY 里加上主键作为第二排序字段比如ORDER BY create_time DESC, id DESC确保排序唯一。5.5 打包后前端页面空白现象java -jar启动后访问首页是空白控制台报 404 找不到 js 文件。原因Vue 打包的 publicPath 配置不对或者 Spring Boot 的静态资源映射没生效。解决确认 vite.config.js 里base: ./然后检查打包产物是否真的复制到了target/classes/static目录下。如果用的是 Maven可以在 pom.xml 里配置 frontend-maven-plugin 自动执行 npm build 并复制文件。6. 进阶技巧用 AOP 做操作日志和笔记热度统计这套系统做完基础功能后如果想在论文里体现一点技术深度我建议加两个东西操作日志和笔记热度统计。操作日志用 Spring AOP 实现在自定义注解标记的方法上自动记录谁在什么时间做了什么操作。热度统计则可以用 Redis 的 ZSet 来做每次笔记被查看就ZINCRBY一次定时把热度值同步回数据库。// LogAspect.java Aspect Component public class LogAspect { Autowired private OperationLogMapper logMapper; Around(annotation(operationLog)) public Object around(ProceedingJoinPoint point, OperationLog operationLog) throws Throwable { long start System.currentTimeMillis(); Object result point.proceed(); long cost System.currentTimeMillis() - start; OperationLogEntity entity new OperationLogEntity(); entity.setUserId(UserContext.getCurrentUserId()); entity.setAction(operationLog.value()); entity.setMethod(point.getSignature().toShortString()); entity.setCost(cost); entity.setCreateTime(LocalDateTime.now()); logMapper.insert(entity); return result; } }逻辑说明Around 环绕通知在目标方法执行前后插入逻辑point.proceed() 执行原方法然后记录耗时和操作信息。参数 operationLog 是自定义注解value 属性描述操作类型。这个切面要注意异常处理——如果原方法抛异常point.proceed() 会直接抛出后面的日志插入不会执行。如果希望异常也记录需要用 try-finally 包起来。热度统计的 Redis 操作// NoteServiceImpl.java public void viewNote(Long noteId) { String key note:hot: LocalDate.now(); redisTemplate.opsForZSet().incrementScore(key, noteId.toString(), 1); redisTemplate.expire(key, 7, TimeUnit.DAYS); }逻辑说明用日期做 key 后缀每天一个 ZSet避免所有数据堆在一个 key 里。incrementScore 给指定笔记的热度加一expire 设置 7 天过期只保留最近一周的热度数据。定时任务每天凌晨把前一天的 ZSet 数据同步到数据库的 note_hot 表然后删除 Redis key。注意Redis 的 ZSet 在数据量大时内存占用会比较高课程设计阶段数据量小没问题生产环境需要考虑分片或改用其他方案。最后说一个我自己的习惯每次做完一个 Spring Boot 项目我都会把 application.yml 里的所有配置项过一遍把不该留的默认值改掉比如数据库密码、Redis 地址、文件上传路径。这些东西在论文里不会写但答辩时老师如果让你现场改配置重新部署你就能看出平时有没有养成好习惯。希望帮到你。本文还有配套的精品资源点击获取