基于Spark的餐饮平台菜品智能分析推荐系统设计
2026/9/11 16:07:46 网站建设 项目流程

简介:一份基于Spark的餐饮平台菜品智能分析推荐系统Java毕业设计源码包,面向计算机相关专业学生,适用于毕业设计、课程设计或期末大作业。系统功能完善、界面美观、操作简单,涵盖前台点餐、后台菜单管理以及基于Spark的菜品偏好分析和协同过滤推荐,代码注释清晰,新手也能看懂,下载后简单部署即可使用。压缩包共49个文件,大小约2.06MB,主要包含17个Java源码、8个XML配置、6个CSS样式、5个JavaScript脚本、3个JSP页面,以及SQL数据库脚本、CSV和JSON样例数据、README说明等文件,完整覆盖后端逻辑、前端展示和数据库初始化模块。目前已有354人学习参考,适合需要快速搭建毕设项目或理解Spark推荐系统落地流程的读者。资源内提供用户菜品评分样例数据(user_meal_rating.csv、user_meal_rating.json)和spark.sql脚本,可直观验证推荐算法效果,便于二次开发与扩展。

1. 这个题目在解决什么问题:菜品智能分析推荐系统的数据闭环

餐饮平台最不缺的是数据,最缺的往往是把数据变成决策的能力。订单流水、用户浏览、菜品销量、门店库存每天都产生大量记录,但多数系统只是把这些数据存进数据库,做几张报表就结束了。"基于Spark的餐饮平台菜品智能分析推荐系统"这个题目的本质,是把两类问题合并处理:一类是对已有订单和菜品数据做统计分析,回答"什么菜卖得好、什么时段是高峰、哪类用户偏好什么口味";另一类是基于这些分析结果,对用户的下一步下单行为做预测,回答"这个用户接下来可能想吃什么"。两者共享同一份数据源,但计算路径不同,前者偏聚合统计,后者偏模型推理。Spark在这个项目里的角色,就是承担这两条路径的分布式计算底座——数据量达到百万级订单时,单机SQL的聚合效率和模型训练速度都会明显吃力,而Spark的DataFrame API和MLlib库正好覆盖这两个场景。这套设计适合作为毕业设计,也适合当作一个入门Spark实战的完整范例,因为它的数据模型不复杂、业务语义清晰、又能把Spark SQL、DataFrame、MLlib、JDBC数据源这些核心知识点全部串起来,比单纯跑一个word count示例有价值得多。下文按一条可实现、可答辩的路线展开:先建数据模型,再做特征工程,然后实现推荐算法,最后处理冷启动和效果评估。

2. 先建模型再写算法:餐饮推荐系统的业务建模与数据库设计

2.1 推荐系统不是"猜你喜欢",是行为序列的建模

很多人在设计餐饮推荐时,习惯模仿电商的"评分预测"思路,给用户-菜品关系建一个rating字段。这在餐饮场景里其实是个误区——用户吃完饭不会像看完电影那样打分,外卖平台也很少让用户对每道菜点星。真实可用的数据是隐式反馈:点了这道菜、浏览了这道菜、把这道菜加入过购物车、退掉过这道菜。这些行为发生或不发生,比一个虚构的1到5分更可信。Spark的MLlib ALS算法原生支持隐式反馈训练,只需要把行为频次映射为置信度权重,不用刻意伪造评分。这也是这个项目比一般推荐系统实现更适合用Spark的原因之一:ALS在分布式环境下计算效率高,且对隐式数据的处理有成熟的参数体系。

2.2 数据库表结构:最少需要几张表才能支撑分析与推荐

推荐系统需要的数据,按角色可以划分为三类:用户实体、菜品实体、交互行为。下面这个表结构是多年做类似项目时沉淀下来的最小可用方案,既能满足统计分析,也能喂给推荐算法。

2.2.1 用户表、菜品表、交互表的最小字段设计
-- 用户表 CREATE TABLE t_user ( user_id BIGINT PRIMARY KEY, user_name VARCHAR(50), gender TINYINT, -- 0未知 1男 2女 age_group VARCHAR(20), -- 年龄段,如 18-24 city VARCHAR(50), -- 城市,用于冷启动规则 register_ts TIMESTAMP ); -- 菜品表 CREATE TABLE t_dish ( dish_id BIGINT PRIMARY KEY, dish_name VARCHAR(100), category VARCHAR(50), -- 分类:川菜/粤菜/甜品... price DECIMAL(8,2), spicy_level TINYINT, -- 辣度 0-5 monthly_sales INT, -- 月销量,用于统计榜单 shelf_status TINYINT -- 1在售 0下架 ); -- 交互表(核心数据) CREATE TABLE t_user_dish_action ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, action_type TINYINT NOT NULL, -- 1浏览 2加购 3下单 4退单 action_cnt INT DEFAULT 1, -- 行为次数 action_ts TIMESTAMP NOT NULL, KEY idx_user (user_id), KEY idx_dish (dish_id), KEY idx_ts (action_ts) );

这里最关键的是action_typeaction_cnt两个字段:action_type区分行为类别,action_cnt记录频次。ALS训练时的"评分"就是从这两个字段推导出来的——例如浏览计1分、加购计2分、下单计3分,再乘以对应次数。注意idx_ts索引,因为做时间衰减和按时间段统计时,这个字段会被高频过滤。

2.2.2 为什么推荐输入要建宽表而不是直接查三张表

训练ALS模型时,如果直接在代码里反复join三张表,每次迭代都要重复扫描和关联,浪费资源且代码难以维护。常见做法是先用Spark SQL做一次ETL,把用户、菜品、行为合成一张训练宽表,输出到临时表或者写成Parquet文件落盘,后续的训练和统计都基于这张表。这个步骤既是一次数据预处理,也能在答辩时展示你对Spark SQL join、groupBy、窗口函数的掌握程度。

-- Spark SQL 中构建训练宽表 CREATE OR REPLACE TEMP VIEW train_data AS SELECT a.user_id, a.dish_id, SUM(CASE a.action_type WHEN 1 THEN a.action_cnt * 1.0 -- 浏览权重 WHEN 2 THEN a.action_cnt * 2.0 -- 加购权重 WHEN 3 THEN a.action_cnt * 3.0 -- 下单权重 WHEN 4 THEN a.action_cnt * 0.2 -- 退单,低权重 ELSE 0 END) AS rating, COUNT(*) AS action_times, MAX(a.action_ts) AS last_action_ts FROM t_user_dish_action a INNER JOIN t_dish d ON a.dish_id = d.dish_id AND d.shelf_status = 1 WHERE a.action_ts >= DATE_SUB(CURRENT_DATE, 180) -- 近180天数据 GROUP BY a.user_id, a.dish_id

这段SQL做了三件事:把行为类型映射成数值权重、过滤下架菜品、限制时间窗口。rating字段不是真实的用户评分,而是算法输入,这一点在论文和答辩中要讲清楚,面试时也经常被追问。

3. 从数据库到特征矩阵:用Spark SQL与DataFrame完成菜品特征工程

3.1 Spark读取MySQL数据的连接参数配置

特征工程的第一步是把MySQL里的数据搬到Spark计算引擎里。读取方式一般有两种:直接JDBC读取,或者先用sqoop/DataX同步到HDFS再加载。毕业设计场景下数据量不会大到必须走离线同步,直接JDBC读最省事,但如果数据量确实大,fetchsize参数一定要设,否则全表扫描时单次拉取记录太少,网络往返会拖慢几倍。

import org.apache.spark.sql.Dataset; import org.apache.spark.sql.Row; import org.apache.spark.sql.SparkSession; SparkSession spark = SparkSession.builder() .appName("DishRecommendationETL") .master("local[*]") // 本地开发用,集群环境换成yarn .config("spark.sql.shuffle.partitions", "200") .getOrCreate(); String jdbcUrl = "jdbc:mysql://localhost:3306/food_platform?useSSL=false&serverTimezone=Asia/Shanghai"; Properties connProps = new Properties(); connProps.setProperty("user", "root"); connProps.setProperty("password", "your_password"); connProps.setProperty("driver", "com.mysql.cj.jdbc.Driver"); connProps.setProperty("fetchsize", "1000"); Dataset<Row> userDf = spark.read().jdbc(jdbcUrl, "t_user", connProps); Dataset<Row> dishDf = spark.read().jdbc(jdbcUrl, "t_dish", connProps); Dataset<Row> actionDf = spark.read().jdbc(jdbcUrl, "t_user_dish_action", connProps);

fetchsize设置为1000表示每次从MySQL批量拉取1000行,减少JDBC网络交互次数。spark.sql.shuffle.partitions控制shuffle时的分区数,本地开发机器建议设小一点,默认200在数据量小时反而浪费调度开销。注意master("local[*]")只适合开发调试,真正要体现Spark优势,应该打成jar包用spark-submit --master yarn提交到集群,这也是热词里"spark集群搭建"考察的重点。

3.2 菜品维度特征:价格带、辣度、品类交叉统计

推荐算法不能只靠用户行为矩阵,菜品本身的属性特征对冷启动和解释性都有帮助。用DataFrame API做特征加工比写SQL更灵活,尤其是需要把多列组合成新特征时。

// 菜品特征:价格带 + 品类销量排名 Dataset<Row> dishFeatures = dishDf .withColumn("price_band", functions.when(functions.col("price").lt(20), "low") .when(functions.col("price").lt(50), "mid") .otherwise("high")) .withColumn("sales_rank", functions.row_number().over( Window.partitionBy("category") .orderBy(functions.col("monthly_sales").desc()))) .filter("shelf_status = 1"); dishFeatures.createOrReplaceTempView("dish_features");

price_band把连续价格离散化为低中高三档,这在做规则推荐时可以直接用,比如"这个用户历史订单均价30元,给他推mid档的菜"。sales_rank用窗口函数算每个品类内部的销量排名,后续在冷启动里"品类TopN"就直接来自这里,不必再写一遍groupBy。

3.3 用户维度特征:历史偏好向量与活跃度分桶

用户特征的目标是把用户变成可计算的向量。做法是聚合用户的历史行为,生成偏好标签,比如常点品类、平均客单价、下单时段偏好。

// 用户偏好聚合 Dataset<Row> userFeatures = userDf .join(actionDf, "user_id") .join(dishDf.select("dish_id", "category", "price"), "dish_id") .groupBy("user_id") .agg( functions.countDistinct("dish_id").alias("dish_cnt"), functions.avg("price").alias("avg_price"), functions.max("action_ts").alias("last_active_ts") ) .withColumn("active_level", functions.when(functions.col("dish_cnt").geq(50), "high") .when(functions.col("dish_cnt").geq(10), "mid") .otherwise("low"));

dish_cnt是用户交互过的菜品数量,用来判断活跃度;avg_price是用户价格敏感度的直接指标;last_active_ts可用于判断用户是否流失。这些特征喂给ALS之外,也可以作为后续冷启动规则里的匹配条件——比如给一个活跃度高、平均客单价50元以上的用户推荐mid到high价格带的菜品,这在模型冷启动时可以兜底。

特征名计算方式用途
dish_cnt用户关联菜品去重计数活跃度分桶、冷启动判断
avg_price用户下单菜品的平均价格价格带匹配规则
last_active_ts最后交互时间用户活跃/流失判断
category_pref频次最高的前3个品类冷启动候选集过滤
time_pref下单时段分布峰值分时推荐策略

这张表里的特征在答辩时可以直接作为"特征工程"章节的目录,比泛泛说"我们提取了用户特征"要有说服力得多。

4. Spark MLlib协同过滤核心:用ALS实现菜品推荐引擎

4.1 协同过滤选型:基于用户的还是基于物品的

餐饮推荐场景里,用户数量和菜品数量往往相差悬殊——用户可能几十万,菜品只有几千。基于物品的协同过滤(ItemCF)在这种情况下更合适:先算菜品之间的相似度矩阵,再根据用户历史点过的菜推荐相似菜品。但ItemCF需要自己维护相似度矩阵的更新逻辑,而ALS隐含地完成了物品向量的学习,所以工程上更常见的是用ALS做矩阵分解,训练出user vector和item vector,然后用向量点积计算评分。ALS训练出来的物品向量还可以直接用于菜品相似度计算,一份产出两种用途。

4.2 ALS模型的训练参数:rank、iterations、lambda、alpha

下面是完整的ALS训练代码,覆盖了数据准备、模型训练、TopN推荐生成三个环节。

import org.apache.spark.ml.recommendation.ALS; import org.apache.spark.ml.recommendation.ALSModel; import org.apache.spark.sql.Dataset; import org.apache.spark.sql.Row; import static org.apache.spark.sql.functions.*; // 训练集和测试集划分 Dataset<Row>[] splits = trainData.randomSplit(new double[]{0.8, 0.2}, 42L); Dataset<Row> train = splits[0]; Dataset<Row> test = splits[1]; // 配置ALS模型 ALS als = new ALS() .setUserCol("user_id") .setItemCol("dish_id") .setRatingCol("rating") .setImplicitPrefs(true) // 关键:使用隐式反馈 .setRank(20) // 潜在因子维度 .setMaxIter(15) // 最大迭代次数 .setRegParam(0.01) // 正则化参数 .setAlpha(1.0) // 隐式反馈置信度参数 .setColdStartStrategy("drop"); ALSModel model = als.fit(train); // 为用户生成TopN推荐 Dataset<Row> userRecs = model.recommendForAllUsers(10); userRecs.show(false); // 保存模型 model.write().overwrite().save("hdfs:///models/dish_als_model");
4.2.1 四个核心参数的调整逻辑

参数逐个说清楚,答辩和面试都会问。

  • rank:潜在因子个数,决定了用户向量和物品向量的维度。rank太小模型欠拟合,太大容易过拟合且计算量大。经验值从10到50之间调,数据量小用10到20,数据量大可以试30到50。
  • maxIter:ALS的迭代次数。15次左右通常能收敛,超过30次收益很小,只会增加训练耗时。
  • regParam:正则化系数,防止过拟合。0.01是个常见起点,如果测试集误差偏大可以加大到0.1试试。
  • alpha:仅对隐式反馈生效,控制行为频次转换成置信度的缩放比例。alpha越大,高频行为的权重越高,对"下单10次"和"下单1次"的区分度越明显。默认1.0,如果数据里行为频次差异极大,可以调到0.5或2.0看效果。
4.2.2implicitPrefs设为true时,rating的含义变了

setImplicitPrefs(true)打开后,ALS不再把rating当作用户的真实偏好程度,而是当作一个观测置信度。算法内部会把大于0的值视为用户与物品存在交互,并用alpha * log(1 + rating / alpha)这样的变换放大或缩小置信度。因此在第一节的ETL里,我们用行为权重累加构造了rating,这一步就顺理成章了——它不需要是精确的评分,只表示交互强度的相对大小。

4.3 生成推荐结果的格式处理与入库

recommendForAllUsers(10)返回的DataFrame里,每个用户对应一个结构体数组,里面是dish_id和评分。这个格式不能直接写到业务库,需要展开成行。

// 展开推荐结果:一行一个(user_id, dish_id, score) Dataset<Row> recsExploded = userRecs .select( col("user_id"), explode(col("recommendations")).alias("rec") ) .select( col("user_id"), col("rec.dish_id").alias("dish_id"), col("rec.rating").alias("score") ); // 写回MySQL,供业务侧查询 Properties writeProps = new Properties(); writeProps.setProperty("user", "root"); writeProps.setProperty("password", "your_password"); recsExploded.write() .mode(SaveMode.Overwrite) .jdbc(jdbcUrl, "t_recommend_result", writeProps);

explode把数组拆成多行,是处理MLlib推荐结果的标准手段。写入MySQL时mode(Overwrite)会先清空旧数据再写入,适合每天跑批更新推荐结果;如果希望保留历史,可以改成Append并加上日期分区字段。

5. 冷启动处理:新菜品新用户的Rule-based补全策略

5.1 为什么ALS解决不了冷启动

ALS矩阵分解依赖用户和菜品的历史交互数据。一个新上架的菜品没有用户行为记录,训练时它对应的列是空的,模型学不到它的向量表示,自然无法被推荐出去。同理,一个新注册用户没有交互历史,recommendForAllUsers对他也输出不了合理结果。这类用户在餐饮平台上占比不小,所以冷启动是推荐系统上线绕不开的问题。

5.2 菜品冷启动:内容特征相似度匹配

菜品冷启动有两个方向:一是用内容特征做相似推荐,二是用规则顶上去。内容特征方案是把菜品的品类、辣度、价格带组合成一个特征向量,然后与用户历史偏好做匹配。Spark DataFrame做这种匹配很简单,不一定要上复杂的向量相似度计算。

// 用户偏好与菜品特征关联,找到未下单过的TopN候选 Dataset<Row> coldStartCandidates = userFeatures .join(dishFeatures, userFeatures.col("user_id").equalTo(dishFeatures.col("dish_id")), "cross") .filter("dish_id NOT IN (SELECT dish_id FROM train_data WHERE user_id = " + userId + ")") .withColumn("match_score", when(col("category").isin("pref_cat1", "pref_cat2"), 3.0) .otherwise(0.0) .plus(when(abs(col("avg_price").minus(col("price"))).lt(15), 2.0) .otherwise(0.0))) .orderBy(col("match_score").desc()) .limit(20);

这段是过程示意,实际项目中pref_cat1pref_cat2需要先通过聚合得到。匹配分由两部分构成:品类符合加3分、价格带偏差在15元以内加2分,最后按分数排序取前20。这个逻辑虽然简单,但在冷启动阶段的表现往往不差,而且便于向业务方解释——"为什么推这道菜"可以归结为"您之前爱吃川菜,这道菜也是川菜,价格也接近"。

5.3 用户冷启动:基于热门榜和分时策略

新用户没有偏好,常规做法是用城市热度、品类热度做推荐。接合热词里的"spark数据分析案例",这部分可以作为统计分析的一个展示点。

// 城市+时段热销榜,存入Redis供接口实时读取 Dataset<Row> hotDishes = dishFeatures .filter("shelf_status = 1") .groupBy("city", "category") .agg(sum("monthly_sales").alias("city_cat_sales")) .withColumn("rank", row_number().over( Window.partitionBy("city").orderBy(col("city_cat_sales").desc()))) .filter("rank <= 10");

热销榜按城市和品类两个维度聚和,既能保证用户看到的不是千篇一律的全国榜单,又能跟时段做拼接——比如午餐时段把主食类排名提前,晚餐时段把烧烤类排名提前。新用户首次进入App时,推荐接口直接读这个榜单,返回速度在毫秒级,不需要跑Spark任务。

6. 用离线评测指标和参数网格搜索验证推荐质量

6.1 评测指标怎么选:RMSE与Precision@K

推荐模型的离线评测不能只看loss降没降。对隐式反馈场景,RMSE的意义有限,因为我们的"评分"是构造出来的,预测值和构造值之间的误差并不等于用户真实满意度。更贴合业务的是TopN命中类指标:在测试集里取出用户真实交互过的菜品,看模型推荐的TopK里有多少是用户真正点过的,对应Precision@K。下面给出一个可运行的评测代码。

// 在测试集上评测Precision@K Dataset<Row> testPositive = test .filter("rating > 0") .select("user_id", "dish_id") .distinct(); Dataset<Row> joined = recsExploded .join(testPositive, recsExploded.col("user_id").equalTo(testPositive.col("user_id")) .and(recsExploded.col("dish_id").equalTo(testPositive.col("dish_id"))), "left_anti"); // 这里要用inner才能统计命中数,示意用left_anti表示取未命中的反例

实际计算Precision@K时,用inner连接拿到命中的记录数,除以推荐总数K。left_anti在这里只是演示反例的取法——拿到未命中的记录,可以分析推荐结果为什么没被用户接受:是品类不对还是价格带偏离。把命中分析拆开看,比一个汇总数值更有诊断价值。

6.2 用ParamGridBuilder做参数搜索

ALS参数多,手动一组组试效率低。Spark MLlib提供了网格搜索工具,可以自动组合参数组合评估。参数搜索在数据量大时比较耗时,建议先用10%的采样数据粗筛,确定rank和regParam的大致范围,再在全集上用较细的网格精调。

import org.apache.spark.ml.tuning.ParamGridBuilder; import org.apache.spark.ml.tuning.CrossValidator; import org.apache.spark.ml.evaluation.RegressionEvaluator; ParamGridBuilder gridBuilder = new ParamGridBuilder() .addGrid(als.rank(), new int[]{10, 20, 30}) .addGrid(als.regParam(), new double[]{0.01, 0.1}) .addGrid(als.alpha(), new double[]{0.5, 1.0, 2.0}); RegressionEvaluator evaluator = new RegressionEvaluator() .setMetricName("rmse") .setLabelCol("rating") .setPredictionCol("prediction"); CrossValidator cv = new CrossValidator() .setEstimator(als) .setEvaluator(evaluator) .setEstimatorParamMaps(gridBuilder.build()) .setNumFolds(3);

网格组合数量是3×2×3=18组,3折交叉验证意味着要训练54次。如果每次训练耗时超过一分钟,就需要考虑增加executor数量或缩小网格范围。

6.3 推荐结果解释性:把模型输出变成业务能懂的语言

推荐系统的最终评判者不是RMSE,而是用户是否按推荐下单。模型产出的(user_id, dish_id, score)三元组对运营没有说服力,常见做法是把推荐结果跟菜品表再关联一次,输出"推荐理由"。这个理由可以来自规则,也可以来自模型的可解释性近似——比如"这道菜和您点过的【麻辣香锅】属于同一品类,且价格相近"。

代码上就是在推荐结果上再join一次菜品表,按菜品字段动态拼接文案。这个步骤不复杂,但在毕业设计展示中很出效果:同样的模型,别人展示的是数字矩阵,你展示的是带理由的推荐列表,业务价值一目了然。推荐系统的完整度,往往不在算法多深,而在数据闭环是否打通——从MySQL到Spark,从训练到评测,从结果入库到理由展示,一整条链路跑通,才算把题目里"智能分析推荐"这六个字落地了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询