简介:这是一份基于JSP+Servlet与协同过滤算法的旅游景点个性化推荐系统毕业设计论文,面向需要完成智慧旅游或推荐系统课题的本专科生,也适合Java Web开发者深入理解景点推荐平台的工程实现。资源包共1个文件,为docx格式文档,容量约1.35MB,正文从摘要、中英文关键词到引言、系统开发关键技术、需求分析与设计层层展开,完整描述用户管理、景点分类展示、推荐引擎、景点详情与首页轮播等模块,并涵盖MVC结构、JSP页面设计、Servlet请求处理、关系型数据库同步等核心技术。已有99人学习下载,内容以传统景点查询网站为基础,突出协同过滤算法的个性化推荐落地,可帮助读者快速掌握旅游平台的功能流程与推荐系统从数据获取到推荐展示的实现思路。文档结构完整、层次清晰,既可作为毕业设计或课程设计的方案蓝本,也能为小型旅游平台开发与推荐算法应用提供直接参考。
1. 旅游景点个性化推荐论文+Java:JSP技术栈为什么仍是毕业论文的稳妥选择
每年毕业季都有大量计算机专业学生把“个性化推荐系统”作为毕设选题,而“旅游景点”这个场景尤其讨喜——数据好找、业务逻辑直观、演示效果比图书推荐和电影推荐更有画面感。可一旦动手就发现,论文里写得天花乱坠的算法和最终答辩时跑的JSP页面之间,隔着一条巨大的鸿沟:理论用Python验证,工程用Java重写,两套代码完全对不上,导师一问算法细节就露馅。
这个标题指向的是“论文+系统”双交付的Java Web项目,技术栈定位在JSP+Servlet+JavaBean+MySQL,这是国内高校软件工程和计算机科学专业最常见、最稳妥的组合。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. 用户登录与评分采集:JSP+Servlet实现最小可运行链路
3.1 用HttpSession管理登录态:JSP页面与Servlet的交互
个性化推荐的前提是“知道你是谁”,所以登录是这个项目的第一道门。不用Spring Security,不用拦截器框架,只用JSP内置的session对象和Servlet的HttpSession,一样能实现完整的登录态管理。关键技术点有两个:登录成功后把用户ID放入Session;在需要身份验证的页面顶部检查Session是否为空。
<%-- login.jsp --%> <%@ page contentType="text/html; charset=UTF-8" language="java" %> <html> <head><title>登录</title></head> <body> <form action="${pageContext.request.contextPath}/loginServlet" method="post"> 用户名:<input type="text" name="username" required> 密码:<input type="password" name="password" required> <input type="submit" 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 border="1"> <tr><th>景点</th><th>城市</th><th>分类</th><th>评分操作</th></tr> <c:forEach items="${spotList}" var="spot"> <tr> <td>${spot.spotName}</td> <td>${spot.city}</td> <td>${spot.category}</td> <td> <!-- 分数直接用下拉框,省去校验 --> <form action="${pageContext.request.contextPath}/ratingServlet" method="post" style="display:inline;"> <input type="hidden" name="spotId" value="${spot.spotId}"> <select name="score"> <option value="1">1分</option> <option value="2">2分</option> <option value="3">3分</option> <option value="4" selected>4分</option> <option value="5">5分</option> </select> <input type="submit" value="评分"> </form> </td> </tr> </c:forEach> </table>@WebServlet("/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?error=1"); } } }这里的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(Map<Integer, Double> userARatings, Map<Integer, Double> userBRatings) { // 找出两个用户共同评分过的景点 List<Integer> 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取多少很关键,K=5是常用值,但论文里不能只写“我们取K=5”,得写“经过实验对比,K=5时推荐准确率达到最高”。
public class RecommendService { public List<RecommendItem> recommendForUser(int userId, int topN) { // 1. 获取所有用户的评分数据,按userId分组 Map<Integer, Map<Integer, Double>> allRatings = RatingDao.getAllRatingsByUser(); // 2. 获取目标用户已经评过分的景点集合 Map<Integer, Double> targetUserRatings = allRatings.get(userId); if (targetUserRatings == null || targetUserRatings.isEmpty()) { // 新用户冷启动:按热度召回景点 return getHotFallback(topN); } // 3. 计算目标用户与其他所有用户的皮尔逊相似度 Map<Integer, Double> similarityMap = new HashMap<>(); for (Map.Entry<Integer, Map<Integer, 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. 按相似度排序,取TopK,K=5是经验值 List<Map.Entry<Integer, Double>> sortedNeighbors = new ArrayList<>(similarityMap.entrySet()); sortedNeighbors.sort((a, b) -> Double.compare(b.getValue(), a.getValue())); int k = Math.min(5, sortedNeighbors.size()); // 5. 预测目标用户对未评分景点的分数 Map<Integer, Double> predictScores = new HashMap<>(); for (int i = 0; i < k; i++) { Map.Entry<Integer, Double> neighbor = sortedNeighbors.get(i); int neighborId = neighbor.getKey(); double neighborSim = neighbor.getValue(); Map<Integer, Double> neighborRatings = allRatings.get(neighborId); for (Map.Entry<Integer, 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 List<Map.Entry<Integer, Double>> sortedResults = new ArrayList<>(predictScores.entrySet()); sortedResults.sort((a, b) -> Double.compare(b.getValue(), a.getValue())); List<RecommendItem> 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 List<RecommendItem> 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页面没有设置pageEncoding,Servlet读取参数时没有设置request.setCharacterEncoding("UTF-8"),MySQL连接URL没有加characterEncoding=utf-8参数,三层中任何一层断了都会乱码。
解决动作也有三步。第一步,login.jsp、spot_list.jsp、recommend_list.jsp页面顶部统一加<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>。第二步,每个doPost方法第一行写request.setCharacterEncoding("UTF-8"),HttpServletResponse的setContentType("text/html;charset=UTF-8"),每个Servlet都加上。第三步,MySQL连接URL改成jdbc:mysql://localhost:3306/travel_db?useUnicode=true&characterEncoding=UTF-8&useSSL=false。
5.2 现象2:JSP页面加载后不刷新,推荐结果永远不变
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的入口,但要记得当k<2时也走这个分支,而不是只处理“完全没有评分”的情况。
5.4 现象4:JDBC连接MySQL不稳定,隔一段时间就报CommunicationsException
数据库连接用完不关闭,或者每次请求都创建新连接,达到MySQL的wait_timeout后连接被服务端关闭,池里的连接变成死连接,再取出来用就报错。
毕设项目的数据库操作很简单,但要养成“一个操作一个连接、用完即关”的习惯,在finally块里关闭Connection、PreparedStatement、ResultSet三个对象,关闭顺序从内向外。更好一点的做法是用一个简单的DBUtil工具类,里面用静态方法获取连接并注册驱动,所有DAO都从DBUtil.getConnection()拿连接,用完在finally里归还,避免到处写重复的Class.forName("com.mysql.jdbc.Driver")。
5.5 现象5:Servlet路径映射404,点击登录按钮页面找不到
@WebServlet("/loginServlet")和JSP页面里写的action="loginServlet"看起来是对上了,但用的是相对路径,当前页面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+协同过滤的组合方案能帮你把论文和系统一次走通,答辩顺利。
本文还有配套的精品资源,点击获取