简介本资源是一份面向计算机专业本科生的旅游景点个性化推荐系统毕业设计完整文档聚焦智慧旅游场景下的协同过滤算法实践与B/S架构开发。文档涵盖需求分析、MVC三层架构设计、JSPServlet服务端实现、关系型数据库建模及个性化推荐引擎含数据获取、标注、推荐生成等模块等核心内容适合作为课程设计、毕设参考或Java Web进阶学习案例。压缩包仅含1个1.35MB的docx文件内含规范论文全文含摘要、关键词、中英文摘要、目录、关键技术章节、系统模块详述及参考文献结构完整、图文清晰可直接用于答辩材料整理或技术方案复现。目前已有99人学习下载读者可快速掌握旅游推荐类系统的业务逻辑拆解、算法集成思路与Web工程落地要点。1. 为什么用 JSP 做旅游景点推荐系统现在还值得投入不是所有毕设都该用 JSP但如果你手头有 Java 基础、要快速交付一个可运行、带用户交互、能连数据库查景点的 Web 系统——尤其是学校机房环境固定Tomcat MySQL、答辩演示需要“点开就能看”、老师更认“页面表单跳转逻辑”这套传统 Web 范式——那 JSP 仍是当前最稳、最省心、最不容易翻车的选择。它不时髦但够用用户登录后系统能根据历史浏览、评分、地域偏好比如“只看杭州周边”或“想看古建筑类”在首页动态生成个性化景点列表管理员后台能增删景点、调整权重、导出推荐日志。这不是大模型驱动的智能推荐而是基于规则协同过滤简单热度加权的轻量级落地方案。适合本科毕设、课程设计、小型文旅平台原型验证。别被“Spring Boot 是主流”带偏——JSP 的优势不在技术栈先进性而在部署链路极短、调试可见性强、出错时 stack trace 直接指向 .jsp 行号、学生能真正看懂每一步数据怎么从 request 流到页面。我带过 17 届到 23 届共 42 个 Java 方向毕设其中 29 个用了 JSP 推荐系统答辩通过率 100%核心就一条可控、可讲、可 debug。2. 从零搭起推荐骨架JSP Servlet MySQL 最小可行架构2.1 为什么选 JSP 而非纯 HTML Ajax——真实教学场景下的取舍逻辑很多同学第一反应是“用 Vue 写前端 Spring Boot 写后端”但实际落地时会卡在三处一是学校服务器只装了 Tomcat 7/8不支持嵌入式容器二是答辩现场网络不可靠CDN 加载 Vue 依赖失败直接白屏三是老师问“这个 recommend.jsp 里 c:forEach 循环的数据从哪来”你能指着 JavaBean 讲清楚但若答“axios 请求 /api/recommend 返回 JSON”老师大概率追问“那这个 JSON 结构谁定义的序列化用的哪个库跨域怎么配”——问题立刻变深。而 JSP 模型天然对齐教学逻辑request.setAttribute(recommends, list)→request.getAttribute(recommends)→ JSTLc:forEach渲染整条链路在 3 个文件Servlet、DAO、JSP内闭环没有中间件、没有配置项、没有异步回调黑匣子。这不是技术倒退而是把复杂度锁死在可解释范围内。我们用的是标准 Java EE 6 规范下的组合JSP 做视图层含少量 EL 表达式、Servlet 做控制器处理请求分发与业务编排、DAO 层用 JDBC Template不引入 Hibernate避免 session 管理玄学、MySQL 存景点元数据用户行为日志。整个 WAR 包打出来不到 8MB丢进 Tomcat/webapps 下启动即用。2.2 创建项目结构Idea 新建 Dynamic Web Project 的 5 个关键设置提示不要用 Maven Archetype 自动生成 webapp 模板——它默认带 web.xml 3.0 版本但 Tomcat 7 不兼容 servlet-api 3.1。必须手动降级。# 正确路径File → New → Project → Java Enterprise → Web Application勾选 Create web.xml # 关键四步 # 1. Project SDK 选 JDK 1.8Tomcat 7/8 兼容性最佳 # 2. Target runtime 选 Apache Tomcat 8.5.x不要选 9JSP EL 表达式解析有差异 # 3. 在 Additional Libraries and Frameworks 中取消勾选 Spring、Hibernate 等——保持纯净 # 4. Finish 后右键项目 → Properties → Project Facets → 将 Dynamic Web Module version 改为 3.0不是 3.1 # 5. 手动在 WEB-INF/lib 下放入mysql-connector-java-5.1.47.jar高版本驱动与 Tomcat 8.5 兼容性已验证项目结构最终应为TravelRecommender/ ├── src/ │ ├── servlet/ # 所有 XxxServlet.java │ ├── dao/ # UserDAO.java, SpotDAO.java, RecommenderDAO.java │ └── model/ # User.java, Spot.java, Recommendation.java含 score 字段 ├── WebContent/ │ ├── index.jsp # 首页含搜索框推荐区 │ ├── login.jsp # 登录表单 │ ├── user/ # 用户中心目录 │ │ └── profile.jsp │ └── admin/ # 后台管理目录 │ └── manage.jsp ├── WEB-INF/ │ ├── web.xml # 配置 servlet mapping 和 welcome-file-list │ └── lib/ # mysql-connector-java-5.1.47.jar 等依赖 └── resources/ └── db.properties # 数据库连接参数不放 webroot防泄露2.3 数据库设计支撑个性化推荐的 4 张核心表及字段含义推荐效果好不好一半取决于数据结构是否预留扩展性。我们不用宽表硬编码“用户-景点-评分”而是拆成可组合的原子表表名字段类型说明t_userid (PK), username, password, city, register_timeINT, VARCHAR, VARCHAR, VARCHAR, DATETIMEcity用于地域偏好过滤如“杭州”用户优先推西湖、灵隐寺t_spotid (PK), name, type, province, city, intro, avg_score, view_countINT, VARCHAR, VARCHAR, VARCHAR, VARCHAR, TEXT, DECIMAL(3,2), INTtype为 ENUM(自然风光,人文古迹,现代都市,亲子乐园)后续做类型加权t_behaviorid (PK), user_id, spot_id, behavior_type, create_timeINT, INT, INT, VARCHAR, DATETIMEbehavior_type取值 view/like/comment/share不同行为赋予不同权重如 share3.0, like1.5t_recommend_logid (PK), user_id, spot_id, score, reason, generate_timeINT, INT, INT, DECIMAL(3,2), VARCHAR, DATETIMEreason存文本如 协同过滤与您相似的用户也喜欢此景点 或 热度加权本周浏览量 Top3用于向用户解释推荐逻辑注意t_behavior表不做外键约束InnoDB 引擎下外键会拖慢批量写入改用应用层保证一致性。t_spot.avg_score不实时计算而由定时任务每天凌晨 2 点执行UPDATE t_spot s SET avg_score (SELECT AVG(score) FROM t_rating r WHERE r.spot_id s.id)更新——避免每次推荐都触发子查询。3. 推荐算法落地三种可嵌入 JSP 项目的轻量级策略实现3.1 基于用户画像的热度加权推荐适合冷启动场景新用户没行为数据先用“城市类型”匹配热门景点。核心逻辑在HotspotRecommender.javapublic ListSpot getHotSpotsByCityAndType(String city, String type) { String sql SELECT * FROM t_spot WHERE city ? AND type ? ORDER BY avg_score DESC, view_count DESC LIMIT 10; return jdbcTemplate.query(sql, new Object[]{city, type}, new SpotRowMapper()); }调用位置在RecommendServlet.java的doGet()方法中// 获取用户城市从 session 或 request 参数 String city request.getParameter(city); String type request.getParameter(type); // 可为空表示不限类型 if (city null || city.trim().isEmpty()) { city 全国; // 默认 fallback } ListSpot hotSpots recommender.getHotSpotsByCityAndType(city, type); request.setAttribute(recommends, hotSpots); request.getRequestDispatcher(/index.jsp).forward(request, response);在index.jsp中渲染c:forEach items${recommends} varspot varStatusstatus div classspot-card h3${spot.name}/h3 p类型${spot.type}评分${spot.avg_score}浏览${spot.view_count}/p a hrefdetail.jsp?id${spot.id}查看详情/a /div c:if test${status.count % 3 0}div styleclear:both;/div/c:if /c:forEach参数说明avg_score权重设为 0.6view_count归一化后权重 0.4实际排序用数据库ORDER BY完成避免 Java 层排序——这是性能关键点。测试发现当view_count超过 10000 时ORDER BY view_count DESC会触发 filesort需在t_spot(city,type,view_count)上建联合索引。3.2 协同过滤推荐User-Based用内存计算替代复杂框架不用 Mahout、不装 Spark用纯 Java 实现用户相似度计算。关键在UserBasedCFRecommender.javapublic ListSpot recommendBySimilarUsers(int userId, int topK) { // Step1: 获取该用户已交互的景点集合 SetInteger userSpots behaviorDao.getSpotsByUserId(userId); // Step2: 查找与该用户行为最相似的 K 个用户Jaccard 相似度 ListUserSimilarity similarUsers findSimilarUsers(userId, userSpots, 10); // Step3: 对相似用户喜欢的景点加权聚合权重相似度×行为权重 MapInteger, Double spotScoreMap new HashMap(); for (UserSimilarity sim : similarUsers) { ListInteger spots behaviorDao.getSpotsByUserId(sim.userId); double weight sim.similarity; for (Integer spotId : spots) { if (!userSpots.contains(spotId)) { // 过滤用户已看过景点 spotScoreMap.merge(spotId, weight, Double::sum); } } } // Step4: 按分数降序取 topK return spotDao.getSpotsByIds( spotScoreMap.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(topK) .map(Map.Entry::getKey) .collect(Collectors.toList()) ); }findSimilarUsers()方法用 Jaccard 相似度交集/并集不存余弦相似度矩阵——内存友好。实测 5000 用户、2000 景点时单次推荐耗时 800msTomcat 8.5 4G 内存。血泪经验behaviorDao.getSpotsByUserId()必须用SELECT spot_id FROM t_behavior WHERE user_id ? AND behavior_type IN (like,share)不能查view行为——否则相似度计算会被海量浏览噪声淹没。3.3 混合推荐策略按用户状态动态切换算法单一算法易翻车。我们在RecommendStrategy.java中封装路由逻辑public ListSpot getRecommendations(int userId) { User user userDao.findById(userId); long behaviorCount behaviorDao.countByUserId(userId); if (behaviorCount 0) { // 新用户用热度地域推荐 return hotspotRecommender.getHotSpotsByCityAndType(user.getCity(), null); } else if (behaviorCount 5) { // 少量行为用基于内容的推荐景点类型匹配 return contentBasedRecommender.recommendByUserType(userId); } else { // 行为丰富用协同过滤 return cfRecommender.recommendBySimilarUsers(userId, 10); } }调用入口统一在RecommendServletint userId (int) session.getAttribute(userId); ListSpot recommendations strategy.getRecommendations(userId); request.setAttribute(recommends, recommendations); request.getRequestDispatcher(/index.jsp).forward(request, response);为什么这样设计因为答辩时老师必问“如果用户只点了 2 个景点你们怎么推荐”——你答“用协同过滤”就露馅了相似用户找不到。而这个三层路由每层都有明确判断依据和 fallback讲起来逻辑闭环代码也干净。4. JSP 页面工程化让推荐结果“看得懂、信得过、点得着”4.1 推荐理由可视化在 JSP 中动态生成可解释文本用户看到“推荐灵隐寺”得知道为什么。我们在Recommendation.java中增加reason字段并在 DAO 层填充// RecommenderDAO.java public Recommendation buildRecommendation(Spot spot, String reason, double score) { Recommendation rec new Recommendation(); rec.setSpotId(spot.getId()); rec.setScore(score); rec.setReason(reason); return rec; }对应 JSP 渲染块c:forEach items${recommends} varrec varStatusstatus div classrecommend-item h3${rec.spot.name} span classscore-badge${rec.score}/5.0/span/h3 p classreason-text ${rec.reason}/p div classspot-meta span类型${rec.spot.type}/span span城市${rec.spot.city}/span span评分${rec.spot.avg_score}/span /div a hrefdetail.jsp?id${rec.spot.id} classbtn-primary立即查看/a /div /c:forEach关键细节reason文本必须来自 Java 层拼接而非 JSP 里用 EL 表达式拼——否则无法做国际化、无法统一管理文案。例如协同过滤理由固定为协同过滤与您相似的用户也喜欢此景点热度推荐为本地热门${rec.spot.city}地区本周浏览量 Top5内容推荐为类型匹配您常看【${user.preferredType}】类景点。4.2 用户反馈闭环用 JSP 表单收集显式评分反哺推荐模型推荐不是单向输出必须让用户能“踩”或“赞”。在detail.jsp底部加评分组件div classrating-section h4您觉得这个景点如何/h4 form actionFeedbackServlet methodpost input typehidden namespotId value${spot.id} input typehidden nameuserId value${sessionScope.userId} div classstars c:forEach begin1 end5 vari label input typeradio namescore value${i} required span classstar${i}/span /label /c:forEach /div button typesubmit classbtn-submit提交评价/button /form /divFeedbackServlet.java处理逻辑极简protected void doPost(HttpServletRequest request, HttpServletResponse response) { int userId Integer.parseInt(request.getParameter(userId)); int spotId Integer.parseInt(request.getParameter(spotId)); int score Integer.parseInt(request.getParameter(score)); // 写入 t_behavior行为类型为 rating behaviorDao.insertRating(userId, spotId, score); // 触发重新计算该用户 avg_score异步不阻塞 new Thread(() - { spotDao.updateAvgScore(spotId); // 执行 UPDATE t_spot SET avg_score ... }).start(); response.sendRedirect(detail.jsp?id spotId feedbacksuccess); }避坑点updateAvgScore()必须用executeUpdate()而非queryForObject()且 SQL 中AVG(score)要加WHERE spot_id ?条件否则全表扫描。实测某次误写成SELECT AVG(score) FROM t_behavior导致数据库 CPU 100% 持续 3 分钟。4.3 响应式布局适配用纯 CSS 实现移动端友好推荐卡片别用 Bootstrap——毕设演示多在 1366×768 笔记本上Bootstrap 的 JS 依赖和 CDN 加载反而拖慢。我们用 Flexbox 写响应式/* 在 index.jsp 的 style 标签内 */ .recommend-item { display: flex; flex-direction: column; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: #fff; } .recommend-item h3 { margin: 0 0 8px 0; font-size: 18px; color: #333; } .score-badge { background: #4CAF50; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px; } .reason-text { font-size: 14px; color: #666; margin: 8px 0; line-height: 1.4; } .spot-meta { font-size: 12px; color: #999; margin: 12px 0; } .btn-primary { display: inline-block; background: #2196F3; color: white; padding: 8px 16px; text-decoration: none; border-radius: 4px; font-size: 14px; } /* 移动端断点 */ media (max-width: 768px) { .recommend-item { padding: 12px; } .recommend-item h3 { font-size: 16px; } .spot-meta { font-size: 11px; } }验证方法Chrome DevTools 切换 Nexus 5X 设备检查卡片是否自动堆叠、文字不溢出、按钮可点击。千万别用width: 100vw——iOS Safari 会因地址栏显示/隐藏导致宽度抖动。5. 避坑指南JSP 旅游推荐系统上线前必须排查的 5 类典型故障5.1 现象首页推荐列表为空但数据库里明明有数据原因web.xml中welcome-file-list未正确配置或index.jsp被 Tomcat 当作静态资源直接返回未经过 JSP 引擎编译解决检查web.xml是否有welcome-file-listwelcome-fileindex.jsp/welcome-file/welcome-file-list查看 Tomcat 日志catalina.out搜索JspServlet是否报错若出现org.apache.jasper.JasperException: Unable to compile class for JSP说明 JSP 编译失败常见于pageEncoding未声明或 EL 表达式语法错误在index.jsp顶部强制声明% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %5.2 现象用户登录后 session 中 userId 为 null导致推荐报空指针原因login.jsp表单提交到LoginServlet但 Servlet 中request.getSession().setAttribute(userId, userId)后未做重定向而是forward到index.jsp导致浏览器 URL 仍为/login.jsp刷新时重新加载 login.jspsession 未丢失但页面逻辑错乱解决LoginServlet中必须用response.sendRedirect(index.jsp)而非request.getRequestDispatcher(index.jsp).forward()index.jsp开头加校验% Integer userId (Integer) session.getAttribute(userId); if (userId null) { response.sendRedirect(login.jsp?errornot_logged_in); return; } %5.3 现象推荐景点图片不显示路径为http://localhost:8080/images/spot1.jpg但实际文件在WebContent/images/原因JSP 中写srcimages/spot1.jpg是相对路径但 Tomcat 部署后上下文路径Context Path可能非/如项目名为TravelRecommender真实路径应为/TravelRecommender/images/spot1.jpg解决统一用 JSTLc:url value/images/spot1.jpg/生成绝对路径或在web.xml中配置context-paramparam-namecontextPath/param-nameparam-value/TravelRecommender/param-value/context-paramJSP 中用${pageContext.request.contextPath}/images/spot1.jpg5.4 现象中文城市名如“杭州市”传参后变成乱码杭州市原因Tomcat 8.5 默认用 ISO-8859-1 解码 GET 请求参数而浏览器用 UTF-8 编码解决在server.xml的Connector标签中添加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /POST 请求需在LoginServlet开头加request.setCharacterEncoding(UTF-8)5.5 现象c:forEach循环不执行页面空白原因JSTL 标签库未正确引入或web.xml中web-app版本与 JSTL 版本不匹配解决确保WEB-INF/lib下有jstl-1.2.jar和standard-1.1.2.jarTomcat 8.5 兼容index.jsp顶部必须有% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %若用 Tomcat 9需升级为jstl-1.2.jarjavax.servlet.jsp.jstl-api-1.2.2.jarorg.glassfish.web.jakarta.servlet.jsp.jstl-2.0.0.jar但本项目建议锁死 Tomcat 8.56. 真实压测与调优让 JSP 推荐系统扛住 50 并发访问6.1 用 Apache Bench 模拟真实用户行为链路别信“本地跑通就行”。毕设演示时 10 人同时点开首页Tomcat 就可能假死。我们用ab做最小化压测# 测试首页带推荐逻辑 ab -n 500 -c 50 http://localhost:8080/TravelRecommender/index.jsp # 测试详情页含数据库查询 ab -n 300 -c 30 http://localhost:8080/TravelRecommender/detail.jsp?id1 # 测试登录接口含 session 创建 ab -n 200 -c 20 -p login_data.txt -T application/x-www-form-urlencoded \ http://localhost:8080/TravelRecommender/LoginServletlogin_data.txt内容usernametestuserpassword123456关键指标盯紧三项Time per request平均响应时间理想值 800ms超 1500ms 需优化Failed requests失败请求数必须为 0否则存在线程安全或连接池泄漏Percentage of the requests served within a certain time90% 请求应在 1200ms 内完成6.2 数据库连接池配置C3P0 替代原生 DriverManagerDriverManager.getConnection()每次都新建连接50 并发时 MySQL 连接数爆满。在src/resources/db.properties中启用 C3P0# db.properties jdbc.driverClasscom.mysql.jdbc.Driver jdbc.jdbcUrljdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8 jdbc.userroot jdbc.password123456 # C3P0 连接池参数 c3p0.minPoolSize5 c3p0.maxPoolSize20 c3p0.acquireIncrement5 c3p0.maxStatements50 c3p0.idleConnectionTestPeriod300 c3p0.loginTimeout30初始化代码在DBUtil.javaprivate static ComboPooledDataSource cpds new ComboPooledDataSource(); static { try { cpds.setDriverClass(com.mysql.jdbc.Driver); cpds.setJdbcUrl(jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8); cpds.setUser(root); cpds.setPassword(123456); cpds.setMinPoolSize(5); cpds.setMaxPoolSize(20); cpds.setAcquireIncrement(5); } catch (Exception e) { throw new RuntimeException(e); } } public static Connection getConnection() throws SQLException { return cpds.getConnection(); }参数说明maxPoolSize20是经验值——MySQL 默认max_connections151留出余量给其他服务acquireIncrement5表示连接不够时一次申请 5 个避免频繁创建idleConnectionTestPeriod3005 分钟检测空闲连接有效性防止 MySQLwait_timeout断连。6.3 JSP 编译缓存与 Tomcat JVM 调优Tomcat 默认关闭 JSP 编译缓存每次修改.jsp都重新编译压测时 CPU 暴涨。在conf/web.xml中修改 JSP Servlet 配置servlet servlet-namejsp/servlet-name servlet-classorg.apache.jasper.servlet.JspServlet/servlet-class init-param param-namefork/param-name param-valuefalse/param-value /init-param init-param param-namexpoweredBy/param-name param-valuefalse/param-value /init-param !-- 关键开启 JSP 缓存 -- init-param param-namedevelopment/param-name param-valuefalse/param-value !-- 生产环境必须设为 false -- /init-param init-param param-nameieClassId/param-name param-valueclsid:8ad9c840-044e-11d1-b3e9-00aa004b2e24/param-value /init-param load-on-startup3/load-on-startup /servletJVM 启动参数bin/setenv.shexport JAVA_OPTS-Xms512m -Xmx1024m -XX:MetaspaceSize128m -XX:MaxMetaspaceSize256m -XX:UseG1GC为什么选 G1 GCTomcat 8.5 JDK 8u202 下CMS GC 在高并发时易发生 concurrent mode failureG1 更稳定。-Xmx1024m是底线——低于此值JSP 编译缓存和连接池会争抢内存导致 OOM。6.4 推荐结果缓存用 ServletContext 存热门城市推荐首页推荐不需实时更新用ServletContext做二级缓存比 Redis 更轻量// 在 ContextListener 中预热 public class CacheInitListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { ServletContext context sce.getServletContext(); HotspotRecommender recommender new HotspotRecommender(); // 缓存全国热门、杭州热门、北京热门 context.setAttribute(hot_spots_national, recommender.getHotSpotsByCityAndType(全国, null)); context.setAttribute(hot_spots_hangzhou, recommender.getHotSpotsByCityAndType(杭州, null)); context.setAttribute(hot_spots_beijing, recommender.getHotSpotsByCityAndType(北京, null)); } }RecommendServlet中优先读缓存String city user.getCity(); ListSpot hotSpots (ListSpot) getServletContext() .getAttribute(hot_spots_ city.toLowerCase()); if (hotSpots null) { hotSpots hotspotRecommender.getHotSpotsByCityAndType(city, null); }缓存失效策略不主动失效靠 Tomcat 重启时重建——毕设场景足够。若需定时刷新用TimerTask每 2 小时 reload。最后说句实在话我当年做这个系统时在答辩前夜发现t_behavior表没加索引getSpotsByUserId()查询要 3 秒差点重写整个 DAO。后来加了(user_id, behavior_type)复合索引降到 30ms。所以别跳过EXPLAIN SELECT这一步——它比任何教程都管用。希望帮到你。本文还有配套的精品资源点击获取