
先说一个很多做毕设的同学容易搞错的地方这道题虽然列了三个子项目——十里香快餐店及个性化菜品推荐系统、味聚轩智慧餐饮平台及基于用户画像的膳食推荐引擎、食光里数字化餐厅管理系统与智能口味匹配服务——但它们本质上不是三个项目而是一个项目的三个视角。管理端、数据端、推荐端各管一段拼起来才是完整闭环。Java 推荐系统 用户画像这三个关键词才是整道题的灵魂。如果最后做出来只是一个菜品的增删改查那跟十年前的交作业系统没有区别答辩时老师问两句推荐逻辑就露馅了。但如果能把用户画像和推荐引擎做出真正可解释的链路这个题目能讲的深度完全不输一个中型互联网项目的核心模块。下面我把整个项目从需求拆解、技术选型、数据模型设计、画像构建、推荐算法落地到冷启动、性能优化、一致性保障的完整思路展开写一遍附上可以直接拷走改用的表结构和核心代码逻辑。内容比较多做毕设的直接照着搭想提升项目经验的也可以当一手参考。1. 三个子项目其实是一道题先拆需求再写代码1.1 十里香、味聚轩、食光里分别该承担什么职责很多同学拿到这种长标题的第一反应是慌觉得要做三个系统。我拆下来的结论是这三个名字对应的是同一套系统的三个子系统彼此之间通过数据库和业务逻辑串联。十里香快餐店及个性化菜品推荐系统侧重的是菜单管理与推荐入口。它解决的是快餐场景下的核心矛盾菜品数量多、用户决策成本高。快餐店翻台快用户不会花十分钟研究菜单系统必须在几秒内把他大概率想吃的菜推到他眼前。味聚轩智慧餐饮平台及基于用户画像的膳食推荐引擎侧重的是数据后台与画像计算。这里的关键词是用户画像也就是说系统需要从用户的点餐行为、口味偏好、消费习惯里提炼出结构化的标签比如偏好重辣晚餐常点米饭类喜欢鸡肉类食材。食光里数字化餐厅管理系统与智能口味匹配服务侧重的是数字化管理能力与匹配服务。它解决的是餐厅经营侧的效率问题桌台管理、订单流转、菜品上下架、库存提醒同时把口味匹配做成一个可复用的服务能力而不只是某个页面上的一个推荐列表。三者关系可以这么理解十里香是前台味聚轩是大脑食光里是躯干骨骼。前台收集行为数据大脑计算画像和推荐结果躯干支撑整个餐厅的数字化运转。1.2 功能清单梳理哪些是标配哪些是拉分项我按模块把功能盘点了一下红色的部分是拉开差距的关键如果你时间有限优先保证核心链路跑通模块功能点等级菜品管理菜品CRUD、分类、上下架、图片上传标配订单管理下单、支付回调、订单状态流转、退款标配用户中心注册登录、个人信息、偏好设置标配桌台管理桌台状态、扫码点餐绑定标配用户画像引擎行为采集、标签计算、画像更新拉分项推荐系统候选集生成、排序、过滤、解释拉分项智能化管理报表菜品销量统计、复购率分析加分项核心链路是用户点餐 → 行为落库 → 画像更新 → 推荐刷新。这条链路跑通项目就立住了。1.3 第一版功能范围怎么定避免陷入过度设计做毕设最怕的就是一上来想得太全做了两周还在设计表结构。我给的建议是分三个迭代版本V1 只做菜品管理和订单闭环保证系统能用V2 加入用户画像和推荐引擎这是核心亮点V3 再加报表、桌台、口味问卷等外围功能。V1 用两周V2 用三周V3 看剩余时间灵活分配。推荐引擎的优先级远高于花哨的报表页面因为它是这个题目区别于普通管理系统的地方。2. Spring Boot MyBatis 打底项目骨架与数据模型设计2.1 技术选型的理由以及为什么不用更重的框架这个项目我建议直接用 Spring Boot 2.7 MyBatis MySQL Redis。理由很简单Spring Boot 让配置变得很轻MyBatis 对复杂 SQL 的掌控力强推荐系统里大量涉及查历史订单、统计口味偏好、批量更新画像这类 SQLMyBatis 写起来比 JPA 直觉得多。Redis 用来缓存推荐结果和热门榜单响应速度能拉开一个量级。前端的话Vue 3 Element Plus 就足够了如果时间紧直接用 Thymeleaf 模板也完全说得过去不要在这上面内耗。核心是后端逻辑和推荐算法。2.2 核心表结构设计订单、菜品、标签、画像、相似度矩阵建表是第一个分水岭。很多同学把表设计得跟 Excel 一样菜品表、订单表、用户表就完了这会导致后面做画像和推荐时无处取数。最少需要下面这五类表用户表user主键、昵称、手机号、注册时间、口味偏好初始标签用于冷启动菜品表dish主键、菜品名称、分类ID、价格、辣度等级、食材、烹饪方式、热量可选、图片URL、状态订单表orders / order_detail订单主表存用户ID、订单时间、总价、桌台号、订单状态订单明细表存菜品ID、数量、单价一个订单对应多行明细用户画像表user_profile用户ID、口味标签JSON类型存标签及其权重、消费能力等级、活跃时段、最近点餐时间单用户一行JSON字段方便扩展标签表tag / dish_tag标签ID、标签名、标签类别口味/食材/烹饪方式/场景再挂一张菜品-标签关联表 dish_tag_rel菜品相似度表dish_similarity菜品A、菜品B、相似度分值这个表是推荐引擎离线计算的产物是核心中的核心建表的 SQL 我贴一下核心部分直接抄也问题不大CREATE TABLE user_profile ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, tag_weights json DEFAULT NULL COMMENT 标签权重, 如 {辣: 2.5, 鸡肉: 1.8}, avg_order_price decimal(10,2) DEFAULT NULL COMMENT 客单价均值, active_time_slot varchar(20) DEFAULT NULL COMMENT 活跃时段, 如 LUNCH/DINNER, last_order_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户画像表; CREATE TABLE dish_similarity ( id bigint(20) NOT NULL AUTO_INCREMENT, dish_a bigint(20) NOT NULL, dish_b bigint(20) NOT NULL, similarity decimal(10,4) NOT NULL COMMENT 相似度 0~1, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_pair (dish_a, dish_b) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品相似度矩阵表;画像表用 JSON 字段是我反复权衡后的选择。标签数量是动态的今天可能只用了 10 个标签下周想加一个低脂如果建关联表就要改表结构JSON 字段则只是加一个 key 的事。MySQL 5.7 的 JSON 类型已经支持索引和表达式查询做毕设和中小型项目完全够用。2.3 工程包结构按业务域划分别按技术层划分包结构我建议按业务域拆而不是按 controller/service/dao 这种技术层拆。按业务域拆的好处是推荐引擎会涉及 controller、service、mapper如果按技术层拆推荐相关代码会散落在三个包下面改起来相当痛苦。推荐的项目结构com.weiweixuan ├── common -- 通用工具类、常量、异常 ├── user -- 用户中心: controller/service/mapper ├── dish -- 菜品管理: controller/service/mapper ├── order -- 订单模块: controller/service/mapper ├── profile -- 用户画像: 采集、计算、更新 ├── recommend -- 推荐引擎: 召回、过滤、排序、解释 └── admin -- 后台管理: 报表、上下架、桌台管理注意controller 层只做参数接收和结果封装所有推荐逻辑都在 recommend 包里user_profile 的数据只允许 profile 包修改其他模块要读画像走接口。这个约束一开始就要定好不然后面写代码很容易各个模块直接改别人家的表最后画像数据变成一团乱麻推荐算法怎么调都不准。3. 用户画像到底怎么建从订单数据里挖出口味标签3.1 显式画像与隐式画像不要只问卷子更要看行为用户画像的构建有两条路。显式画像来自用户主动表达的信息比如注册时勾选我喜欢吃辣、给菜品打分、填写口味问卷隐式画像则来自用户的操作行为比如点了某道菜、反复浏览某个分类、同一道菜点了三次。显式数据准确但稀疏隐式数据丰富但有噪声。比如用户勾了我不吃辣结果夜宵时段点了三次麻辣香锅那显式画像和隐式画像就冲突了。我建议以隐式数据为主、显式数据作为修正因子。换句话说行动比语言更诚实。3.2 行为权重怎么定点餐、复购、收藏、评分的加权逻辑要给行为打权重首先要定义一次行为意味着什么。我设计过一套相对好用的权重规则行为类型权重值说明下单点餐1.0基础行为证明用户接受这道菜同一菜品复购2.0复购是强信号说明口味高度匹配点餐未支付0.3感兴趣但决策未落地主动收藏3.0主动行为意图强烈菜品评分高2.5明确反馈菜品评分低-2.0明确排斥浏览菜品详情0.2弱信号只用于冷启动补充画像更新的逻辑可以用一句话概述用户对某个标签的偏好强度 历史累计权重 * 时间衰减因子 新行为产生的权重。时间衰减很重要因为用户的饮食习惯会漂移。夏天爱吃凉菜冬天爱点热汤如果不加衰减那些过期偏好会一直霸占画像推荐结果就会失真。我用的衰减公式比较简单weight old_weight * pow(0.95, days_since_last_update)也就是 30 天后旧权重衰减到原来的 0.21约两个月前的偏好基本就淡出主要画像了。这个逻辑在项目里可以用定时任务每天跑一次也可以在用户产生新行为时顺带计算。3.3 标签体系的建立口味、食材、烹饪方式、场景做好画像的第一步是定义标签体系。我当时建了四个维度口味维度辣度微辣/中辣/特辣、酸、甜、咸鲜、清淡 食材维度鸡肉、猪肉、牛肉、鱼虾、蔬菜、豆腐 烹饪方式炒、炸、烤、炖、蒸、凉拌 场景维度单人工作餐、朋友聚餐、夜宵、减脂餐菜品的标签是半人工半自动打上去的。半人工是管理员在录入菜品时直接勾选基础标签比如宫保鸡丁打上鸡肉微辣炒半自动是指系统会根据订单间的共现关系自动补充关联标签。这一步不要想着全自动全自动的准确率撑不起推荐系统。3.4 画像计算的落地代码逻辑画像计算可以抽象成三个步骤拉取用户最近行为 → 聚合行为对应的标签权重 → 合并进用户画像表。核心伪代码Java 实现思路public void updateUserProfile(Long userId, OrderEvent event) { ListString tags dishTagMapper.selectTagsByDishId(event.getDishId()); for (String tag : tags) { int weight BehaviorWeightRule.getWeight(event.getBehaviorType(), event.getOrderCount()); profileTagMapper.incrementTagWeight(userId, tag, weight); } // 时间衰减 profileTagMapper.decayAllTags(userId, 0.95); // 更新时间戳 profileTagMapper.updateProfileTime(userId, new Date()); }这里面有两个容易踩的坑。第一个是订单明细里同一个菜出现了三次如果你直接给三次行为各加一份权重复购信号就会被放大成点餐信号的 3 倍所以复购的 2.0 权重应该是在数量 1 的基础上计算的而不是乘以数量。第二个坑是衰减和新增的先后顺序必须先衰减再加新权重否则新增的权重也会被衰减一遍结果偏低。画像数据是推荐系统的燃料燃料不准后面的算法再花哨也是白搭。我在做这个项目时花在画像校准上的时间比写推荐算法的时间还多这句话希望你能提前理解。4. 推荐引擎的核心菜品相似度计算与混合推荐策略4.1 为什么菜品推荐首选 ItemCF而不是 UserCF推荐算法里两大流派基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。UserCF 是找口味相似的人推荐他们点过的菜ItemCF 是找你点过的菜相似的菜。快餐和正餐场景我强烈建议以 ItemCF 为主。原因有三条第一餐厅菜品数量一般在几十到几百的量级远小于用户数量物品间相似度矩阵的计算和存储都容易很多第二菜品的口味属性相对客观两个菜相似不相似在数据和逻辑上都更容易解释第三UserCF 依赖的用户相似度矩阵会因为新用户涌入而变得极其稀疏算出来的相似用户往往不可靠。我之前做过一个对比同一批订单数据分别用 UserCF 和 ItemCF 跑ItemCF 推荐的菜品被用户接受的占比点了推荐菜高了将近 15 个百分点。这个差距在数据量小的时候特别明显。4.2 余弦相似度与共现矩阵一种简单可落地的实现菜品相似度的计算最常见的方法是借助用户的历史订单构建共现矩阵。核心思想是如果两个菜品经常出现在同一个用户的点餐集合里那它们大概率是存在替代或互补关系的菜。比如点了鱼香肉丝的人常常也会点宫保鸡丁因为口味都属于咸鲜微辣。Step 1. 找出所有用户的点餐集合比如用户A点过 [宫保鸡丁, 酸辣土豆丝, 米饭]用户B点过 [宫保鸡丁, 可乐]。 Step 2. 统计每个用户点餐集合内所有菜品两两共现的次数。 Step 3. 用余弦相似度公式计算similarity(A, B) |A ∩ B| / sqrt(|A| * |B|)用烹饪界的话说就是交集大小除以两者点餐量的几何平均值。这个公式的好处是能惩罚热门菜。比如米饭几乎是每个人都会点的如果直接用共现次数米饭跟所有菜都很相似这显然是错的。余弦公式的分母把这种情况压住了。这一段逻辑的 Java 实现可以直接用 HashMap 统计共现次数然后遍历矩阵计算相似度。菜品量如果不大比如 200 道菜矩阵就是 200*200 个格子完全不需要引入大数据框架。// 伪代码: 计算菜品的共现次数并生成相似度矩阵 MapPairLong, Long, Integer coCount new HashMap(); for (ListLong orderDishList : userOrderDishList) { for (int i 0; i orderDishList.size(); i) { for (int j i 1; j orderDishList.size(); j) { PairLong, Long key Pair.of(orderDishList.get(i), orderDishList.get(j)); coCount.put(key, coCount.getOrDefault(key, 0) 1); } } } // 遍历 coCount, 结合每个菜品被点的次数计算余弦相似度, 存 dish_similarity 表这个离线任务建议放在每天凌晨执行更新整个相似度矩阵。线上接口只读取预计算结果不做实时相似度计算保证响应速度。4.3 召回、过滤、排序三步走推荐不是从相似菜里随机挑很多初学者以为推荐就是找到相似菜然后列出来但实战中必须分三步走。召回从相似度矩阵里拿到用户点过的菜的 TopN 相似菜合并成一个候选池。比如用户点过 3 个菜每个菜取 Top20 相似菜合起来是 60 道菜去掉重复后大约有 40~50 道。过滤把用户已经点过的菜、已经下架的菜、不适合当前时段投放的菜比如早餐时段就不推正餐大菜从候选池里剔除。这块逻辑千万别省不剔除的话推荐列表里全是你吃过的东西观感极差。排序剩下的候选菜按画像标签匹配度 × 相似度分数 × 流行度校正综合排序。单纯按相似度排序容易陷入小众怪圈推一堆冷门菜给用户单纯按销量排序又失去了个性化。我设计的打分公式score 0.5 * sim_score 0.3 * profile_tag_score 0.2 * popularity_scoresim_score 来自菜品相似度表profile_tag_score 是菜品标签与用户画像标签的余弦相似度popularity_score 是菜品近 7 天销量归一化后的值。三个维度加权既保证口味匹配又避免推荐结果过于冷门让用户觉得这推荐不靠谱。4.4 个性化口味匹配服务的接口设计食光里里提到的智能口味匹配服务可以封装成一个独立的推荐接口供各个端调用public RecommendResult recommendForUser(Long userId, int size, RecommendContext ctx)RecommendContext 可以传当前用餐时段、桌台人数、是否节日这些会影响推荐结果。比如午间快餐场景优先推出餐快的菜晚间聚餐场景优先推肉类硬菜。这个接口做成独立服务后不管前端是点餐页面、详情页的猜你喜欢还是结算页的再来一单都是同一套逻辑。还有一点值得做推荐解释。简单说就是在推荐结果旁边附上一句话因为您喜欢川菜为您推荐麻辣香锅。这个解释的技术含量不高就是从画像里取权重最高的标签再判断菜品是否带这个标签但用户体验提升非常明显答辩时讲出来也很加分。5. 冷启动、性能与一致性上线前必须处理的三道坎5.1 冷启动新用户没有行为数据怎么推荐冷启动是推荐系统最常见的坑。一个刚注册的用户订单表里根本没有他的记录用户画像的标签权重全是零这时候如果直接跑协同过滤推荐结果就是空的页面出现暂无推荐四个字非常尴尬。我用的方案是三明治冷启动第一层用热门榜兜底。计算全站近 7 天销量 Top20 的菜品针对新用户直接推荐热门菜这个永远不会出错。第二层用注册问卷修正。用户注册时勾选口味偏好比如微辣不吃香菜偏爱鸡肉根据偏好标签反向匹配标签重合度高的菜品。第三层用浏览行为快速点火。只要用户在页面多停留了几秒浏览了某道菜系统立刻更新其画像下一轮推荐就开始带上个性化的结果。新菜品也一样有冷启动问题。一道新上架的菜没有任何订单数据相似度矩阵里跟谁都不相似。我的做法是内容标签映射新菜的标签是管理员录入时就打好的直接用标签和画像的匹配度来参与排序不需要等它攒够订单。等它被点了几十次之后再切换成正常协同过滤逻辑。5.2 性能优化Redis 缓存推荐结果定时任务预计算推荐引擎最怕的其实是实时计算。如果每个用户刷新页面时都现算一次画像和相似度数据库基本会扛不住。我当时的方案是每日凌晨定时任务把用户的推荐列表算好存 Rediskey 为recommend:user:{userId}过期时间 12 小时。用户刷新推荐页时直接读缓存命中率极高。只有当用户产生了新的点餐行为时才异步触发该用户推荐结果的增量刷新保证刚点的菜不会下一秒还出现在推荐列表里。这个异步刷新可以用 Spring 的Async做也可以用消息队列毕设项目用Async就够了别为了用 MQ 而硬上 MQ。热门菜品榜、相似度矩阵这样的基础数据也都放 Redis。特别是相似度矩阵200 道菜就是 4 万个键值对放数据库每次实时查的话推荐接口的响应要慢好几倍。放 Redis 后用批量读取单次推荐接口耗时可以从 500ms 降到 30ms 以内。5.3 数据一致性画像更新和订单入库不能一个成功一个失败这里我要重点说一个经常被忽略的问题点餐事务和画像更新的关系。如果你在提交订单的 Service 里既写订单表又更新用户画像那一旦画像更新失败整个订单也回滚了用户会莫名其妙下单失败这是完全不能接受的。正确的做法是订单入库单独开一个事务保证订单数据强一致订单成功后发一个事件或消息画像模块异步消费并更新画像。这样即使画像更新失败也不影响核心交易链路画像最多晚一点更新下次推荐结果刷新时会补上。如果你不用消息队列用 Spring 的事务同步事件也能做到TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { Override public void afterCommit() { profileService.updateUserProfileAsync(userId, orderId); } });这样订单提交事务真正提交后才异步触发画像更新既不会拖慢下单单体验又不会因为画像模块的故障把订单搞挂。这个点你在答辩时讲出来能明显看出你是有工程经验的。5.4 排序、SQL 与 N1 问题被问烂但必须过关的细节热词里反复出现 Java 排序和 Java 面试题其实跟这个项目是强相关的。推荐引擎的排序算法、订单列表的分页排序、热门菜品排行的统计每一处都在用排序。推荐排序我会用 Java 的 Comparator 直接写成多条件比较器而不是在 SQL 里 ORDER BY 一个复杂表达式因为推荐排序涉及画像匹配度计算SQL 写不出来。附带说一句订单明细的查询最容易触发 N1 问题。主订单和明细分开查一次查 20 个订单然后循环 20 次去查明细数据库瞬间就哑了。正确做法是先把订单 ID 列表查出来再一次性WHERE order_id IN (...)把明细全部查出来内存里做分组。这种问题面试官爱问实际上线也必然遇到。6. 从项目到答辩与面试这套系统怎么讲出亮点6.1 让推荐结果可解释是项目亮点的放大器如果把纯协同过滤的推荐结果比作黑箱, 那加上推荐解释之后, 整条链路就变得可追溯、可检验了。我在推荐接口返回的 DTO 里加了reason字段比如跟您常点的宫保鸡丁口味相似匹配您的微辣偏好本周热门菜品。用户能看懂评委也能看懂比一个冷冰冰的推荐列表有说服力得多。实现上reason的生成逻辑不复杂如果候选菜与用户历史点餐菜的相似度超过了阈值就生成口味相似解释如果画像标签匹配度高就生成符合您偏好的XX口味解释两者都不满足就落到热门推荐解释。三条解释路径基本能覆盖全部推荐结果代码量与收益的性价比很高。6.2 简历与答辩时最值得展开的三个问题做这个项目面试官大概率会问这三个问题提前把回答准备充分第一个问题「用户画像的标签权重怎么更新的」答分两类行为加权用时间衰减公式处理偏好漂移复购行为单独加权。回答时一定要把衰减公式说出来面试官一听就知道你不仅写了代码还思考过算法效果。第二个问题「推荐系统冷启动怎么处理」答热门兜底 注册问卷 浏览行为微点火的组合方案。这道题的重点不是要一个完美方案而是展示你能具体问题具体分析而不是只会背 ItemCF 公式。第三个问题「订单事务和画像更新冲突时怎么取舍」答订单强一致画像最终一致通过事务同步事件做异步更新。这类一致性与可用性权衡的回答很容易把普通项目区分成有工程深度的项目。6.3 还可以继续扩展的方向膳食推荐的能量考虑题目里味聚轩的子标题有膳食推荐引擎六个字如果想让项目更进一步可以在菜品表里加上热量、蛋白质、脂肪字段画像里加上减脂餐偏好健身人群标签推荐时做一层营养过滤。比如用户近一周连续点了高热量菜品系统自动引导推荐低卡选项。这个方向不需要复杂的算法在现有画像体系上扩展即可但主题立意会明显高出普通点餐系统一截。整体做下来我的感觉是这道题的关键不在于写了多少行代码而在于是否把数据的流转跑通。点餐数据 → 行为打分 → 标签聚合 → 画像更新 → 相似度计算 → 推荐排序 → 结果解释这条链路每多跑通一环项目的含金量就上一个台阶。如果你正在做这道题我建议先别急着写页面拿一天时间把表结构和这条链路想清楚后面写代码会顺畅非常多。最后说一个实操教训推荐系统里的所有效果都要用数据说话。我当时给推荐系统加了一个推荐位点击率的统计埋点跑了三天后发现推荐的点击率是普通菜谱展示的 2.3 倍这个数字在答辩时一摆出来比任何技术名词都有说服力。如果你还有时间优先把埋点加上有了真实数据支撑整个项目的可信度完全不一样。