SSM框架实战:手写电影推荐系统与协同过滤算法
简介这是一套面向高校计算机专业毕业设计的电影推荐系统完整项目包采用JAVA语言与SpringSpringMVCMyBatis后端框架搭配MySQL数据库适合正在准备毕设、需要可运行参考项目的学生直接使用。压缩包共846个文件约22.45MB涵盖128个java源码、167个js脚本、49个vue组件、47个html页面、55个css样式及xml配置、sql脚本、properties配置等前端页面、后端逻辑与数据库脚本层次分明另附论文与部署说明便于理解推荐算法与系统架构。项目源码经测试校正后可百分百成功运行可直接作为毕设选题方案。目前已有49人学习下载。读者可获得完整源码、数据库脚本、部署文档与论文参考快速搭建环境、梳理模块结构并完成二次开发与答辩准备。1. 电影推荐系统为什么值得用 SSM 手写一遍很多同学做毕设或者练手项目时第一反应是上 Spring Boot 全家桶觉得 SSMSpring SpringMVC MyBatis已经过时了。但如果你真的想搞懂一个 Java Web 项目从请求进来到数据落库的完整链路SSM 反而是最合适的练手对象。这套电影推荐系统就是典型场景用户表、电影表、评分表、推荐结果表四张表串起登录、浏览、打分、推荐四条业务线每一层都得自己配、自己写、自己调。它解决的不是推荐算法多先进的问题而是一个 Java 后端工程师能不能独立把分层架构、事务控制、SQL 映射、推荐逻辑串起来的问题。适合正在学 Java 基础、准备面试、或者需要交一份带论文的完整项目的同学。下面我按实际搭建顺序把选型、建表、推荐算法、避坑点全部拆开讲。2. 技术选型与分层架构SSM 每个零件到底管什么2.1 为什么是 SSM 而不是 Spring Boot先说结论如果你时间紧、只想跑起来Spring Boot 确实更快。但如果你要理解一个请求从浏览器到数据库再回来的完整路径SSM 的显式配置反而帮你看清每一层。Spring 管 Bean 的生命周期和依赖注入SpringMVC 管 URL 映射和参数绑定MyBatis 管 SQL 和 Java 对象的映射。三者各司其职配置文件里每一行你都知道它在干什么。常见做法是用 Maven 做依赖管理pom.xml 里引入 spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java、druid 连接池、jstl 这几个核心包。版本上我一般选 Spring 5.3.x MyBatis 3.5.x MySQL 8.0这套组合稳定、资料多、踩坑少。JDK 用 1.8 就行别上太新的版本有些老教程的配置在 JDK 17 上会因为模块化报错。分层上分四层Controller 层接收前端请求、Service 层写业务逻辑和事务、Dao 层也叫 Mapper 层写 SQL 接口、Entity 层放实体类。推荐算法放在 Service 层因为它需要同时读评分表和电影表属于业务逻辑而不是数据访问。2.2 数据库表设计四张表撑起整个系统表设计是整个项目的地基设计不好后面推荐算法根本没法写。我一般会建这四张核心表表名作用关键字段user存用户信息id, username, password, emailmovie存电影信息id, title, genre, director, year, avg_scorerating存用户对电影的评分id, user_id, movie_id, score, create_timerecommend存预计算的推荐结果id, user_id, movie_id, predict_scorerating 表是推荐算法的核心数据源user_id 和 movie_id 建联合索引否则数据量一上来查询会慢得让你怀疑人生。movie 表的 avg_score 字段是冗余字段每次有人打分就更新一次这样首页展示时不用每次都去 rating 表做聚合查询。建表 SQL 大概长这样CREATE TABLE rating ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, score DOUBLE NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), INDEX idx_movie (movie_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_user_movie这个唯一索引很关键它保证同一个用户对同一部电影只能有一条评分记录避免重复打分导致推荐结果失真。idx_movie是为了加速查某部电影的所有评分这个操作协同过滤里会频繁用到。2.3 MyBatis 映射配置XML 还是注解MyBatis 有两种写法XML 映射文件和注解。小项目用注解快但推荐系统里 SQL 比较复杂多表关联、动态条件我建议用 XML。在 mybatis-config.xml 里开启驼峰命名自动映射这样数据库的user_id能自动映射到 Java 的userId省掉大量 resultMap 配置。settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settingslogImpl设成 STDOUT_LOGGING 后控制台会打印实际执行的 SQL 和参数调试阶段这是你的后悔药——SQL 写错了、参数传空了一眼就能看出来。上线前记得关掉或者改成日志框架。Mapper 接口和 XML 的对应关系要严格namespace 写接口全限定名每个select的 id 对应接口方法名parameterType 和 resultType 别写错。我见过太多人因为 namespace 少写一个字母启动时报 Invalid bound statement查半天。3. 推荐算法落地从评分矩阵到推荐结果3.1 协同过滤的核心思路推荐算法听着玄学但最常用的协同过滤Collaborative Filtering本质就一句话找到和你口味相似的人把他们喜欢但你没看过的电影推给你。分两种UserCF 是找相似用户ItemCF 是找相似物品。电影推荐场景下 ItemCF 更稳定因为电影的数量和属性相对固定而用户口味会漂移。核心步骤三步第一构建用户-电影评分矩阵第二计算电影之间的相似度用余弦相似度或皮尔逊相关系数第三根据用户已评分的电影加权预测未评分电影的分数取 TopN 推荐。相似度计算用余弦相似度# 余弦相似度衡量两部电影被同一批用户打分时的向量夹角 import math def cosine_sim(vec_a, vec_b): # vec_a, vec_b 是两部电影在各用户上的评分向量 dot sum(a * b for a, b in zip(vec_a, vec_b)) norm_a math.sqrt(sum(a * a for a in vec_a)) norm_b math.sqrt(sum(b * b for b in vec_b)) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b)这段是理解原理用的实际项目里用 Java 实现。dot是点积norm_a和norm_b是模长分母为零时返回 0 避免除零异常。评分向量里缺失的值补 0表示没打过分这是最常见的处理方式。3.2 Java 实现 ItemCF 的 Service 层代码实际项目里推荐逻辑写在 Service 层核心方法分两块算相似度矩阵、生成推荐列表。Service public class RecommendService { Autowired private RatingMapper ratingMapper; Autowired private MovieMapper movieMapper; // 为指定用户生成推荐列表 public ListMovie recommendForUser(int userId, int topN) { // 1. 拿到该用户已评分的电影及分数 ListRating myRatings ratingMapper.selectByUserId(userId); MapInteger, Double myMap new HashMap(); for (Rating r : myRatings) { myMap.put(r.getMovieId(), r.getScore()); } // 2. 对每部未看过的电影计算预测分数 ListMovie allMovies movieMapper.selectAll(); ListMovie candidates new ArrayList(); for (Movie m : allMovies) { if (myMap.containsKey(m.getId())) continue; // 跳过已看过的 double predict predictScore(m.getId(), myMap); m.setPredictScore(predict); candidates.add(m); } // 3. 按预测分数降序取前 topN candidates.sort((a, b) - Double.compare(b.getPredictScore(), a.getPredictScore())); return candidates.subList(0, Math.min(topN, candidates.size())); } // 预测某部电影的分数用相似电影的评分加权 private double predictScore(int movieId, MapInteger, Double myMap) { double numerator 0.0, denominator 0.0; for (Map.EntryInteger, Double entry : myMap.entrySet()) { int ratedMovie entry.getKey(); double sim SimilarityUtil.cosine(movieId, ratedMovie); if (sim 0) continue; // 负相关或无关的忽略 numerator sim * entry.getValue(); denominator Math.abs(sim); } return denominator 0 ? 0 : numerator / denominator; } }recommendForUser是入口先拿用户已有评分构建 Map再遍历所有电影排除已看过的对候选电影逐个预测分数最后排序取 TopN。predictScore是加权平均相似度越高、用户给那部电影的分数越高预测分就越高。sim 0时跳过因为负相关意味着喜欢那部就不喜欢这部不该参与正向推荐。3.3 相似度矩阵的缓存策略上面的代码有个性能问题每次推荐都要实时计算相似度如果电影有几千部、用户几百个计算量是 O(电影数 × 用户评分数)响应会非常慢。常见做法是离线预计算相似度矩阵存到 recommend 表或者 Redis 里推荐时直接查。我一般会写一个定时任务每天凌晨跑一次全量相似度计算把每部电影的 Top50 相似电影存下来。这样在线推荐时只需要查表做加权响应时间从秒级降到毫秒级。定时任务用 Spring 的Scheduled注解就能搞定cron 表达式配0 0 3 * * ?表示每天凌晨三点执行。注意相似度矩阵的大小是电影数的平方如果电影超过一万部内存可能扛不住。这时候要么只存 TopN 相似对要么上分布式计算框架别硬扛。4. 避坑与排查那些让我加班到凌晨的问题4.1 中文乱码从数据库到页面的全链路排查现象电影标题存进去是肖申克的救赎页面上显示??????。原因字符集不统一。数据库建表时用了 latin1JDBC 连接串没指定编码Tomcat 的 URIEncoding 没配。解决三处一起改。数据库和表用 utf8mb4JDBC URL 加?useUnicodetruecharacterEncodingutf8Tomcat 的 server.xml 里 Connector 加URIEncodingUTF-8。三处缺一处都可能乱码这是血泪经验。4.2 事务不生效Service 方法内部调用自己的坑现象用户打分后rating 表插入了但 movie 表的 avg_score 没更新明明加了Transactional。原因Spring 的声明式事务基于 AOP 代理同一个类内部方法直接调用this.xxx()不走代理事务注解形同虚设。解决把更新 avg_score 的逻辑抽到另一个 Service 类里或者通过 ApplicationContext 拿到自己的代理对象再调用。最省事的办法是拆成两个 Service别在一个类里自己调自己。4.3 推荐结果为空冷启动用户怎么处理现象新注册用户打开推荐页一片空白。原因协同过滤依赖历史评分数据新用户没有任何评分记录算不出相似度。解决做降级策略。新用户没有评分时推荐热门电影按 avg_score 排序取 TopN或者让用户注册时勾选几个喜欢的类型按类型推荐。这个兜底逻辑一定要有否则新用户体验直接翻车。4.4 MyBatis 返回 null字段名和属性名对不上现象查询返回的对象里某些字段是 null但数据库里明明有值。原因数据库字段是avg_scoreJava 属性是avgScore但没开驼峰映射或者 resultMap 里列名写错了。解决确认 mybatis-config.xml 里mapUnderscoreToCamelCase设为 true。如果用了 resultMap逐字段核对 column 和 property 的对应关系。别偷懒这个错误排查起来最费时间。4.5 分页查询越界最后一页数据量不足现象翻到最后一页时报 IndexOutOfBoundsException。原因分页计算时(pageNum - 1) * pageSize超过了总记录数或者 subList 的结束索引超出列表大小。解决用 PageHelper 插件自动处理分页或者手动计算时对结束索引取Math.min(end, list.size())。上面推荐代码里的subList(0, Math.min(topN, candidates.size()))就是这个道理。5. 论文与代码怎么对齐让答辩老师挑不出毛病5.1 论文结构和技术章节的写法带论文的项目代码和论文必须能对上。论文一般五章绪论、需求分析、系统设计、系统实现、测试与总结。关键是第三章和第四章——设计章节画 ER 图、架构图、流程图实现章节贴核心代码和运行截图。我一般建议论文里的算法描述和代码逻辑严格一致。比如论文写采用基于物品的协同过滤余弦相似度计算代码里就必须有对应的 cosine 方法别论文写协同过滤、代码里是随机推荐答辩时老师一问就露馅。5.2 推荐效果的验证方法论文里需要证明推荐有效不能只说实现了推荐功能。常用的验证指标是准确率Precision和召回率Recall把用户评分数据按 8:2 分成训练集和测试集用训练集算推荐看推荐结果里有多少在测试集里用户确实打了高分。// 简单的准确率计算推荐列表中用户实际喜欢的比例 public double precision(ListInteger recommended, SetInteger actualLiked) { if (recommended.isEmpty()) return 0.0; int hit 0; for (Integer movieId : recommended) { if (actualLiked.contains(movieId)) hit; } return (double) hit / recommended.size(); }recommended是算法推的 TopN 电影 IDactualLiked是测试集里用户实际打了 4 分以上的电影集合。命中数除以推荐总数就是准确率。这个指标写进论文比空口说推荐效果良好有说服力得多。5.3 一个我常用的答辩准备习惯每次带论文的项目我都会提前把论文里提到的每个技术点代码里对应哪个文件哪一行整理成一张对照表。答辩老师翻到论文某一页问这个功能怎么实现的我能立刻打开对应的 Controller 或 Service 指给他看。这个习惯帮我省了很多临场翻代码的尴尬。另外推荐算法的参数比如 TopN 取多少、相似度阈值设多少一定要能说出选择理由别答随便设的。希望帮到你。本文还有配套的精品资源点击获取