基于协同过滤的SSM旅游平台推荐算法实战解析 简介一套基于SSMSpringSpringMVCMyBatis框架的在线通用旅游平台网站毕业设计源码采用协同过滤推荐算法面向需要完成JavaWeb毕业设计的学生以及想学习SSM整合与推荐系统开发的开发者。系统已实现景点推荐管理、精选路线管理、用户信息管理和系统管理四大核心模块其中管理员可维护景点基本信息、设置精选路线、审核注册用户信息并发布系统公告、管理在线留言与站内新闻前端与后端代码完整配套MySQL数据库脚本导入后即可运行。压缩包共1288个文件大小约52.7MB主要文件类型包括JSP页面、Java类、MyBatis映射XML、Spring配置文件、JAR依赖包、JS/CSS/HTML静态资源以及SQL脚本并含有较多项目运行截图与演示动图目录结构清晰便于对照学习。目前已有58人学习使用适合作为毕业设计参考资料、协同过滤推荐实践案例或旅游类网站二次开发的基础工程。1. 一个含协同过滤的 SSM 旅游平台值得你打开看看做毕业设计搜“java 毕业设计 源码”八成会看到这种标题基于协同过滤的在线通用旅游平台SSM 前后端完整源码。它不是一个只有登录页的 demo而是一整套能跑起来演示的旅游网站游客注册、浏览景点、看精选路线、留言管理员维护景点信息、编辑路线、发公告、处理留言。技术核心是协同过滤推荐能根据用户历史行为给景点排序这让它比普通增删改查项目多了个能写进简历的算法点。适合两类人一类是课程设计/毕业设计需要可演示、可改动的 java web 项目另一类是准备面试、想通过读真实项目搞懂 SpringMVC MyBatis 到底怎么协作的人。下面按部署、读表、读推荐代码的顺序拆再给你列一份避坑清单。2. 先把 SSM 全家桶地图画出来配置文件、表结构与 MySQL 导入最开始别急着点启动按钮。从 zip 包解压出来后第一件事永远是把 src/main/resources 和 web.xml 整个翻一遍。SSM 不是像 Spring Boot 那样一个自动配置全搞定它靠一堆 XML 手工拧合任何一个路径配错后面的界面全白搭。这个项目既然标着“前后端完整源码”前端页面通常在 webapp 或 WebContent 下后端 Java 在 src 下剩下的重点就是看 Spring、SpringMVC、MyBatis 三个框架怎么接在一起。2.1 从 web.xml 和 spring-*.xml 看框架协作方式SSM 项目启动后Tomcat 会先读 web.xml。它里面注册了两个关键东西一个是 ContextLoaderListener负责拉起 Spring 根容器另一个是 DispatcherServlet它是 SpringMVC 的前端控制器拦截匹配的请求。常见写法是把 spring-mvc.xml 和 spring-mybatis.xml 分开我拿到源码包后也是先找这两个文件。!-- web.xml 关键配置片段 -- listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mybatis.xml/param-value /context-param servlet servlet-namespringmvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mapping这里最容易看走的点是url-pattern。如果写成*.doController 里的映射路径就要带上 .do如果写成/SpringMVC 接管所有请求静态资源必须额外配mvc:resources否则 css/js 全 404。我见到很多毕设源码里 Controller 的 RequestMapping 写的是/scenic/list.do而项目里 web.xml 却用的/那你访问的时候就要按代码里的实际路径来别想当然。再往下看 spring-mybatis.xml!-- spring-mybatis.xml -- bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.travel.dao/ /beanxml 里引入了外部的 jdbc.properties所以数据库连接信息不用在 xml 里硬编码。mapperLocations 指向所有 Mapper 映射文件MapperScannerConfigurer 的 basePackage 是 DAO 接口所在包。实际项目里很多人把 DAO 接口写在 com.travel.dao把 mapper.xml 放在 resources/mapper路径对不上就会报 Invalid bound statement。SSM 的常用注解 Repository、Service、Controller 也是在包扫描里被认出来的这个后面讲到 Controller 时再展开。2.2 旅游平台的表结构其实没有想象中那么多摘要里写的四个功能模块——景点推荐管理、精选路线管理、用户信息管理、系统管理落到数据库里就是围绕用户、景点、路线、公告、留言这几张核心表。系统管理里的站点简介、站内新闻基本也是一张单表加一个后台上传入口。我习惯先从源码包里找 sql 文件把表关系画出来再看代码这样看业务会快一倍。下面是一份典型的建表脚本实际包里的字段可能更多但结构类似-- 用户表 CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(64) NOT NULL COMMENT 密码可存MD5值, nickname varchar(50) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 景点表 CREATE TABLE t_scenic ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, intro text COMMENT 景点介绍, address varchar(255) DEFAULT NULL, price decimal(10,2) DEFAULT 0.00, image varchar(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户-景点行为表协同过滤的核心输入 CREATE TABLE t_user_scenic_behavior ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, scenic_id int(11) NOT NULL, behavior_type tinyint(4) DEFAULT 0 COMMENT 0浏览 1收藏 2评分, score tinyint(4) DEFAULT 0 COMMENT 评分1-5浏览时为空, update_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_scenic (scenic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;另外还有精选路线表 t_route 和路线与景点的关联表 t_route_scenic以及公告表、留言表。重点说行为表协同过滤算法不关心用户长什么样只关心谁对哪些景点产生了行为。收藏和评分是最直接的信号浏览因为是隐性信号可以做加权浏览算 1 分收藏算 3 分评分直接用分数。如果没有评分表很多实现里默认把收藏行为当成“喜欢”的标记。在设计表的时候我一般建议加一个 status 字段管理员下架景点时不要物理删除否则已经产生的行为数据跟着没了推荐结果会突然断层。这个细节在毕业设计答辩时提到老师会觉得你考虑过数据一致性。2.3 导入 MySQL 脚本两个动作三个易错点拿到 sql 文件后我通常不用 Navicat 那种图形工具因为命令行导入出错时错误信息更明确。先建库再导入# 登录 MySQL 并创建数据库注意字符集 mysql -u root -p -e CREATE DATABASE IF NOT EXISTS travel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入 sql 文件 mysql -u root -p travel_db /path/to/travel_db.sql # 验证表都建好 mysql -u root -p travel_db -e SHOW TABLES;这里三个易错点是第一建库的字符集要和表一致如果建库用了 utf8但表定义是 utf8mb4之后连接库再查表情符号可能出问题。第二导入的顺序一般没问题但要确认 sql 里有 USE 语句或者导入时指定了库名否则会导到默认库里。第三如果 sql 文件里已经包含 DROP TABLE导入会覆盖旧数据那你在自己机器上可以用但不要拿去覆盖别人的库。导入完以后顺手跑一句SELECT u.username, s.name, b.behavior_type, b.score FROM t_user_scenic_behavior b JOIN t_user u ON b.user_id u.id JOIN t_scenic s ON b.scenic_id s.id LIMIT 20;看到能查出游客对景点的行为记录说明数据基本就位了后面的推荐算法才有东西可算。如果这个表是空的那协同过滤就只会返回热门景点这个冷启动问题后面避坑章再说。3. 让项目跑起来从 IDEA 启动到 Tomcat 部署的完整操作光看懂配置还不够毕设现场演示才是硬道理。这里给出一套我在本地跑 java 毕业设计源码的固定流程包括版本匹配、参数修改和部署顺序。按照这套下来大多数 ssm 项目都能在同一套环境里跑通。3.1 环境版本哪些必须固定哪些可以放宽先给一个版本范围表格这个范围是我实际配过的组件推荐版本说明JDK1.8SSM 老项目大多按 Java 8 编译Maven3.6.x3.8 以上也能用个别老私服仓库可能报错Tomcat8.5javax 包命名空间的直接按 8.5 用省去兼容问题MySQL5.7 / 8.08.0 注意驱动改 com.mysql.cj.jdbc.DriverIDEA2019新版本不背锅干净 Maven 项目即可为什么 JDK 必须 1.8很多 2018 到 2021 年的 ssm 毕业设计用的是 CGLIB、Javassist 这些字节码库它们在 JDK 17 上偶尔会直接抛 java.lang.reflect.InaccessibleObjectException调 module 开关能救一部分但与其折腾不如装一个 1.8。Tomcat 8.5 道理也一样Tomcat 10 把 javax.servlet 移到 jakarta.servlet代码里 import javax.servlet 的包在 Tomcat 10 上根本编译不过除非你全局替换 import否则直接翻车。Maven 仓库建议先把阿里云镜像配好否则第一次拉依赖会等很久。在 settings.xml 里加上mirror idaliyunmaven/id mirrorOf*/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror3.2 改配置数据库、端口、上传路径环境装好后做三件事。第一件事是 jdbc.properties 里的连接信息改成你本地数据库注意 MySQL 8.0 的驱动类名要带 cjjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你自己机器的密码参数解释useUnicodetrue 和 characterEncodingutf8 处理中文serverTimezoneAsia/Shanghai 解决 MySQL 8.0 时区报错useSSLfalse 避免 SSL 握手警告allowPublicKeyRetrievaltrue 是 MySQL 8.0 客户端用 caching_sha2_password 时容易需要的。如果项目用的是 MySQL 5.7驱动类名配成 com.mysql.jdbc.Driver 就行但 8.0 驱动也能向下兼容连 5.7这里不用纠结。第二件事是 Tomcat 的端口。如果你的 8080 被占去 conf/server.xml 改 Connector port。改完之后访问路径会变记得同时看项目的 Artifact name。第三件事是文件上传路径。景点图片很可能上传到本地磁盘代码里常见的是把路径配置在某个 Const 类或 properties 里需要搜一下 uploadPath 这个关键词然后把它指向一个你有写权限的目录比如 D:/upload。如果不管它图片可能存到 Tomcat 临时目录重启后图片就丢了。3.3 从 IDEA 到浏览器war 包的两种跑法我一般先在 IDEA 里用 Maven 的 package 把它打成 war再丢到 Tomcat 的 webapps 下这样最接近线上部署。命令是这样# 在项目根目录执行跳过测试打包 mvn clean package -DskipTests打包成功后 target 目录会生成 xxx.war。把这个 war 复制到 Tomcat webapps 下再启动 Tomcat它会自动解压。启动后访问http://localhost:8080/项目名/如果你是在 IDEA 里直接配 Tomcat也可以新建一个 Local Tomcat Server在 Deployment 里选 war explodedApplication context 填 /travel。两种方式都行但要注意war 包解压后的目录名就是上下文路径访问路径别少了斜杠或打错大小写。启动以后日志里看到类似 “Deployment of web application archive ... has finished” 才算成功。如果只是看到 Tomcat started 但项目没部署十有八九是 artifact 没配好。如果项目里没有 Maven 的 pom.xml那可能是被精简成了普通 web 工程IDEA 里直接导入 src 和 webapp 也能跑Lib 里要手动加依赖。这种情况建议先看根目录有没有 pom.xml有就省事没有就要检查 Web/WEB-INF/lib 下是否躺着一堆 jar 包。放到 Linux 服务器部署时操作也类似把 war 包 scp 到服务器的 webapps 下然后用 startup.sh 启动。习惯上我会先看 catalina.out 日志确认数据库连接和 mapper 加载都过了再放给测试机器用。4. 核心看点协同过滤怎么在旅游平台上生成“猜你喜欢”前面把项目跑起来后最值得看的就是推荐模块。协同过滤在标题里标得明明白白但源码到底实现到什么程度决定你答辩时能讲多深——是只写了相似度算法类放着还是真正接进了景点推荐管理模块这是两码事。从这个包的标题和功能对应关系看它的推荐器至少是接进了景点推荐或路线推荐任一条链路的下面按实现思路讲。4.1 基于用户还是基于物品先把选型逻辑说清协同过滤分成两类。基于用户UserCF的思路是找到和你兴趣相似的一批用户把这些人喜欢的、但你还没看过的景点推荐给你。基于物品ItemCF的思路是如果你收藏过西湖那么和西湖相似的其他景区也值得推荐。旅游平台有一个特点景点数量不会像电商那样动辄几百万但用户数量可以很多新游客每天都有。所以做好推荐系统从原理上我更推荐基于物品因为物品相似度相对稳定可以根据景点属性提前算好但毕设源码为了教学方便大多写基于用户因为代码更直观、展示效果更符合“猜你喜欢”的直觉。你读代码时先看有没有计算用户相似度的部分没有就把基于用户的例子拿过来改。4.2 评分矩阵与余弦相似度一段能跑的 Java 实现先给一个只依赖 JDK 的简化版协同过滤类完全可以拷到自己的 service 里跑不用引第三方推荐库。代码里用 Map 表示用户到景点到分数的矩阵public class UserCF { // userId - (scenicId - 得分) private MapInteger, MapInteger, Double userItemScore new HashMap(); // 余弦相似度只统计两个用户共同打分过的景点 public double cosine(int userA, int userB) { MapInteger, Double a userItemScore.getOrDefault(userA, Collections.emptyMap()); MapInteger, Double b userItemScore.getOrDefault(userB, Collections.emptyMap()); if (a.isEmpty() || b.isEmpty()) return 0.0; double dot 0.0, normA 0.0, normB 0.0; for (Map.EntryInteger, Double entry : a.entrySet()) { int scenicId entry.getKey(); double scoreA entry.getValue(); normA scoreA * scoreA; Double scoreB b.get(scenicId); if (scoreB ! null) { dot scoreA * scoreB; } } for (Double scoreB : b.values()) { normB scoreB * scoreB; } if (normA 0.0 || normB 0.0) return 0.0; return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } }这段代码核心在 dot 那步两个向量重叠的维度越少分子越小相似度越接近 0。公共景点为空时直接返回 0省去除零判断。要注意的是当用户行为非常稀疏时余弦相似度会把“只有一次共同收藏”的两个用户算成相似度 1这会导致推荐偏差。所以在实际业务层我会加一个修正共同景点数少于 3 的相似度乘个惩罚系数或者直接置 0。答辩老师问你“稀疏矩阵怎么办”时用这个回答。下一步是推荐。推荐分两层先找 Top-K 相似用户再把这些用户打分过的景点按相似度加权汇总public MapInteger, Double recommend(int targetUser, int topN) { MapInteger, Double score new HashMap(); MapInteger, Double target userItemScore.getOrDefault(targetUser, Collections.emptyMap()); // 遍历所有用户找出相似度最高的前 K 个 PriorityQueueint[] simQueue new PriorityQueue(Comparator.comparingDouble(arr - arr[1])); for (Map.EntryInteger, MapInteger, Double entry : userItemScore.entrySet()) { int otherUser entry.getKey(); if (otherUser targetUser) continue; double sim cosine(targetUser, otherUser); if (sim 0) continue; simQueue.offer(new int[]{otherUser, (int) (sim * 1000)}); // 小数转整方便比较 if (simQueue.size() 20) simQueue.poll(); // 只保留 Top20 } // 用相似用户的得分加权累加 while (!simQueue.isEmpty()) { int[] item simQueue.poll(); int otherUser item[0]; double sim item[1] / 1000.0; MapInteger, Double otherScores userItemScore.get(otherUser); for (Map.EntryInteger, Double e : otherScores.entrySet()) { int scenicId e.getKey(); if (target.containsKey(scenicId)) continue; // 过滤已消费 score.merge(scenicId, e.getValue() * sim, Double::sum); } } // 按分数排序取 topN return score.entrySet().stream() .sorted(Map.Entry.Integer, DoublecomparingByValue().reversed()) .limit(topN) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) - a, LinkedHashMap::new)); }这段容易出错的有两个地方。第一是 PriorityQueue 里我把相似度乘以 1000 转成 int比较时只比较到小数点后 3 位如果你要更高精度可以改写成 Double 类型队列不要为了省事丢精度。第二是过滤步骤recommend 里只过滤了“目标用户已经消费过”的景点但没过滤掉相似用户之间重复的景点这是可以的因为加权求和正好把热门景点分数拉高如果不想太热可以再加一个“物品被推荐的次数”惩罚这里不展开。4.3 和 Spring 业务层对接推荐结果怎么变成景点列表协同过滤类本身不算业务代码真正上线的是 Service。Controller 收到前端请求后Service 要先把行为数据灌进 UserCF再计算推荐最后拿景点 ID 去数据库中查详情Service public class RecommendServiceImpl implements RecommendService { Autowired private UserBehaviorDao behaviorDao; Autowired private ScenicDao scenicDao; Override public ListScenic recommendScenicList(int userId, int topN) { UserCF cf new UserCF(); // 查用户近30天的行为记录 ListUserBehavior list behaviorDao.findRecentByUserId(userId, 30); for (UserBehavior b : list) { cf.addBehavior(b.getUserId(), b.getScenicId(), b.getScore()); } MapInteger, Double rec cf.recommend(userId, topN); if (rec.isEmpty()) { // 兜底用户行为太少返回热门景点 return scenicDao.findHotList(topN); } return scenicDao.findDetailedByIds(rec.keySet()); } }这里的 addBehavior 是对前面 Map 的封装把一条行为记录塞进 userItemScore。关键点是兜底逻辑当行为数据不足时冷启动直接查热门榜。很多项目会忽略这一步导致推荐结果为空页面空白。加一个热门兜底整个推荐模块的健壮性就上来了。Controller 层用 SSM 常用注解就三条Controller RequestMapping(/recommend) public class RecommendController { Autowired private RecommendService recommendService; GetMapping(/list) public String list(HttpSession session, Model model) { Integer userId (Integer) session.getAttribute(loginUserId); ListScenic list recommendService.recommendScenicList(userId null ? 0 : userId, 6); model.addAttribute(recommendList, list); return recommend; } }逻辑说明从 session 拿当前登录用户 ID如果没登录就用 userId0Service 端对 0 直接返回热门这样未登录用户也能看到推荐区。Model 里的数据传到 JSP 后视图渲染就是下一步。4.4 前端怎么展示JSP 循环渲染推荐卡片这个项目是“前后端完整源码”但严格说属于服务端渲染不是真正的前后端分离——页面由 JSP 在 Tomcat 端组装前端拿到的是最终 HTML。推荐模块的页面通常是一排卡片遍历后台传过来的 recommendList%-- recommend.jsp 推荐列表片段 --% % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach varscenic items${recommendList} div classscenic-card img src${scenic.image} alt${scenic.name} / div classscenic-info h3${scenic.name}/h3 p${scenic.intro}/p span classprice${scenic.price} 元起/span /div /div /c:forEach当你把它伪装成前后端分离项目时可以在 Controller 上方加 ResponseBody把返回类型改成 JSON 对象前端再用 ajax 渲染。适合在简历里写“前后端数据交互”但要注意 JSP 的 ModelAndView 和 JSON 返回方式不能同时用需要单独加一个 Rest 接口。5. 部署与调优避坑从乱码到冷启动的 5 个翻车现场源码资源真正值钱的地方不在于“看得到功能”而在于“别人已经把坑踩过一遍”。这里整理五类最常见的问题也是你在答辩现场最可能被环境整崩溃的瞬间。每一条都按现象、原因、解决的顺序写。5.1 中文乱码改了连接串也没用现象页面标题、景点名称全是问号或一堆编码符号MySQL 命令行里查表却是好的。原因三处编码必须一致——数据库表字符集、JDBC 连接串、JSP 或 Servlet 的响应编码。只改一处往往不生效很多项目在 web.xml 里配了 CharacterEncodingFilter但 URL 里没带 characterEncoding或者数据库建库时用了 latin1照样乱。解决先查库表字符集把库表统一成 utf8mb4再保证 jdbc.url 里带 characterEncodingutf8最后确认 SpringMVC 的 StringHttpMessageConverter 设置了 UTF-8。命令行里可以这样改ALTER DATABASE travel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE t_scenic CONVERT TO CHARACTER SET utf8mb4;5.2 Tomcat 启动报 java.lang.NoClassDefFoundError / ClassNotFoundException现象Tomcat 9 甚至 10 上刚启动就抛 java.lang.NoClassDefFoundError一堆 javax.servlet 相关异常刷屏。原因老的 SSM 项目依赖 javax.servlet而 Tomcat 10 已经从 javax 迁移到 jakarta.servlet 命名空间Tomcat 9 与老版本大体兼容但某些依赖如 jsp-api、el-api 版本不匹配也会犯病。如果你用的是 JDK 11还可能牵扯模块访问限制。解决把 Tomcat 换成 8.5JDK 保持 1.8。如果必须用新版本就在 pom.xml 里把 javax 前缀全局替换成 jakarta但那个改动量不比重新搭个 Spring Boot 少普通演示没必要。判断方法很简单解压后随便看一个 java 文件的 import写的是 javax.servlet 就用 8.5写的是 jakarta.servlet 才用 10。5.3 MyBatis 突然报 Invalid bound statement (not found)现象启动不报错但一调用景点查询接口就抛 BindingExceptionInvalid bound statement (not found): com.travel.dao.ScenicDao.findByIds。原因Mapper 接口的方法和 mapper.xml 里的 id 对不上或者 mapper.xml 没有打进 classes 目录。用 IDEA 打包时resources 目录下的 xml 会被抽到 classes但如果 xml 放在 java 源码目录下Maven 默认不会复制 xml。解决检查接口方法名、namespace、id 是否一致然后去编译输出目录 target/classes 下看 mapper 目录是否存在。如果不存在在 pom.xml 的 build 里把 mapper 目录加进 resourcesresources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources写这段是因为我确实见过不少毕设源码把 mapper.xml 和 DAO 放在同一个包下用 IDEA 原版启动没问题一打 war 包 XML 没进 classes 就死了。这个坑不注意线上部署必炸。5.4 推荐结果永远是一模一样的“热门榜”现象换了账号登录推荐卡片没区别永远是销量最高或管理员置顶的那七八个景点。原因协同过滤冷启动。行为表里数据太少或者新用户根本没行为记录推荐算法退化成“找热门”。还有一种情况是算法类写好了但 Controller 没调用它只是查了 scenic 表然后 order by 某个字段。解决给行为表插一批种子数据模拟 6 个用户、每人十多个行为记录至少让 demo 能看到用户 A 和用户 B 推荐结果差异。种数据随时可以删别污染真实数据。插的时候要注意 user_id、scenic_id 外键都能对上。答辩时准备两个账号一个搜过西湖、收藏过灵隐寺另一个只看过草原再把行为表数据调大推荐结果明显不同。5.5 管理员登录后台提示密码错误现象前台游客能注册能登录后台管理员输入 admin/admin123 却一直提示密码错误或者查不到记录。原因初始化脚本里管理员密码不是明文而是 MD5 加密后的哈希值。你直接往数据库插了一行明文记录登录 Service 拿 MD5 后去比对当然对不上。解决先看登录 Service 里是 MD5 还是 SHA。如果是 MD5用下面这段生成哈希替换数据库里的密码字段public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } }如果代码里还加了盐那就要找到盐值用“密码加盐后再 md5”的方式生成。看源码时别跳过这条很多毕设项目的“默认密码”其实真不是你想的那个。6. 把毕设做厚一点给推荐结果加“可解释文案”和本地缓存想给这个项目加分我建议给推荐模块加两个小点解释为什么推以及别让每次刷新都重算相似度。第一点在推荐的景点卡片下加一行字比如“和你有相似兴趣的游客也看过这里”这行字的实现很简单在推荐 Service 里多查一个相似用户的数量即可更重要的是它让评委一眼看出你不是死背概念真的把推荐逻辑接进了前端。// 推荐卡片里增加推荐理由 if (recResult.containsKey(scenicId)) { String reason referralCount 0 ? 和你有相似兴趣的游客也去过 : 近期热门推荐; model.put(reason_ scenicId, reason); }第二个点我在本地跑这个项目时发现每次打开首页都会重新跑一遍全用户相似度计算数据一大就开始卡。我给推荐结果套了一层简单的本地缓存用 ConcurrentHashMap 存 userId - 推荐结果设置两小时过期没引 Redis 也能应付演示private MapInteger, CacheItemMapInteger, Double cache new ConcurrentHashMap(); public MapInteger, Double recommendWithCache(int userId, int topN) { CacheItem item cache.get(userId); if (item ! null !item.isExpired()) { return item.getValue(); } MapInteger, Double result cf.recommend(userId, topN); cache.put(userId, new CacheItem(result, System.currentTimeMillis() 2 * 3600 * 1000)); return result; }从那以后我每次拿到这类 ssm 毕业设计源码第一步必看推荐模块是“真算”还是“假热门”第二步就把缓存和兜底逻辑补上这两个小动作在答辩现场很能撑场面。希望帮到你。本文还有配套的精品资源点击获取