如果你正在准备毕业设计,并且已经确定要做“校园周边美食推荐系统”这类题目,那我建议你先想清楚一个问题:你的系统凭什么比一个普通的信息管理系统更有竞争力?
每年毕业季,校园美食推荐系统的数量非常多,选题本身并不稀缺。SpringBoot、Vue、MySQL、推荐算法,这些都已经是成熟技术,难的不是“把这些技术拼起来”,而是如何在论文和答辩中讲清楚两件事:第一,你的系统解决了什么真实问题;第二,你的推荐逻辑到底是怎么设计的。
这篇文章会围绕一个毕设级别的校园周边美食推荐系统,完整拆解后端SpringBoot、前端Vue、数据库设计、推荐算法实现、论文写作和答辩准备。你可以把它当作一套可落地的项目框架,也可以挑其中一部分移植到自己的项目里。我要给的判断是:这类系统真正的分数,不在CRUD,而在推荐逻辑的设计与讲解。
1. 这类毕业设计真正难在哪
先说一个容易被忽略的事实:校园周边美食推荐系统的业务功能,本质上还是用户、商家、美食、收藏、评分、评论这些模块的增删改查。如果只做到这一步,项目能运行,但没有任何记忆点。
真正拉开差距的,是推荐模块。
为什么这么说?因为推荐系统要处理的问题不是“把数据存进数据库然后查出来”,而是“在用户没有明确搜索关键词时,如何把合适的内容主动推送给他”。这涉及到用户行为数据的采集、用户偏好的分析、推荐结果的排序,以及冷启动场景的处理。整个链路比普通的管理系统多了一层数据分析和算法设计的逻辑。
另一个难点是数据从哪来。很多同学做这个题目时,手边没有真实数据,于是随便造几条测试数据,结果推荐效果非常假。比较稳妥的做法是:构建一个可复用的模拟数据生成逻辑,比如随机生成100个用户、50家商家、200条美食记录,再随机生成一批收藏和评分记录。这样既能验证推荐效果,也能在论文里写清楚测试数据的来源和构造方式。
同时,推荐效果的评估也是答辩时容易被追问的地方。你不能只说自己“推荐了相似美食”,还要能说出用什么指标衡量推荐效果。毕业设计一般不需要做完整的离线实验,但至少要能说明:你是用什么规则排序的,准确率和覆盖率在测试集上大概是什么水平,或者至少说清楚你的推荐结果有可解释性。
所以这篇文章要解决的问题是:怎么在毕业设计的时间范围内,做一个“能跑、能讲、能过答辩”的校园周边美食推荐系统。它的核心不是堆技术栈,而是把推荐链路设计清楚,并且能用代码和论文把整个过程表达出来。
2. 核心技术栈与推荐算法选型
2.1 后端技术选型
系统后端选择SpringBoot,这是目前Java毕设使用最广的框架,理由也很直接:SpringBoot通过自动配置和Starter机制,把SSM时代的繁琐配置大幅简化。开发者只需要引入相关依赖,编写少量配置就能启动一个独立运行的Web服务,内嵌Tomcat让部署也变得很轻量。
持久层框架推荐使用MyBatis-Plus。如果你用过原生MyBatis,一定体验过自己写大量Mapper XML的繁琐过程。MyBatis-Plus在MyBatis基础上提供了通用CRUD接口,单表操作基本不用手写SQL,分页插件也内置好了。这对于毕设项目来说省时间,而且代码量减少后,论文里贴代码也不会占太多篇幅。需要注意,MyBatis-Plus不是银弹,复杂的多表关联查询还是要自己写注解SQL或XML。
数据库方面,MySQL是首选,成本低、资料多、环境搭建简单。表结构设计按第三章的规划来就行。
2.2 前端技术选型
前端选择Vue。Vue的学习曲线比React更平滑,对于不常写前端的大四学生来说更加友好。这里建议使用Vue 3配合Element Plus组件库,页面风格接近管理后台,适合快速搭建。当然,如果你对Vue 2和Element UI更熟,使用Vue 2也完全没有问题,本文讲到的是通用思路。
前后端分离是毕设中最常见的架构。后端只提供JSON接口,前端通过axios调用。项目结构上,前端项目和后端项目是两个独立目录,这也方便在论文中分别描述。
2.3 推荐算法选型对比
推荐算法是核心,但不要一上来就追求复杂的深度学习模型。毕设的时间有限,需要的是一个能讲清楚、能跑出效果、出现Bug时能自己排查的算法。下面是常见的推荐方案对比。
| 方案 | 原理 | 优点 | 缺点 | 毕设适配度 |
|---|---|---|---|---|
| 基于内容推荐 | 根据用户喜欢过的食物的分类、标签,推荐同类食物 | 可解释性强、没有冷启动依赖 | 推荐结果单一,缺乏新颖性 | 高 |
| 基于用户的协同过滤 | 找到口味相似的用户,推荐他们喜欢的食物 | 能发现不同分类的新食物 | 用户行为少时效果差 | 中高 |
| 基于物品的协同过滤 | 找到相似食物,推荐和用户喜欢过的食物相似的 | 推荐结果稳定 | 计算食物相似度需要一定数据量 | 中 |
| 基于规则推荐 | 按热度、评分、距离等规则排序 | 实现简单、稳定 | 个性化程度低 | 中 |
| 混合推荐 | 多种策略结合 | 效果均衡 | 实现复杂度略高 | 高 |
对于这个毕设,推荐采用“基于内容推荐 + 热度兜底”作为第一版实现,如果学有余力,再加入基础的基于用户的协同过滤。这样既能保证冷启动用户有推荐结果,又有个性化逻辑,论文里也有两个可对比的策略,内容不会显得单薄。
3. 系统功能模块与整体架构设计
3.1 用户角色设计
校园美食推荐系统的用户角色分为三类:普通学生用户、商家、系统管理员。
普通用户是系统的主要服务对象,能完成注册登录、浏览美食、搜索美食、查看美食详情、收藏、评分、评论,以及查看个性化推荐列表。商家的功能则会少一些,主要维护自己店铺和菜品的信息,查看用户对自己菜品的评价。管理员负责整体平台的数据维护,包括用户管理、商家审核、分类管理、数据统计等。
在毕设开发中,商家和管理员模块可以精简,但普通用户的主流程必须完整。因为论文中的用例分析、功能测试主要围绕主流程展开,主流程越完整,测试章节越好写。
3.2 功能模块划分
系统按功能可以拆成以下模块:
- 用户认证模块:注册、登录、退出、密码加密存储。
- 美食浏览模块:美食列表、分类筛选、关键字搜索、美食详情。
- 用户行为模块:收藏、评分(1到5分)、评论。
- 个性化推荐模块:根据用户行为生成推荐列表、热门美食推荐。
- 商家管理模块:美食上下架、信息维护。
- 管理员模块:用户管理、分类管理、数据统计。
这里面推荐模块和用户行为模块是核心,也是论文创新点所在。实际开发时建议先完成美食浏览和改进,再补行为采集,最后做推荐列表,顺序不能反。
3.3 系统架构分层
系统逻辑上采用三层架构:
- Controller层:接收前端请求,参数校验,返回统一响应体。
- Service层:业务逻辑处理,推荐算法的核心代码也放在这一层。
- Mapper层:数据库操作,基于MyBatis-Plus完成。
数据流向为:前端Vue发起请求 → 后端Controller接收 → Service处理业务与推荐逻辑 → Mapper访问MySQL → 数据逐层返回前端展示。
这种分层方式清晰,答辩时画架构图也会很方便。
4. 数据库表结构设计
数据库设计的目标是支撑上面的功能模块。推荐系统核心表有这些。
4.1 数据表划分
| 表名 | 说明 | 主要字段 |
|---|---|---|
| t_user | 用户表 | id, username, password, nickname, avatar, create_time |
| t_category | 美食分类表 | id, name, sort |
| t_merchant | 商家表 | id, name, address, phone, business_hours |
| t_food | 美食表 | id, category_id, merchant_id, name, price, image, tags, rating, description |
| t_rating | 评分表 | id, user_id, food_id, score, create_time |
| t_collection | 收藏表 | id, user_id, food_id, create_time |
| t_comment | 评论表 | id, user_id, food_id, content, create_time |
其中,t_rating和t_collection是推荐算法的数据来源。t_food中的tags字段用于存储“川菜”“辣”“高性价比”这类标签,推荐时会用到。
4.2 建表SQL示例
这里给出核心表的建表SQL,MySQL 5.7及以上版本都可以使用。
CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL UNIQUE COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密存储)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `t_category` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', `sort` int(11) DEFAULT 0 COMMENT '排序号', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美食分类表'; CREATE TABLE `t_merchant` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商家名称', `address` varchar(255) DEFAULT NULL COMMENT '地址', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `business_hours` varchar(100) DEFAULT NULL COMMENT '营业时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家表'; CREATE TABLE `t_food` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `merchant_id` bigint(20) DEFAULT NULL COMMENT '商家ID', `name` varchar(100) NOT NULL COMMENT '美食名称', `price` decimal(10,2) DEFAULT 0.00 COMMENT '价格', `image` varchar(255) DEFAULT NULL COMMENT '图片地址', `tags` varchar(255) DEFAULT NULL COMMENT '标签,多个用逗号分隔', `rating` decimal(3,1) DEFAULT 5.0 COMMENT '综合评分', `description` varchar(500) DEFAULT NULL COMMENT '描述', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='美食表'; CREATE TABLE `t_rating` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `food_id` bigint(20) NOT NULL, `score` int(11) NOT NULL COMMENT '评分1-5', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_food_id` (`food_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户评分表'; CREATE TABLE `t_collection` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `food_id` bigint(20) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏表';用户表里要注意password字段的长度。很多同学使用varchar(20)然后直接明文存储,这样做既不合规,答辩也会被老师质疑。这里建议使用BCrypt加密,字段长度留到100。
评分表和收藏表一定要加索引,因为推荐算法的核心查询都是按user_id去查行为记录,不加索引时数据量稍微变大就会出现明显的性能问题。
5. 后端SpringBoot接口实现
5.1 项目初始化与依赖配置
使用Spring Initializr创建项目,或直接在IDE中新建Spring Boot工程。项目结构如下:
food-recommend-backend ├── src/main/java/com/example/food │ ├── controller │ ├── entity │ ├── mapper │ ├── service │ └── FoodApplication.java └── src/main/resources └── application.ymlpom.xml核心依赖如下。版本号以你实际初始化的项目为准,不建议直接照抄。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>使用MyBatis-Plus时有一个比较容易踩的坑:版本兼容。MyBatis-Plus 3.5.x和不同版本的SpringBoot组合时,偶尔会出现Invalid value type for attribute 'factoryBeanObjectType'这类错误。遇到这种情况,优先检查MyBatis-Plus版本是否和你项目的SpringBoot版本匹配。
5.2 application.yml配置
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/food_recommend?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: automap-underscore-to-camel-case这个配置很关键,开启后数据库里的create_time会自动映射到Java实体里的createTime字段,省去大量手写@TableField注解的麻烦。
5.3 实体类与Mapper
实体类以Food为例。
package com.example.food.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; @Data @TableName("t_food") public class Food { @TableId(type = IdType.AUTO) private Long id; private Long categoryId; private Long merchantId; private String name; private BigDecimal price; private String image; private String tags; private BigDecimal rating; private String description; }Mapper接口继承了BaseMapper之后,就自动拥有了按主键查、按条件查、分页查等能力。
package com.example.food.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.food.entity.Food; public interface FoodMapper extends BaseMapper<Food> { }5.4 统一响应体封装
推荐一个简单的统一返回结构,这样前端处理时只需要判断code字段。
package com.example.food.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }这样做的好处是,后面加拦截器做登录鉴权时,返回格式不会乱。
5.5 美食浏览Controller
美食浏览是用户进入系统后的第一个页面,接口一般包含分类查询、按条件搜索等。
package com.example.food.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.food.common.Result; import com.example.food.entity.Food; import com.example.food.mapper.FoodMapper; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/food") public class FoodController { private final FoodMapper foodMapper; public FoodController(FoodMapper foodMapper) { this.foodMapper = foodMapper; } @GetMapping("/list") public Result<List<Food>> list(@RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<Food> wrapper = new LambdaQueryWrapper<>(); if (categoryId != null) { wrapper.eq(Food::getCategoryId, categoryId); } if (keyword != null && !keyword.isEmpty()) { wrapper.like(Food::getName, keyword).or().like(Food::getTags, keyword); } return Result.success(foodMapper.selectList(wrapper)); } @GetMapping("/detail") public Result<Food> detail(@RequestParam Long id) { return Result.success(foodMapper.selectById(id)); } }注意,wrapper.like(Food::getName, keyword).or().like(Food::getTags, keyword)这种写法在和其他条件组合时很容易出现SQL拼接问题。如果你同时在前面加了categoryId条件,这里会变成WHERE category_id = ? OR name LIKE ? OR tags LIKE ?,逻辑不对。更好的是用and方法包一层。遇到这种情况,建议在本地日志中查看SQL并调整,这也是排查Bug时要养成的习惯。
6. 前端Vue页面与接口对接
6.1 创建Vue项目
使用Vite创建Vue 3项目。
npm create vite@latest food-frontend -- --template vue cd food-frontend npm install npm install vue-router@4 axios element-plus npm run dev如果你的网络环境安装依赖比较慢,可以配置镜像源。项目创建成功后,前端目录结构大概是:
food-frontend ├── src │ ├── api │ ├── components │ ├── router │ ├── views │ └── App.vue6.2 路由配置
// 文件路径:src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', redirect: '/home' }, { path: '/home', name: 'Home', component: () => import('../views/Home.vue') }, { path: '/recommend', name: 'Recommend', component: () => import('../views/Recommend.vue') }, { path: '/food/:id', name: 'FoodDetail', component: () => import('../views/FoodDetail.vue') } ] const router = createRouter({ history: createWebHistory(), routes }) export default router6.3 axios请求封装
// 文件路径:src/api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: 'http://localhost:8080/api', timeout: 10000 }) request.interceptors.response.use( response => { // 假设后端返回结构是 { code, message, data } if (response.data.code === 200) { return response.data.data } ElMessage.error(response.data.message || '请求失败') return Promise.reject(new Error(response.data.message)) }, error => { ElMessage.error('网络异常,请检查后端服务是否启动') return Promise.reject(error) } ) export default request6.4 调用推荐接口的API
// 文件路径:src/api/food.js import request from './request' export function getRecommendList(userId) { return request.get('/recommend/list', { params: { userId } }) } export function getFoodList(params) { return request.get('/food/list', { params }) } export function addRating(userId, foodId, score) { return request.post('/rating/add', { userId, foodId, score }) }前端代码不用写得特别复杂,推荐系统前端页面的核心价值在于把后端返回的推荐结果可视化,比如使用图片卡片列表,配上价格和评分,再提供一个“换一批”按钮重新请求推荐接口。
7. 推荐算法核心逻辑实现
这一章是整个项目的加分项,值得认真写。
7.1 推荐流程设计
后端推荐接口的处理流程可以分为以下步骤:
- 获取当前用户ID。
- 查询用户的历史行为数据:评分记录、收藏记录。
- 如果用户没有任何行为,返回热门美食列表。
- 如果用户有行为,提取用户偏好的分类和标签。
- 在偏好分类下查找候选美食,排除已经消费或已经评分的食物。
- 综合计算排序分:偏好相似度、食物评分、热度。
这里的核心思想是:先缩小候选集,再排序。这样做的好处是效率高,逻辑也容易讲清楚。
7.2 推荐服务核心代码
package com.example.food.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.food.entity.Food; import com.example.food.entity.Rating; import com.example.food.mapper.FoodMapper; import com.example.food.mapper.RatingMapper; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Service public class RecommendService { private final FoodMapper foodMapper; private final RatingMapper ratingMapper; public RecommendService(FoodMapper foodMapper, RatingMapper ratingMapper) { this.foodMapper = foodMapper; this.ratingMapper = ratingMapper; } public List<Food> recommend(Long userId, int limit) { // 1. 查询当前用户的评分记录 List<Rating> ratings = ratingMapper.selectList( new LambdaQueryWrapper<Rating>() .eq(Rating::getUserId, userId)); // 2. 冷启动处理:没有评分记录时,返回热门食物 if (ratings.isEmpty()) { return getHotFood(limit); } // 3. 统计用户偏好的分类ID集合:评分>=4的食物分类,出现次数多的优先 Map<Long, Integer> categoryCount = new HashMap<>(); for (Rating rating : ratings) { if (rating.getScore() >= 4) { Food food = foodMapper.selectById(rating.getFoodId()); if (food != null && food.getCategoryId() != null) { categoryCount.put(food.getCategoryId(), categoryCount.getOrDefault(food.getCategoryId(), 0) + 1); } } } // 4. 如果用户没有高分记录,退回到按平均评分推荐 if (categoryCount.isEmpty()) { return getHotFood(limit); } // 5. 按偏好分类出现次数排序,取top分类 List<Long> categoryIds = categoryCount.entrySet().stream() .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) .map(Map.Entry::getKey) .limit(2) .collect(Collectors.toList()); // 6. 在偏好分类下查询候选美食,排除已评分的 List<Long> ratedFoodIds = ratings.stream() .map(Rating::getFoodId) .collect(Collectors.toList()); List<Food> candidates = foodMapper.selectList( new LambdaQueryWrapper<Food>() .in(Food::getCategoryId, categoryIds) .last("limit 50")); List<Food> result = candidates.stream() .filter(food -> !ratedFoodIds.contains(food.getId())) .sorted((f1, f2) -> { // 排序:综合评分高的优先,评分相同看价格低的优先 int compare = f2.getRating().compareTo(f1.getRating()); if (compare != 0) { return compare; } return f1.getPrice().compareTo(f2.getPrice()); }) .limit(limit) .collect(Collectors.toList()); // 7. 如果候选不足,用热门食物补足 if (result.size() < limit) { List<Food> hotFood = getHotFood(limit * 2); for (Food food : hotFood) { if (result.size() >= limit) { break; } if (!result.contains(food) && !ratedFoodIds.contains(food.getId())) { result.add(food); } } } return result; } private List<Food> getHotFood(int limit) { return foodMapper.selectList( new LambdaQueryWrapper<Food>() .orderByDesc(Food::getRating) .last("limit " + limit)); } }这段代码有两个值得在论文里展开的细节:
第一,为什么用分类出现次数作为用户偏好?因为评分数据比较稀疏,用户可能只评过三五次,直接计算标签权重容易过拟合。按分类聚合更稳。
第二,为什么排序时综合评分优先?因为推荐系统的第一阶段目标不是“猜得最准”,而是“不出错”。把热门高分食物推荐出去,用户就算不惊喜,至少不会反感。
7.3 协同过滤的思路(进阶)
如果基础版推荐完成后还有时间,可以在论文里补充一个基于用户协同过滤的改良。思路是:找出和当前用户口味最相似的其他用户,将这些用户评分高的食物推荐给当前用户。
相似度可以使用余弦相似度,把每个用户对食物的评分看作一个向量,向量维度是食物ID,值是对应的评分。毕设实现时可以简化:只取评分>=4的成交记录,用Jaccard系数计算相似度,也就是两个用户共同高分美食数量除以两个用户高分美食集合的并集数量。这么做代码量小,论文里也能说清楚。
7.4 冷启动与兜底策略
冷启动是推荐系统必问的问题。这个系统的方案是:如果用户没有评分记录,就返回综合评分最高的一批食物,并保证来自不同分类,以避免结果过于单一。
另一个兜底策略,是推荐结果不足时用热门食物补齐。这个策略在代码里已经体现,答辩时可以直接从代码里指给评委看,很有说服力。
8. 系统运行与功能验证
8.1 启动后端
在项目根目录执行。
mvn spring-boot:run看到类似下面的日志,就表示启动成功。
Started FoodApplication in 2.3 seconds8.2 启动前端
在food-frontend目录下执行。
npm run dev浏览器访问Vite输出的本地地址,默认是http://localhost:5173。
8.3 功能验证流程
推荐以下测试路径验证系统完整性。
- 注册新用户并登录。
- 浏览美食列表,打开几个美食详情页。
- 对至少5种食物进行评分,其中3种打4分以上。
- 打开推荐页面,观察结果是否包含你打高分的食物所属分类。
- 换一个新注册的空用户,查看推荐结果是否为热门食物列表。
如果步骤4推荐结果里出现了你从未浏览过但分类相似的食材,说明推荐逻辑生效。如果步骤5推荐结果全是评分很高的食物,说明冷启动兜底生效。
8.4 通过接口验证
除了页面验证,也可以直接调用接口测试。
curl "http://localhost:8080/api/recommend/list?userId=1"返回结果中应该是一个JSON数组,数组元素即推荐的美食列表。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 后端启动报数据库连接失败 | MySQL未启动或账号密码错误 | 检查application.yml配置;命令行连接测试 | 确认MySQL已启动,修正url/username/password |
| 前端页面请求接口404 | 后端接口路径不匹配 | 打开浏览器F12控制台,查看请求URL | 核对Controller的RequestMapping与实际请求路径;注意baseURL |
| 使用MyBatis-Plus报Invalid value type错误 | 依赖版本与SpringBoot不兼容 | 查看启动日志中的Caused by | 降低或升级MyBatis-Plus版本,统一使用Spring Boot BOM管理 |
| 推荐结果总是热门食物 | 当前用户没有评分记录;推荐代码里categoryCount为空 | 查t_rating表确认数据;在后端打印日志 | 给测试用户补充评分数据;调整冷启动判断逻辑 |
| 前端跨域报错 | 后端未配置CORS | 查看浏览器控制台CORS错误 | 在Controller添加@CrossOrigin,或配置全局CorsFilter |
| 分页查询返回总数为0 | MyBatis-Plus分页插件未配置 | 检查是否有MybatisPlusInterceptor的Bean | 添加分页插件配置类 |
| 中文乱码 | 数据库连接未指定utf8 | 检查url中characterEncoding参数 | 使用characterEncoding=utf8并保证表为utf8mb4 |
这里有一个容易忽略的问题:如果前后端在不同端口运行,比如前端5173、后端8080,跨域问题是无法避免的。最快的方式是在后端写一个全局CORS配置,或者直接在Controller上加@CrossOrigin。如果要写进论文,使用CorsFilter配置类会显得更规范。
package com.example.food.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .maxAge(3600); } }10. 论文写作与答辩准备建议
10.1 论文目录结构
毕业论文一般包含六到七章。这个系统的论文章节可以这样安排。
- 第一章 绪论:选题背景、研究意义、国内外研究现状、论文结构。
- 第二章 相关技术介绍:SpringBoot、Vue、MyBatis-Plus、推荐算法。
- 第三章 系统需求分析:功能需求、非功能需求、用例分析。
- 第四章 系统设计:总体架构、功能模块设计、数据库设计、推荐算法设计。
- 第五章 系统实现:各模块的核心代码与界面截图。
- 第六章 系统测试:功能测试用例、测试结果、推荐效果分析。
- 第七章 总结与展望:项目总结、不足、改进方向。
10.2 开题报告的写作侧重点
开题报告一般不需要写代码,重点是研究内容和方法。针对这个题目,研究内容可以写以下四点:用户行为数据采集与存储设计、基于分类标签的用户偏好建模、冷启动推荐策略、前后端分离架构下的系统集成。
研究方法可以写:文献研究法、原型设计法、对比实验法。最好在题目中明确指出“使用基于内容的推荐算法”或“融合多种推荐策略”,把算法写进标题会让研究内容更聚焦。
10.3 答辩高频问题
根据往年经验,评委老师最爱问以下几个角度的问题。
- 你的推荐算法为什么选择基于内容的推荐?
- 冷启动问题怎么解决?
- 用户没有评分数据时系统怎么办?
- 推荐效果用什么指标衡量?
- 数据量增大时系统的性能瓶颈在哪里?
回答这些问题时要直接引用自己的设计和代码。比如问“为什么不用协同过滤”,可以回答:“协同过滤依赖足够的用户评分数据,而校园场景下的新用户评分非常稀疏,所以我先使用基于内容推荐作为主策略,将协同过滤作为后续扩展方向。”这个回答既合理又诚实。
11. 总结与后续学习方向
这套校园周边美食推荐系统,从功能角度看,它覆盖了用户、商家、管理员三类角色,完成了美食浏览、评分、收藏、评论和推荐的主要链路。从毕设角度看,它的架构清晰、算法有讲解空间、测试路径完整,能在开题报告和论文中找到对应的内容支撑。
如果你打算继续深入,推荐的改进方向有三个:第一是把推荐策略从“基于内容推荐”升级为“基于内容+协同过滤的混合推荐”,让个性化效果更好;第二是加入Redis缓存,把热门美食列表和推荐结果缓存起来,提升接口响应速度;第三是增加用户行为埋点,把浏览时长、点击次数也纳入偏好计算,让推荐结果更准确。
实际做项目时,不要一上来就追求代码量。先把用户行为数据这个基础建好,再围绕数据设计推荐逻辑,最后用页面展示效果。这个开发顺序,比什么都重要。建议收藏这篇文章,动手开发时对照着搭项目,能省下不少排查问题的时间。