简介:这是一份基于Java技术栈的美食推荐管理系统毕业设计文档,适合计算机相关专业学生用于课程设计、毕业设计参考,也可供对JSP+B/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 混写,后续改需求很痛苦 |
| SSH(Struts+Spring+Hibernate) | 高 | 多 | 十年前的老项目 | 配置链太长,环境差异大时启动艰难 |
| 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 与 Spring Boot 的桥接依赖 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok,减少 getter/setter 样板代码,需要在 IDEA 里装插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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里的characterEncoding=utf8和serverTimezone=Asia/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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 菜品分类表:建议先固定几个大类,如川菜、粤菜、甜品 CREATE TABLE `category` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '分类名', `sort` INT DEFAULT 0 COMMENT '排序权重,数值越小越靠前' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 菜品表:推荐系统的被推荐对象 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 收藏表:记录用户主动收藏的菜品 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`) -- 防止重复收藏 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 评分行为表:用户给菜品的打分,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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;设计时有几个细节值得注意。第一,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 List<DishDTO> recommendForUser(Integer userId, int topN) { // 1. 取出该用户所有行为,统计标签得分 Map<String, Double> tagScoreMap = new HashMap<>(); // 1.1 收藏行为:收藏的菜品标签按权重累加 List<Dish> favoriteDishes = favoriteMapper.selectDishesByUserId(userId); for (Dish dish : favoriteDishes) { addTagScores(tagScoreMap, dish.getTasteTags(), FAVORITE_WEIGHT); } // 1.2 评分行为:评分越高,标签得分越高 List<Rating> ratings = ratingMapper.selectByUserId(userId); for (Rating rating : ratings) { Dish dish = dishMapper.selectById(rating.getDishId()); if (dish != null) { addTagScores(tagScoreMap, dish.getTasteTags(), rating.getScore()); } } // 2. 遍历全部菜品,按标签匹配度 + 菜品热度 计算推荐得分 List<Dish> allDishes = dishMapper.selectAll(); List<DishScore> 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())); List<DishDTO> 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(Map<String, 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 Result<List<DishDTO>> recommendList(@RequestParam(defaultValue = "6") int topN, HttpSession session) { User user = (User) session.getAttribute("loginUser"); if (user == null) { // 未登录时,直接返回按热度排序的菜品,作为冷启动兜底 return Result.success(recommendService.hotDishes(topN)); } List<DishDTO> list = recommendService.recommendForUser(user.getId(), topN); // 如果推荐结果不足 topN,用热搜菜品补齐 if (list.size() < topN) { List<DishDTO> 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.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <java.version>17</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 'root'@'localhost',系统起是起了,但打开页面接口一直转圈。
原因:数据源配置里三样东西最容易出错: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.49,MySQL 8.0+ + 驱动 8.x,不要混搭。
连接串里还有两个参数建议直接写上:useSSL=false和allowPublicKeyRetrieval=true。前者避免本地开发时 SSL 握手造成的启动延迟,后者解决 MySQL 8 在非 SSL 连接下偶尔报的Public Key Retrieval is not allowed错误。这两个参数不属于安全最佳实践,但非常符合本地开发的真实使用需求。
5.3 中文乱码:全链路统一编码 utf8mb4
现象:数据库存进去的「麻婆豆腐」显示成???,或者接口返回的数据是好的,前端页面渲染出来却是乱码。
原因:编码问题从来不是一个点的事,而是一条链的事。数据库连接串没加characterEncoding=utf8,Java 读取时用的是系统默认编码;数据库建表时DEFAULT CHARSET写成了latin1;前端页面<meta charset>没设置或者设置成了gbk。这三个环节只要有一个断掉,乱码就来了。
解决:从下往上依次排查。第一,数据库、表、字段三级全部确认utf8mb4字符集——检查库可以用SHOW CREATE DATABASE food_recommend,检查表用SHOW CREATE TABLE dish。第二,连接串里的useUnicode=true&characterEncoding=utf8双参数必须存在,注意不要写成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 道菜,里面有几道是用户真喜欢的 |
| 召回率 | 推荐列表中命中测试集的数目 / 测试集总数目 | 用户喜欢的菜里,系统推出来了几个 |
| F1 | 2 * 精确率 * 召回率 / (精确率 + 召回率) | 综合平衡指标,答辩时好用 |
手动实现这个评估只需要在RecommendService里加一个统计方法:遍历推荐列表,判断rating表里该用户是否对这道菜打过分。演示时对着老师汇报「精确率 0.35、召回率 0.28」,比说「我觉得推荐得还行」强太多。
接口提速方面,最值得做的是给「热门菜品」加缓存。当前hotDishes()每次都要执行一次全表查询再排序,而热搜榜单的实时性要求很低。常见做法是启动时加载一次,定时刷新,用@Scheduled注解可以轻松实现:
@Component public class HotDishCache { @Resource private DishMapper dishMapper; private List<DishDTO> hotList; @PostConstruct public void init() { refresh(); } @Scheduled(fixedDelay = 600000) // 每 10 分钟刷新一次 public void refresh() { hotList = dishMapper.selectHotDishes(10); } public List<DishDTO> getHotList() { return hotList; } }这段逻辑的核心价值在于:把「每次查询都全表扫描」变成「内存里存一份,定时更新」。课程设计阶段没有 Redis 一样能写出高性能接口,回答老师「如何缓存」提问时也有实际代码可讲。等你把这个方向摸透了,再往「java 后端完整成长路线」里进阶时,自然就知道 Redis、消息队列那些组件是在解决哪一类问题。
回到推介系统本身,我习惯在项目根目录放一个style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />