做了多年 Java 后端也带过不少同学做毕业设计看到“基于 Hadoop 技术的酒店推荐系统”这种题目我第一反应是这题出得挺聪明。它不只是让写一个酒店预订管理系统而是把常规的增删改查、前端交互和大数据离线计算绑在了一起。你做出来之后既能在答辩时展示完整的 Web 工程能力又能讲清楚推荐算法和 Hadoop 的配合逻辑一个是“系统能用”一个是“系统有亮点”这就比纯管理系统高出不少。这套系统说白了要干两件事第一做一个完整的酒店预订一体化平台包括酒店管理、房间管理、用户注册登录、搜索筛选、在线下单、订单状态管理这些基础业务第二基于用户在前台的浏览、收藏、预订行为用 Hadoop 做离线分析算出“你可能还喜欢哪些酒店”然后在前端推荐位上展示出来。适合的人群也很明确计算机相关专业做毕设的同学、想从普通管理系统进阶到“带算法模块”的 Java 学习者还有那些想把 SpringBoot 和 Hadoop 生态串起来跑通一个完整项目的人。1. 项目定位这套题目到底在考什么1.1 核心需求拆解这个标题看起来很长拆开其实只有三层。第一层是“酒店预订管理系统”这是基础业务层解决的是酒店信息的发布与检索、用户下单与订单管理这些日常问题。第二层是“智能酒店预订一体化平台”这是产品层意味着前台用户端、后台管理端要打通数据流要闭环。第三层是“基于 Hadoop 技术的酒店推荐系统”这是算法与大数据层也是全项目最大的亮点。我见过不少同学的毕设选题是“酒店管理系统”“图书馆管理系统”这类纯业务项目最后做出来就是一个标准 CRUD答辩演示的时候老师问“那你用了什么核心技术”只能答 MyBatis 和 Bootstrap亮点不足。而这个题目的巧妙之处在于推荐引擎本身能讲算法Hadoop 能讲大数据处理SpringBoot 能讲工程化HTML5 能讲前端交互每一层都有话可说。具体到功能点系统大概要覆盖这些用户端注册、登录、酒店列表、条件搜索、酒店详情、房间选择、提交订单、订单列表、收藏酒店、个人中心、推荐位展示。管理端酒店信息管理、房间信息管理、订单审核与处理、用户管理、数据分析、推荐结果管理。大数据侧用户行为日志收集、日志导入 HDFS、MapReduce 离线计算用户相似度、生成 Top-N 推荐列表。展示侧首页推荐位、酒店详情页“猜你喜欢”、管理后台的订单统计和热门酒店排行。推荐引擎不是简单调一个算法库而是要在 Hadoop 环境里实现一个完整的离线推荐流程这部分我会在后面详细拆。1.2 为什么选 Hadoop 而不是凑一个算法库交差有些同学可能会想协同过滤推荐用 Python 调个库就能跑或者直接用 Java 内存计算为什么非要绕一圈上 Hadoop这个问题的答案就是项目选题的精髓。毕设项目区别于平时课程作业的关键在于它需要体现“系统设计能力”和“完整数据链路”。如果你只在 Java 里写一个相似度计算函数那数据从哪来、算完存哪、怎么跟业务系统联动这些问题都答不完整。而把 Hadoop 加进来后整个链路自然而然就完整了业务系统的行为数据落地到 MySQL再定期导出到 HDFSMapReduce 作业从 HDFS 读取海量日志做离线计算结果回写到 MySQL前端通过接口读取推荐列表。这条链路能讲清楚答辩时老师顺着你的架构图问下去基本都能应对。Hadoop 选型还有一个现实考量它对硬件要求低。伪分布式模式一台普通电脑就能跑课程设计或毕设的环境完全能撑住。相比 Spark 需要更熟悉内存计算和 ScalaHadoop 的 MapReduce 模型更直观用 Java 写 Mapper 和 Reducer 的门槛也更低和你的 SpringBoot 后台还能共用一套 JDK 环境。2. 系统整体架构与数据流设计2.1 分层架构设计整个系统我建议按五层来组织分工清楚答辩画架构图也好看。表现层HTML5 JavaScript CSS配合 Bootstrap 或原生 jQuery 实现页面渲染和交互。这里说的 HTML5 不只是标签还可以用到 localStorage 做本地缓存、Canvas 做统计图表、表单验证 API 做前端校验。接口层SpringBoot 的 Controller统一返回 JSON 格式数据负责接收前端请求、参数校验、调用业务服务、返回结果。业务层Service 接口 实现类处理用户管理、酒店检索、订单流程、收藏逻辑这些核心业务。数据访问层MyBatis 操作 MySQLMapper 接口与 XML 文件配合完成增删改查和统计查询。大数据层HDFS 存储用户行为日志文件MapReduce 完成离线推荐计算计算结果通过 JDBC 回写到 MySQL。这个分层的好处是每一层都能独立替换。比如你后续想把推荐引擎从 MapReduce 换成 Spark只需要改大数据层和回写逻辑业务层和表现层完全不动。2.2 数据流与推荐闭环推荐系统最怕的就是“推荐结果和用户行为没关系”。为了保证关系紧密我把数据流设计成了一个闭环用户在前台产生行为浏览酒店详情、搜索、收藏、预订→ 后台异步写入行为日志表 → 定期比如每天凌晨将行为日志导出为文件上传到 HDFS → MapReduce 作业读取日志构建“用户-酒店”评分矩阵 → 计算用户相似度 → 为目标用户生成 Top-N 推荐列表 → 回写到 MySQL 中的推荐结果表 → 用户再次访问首页时SpringBoot 从推荐结果表查询数据并展示“猜你喜欢”。这个闭环里最关键的一点是推荐结果一定来源于真实行为不是随机数据。这样答辩演示时你可以先用一个测试账号去浏览、收藏几家酒店等离线计算跑完后刷新首页推荐位就会发生变化演示效果非常直观。2.3 Hadoop 在系统里的角色并不是“替代数据库”要澄清一个常见误区Hadoop 在这里不是用来替代 MySQL 的它做的是离线分析和批量计算。在线业务请求量不大、数据量也就几万条订单的时候让 MySQL 直接服务业务是最高效的选择。Hadoop 处理的是用户行为日志这种量大但价值密度低的数据通过 MapReduce 批量洗出高价值的推荐结果。我用一个生活类比来说MySQL 像便利店的收银台专门快速处理每一笔交易Hadoop 像总部的财务分析室每天晚上把全部门店的销售小票汇总起来算出哪些商品搭着卖最好再告诉门店明天怎么摆货。两边分工不同但合起来才是一个完整的商业闭环。2.4 技术选型理由为什么不选 Spark / HBase / 前后端分离选型问题在毕设答辩时几乎必问提前准备好理由比临场发挥强得多。关于 Spark题目明确写了 Hadoop且 MapReduce 更容易解释计算过程。Spark 虽是更快的计算框架但对机器内存要求高伪分布式环境下体现不出优势。你可以在“进一步扩展”部分提一句“未来可改用 Spark 加速计算”但当前实现用 Hadoop 足够。关于 HBase推荐结果数据量不大用 MySQL 存查询更方便。HBase 适合海量随机读写但引入它会让系统复杂度增加对毕设来说没必要。存档日志放 HDFS业务数据放 MySQL这是最务实的分工。关于前后端分离很多同学纠结要不要用 Vue。本题强调 HTML5用 Bootstrap jQuery 或原生页面反而更稳妥因为这样减少了 Node 环境和跨域问题部署也更简单。如果你自己技术栈里有 Vue 经验也可以做前后端分离但需要额外处理跨域我会在后面问题排查部分专门提到。3. 核心功能模块与实现要点3.1 用户登录与权限控制用户模块是系统的地基我习惯用 Session 拦截器的方式实现。用户登录成功后把用户对象和角色标识放入 Session后台写一个拦截器对需要登录的请求做校验对管理端接口校验角色是否为管理员。代码结构上可以定义一个注解或者直接按路径前缀拦截/api/user/** 需要登录/api/admin/** 需要管理员角色需要注意权限校验必须在 Controller 之前完成所以用 SpringBoot 的 HandlerInterceptor 实现并在 WebMvcConfig 里注册。密码存储不要用明文用 MD5 加盐或者 BCrypt 散列。虽然毕设对安全要求没那么高但这一点写在文档里能加分。3.2 酒店信息管理与组合检索酒店管理包括酒店新增、编辑、上下架、图片上传以及房间类型的维护。每个酒店有名称、城市、地址、星级、评分、价格区间、简介、封面图等字段。检索功能是用户端的核心体验。建议做多条件组合查询城市必须选星级、价格区间、关键词可空所有条件动态拼接 SQL。这里有一个容易踩的坑用 MyBatis 动态 SQL 时条件拼接要用where标签加if避免出现where 11或者多出and的情况。分页用 PageHelper 或者手写 LIMIT 都可以。手写分页要记得返回总条数小程序前端列表滚动加载才能算出总页数。酒店详情的图片尽量用相对路径存储部署后通过配置的静态资源映射路径访问别把图片直接放在数据库里。3.3 预订流程与订单状态机预订流程是业务闭环的关键用户选酒店 → 查看房间 → 选择入住时间 → 提交订单 → 模拟支付 → 订单完成。订单状态建议设计一个状态机待支付、已支付、入住中、已完成、已取消。每次状态变更都记录订单操作日志方便管理员追溯。用户在提交订单前必须校验房间是否被占用最简单的方式是查询订单表中是否存在“入住时间与退房时间”与当前请求时间重叠的记录且状态为已支付或入住中。这里我踩过一次很深的坑只判断房间库存没判断时间重叠。结果两个用户订了同一个房间的不同时间段数据库中库存都没扣看起来没问题但后来检查发现同一个日期被重复预订了。改成时间重叠校验后问题才解决。如果你是初学我强烈建议把这种对账逻辑写在文档里答辩时可以讲“我处理了并发冲突的基本问题”。3.4 推荐引擎的整体设计推荐引擎是本系统的灵魂设计上分离线计算和在线展示两层。离线计算层用 Hadoop MapReduce处理用户的历史行为数据。在线展示层用 SpringBoot 提供服务用户访问首页或酒店详情页时先查 Redis 缓存推荐结果缓存没有则查 MySQL 的推荐结果表再渲染到页面。离线计算具体分三步第一步从 MySQL 行为日志表导出数据转换成文本文件上传到 HDFS 的指定目录。 第二步提交 MapReduce 作业按照“用户-项目评分矩阵 → 用户相似度矩阵 → 用户对项目的预测评分 → 生成 Top-N 推荐”的流程计算。 第三步作业完成后用 Java 客户端读取输出目录把推荐结果写回 MySQL 的推荐结果表。推荐结果表设计成(id, user_id, hotel_id, score, reason, create_time)reason 字段可以存“根据您的浏览记录推荐”“与您喜欢的酒店相似”这类文案前端展示时更有信任感。3.5 管理后台的数据分析与可视化管理后台除了基础业务管理最好再加一个“数据看板”这是体现完整性的加分项。看板内容包括每日订单数、累计注册用户、酒店热度排行、推荐点击转化率。画图的实现方式有很多种在 HTML5 技术栈下首推 Canvas 手绘柱状图或饼图也可以用 ECharts 的 CDN 引入。如果担心引入库太重手写 Canvas 柱状图其实很简单还能在答辩时讲“HTML5 Canvas 画图原理”。统计类 SQL 一定要用聚合函数GROUP BY、COUNT、SUM、AVG。比如热门酒店排行按订单明细表分组统计每个酒店的预订次数降序取前十。4. 推荐算法原理与 MapReduce 工程实现4.1 协同过滤推荐算法的“入门必修”推荐算法里最有解释性的就是协同过滤。核心思想很简单如果用户 A 和用户 B 看过的酒店高度相似那么 A 喜欢的酒店 B 大概率也喜欢。协同过滤分两类基于用户的UserCF和基于物品的ItemCF。酒店这个场景我建议两个都可以实现但优先实现基于用户协同过滤。为什么因为酒店的行为数据相比电商没有那么多物品属性用户-酒店矩阵的稀疏程度高UserCF 的结论更容易从业务角度解释。算法流程分四步构建用户-酒店评分矩阵。行是用户列是酒店值是该用户对酒店的评分权重。计算用户之间的相似度。常用余弦相似度两个用户对同一批酒店的评分向量夹角越小相似度越高。找到目标用户的 K 个最近邻用户。K 一般取 10 到 30。根据近邻用户对某家酒店的评分加权预测目标用户对这家酒店的评分取分数最高的前 N 家店生成推荐列表。落地到 MapReduce整个过程可以拆成三个 Job串行提交。工程上写成三个独立 Job 的好处是每个 Job 的输入输出都很清晰逻辑容易用故事讲清楚。4.2 评分矩阵的构建与行为权重转换用户没有直接打分怎么办这正是实战项目和教学案例的区别。我的做法是将用户行为转换成带权重的评分浏览酒店详情1 分搜索后点击进入0.5 分收藏酒店3 分提交订单4 分完成入住5 分行为记录表里存行为类型和操作时间清洗时按上述映射规则计算用户对每个酒店的总分。比如一个用户浏览了某酒店 2 次、收藏了 1 次那他对这家酒店的评分就是 2×1 1×3 5 分。这样做的合理性在于“重复行为代表更强的偏好”也更方便后面算余弦相似度。4.3 MapReduce 三个 Job 的实现思路第一个 Job构建“用户-酒店评分矩阵”。输入是清洗后的行为日志文件每行格式userId,hotelId,scoreMapper 端按空格切分字段输出 key 为userId_hotelIdvalue 为 score。Reducer 端按 key 聚合sum 出用户对酒店的最终评分输出到评分矩阵目录。第二个 Job计算用户协同过滤矩阵。这个 Job 的目的是得到任意两个用户对同一个酒店评分的组合。核心操作在 Mapper读取评分矩阵把同一酒店的评分数据发给同一个 Reducer 做笛卡尔积结算两个用户的相似度。第三个 Job为目标用户生成推荐列表。输入是相似用户关系数据Reducer 端对相似度做聚合排序结合酒店的评分预测公式输出 Top-N。这个三段式流程会很完整地模拟一个离线推荐 pipeline。为了控制篇幅下面给出第二个 Job 的 Reducer 核心伪代码public void reduce(Text key, IterableUserHotelScore values, Context context) { // key 是 hotelId // values 里是所有对这同一个酒店有行为的用户评分列表 ListUserHotelScore list new ArrayList(); for (UserHotelScore v : values) { list.add(new UserHotelScore(v.getUserId(), v.getScore())); } // 两两配对计算用户相似度 for (int i 0; i list.size(); i) { for (int j i 1; j list.size(); j) { UserHotelScore u1 list.get(i); UserHotelScore u2 list.get(j); // 累加分子、分母按余弦相似度公式计算 double similarity calcCosine(u1.getScore(), u2.getScore()); context.write(new Text(u1.getUserId() \t u2.getUserId()), new DoubleWritable(similarity)); } } }真实的余弦相似度计算要考虑多个酒店维度的向量点积和模长所以第一和第二个 Job 里需要维护用户对所有酒店的评分列表才能算完整向量。我这里用简化版说明思路实际代码要按向量完整版写。4.4 相似度计算里的数据稀疏问题酒店数量往往比用户多用户行为的稀疏度很高。两个用户可能只共同看过一家酒店Cosine 相似度算出来是 1.0但这不代表他们真的高度相似。这个问题在前人的论文里叫“冷启动”或者“稀疏性”问题。实际工程中的缓解策略有几种只计算“共同评分项大于等于 2”的用户对过滤掉只重叠一个酒店的噪声邻居。对行为少的新用户直接返回全站热门酒店列表不参与协同过滤计算。对没有历史记录的用户推荐位展示热门酒店并引导用户去浏览、收藏。在毕设答辩时能把“稀疏性”问题和自己的解决方案说出来是非常加分的。4.5 推荐结果的过滤与兜底MapReduce 算出来的原始 Top-N 列表不能直接展示还要做一层过滤把用户已经预订过的、已经收藏过的酒店过滤掉。因为推荐“已订酒店”会让用户觉得系统不智能。过滤逻辑建议放在回写 MySQL 之前做。Hadoop 计算出结果后可以用一个 Hive 语句或者再用一个 MapReduce 小作业去关联订单表如果觉得工程量大也可以直接在 Java 客户端读取结果后查询数据库用工代码过滤。毕设阶段用 Java 代码过滤即可因为数据量不大逻辑更直观答辩也好讲清。过滤完之后还要一个兜底策略如果推荐结果不足 N 条比如只算出 2 条就用全站热门酒店补到 10 条保证前端推荐位永远有内容。兜底策略一定要写在文档里面试官或答辩老师很喜欢问“你如何处理推荐结果不足的情况”。5. 实操过程从环境搭建到前后端联调5.1 Hadoop 伪分布式环境搭建我推荐在 Linux 虚拟机里搭 Hadoop比 Windows 原生部署省心太多。建议直接用某 CentOS 7 镜像虚拟机分配 2 核 4G 内存。环境清单JDK 1.8Hadoop 3.3.xMySQL 8.0SpringBoot 2.7.xMaven 3.6IDEAHadoop 伪分布式配置两个核心文件core-site.xmlconfiguration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/usr/local/hadoop/tmp/value /property /configurationhdfs-site.xmlconfiguration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/usr/local/hadoop/hdfs/name/value /property property namedfs.datanode.data.dir/name value/usr/local/hadoop/hdfs/data/value /property /configurationreplication 设为 1 是因为伪分布式只有一个 DataNode副本数设大反而报错。启动前先格式化hdfs namenode -format start-dfs.sh如果虚拟机能联网强烈建议把 Hadoop 的 JAR 包直接放到 IDEA 项目的 lib 目录下或者通过 Maven 引入hadoop-client相关依赖。这样 SpringBoot 项目里可以直接通过FileSystemAPI 读写 HDFS。5.2 数据库核心表设计一个酒店推荐系统至少需要这几张核心表。我用简化 DDL 说明关键字段CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, nickname varchar(50), avatar varchar(255), role tinyint DEFAULT 1 COMMENT 1-用户 2-管理员, create_time datetime, PRIMARY KEY (id) ); CREATE TABLE hotel ( id int NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, city varchar(50) NOT NULL, address varchar(255), star tinyint COMMENT 3-三星 4-四星 5-五星, price_min decimal(10,2), price_max decimal(10,2), score decimal(3,2) COMMENT 综合评分, image varchar(255), description text, status tinyint DEFAULT 1 COMMENT 1-上架 0-下架, PRIMARY KEY (id) ); CREATE TABLE room ( id int NOT NULL AUTO_INCREMENT, hotel_id int NOT NULL, room_type varchar(50), price decimal(10,2), total_count int, remain_count int, PRIMARY KEY (id) ); CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id int NOT NULL, hotel_id int NOT NULL, room_id int NOT NULL, check_in_date date, check_out_date date, total_price decimal(10,2), status tinyint COMMENT 0-待支付 1-已支付 2-入住中 3-已完成 4-已取消, create_time datetime, PRIMARY KEY (id) ); CREATE TABLE behavior_log ( id bigint NOT NULL AUTO_INCREMENT, user_id int NOT NULL, hotel_id int NOT NULL, behavior_type tinyint COMMENT 1-浏览 2-收藏 3-下单 4-入住, create_time datetime, PRIMARY KEY (id) ); CREATE TABLE recommend_result ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, hotel_id int NOT NULL, score decimal(10,4), reason varchar(255), create_time datetime, PRIMARY KEY (id) );行为日志表我说明一下不要把这个表当普通订单表用它只做用户行为采集设计上允许有大量重复数据方便定期导出给 Hadoop 做离线分析。生产上这类表要考虑分区但毕设规模合理即可。5.3 从日志导出到 Hadoop 分析离线计算流程的第一步是把行为日志表的数据导出成文件。最简单的实现写一个定时任务用 JDBC 查询最近一天的行为日志输出到本地临时文件再调用 Hadoop 的 FileSystem API 上传到 HDFS。我把这个逻辑封装成一个HadoopExportServiceService public class HadoopExportService { Value(${hdfs.uri}) private String hdfsUri; public void exportBehaviorToHdfs() throws IOException { ListBehaviorLog logs behaviorLogMapper.selectRecentLogs(); String localFile /tmp/behavior_ System.currentTimeMillis() .log; // 1. 写本地临时文件 try (PrintWriter pw new PrintWriter(new FileWriter(localFile))) { for (BehaviorLog log : logs) { pw.println(log.getUserId() , log.getHotelId() , convertScore(log.getBehaviorType())); } } // 2. 上传到 HDFS Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfsUri); FileSystem fs FileSystem.get(conf); Path src new Path(localFile); Path dst new Path(/user/hotel-recommend/behavior/ System.currentTimeMillis() .log); fs.copyFromLocalFile(false, true, src, dst); // 3. 删除本地临时文件 Files.delete(Paths.get(localFile)); } }这个流程的优点是把“业务数据”和“分析数据”彻底隔离MapReduce 只读 HDFS 里的文件不影响在线业务库。本地临时文件用完即删也避免磁盘堆积。5.4 推荐结果回写与接口对接MapReduce 作业跑完后输出目录下是类似userId,hotelId,score,reason的文本文件。这时需要另一个 Service 负责读取结果并批量写入recommend_result表public void loadRecommendIntoDb(String outputPath) throws IOException { Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfsUri); FileSystem fs FileSystem.get(conf); Path path new Path(outputPath); RemoteIteratorLocatedFileStatus iterator fs.listFiles(path, true); while (iterator.hasNext()) { LocatedFileStatus status iterator.next(); if (!status.getPath().getName().startsWith(part-)) { continue; } try (BufferedReader reader new BufferedReader( new InputStreamReader(fs.open(status.getPath())))) { String line; while ((line reader.readLine()) ! null) { String[] fields line.split(,); RecommendResult record new RecommendResult(); record.setUserId(Integer.parseInt(fields[0])); record.setHotelId(Integer.parseInt(fields[1])); record.setScore(Double.parseDouble(fields[2])); record.setReason(fields.length 3 ? fields[3] : ); recommendResultMapper.insertOrUpdate(record); } } } }建议在recommend_result表上建一个唯一索引(user_id, hotel_id)回写时用INSERT ... ON DUPLICATE KEY UPDATE覆盖旧结果避免重复推荐同一家酒店。在线接口的逻辑就简单了用户登录后从当前 Session 拿 userId查recommend_result表按 score 降序返回 Top-N。为了提高响应速度可以把结果放一份进 Rediskey 设计为hotel:recommend:{userId}过期时间半天。5.5 前后端联调与推荐位展示前端页面用 HTML5 Bootstrap核心页面包括首页、酒店列表页、酒店详情页、订单确认页、订单列表页、后台管理页。首页最显眼的位置放“猜你喜欢”推荐位轮播图或者卡片列表。实现逻辑简单页面加载时调用/api/recommend/hotels接口拿到推荐酒店列表用 JavaScript 遍历生成 DOM。没有推荐结果或者用户未登录时展示热门酒店。推荐位上的每张卡片要展示酒店名、城市、星级、价格、综合评分点击后跳转酒店详情页。这里有一个交互细节酒店卡片上的“收藏”按钮别用普通跳转用 Ajax 异步调用收藏接口成功后局部更新按钮状态。这样用户体验更好也体现你懂前端交互的基本优化。管理端页面建议做一个独立布局左侧是菜单栏右侧是内容区包括订单管理表格、酒店管理表单、数据看板图表。6. 常见问题与排错实录6.1 Hadoop 启动失败与格式化误区最常见的问题有两个NameNode启动失败或者格式化次数的坑。如果你在格式化 NameNode 之前已经启动过集群会报NameNode is not formatted或者文件系统 ID 不一致。这时候需要先停掉所有 Hadoop 进程把hadoop.tmp.dir下的文件删干净再重新格式化。格式化操作只能执行一次如果频繁格式化后续启动会出现InconsistentFSStateException。启动后检查进程状态jps正常情况下应该看到NameNode、DataNode、SecondaryNameNode三个进程。缺少 DataNode 就去日志目录看hadoop-xxx-datanode-xxx.log多半是端口没放开或者 replica 配置不对。6.2 本机能访问 HDFS 但 SpringBoot 访问不到开发时我把 SpringBoot 跑在 Windows 本机Hadoop 跑在虚拟机里结果 FileSystem 客户端总是连不上。排查下来主要是两个原因第一是 Windows 用户没有对应权限需要设置HADOOP_USER_NAME环境变量System.setProperty(HADOOP_USER_NAME, root);第二是/etc/hosts没配好Hadoop 的 NameNode 地址写的是localhostSpringBoot 端连不上。解决办法是统一用虚拟机 IP 配置 fs.defaultFS。跨环境的java.io.IOException: Permission denied错误基本都是用户权限问题设置用户环境变量后重启项目就能解决。6.3 推荐结果为空或迟迟不更新推荐结果为空先检查三件事行为日志表里有没有数据、MapReduce 的输入目录有没有文件、recommend_result表有没有被写入。按照“数据源 → 数据加工 → 数据落地”的顺序排查能在十分钟内定位。推荐结果不更新最常见的原因是定时任务没触发或者触发频率太短。我建议做一个管理端的手动“一键推荐”按钮点击后执行完整流程导出日志 → 提交 MapReduce → 回写数据库。这个手动触发在开发调试时能救命答辩演示也更方便。6.4 前端跨域问题如果你用了前后端分离开发比如前端在 8080 端口后端在 9090 端口跨域问题必然出现。最简单的处理方式有两种第一种是后端统一配置 CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true); } }第二种方案是让前端所有 Ajax 都打相对路径/api由 Nginx 或后端网关转发从根上避开跨域。毕设阶段我推荐第一种简单直接。6.5 订单时间重叠校验的坑很多同学做酒店预订时只检查剩余房间数没检查时间段容易出现同一房间同一天被两个订单占用的 bug。我的校验 SQL 是SELECT COUNT(*) FROM orders WHERE room_id #{roomId} AND status IN (1, 2) AND check_in_date #{checkOutDate} AND check_out_date #{checkInDate}这个[checkIn, checkOut)区间判断能覆盖大部分重叠场景。写完后我用几个测试用例验证过恰好相邻日期不冲突完全重叠冲突部分重叠冲突结果都正确。6.6 数据倾斜与计算耗时过长如果你的模拟数据造得比较多例如把近十万条行为日志一次性丢进去跑协同过滤可能会遇到部分热门酒店被大量用户消费导致某个 Reducer 处理的数据量远大于其他节点MapReduce 作业卡在 99% 不结束。这就是典型的数据倾斜。缓解办法给热门酒店的 key 加随机前缀打散到不同 Reducer计算完成后去掉前缀再合并或者直接限制参加计算的用户数量只取活跃度最高的前一万个用户。毕设演示数据量不大一般碰不到但答到这个问题可以展示你的理论基础。写在最后这套系统我前前后后调试过很多轮最大的体会是推荐项目最容易出彩的部分其实不是算法本身而是“让数据活起来”。很多同学的代码本身没问题但演示时推荐结果永远是固定几条一眼就能看出是写死的答辩直接露馅。真正做得好的项目是让数据从业务系统流向计算平台再通过推荐结果回流到用户界面形成完整的环路。如果你时间充裕我建议扩展三个方向第一把推荐结果按城市做过滤给用户推荐同一城市的候选酒店转化率更高逻辑也更合理第二增加推荐理由的多样性除了“和你浏览过的酒店相似”可以加“热门酒店”作为补充第三把 MapReduce Job 的提交过程封装成带进度条的可视化页面评委可以直接看到推荐计算正在跑这个交互在演示时非常加分。做毕设不要只想着“糊弄完”而是把它当成一次完整的小型工程项目来打磨。数据链路打通的那一瞬间你对 SpringBoot、Hadoop 和推荐算法的理解会比看十篇博客都深。