
1. 项目先说清楚这到底是个什么系统1.1 模块地图管理员端加用户端很多同学拿到一个源码项目第一件事就是急着启动结果启动完了开始乱点过一会儿就不知道自己在干嘛。我习惯拿到项目先看模块结构先弄清楚功能边界。这套基于SpringBoot加Vue的个性化图书推荐系统前后端分离角色上拆得非常清爽一端是普通读者一端是管理员。读者这边能做的事可以归成四条线注册登录之后浏览图书、按分类检索图书、查看推荐列表、对图书进行收藏和借阅。推荐列表不是摆设后面我会细讲它到底是怎么算出来的这也是这个项目名字里“个性化”三个字的核心所在。管理员那边就传统多了负责图书的增删改查、分类管理、用户管理、借阅记录管理、推荐算法的参数维护。说白了读者端是给用户用的管理端是给运营方维护数据用的两边公用一套MySQL数据库通过后端接口串联起来。这套结构放在课设或者毕设答辩里最大的好处是“讲得出东西”每一个模块都有明确的业务闭环不是那种为了凑功能硬塞上去的假需求。1.2 个性化推荐从哪里来把协同过滤落地“个性化推荐”听着高大上落地到课设项目里最常用的方案并不是深度学习而是协同过滤。原理通俗讲就是一句话跟你口味相似的人喜欢什么我也试着推荐给你。放到图书场景里来理解。假设读者A喜欢《三体》和《球状闪电》读者B也喜欢《三体》《球状闪电》同时还借了《流浪地球》。系统发现A和B的阅读历史高度重合就会把《流浪地球》推给A因为“跟你相似的人也在看这本书”。这个项目里推荐模块的输入主要来自三类数据借阅记录、收藏记录、评分记录。如果用户还没产生足够的行为数据系统会退化为热门推荐也就是按借阅次数和评分排序把最受欢迎的图书推出来。这种“冷启动降级”的处理方式是实际工程里非常常见的套路直接体现了设计者有没有工程经验。2. 技术栈选型解析为什么这套组合是“标准答案”2.1 后端SpringBoot零配置带来的效率提升后端用SpringBoot早几年可能还有人纠结要不要用SSHStruts加Spring加Hibernate到现在这个时间点基本不用犹豫了。SpringBoot最大的价值在于“约定大于配置”——你不用再像传统SSH那样维护一堆XML配置文件启动一个Web项目只需要一个加了SpringBootApplication注解的入口类。我做过的几个课设项目对比下来SpringBoot对新手最友好的地方其实有两个。第一个是内嵌Tomcat。以前部署一个项目要单独装Tomcat、配置server.xml、把war包丢进webapps现在直接mvn spring-boot:run或者java -jar就能跑起来少了一层环境认知负担。第二个是起步依赖Starter。比如你想用Web能力就引入spring-boot-starter-web想操作数据库就引入mybatis-spring-boot-starter。依赖关系由SpringBoot统一管理版本基本告别了“依赖冲突地狱”。2.2 前端Vue加Element UI前后端分离的工程化体验前端选了Vue具体版本需要留意一下。如果项目用的是Vue 2那配套的是Element UI如果是Vue 3那对应的是Element Plus。这两套组件库API有差异混着看容易越看越乱。Vue这个框架的上手曲线是平缓的模板语法类似HTML数据绑定用v-model列表渲染用v-for条件渲染用v-if有HTML基础的同学两天就能写出像样的页面。配合Vue Router做路由跳转配合Axios做接口请求就很自然的把前端工程化这件事做了出来。我在开发这套系统的时候前端目录结构是这么拆的src/ ├── api/ // 所有后端接口请求封装 │ ├── book.js │ ├── user.js │ └── recommend.js ├── router/ // 前端路由表 ├── views/ // 页面组件 │ ├── Login.vue │ ├── Home.vue │ ├── BookList.vue │ └── Recommend.vue ├── components/ // 复用的公共组件 └── utils/ // 工具类比如axios封装把api和views分开是我给课设项目定的一个硬性规范。很多同学喜欢在页面里直接写axios.get页面一多改一个接口地址要全局搜非常痛苦。统一封装之后后端接口变了只改一个文件省心得多。2.3 MyBatis加MySQLSQL可控、上手成本低持久层选了MyBatis而不是JPA有一个非常现实的原因课设答辩的时候老师大概率会问你SQLMyBatis把SQL写在自己手里你能清楚讲出每一条查询是怎么写的JPA那种自动生成的SQL反而容易让自己说不清。MySQL就更不用说了开源、免费、跨平台大学课程里教的基本就是它。搭配Navicat或者DataGrip做可视化操作建库建表导数据都很快。这三样组合在一起加上Maven做依赖管理整个技术栈全部是业界主流。换句话说哪怕你不是为了做课设是用这套东西练手找工作简历上写“熟悉SpringBoot、Vue、MyBatis、MySQL全家桶开发”也是有分量的。3. 核心实现拆解推荐算法、权限、分页与缓存3.1 推荐算法的具体落地三步走实现UserCF这个项目的推荐模块我实现的是基于用户的协同过滤UserCF。虽然叫“算法”但落到代码上就是一个三步走的流程。第一步构建“用户-图书”行为矩阵。数据源是借阅表、收藏表和评分表为了简单直观我把它们统一折算成用户对图书的兴趣度。-- 构建用户行为表 user_book_behavior SELECT user_id, book_id, MAX(score) AS score FROM ( SELECT user_id, book_id, 5 AS score FROM borrow_record UNION ALL SELECT user_id, book_id, 3 AS score FROM favorite UNION ALL SELECT user_id, book_id, rating AS score FROM rating ) t GROUP BY user_id, book_id这里我用的是SQL层直接聚合比在Java内存里遍历要清爽很多。分数映射的逻辑是借阅记5分、收藏记3分、评分按1到5原值计算。第二步计算用户之间的相似度。经典方式是余弦相似度或者皮尔逊相关系数在课设场景我选余弦相似度实现简单讲起来也直白。public double cosineSimilarity(MapInteger, Double user1, MapInteger, Double user2) { SetInteger commonKeys new HashSet(user1.keySet()); commonKeys.retainAll(user2.keySet()); if (commonKeys.isEmpty()) return 0.0; double dotProduct 0.0; double norm1 0.0; double norm2 0.0; for (Integer key : user1.keySet()) { norm1 Math.pow(user1.get(key), 2); } for (Integer key : user2.keySet()) { norm2 Math.pow(user2.get(key), 2); } for (Integer key : commonKeys) { dotProduct user1.get(key) * user2.get(key); } return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }这里有个细节值得说相似度计算只遍历了两个用户共同看过的图书没有共同看过的用户相似度直接是0可以提前跳过。这个剪枝优化在数据量小的时候感觉不出来但数据量一旦上千差距非常明显能把几秒的计算压缩到几百毫秒。第三步基于相似用户生成推荐。拿到当前用户的TopN相似用户后把这些用户的行为图书汇总剔除当前用户已经读过的书按相似度加权后的分数排序取前N本。推荐结果最终落到一张推荐表里前端查询的时候直接读这张表。为什么要落表而不是实时算因为实时计算的话每次用户刷新推荐页都要触发全量计算性能扛不住而把结果定时刷新到推荐表比如每天凌晨跑一次或者每次用户产生行为后增量更新体验要好得多。3.2 登录与权限JWT怎么集成不踩坑登录模块我用的JWT加拦截器方案。流程是用户输入账号密码后端校验通过之后生成一个带有效期的token返回给前端前端把token存在localStorage里每次请求在请求头里带上Authorization: Bearer xxx后端拦截器统一校验token没带或者过期就返回401。项目里拦截器配置要注意放行路径。登录接口、注册接口、静态资源一般不需要鉴权图书列表这种基础数据接口通常也不用但借阅、收藏、后台管理等接口必须校验。我见过不少同学把拦截器配置成了拦截所有路径结果静态资源全成404排查一个小时才发现是拦截器把所有请求都吃了。Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/login, /api/register, /api/books/**); }还有一个必须要提醒的JWT密钥不要写在代码里写死。哪怕课设很简单也建议放在application.yml里配置至少做到配置与代码分离。别问我为什么答辩老师看到硬编码密钥是有可能追问的。3.3 MyBatis分页与缓存这些用法直接写到简历里3.3.1 PageHelper分页插件图书列表如果没有分页数据一多页面就卡。MyBatis的分页方案有很多种最省心的是PageHelper插件。用法就三步引入依赖、配置拦截器、在Mapper查询前调用PageHelper.startPage。dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency// Service层 public PageInfoBook getBookList(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListBook books bookMapper.selectAll(); return new PageInfo(books); }PageHelper.startPage之后紧接着的那一条SQL查询会被自动加上limit注意中间不能再有别的查询操作否则MyBatis的拦截器会傻傻分不清到底给哪条SQL做分页。这里还有个坑在于分页参数如果直接从前端传过来要加参数校验。pageNum最小是1pageSize最大建议100不校验的话有人传个9999999进来能一次把数据库打穿。3.3.2 MyBatis缓存一级缓存和二级缓存MyBatis的一级缓存是默认开启的作用范围是同一个SqlSession。这个在单体应用里感知不强因为Spring整合之后每次执行一个方法可能都新建了SqlSession缓存根本用不上。二级缓存才是真正跨SqlSession的。在Mapper XML里加一行cache/就能开启查询结果会序列化后存入缓存下次相同查询直接命中。但二级缓存我建议慎开尤其在课设项目里。原因是它默认对图书数据没问题一旦涉及关联查询和多表更新缓存失效的清理逻辑很麻烦。如果一张图书表的数据更新了但你查询的是“图书加分类”的联表结果MyBatis默认只认识自己的命名空间其他命名空间更新了数据它不知道查询出来的还是旧缓存。我当初踩过这个坑后直接把二级缓存关了图书这类低频变更数据用Redis做缓存才是正解。3.4 Vue端路由与请求封装前端也不能写得太随意前端的核心就是路由和请求。路由需要在vue-router里配置并且要用动态路由做权限控制。管理员登录后能看到“后台管理”入口普通用户看不到这个不需要复杂的权限框架前端路由加一个meta.roles字段路由守卫里判断一下就行。// 路由守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } });请求封装用Axios创建实例设置baseURL和超时时间再添加请求拦截器和响应拦截器。响应拦截器里最重要的是统一处理401后端返回未认证直接跳回登录页把过期token清掉。// axios 请求封装 service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(error); } );这个写法能避免每个页面都去写一遍401处理逻辑。4. 从源码到跑通全套实操记录4.1 环境准备版本搭配对照表拿到源码后别急着启动先把环境对齐。我整理了一份我实测过的版本搭配组件推荐版本备注JDK1.8 或 11大多数SpringBoot 2.x项目JDK8即可Maven3.6.3及以上低于3.5容易下载依赖失败Node.js14.21.3Vue2项目18.xVue3项目建议用nvm切换npm6.x或8.x跟随Node版本MySQL5.7或8.0两者SQL语法略有差异SpringBoot2.x具体看项目pom文件以上这套版本组合我跑通的不止一个项目稳定性很好。特别提醒不要一上来就装JDK17加SpringBoot3.x那是另外一套玩法很多课设项目并不兼容。JDK安装之后记得配环境变量JAVA_HOME和PATH两个配置缺一不可。在命令行里执行java -version能正常输出版本说明配置成功。Node安装后执行node -v和npm -v验证。Maven安装后执行mvn -v验证。MySQL安装后能通过命令行或者Navicat连上就行。4.2 数据库初始化建库、导表、灌数据数据库这块是整个项目启动成败的关键也是最容易出问题的地方。源码包里通常会有sql文件夹里面有项目对应的建表脚本和初始数据。第一步是登录MySQL创建数据库。注意字符集一定要指定utf8mb4不然以后存中文、存表情符号会乱码。CREATE DATABASE IF NOT EXISTS book_recommend DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后用mysql命令行导入SQL文件mysql -u root -p book_recommend book_recommend.sql或者直接在Navicat里右键运行SQL文件都行。导入完成后重点看一下user表、book表、borrow_record表有没有数据。如果借阅记录是空的推荐模块暂时不会有输出属正常现象后面测试的时候可以手动造几条记录。数据库里容易忽略的是初始管理员的账号密码一般SQL脚本里会写死比如admin / 123456。如果文档里没写直接查库SELECT id, username, password, role FROM user;就能看到。4.3 后端启动四个步骤后端启动路径比较标准化我按顺序来第一步用IDEA打开后端项目目录等待Maven下载依赖。这一过程快慢取决于网络国内建议配阿里云镜像。第二步检查application.yml里的数据库配置。重点核对这几个值spring: datasource: url: jdbc:mysql://localhost:3306/book_recommend?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456MySQL 8.0以下版本不需要serverTimezone参数8.0以上必须加否则会报时区错误。第三步直接运行主类里的main方法或者在根目录执行mvn spring-boot:run看到Started Application in xxx seconds字样后端就算启动成功了。默认端口一般配置在server.port常见的是8080如果被占用改掉即可。第四步验证接口是否可用。浏览器访问http://localhost:8080/api/books/list?pageNum1pageSize10能返回JSON数据说明后端和数据库链路正常。4.4 前端启动npm安装依赖前端启动别直接双击index.htmlVue项目是工程化的必须走构建流程。进入前端目录执行npm install这个过程同样依赖网络如果卡住不动很可能是npm源的问题。换成淘宝镜像能解决90%的安装问题npm config set registry https://registry.npmmirror.com安装完成后再启动开发服务npm run serve默认启动在http://localhost:8081。端口跟前端环境配置文件里的proxy目标要对上一般前端项目里的.env.development文件会写VUE_APP_BASE_API这个变量如果你是后端8080这里保持默认即可。浏览器打开前端地址能跳到登录页并成功登录前后端联调就跑通了。4.5 联调自测从注册到推荐全链路整个系统跑起来之后我建议自测这么一轮流程注册一个新用户验证用户表新增成功。用新身份浏览图书列表翻页正常收藏一本感兴趣的图书。再注册第二个用户也收藏相同的书并且借阅其中一两本。回到第一个用户的推荐页看是否有来自相似用户的图书推荐。用管理员账号登录后台新增一本图书、编辑一条借阅记录验证CRUD正常。退出管理员账号确认普通用户无法访问后台接口直接拼接URL访问也被拦截。这套自测走完系统的主要功能就都覆盖到了答辩的时候也能理直气壮说“我完整实测过”。5. 踩坑实录与避坑指南5.1 问题表格速查我把实际开发和跑通过程中最常遇到的问题整理成一张速查表问题现象可能原因解决方案前端请求后端接口报404前后端端口不一致或后端未启动确认server.port和前端baseURL匹配登录接口报401后跳转登录页循环请求头没带token或token过期检查Axios请求拦截器是否携带Authorization数据库连接报“Public Key Retrieval is not allowed”MySQL 8.0的安全限制URL加allowPublicKeyRetrievaltrue中文乱码数据库字符集不是utf8mb4建库时指定utf8mb4连接串加characterEncodingutf8分页不生效返回全部数据PageHelper被误用在查询之前还有别的SQL确保startPage紧跟目标SQL前端npm run serve失败Node版本过高或者依赖缺失删除node_modules重新npm install推荐列表始终为空用户行为数据太少手工插入借阅、收藏数据后再测试图片不显示上传文件路径问题检查上传目录和静态资源映射配置5.2 JWT过期时间这个细节很多人测试的时候会遇到一个特别尴尬的场景用着用着突然被踢出登录了重新登录一趟又能继续用。并不是代码写错了而是JWT过期时间设置得太短。有的源码把过期时间设成30分钟甚至15分钟对课设演示来说确实太短了。我建议把它改成24小时也就是毫秒值设为86400000这样演示全程不会中途跳登录。正式商业项目当然要结合安全策略设置短一点但课设场景以演示顺畅为优先。5.3 MyBatis需要留心的两个Mapper细节第一个是Mapper接口和XML文件的映射。接口方法名和XML里的id必须完全一致否则运行时报Invalid bound statement (not found)。这个问题排查要点是看编译后target目录下有没有对应的XML有同学把XML放在src目录下的java包里没在pom里配置资源路径结果编译时直接被跳过怎么调都报错。!-- pom.xml里加上资源配置 -- resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources第二个是![CDATA[]]符号。MyBatis的XML里写、、会被当成XML标签解析时间范围查询里的就会报错解决办法是把SQL包在CDATA段里。select idselectByTime resultTypeBook ![CDATA[ SELECT * FROM book WHERE create_time #{endTime} ]] /select5.4 推荐系统没有推荐数据的排查思路这是这个项目里最值得唠叨的一个点。很多同学把系统跑起来之后发现推荐页空空如也第一反应是算法写错了。按照我的经验90%的情况不是算法问题而是数据问题。具体排查思路按顺序走先看行为表数据量。如果用户表里只有一两个用户图书表里只有几条记录任何推荐算法都变不出花样。要手工在数据库里造数据至少让两个用户有交集比如用户A收藏了《三体》、借阅了《流浪地球》用户B也收藏了《三体》、借阅了《球状闪电》这样A和B才有相似度可算。再看当前用户是否已经有推荐结果。推荐表在用户首次登录时可能不会立即生成需要触发一次计算任务。很多源码里的推荐任务是定时跑的测试时手动执行一次计算接口或者重启服务时执行初始化才能看到效果。最后才怀疑算法本身。检查相似度函数入参的Map构建是否为空检查推荐结果去重时是不是把当前用户读过的书全剔除了数据少的时候很可能全被剔光。5.5 数据库升级到MySQL 8之后的一些坑如果你的本机是MySQL 8.0而源码里的驱动依赖还是5.1.47之类的老版本连接时大概率报Access denied for user或者Communications link failure。把驱动版本升级到8.0.x同时把连接串换成com.mysql.cj.jdbc.Driver就行。MySQL 8比MySQL 5.7多了个caching_sha2_password认证插件老版本的客户端驱动不兼容这个插件驱动升级后问题自然就消失了。另外MySQL 8默认的only_full_group_by模式比较严格如果你写的分组查询的select字段没有全部出现在group by里会直接报SQL错误。实在不想改SQL的话可以修改SQL模式SET GLOBAL sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO;5.6 前后端联调的跨域问题本地开发最典型的报错是浏览器控制台出现Access to XMLHttpRequest at ... from origin ... has been blocked by CORS policy。这个问题的思路是后端没做跨域配置或者做了但没生效。SpringBoot里最直接的解法是写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8081) .allowedMethods(*) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }更稳妥的方式是前端在vue.config.js里配置代理把/api的请求代理到后端8080这样浏览器看到的始终是同源请求不会有跨域问题。// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };两种方式我用得比较多的是代理方案因为生产环境部署时一样要配反向代理提前用代理模式开发能让前后端环境更加接近线上形态。6. 我对这套源码的一些看法最后说点实际开发的体会。这套项目的技术栈选择很稳SpringBoot、Vue、MyBatis、MySQL都是当前最主流的技术不是过气框架。哪怕将来你要在这个项目基础上再做扩展比如接入Redis缓存、换成RabbitMQ做异步消息、加一个Spark离线计算推荐任务技术路线都是平滑的。对准备拿它做课设的同学我的建议是不要只把系统跑通就完事至少把推荐模块的代码完整读一遍把协同过滤的三步讲清楚——如何构建用户行为矩阵、如何计算相似度、如何生成推荐结果。答辩时能把这个链路讲明白比什么花哨的功能都加分。对想把它当“毕设起点”的同学也有一条明确的升级路径可以给推荐模块加入实时计算能力用户每次借阅或收藏后立即更新相似用户和推荐列表或者引入Redis缓存热门图书和推荐结果再或者做一个GraphQL接口层替代现在的Restful接口。每一步改造都让项目的技术含量上一个台阶。这套代码我前后部署过好几次每次都能顺利跑通。只要严格按照顺序来——先建库导数据再启动后端验证接口最后启动前端联调——基本半小时内就能看到完整页面。遇到问题先看环境版本再看日志最后看代码逻辑大部分坑都能自己解决。最后再送给读者一个实用技巧拿到任何源码项目第一件事不是看代码而是看项目根目录的README和SQL文件夹。README里往往写着启动步骤、默认账号密码、端口号SQL文件夹里的表结构能让你对整个系统的数据结构心里有数。把这两个看完你对这个项目的理解就已经超过了60%的人。