多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于Java Spring Boot的美食推荐管理系统:从数据库到推荐算法实战解析

基于Java Spring Boot的美食推荐管理系统:从数据库到推荐算法实战解析 简介这是一份基于Java技术栈的美食推荐管理系统毕业设计文档适合计算机相关专业学生用于课程设计、毕业设计参考也可供对JSPB/S架构开发感兴趣的开发者学习。资源为单个docx文档共1个文件压缩包大小约4.76MB内容完整包含摘要、目录、需求分析、系统设计、数据库设计、功能模块说明及测试章节。文档围绕管理员与普通用户两类角色详细介绍了个人中心、美食分类、热门美食推荐、美食教程、美食店铺、美食社区等模块的实现思路并采用Mysql作为后台数据库整体结构清晰便于直接借鉴系统框架与开发流程。当前已有76人学习下载适合需要快速了解美食推荐系统完整设计方案、撰写相关论文或搭建同类项目的读者使用。1. 从一份 docx 到一套能跑的系统美食推荐管理系统到底要交付什么拿到一份「基于java美食推荐管理系统设计与实现.docx」先别急着翻页看目录。这类题目的套路其实非常固定一个 Java Web 后端配一个普通的前端页面核心是「菜品数据的管理」加上「给用户做推荐」这两件事。对正在做课程设计或毕业设计的同学来说它真正要交付的不是一本厚厚的文档而是「打开浏览器能登录、能看菜、能点推荐、能管理菜品」的完整闭环对已经上班的工程师来说这套结构又恰好是理解业务系统分层的最佳切入点。我见过太多人把精力花在文档排版上最后演示时系统跑不起来。这篇笔记会按「技术选型 → 数据设计 → 推荐实现 → 现场排错」的顺序把这套系统的每个关键节点拆开讲清楚并给出能直接复制的代码结构。你不需要分布式中间件不需要微服务一台电脑、一个数据库、一个 IDEA 就能把它跑通。读完之后你至少能把 docx 里的概念图变成自己手里真正能点能查的系统。2. 技术选型与工程骨架先解决「用 Java 的哪一套」再写业务代码美食推荐管理系统叫「基于 Java」不代表只能用 JSP Servlet。很多课程设计文档里写的「Java Web」实际上是指 Java 生态里的 Web 开发方案。我把常见方案放在一起比对过理解每个方案的边界之后再动手能省掉大量返工时间。2.1 JSP/Servlet、SSH、Spring Boot 怎么选一张表看清利弊先给结论如果这份文档没有强制指定 JSP我一般会直接选 Spring Boot。理由不是因为它新而是因为它让「配置」这件事从玄学变成了可复现的步骤。下面这张表是我给团队做内部培训时常用的对比口径对初学者同样适用方案上手成本配置文件量适合场景常见坑JSP Servlet中少学校要求、老系统维护页面和 Java 混写后续改需求很痛苦SSHStrutsSpringHibernate高多十年前的老项目配置链太长环境差异大时启动艰难Spring Boot MyBatis低少新项目、课程设计、中小型管理系统版本间依赖需确认其他都算友好Spring Boot JPA低少快速原型、CRUD 居多复杂查询时需要碰 JPQL学习曲线中等美食推荐系统的核心业务是「增删改查 一个推荐算法」没有复杂的分布式事务也没有高并发压力。Spring Boot MyBatis 是最稳的组合MyBatis 让你手写 SQL能看到每一条数据是怎么查出来的这对答辩时的提问环节特别有利——老师问「这个菜品的列表怎么来的」你直接讲 SQL 比讲框架自动生成要踏实得多。2.2 Maven 工程目录与 pom.xml先把骨架立起来打开 IDEA新建 Spring Boot 工程时选择 Maven 管理依赖。推荐模块拆分方式不是多模块而是单模块 分包因为课程设计规模不需要微服务那套。工程目录我习惯这样规划src/main/java/com/example/food ├── controller # 接口层只做参数接收和结果返回 ├── service # 业务层处理推荐逻辑、事务边界 ├── mapper # MyBatis 数据访问接口 ├── entity # 数据库对应的实体类 ├── dto # 给前端传输的对象避免直接暴露实体 ├── config # 拦截器、跨域等配置 └── common # 统一返回结果、异常处理对应 pom.xml 里的核心依赖我通常只保留下面这几个。版本号不用记在 IDEA 创建工程时选好 Spring Boot 版本后让工具自动管理即可dependencies !-- Web 启动器内嵌 Tomcat提供 MVC 能力 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 与 Spring Boot 的桥接依赖 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok减少 getter/setter 样板代码需要在 IDEA 里装插件 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段配置里要特别注意scope为 runtime 的 MySQL 驱动编译时不需要运行期才加载。很多同学在写代码时发现编辑器不报错一启动就报ClassNotFoundException: com.mysql.cj.jdbc.Driver查下来都是依赖没拉全或者驱动被标成了 provided。2.3 application.yml 配置数据源、端口、编码三件套依赖只是骨架真正让系统活过来的是配置文件。我把最小可用配置写在下面每个参数后标注了作用server: port: 8080 # 后端端口IDEA 里可以直接改掉避免冲突 spring: datasource: url: jdbc:mysql://localhost:3306/food_recommend?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 # 换成你自己 MySQL 的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml # 所有 SQL 映射文件放在 resources/mapper 下 configuration: map-underscore-to-camel-case: true # 数据库下划线字段自动映射为实体驼峰属性 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL排查问题必备url里的characterEncodingutf8和serverTimezoneAsia/Shanghai这两个参数是中文乱码和日期错乱的两大救星。map-underscore-to-camel-case设为 true 后数据库里user_name会自动对应实体的userName这个配置能让代码里面省掉大量Results注解。配置完成后启动一次控制台出现 Started 字样并且访问http://localhost:8080/能返回内容骨架就算立住了。如果你用的是 JSP 技术栈还需要在 pom.xml 中额外引入tomcat-embed-jasper依赖且 JSP 页面必须放在src/main/webapp/WEB-INF/下否则运行时直接 404。3. 数据模型与会话设计五张表支撑用户、菜品与行为记录推荐系统不管算法多花哨底层都依赖「用户对菜品产生的行为数据」。也就是说你在设计表结构的那一刻其实就在决定推荐算法能吃什么数据。这一章我用五张核心表把管理系统的数据底座讲清楚。3.1 建表 SQL用户、菜品、分类、收藏、评分行为先给出完整的建表脚本这五张表基本涵盖了一个美食推荐管理系统的全部业务场景-- 用户表保存登录账号和基本信息 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码建议 BCrypt 加密存储, nickname VARCHAR(50) COMMENT 昵称展示给其他用户看, avatar VARCHAR(255) COMMENT 头像图片地址, role TINYINT DEFAULT 0 COMMENT 0-普通用户 1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜品分类表建议先固定几个大类如川菜、粤菜、甜品 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名, sort INT DEFAULT 0 COMMENT 排序权重数值越小越靠前 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜品表推荐系统的被推荐对象 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 所属分类, name VARCHAR(100) NOT NULL COMMENT 菜品名, description TEXT COMMENT 菜品描述可包含食材和口味, image VARCHAR(255) COMMENT 图片路径, price DECIMAL(10,2) DEFAULT 0 COMMENT 参考价格, spicy_level TINYINT DEFAULT 0 COMMENT 辣度 0-3, taste_tags VARCHAR(255) COMMENT 口味标签逗号分隔如 麻辣,鲜香, avg_score DECIMAL(3,2) DEFAULT 0 COMMENT 平均评分推荐权重之一, view_count INT DEFAULT 0 COMMENT 浏览量统计热度, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 收藏表记录用户主动收藏的菜品 CREATE TABLE favorite ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, dish_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_dish (user_id, dish_id) -- 防止重复收藏 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 评分行为表用户给菜品的打分1~5 分 CREATE TABLE rating ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, dish_id INT NOT NULL, score TINYINT NOT NULL COMMENT 1-5 分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_dish_rating (user_id, dish_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;设计时有几个细节值得注意。第一taste_tags字段我存的是逗号分隔的字符串而不是单独建一张标签关联表原因是你没有复杂到需要标签反查的场景在 Java 代码里用Arrays.asList(tasteTags.split(,))就能拆出来用。第二收藏表和评分表的唯一索引uk_user_dish相当关键它从数据库层杜绝了「同一个用户重复收藏同一道菜」这种脏数据比在 Service 里每次先查询再插入要稳得多。3.2 登录会话与拦截器管理员接口不能裸奔管理系统和普通展示页最大的区别在于部分接口只能管理员访问。如果你不做任何拦截任何人调用「删除菜品」的接口地址都能删数据这在答辩演示时是硬伤。Spring Boot 里用拦截器实现这个控制非常干净Component public class AdminInterceptor implements HandlerInterceptor { // 在进入 Controller 方法之前执行 Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从 Session 中取当前登录用户 User user (User) request.getSession().getAttribute(loginUser); if (user ! null user.getRole() 1) { return true; // 管理员放行 } // 不是管理员返回 302 跳转到登录页 response.sendRedirect(/login); return false; } }然后在配置类里注册拦截器并指定拦截路径。这里最容易被忽略的是路径匹配规则一般我拦截/admin/**这类统一前缀而不是把所有接口都拦住。Configuration public class WebConfig implements WebMvcConfigurer { Resource private AdminInterceptor adminInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(adminInterceptor) .addPathPatterns(/admin/**) // 拦截所有以 /admin/ 开头的请求 .excludePathPatterns(/admin/login); // 登录接口本身要放行 } }逻辑解释preHandle在 Controller 方法被调用前触发。代码里先从 Session 里取出登录时存入的loginUser对象判断role是否为 1。这里要留个心眼——用户在登录时如果不把User对象塞进 Session这段代码每次都会把管理员拦在门外表现成「登录成功了但访问 /admin/dish 还是跳回登录页」。拦截器的实现思路同样适用于 JSP 技术栈只是把Component换成 XML 配置把preHandle换成Filter本质逻辑完全一致。这一步做完「管理系统」的身份边界就有了。3.3 实体类与数据传输对象Entity 和 DTO 别混用很多翻车现场发生在对象拷贝上。数据库查询返回的Dish实体类直接扔给前端 API 返回刚开始没问题等你想在菜品列表里额外带一个「是否已收藏」字段时就只能在实体里硬塞一个和数据库无关的属性时间一长类就烂了。我的习惯是 Controller 的返回值一律用 DTO哪怕字段长得一模一样也单独建类。MyBatis 查询时用 ResultMap 手动映射或者直接用 BeanUtils 进行浅拷贝。关于「java 对象深度拷贝」这个常见讨论点这里根本用不上深度拷贝。Dish和DishDTO都是简单字符串、数字、日期字段没有嵌套对象BeanUtils.copyProperties()的浅拷贝就完全够用。如果你在处理一个 List 的拷贝就循环调用。深度拷贝通常出现在缓存对象复用、配置对象共享这些场景这个项目里硬上反而增加阅读复杂度。4. 推荐引擎的简化实现没有协同过滤也能做出有效推荐推荐算法是整个系统的灵魂也是答辩时老师最可能深挖的地方。但很多人一上来就想着实现协同过滤结果卡在矩阵计算里出不来。这一章我讲一种课程设计最稳妥的方案基于用户行为的加权评分推荐。它简单、可解释、代码量少但效果对演示来说完全足够。4.1 算法选型为什么先放弃协同过滤协同过滤Collaborative Filtering是推荐系统的经典方案但它在美食推荐管理系统这个场景里有两个天然的困难。第一是数据稀疏。协同过滤需要「用户对物品」的评分矩阵假设系统有 50 个用户、200 道菜打过分的数据可能只有几百条矩阵里 95% 以上的位置是空值。稀疏矩阵下算出来的用户相似度非常不可靠你给用户 A 推荐「和他相似的用户 B 喜欢的东西」但 B 可能只评过一道菜推荐结果几乎没有解释力。第二是实现复杂度。你要计算相似度矩阵、要处理冷启动、要考虑实时更新一套流程跑下来代码量远超预期。对于管理系统这个量级投入产出比不划算。所以我一般先做「基于内容 行为加权」的推荐给菜品打标签给用户的收藏、评分、浏览行为分配不同权重然后按得分排序。这套算法的逻辑用一个词概括就是「标签匹配 行为加权」。用户喜欢川菜收藏了三道辣菜系统就该给他推更多「辣」属性的菜——这个逻辑简单到可以直接讲给答辩老师听。4.2 核心 Java 代码标签兴趣向量 TopN 推荐我把推荐服务的核心代码写在这里。它做的事情是从用户的行为记录算出每个标签的得分再从全量菜品里找出分数最高的前 N 道菜返回。Service public class RecommendService { Resource private DishMapper dishMapper; Resource private FavoriteMapper favoriteMapper; Resource private RatingMapper ratingMapper; // 用户行为权重收藏 3 分评分 1~5 分直接累加浏览 1 分 private static final double FAVORITE_WEIGHT 3.0; private static final double VIEW_WEIGHT 1.0; public ListDishDTO recommendForUser(Integer userId, int topN) { // 1. 取出该用户所有行为统计标签得分 MapString, Double tagScoreMap new HashMap(); // 1.1 收藏行为收藏的菜品标签按权重累加 ListDish favoriteDishes favoriteMapper.selectDishesByUserId(userId); for (Dish dish : favoriteDishes) { addTagScores(tagScoreMap, dish.getTasteTags(), FAVORITE_WEIGHT); } // 1.2 评分行为评分越高标签得分越高 ListRating ratings ratingMapper.selectByUserId(userId); for (Rating rating : ratings) { Dish dish dishMapper.selectById(rating.getDishId()); if (dish ! null) { addTagScores(tagScoreMap, dish.getTasteTags(), rating.getScore()); } } // 2. 遍历全部菜品按标签匹配度 菜品热度 计算推荐得分 ListDish allDishes dishMapper.selectAll(); ListDishScore scoreList new ArrayList(); for (Dish dish : allDishes) { double score 0; String[] tags dish.getTasteTags().split(,); for (String tag : tags) { // 命中用户兴趣标签累加对应得分 if (tagScoreMap.containsKey(tag)) { score tagScoreMap.get(tag); } } // 3. 加上小部分热度分让新用户也有的推 score dish.getViewCount() * 0.01 dish.getAvgScore(); scoreList.add(new DishScore(dish, score)); } // 4. 降序排序取前 N scoreList.sort((a, b) - Double.compare(b.getScore(), a.getScore())); ListDishDTO result new ArrayList(); for (int i 0; i Math.min(topN, scoreList.size()); i) { result.add(convertToDTO(scoreList.get(i).getDish())); } return result; } // 将逗号分隔的标签字符串拆分并累加到得分 Map 中 private void addTagScores(MapString, Double tagScoreMap, String tasteTags, double weight) { if (tasteTags null || tasteTags.isEmpty()) { return; } for (String tag : tasteTags.split(,)) { tagScoreMap.put(tag, tagScoreMap.getOrDefault(tag, 0.0) weight); } } }这段代码的逻辑分四步走。第一步是统计用户标签偏好收藏的盘子较大评分的按分数累加这样「用户给了 5 分」比「用户给 3 分」在后续推荐里权重更高。第二步是遍历所有菜品对每道菜的标签依次匹配用户的兴趣标签命中一次累加一次。第三步加上热度分和平均分作为兜底信号目的是让没有任何行为记录的新用户拿到推荐结果时不会看到一片空白——全是高分菜。第四步做降序排序截取前 N 个。参数topN通常设 6 或 8适配前端展示的网格布局。FAVORITE_WEIGHT和VIEW_WEIGHT是调参的关键如果发现收藏行为对推荐结果影响太大、推荐列表总是一成不变就把收藏权重调低如果觉得推荐的东西和用户偏好差距太大就把浏览量权重降下去。这类基于经验的调参不需要数学推导演示前自己多试几次就能找到手感。4.3 推荐接口与冷启动策略保证列表永远不为空推荐算法写好了还得有接口暴露给前端。Controller 层保持薄只负责接收参数、调用 Service、返回统一结构RestController RequestMapping(/api/recommend) public class RecommendController { Resource private RecommendService recommendService; GetMapping(/list) public ResultListDishDTO recommendList(RequestParam(defaultValue 6) int topN, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { // 未登录时直接返回按热度排序的菜品作为冷启动兜底 return Result.success(recommendService.hotDishes(topN)); } ListDishDTO list recommendService.recommendForUser(user.getId(), topN); // 如果推荐结果不足 topN用热搜菜品补齐 if (list.size() topN) { ListDishDTO hot recommendService.hotDishes(topN - list.size()); list.addAll(hot); } return Result.success(list); } }接口层面我处理了两件容易被忽略的事。一个是未登录用户Session 里拿不到 userId算法跑不了直接返回热门菜品列表这个行为在演示时很有说服力可以大方地解释为「冷启动策略」。另一个是登录用户推荐不足比如用户只收藏了一道菜兴趣标签就一个匹配到的菜品可能不足 6 道这时候拿热门菜补齐位子保证前端永远不会渲染空列表。这段代码还间接回答了「java 动态代理」这类概念在这个项目里的位置——Spring 的Service类默认被 CGLIB 动态代理包裹事务和方法权限都靠代理的拦截机制来实现。你不必手动写代理但理解它有助于解释「为什么我在同一个 Service 类里调用自己的方法时事务不生效」这类经典坑。5. 部署与运行排查四类高频翻车现场及对应修法不管代码写得多顺最终都要过「能跑起来」这一关。这一章整理我在实际调试中遇到最多的四个问题场景每一条都按「现象 → 原因 → 解决」的格式记录可以当作你自己的排错手册用。5.1 环境配置翻车启动报错或提示源发行版本不对现象双击启动类后控制台直接报错误: 不支持发行版本 5或者java: 警告: 源发行版 17 需要目标发行版 17有时还会出现编译后运行时提示类找不到。原因绝大多数情况是 IDEA 的项目 SDK、Java Compiler 的 Target bytecode version 和 Maven 的pom.xml里maven.compiler.source/target三者不一致。装了 JDK 17 但项目设置里编译等级还是 1.5就会出现第一个报错装了高版本 JDK 但本地环境变量指向旧版本就会出现第二个报错。解决按「三处统一」原则处理。OpenFile → Project Structure → Project确认 SDK 为你安装的版本再进Settings → Build Tools → Maven → Runner把 JRE 设为和项目 SDK 一致最后在pom.xml的properties里显式声明properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target java.version17/java.version /properties我一般在接到一台新电脑时直接打开命令行敲java -version和javac -version对比两者输出。命令行的javac版本和 IDEA 内编译版本不一致是最隐蔽的翻车点——IDE 里编译通过部署到自己电脑的 Tomcat 时突然就废了。Java 环境变量配置这件事静态看无非是JAVA_HOME和Path两条但多数报错都发生在改完环境变量后没有重启终端或者 IDEA 里还在用启动时缓存的旧 JDK 路径。5.2 数据库连不上时区、驱动、密码三连问现象启动日志出现Cannot create PoolableConnectionFactory或者Access denied for user rootlocalhost系统起是起了但打开页面接口一直转圈。原因数据源配置里三样东西最容易出错MySQL 的密码不对版本不匹配——MySQL 8.x 必须用com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver时区参数缺失——MySQL 8 要求明确提供serverTimezone否则连接被拒。解决先做最小化验证。打开 MySQL 命令行用配置里的账号密码手动执行SELECT 1排除数据库本身的问题。然后检查驱动依赖的版本是否和 MySQL 一致我见过数据库是 MySQL 5.7、驱动却引了 MySQL 8 的mysql-connector-java结果权限认证方式不兼容。最稳妥的配合是MySQL 5.7 驱动 5.1.49MySQL 8.0 驱动 8.x不要混搭。连接串里还有两个参数建议直接写上useSSLfalse和allowPublicKeyRetrievaltrue。前者避免本地开发时 SSL 握手造成的启动延迟后者解决 MySQL 8 在非 SSL 连接下偶尔报的Public Key Retrieval is not allowed错误。这两个参数不属于安全最佳实践但非常符合本地开发的真实使用需求。5.3 中文乱码全链路统一编码 utf8mb4现象数据库存进去的「麻婆豆腐」显示成???或者接口返回的数据是好的前端页面渲染出来却是乱码。原因编码问题从来不是一个点的事而是一条链的事。数据库连接串没加characterEncodingutf8Java 读取时用的是系统默认编码数据库建表时DEFAULT CHARSET写成了latin1前端页面meta charset没设置或者设置成了gbk。这三个环节只要有一个断掉乱码就来了。解决从下往上依次排查。第一数据库、表、字段三级全部确认utf8mb4字符集——检查库可以用SHOW CREATE DATABASE food_recommend检查表用SHOW CREATE TABLE dish。第二连接串里的useUnicodetruecharacterEncodingutf8双参数必须存在注意不要写成utf8和UTF-8混用。第三Spring Boot 中还需要在 application.yml 里配置消息转换编码server: servlet: encoding: charset: UTF-8 enabled: true force: trueforce: true的意思是即使响应头已经设置了编码也强制使用 UTF-8。这一步容易漏漏了的表现是 POST 请求的 JSON 体中文乱码而 GET 请求正常。5.4 图片上传后 404路径映射与虚拟目录的关系现象本地开发时菜品图片能正常预览打包部署后就裂了或者换台电脑打开项目图片全部丢失。查看后台日志没有异常请求图片地址返回 404。原因Spring Boot 默认只处理classpath:/static/下的静态资源你把图片存到了项目根目录的upload文件夹下这个文件夹在打包成 jar 后根本不存在或者 IDE 的启动工作目录不同导致路径不一致。解决不要把上传文件放在项目源码目录里。在application.yml中配置一个绝对路径再映射成一个 URL 前缀file: upload-dir: D:/food-recommend/upload # 换成你自己的绝对路径 spring: web: resources: static-locations: classpath:/static/,file:${file.upload-dir}代码里获取保存路径时用System.getProperty(user.dir)拼接也行但更推荐配置化。前端展示图片时URL 直接写/images/xxx.jpg即可。如果你在部署环境用的是 Linux 服务器路径要写成/home/xxx/food-recommend/upload这种绝对路径别带 Windows 盘符。6. 收尾进阶推荐效果的离线验证与接口提速技巧系统能跑通只是第一步离「可以被拿出来讲」还差最后两件事怎么证明推荐有效果怎么让接口更快。这一章不展开新功能只讲两个可以直接落地的验证与优化方向。先给推荐效果做一个简单的离线评估。常见做法是预留 20% 的评分行为数据作为测试集剩下的作为训练集用训练集算出推荐列表再看测试集里用户实际喜欢的菜品有多少出现在推荐列表中。计算公式是精确率Precision和召回率Recall指标公式含义精确率推荐列表中命中测试集的数目 / 推荐列表总长度系统推 10 道菜里面有几道是用户真喜欢的召回率推荐列表中命中测试集的数目 / 测试集总数目用户喜欢的菜里系统推出来了几个F12 * 精确率 * 召回率 / (精确率 召回率)综合平衡指标答辩时好用手动实现这个评估只需要在RecommendService里加一个统计方法遍历推荐列表判断rating表里该用户是否对这道菜打过分。演示时对着老师汇报「精确率 0.35、召回率 0.28」比说「我觉得推荐得还行」强太多。接口提速方面最值得做的是给「热门菜品」加缓存。当前hotDishes()每次都要执行一次全表查询再排序而热搜榜单的实时性要求很低。常见做法是启动时加载一次定时刷新用Scheduled注解可以轻松实现Component public class HotDishCache { Resource private DishMapper dishMapper; private ListDishDTO hotList; PostConstruct public void init() { refresh(); } Scheduled(fixedDelay 600000) // 每 10 分钟刷新一次 public void refresh() { hotList dishMapper.selectHotDishes(10); } public ListDishDTO getHotList() { return hotList; } }这段逻辑的核心价值在于把「每次查询都全表扫描」变成「内存里存一份定时更新」。课程设计阶段没有 Redis 一样能写出高性能接口回答老师「如何缓存」提问时也有实际代码可讲。等你把这个方向摸透了再往「java 后端完整成长路线」里进阶时自然就知道 Redis、消息队列那些组件是在解决哪一类问题。回到推介系统本身我习惯在项目根目录放一个style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
返回列表