简介:Java毕业设计基于Spark的餐饮平台菜品智能分析推荐系统源码与数据库,面向计算机相关专业毕业生、课程设计与期末大作业学生;系统围绕餐饮菜品数据,基于Spark进行智能分析与推荐,涵盖用户菜品评分数据、推荐算法逻辑和后台管理界面。压缩包共49个文件,主要包含Java核心源码、XML配置、JSP页面、CSS与JS前端样式脚本、SQL数据库脚本以及README说明,整体仅2.06MB,结构简洁且带有代码注释,部署门槛低,适合作为毕设或高分课设参考;目前已有354人学习下载。资源内含用户菜品评分CSV与JSON样例数据,可快速导入数据库验证推荐流程,结合源码目录能理解数据处理、推荐算法、后台管理等模块的协作方式。对于希望快速搭建一个功能完整、可演示的餐饮推荐系统并熟悉Spark应用开发的读者而言,这套资料具有较直接的参考价值。
1. 一个数据文件决定推荐系统成败的毕设项目
拿到这份 zip,多数人习惯先打开 README,再钻进 src 读 Java 代码。把业务代码读完你会发现它并不复杂:真正决定毕设能不能跑通、答辩能不能讲出深度的,是根目录里那个不起眼的 user_meal_rating.csv。这个项目用 Spark 的 ALS 协同过滤做菜品推荐,Java 层负责业务接口与结果展示,Spark 做离线训练,MySQL 存推荐结果,串起来就是一条完整的“数据采集 — 特征处理 — 模型训练 — 推荐下发”链路。它适合两类人:一是推荐系统方向、需要可演示可答辩的毕设同学;二是主攻 Java 业务、想用最小样本理解 Spark MLlib 落地的工程师。下面直接从这个数据文件切入,把链路中的命令、参数与常见的坑逐个拆开。
2. 源码目录与数据链路:user_meal_rating 才是核心
拿到材料先别急着改 Java 代码。推荐系统的质量上限由输入数据决定,而不是由框架决定。把目录结构理清楚,搞清楚每个文件的职责,后面换数据集、调参数、排查问题都会省很多时间。
2.1 四个文件四种角色:先别急着改 Java 代码
主-文件夹-main ├── src/main │ ├── java # Java 业务逻辑、Controller 与 Spark 作业入口 │ └── resources # Spring/MyBatis 等配置文件 ├── README.md # 部署步骤与启动说明 ├── spark.sql # Spark SQL 初始化脚本,创建库表并加载 CSV ├── user_meal_rating.csv # ALS 训练的输入数据:用户-菜品评分 └── user_meal_rating.json # 原始评分备份,多为接口/日志采集落盘格式在这个结构里,src 并不是最需要研究的部分。毕业设计项目常见的通病是代码看着多,实际跑起来花时间的反而是环境;这个项目把训练数据直接外置成 CSV,很大程度上降低了演示时的数据准备成本。CSV 与 JSON 并存,是因为推荐链路里 JSON 往往是原始采集结果,CSV 则是由 JSON 清洗后得到的训练集。你拿到材料后第一件该做的事,是确认 CSV 与 JSON 的字段是否一一对应,再看 spark.sql 里的表结构与 CSV 表头是否匹配。
2.2 CSV 与 JSON:同一份数据的两种形态
推荐系统训练数据通常至少包含三个字段:用户 ID、物品 ID、评分值。菜品推荐场景还会加一个时间戳,用来做时间衰减或者按时间划分训练集与测试集。常见格式如下:
{"userId": 101, "mealId": 24, "rating": 4.0, "timestamp": 1710000000} {"userId": 102, "mealId": 31, "rating": 3.5, "timestamp": 1710003600}CSV 是它的扁平化版本:
userId,mealId,rating,timestamp 101,24,4.0,1710000000 102,31,3.5,1710003600Spark 读取 CSV 比解析 JSON 直接得多,尤其是带上inferSchema之后,Spark 能自动推断 userId 为整数、rating 为浮点数,省去手动定义 schema 的步骤。ALS 训练时其实用不到 timestamp,但它有两个隐藏价值:一是可以用时间窗口把数据拆成训练集和测试集;二是可以统计最近三十天内的用户活跃度,给 Java 端的“热门榜单”提供数据基础。如果字段命名不同,需要统一在读取阶段做 rename,推荐系统对字段名极其敏感,一个user_id和userId的差异就能让任务在运行时抛 Column 不存在的异常。
2.3 spark.sql:建库建表与 CSV 装载
spark.sql 这个文件承担的是初始化职责。常见做法是创建数据库和 CSV 外表,让 Spark SQL 后续可以直接对用户评分数据做聚合统计。典型内容如下:
CREATE DATABASE IF NOT EXISTS restaurant_reco; USE restaurant_reco; CREATE TABLE IF NOT EXISTS user_meal_rating ( user_id INT, meal_id INT, rating DOUBLE, rating_time BIGINT ) USING CSV OPTIONS ( path '/opt/data/user_meal_rating.csv', header 'true', inferSchema 'true' );执行方式是在项目根目录直接调用 Spark SQL 的-f参数:
spark-sql -f spark.sql-f表示执行整个 SQL 文件,等价于把文件内容逐条交给 spark-sql 解释器。这里的USING CSV指文件格式,header 'true'告诉 Spark 第一行是列名,inferSchema 'true'让 Spark 自动推断类型。如果部署在 Windows 本地开发环境,path要写成绝对路径,注意正斜杠和转义字符,否则很容易出现文件找不到的报错。
建表完成后,可以先跑一条聚合语句验证数据是否装载成功,这也是答辩时最好的“开场查询”:
SELECT meal_id, COUNT(*) AS rating_cnt, ROUND(AVG(rating), 2) AS avg_rating FROM user_meal_rating GROUP BY meal_id ORDER BY rating_cnt DESC, avg_rating DESC LIMIT 10;这条 SQL 统计菜品被评分次数与平均分,能直观展示用户偏好的分布情况。COUNT(*)与AVG(rating)的组合在推荐领域叫“热门榜基线”,虽然没有个性化能力,但它是后面评估 ALS 模型效果时最基础的对比对象。
3. 推荐引擎实现:ALS 协同过滤的模型训练与参数选择
数据装载完成,只解决了“有数据可用”的问题。真正让这个毕设区别于普通 CRUD 管理系统的,是 Spark MLlib 里 ALS 协同过滤算法的接入方式和参数调优。
3.1 为什么用 ALS 而不是 Item-CF 或 Slope One
ALS(交替最小二乘)把用户-菜品评分矩阵分解成两个低维矩阵:用户隐因子矩阵和菜品隐因子矩阵。矩阵分解的思路是假设用户对菜品的偏好能被少数几个隐藏因子解释,比如口味、价格带、菜系、辣度等。餐饮平台评分矩阵通常非常稀疏,一个用户可能只对几十道菜打过分数,而菜品总数是上千级别的。ALS 对这种稀疏矩阵的拟合能力,比传统的物品协同过滤更稳定。
Item-CF 计算的是物品间相似度,逻辑直观但依赖共现矩阵的稠密度;Slope One 简单但精度有限。ALS 在 Spark MLlib 里有原生 Java API 支持,训练过程是迭代式的最小二乘优化,代码量小,答辩时无论是讲原理还是展示结果都很顺。从团队项目经验看,ALS 也是离线推荐系统中应用面最广的基线模型:先跑 ALS,再考虑是否引入内容特征或深度学习排序模型,这个路径特别适合毕设和课程设计。
3.2 从 CSV 装载到模型训练的完整代码
在 Java 项目中,Spark 作业入口类通常长这样:
SparkSession spark = SparkSession.builder() .appName("MealRecommendationALS") .master("local[*]") // 本地模式,* 表示使用全部可用核心 .getOrCreate(); Dataset<Row> ratings = spark.read() .option("header", "true") .option("inferSchema", "true") .csv("user_meal_rating.csv") .select( col("userId").cast("int").as("userId"), col("mealId").cast("int").as("mealId"), col("rating").cast("double").as("rating") ); ALS als = new ALS() .setMaxIter(10) // 迭代次数 .setRank(12) // 隐因子维度 .setRegParam(0.1) // 正则化系数 .setUserCol("userId") .setItemCol("mealId") .setRatingCol("rating"); ALSModel model = als.fit(ratings); Dataset<Row> recommendations = model.recommendForAllUsers(5); // 每个用户推荐 5 道菜 recommendations.show();SparkSession.builder().master("local[*]")指定本地运行模式,local[*]自动使用机器所有 CPU 核心,毕设环境完全够用。inferSchema开启后,Spark 读取 CSV 时会自动把 userId 识别成整数,省去手工解析。cast("int")是双保险,防止 CSV 中存在空字符串导致类型推断为 String。ALS 训练时,setRank是效果影响最大的参数,它控制隐因子的数量,太小欠拟合,太大容易过拟合且训练时间明显增加。
recommendForAllUsers(5)返回的 DataFrame 包含两列:userId与recommendations,后者是一个数组结构,每个元素包含mealId和rating预测分数。这一步的输出就是 Java Web 层要展示的数据。注意运行前要在 pom.xml 中引入 spark-sql 与 spark-mllib 依赖,版本要和本机安装的 Spark 保持一致,否则会出现NoSuchMethodError这类运行时异常。
3.3 参数怎么调:rank / maxIter / regParam 对照
ALS 最核心的可调参数有三个,外加一个冷启动选项。调参顺序建议是:先固定 maxIter 和 regParam,用网格搜索试 rank;确定 rank 后再调 regParam;最后增大 maxIter 看效果是否继续提升。
| 参数 | 含义 | 推荐范围 | 调参场景 |
|---|---|---|---|
| rank | 隐因子维度,决定向量表达能力 | 8 ~ 20 | 数据量大、菜品品类多取偏大值 |
| maxIter | ALS 迭代次数 | 5 ~ 20 | 增大可提升拟合度,但超过 20 收益很小 |
| regParam | 正则化系数,防止过拟合 | 0.01 ~ 0.1 | 训练集 RMSE 低但测试集高时增大 |
| coldStartStrategy | 冷启动处理策略 | drop / nan | 遇到新用户或新菜品时避免程序报错 |
| implicitPrefs | 是否使用隐式反馈 | false / true | 只有点击、浏览数据时设为 true |
implicitPrefs是一个很容易被忽略的参数。如果数据里有明确的点赞或评分字段,保持默认false;如果只有浏览、收藏这类隐式行为,就要设置为true,并额外调整alpha参数控制置信度权重。毕业设计多数使用显式评分,这个参数保持默认即可。
调参之后一定要重新执行训练并观察推荐结果变化。比较简单的做法是打印几个用户的推荐列表,人工判断结果是否符合常识:比如一个频繁给川菜高分的用户,推荐结果里是否出现大量川菜馆的菜。如果推荐结果看起来和用户历史行为毫无关联,优先检查数据是否倾斜、菜品 ID 是否连续,而不是盲目加大 rank。
3.4 推荐结果落库:让 Java Web 层拿到分数
ALS 模型训练完后如果每次都现场计算推荐结果,响应延迟会非常高。常规做法是把结果批量写回 MySQL,Java Web 层直接通过 MyBatis 查询。首先创建结果表:
CREATE TABLE meal_recommendation ( user_id INT NOT NULL, meal_id INT NOT NULL, score DOUBLE NOT NULL, rank_no INT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, meal_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;写入逻辑在 Spark 作业中实现,将recommendations展开为行后通过 JDBC 批量插入。批量写库的常见问题是单条 insert 太慢,正确做法是使用foreachPartition,每个分区建立一个 JDBC 连接,使用addBatch批量提交。这样可以避免每个用户都创建数据库连接导致频繁的握手开销,整个训练加写库的时间能缩短三分之一以上。
4. 部署与实战:版本选型、提交命令与数据库初始化
代码能跑和项目能部署是两码事。Spark 生态的版本兼容性很容易让人在起步阶段卡住,这一章直接给出可复制的部署方案。
4.1 版本组合与环境说明
优先使用主流的稳定组合,不要为了“追新”选择刚发布的版本。表格里的组合是在毕设和中小型项目中验证过、踩坑最少的搭配:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | Spark 3.x 对 JDK 8 支持最好 |
| Scala | 2.12 | 与 Spark 3.2 匹配 |
| Spark | 3.2.x | 稳定且资料多 |
| Hadoop | 3.2.x | 配套 winutils 容易找到 |
| MySQL | 5.7 或 8.0 | 注意 JDBC 驱动版本与 8.0 的时区参数 |
版本组合的原则是:Spark 官方预编译包已经绑定了 Scala 版本,下载时务必选择spark-3.2.0-bin-hadoop3.2这类与本地 Hadoop 版本一致的包。Windows 本地运行 Spark 时还需要配置HADOOP_HOME并放置匹配的winutils.exe,否则运行时会报Failed to locate the winutils binary。这个问题在毕设演示现场经常出现,提前在环境变量里配好能省掉大量临时排错时间。
4.2 本地提交命令与参数说明
整个项目的启动顺序是先执行 spark.sql 初始化数据表,再运行 Spark 作业训练模型并写结果,最后启动 Java Web 服务。Spark 作业的提交命令如下:
spark-sql -f spark.sql spark-submit \ --master local[*] \ --driver-memory 2g \ --executor-memory 1g \ --class com.restaurant.reco.RecommendationApp \ restaurant-reco-1.0.jar--driver-memory 2g控制 Driver 进程堆内存,毕设机器 8G 内存以下时建议设置为 2g,同时将--executor-memory合并进 driver。--class指定包含 main 方法的全限定类名。Jar 包名和类名必须与 pom.xml 配置一致,否则会抛出ClassNotFoundException。如果训练数据量在几千行以内,用local[2]指定双核就足够,不必把资源全部吃满,给数据库和 Tomcat 留出余量。
4.3 从 local 迁移到 Spark 集群
如果课程设计需要展示集群运行效果,把--master从local[*]改成--master spark://node01:7077,并将任务提交到 Standalone 集群即可:
spark-submit \ --master spark://node01:7077 \ --deploy-mode client \ --class com.restaurant.reco.RecommendationApp \ restaurant-reco-1.0.jar--deploy-mode client表示 Driver 运行在提交任务的机器上,方便在控制台直接查看训练日志。若使用 cluster 模式,Driver 会运行在集群随机节点,控制台看不到日志,需要对 Spark Web UI 或者日志聚合页面排查错误。集群模式下还要特别注意 MySQL 连接地址,不能写localhost,必须写成集群中任意一台可访问 MySQL 的机器 IP。同时确认每个 Worker 节点都能访问到 CSV 文件路径,HDFS 路径是最稳妥的选择,本地文件路径在 Worker 节点上大概率不存在。
4.4 常见报错与排查思路
| 报错现象 | 可能原因 | 处理方式 |
|---|---|---|
| Failed to locate the winutils binary | Windows 未配置 Hadoop 环境 | 下载匹配版本的 winutils.exe 并设置 HADOOP_HOME |
| java.lang.OutOfMemoryError | driver 内存或 executor 内存不足 | 调大--driver-memory,或减小 rank 与 maxIter |
| Task not serializable | 匿名内部类捕获了非序列化对象 | 将闭包内使用的变量改为可直接序列化的类型,或用静态常量 |
| NoClassDefFoundError | Spark 依赖版本与运行环境不一致 | 检查 pom.xml 中 spark-core 与 spark-mllib 版本 |
| Public Key Retrieval is not allowed | MySQL 8.0 连接参数缺失 | JDBC URL 添加allowPublicKeyRetrieval=true&useSSL=false |
Task not serializable是 Spark 作业中最常见的报错之一,主要原因是foreach或map回调函数里引用了外部创建的 Controller 或 Service 对象,这些对象没有实现Serializable。解决办法是把变量定义为类常量,或使用foreachPartition在分区内创建局部变量,而不是在回调外部持有不可序列化的引用。
5. 验证与进阶:把推荐质量从“能跑”做到“可信”
部署完成后,推荐系统能出结果只是第一步。答辩和实际应用中最容易被追问的问题是:结果准不准?怎样验证?这一章给出两个能直接落地的验证与兜底方案。
5.1 用 RMSE 验证模型质量
ALS 训练完成后,需要把原始数据拆分成训练集和测试集,用测试集计算预测误差。Spark MLlib 中可以直接用RegressionEvaluator:
Dataset<Row>[] splits = ratings.randomSplit(new double[]{0.8, 0.2}, 42L); ALSModel model = new ALS() .setMaxIter(12) .setRank(12) .setRegParam(0.08) .fit(splits[0]); Dataset<Row> predictions = model.transform(splits[1]); RegressionEvaluator evaluator = new RegressionEvaluator() .setMetricName("rmse") .setLabelCol("rating") .setPredictionCol("prediction"); double rmse = evaluator.evaluate(predictions); System.out.println("RMSE = " + rmse);randomSplit(new double[]{0.8, 0.2}, 42L)表示按 8:2 比例随机切分,第二个参数 42 是随机种子,固定种子才能保证每次运行结果可复现。model.transform会为测试集中的每个样本生成prediction列。RMSE 计算的是预测评分与真实评分的平均误差,如果 RMSE 在 0.8 到 1.2 之间,说明模型在一个 1 到 5 分量纲的数据集上有基本可用精度;低于 0.5 要考虑是否发生了数据泄漏,远大于 1.5 则要检查数据分布是否存在异常。
5.2 冷启动与新菜品兜底
ALS 无法为完全没有历史行为的新用户生成推荐,新上架菜品也几乎没有机会出现在推荐列表里。实际项目中不会让用户面对空白推荐页,而是用热门榜单做兜底。把热门榜与 ALS 结果结合起来:优先查询meal_recommendation表,如果结果为空,就从热门榜接口取数据返回。
SELECT meal_id, rating_cnt, avg_rating, 0.6 * LOG(rating_cnt + 1) + 0.4 * avg_rating AS hot_score FROM ( SELECT meal_id, COUNT(*) AS rating_cnt, AVG(rating) AS avg_rating FROM user_meal_rating GROUP BY meal_id ) t ORDER BY hot_score DESC LIMIT 10;hot_score用对数函数平滑评分次数,避免某道菜只被少数人打满分就霸榜;0.6 与 0.4 的权重可以按业务调整,更看重口碑就提高avg_rating的占比。Java 控制器层可以写一个简单的分流判断:从meal_recommendation查不到记录时,自动切换到热门榜查询,整个切换过程对前端透明。这一招在答辩演示时尤其管用,因为演示环境很难避免新账号或空数据场景,有了兜底逻辑,系统永远不会返回空白推荐页。
本文还有配套的精品资源,点击获取