news 2026/10/19 3:26:15

基于协同过滤的Java电影推荐系统:从评分矩阵到Web应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于协同过滤的Java电影推荐系统:从评分矩阵到Web应用

简介:一份基于协同过滤算法的电影推荐系统完整源码,主要面向Java初中级开发者、课程设计/毕业设计人群,以及需要快速搭建推荐模块的流媒体项目。系统以用户历史行为数据为输入,通过协同过滤算法衡量用户或物品之间的相似度,生成个性化电影推荐列表;后端以Java为主,配合JSP、JavaScript与CSS完成前台展示,覆盖数据读取、评分矩阵构建、相似度计算到结果输出的完整链路。压缩包共76个文件,核心组成包括38个Java源文件、15个JSP页面、10个XML配置文件和2个SQL数据库脚本,另有少量JS、CSS及属性文件,整体体积仅1.1MB,结构紧凑,可直接导入IDE运行。目前已有823人学习/下载,可作为课程设计、毕业设计或推荐系统入门实践参考。阅读和运行这套源码,可以直观理解协同过滤的工程实现思路,并在此基础上扩展算法或对接真实业务场景。

1. 一个能跑起来的 Java 协同过滤电影推荐系统:不是算法 Demo,是完整 Web 应用

如果你只是想给项目加一个推荐功能,这个基于协同过滤算法的 Java 电影推荐系统源码,是我见过少有的直接解压就能部署的完整示例。它不是只丢一个算法类给你,而是把用户登录、电影列表、评分提交、推荐结果展示全部串成了 Web 应用,数据库脚本里也准备好了电影数据和评分历史。我拆完之后最深的感触是:协同过滤的数学公式并不复杂,真正花时间的是把用户行为数据喂给算法,再把推荐结果塞回 JSP 页面。适合正在做课程设计的学生,也适合想在自己的业务系统里加“猜你喜欢”模块的 Java 开发者。

2. 项目结构与技术栈拆解:76 个文件如何支撑一套推荐系统

2.1 技术选型:Servlet + JSP + JDBC 的轻量级组合,算法用纯 Java 实现

从摘要信息看,这套项目一共 76 个文件,其中 Java 源文件 38 个、JSP 页面 15 个、XML 配置文件 10 个、JavaScript 脚本 4 个、SQL 脚本 2 个、CSS 样式表 2 个。这个组成结构本身就说明了技术选型:后端以 Java 为核心,前端没有采用前后端分离的框架,而是用 JSP 直接渲染页面,JavaScript 负责评分按钮的交互和局部刷新,CSS 负责样式。它属于典型的“前后端不分离”的 Java Web 项目。

从 pom.xml 和 webapp 目录来看,这套系统建立在 Servlet + JSP + JDBC 这套经典组合之上,部署环境是 Tomcat 和 MySQL。Servlet 收到浏览器请求后调用 Service 层,Service 层调用 DAO 层读写数据库,最后把数据塞给 JSP 渲染。整个过程没有 Spring 那种依赖注入和自动配置,每个对象都是手动 new 出来的。对学习者来说,这种看似“原始”的方式反而更容易看清楚一次请求从浏览器到数据库再回到页面的完整链路;对二次开发来说,改代码也不会被框架的各种约定干扰。

为什么不直接用 Spring Boot?因为这个项目的定位是展示协同过滤算法本身,而不是演示框架用法。Maven 带来的好处是依赖管理和打包标准化,编译、测试、打 war 都能用一条命令完成。JavaScript 和 CSS 在整个项目里是辅助角色,但没有了它们,评分按钮无法把分数提交到后端,推荐列表的异步展示也会退化成整页刷新。轻量级选型的直接收益是:拿到源码后只需要装好 JDK、Maven、Tomcat、MySQL,几乎不需要额外引入组件就能跑起来。

2.2 目录结构:从压缩包到 Maven 工程,每个目录各管一件事

把 upload.zip 解压后,可以看到 data_base、src、pom.xml、readme.txt 这样的标准布局。用目录树表达出来是这样的:

upload.zip ├── data_base │ ├── movie.sql │ └── data.sql ├── src │ ├── main │ │ ├── java │ │ │ ├── entity │ │ │ ├── dao │ │ │ ├── service │ │ │ ├── servlet │ │ │ └── algorithm │ │ ├── resources │ │ │ ├── db.properties │ │ │ └── ...xml │ │ └── webapp │ │ ├── css │ │ ├── js │ │ ├── images │ │ └── *.jsp │ └── test │ └── java ├── pom.xml └── readme.txt

包名和路径是我的整理,实际项目可能略有差异,但职责划分基本一致。entity 包里放 User、Movie、Rating 这类数据对象,字段和数据库表一一对应;dao 包里是 JDBC 访问类,提供按 ID 查询、插入评分、获取评分矩阵等方法;service 包里是电影服务和推荐服务,推荐服务会调用 algorithm 包的算法类;servlet 包是控制器,处理 /login、/register、/rate、/recommend 这类请求;algorithm 包只放着协同过滤相关的相似度计算和推荐生成代码。

webapp 目录是 Web 应用的根目录,15 个 JSP 页面按功能可以分成登录注册、电影列表、电影详情、评分页、推荐结果这几组。css、js、images 各自存放静态资源。src/test/java 下有一些单元测试类,通常针对评分矩阵构建和相似度计算做验证,Maven 执行 package 时会自动运行,不想跑可以加 -DskipTests 跳过。

从二次开发的视角看,这种分层最大的好处是改动可以被限制在局部范围。你想换推荐算法,只需要动 algorithm 和 service 两个包;你想加一个“我看过”的收藏功能,在 entity 里加实体、在 dao 里加方法、在 servlet 里加接口、在 webapp 里加页面,四步走完,不影响原有推荐流程。

2.3 数据库脚本:movie.sql 建表,data.sql 灌数据

data_base 里的两个 SQL 脚本分工很清晰:movie.sql 负责建库建表,data.sql 负责插入业务数据。这种拆分在真实项目里很常见——重新建一个干净的环境只需要执行 movie.sql,保留业务数据的环境就不要再执行它;而 data.sql 可以反复用来重置测试数据。

电影推荐系统的核心表主要有三张,表结构大致如下:

CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, genres VARCHAR(200), release_year INT, poster_url VARCHAR(500) ); CREATE TABLE t_rating ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, score DECIMAL(3,1) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(id), FOREIGN KEY (movie_id) REFERENCES t_movie(id) );

t_user 保存用户名和密码,t_movie 保存电影标题、类型、上映年份、海报路径,t_rating 保存某用户对某电影的评分。这三张表构成了协同过滤的基础:算法并不需要理解剧情和导演,只看 t_rating 里的 user_id、movie_id、score 三列,就能构建出“用户-电影评分矩阵”,进而计算用户之间的口味相似度。

字段设计上有几个细节值得注意。score 用 DECIMAL(3,1),意味着最大可以存 99.9,实际业务里我们的评分只用到 1 到 5,保留一位小数是为了支持 4.5 这样的评分。username 加了 UNIQUE 约束,避免注册重名用户。外键约束保证评分不会指向不存在的用户或电影。data.sql 中的 user_id、movie_id 必须和 movie.sql 中自增主键对应上,这就是为什么执行顺序必须是 movie.sql 在前。

如果后续要扩展基于内容的推荐,可以在 t_movie 里加 genres 字段的拆表,比如存成 t_genre 关联表,但当前这套核心逻辑只依赖三张表,加扩展表不会影响已经写好的推荐代码。

2.4 配置文件:pom.xml、web.xml、db.properties 各管一件事

Maven 项目的配置集中在 pom.xml。从文件构成里的 10 个 XML 来看,pom.xml 之外,webapp/WEB-INF 下会有 web.xml,resources 下还会有日志或数据源相关的 XML 配置。pom.xml 定义了打包方式为 war,依赖包括 Servlet API、JSP API、MySQL 驱动、JSTL 以及 JSON 处理库。对于推荐系统来说,算法部分不需要额外依赖,工具类足够。

数据库连接配置通常放在 db.properties 中,内容类似这样:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/movie_recommend?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

这里每个参数都有实际意义。useUnicode 和 characterEncoding=utf8 解决数据库读写中文乱码;useSSL=false 避免 MySQL 8 在本地连接时做无意义的 SSL 握手;serverTimezone=Asia/Shanghai 告诉驱动服务器时区,否则 MySQL 8 会报 CST 时区错误。jdbc.username 和 jdbc.password 要根据本机环境修改,项目里默认的密码通常不是你的密码,不改的话第一步连接就会失败。

web.xml 是 Servlet 容器的部署描述文件,里面配置了欢迎页、Servlet 映射、URL 模式,还会有一个 CharacterEncodingFilter 把请求和响应统一设置成 UTF-8。如果你发现有些页面中文正常、有些页面中文乱码,优先检查这个过滤器是否覆盖了所有路径。pom.xml 里的 10 个 XML 数量看起来多,但日常需要改的就三个:web.xml 改路由,db.properties 改数据库连接,pom.xml 改依赖版本。其余 XML 只是 Maven 插件的配置,实际开发中很少会去动。

3. 核心模块拆解:协同过滤算法从评分矩阵到 Top-N 推荐

3.1 算法链路:基于用户的协同过滤,找口味相似的人

协同过滤分成基于用户(UserCF)和基于物品(ItemCF)两种实现。UserCF 的核心逻辑是:先找到和当前用户兴趣最相似的 K 个用户,再把这 K 个用户评分高且当前用户没看过的电影推荐出来。ItemCF 则反过来,先建立电影与电影的相似关系,再根据用户看过的电影去推荐相似电影。本项目采用的是基于用户的协同过滤,这个选择在课程设计和中小型数据集上很常见。

从抽象角度看,算法链路可以拆成四步:第一步从 t_rating 表把所有评分记录加载进内存,组织成 userId -> movieId -> score 的两级 Map;第二步计算当前用户和其他每个用户的相似度;第三步按相似度排序,取前 K 个用户作为最近邻;第四步把最近邻评分过、而当前用户没有评分过的电影加权汇总,排序后取前 N 部输出。

为什么要用两级 Map 而不是直接用 SQL 联表查询?因为相似度计算需要反复访问每个用户的评分向量。如果每次访问都去执行数据库查询,会产生大量重复 SQL,响应时间会变得不可接受。一次性加载到内存,虽然会在 JVM 堆里占用一块空间,但对于几千用户、几百部电影的数据量完全够用。如果你换到几十万用户的数据规模,就需要考虑用 Spark 或离线计算框架,那是另一个话题。

3.2 评分矩阵构建:把 ResultSet 行转换成嵌套 Map

DAO 层从数据库读出来的是 ResultSet 行,每一行是一条评分记录。算法层需要的是以用户为外层键、以电影为内层键的嵌套结构。转换的核心代码是这样一段:

public Map<Integer, Map<Integer, Double>> loadRatingMatrix() { Map<Integer, Map<Integer, Double>> matrix = new HashMap<>(); String sql = "SELECT user_id, movie_id, score FROM t_rating"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { int userId = rs.getInt("user_id"); int movieId = rs.getInt("movie_id"); double score = rs.getDouble("score"); matrix.computeIfAbsent(userId, k -> new HashMap<>()) .put(movieId, score); } } catch (SQLException e) { e.printStackTrace(); } return matrix; }

代码里 computeIfAbsent 是这段逻辑的精华。它首先查看 matrix 中是否存在 userId 这个键,如果不存在,就执行后面的 new HashMap<>() 创建一个空 Map 并放入 matrix,然后把 movieId 和 score 放进用户对应的评分 Map 中。如果用传统的写法,需要先写 if (matrix.get(userId) == null) 再 new,代码会多出好几行,而且容易漏掉 else 分支。

参数说明:userId 和 movieId 分别对应 t_rating 表的外键列,score 通过 getDouble 从 DECIMAL 字段转成 double。这个方法返回后,matrix.size() 就是用户数,某个 user 的 Map size 就是该用户的评分电影数。推荐服务调用这个矩阵时,建议在构造函数里只加载一次,每次请求都重新加载全表不是好习惯。

3.3 相似度计算:余弦相似度实现里的两个边界坑

计算相似度时,项目通常会用余弦相似度或皮尔逊相关系数。余弦相似度的公式是向量夹角的余弦,两个用户共同评分的电影方向越一致,余弦值越大。拆出来的核心代码大致如下:

private double cosineSimilarity(Map<Integer, Double> user1, Map<Integer, Double> user2) { double dotProduct = 0.0; double norm1 = 0.0; double norm2 = 0.0; for (Map.Entry<Integer, Double> entry : user1.entrySet()) { int movieId = entry.getKey(); double score = entry.getValue(); norm1 += score * score; if (user2.containsKey(movieId)) { dotProduct += score * user2.get(movieId); } } for (double score : user2.values()) { norm2 += score * score; } if (norm1 == 0.0 || norm2 == 0.0) return 0.0; return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }

第一个坑是范数计算方式。上面这段代码计算 norm1 时遍历了 user1 的所有评分,norm2 遍历了 user2 的所有评分,但 dotProduct 只在两个用户共同评分过的电影上累加。这意味着没有共同评分的电影得分为 0,不会对点积产生贡献,但会体现在范数里。这是一种处理稀疏矩阵的常规做法,如果你把缺失评分全部补成 0,计算得到的相似度会被拉向评分数量多的用户。

第二个坑是零值判断。新注册用户很可能没有任何评分,此时 norm1 或 norm2 为 0,直接做除法会得到 NaN,下面的排序就会失效。所以代码里先判断分母是否为 0,是则返回 0.0,表示两个用户没有相似度。这个防御性判断在真实数据中非常关键,因为绝大多数注册用户在一开始都是冷启动状态。

如果要把余弦改成皮尔逊,只需要在进入循环前给每个用户算一个平均分,然后用 score - avg 代替 score,原理是消除用户打分尺度的偏差。套路不变,工程上改动很小。

3.4 最近邻推荐:加权累加候选分数,Top-N 输出

得到当前用户与其他用户的相似度之后,推荐生成的逻辑就是“找邻居、攒候选、排序”。通常的做法是取相似度最高且大于某个阈值的 K 个用户,然后遍历每个邻居的评分记录,把邻居的相似度乘以他对电影的评分作为加权分数,累加到候选电影上。这段逻辑的伪代码如下:

// similarityMap: 当前用户与每个邻居的相似度 // K: 最近邻个数, TOP_N: 推荐结果数量 Map<Integer, Double> candidate = new HashMap<>(); for (Map.Entry<Integer, Double> simEntry : similarityMap.entrySet()) { int neighborId = simEntry.getKey(); double sim = simEntry.getValue(); if (sim <= 0) continue; Map<Integer, Double> neighborRatings = ratingMatrix.get(neighborId); for (Map.Entry<Integer, Double> ratingEntry : neighborRatings.entrySet()) { int movieId = ratingEntry.getKey(); if (currentUserRatings.containsKey(movieId)) continue; candidate.merge(movieId, sim * ratingEntry.getValue(), Double::sum); } } List<Integer> recommendIds = candidate.entrySet().stream() .sorted(Map.Entry.<Integer, Double>comparingByValue().reversed()) .limit(TOP_N) .map(Map.Entry::getKey) .collect(Collectors.toList());

merge 方法的第一个参数是电影 ID,第二个参数是当前邻居贡献的加权分,第三个参数是重映射函数 Double::sum。它的效果是:如果候选池里还没有这部电影,就把加权分放进去;如果已经有了,就把两次加权分相加。多个相似用户都打高分的电影,最终得分就会更高,这正好符合推荐系统的直觉。

这里还有一个容易翻车的点:候选池里的分数是累加值,不是标准化的概率值,不能直接拿来做不同用户之间的横向对比,但在同一个用户的排序场景下足够。推荐 ID 列表生成后,Service 层再去 t_movie 表查询电影标题、海报和类型,填充成一个完整的推荐响应。算法层和 Web 层在这个位置可以被清晰地分开:算法只返回 ID 列表,Web 层负责拼装展示数据。

4. 从源码到跑起来:数据库导入、Maven 打包与 Tomcat 部署

4.1 环境准备:四件套,版本别太新

跑这个项目不需要复杂的中间件,四样东西就够:JDK 8 或 11、Maven 3.6 以上、Tomcat 8.5 或 9、MySQL 5.7 或 8.0。我建议用 JDK 8 和 MySQL 5.7 的组合,兼容性最好。如果你本机只装了高版本 JDK,也不是不行,但遇到模块访问报错时,大概率是 JDK 版本过高导致的。

在开始之前,建议用命令行确认:

java -version mvn -v mysql --version

这三条命令都能正常输出版本信息,就说明基础环境没大问题。Tomcat 不需要安装,解压之后放在一个没有空格和中文的路径下,比如 D:\apache-tomcat-9.0。目录带特殊字符有时候会让部署路径解析出错,这种问题排查起来非常玄学。开发时也可以直接用集成开发环境自带的服务器插件,但第一次建议手动部署一次,能更清楚 war 包从生成到解压发生了什么。

4.2 初始化数据库:先 movie.sql 后 data.sql

在项目根目录中有一个 data_base 文件夹,里面的两个 SQL 脚本是顺序敏感的。进入命令行执行:

mysql -u root -p < data_base/movie.sql mysql -u root -p < data_base/data.sql

第一条创建数据库和表结构,第二条向表里插入用户、电影、评分。如果反过来执行,data.sql 里的 INSERT 语句会因为表不存在而失败。如果你用的是数据库图形化客户端工具,可以直接把脚本文件拖进去运行,效果一样,但同样要按顺序执行。

导入完成后,用 SQL 验证一下数据是否到位:

USE movie_recommend; SELECT COUNT(*) AS user_count FROM t_user; SELECT COUNT(*) AS rating_count FROM t_rating;

看到 user_count 和 rating_count 不为 0,才能继续往下走。如果 rating_count 是 0,说明 data.sql 没有执行成功,推荐算法必然会返回空列表,因为评分矩阵是空的。

接下来修改数据库连接配置。打开 src/main/resources/db.properties,把 jdbc.url 中的库名、jdbc.username、jdbc.password 改成实际值。注意 MySQL 8 的驱动类是 com.mysql.cj.jdbc.Driver,MySQL 5.7 的老驱动是 com.mysql.jdbc.Driver,pom.xml 里依赖版本如果不匹配,会在后续启动时报 ClassNotFoundException。

4.3 Maven 打包与部署:war 从生成到解压

在项目根目录执行:

mvn clean package -DskipTests

第一次执行会下载很多依赖,如果网络访问 Maven 中央仓库很慢,建议在 settings.xml 里配置国内镜像。打包完成后,target 目录下会生成 war 文件。war 是 Web 应用的归档格式,里面包含了 class 文件、JSP、静态资源、依赖 jar 和配置文件。

部署到 Tomcat 有两种常见方式。一种是直接把 war 文件复制到 Tomcat 的 webapps 目录,启动 Tomcat 后它会自动解压并部署;另一种是在集成开发环境中配置 Tomcat Server,使用 war exploded 模式,可以把 exploded 目录作为发布目录,这样修改 Java 文件或 JSP 后可以热部署,开发效率高。对第一次跑通项目来说,我建议用第一种,因为它能看到一个完整的“复制 -> 解压 -> 启动”过程。

启动 Tomcat 后,观察 logs 目录下的 catalina.out。看到类似 “Deployment of web application archive ... has finished” 的日志,说明部署成功。如果启动过程中抛出异常,重点看第一行的异常类型,把关键行复制到搜索引擎里基本能定位出原因。

4.4 运行验证:按这个顺序走查功能

打开浏览器,访问 http://localhost:8080/应用名/ 或者直接 http://localhost:8080/,具体路径取决于 Tomcat 的配置。页面打开后建议按这样的顺序走查:先注册一个新账号,然后登录,进入电影列表页,打开一部电影的详情,给它评分,再进入推荐页面查看推荐结果。

这里有一句非常重要的提醒:不要用一个刚注册且没有任何评分的账号直接看推荐页。协同过滤推荐系统本质上依赖历史行为数据,一个新用户没有评分,算法找不到相似用户,推荐结果大概率是空的。测试时最好用 data.sql 中已有的账号登录,或者先给五六部电影打分,再刷新推荐页,这样能看到“猜你喜欢”的实际效果。如果你看到推荐页为空,先不要怀疑源码有问题,先检查自己有没有评分数据。

页面走查时打开浏览器开发者工具的 Network 面板。接口返回 500 时,去 Tomcat 日志里找对应的异常堆栈;静态资源加载 404 时,检查访问路径是否带了正确的上下文路径。这两类问题覆盖了这个项目部署后 80% 的故障。

5. 避坑指南:协同过滤电影推荐系统常见问题排查

这一章把我在本地部署和二次开发过程中踩过的问题按“现象 -> 原因 -> 解决”整理成了五条。每一条都可以在项目源码里直接定位,适合第一次跑这个系统的人对照排查。

5.1 问题:Tomcat 启动后接口 500,日志打印 ClassNotFoundException

现象是登录页面能打开,但是点击登录或加载电影列表时页面报 500,Tomcat 控制台或 catalina.out 里出现 java.lang.ClassNotFoundException,常见缺的是 com.mysql.jdbc.Driver 或 javax.servlet 相关类。

造成这个问题的原因通常是依赖 jar 没有打进 war 包。在 pom.xml 中,如果 mysql-connector-java 依赖的 scope 被写成 provided,Maven 打包时不会把它放进 WEB-INF/lib,运行时就找不到驱动类。另一种情况是集成开发环境的部署方式不对,只同步了编译后的 class,没有同步依赖 jar。

解决方法是先打开 pom.xml 检查依赖的 scope,把涉及运行期的依赖改成默认的 compile;然后执行 mvn clean package,解压生成的 war 文件,查看 WEB-INF/lib 目录下是否包含 mysql-connector 的 jar。如果缺失,说明打包姿势不对,换用 Maven 打包后的 war 重新部署。只要 war 里 lib 目录有了对应 jar,再启动就不会继续报这个错。

5.2 问题:数据库连接失败,报 Public Key Retrieval is not allowed

现象是项目启动正常,但登录提交后抛数据库连接异常,错误信息里有 “Public Key Retrieval is not allowed” 或者 “Communications link failure”。

原因是 MySQL 8 默认的认证插件是 caching_sha2_password,旧版 JDBC 驱动在建立连接时没有权限获取服务器公钥,因此服务器拒绝连接。这个问题在本地数据库从 MySQL 5.7 升级到 8.0 后特别容易遇到,因为连接 URL 还是旧的那一套。

解决方法是修改 db.properties 中的 jdbc.url,加上三个参数:allowPublicKeyRetrieval=true、useSSL=false、serverTimezone=Asia/Shanghai。如果加了参数仍然报错,还需要检查驱动的 Maven 依赖版本是否和 MySQL 版本对应,MySQL 8 必须使用 mysql-connector-java 8.x 的驱动。修改完配置文件后要重新打 war 包,因为 db.properties 位于 classes 目录下,不会因为 Tomcat 解压旧 war 而自动更新。

5.3 问题:推荐列表永远为空,协同过滤像没生效

现象是评分功能正常,数据库里也能查到评分记录,但推荐页就是显示“暂无推荐”。这个问题最容易让人怀疑算法代码写错了,实际上大多数时候是数据或参数的问题。

原因有几种。第一种是当前用户没有评分记录,余弦相似度计算时范数为 0,推荐池为空;第二种是最近邻参数设置太严格,比如要求相似度大于 0.8 还必须有 10 个共同评分电影,当用户很少时一个邻居都选不出来;第三种是相似度计算结果全是 0 或负数,候选池在过滤时被全部跳过。还有一种隐蔽的原因是评分矩阵加载时用了全表查询,但新插入的评分还没来得及提交,导致矩阵里根本没有当前用户。

解决方法是先确认数据库 t_rating 表里当前用户确实有评分,再用一个已知评分数最多的账号做测试。如果还是空,把算法代码中的 nearestNeighborCount 调大一点,把相似度阈值调低,并在代码里输出 candidate.size() 和 Top 分数,看到输出数值才能定位是哪一步断了。我一般会在推荐生成入口加一行日志,输出当前用户评分数和邻居数量,这是最快缩小范围的办法。

5.4 问题:页面能打开,但 CSS/JS 全部 404,样式错乱

现象是 JSP 渲染出了文字,但所有按钮没有背景色,布局乱成一团,打开浏览器开发者工具能看到 css、js 请求的状态是 404。

原因是资源地址没有包含项目的上下文路径。比如 JSP 里直接写 href="css/style.css",在 Tomcat 部署后,浏览器会解析成当前域名下的 css/style.css,而不是 /项目名/css/style.css。如果项目的 servlet 映射又拦截了 / 路径,Tomcat 默认的静态资源处理器会被绕过,静态文件就更难被访问到。

解决方法是把 JSP 里的静态资源路径统一改成 ${pageContext.request.contextPath}/css/style.css 这种写法。同时在 web.xml 中确认静态资源是否被 Servlet 拦截,如果是,需要配置一个专门的映射来放行 css、js、images 目录。排查时可以打开 Network 面板,比较请求 URL 和磁盘上的实际文件路径,一眼就能看出差一段路径。

5.5 问题:中文乱码,电影标题显示成问号

现象是数据库客户端中看数据是中文,网页上却显示问号;或者通过表单提交的中文,存进数据库后变成乱码。这属于经典的多环节编码问题。

原因是编码链路没有统一。数据库表或数据库本身的字符集不是 utf8mb4,JDBC 连接 URL 里没有指定 characterEncoding=utf8,JSP 页面没有声明 UTF-8 的 pageEncoding,web.xml 里缺少强制设置请求编码的过滤器。只要这四个环节里有一个不匹配,中文就会在某一环被打回原形。

解决方法也是四处统一。第一,在 movie.sql 建库语句里加上 DEFAULT CHARSET=utf8mb4;第二,在 db.properties 的 jdbc.url 中加上 useUnicode=true&characterEncoding=utf8;第三,在每个 JSP 页面顶部加上 <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>;第四,在 web.xml 里配置一个 CharacterEncodingFilter,把 request 的编码统一为 UTF-8。改完后清掉 Tomcat 缓存、重启,再刷新页面验证。不要改一处就停手,这个排查必须沿着请求路径把所有环节过一遍。

6. 二次开发:把协同过滤改造成可切换的混合推荐

6.1 改造点一:把相似度算法抽成策略接口,支持 A/B 对比

目前相似度计算通常写在某个工具类里,方法被直接调用。二次开发时第一步可以抽象出一个 Similarity 接口,让推荐服务依赖接口而不是具体实现。这样就能在不改推荐流程的前提下,对比余弦相似度和皮尔逊相关系数的推荐效果。

public interface Similarity { double calculate(Map<Integer, Double> v1, Map<Integer, Double> v2); }

然后实现 CosineSimilarity 和 PearsonSimilarity 两个类。推荐服务通过构造器注入一个 Similarity 实例,配合配置文件里的一个开关,就能在部署时切换算法。改造之后,你会发现后续加一个 Jaccard 相似度实现只需要新增一个类,其他代码完全不动。

6.2 改造点二:加入热门榜兜底,解决冷启动

前面反复提到冷启动问题,实际生产环境不会让推荐页空着。常见的做法是在推荐服务入口处加一个判断:如果当前用户的评分数量小于某个阈值,直接返回热门电影榜;否则走协同过滤。热门榜可以简单实现为按 t_rating 表平均分和评分人数排序的 SQL。把兜底逻辑和协同过滤封装成一个组合推荐器,对外接口不变,调用方不需要感知内部策略。

6.3 改造点三:给推荐结果加一层缓存

协同过滤的时间复杂度集中在相似度计算,同一用户短期内反复刷新推荐页,计算结果其实一样。可以加一个 ConcurrentHashMap 做缓存,key 是 userId,value 是推荐电影 ID 列表。用户产生新的评分行为后,删掉对应用户的缓存。这样可以显著降低推荐接口的响应时间,也能减少对数据库的重复压力。如果你以后接入 Redis,把缓存组件替换掉即可。

从那以后,我每次接手推荐系统项目,都会先用最小数据集把“数据 -> 算法 -> 推荐结果”这条链路跑通,再开始动手改算法。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 3:26:03

open-code-review:基于CLI+Git+LLM的可审计代码审查新范式

1. 这不是又一个“AI写代码”工具——open-code-review的本质是重构代码审查的权力结构你有没有遇到过这样的场景&#xff1a;PR提了三天没人点合并&#xff0c;团队里资深工程师忙得连咖啡都顾不上续&#xff0c; junior同学写的逻辑明明有隐患&#xff0c;但Code Review Comm…

作者头像 李华
网站建设 2026/10/19 3:25:53

CCS 6.1.3安装与C2000兼容性深度指南

简介&#xff1a;本资源为TI官方集成开发环境Code Composer Studio&#xff08;CCS&#xff09;6.1.3完整安装包&#xff0c;专为嵌入式系统开发者、高校电子/自动化专业师生及TI处理器&#xff08;如MSP430、C2000、C6000系列&#xff09;学习者设计&#xff0c;解决旧版本兼容…

作者头像 李华
网站建设 2026/10/19 3:25:38

开源代码评审CLI工具:Git集成+本地LLM驱动的自动化Code Review

1. 项目概述&#xff1a;这不是又一个“AI写代码”玩具&#xff0c;而是一套可嵌入真实开发流水线的开源代码评审协作者open-code-review 这个名字乍看平平无奇&#xff0c;但拆开来看——“open”不是指“开源”那么简单&#xff0c;它指向的是开放协议、开放集成、开放上下文…

作者头像 李华
网站建设 2026/10/19 3:25:18

开源代码审查工作流:CLI+Git驱动的LLM静态分析实践

1. 项目概述&#xff1a;这不是一个“工具”&#xff0c;而是一套可落地的开源代码审查工作流open-code-review 这个名字乍看像某个具体软件&#xff0c;但实际它代表的是一类正在快速成型的工程实践范式——用开源、透明、可审计的方式&#xff0c;把大语言模型&#xff08;LL…

作者头像 李华
网站建设 2026/10/19 3:25:17

SRS WebRTC播放源码深度解析:从信令协商到NACK重传的工程实践

1. 从一次卡顿排查说起&#xff1a;为什么要啃 WebRTC 播放源码去年帮一个做在线教育的朋友排查直播卡顿问题&#xff0c;场景很典型&#xff1a;老师端推流正常&#xff0c;学生端用浏览器看 WebRTC 流&#xff0c;网络稍微抖一下画面就花&#xff0c;声音断断续续&#xff0c…

作者头像 李华
网站建设 2026/10/19 3:24:56

CNN识别花卉绽放状态:从毕设选题到模型部署全流程解析

每年毕业季帮学弟学妹把关毕设选题的时候&#xff0c;总能看到这类题目&#xff1a;《基于Python和CNN卷积神经网络识别花卉是否绽放》。去年A同学拿过来的时候我第一反应是"又是个标准图像分类"&#xff0c;但真正陪他把整套流程跑下来才发现&#xff0c;这个题目里…

作者头像 李华