ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Spring Boot的智能推荐点餐系统:ItemCF协同过滤实践

基于Spring Boot的智能推荐点餐系统:ItemCF协同过滤实践 简介一套基于Spring Boot的智能推荐点餐系统完整项目源码面向餐饮行业开发者、Spring Boot学习者以及需要快速搭建推荐系统原型的从业者旨在解决传统点餐流程效率低、用户个性化需求难满足等问题。资源压缩包约107.95MB内含项目源码、开发说明文档docx、售后服务说明md、系统演示视频mp4以及附带的平台网站源码与PPTrar等可满足环境搭建、功能演示和二次开发等不同需求。目前已有78人学习下载适合进阶参考。内容覆盖Spring Boot自动配置、协同过滤与基于内容推荐算法以及前端展示层、业务逻辑层和服务数据层的分层设计配套文档提供从JDK、MySQL到项目导入的详细步骤演示视频直观展示注册、点餐、推荐等核心流程能够帮助开发者快速掌握智能点餐系统的完整实现路径同时为论文写作或课程设计提供可复用的完整实例。1. 智能推荐点餐系统的设计与实现本质是“springboot 加分项”在哪点餐系统本身不难菜品表、订单表、购物车再加几个管理端页面任何一个学过 Spring Boot 的人两周都能搭出来。但标题里多了“智能推荐”四个字整个项目的量级就不一样了——它把系统从一个 CRUD 后台抬升到了需要处理用户行为数据、计算相似度、设计兜底策略的推荐系统。做这个毕设或者练手项目真正值得投入时间的不是登录注册也不是菜品管理而是“推荐”这条线。推荐做得好不好直接决定论文里的“创新点”和答辩时老师追着问的问题。这篇博文按常见做法把“springboot 智能推荐点餐系统”拆成工程骨架、算法落地、数据存储、验证调优四块来讲。适合正在做毕设的学生也适合想让简历上多一个完整推荐案例的 Java 工程师。你会看到推荐算法不是孤立存在的它跟 Spring Boot 的模块边界、表结构设计、缓存策略都耦合在一起。2. 用 springboot 工程骨架拆出点餐系统的模块边界2.1 标准三层架构与推荐包的划分一个基于 Spring Boot 的点餐系统最常见的工程结构是 controller / service / mapper 三层。但不要把推荐逻辑直接塞进 OrderService 里否则后续调参和排查都会很痛苦。我一般会这样做src/main/java/com/example/order/ ├── controller/ # 接收 HTTP 请求 │ ├── UserController.java │ ├── DishController.java │ └── RecommendController.java ├── service/ # 业务逻辑层 │ ├── OrderService.java │ ├── UserService.java │ └── RecommendService.java ├── recommend/ # 推荐算法独立包 │ ├── ItemCF.java │ ├── SimilarityMatrix.java │ ├── RecommendContext.java │ └── ColdStartHandler.java ├── mapper/ # MyBatis 数据访问层 ├── entity/ # 数据库实体 └── config/ # 配置类把recommend独立出来有几个好处。第一算法类不依赖 Spring 的 controller 和 service可以单独写单元测试第二答辩的时候可以直接展示“推荐模块与业务模块解耦”的设计思想第三以后把 ItemCF 换成基于矩阵分解的算法时只动recommend包不影响订单、用户这些稳定模块。热点词里经常搜到的“springboot框架介绍”“springboot项目结构”其实讲的就是这种分层方式。在 controller 层推荐接口可以做得通用一些方便前端同学对接RestController RequestMapping(/api/recommend) public class RecommendController { private final RecommendService recommendService; public RecommendController(RecommendService recommendService) { this.recommendService recommendService; } GetMapping(/dishes) public ResultListDishVO recommend(RequestParam Long userId, RequestParam(defaultValue 10) int size) { ListDishVO dishes recommendService.recommendForUser(userId, size); return Result.success(dishes); } }这段代码的逻辑很直接接收用户 ID 和期望返回的菜品数量交给RecommendService处理。defaultValue 10是给前端一个默认值避免每次请求都要带参Result是统一响应体里面封装 code、message、data 三件套这是 springboot 接口开发的常规做法。2.2 springboot 常用注解在点餐系统里的实际落点很多刚接触 springboot 的人背了一堆注解但不知道在哪里用。放到点餐系统里这些注解的落点非常明确注解落地位置作用RestController所有 Controller 类返回 JSON省掉ResponseBodyServiceOrderService、RecommendService声明业务层组件Mapper所有 Mapper 接口让 MyBatis 扫描到ConfigurationProperties推荐参数配置类把算法参数外置到 application.ymlScheduled推荐结果预计算任务定时刷新相似度矩阵Transactional下单、支付方法保证订单与库存原子性Cacheable热门榜单接口缓存推荐结果降低数据库压力推荐参数外置是很多人忽略的细节。相似度阈值、推荐数量、冷启动权重这些数字不要硬编码在 Java 里而是放到application.ymlrecommend: itemcf: sim-threshold: 0.3 # 相似度低于该值的菜品不进入推荐候选 max-candidate: 50 # 候选集大小 top-n: 10 # 最终返回数量 cold-start: hot-dish-limit: 6 # 新用户返回的热门菜数量然后在配置类里读取Component ConfigurationProperties(prefix recommend.itemcf) public class ItemCFProperties { private double simThreshold 0.3; private int maxCandidate 50; private int topN 10; // getter / setter 略 }这样做的好处是答辩时老师问“你这个阈值怎么定的”你可以直接说“参数在配置文件里改完重启或走 nacos 刷新就能生效”而不是回答“在代码里写死的”——后者在 springboot 面试题里就是扣分项。2.3 springboot 自动装配原理对推荐模块的影响重点讲一下 springboot 自动装配和推荐模块的关系。SpringBootApplication里包含EnableAutoConfiguration它通过spring.factories机制加载各种AutoConfiguration类。但推荐算法是纯业务代码不是基础设施不需要自己写 AutoConfiguration。常见的一个错误是把算法初始化逻辑放在PostConstruct里硬算相似度矩阵导致应用启动时间长达十几秒。正确做法是异步初始化应用先起来推荐模块在后台线程里构建矩阵构建完成前走冷启动兜底。用EnableAsync加Async可以实现Component public class RecommendBootstrapper { private final ItemCF itemCF; public RecommendBootstrapper(ItemCF itemCF) { this.itemCF itemCF; } Async public void loadMatrix() { itemCF.buildSimilarityMatrix(); log.info(相似度矩阵构建完成耗时 {} ms, itemCF.getLastBuildCost()); } }这样点餐系统的主链路——下单、支付、浏览菜品——不受推荐模块启动影响符合真实的 springboot 微服务拆分逻辑。3. 智能推荐算法选型用 ItemCF 协同过滤做菜品推荐3.1 为什么毕设场景选协同过滤而不是深度学习点餐系统的“智能推荐”听起来像深度学习但真实的毕设和中小型项目里用得最稳的是协同过滤。原因有三个第一点餐系统没有富媒体特征没有图片向量、没有评论长文本深度学习拿不到足够的输入第二深度学习模型训练需要 GPU 和环境配置答辩现场很容易因环境问题翻车第三协同过滤的公式能写进论文评审老师看得懂好提问也好回答。协同过滤分基于用户UserCF和基于物品ItemCF两类。点餐场景选 ItemCF因为菜品数量远小于用户数量相似度矩阵规模可控且用户口味相对稳定“看了 A 的人也会看 B”这个逻辑比“和你相似的人在看什么”更好解释。推荐流程分三步从订单明细或用户行为日志构建“用户-菜品”评分矩阵计算菜品与菜品之间的相似度矩阵根据用户历史喜欢的菜品找到最相似的 K 个菜品生成推荐列表。初始化数据结构与计算逻辑Component public class ItemCF { private final DishBehaviorMapper behaviorMapper; private MapLong, MapLong, Double userItemMatrix; private MapLong, MapLong, Double itemSimMatrix; public ItemCF(DishBehaviorMapper behaviorMapper) { this.behaviorMapper behaviorMapper; } /** * 构建相似度矩阵建议在应用启动后异步执行。 * 这里按列求和来复现余弦公式的分母部分。 */ Async public void buildSimilarityMatrix() { ListUserDishScore behaviors behaviorMapper.selectAllScores(); MapLong, MapLong, Double userItem new HashMap(); for (UserDishScore b : behaviors) { userItem.computeIfAbsent(b.getUserId(), k - new HashMap()) .put(b.getDishId(), b.getScore()); } this.userItemMatrix userItem; this.itemSimMatrix computeSimilarity(userItem); } private MapLong, MapLong, Double computeSimilarity( MapLong, MapLong, Double userItemMatrix) { // 详情见下方分步骤解释 return new HashMap(); } }Async保证了推荐模块的初始化不阻塞主线程。userItemMatrix的结构是“用户 ID - (菜品 ID - 评分)”这是协同过滤的标准输入格式。评分数据哪里来用户在点餐系统里的下单、点击、收藏行为都可以转化为评分转化规则下文会给出。Ic3.2 相似度计算方法对比与参数选择ItemCF 的核心是计算两个菜品之间的相似度。常见公式有三个方法公式适用场景余弦相似度cosine (A·B)。B / (A皮尔逊相关系数对行向量做中心化后算余弦不同用户评分尺度不一致时Jaccard 相似度A ∩ B点餐系统的用户行为数据里有明确分值比如 1-5 分但不同用户的打分标准不同——有人只点一个菜就给 5 分有人点五个菜最高才给 3 分。所以最稳的是皮尔逊相关系数。不过如果行为表里只有“点过/没点过”这种布尔数据Jaccard 更合适。我在项目里默认用皮尔逊并保留一个配置项切换。pom 里引入一个轻量计算库或者自己写公式都行。自己写的话这个过程很简单private double pearson(MapLong, Double vecA, MapLong, Double vecB) { // 只取共同评分的菜品 ListLong common vecA.keySet().stream() .filter(vecB::containsKey).collect(Collectors.toList()); if (common.size() 2) { return 0.0; } double avgA common.stream().mapToDouble(vecA::get).average().orElse(0); double avgB common.stream().mapToDouble(vecB::get).average().orElse(0); double upper 0, lowerA 0, lowerB 0; for (Long dishId : common) { double diffA vecA.get(dishId) - avgA; double diffB vecB.get(dishId) - avgB; upper diffA * diffB; lowerA diffA * diffA; lowerB diffB * diffB; } if (lowerA 0 || lowerB 0) { return 0.0; } return upper / Math.sqrt(lowerA * lowerB); }common.size() 2这个判断很重要。如果两个菜品只有一个用户共同评分过计算出的相似度不可靠直接返回 0 处理避免噪声进入推荐候选。avgA和avgB中心化的步骤就是皮尔逊和余弦的本质区别——它把不同用户的打分尺度拉齐了。系数落在 [-1, 1]负数直接过滤即可因为点餐推荐里“不喜欢”的口味影响不如“喜欢”的强建议按配置阈值sim-threshold过滤。通过相似度矩阵生成推荐 Top-N。对于用户 u把他没点过的菜品 i 的分值累加public ListLong recommend(Long userId, int topN) { MapLong, Double userVec userItemMatrix.getOrDefault(userId, Collections.emptyMap()); MapLong, Double score new HashMap(); for (Long likedDish : userVec.keySet()) { MapLong, Double simMap itemSimMatrix.getOrDefault(likedDish, Collections.emptyMap()); for (Map.EntryLong, Double entry : simMap.entrySet()) { Long candidate entry.getKey(); // 过滤掉用户已点过的菜品 if (userVec.containsKey(candidate)) { continue; } score.merge(candidate, entry.getValue(), Double::sum); } } return score.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); }这里的排序复杂度是 O(n log n)候选集不到几百个菜品的情况下完全够用。Double::sum是带权重累加代表“这个菜品跟用户喜欢的多个菜都相似分就高”。注意过滤条件用的是userVec.containsKey(candidate)而不是 candidate 跟 userVec 的相似度否则会把用户点过的菜再次推荐出来。这就是 ItemCF 在真实落地时最容易出 bug 的坑也是答辩时导师爱问的细节。3.3 冷启动兜底新用户没有行为数据时的推荐策略新用户没有任何点餐记录用户向量是空的协同过滤直接失效。点餐系统里最有效的兜底方案是三段式默认按总销量和好评数推出“人气榜”保证用户有东西可点用户浏览了某个菜品详情页后立即返回“看了这道菜的人还点了”的关联菜品用户完成第一单后再切换到协同过滤通道。冷启动逻辑建议单独抽一个类处理Component public class ColdStartHandler { private final DishMapper dishMapper; public ListDishVO popularDishes(int limit) { return dishMapper.selectPopularDishes(limit); } public ListDishVO relatedByCategory(Long dishId, int limit) { return dishMapper.selectSameCategory(dishId, limit); } }对应 Mapper 里的 SQL 在下一章给出。这两条路径不依赖任何用户历史行为查询也很快所以接口响应能稳定控制在 100ms 以内。冷启动策略在推荐系统里不是“临时方案”而是长期存在的兜底通道——这个认知放在答辩里讲会让老师觉得你不是只做了一个算法而是理解了推荐系统的完整结构。4. 点餐系统的数据库设计与推荐评分的数据来源4.1 五张核心表的关系拆分智能推荐点餐系统最少需要五张表用户表、菜品表、订单表、订单明细表、用户行为表。订单表跟订单明细表拆开是为了支持一个订单含多个菜品用户行为表单独建表是为了给推荐算法供数据不污染业务表。表间关系用一句话概括用户表 1 对多订单表订单表 1 对多订单明细表明细表里的dish_id关联菜品表用户行为表的每行记录是“用户-菜品-行为-时间”的单独事件。不要为了省事把评分字段塞进订单明细表——订单是业务数据带着状态流转行为数据要记录的是“点过、收藏、评价”等事件生命周期不同。建表 SQL 和关键字段设计如下CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dish ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(128) NOT NULL, category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, sales_count INT DEFAULT 0, image_url VARCHAR(255) DEFAULT , PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, quantity INT DEFAULT 1, PRIMARY KEY (id), KEY idx_dish (dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user_dish_behavior ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, behavior_type TINYINT COMMENT 1:浏览 2:下单 3:收藏 4:评价, score TINYINT DEFAULT NULL COMMENT 评价分数 1-5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_dish (user_id, dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;联合索引idx_user_dish专门服务协同过滤的“取某个用户所有行为”的查询避免全表扫描。behavior_type用 TINYINT 而不是字符串是考虑到数据量上来后索引占用会更小score字段在浏览和下单行为时为空只有评价行为才有值。这个评分数据结构是智能推荐系统的重点——它决定了你能算出什么质量的相似度。4.2 行为日志到评分矩阵的转换规则行为表记录的是事件推荐算法需要的是“用户-菜品”的评分矩阵。转换规则可以自己定义但要给出论文上的合理性和可解释性。常用的做法是加权求和浏览计 1 分、下单计 3 分、收藏计 2 分、评价分数直接取原值。用一个 SQL 就能跑出训练矩阵SELECT user_id, dish_id, MAX(total_score) AS score FROM ( SELECT user_id, dish_id, SUM( CASE behavior_type WHEN 1 THEN 1 WHEN 2 THEN 3 WHEN 3 THEN 2 WHEN 4 THEN COALESCE(score, 3) END ) AS total_score FROM user_dish_behavior WHERE create_time DATE_SUB(NOW(), INTERVAL 90 DAY) GROUP BY user_id, dish_id ) t GROUP BY user_id, dish_id;内层子查询按用户和菜品聚合加权分外层取用户对每个菜品的最高综合分。COALESCE(score, 3)处理“评价了但没打分”的边界情况默认给 3 分。时间窗口 90 天是为了过滤掉过于久远的口味变化——人的口味会漂移半年前爱吃重辣现在不一定。这类后过滤参数也是 springboot 配置外置的数据跟recommend.itemcf.sim-threshold放一起管理。SQL 跑出来的结果直接灌进ItemCF.buildSimilarityMatrix()的入参。具体做法是在DishBehaviorMapper上加一个查询方法Mapper public interface DishBehaviorMapper { Select(SELECT user_id AS userId, dish_id AS dishId, MAX(score) AS score FROM ( SELECT user_id, dish_id, SUM(CASE behavior_type WHEN 1 THEN 1 WHEN 2 THEN 3 WHEN 3 THEN 2 WHEN 4 THEN COALESCE(score, 3) END) AS score FROM user_dish_behavior WHERE create_time DATE_SUB(NOW(), INTERVAL 90 DAY) GROUP BY user_id, dish_id ) t GROUP BY user_id, dish_id) ListUserDishScore selectAllScores(); }接口返回UserDishScore对象MyBatis 会自动把userId、dishId、score映射到实体字段。这里有个容易踩的坑COALESCE(score, 3)里的score如果跟外层的别名score混淆查询结果会变成全部 3 分。所以 SQL 里内层命名为total_score外层映射为score避免歧义。MyBatis 的映射规则是下划线转驼峰这个结果集的列别名可以直接用 Java 属性名也可以靠map-underscore-to-camel-case: true配置自动转换。4.3 订单数据与行为数据的衔接订单表和订单明细表是行为表的数据来源之一。每次用户下单除了写订单表和明细表还要往user_dish_behavior表插一条behavior_type 2的记录。这个逻辑要放在OrderServiceImpl的Transactional方法里保证订单和行为的写入是同一事务——如果行为插入失败整个下单要回滚。示例如下Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { Order order new Order(); order.setUserId(dto.getUserId()); order.setTotalAmount(dto.getTotalAmount()); orderMapper.insert(order); ListUserDishBehavior behaviors new ArrayList(); for (OrderItemDTO item : dto.getItems()) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDishId(item.getDishId()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); UserDishBehavior behavior new UserDishBehavior(); behavior.setUserId(dto.getUserId()); behavior.setDishId(item.getDishId()); behavior.setBehaviorType(2); behavior.setCreateTime(LocalDateTime.now()); behaviors.add(behavior); } behaviorMapper.batchInsert(behaviors); return order.getId(); }rollbackFor Exception.class是把所有异常都纳入回滚范围别用默认的只回滚 RuntimeException——点餐系统的下单可能抛受检异常漏配会导致订单有了但行为没记上。顺序是先插订单头、再插明细、最后插行为任何一步失败前面的数据全部回滚。数据一致性是推荐系统跟业务系统的接口边界也是 springboot 事务面试题的实际场景。5. 推荐接口的正确性验证与参数调优技巧5.1 用断言脚本预防推荐结果“看着合理但实际跑偏”推荐接口返回的是 JSON肉眼看个三五条“还挺像那么回事”不代表算法是对的。常见跑偏情况有三种用户点过的菜出现在结果里、推荐列表品种全相同、冷启动用户返回空数组。写一个简单的验证主函数或测试用例比手工核对高效得多Test void recommendShouldExcludeOrderedDishes() { Long userId 18L; ListDishVO result recommendService.recommendForUser(userId, 10); ListLong ordered orderMapper.selectDishIdsByUserId(userId); ListLong recommendedDishIds result.stream() .map(DishVO::getId).collect(Collectors.toList()); for (Long orderedId : ordered) { assertFalse(recommendedDishIds.contains(orderedId), 推荐列表包含用户已点菜品: orderedId); } }另一个能立刻暴露问题的技巧是给固定测试用户造数据手动往行为表里插入两条“用户 A 点过宫保鸡丁和鱼香肉丝”的记录然后断言推荐结果里必须包含与这两道菜相似度最高的第三道菜。跑通了这个断言至少能证明矩阵构建和 Top-N 排序的主链路是通的。验证正确性要比对着浏览器刷新更快发现问题尤其是改动相似度公式或过滤阈值的时候。5.2 参数调节的推荐值一栏表推荐系统在点餐场景下的参数调优重点看三个指标接口响应时间、排序稳定性、冷启动通道命中率。综合多个项目经验给出一个可以直接当作初始值的参数表参数推荐值调优方向sim-threshold0.3调高则推荐更多“热门相似”结果调低则引入长尾但噪声增加max-candidate50响应超时优先调低推荐多样性差可以适度调高top-n10移动端建议 6Web 端建议 10-12行为时间窗口90 天菜品季节性强建议缩短至 30-60 天冷启动热门榜条数6新用户首页展示 6 道菜配合分页滚动相似度计算采样用户数最近 5000 用户数据量大时限制规模保证启动初始化在 3 秒内最后检查itemSimMatrix里是否全是 0若相似度矩阵全为 0大概率是user_dish_behavior表里没有数据或者common.size() 2的过滤生效太狠。前者回到第 4 章补数据后者降低sim-threshold或者在皮尔逊公式分母上加平滑项λ0.5公式改成return upper / (Math.sqrt(lowerA * lowerB) 0.5);避免分母过小导致相似度虚高。用日志把矩阵里的 Top 5 相似菜品打出来跟人工判断对比一遍就能确认推荐模块从数据到公式的每个环节都正确。本文还有配套的精品资源点击获取
返回列表