资讯详情

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

📅 2026/10/6 11:15:56 | 华诺云谱 👁 阅读
旅游景点个性化推荐系统:基于JSP与协同过滤的Java毕设实战
简介这是一份基于JSPServlet与协同过滤算法的旅游景点个性化推荐系统毕业设计论文面向需要完成智慧旅游或推荐系统课题的本专科生也适合Java Web开发者深入理解景点推荐平台的工程实现。资源包共1个文件为docx格式文档容量约1.35MB正文从摘要、中英文关键词到引言、系统开发关键技术、需求分析与设计层层展开完整描述用户管理、景点分类展示、推荐引擎、景点详情与首页轮播等模块并涵盖MVC结构、JSP页面设计、Servlet请求处理、关系型数据库同步等核心技术。已有99人学习下载内容以传统景点查询网站为基础突出协同过滤算法的个性化推荐落地可帮助读者快速掌握旅游平台的功能流程与推荐系统从数据获取到推荐展示的实现思路。文档结构完整、层次清晰既可作为毕业设计或课程设计的方案蓝本也能为小型旅游平台开发与推荐算法应用提供直接参考。1. 旅游景点个性化推荐论文JavaJSP技术栈为什么仍是毕业论文的稳妥选择每年毕业季都有大量计算机专业学生把“个性化推荐系统”作为毕设选题而“旅游景点”这个场景尤其讨喜——数据好找、业务逻辑直观、演示效果比图书推荐和电影推荐更有画面感。可一旦动手就发现论文里写得天花乱坠的算法和最终答辩时跑的JSP页面之间隔着一条巨大的鸿沟理论用Python验证工程用Java重写两套代码完全对不上导师一问算法细节就露馅。这个标题指向的是“论文系统”双交付的Java Web项目技术栈定位在JSPServletJavaBeanMySQL这是国内高校软件工程和计算机科学专业最常见、最稳妥的组合。JSP负责页面渲染和推荐结果的展示Servlet承担请求分发和业务控制Java类实现协同过滤算法整个项目结构清晰、代码量可控、答辩时可拆可讲。它解决的核心问题只有一个如何让不同用户打开同一个旅游网站时看到的是根据自己历史行为和偏好生成的差异化景点推荐列表而不是千篇一律的热门排行榜。适合谁准备毕设需要快速落地一套可运行系统的学生以及想了解传统Java Web技术如何承载算法逻辑的初级工程师。如果你是冲着Spark、Flink、图神经网络去的这篇文章帮不上忙但如果你想用最少的依赖把“个性化”三个字跑通并写进论文下面这套方案就是一条可复制的直路。先说明一个反直觉的事实JSP没有被淘汰它只是退出了互联网大厂的生产环境但在毕业论文和课程设计这个特定赛道上它依然是代码量、可读性和答辩通过率综合评分最高的选择。2. 推荐算法选型与数据库设计先把论文的“理论基础”立住2.1 为什么选协同过滤而不是深度学习论文工作量与可解释性的平衡做旅游推荐系统的论文最容易犯的错误是一上来就堆模型。用户行为数据只有几百条硬套神经网络训练出来的是黑匣子导师问“你为什么选这个模型”“参数怎么调的”“你的Embedding怎么做的”一个都答不上来。在毕设体量的数据规模下协同过滤无论是基于用户还是基于物品都能给出清晰的计算过程和可解释的推荐依据这恰恰是评审老师最看重的。旅游场景有个特点用户的偏好有明显的“季节性波动”和“地域聚集性”。喜欢山水的用户去四川大概率会选九寨沟喜欢历史人文的去西安大概率会选兵马俑这种偏好用“物品属性标签”就能很好刻画。因此论文里建议采用基于用户的协同过滤User-CF为主、基于物品属性的召回为辅助的混合策略。User-CF负责“和你相似的人去过哪”属性召回负责“你没看过但符合你标签的冷门景点”两者各占一定权重最后融合成Top-N推荐列表。提示论文的理论部分不需要长篇大论写推荐系统发展史重点写清楚“为什么旅游场景适合User-CF”和“相似度计算为什么选皮尔逊相关系数而不是余弦相似度”这两点是答辩时最容易加分的地方。2.2 数据库表结构用户表、景点表、评分表、推荐结果表的字段设计代码写得再漂亮表结构设计一塌糊涂论文的数据流图就画不圆。这个项目的核心表只有四张用户表、景点表、用户评分表、推荐结果缓存表。其中评分表是最重要的它是协同过滤算法的数据源。-- 用户表 CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, register_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 景点表 CREATE TABLE scenic_spot ( spot_id INT PRIMARY KEY AUTO_INCREMENT, spot_name VARCHAR(100) NOT NULL, city VARCHAR(50), category VARCHAR(50), -- 自然风光/历史古迹/主题乐园/城市地标 description TEXT, avg_score DECIMAL(3,2), -- 景点平均分用于冷启动 heat INT DEFAULT 0 -- 浏览次数用于热门召回 ); -- 用户评分表协同过滤的数据源 CREATE TABLE user_rating ( rating_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, spot_id INT NOT NULL, score TINYINT NOT NULL, -- 1~5分 create_time DATETIME, FOREIGN KEY (user_id) REFERENCES user(user_id), FOREIGN KEY (spot_id) REFERENCES scenic_spot(spot_id), UNIQUE KEY uk_user_spot (user_id, spot_id) -- 同一用户对同一景点只保留一条评分 ); -- 推荐结果缓存表 CREATE TABLE recommend_cache ( cache_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, spot_id INT NOT NULL, predict_score DECIMAL(5,4), -- 算法预测分数 rank_no INT, -- 排名序号 update_time DATETIME );表设计的两个关键点。第一user_rating必须加唯一约束否则用户重复点击“评分”按钮会插入多条记录算相似度时数据直接失真。第二recommend_cache表看起来多余但它在答辩演示时价值巨大——推荐算法跑一遍需要几百毫秒甚至更长时间如果每次页面刷新都实时计算JSP页面会慢到让评委失去耐心有了缓存表只需要在用户评分行为发生后触发一次重算即可。2.3 JSP项目如何组织WebContent、src、lib三区划分很多学生的JSP项目乱成一锅粥Java文件、JSP页面、jar包混在一起连Tomcat都懒得配虚拟目录。一个标准的Eclipse或IDEA项目结构应该是三区分离的这样论文里的架构图也好画。src目录放Java源码实体类、DAO类、Servlet、推荐算法类核心。WebContent目录放JSP页面、CSS、JS、图片资源。WebContent/WEB-INF/lib目录放依赖的jar包通常需要mysql-connector-java和jstl库。JSP页面建议按“功能前缀”命名登录页叫login.jsp、注册页叫register.jsp、景点列表叫spot_list.jsp、推荐结果页叫recommend_list.jsp。千万避免page1.jsp、page2.jsp这种名字答辩演示时切来切去连自己都分不清哪个是哪个。为了控制评分数据的稀疏度在spot_list.jsp里要给每个景点一个评分按钮模拟用户行为数据的采集。3. 用户登录与评分采集JSPServlet实现最小可运行链路3.1 用HttpSession管理登录态JSP页面与Servlet的交互个性化推荐的前提是“知道你是谁”所以登录是这个项目的第一道门。不用Spring Security不用拦截器框架只用JSP内置的session对象和Servlet的HttpSession一样能实现完整的登录态管理。关键技术点有两个登录成功后把用户ID放入Session在需要身份验证的页面顶部检查Session是否为空。%-- login.jsp --% % page contentTypetext/html; charsetUTF-8 languagejava % html headtitle登录/title/head body form action${pageContext.request.contextPath}/loginServlet methodpost 用户名input typetext nameusername required 密码input typepassword namepassword required input typesubmit value登录 /form /body /html// LoginServlet.java WebServlet(/loginServlet) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { // 登录成功把用户ID和用户名写入Session HttpSession session request.getSession(); session.setAttribute(userId, user.getUserId()); session.setAttribute(username, user.getUsername()); response.sendRedirect(recommend_list.jsp); // 直接跳到推荐页 } else { // 登录失败转发回登录页并携带错误提示不用sendRedirect request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } }这里有一个经典的选择题登录失败后应该用sendRedirect还是forward前者是302跳转浏览器重新发起一次新请求你塞进request里的errorMsg取不到后者是服务端内部转发request里的属性还能读到所以在login.jsp里用%request.getAttribute(errorMsg)null?:request.getAttribute(errorMsg)%就能显示错误信息。这个细节很多学生不知道答辩时被问到就扣分。3.2 景点评分接口前端表单提交与后端入库推荐算法的数据源是评分。为了模拟真实用户行为需要在景点列表页给用户一个打分入口通过表单把userId、spotId、score三个参数提交到后端的评分Servlet。注意这里的userId不能从页面隐藏域传应该从Session里取否则用户篡改页面源码就能伪造别人的评分数据这一点要在论文的“安全性设计”小节里专门写一段。%-- spot_list.jsp片段 --% table border1 trth景点/thth城市/thth分类/thth评分操作/th/tr c:forEach items${spotList} varspot tr td${spot.spotName}/td td${spot.city}/td td${spot.category}/td td !-- 分数直接用下拉框省去校验 -- form action${pageContext.request.contextPath}/ratingServlet methodpost styledisplay:inline; input typehidden namespotId value${spot.spotId} select namescore option value11分/option option value22分/option option value33分/option option value4 selected4分/option option value55分/option /select input typesubmit value评分 /form /td /tr /c:forEach /tableWebServlet(/ratingServlet) public class RatingServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 从Session拿当前登录用户不从request拿防止伪造 HttpSession session request.getSession(); Integer userId (Integer) session.getAttribute(userId); if (userId null) { response.sendRedirect(login.jsp); return; } Integer spotId Integer.parseInt(request.getParameter(spotId)); Integer score Integer.parseInt(request.getParameter(score)); RatingDao ratingDao new RatingDao(); boolean success ratingDao.saveOrUpdate(userId, spotId, score); if (success) { // 保存成功后触发推荐结果重新计算 RecommendService recommendService new RecommendService(); recommendService.recomputeForUser(userId); response.sendRedirect(recommend_list.jsp); } else { response.sendRedirect(spot_list.jsp?error1); } } }这里的saveOrUpdate方法要在DAO里用INSERT ... ON DUPLICATE KEY UPDATE的SQL语句实现因为user_rating表已经加了唯一约束用户第二次评分同一景点时应该更新分数而不是插入新记录。评分成功后的重算动作在真实系统中不可能同步做但毕设的数据量小几百条评分算一次相似度矩阵也就是几十毫秒的事同步算完再跳转完全来得及。4. 个性化推荐核心逻辑基于用户的协同过滤Java实现4.1 皮尔逊相关系数计算的Java代码这是整个系统的核心。论文里写的是“用一个m×n的评分矩阵表示用户对景点的评分”代码里就要有一个方法能够计算两个用户之间的相似度。皮尔逊相关系数比余弦相似度的优势在于它去除了用户评分习惯的偏置——有的用户习惯打4到5分有的用户习惯打2到3分余弦相似度会把这种“尺度差异”误判为偏好差异皮尔逊不会。public class UserSimilarity { /** * 计算两个用户基于共同评分景点的皮尔逊相关系数 * param userARatings 用户A的评分映射spotId - score * param userBRatings 用户B的评分映射spotId - score * return 相似度范围[-1, 1]无共同评分时返回0 */ public static double pearsonCorrelation(MapInteger, Double userARatings, MapInteger, Double userBRatings) { // 找出两个用户共同评分过的景点 ListInteger commonKeys new ArrayList(); for (Integer key : userARatings.keySet()) { if (userBRatings.containsKey(key)) { commonKeys.add(key); } } // 没有共同评分无法计算相似度 if (commonKeys.isEmpty()) { return 0.0; } int n commonKeys.size(); double sumA 0.0, sumB 0.0; double sumASq 0.0, sumBSq 0.0; double sumAB 0.0; for (Integer key : commonKeys) { double a userARatings.get(key); double b userBRatings.get(key); sumA a; sumB b; sumASq a * a; sumBSq b * b; sumAB a * b; } double numerator n * sumAB - sumA * sumB; double denominator Math.sqrt(n * sumASq - sumA * sumA) * Math.sqrt(n * sumBSq - sumB * sumB); // 分母为0说明两个用户评分完全相同或某方差为0此时判定为不相似 if (denominator 0.0) { return 0.0; } return numerator / denominator; } }这段代码的逻辑关键是“先求共同评分集合再算各种求和值”。pearsonCorrelation函数接收两个Map参数其中Key是景点ID、Value是分数如果if共同评分数量小于2相关系数在统计学上就不可靠所以在调用处要有个过滤逻辑要求两个用户至少共同评过2个景点才算相似否则即使算出来一个接近1的分数也是巧合推荐结果的噪声会很大。另外分母为0的情况很多初学者没处理两个用户都打了完全一样的分数时方差为0除数为0直接抛异常系统就白屏了。4.2 生成Top-N推荐结果并在JSP页面渲染算完相似度之后要做的三件事找K个最近邻、预测当前用户对未评分景点的分数、按预测分数排序取Top-N。这里K取多少很关键K5是常用值但论文里不能只写“我们取K5”得写“经过实验对比K5时推荐准确率达到最高”。public class RecommendService { public ListRecommendItem recommendForUser(int userId, int topN) { // 1. 获取所有用户的评分数据按userId分组 MapInteger, MapInteger, Double allRatings RatingDao.getAllRatingsByUser(); // 2. 获取目标用户已经评过分的景点集合 MapInteger, Double targetUserRatings allRatings.get(userId); if (targetUserRatings null || targetUserRatings.isEmpty()) { // 新用户冷启动按热度召回景点 return getHotFallback(topN); } // 3. 计算目标用户与其他所有用户的皮尔逊相似度 MapInteger, Double similarityMap new HashMap(); for (Map.EntryInteger, MapInteger, Double entry : allRatings.entrySet()) { int otherUserId entry.getKey(); if (otherUserId userId) { continue; } double similarity UserSimilarity.pearsonCorrelation( targetUserRatings, entry.getValue()); // 只保留相似度大于0的正向邻居负相关用户没有参考价值 if (similarity 0) { similarityMap.put(otherUserId, similarity); } } // 4. 按相似度排序取TopKK5是经验值 ListMap.EntryInteger, Double sortedNeighbors new ArrayList(similarityMap.entrySet()); sortedNeighbors.sort((a, b) - Double.compare(b.getValue(), a.getValue())); int k Math.min(5, sortedNeighbors.size()); // 5. 预测目标用户对未评分景点的分数 MapInteger, Double predictScores new HashMap(); for (int i 0; i k; i) { Map.EntryInteger, Double neighbor sortedNeighbors.get(i); int neighborId neighbor.getKey(); double neighborSim neighbor.getValue(); MapInteger, Double neighborRatings allRatings.get(neighborId); for (Map.EntryInteger, Double ratingEntry : neighborRatings.entrySet()) { int spotId ratingEntry.getKey(); double score ratingEntry.getValue(); // 跳过用户已经评过分的景点 if (targetUserRatings.containsKey(spotId)) { continue; } // 加权累加相似度 × 评分 predictScores.merge(spotId, neighborSim * score, Double::sum); } } // 6. 按预测分数排序返回TopN ListMap.EntryInteger, Double sortedResults new ArrayList(predictScores.entrySet()); sortedResults.sort((a, b) - Double.compare(b.getValue(), a.getValue())); ListRecommendItem result new ArrayList(); for (int i 0; i Math.min(topN, sortedResults.size()); i) { int spotId sortedResults.get(i).getKey(); double score sortedResults.get(i).getValue(); // 这里分数没有除以相似度和是一个加权和不是均值注意论文里要写清楚 result.add(new RecommendItem(spotId, score)); } return result; } // 冷启动新用户没有评分返回热度最高的景点 private ListRecommendItem getHotFallback(int topN) { return RatingDao.getTopHotSpots(topN); } }预测分数的公式有两个流派一是“加权和”二是“加权平均值”。上面代码用的是加权和它的特点是相似度高的邻居贡献更多分数但结果值会大于5需要归一化展示加权平均值要除以邻居相似度之和范围更可控。论文里建议写加权平均值因为更接近“预测评分”的语义但实现时要处理“相似度和为0”的边界情况比如把所有邻居的相似度求个和算完预测值再除以这个和。预测完之后结果要从recommend_list.jsp展示出来。这里不直接写死HTML表格而是用JSTL的c:forEach循环渲染request.setAttribute(recommendList, recommendList)里的数据并用EL表达式取景点的名称、城市、预测分数。5. JSP项目避坑5个让推荐系统跑不起来的常见问题与排查5.1 现象1中文景点名在页面显示成“???”或者乱码原因很典型JSP页面没有设置pageEncodingServlet读取参数时没有设置request.setCharacterEncoding(UTF-8)MySQL连接URL没有加characterEncodingutf-8参数三层中任何一层断了都会乱码。解决动作也有三步。第一步login.jsp、spot_list.jsp、recommend_list.jsp页面顶部统一加% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%。第二步每个doPost方法第一行写request.setCharacterEncoding(UTF-8)HttpServletResponse的setContentType(text/html;charsetUTF-8)每个Servlet都加上。第三步MySQL连接URL改成jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingUTF-8useSSLfalse。5.2 现象2JSP页面加载后不刷新推荐结果永远不变Tomcat对recommend_list.jsp页面默认会缓存用户评分后跳转过来看到的还是旧的推荐列表就像页面有“记忆”一样怎么刷新都没用。这是最坑的缓存问题。JSP引擎编译后就是Servlet第一次访问后编译结果常驻内存如果浏览器也有缓存直接命中就完全不重新执行了。解决方法是三管齐下在生成推荐的Servlet里设置response.setHeader(Cache-Control, no-cache)同时给推荐页面的URL拼接一个时间戳参数比如recommend_list.jsp?t%System.currentTimeMillis()%强制浏览器重新请求。5.3 现象3用户评分数据太少协同过滤算出来的推荐列表是空的刚登录的新用户只评分了1个景点其他所有用户跟他的共同评分景点数为0相似度全是0邻居集合为空sortedNeighbors为空最终推荐列表直接白屏。这在新手项目里出现概率极高。解决路径是设计一个“冷启动兜底策略”当相似邻居数量小于2时放弃协同过滤改用热度推荐——按景点heat字段降序取前N个并且把分类标签与用户评分最高的景点分类一致的景点优先排在前面。这个逻辑在RecommendService.recommendForUser里已经预留了getHotFallback的入口但要记得当k2时也走这个分支而不是只处理“完全没有评分”的情况。5.4 现象4JDBC连接MySQL不稳定隔一段时间就报CommunicationsException数据库连接用完不关闭或者每次请求都创建新连接达到MySQL的wait_timeout后连接被服务端关闭池里的连接变成死连接再取出来用就报错。毕设项目的数据库操作很简单但要养成“一个操作一个连接、用完即关”的习惯在finally块里关闭Connection、PreparedStatement、ResultSet三个对象关闭顺序从内向外。更好一点的做法是用一个简单的DBUtil工具类里面用静态方法获取连接并注册驱动所有DAO都从DBUtil.getConnection()拿连接用完在finally里归还避免到处写重复的Class.forName(com.mysql.jdbc.Driver)。5.5 现象5Servlet路径映射404点击登录按钮页面找不到WebServlet(/loginServlet)和JSP页面里写的actionloginServlet看起来是对上了但用的是相对路径当前页面URL是http://localhost:8080/travel/spot_list.jsp表单提交到的是http://localhost:8080/travel/loginServlet这个能通但如果在更深的目录页面里比如/travel/user/detail.jsp相对路径就找不对了。凡是JSP页面里的表单提交路径、超链接路径一律用EL表达式拼接绝对路径前缀${pageContext.request.contextPath}/loginServlet。这个表达式会自动输出/travel作为项目上下文路径配合Web服务器就能生成绝对URL彻底绕开相对路径问题。所有sendRedirect跳转的地址也要加request.getContextPath()前缀否则从http://localhost:8080/travel/loginServlet重定向到recommend_list.jsp这个相对路径时实际跳转的是http://localhost:8080/recommend_list.jsp外层目录整个丢了。6. 论文里的实验论证用覆盖率与准确率验证推荐质量6.1 评估指标准确率、召回率、覆盖率怎么算推荐结果写进论文不能只靠几张截图得有量化指标。最常被答辩老师追问的三个指标是准确率、召回率和覆盖率。准确率是“推荐列表里用户真正喜欢的景点占比”召回率是“用户喜欢的景点里被推荐出来的占比”覆盖率是“推荐列表包含的景点占全部景点的比例”。这三个指标用200条用户评分数据就能算不需要大数据平台。计算准确率的实验方案是离线模拟把评分数据按用户分组每个用户的评分随机抽20%作为测试集剩下80%作为训练集用训练集跑推荐算法得出Top-10列表然后用测试集验证如果某个景点的评分大于等于4分且出现在推荐列表里算作一次命中。把全部用户的命中次数除以总推荐次数就是推荐精度通常能达到15%到25%就算合理因为旅游推荐场景本身比电影推荐稀疏得多。6.2 用交叉验证支撑论文结论与推荐结果解释性单次切分数据得出的准确率有偶然性论文里最好用5折交叉验证把评分数据均匀分成5份轮流取1份做测试集、4份做训练集跑5次取平均值这样评审老师很难挑出数据划分的硬伤。在论文结果部分放一张表格对比“基于用户的协同过滤”“基于物品的协同过滤”“热门推荐基线”三组实验的准确率、召回率和覆盖率差异一目了然。覆盖率这个指标尤其重要因为热门推荐算法的覆盖率一定是最低的个性化推荐系统的覆盖率明显更高这个数据能直接证明系统的“个性化”价值是论文里最出彩的一张表。交叉验证的Java代码通常不在JSP项目里跑而是单独写一个测试类把评分数据从数据库导出成CSV文件后用纯Java计算指标这样即使论文的系统部分出问题实验结果依然独立可复现。我当年做毕业设计的习惯是先跑通算法和评估再动手写JSP页面因为推荐列表的价值最终要由指标背书页面只是展示载体顺序不能反——代码全写完了再补实验数据那就是给自己挖坑填土了。最后提醒一个容易忽略的细节论文的“系统测试”章节里除了常规的功能测试用例表一定要加入“冷启动场景测试”——新注册用户无任何评分时推荐页是否展示热门景点“极端稀疏场景测试”——只有3条评分时推荐页是否不报错。这两个用例写进测试表答辩时展示给老师看比任何性能测试的数据都更能体现你考虑过真实边界问题希望这套JSP协同过滤的组合方案能帮你把论文和系统一次走通答辩顺利。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑