
简介本资源是一份面向计算机专业本科生及Java Web初学者的毕业设计类文档聚焦个性化影片推荐系统的完整开发实践解决传统影视平台推荐精准度低、用户黏性不足等实际问题。文档以JSP技术栈与MySQL数据库为核心系统覆盖需求分析、总体设计含结构、数据、功能与安全设计、详细设计关键模块代码、数据库访问逻辑、界面实现及功能测试全流程内容兼具理论严谨性与工程落地性。资源为单个3.06MB的DOCX文件内含摘要、英文摘要、目录、绪论、相关技术介绍JSP、MyEclipse环境、系统设计与实现章节及总结结构完整、排版规范适合作为课程设计参考、毕设开题与写作范本。目前已有64人学习下载读者可直接获取从需求建模到测试验证的全周期技术方案、可复用的设计思路与标准化文档模板。1. 为什么用 Java 做个性化影片推荐系统不是“炫技”而是工程落地的刚性选择你可能已经见过太多“基于 Python 的电影推荐系统”——Jupyter Notebook 里跑个 Surprise 库、画几条准确率曲线就收工。但真要上线一个能扛住百人并发、用户行为实时写入、海报图评分标签全链路渲染、后台还能查漏补缺的影片推荐服务Java 生态的稳定性、线程模型、事务控制和 Tomcat JSP MySQL 这套被验证十年以上的 Web 工程栈反而成了最不玄学的选择。这不是复古是权衡Spring Boot 虽好但对初学者或教学型项目而言直接上 Servlet/JSP 更透明——你能看清 session 怎么存、request 怎么解析、SQL 怎么拼、JSP 页面里${movie.title}是怎么从 ResultSet 里一层层冒出来的。本系统不追求算法 SOTA目标明确用最朴素的协同过滤User-Based CF 内容特征加权跑通“用户登录 → 浏览历史入库 → 推荐列表生成 → JSP 渲染展示”全链路所有代码可本地一键部署MySQL 表结构清晰可查Tomcat 日志能定位到某次推荐失败的具体 SQL 执行耗时。适合课程设计、毕设答辩、中小团队快速验证推荐逻辑也适合作为 Java Web 工程能力的“压力测试器”。2. 搭建最小可行环境JDK 8 Tomcat 8.5 MySQL 5.7 三件套实操这套组合不是怀旧是兼容性与教学友好性的平衡点。JDK 8 提供稳定的 Lambda 和 Stream API避免 JDK 17 的模块化陷阱Tomcat 8.5 对 JSP 2.3 和 Servlet 3.1 支持成熟且web.xml配置直观MySQL 5.7 的utf8mb4字符集能完整存下中文片名和用户昵称事务隔离级别可控。下面步骤全部基于 Linux/macOS 终端Windows 用户请将export替换为set路径分隔符\改/。2.1 JDK 8 安装与验证别跳过JAVA_HOME的绝对路径校验# 下载并解压以 macOS 为例Linux 同理 curl -O https://corretto.aws/downloads/resources/8.402.08.1/amazon-corretto-8.402.08.1-macos-x64-jdk.tar.gz tar -xzf amazon-corretto-8.402.08.1-macos-x64-jdk.tar.gz sudo mv jdk8.0_402-amzn/ /Library/Java/JavaVirtualMachines/ # 设置 JAVA_HOME关键必须指向 jdk 目录不是 jre export JAVA_HOME$(/usr/libexec/java_home -v 1.8) export PATH$JAVA_HOME/bin:$PATH # 验证输出应为 1.8.0_402且 javac 可用 java -version javac -version提示/usr/libexec/java_home -v 1.8是 macOS 官方推荐方式比硬写/Library/Java/...更可靠Linux 用户可用update-alternatives --config java切换。若javac报错“command not found”说明PATH未生效重启终端或执行source ~/.zshrc。2.2 Tomcat 8.5 部署解压即用但必须改conf/tomcat-users.xml# 下载官方二进制包非 Docker 镜像避免容器网络调试干扰 curl -O https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.99/bin/apache-tomcat-8.5.99.tar.gz tar -xzf apache-tomcat-8.5.99.tar.gz mv apache-tomcat-8.5.99 ~/tomcat8 # 启用 Manager App用于后续上传 WAR 包 # 编辑 ~/tomcat8/conf/tomcat-users.xml在 /tomcat-users 前插入 # role rolenamemanager-gui/ # role rolenamemanager-script/ # user usernameadmin password123456 rolesmanager-gui,manager-script/参数说明manager-gui用于浏览器访问http://localhost:8080/manager/htmlmanager-script用于 curl 上传 WAR。密码明文存储仅限开发环境生产环境需配合 SSL 和 LDAP。启动命令~/tomcat8/bin/startup.sh日志在logs/catalina.out首次启动后访问http://localhost:8080出现 Apache Tomcat 页面即成功。2.3 MySQL 5.7 安装与初始化字符集必须设为utf8mb4# macOS 使用 HomebrewLinux 用 yum/apt brew install mysql5.7 brew services start mysql5.7 # 初始化 root 密码并创建推荐系统专用库 mysql -u root -e SET NAMES utf8mb4; CREATE DATABASE movie_recommend DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER recuserlocalhost IDENTIFIED BY recpass123; GRANT ALL PRIVILEGES ON movie_recommend.* TO recuserlocalhost; FLUSH PRIVILEGES; 关键点utf8mb4_unicode_ci支持 emoji 和生僻汉字如《阿凡达水之道》的冒号recuser账户权限严格限定在movie_recommend库避免root直连 Web 应用。验证命令mysql -u recuser -precpass123 -D movie_recommend -e SHOW VARIABLES LIKE character_set%;应全部返回utf8mb4。3. 数据库设计5 张表撑起推荐核心拒绝过度范式化推荐系统不是 ERP表结构要服务于查询效率而非理论完美。本设计放弃“用户-标签-影片”三元组中间表用冗余字段换取单表 JOIN 速度评分表不拆分维度因协同过滤只依赖user_id,movie_id,rating三元组。3.1 核心表结构与建表语句直接可执行-- 影片主表含基础信息内容特征向量简化版用逗号分隔标签 CREATE TABLE movies ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, director VARCHAR(100), actors TEXT, -- 存 JSON 字符串或逗号分隔如 张艺谋,巩俐 genres VARCHAR(200), -- 如 剧情,爱情,文艺 year YEAR, poster_url VARCHAR(500), -- 存相对路径如 /images/avator.jpg avg_rating DECIMAL(3,2) DEFAULT 0.00, rating_count INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表最小化设计仅存登录凭证和基础画像 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- 实际应 BCrypt 加密此处明文仅演示 gender ENUM(M,F,O) DEFAULT O, age_group VARCHAR(10), -- 18-25, 26-35 等用于冷启动兜底 reg_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 评分表核心行为数据联合索引加速 User-Based CF 查找 CREATE TABLE ratings ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, rating TINYINT NOT NULL CHECK (rating BETWEEN 1 AND 5), rated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_movie (user_id, movie_id), INDEX idx_movie_user (movie_id, user_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户相似度缓存表避免每次请求都计算定时任务更新 CREATE TABLE user_similarity ( user_id1 INT NOT NULL, user_id2 INT NOT NULL, similarity DECIMAL(5,4) NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (user_id1, user_id2), INDEX idx_user1 (user_id1), INDEX idx_user2 (user_id2) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 推荐结果缓存表按用户 ID 缓存 Top-N 推荐减少实时计算压力 CREATE TABLE recommendations ( user_id INT PRIMARY KEY, movie_ids TEXT NOT NULL, -- 存逗号分隔的 ID 列表如 101,205,333 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;设计理由ratings表的双索引idx_user_movie和idx_movie_user是协同过滤的命脉——查“用户 A 看过哪些片”和“影片 X 被谁评过分”都需毫秒级响应user_similarity表不存全量矩阵N×N 太大只存相似度 0.6 的 Top-K 邻居节省空间recommendations表用TEXT存 ID 列表而非关联表因推荐结果更新频次低每小时批处理且 JSP 层解析字符串比 JOIN 更轻量。3.2 初始化测试数据10 部影片 5 个用户 50 条评分-- 插入影片精简版实际项目应爬取豆瓣或 TMDB INSERT INTO movies (title, director, genres, year, poster_url) VALUES (阿凡达, 詹姆斯·卡梅隆, 动作,科幻,冒险, 2009, /images/avatar.jpg), (泰坦尼克号, 詹姆斯·卡梅隆, 剧情,爱情,灾难, 1997, /images/titanic.jpg), (盗梦空间, 克里斯托弗·诺兰, 动作,科幻,悬疑, 2010, /images/inception.jpg), (肖申克的救赎, 弗兰克·德拉邦特, 剧情,犯罪, 1994, /images/shawshank.jpg), (千与千寻, 宫崎骏, 动画,奇幻,冒险, 2001, /images/sen.jpg); -- 插入用户 INSERT INTO users (username, password, gender, age_group) VALUES (alice, e10adc3949ba59abbe56e057f20f883e, F, 18-25), -- MD5(123456) (bob, e10adc3949ba59abbe56e057f20f883e, M, 26-35), (carol, e10adc3949ba59abbe56e057f20f883e, F, 36-45); -- 插入评分模拟真实分布热门片评分多冷门片少 INSERT INTO ratings (user_id, movie_id, rating) VALUES (1,1,5),(1,2,4),(1,3,5),(1,4,5),(1,5,4), (2,1,4),(2,2,5),(2,3,4),(2,4,3),(2,5,2), (3,1,3),(3,2,4),(3,3,2),(3,4,5),(3,5,5); -- 其余评分用循环插入略确保总记录数 ≥50注意密码e10adc3949ba59abbe56e057f20f883e是123456的 MD5仅用于演示。实际项目必须用BCryptPasswordEncoder加密且password字段长度需扩至 100。4. 推荐算法实现User-Based CF 的 Java 版本拒绝黑匣子协同过滤不是调包是理解“相似用户”如何定义、“邻居”如何筛选、“预测评分”如何加权。本实现完全基于 JDBC 和基础 Java 集合不依赖 Mahout 或 Spark确保你能一行行 debug。4.1 核心类UserBasedRecommender三层结构清晰可读// src/main/java/com/movie/recommender/UserBasedRecommender.java public class UserBasedRecommender { private final Connection conn; public UserBasedRecommender(Connection conn) { this.conn conn; } // 步骤1获取目标用户的已评影片ID集合 public SetInteger getUserRatedMovies(int userId) throws SQLException { String sql SELECT movie_id FROM ratings WHERE user_id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); SetInteger rated new HashSet(); try (ResultSet rs ps.executeQuery()) { while (rs.next()) rated.add(rs.getInt(movie_id)); } return rated; } } // 步骤2计算用户相似度皮尔逊相关系数简化版 public double calculatePearsonSimilarity(int user1, int user2) throws SQLException { // 获取两用户共同评分的影片ID String sql SELECT r1.movie_id, r1.rating as r1_rating, r2.rating as r2_rating FROM ratings r1 INNER JOIN ratings r2 ON r1.movie_id r2.movie_id WHERE r1.user_id ? AND r2.user_id ? ; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, user1); ps.setInt(2, user2); Listdouble[] pairs new ArrayList(); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { pairs.add(new double[]{rs.getDouble(r1_rating), rs.getDouble(r2_rating)}); } } if (pairs.size() 5) return 0.0; // 共同评分太少无意义 // 计算均值 double sum1 pairs.stream().mapToDouble(p - p[0]).sum(); double sum2 pairs.stream().mapToDouble(p - p[1]).sum(); double mean1 sum1 / pairs.size(); double mean2 sum2 / pairs.size(); // 计算分子分母 double numerator pairs.stream() .mapToDouble(p - (p[0] - mean1) * (p[1] - mean2)).sum(); double denominator1 Math.sqrt(pairs.stream() .mapToDouble(p - Math.pow(p[0] - mean1, 2)).sum()); double denominator2 Math.sqrt(pairs.stream() .mapToDouble(p - Math.pow(p[1] - mean2, 2)).sum()); return denominator1 0 || denominator2 0 ? 0.0 : numerator / (denominator1 * denominator2); } } // 步骤3为用户生成 Top-N 推荐N10 public ListInteger recommendForUser(int userId, int topN) throws SQLException { SetInteger ratedMovies getUserRatedMovies(userId); // 查找相似用户从缓存表读若无则实时计算并缓存 String cacheSql SELECT user_id2, similarity FROM user_similarity WHERE user_id1 ? ORDER BY similarity DESC LIMIT 20; try (PreparedStatement ps conn.prepareStatement(cacheSql)) { ps.setInt(1, userId); ListInteger neighbors new ArrayList(); try (ResultSet rs ps.executeQuery()) { while (rs.next()) neighbors.add(rs.getInt(user_id2)); } // 若缓存为空实时计算 Top-20 相似用户 if (neighbors.isEmpty()) { neighbors findTopSimilarUsers(userId, 20); } // 收集邻居推荐的影片排除用户已评 MapInteger, Double candidateScores new HashMap(); for (int neighborId : neighbors) { String neighborRatings SELECT movie_id, rating FROM ratings WHERE user_id ? AND movie_id NOT IN ( ratedMovies.stream().map(String::valueOf).collect(Collectors.joining(,)) ); try (PreparedStatement ps2 conn.prepareStatement(neighborRatings)) { ps2.setInt(1, neighborId); try (ResultSet rs2 ps2.executeQuery()) { while (rs2.next()) { int movieId rs2.getInt(movie_id); double rating rs2.getDouble(rating); double similarity getSimilarity(userId, neighborId); // 从缓存或实时计算 candidateScores.merge(movieId, rating * similarity, Double::sum); } } } } // 按预测分排序取 Top-N return candidateScores.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); } } private ListInteger findTopSimilarUsers(int userId, int limit) throws SQLException { // 简化扫描 users 表跳过自己计算相似度取 Top-limit String sql SELECT id FROM users WHERE id ! ? LIMIT 100; // 限制扫描范围防慢SQL try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ListInteger candidates new ArrayList(); try (ResultSet rs ps.executeQuery()) { while (rs.next()) candidates.add(rs.getInt(id)); } return candidates.stream() .mapToInt(Integer::intValue) .boxed() .sorted((a, b) - Double.compare( -calculatePearsonSimilarity(userId, b), // 降序 -calculatePearsonSimilarity(userId, a))) .limit(limit) .collect(Collectors.toList()); } } private double getSimilarity(int u1, int u2) throws SQLException { String sql SELECT similarity FROM user_similarity WHERE user_id1 ? AND user_id2 ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, u1); ps.setInt(2, u2); try (ResultSet rs ps.executeQuery()) { return rs.next() ? rs.getDouble(similarity) : 0.0; } } } }逻辑说明getUserRatedMovies()是推荐起点避免给用户推他看过的片calculatePearsonSimilarity()用皮尔逊系数衡量用户口味相似性要求至少 5 部共同评分才计算防噪声recommendForUser()先查缓存表user_similarity无则调findTopSimilarUsers()实时计算再聚合邻居的加权评分candidateScores.merge(...)是 Java 8 的优雅写法等价于score rating * similarity。4.2 缓存更新策略定时任务 vs 请求触发选前者实时计算相似度太重必须异步更新。本系统采用TimerTask每 2 小时刷新一次user_similarity表// src/main/java/com/movie/scheduler/SimilarityUpdater.java public class SimilarityUpdater extends TimerTask { private final Connection conn; public SimilarityUpdater(Connection conn) { this.conn conn; } Override public void run() { try { // 清空旧缓存 conn.createStatement().execute(TRUNCATE TABLE user_similarity); // 重新计算 Top-10 相似用户对仅活跃用户 String activeUsers SELECT id FROM users WHERE id IN (SELECT DISTINCT user_id FROM ratings GROUP BY user_id HAVING COUNT(*) 3); try (ResultSet rs conn.createStatement().executeQuery(activeUsers)) { ListInteger users new ArrayList(); while (rs.next()) users.add(rs.getInt(id)); // 两两计算O(n²)故限制用户数 100 for (int i 0; i users.size() i 50; i) { for (int j i 1; j users.size() j i 20; j) { double sim new UserBasedRecommender(conn) .calculatePearsonSimilarity(users.get(i), users.get(j)); if (sim 0.6) { String insert INSERT INTO user_similarity VALUES (?, ?, ?, NOW()); try (PreparedStatement ps conn.prepareStatement(insert)) { ps.setInt(1, users.get(i)); ps.setInt(2, users.get(j)); ps.setDouble(3, sim); ps.executeUpdate(); } } } } } } catch (SQLException e) { e.printStackTrace(); // 生产环境应记日志 } } } // 在 ServletContextListener 中启动 public class StartupListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { Connection conn DBUtil.getConnection(); // 自定义连接工具类 Timer timer new Timer(SimilarityUpdater, true); timer.scheduleAtFixedRate(new SimilarityUpdater(conn), 0, 2 * 60 * 60 * 1000); // 2小时 } }参数说明j i 20限制每个用户只找 20 个潜在邻居避免 O(n²) 爆炸sim 0.6是经验值低于此值视为无实质相似性TRUNCATE比DELETE快因不走事务日志。5. JSP 页面渲染从 Servlet 到页面每一行代码都可控JSP 不是过时技术而是“Java 逻辑 HTML 模板”最直白的结合。本节教你如何把recommendForUser()返回的ListInteger变成带海报、片名、评分的响应式卡片流。5.1 推荐 Servlet接收用户 ID调用推荐器转发到 JSP// src/main/java/com/movie/servlet/RecommendServlet.java WebServlet(/recommend) public class RecommendServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取当前登录用户ID从session读非URL参数防篡改 Integer userId (Integer) req.getSession().getAttribute(userId); if (userId null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } try (Connection conn DBUtil.getConnection()) { UserBasedRecommender recommender new UserBasedRecommender(conn); ListInteger movieIds recommender.recommendForUser(userId, 10); // 2. 查询影片详情一次SQL查出10部片避免N1 String placeholders String.join(,, Collections.nCopies(movieIds.size(), ?)); String sql SELECT id, title, poster_url, avg_rating FROM movies WHERE id IN ( placeholders ); try (PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i movieIds.size(); i) { ps.setInt(i 1, movieIds.get(i)); } ListMovie movies new ArrayList(); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { movies.add(new Movie( rs.getInt(id), rs.getString(title), rs.getString(poster_url), rs.getDouble(avg_rating) )); } } req.setAttribute(recommendedMovies, movies); req.getRequestDispatcher(/recommend.jsp).forward(req, resp); } } catch (SQLException e) { throw new ServletException(Recommend failed, e); } } }关键点req.getSession().getAttribute(userId)确保推荐基于真实登录态非 URL 传参String.join(,, Collections.nCopies(...))动态生成IN (?)占位符安全防注入Movie是简单 POJO含id,title,poster_url,avg_rating四字段JSP 直接 EL 表达式取值。5.2 JSP 页面用 JSTL Bootstrap 4 快速搭建响应式推荐流% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head meta charsetUTF-8 title个性化推荐/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap4.6.2/dist/css/bootstrap.min.css relstylesheet /head body div classcontainer mt-4 h2为您推荐/h2 div classrow c:forEach varmovie items${recommendedMovies} div classcol-md-3 mb-4 div classcard h-100 shadow-sm img src${pageContext.request.contextPath}${movie.posterUrl} classcard-img-top alt${movie.title} onerrorthis.src${pageContext.request.contextPath}/images/default.jpg div classcard-body d-flex flex-column h6 classcard-title${movie.title}/h6 div classmt-auto span classbadge badge-primary★ ${movie.avgRating}/span a hrefdetail.jsp?id${movie.id} classbtn btn-sm btn-outline-secondary mt-2查看详情/a /div /div /div /div /c:forEach /div /div script srchttps://cdn.jsdelivr.net/npm/jquery3.5.1/dist/jquery.slim.min.js/script script srchttps://cdn.jsdelivr.net/npm/bootstrap4.6.2/dist/js/bootstrap.bundle.min.js/script /body /html参数说明onerrorthis.src...是 JSP 图片加载失败的兜底方案避免海报空白classcol-md-3在桌面端每行 4 张卡片移动端自动堆叠d-flex flex-columnmt-auto让按钮始终贴底解决高度不一致问题pageContext.request.contextPath确保图片路径正确无论应用部署在/还是/movie-recommender。6. 避坑指南那些让 Java 影片推荐系统上线前翻车的 5 个血泪经验做这个系统时我踩过足够多的坑有些当场报错有些潜伏数周后爆发。以下全是真实场景按发生频率排序每一条都附带复现方式和根治方案。6.1 现象Tomcat 启动后 JSP 页面显示HTTP Status 500 – Internal Server Error日志报javax.el.PropertyNotFoundException: Property posterUrl not found原因JSP 中${movie.posterUrl}调用的是getPosterUrl()方法但Movie类中 getter 名为getPoster_url()下划线命名EL 表达式按 JavaBean 规范解析会忽略下划线导致找不到属性。解决统一使用驼峰命名。private String poster_url;→private String posterUrl;getter/setter 也同步改为getPosterUrl()/setPosterUrl()。检查所有 POJO包括User,Rating。6.2 现象MySQL 插入中文片名时报错Incorrect string value: \xE4\xB9\x8C\xE5\xA4\xA9... for column title原因虽然数据库和表设了utf8mb4但 JDBC 连接 URL 缺少?useUnicodetruecharacterEncodingutf8mb4参数驱动仍用latin1解析。解决修改DBUtil.getConnection()中的 URLString url jdbc:mysql://localhost:3306/movie_recommend?useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8;验证执行SELECT character_set_database, collation_database;和SHOW CREATE TABLE movies;确认均为utf8mb4。6.3 现象推荐结果每次刷新都不同甚至出现NullPointerException原因UserBasedRecommender实例被多个线程共享而Connection conn是实例变量未做线程隔离。Servlet 容器中一个 Servlet 实例被多请求复用conn被并发 close 或重用。解决将conn移出成员变量改为方法参数传递。所有recommendForUser()、calculatePearsonSimilarity()等方法签名增加Connection conn参数并在 Servlet 中每次请求新建连接或从连接池取。示例// 错误成员变量 conn // private final Connection conn; // 正确方法参数 public ListInteger recommendForUser(Connection conn, int userId, int topN) throws SQLException { ... }6.4 现象Tomcat Manager 页面上传 WAR 包后访问/recommend返回404原因WAR 包名含版本号如movie-recommender-1.0.warTomcat 自动部署为/movie-recommender-1.0上下文但浏览器访问的是/recommend根上下文。解决两种方案任选其一方案1推荐重命名 WAR 包为ROOT.warTomcat 会部署到根路径方案2在conf/server.xml的Host节点内添加Context path docBasemovie-recommender-1.0 /强制映射到根。6.5 现象用户登录后req.getSession().getAttribute(userId)始终为null原因登录 Servlet 中未调用session.setAttribute(userId, userId)或web.xml中session-config的cookie-http-only设为true但前端 JS 尝试读取 cookie。解决登录成功后必须显式设置req.getSession().setAttribute(userId, userId);检查web.xml是否有http-onlytrue/http-only若有确保前端不依赖document.cookie读取 session ID而是靠容器自动管理。7. 进阶技巧用 JUnit 4 写 3 个关键测试让推荐逻辑不再玄学写完代码不测试等于没写。本系统推荐逻辑的脆弱点在于相似度计算是否稳定冷启动时能否兜底缓存更新是否原子下面三个测试覆盖核心路径全部通过才能放心上线。7.1 测试相似度计算固定数据集验证皮尔逊系数准确性// src/test/java/com/movie/recommender/UserBasedRecommenderTest.java public class UserBasedRecommenderTest { private Connection conn; Before public void setUp() throws SQLException { conn H2DatabaseUtil.getMemoryConnection(); // 使用内存 H2 数据库免 MySQL 依赖 // 初始化测试数据用户1和用户2对影片1-3的评分完全一致 conn.createStatement().executeUpdate(INSERT INTO ratings VALUES (1,1,1,5,2023-01-01),(1,1,2,4,2023-01-01),(1,1,3,3,2023-01-01)); conn.createStatement().executeUpdate(INSERT INTO ratings VALUES (1,2,1,5,2023-01-01),(1,2,2,4,2023-01-01),(1,2,3,3,2023-01-01)); } Test public void testPearsonSimilarity_WhenRatingsIdentical_ShouldReturnOne() throws SQLException { UserBasedRecommender recommender new UserBasedRecommender(conn); double similarity recommender.calculatePearsonSimilarity(1, 2); assertEquals(1.0, similarity, 0.001); // 允许浮点误差 } }为什么用 H2内存数据库启动快无需外部 MySQLBefore每次新建干净环境避免测试污染。H2DatabaseUtil是封装好的工具类建表语句与 MySQL 一致。7.2 测试冷启动兜底新用户无评分时返回热门影片Test public void testRecommendForNewUser_WhenNoRatings_ShouldReturnPopularMovies() throws SQLException { // 新用户ID999无任何评分记录 UserBasedRecommender recommender new UserBasedRecommender(conn); ListInteger result recommender.recommendForUser(conn, 999, 5); // 验证返回非空且影片ID来自预设热门榜如评分人数最多的前5部 assertFalse(result.isEmpty()); assertTrue(result.contains(1)); // 假设影片1是热门 assertTrue(result.contains(2)); }兜底逻辑实现在recommendForUser()开头加判断if (ratedMovies.isEmpty()) { // 查询 rating_count 最高的5部影片 return getPopularMovies(conn, topN); }7.3 测试缓存更新原子性并发更新user_similarity不导致数据丢失Test public void testSimilar p a hrefhttps://download.csdn.net/download/qq_33037637/89775845 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p