资讯详情

旅游景点个性化推荐系统:基于JSP与协同过滤的完整实战

📅 2026/10/6 11:15:56 | 华诺云谱 👁 阅读
旅游景点个性化推荐系统:基于JSP与协同过滤的完整实战
简介旅游景点个性化服务系统的本科毕业论文以docx文档收录文件大小1.35MB共1个文件。论文基于JSPServlet及协同过滤算法按面向对象流程搭建客户端浏览器、服务端、关系型数据库的三层架构系统涵盖用户管理、夏季旅游/文化旅游/高原线路等分类展示、推荐引擎、景点详情标注、首页热门轮播、景点搜索与旅游笔记等功能。文档包含摘要、关键词、目录与正文完整呈现从背景技术、需求分析到系统设计实现及数据库同步更新的写作范式章节划分规范便于快速定位协同过滤推荐算法与MVC框架相关论述。对旅游管理、计算机相关专业学生而言既提供智慧旅游选题的完整骨架也可作为毕业设计框架搭建、技术路线表述和系统功能描述的参考样例。已有99人浏览学习适合需要完成类似课题的学生对照撰写开题、中期及结题材料。1. 旅游景点个性化推荐论文JSP 在其中的真实份量很多高校的 Java 课设和毕设选题里旅游景点个性化推荐系统是常驻选项Java 写后端JSP 写页面推荐算法做内核。这个题目听起来完整真正动手时多数人却把时间花在 JSP 页面上等到答辩被问一句“你的推荐算法和热门推荐比好在哪里”就卡住。实际上JSP 只是展示层推荐算法和它的评测才是这篇论文能不能站住的关键。下面按我实际带课设项目的顺序来拆先定算法选型再建数据表接着写推荐引擎的 Java 代码最后处理 JSP 联调里最常见的几个坑。适合正在写课程设计或毕业设计的人也适合接手老 JSP 旅游网站想补推荐功能的一线工程师。2. 先定推荐算法再定技术栈UserCF 与 ItemCF 的论文选型逻辑我带过的学生里十个有八个是先搭好 JSP 环境再想算法最后论文里随便塞一个协同过滤公式连相似度函数都解释不清。这是典型的翻车路径。技术栈是外壳算法是内核论文的第二章和实验章全压在算法身上所以动手写代码之前得先把 UserCF 和 ItemCF 的选型定下来。2.1 基于用户的协同过滤UserCF原理与适合什么场景基于用户的协同过滤核心假设是口味相近的人喜欢的景点也相近。流程分四步构建用户对景点的评分矩阵计算用户之间的相似度找到当前用户最相似的 K 个邻居把这 K 个邻居去过而当前用户没去过的景点按评分加权排序生成 Top-N 推荐。这个思路在新闻推荐、社区内容推荐里很常用因为它能捕捉群体热度用户今天看什么相似用户明天也会看什么。但放到旅游景点场景它有两个先天问题。一是评分矩阵极度稀疏多数用户只评过三五个景点两个用户之间几乎没有共同评分项算出来的相似度等于随机噪声。二是用户量一大在线实时计算用户相似度的开销就会失控而 JSP 这种传统同步请求模型根本扛不住每次刷新页面都重算一遍。2.2 基于物品的协同过滤ItemCF为什么更适合旅游景点基于物品的协同过滤不走用户相似走的是物品相似先算景点与景点之间的相似度再根据用户的历史评分预测用户对没去过的景点的兴趣分。一句话版本就是“看过黄山的用户通常也会对宏村感兴趣”。它适合旅游推荐的四个理由很实在其一景点数量比用户数量稳定一个中等规模的旅游网站维护几千个景点就够用相似度矩阵可以离线算好放进内存论文里也好描述“离线训练、在线预测”的架构。其二物品共现数据比用户共同评分容易积累用户 U 同时评过分 A 和景点 B就能为 A-B 的相似度贡献一次计数不需要两个用户之间有交集。其三新景点只要被少量用户评过几次分就能立刻和已有景点建立相似关系冷启动压力比 UserCF 小得多。其四推荐结果可解释“因为你喜欢西塘所以推荐同属古镇类的乌镇”这种理由放在 JSP 页面上演示效果远比“因为有个相似用户去过”更直观。所以常见做法是ItemCF 做核心推荐算法UserCF 作为对比算法出现在论文实验章两组跑同一份数据和同一个评测指标谁的效果好让数据说话。2.3 热门基线、对比实验与论文结构对照论文里只放协同过滤没有说服力因为评委会问“比最简单的热门推荐好在哪里”。标准配置是三组实验全局热门 Top-N 作为 baselineItemCF 作为主模型UserCF 作为对比模型。这样既有基线又有横向对照实验章的表格一下就丰满了。算法一句话原理论文里的角色代码模块热门 Top-N按景点热度排序Baseline 基线RecommendHotItemCF物品相似度 用户历史核心推荐算法RecommendItemUserCF用户相似度 邻居聚合对比实验RecommendUser从落地顺序上我一般建议项目第一周先写热门 Top-N因为热门算法不需要训练立刻能验证从数据库到 DAO 再到 Servlet 最后到 JSP 的整条链路是否通。链路通了再回头把 recommendHot 替换成 recommendItemJSP 页面一行都不用改。这个顺序能帮你把“环境问题”和“算法问题”分开排查不会出现页面报错时搞不清是框架没配好还是代码逻辑写错了。提示别一上来就写协同过滤。先让热门推荐跑通你才有资格判断后面算法报的错到底是算法问题还是数据问题。3. 数据库建模与 JSP 项目骨架从建表到能跑通的目录算法选定了下一步是数据模型。旅游推荐系统的表不用多三张核心表足够支撑整篇论文用户表、景点表、评分表。很多课设会顺手加一个收藏表和评论表如果要控制工作量三张表完全够写多出的表只会让论文的数据流图变复杂。3.1 用户、景点、评分三张核心表SQL 设计与字段理由先看建表 SQL。我用 MySQL 5.7 语法字符集统一 utf8mb4这是中文景点名称不乱码的前提。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, city VARCHAR(50) DEFAULT NULL COMMENT 常住城市用于冷启动兜底推荐, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE scenic ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 景点名称, city VARCHAR(50) NOT NULL COMMENT 所在城市, category VARCHAR(50) NOT NULL COMMENT 景点类型古镇/山川/海滨/博物馆等, price DECIMAL(10,2) DEFAULT 0 COMMENT 门票参考价, heat INT DEFAULT 0 COMMENT 热度用于热门基线推荐, image VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE rating ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, scenic_id INT NOT NULL, score TINYINT NOT NULL COMMENT 评分1-5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_scenic (user_id, scenic_id), KEY idx_scenic (scenic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段设计的细节说明。scenic.heat看起来冗余但它让热门基线推荐只需要一条 ORDER BY heat DESC论文的 baseline 实现成本趋近于零。rating.score用 TINYINT 而不是 INT因为评分只有 1 到 5 五个取值TINYINT 节省空间也约束了数据范围。uk_user_scenic唯一键防止同一用户对同一景点重复评分否则后续算相似度时同一对物品会重复计数结果偏得离谱。user.city和scenic.category两个字段是为冷启动准备的。新用户没有评分记录时按常住城市推荐该城市下热度最高的景点新景点没有评分时按 category 匹配同类已有景点这两条兜底策略写好之后推荐模块永远不会返回空列表。3.2 JSP 项目分层entity、dao、service、servlet、webapp 各干什么如果是课设项目我强烈建议不要引 Spring BootServlet JDBC JSP 就够。理由很简单答辩时老师问“你的请求是怎么从页面走到数据库的”Servlet 的链路一句话能讲清Spring Boot 的自动装配反而成了黑匣子学生答不上来容易扣分。recommend/ ├── src/main/java/com/recommend/ │ ├── entity/ # User.java, Scenic.java, Rating.java │ ├── dao/ # ScenicDao.java, RatingDao.javaJDBC操作 │ ├── service/ # RecommendService.java推荐算法入口 │ ├── servlet/ # IndexServlet, RecommendServlet, LoginServlet │ └── util/ # DBUtil.java, SimilarityUtil.java ├── src/main/webapp/ │ ├── static/ # css, js, images │ ├── WEB-INF/ │ │ └── lib/ # mysql-connector-java.jar, jstl.jar │ ├── index.jsp # 推荐页 │ ├── login.jsp │ └── scenic_detail.jsp └── pom.xml # 如果没用 Maven用普通 Web 工程即可各层职责一句话entity 对应三张表的结构dao 里只写 JDBC 查询不写算法service 调 dao 拿数据再调算法工具类生成推荐结果servlet 负责接收请求、调 service、把结果塞进 request 域、转发到 JSPJSP 只做展示。分层的目的不是学规范是让论文里的“系统架构图”有的画也让出问题时知道该看哪一层。3.3 最小可运行配置JDK、Tomcat、MySQL 的版本搭配JSP 项目跑不起来的头号原因不是代码是环境和部署配置。搜“java 环境变量配置详细教程”的人多半是 JDK 装完但 Tomcat 起不来搜“java容器”的则容易把 Tomcat 和 JDK 的角色搞混。Tomcat 是 Servlet 容器负责运行 JSP 和 ServletJDK 是它运行时的基础两者缺一不可。常见的稳定搭配是JDK 8、Tomcat 8.5 或 9、MySQL 5.7 以上、IDEA 或 Eclipse。注意 mysql-connector-java 的版本要和 MySQL 对得上MySQL 5.7 用 5.1.49MySQL 8.0 用 8.0.x版本错配会出现通信协议报错。数据库连接参数单独放一个文件方便论文里写“配置模块”jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/recommend?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8 jdbc.usernameroot jdbc.password123456useSSLfalse 避免 MySQL 8 默认 SSL 握手带来的告警serverTimezoneAsia/Shanghai 解决 JDBC 驱动和数据库时区不一致的报错characterEncodingUTF-8 是中文不乱码的第二道保险。启动时如果 404优先检查 IDEA 里 Artifacts 部署的上下文路径是不是和访问地址一致这块比算法代码更能浪费你半天时间。4. 推荐引擎落地相似度计算与 Top-N 生成的 Java 代码数据表和骨架就绪之后进入整篇论文的核心模块。推荐引擎我一般放在 service 层对外只暴露一个接口根据 userId 返回 List 。页面和 Servlet 完全不关心内部用的是 ItemCF 还是 UserCF这也是前面说“先跑通热门再替换算法”能成立的架构前提。4.1 用双索引 Map 把评分数据加载进内存算法模块不要在每次请求时都查库启动时一次性把评分数据加载进内存。内存结构需要两套索引按用户取评分按景点取被谁评过。UserCF 从用户索引出发ItemCF 从物品索引出发双索引的好处是切换算法时数据层不用改。public class RatingMatrix { // userId - (scenicId - score) private MapInteger, MapInteger, Double userRatings new HashMap(); // scenicId - (userId - score) private MapInteger, MapInteger, Double itemRatings new HashMap(); public void load(ListRating ratings) { for (Rating r : ratings) { userRatings.computeIfAbsent(r.getUserId(), k - new HashMap()) .put(r.getScenicId(), (double) r.getScore()); itemRatings.computeIfAbsent(r.getScenicId(), k - new HashMap()) .put(r.getUserId(), (double) r.getScore()); } } }computeIfAbsent 是 Java 8 提供的便利方法key 不存在时才执行后面的 new HashMap比先判断再 put 的写法短一半。这里的数据量通常只有几千行HashMap 完全够用如果你的爬虫数据到了十万行以上再考虑 Redis 之类的缓存课设阶段没必要上。4.2 皮尔逊相关系数的 Java 实现与最小共同评分项阈值相似度算法在论文里是最容易被追问的部分。余弦相似度实现最简单但它把“未评分”当作 0 处理稀疏矩阵下会严重低估相似度。皮尔逊相关系数只统计两个用户共同评过分的景点并且各自减掉平均分能抵消“有人习惯打 5 分、有人只打 3 分”的尺子差异旅游推荐论文里更常见。public class SimilarityUtil { // 最小共同评分项数低于它直接判定为不相似 private static final int MIN_COMMON 2; public static double pearson(MapInteger, Double a, MapInteger, Double b) { ListInteger common new ArrayList(); for (Integer key : a.keySet()) { if (b.containsKey(key)) { common.add(key); } } if (common.size() MIN_COMMON) { return 0.0; } double avgA common.stream().mapToDouble(a::get).average().orElse(0); double avgB common.stream().mapToDouble(b::get).average().orElse(0); double numerator 0, denomA 0, denomB 0; for (int key : common) { double da a.get(key) - avgA; double db b.get(key) - avgB; numerator da * db; denomA da * da; denomB db * db; } if (denomA 0 || denomB 0) { return 0.0; } return numerator / (Math.sqrt(denomA) * Math.sqrt(denomB)); } }MIN_COMMON 是这个函数的灵魂参数。设置为 2意思是两个用户至少共同评过两个景点才算有计算基础否则直接返回 0。这个阈值太小巧合的相似会被当回事太大稀疏数据下几乎找不到邻居。对于课设规模的数据2 到 5 是合理区间论文里可以放一张不同阈值下的对比表。4.3 ItemCF 的 Top-N 推荐预测评分与排序ItemCF 的实现分两个阶段。离线阶段先算景点与景点的相似度矩阵在线阶段对用户未访问的每个景点累加“该用户评过分的景点”和“目标景点”之间的相似度评分再加权汇总。public class ItemCFRecommender { // 物品相似度矩阵: scenicIdA - (scenicIdB - similarity) private MapInteger, MapInteger, Double itemSim new HashMap(); private static final int NEIGHBOR_K 30; public void train(RatingMatrix matrix) { ListInteger scenicIds new ArrayList(matrix.itemRatings.keySet()); for (int i 0; i scenicIds.size(); i) { for (int j i 1; j scenicIds.size(); j) { int idA scenicIds.get(i), idB scenicIds.get(j); double sim SimilarityUtil.pearson( matrix.itemRatings.get(idA), matrix.itemRatings.get(idB)); if (sim 0) { itemSim.computeIfAbsent(idA, k - new HashMap()).put(idB, sim); itemSim.computeIfAbsent(idB, k - new HashMap()).put(idA, sim); } } } } public ListScenic recommend(int userId, RatingMatrix matrix, ListScenic allScenics) { MapInteger, Double userRated matrix.userRatings.getOrDefault(userId, new HashMap()); MapInteger, Double scoreMap new HashMap(); for (Map.EntryInteger, Double ratedEntry : userRated.entrySet()) { int ratedScenicId ratedEntry.getKey(); double userScore ratedEntry.getValue(); MapInteger, Double sims itemSim.getOrDefault(ratedScenicId, new HashMap()); ListMap.EntryInteger, Double sortedSims sims.entrySet().stream() .sorted((a, b) - Double.compare(b.getValue(), a.getValue())) .limit(NEIGHBOR_K).collect(Collectors.toList()); for (Map.EntryInteger, Double simEntry : sortedSims) { int targetScenicId simEntry.getKey(); if (userRated.containsKey(targetScenicId)) { continue; // 用户已经去过的景点不再推荐 } scoreMap.merge(targetScenicId, userScore * simEntry.getValue(), Double::sum); } } return scoreMap.entrySet().stream() .sorted((a, b) - Double.compare(b.getValue(), a.getValue())) .limit(10) .map(e - scenicById(e.getKey())) .collect(Collectors.toList()); } }两个关键点说一下。第一预测分是“用户对该景点的历史评分 × 相似度”的加权和相似但用户评分高的物品会拿到更大权重这是 ItemCF 最朴素的预测写法容易在论文里用公式表达。第二限制成 NEIGHBOR_K 个相似物品再累加而不是遍历该物品的全部相似项目的是砍掉长尾的低相似度噪声也顺便控制单次请求的计算量。NEIGHBOR_K 一般取 20 到 50越大召回越好、精确率越差具体调参放最后一章讲。5. JSP 展示推荐结果的常见问题排查4 个必踩的坑算法在控制台里跑得通一接到 JSP 页面就出各种幺蛾子。这一章按“现象 → 原因 → 解决”的格式写四个我反复遇到的坑每一个都在答辩前坑过不止一届学生。5.1 推荐列表一片空白浏览器直接 500JSTL 标签库缺失现象Servlet 把推荐结果放进 request 域后转发到 index.jsp页面整屏 500Tomcat 日志报 org.apache.jasper.JasperException 找不到 uri 之类的错误。原因JSP 页面用了c:forEach遍历推荐列表但 WEB-INF/lib 下根本没有 jstl.jar 和 standard.jar。JSP 内置的 EL 表达式不会出这个错一旦用了 JSTL 标签缺依赖就是硬报错。解决把 jstl-1.2.jar 和 standard-1.1.2.jar 放进 WEB-INF/lib 并重启 Tomcat。如果是 Maven 工程加两行依赖后重新打包。页面头部这样引入% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %这个坑最难受的点在于它不报“jar 不存在”而是报 JSP 解析错误新手很难联想到缺失依赖。以后凡是 JSP 页面出现尖括号标签相关的 500先看 lib 目录。5.2 中文乱码请求、响应、数据库三处编码同时要统一现象景点名显示成“???”或者在页面上是乱码数据库里却是正常中文。原因编码有三处任何一处不一致就乱。第一处是 JSP 页面本身的 pageEncoding第二处是 Servlet 响应的 ContentType第三处是 JDBC 连接串里的 characterEncoding。常见写法里往往只设了第一处。解决JSP 页面顶部固定写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %Servlet 里转发前设置response.setContentType(text/html; charsetUTF-8);JDBC URL 带上characterEncodingUTF-8。如果是 POST 表单提交搜索关键词还需要在 Servlet 入口执行request.setCharacterEncoding(UTF-8);否则取到的参数就是乱码。做项目第一天就统一这三处能省掉后面所有中文问题。5.3 每次打开页面都全量重算卡顿明显相似度矩阵没缓存现象首页加载要两三秒点开另一个用户页面又是两三秒服务器 CPU 持续高占用。原因Servlet 里每次都重新调train()方法几百个景点的两两相似度全部重算。有人想用 JSP 页面加载完后刷新一次的办法绕过去结果整页闪烁、数据库压力翻倍完全是给性能问题雪上加霜。解决把物品相似度矩阵做成静态的在项目启动时算一次之后全部复用。评分数据变化比较频繁可以定期失效重建不需要每次请求都触发。public class ItemCFHolder { private static volatile MapInteger, MapInteger, Double simCache; private static final Object LOCK new Object(); public static MapInteger, MapInteger, Double getSimMatrix() { if (simCache null) { synchronized (LOCK) { if (simCache null) { simCache new ItemCFRecommender().train(new RatingMatrix()); } } } return simCache; } }双重检查锁是懒加载的标准写法volatile 防止半初始化对象被其他线程读到。更讲究的做法是用 Java 定时任务框架比如 ScheduledExecutorService 或 Quartz每天凌晨离线重算一次配合 JSP 页面展示这就是论文系统架构图里的“离线计算层 在线推荐层”。5.4 新用户、新景点零推荐冷启动的三个兜底策略现象刚注册的新用户登录首页推荐区域空白刚录入的新景点迟迟不出现。原因协同过滤完全依赖评分行为新用户没有历史评分新景点没有用户评分相似度矩阵里它们的关系全为 0。解决写一个兜底方法按用户状态分三档处理。public ListScenic recommendWithFallback(int userId, RatingMatrix matrix) { if (!matrix.userRatings.containsKey(userId)) { // 新用户根据常住城市推荐当地热门景点 return scenicDao.findHotByCity(userService.findById(userId).getCity(), 10); } ListScenic result recommendByItemCF(userId, matrix); if (result.isEmpty()) { // 有历史但推荐结果为空降级到全局热门 return scenicDao.findGlobalHot(10); } return result; }兜底逻辑的顺序是推荐系统的通用套路个性化优先个性化没有结果就降级到热门无论如何不给用户空页面。新景点没有评分时用它的 category 和 city 字段在数据库里匹配同类景点直接算文本相似度当作伪评分。这套策略写进论文正好对应“冷启动问题的三种解决方案”章节答辩时是加分项。6. 让评测数据替你说服评委离线指标与两个调参技巧推荐效果不能靠“我觉得挺准”要拿数据说话。常见评测流程是把评分数据按 8:2 的比例分成训练集和测试集训练集喂给算法测试集里用户真实去过的景点作为正确答案计算 PrecisionK 和 RecallK。PrecisionK 是推荐列表里用户真正去过的比例RecallK 是用户去过的景点里被推荐出来的比例两者天然此消彼长。6.1 划分训练集时用固定随机种子我印象最深的一次翻车是切分测试集时忘记固定随机种子每次运行结果都不同怎么调参心里都没底。后来习惯在切分代码里固定 seed保证论文里的每组实验可复现。Collections.shuffle(ratings, new Random(42));6.2 邻居数 K 与 MIN_COMMON 的调整方向两个最值得调的参数。MIN_COMMON 越大相似度越可信但覆盖越差数据足够多时设 5课设这种几百条评分的小样本保持 2 到 3。NEIGHBOR_K 增大召回率上去了精确率掉下来论文里跑一个 10、20、30 的小对比挑平衡点展示。这两组参数一写实验章的深度立刻超过及格线。做完这些整套项目的完整度已经不是“能跑”级别了而是“能讲、能比、能解释”。希望这套落地方案帮到你少熬几个夜。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑