目录

一、项目背景

二、技术介绍

三、功能介绍

四、代码设计

五、系统实现


一、项目背景

在移动互联网与社交媒体深度渗透人们日常生活的当下,美食文化已成为现代城市生活中不可或缺的重要组成部分。随着大众消费水平的不断提升与生活节奏的日益加快,外出就餐、美食打卡已成为年轻人社交娱乐与品质生活的主流方式。据统计,我国餐饮市场规模已突破4万亿元,餐饮用户数量持续攀升,消费者对于美食信息获取的需求呈现出多元化、个性化、即时化的显著特征。

然而,面对海量的餐饮信息与层出不穷的网红餐厅,消费者在美食选择过程中普遍面临着“信息过载”的困境。一方面,传统的美食推荐平台多采用大众评分、距离排序等单一维度进行筛选,难以精准匹配用户的个性化口味偏好、消费预算、用餐场景等差异化需求;另一方面,大量优质的本土特色餐厅因缺乏有效的曝光渠道,难以触达目标消费群体,导致供需双方信息不对称问题突出。此外,现有平台普遍存在推荐结果同质化严重、评价真实性难以保障、交互体验欠佳等痛点,无法为用户提供真正贴心、高效的美食发现体验。

随着大数据技术与人工智能算法的快速发展,个性化推荐系统已成为解决信息过载问题的有效手段。通过分析用户的历史行为、口味偏好、社交关系等多维数据,构建用户画像与菜品特征模型,能够实现“千人千面”的精准推荐,显著提升用户的美食探索体验。同时,前后端分离的架构模式使得系统开发更加灵活高效,前端Vue框架能够构建流畅友好的用户交互界面,后端SpringBoot框架则提供了稳定可靠的服务支撑。

基于此,本课题拟设计并实现一套基于SpringBoot+Vue前后端分离架构的美食推荐系统。该系统将集成协同过滤推荐算法与内容推荐算法,结合用户的地理位置、消费记录、口味标签等信息,为用户提供个性化的餐厅与菜品推荐服务。系统还将包含餐厅信息管理、用户评价、美食社区、收藏分享等核心功能模块,致力于打造一个集美食发现、社交互动、信息管理于一体的综合性美食服务平台。该系统的开发与应用,对于提升用户美食消费体验、助力中小餐饮商家精准获客、推动餐饮行业数字化转型具有重要的理论价值与现实意义。

二、技术介绍

本系统采用前后端分离的架构模式进行设计与开发,前端基于Vue.js框架构建用户界面,后端基于SpringBoot框架提供RESTful API服务,前后端通过JSON格式进行数据交互,实现界面与业务逻辑的解耦,提高系统的可维护性与扩展性。

后端技术栈方面,系统以SpringBoot为核心框架,利用其自动配置与起步依赖特性,简化项目搭建与开发流程。SpringBoot提供了内嵌的Servlet容器,使得应用可以独立运行,无需部署WAR包。持久层选用MyBatis作为ORM框架,通过XML或注解的方式编写SQL语句,实现灵活的数据库操作。MyBatis支持定制化SQL、存储过程以及高级映射,能够满足复杂业务场景下的数据访问需求。数据库采用MySQL关系型数据库,MySQL具有性能稳定、开源免费、支持海量数据存储等优势,适合作为中小型Web应用的数据存储方案。

前端采用Vue.js渐进式JavaScript框架进行开发。Vue采用MVVM设计模式,通过响应式的数据绑定与组件化开发机制,能够高效构建交互丰富的单页面应用。Vue Router负责前端路由管理,实现页面间的无刷新跳转;Vuex作为状态管理工具,统一维护用户登录状态、全局数据等共享信息,确保数据流向清晰可控。

在系统架构上,严格遵循MVC分层设计原则,将Controller控制层、Service业务层、Mapper持久层各司其职,代码结构清晰规范。通过合理的分层设计,系统具备良好的可测试性与可维护性,为后续功能扩展与迭代升级奠定了坚实基础。

三、功能介绍

1. 用户认证与个人中心
登录注册:用户登录、注册
个人中心:用户信息管理、个人资料维护

2. 美食资讯与社区
美食资讯:查看最新美食相关文章
论坛交流:用户发帖、回帖、讨论美食话题

3. 美食商城
美食分类:按分类浏览美食商品
商品展示:查看美食商品详情
购物车:添加商品、管理购物车
订单管理:确认订单、查看订单列表
地址管理:收货地址的增删改查

4. 社交功能
收藏功能:收藏喜欢的商品或内容
好友聊天:添加好友、实时聊天
客服系统:与客服沟通

5. 系统功能
操作日志:查看用户操作记录
友情链接:合作伙伴链接展示


二、后台管理系统
1. 系统管理
管理员管理:管理员账号的增删改查
操作日志:系统操作记录查看
轮播图配置:首页轮播图管理

2. 用户管理
用户管理:注册用户信息管理
管理员个人中心:修改密码、个人信息

3. 内容管理
美食资讯管理:资讯文章的发布与管理
论坛管理:用户发帖内容管理
评论管理:论坛和商城的评论审核

4. 商品管理
美食分类管理:商品分类的维护
美食商城管理:商品上下架、信息编辑
订单管理:订单处理、状态更新

5. 系统配置
友情链接管理:合作伙伴链接维护
客服聊天管理:客服对话监控

四、代码设计

// RecommendationService.java
package com.food.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.food.dto.RecommendRequestDTO;
import com.food.entity.Recommendation;
import com.food.entity.Restaurant;
import com.food.entity.UserBehavior;
import com.food.mapper.RecommendationMapper;
import com.food.mapper.RestaurantMapper;
import com.food.mapper.UserBehaviorMapper;
import com.food.vo.RestaurantVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class RecommendationService {
    
    private final RestaurantMapper restaurantMapper;
    private final UserBehaviorMapper userBehaviorMapper;
    private final RecommendationMapper recommendationMapper;
    
    // 行为类型权重
    private static final Map<Integer, Double> BEHAVIOR_WEIGHT = new HashMap<>();
    static {
        BEHAVIOR_WEIGHT.put(1, 0.5);  // 浏览
        BEHAVIOR_WEIGHT.put(2, 2.0);  // 收藏
        BEHAVIOR_WEIGHT.put(3, 1.5);  // 点赞
        BEHAVIOR_WEIGHT.put(4, 3.0);  // 评分
        BEHAVIOR_WEIGHT.put(5, 1.2);  // 分享
    }
    
    /**
     * 混合推荐算法:协同过滤 + 基于内容推荐 + 热门推荐
     */
    public List<RestaurantVO> getHybridRecommendations(RecommendRequestDTO request) {
        Long userId = request.getUserId();
        String city = request.getCity();
        int limit = request.getPageSize();
        
        // 1. 获取用户已有行为数据
        List<Long> viewedRestaurants = userBehaviorMapper.selectUserViewedRestaurants(userId);
        List<Long> favoriteRestaurants = userBehaviorMapper.selectUserFavoriteRestaurants(userId, 10);
        
        Map<Long, Double> candidateScores = new HashMap<>();
        
        // 2. 协同过滤推荐(基于用户相似度)
        if (!favoriteRestaurants.isEmpty()) {
            Map<Long, Double> cfScores = collaborativeFiltering(userId, favoriteRestaurants);
            cfScores.forEach((id, score) -> 
                candidateScores.merge(id, score * 0.4, Double::sum));
        }
        
        // 3. 基于内容的推荐(基于用户偏好标签)
        Map<Long, Double> contentScores = contentBasedRecommendation(userId, viewedRestaurants);
        contentScores.forEach((id, score) -> 
            candidateScores.merge(id, score * 0.3, Double::sum));
        
        // 4. 热门推荐(基于评分和评论数)
        Map<Long, Double> hotScores = hotRecommendation(city);
        hotScores.forEach((id, score) -> 
            candidateScores.merge(id, score * 0.2, Double::sum));
        
        // 5. 地理位置推荐
        Map<Long, Double> locationScores = locationBasedRecommendation(userId, city);
        locationScores.forEach((id, score) -> 
            candidateScores.merge(id, score * 0.1, Double::sum));
        
        // 6. 过滤已浏览过的餐厅
        Set<Long> viewedSet = new HashSet<>(viewedRestaurants);
        candidateScores.entrySet().removeIf(entry -> viewedSet.contains(entry.getKey()));
        
        // 7. 排序并返回Top N
        List<Long> recommendedIds = candidateScores.entrySet().stream()
            .sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
            .limit(limit)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
        
        // 8. 保存推荐记录
        saveRecommendations(userId, recommendedIds, candidateScores);
        
        // 9. 获取餐厅详情并返回
        return getRestaurantDetails(recommendedIds);
    }
    
    /**
     * 协同过滤推荐:找到相似用户喜欢的餐厅
     */
    private Map<Long, Double> collaborativeFiltering(Long userId, List<Long> favoriteRestaurants) {
        Map<Long, Double> scores = new HashMap<>();
        
        // 查询与该用户有相似喜好的其他用户
        LambdaQueryWrapper<UserBehavior> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(UserBehavior::getRestaurantId, favoriteRestaurants)
                .ne(UserBehavior::getUserId, userId)
                .eq(UserBehavior::getBehaviorType, 2); // 收藏行为
        
        List<UserBehavior> similarBehaviors = userBehaviorMapper.selectList(wrapper);
        
        // 统计相似用户喜欢的餐厅
        Map<Long, Integer> restaurantCount = new HashMap<>();
        for (UserBehavior behavior : similarBehaviors) {
            restaurantCount.merge(behavior.getRestaurantId(), 1, Integer::sum);
        }
        
        // 计算推荐分数
        for (Map.Entry<Long, Integer> entry : restaurantCount.entrySet()) {
            double score = entry.getValue() / (double) favoriteRestaurants.size();
            scores.put(entry.getKey(), score);
        }
        
        return scores;
    }
    
    /**
     * 基于内容的推荐:根据用户历史偏好的标签和菜系
     */
    private Map<Long, Double> contentBasedRecommendation(Long userId, List<Long> viewedRestaurants) {
        Map<Long, Double> scores = new HashMap<>();
        
        if (viewedRestaurants.isEmpty()) {
            return scores;
        }
        
        // 获取用户历史浏览的餐厅
        List<Restaurant> viewed = restaurantMapper.selectBatchIds(viewedRestaurants);
        
        // 统计用户偏好的菜系和标签
        Map<String, Integer> categoryPreference = new HashMap<>();
        Map<String, Integer> tagPreference = new HashMap<>();
        
        for (Restaurant restaurant : viewed) {
            categoryPreference.merge(restaurant.getCategory(), 1, Integer::sum);
            if (restaurant.getTags() != null) {
                String[] tags = restaurant.getTags().split(",");
                for (String tag : tags) {
                    tagPreference.merge(tag.trim(), 1, Integer::sum);
                }
            }
        }
        
        // 获取所有餐厅进行匹配
        List<Restaurant> allRestaurants = restaurantMapper.selectList(
            new LambdaQueryWrapper<Restaurant>().eq(Restaurant::getStatus, 1)
        );
        
        for (Restaurant restaurant : allRestaurants) {
            double score = 0.0;
            
            // 菜系匹配度
            Integer categoryCount = categoryPreference.getOrDefault(restaurant.getCategory(), 0);
            score += categoryCount * 0.6;
            
            // 标签匹配度
            if (restaurant.getTags() != null) {
                String[] tags = restaurant.getTags().split(",");
                for (String tag : tags) {
                    Integer tagCount = tagPreference.getOrDefault(tag.trim(), 0);
                    score += tagCount * 0.4;
                }
            }
            
            if (score > 0) {
                scores.put(restaurant.getId(), score);
            }
        }
        
        return scores;
    }
    
    /**
     * 热门推荐:基于综合评分和评论数量
     */
    private Map<Long, Double> hotRecommendation(String city) {
        Map<Long, Double> scores = new HashMap<>();
        
        LambdaQueryWrapper<Restaurant> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Restaurant::getStatus, 1);
        if (city != null && !city.isEmpty()) {
            wrapper.eq(Restaurant::getCity, city);
        }
        wrapper.orderByDesc(Restaurant::getScore, Restaurant::getReviewCount);
        wrapper.last("LIMIT 50");
        
        List<Restaurant> hotRestaurants = restaurantMapper.selectList(wrapper);
        
        for (Restaurant restaurant : hotRestaurants) {
            double score = restaurant.getScore().doubleValue() * 0.6 + 
                          Math.log(restaurant.getReviewCount() + 1) * 0.4;
            scores.put(restaurant.getId(), score);
        }
        
        return scores;
    }
    
    /**
     * 地理位置推荐
     */
    private Map<Long, Double> locationBasedRecommendation(Long userId, String city) {
        Map<Long, Double> scores = new HashMap<>();
        
        // 获取用户所在城市的餐厅
        LambdaQueryWrapper<Restaurant> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Restaurant::getStatus, 1);
        if (city != null && !city.isEmpty()) {
            wrapper.eq(Restaurant::getCity, city);
        }
        wrapper.orderByDesc(Restaurant::getScore);
        wrapper.last("LIMIT 30");
        
        List<Restaurant> nearby = restaurantMapper.selectList(wrapper);
        
        for (Restaurant restaurant : nearby) {
            // 基于评分的简单地理位置推荐
            double score = restaurant.getScore().doubleValue() / 5.0;
            scores.put(restaurant.getId(), score);
        }
        
        return scores;
    }
    
    /**
     * 保存推荐记录
     */
    @Transactional
    public void saveRecommendations(Long userId, List<Long> restaurantIds, 
                                     Map<Long, Double> scores) {
        // 先删除旧的未点击推荐记录
        LambdaQueryWrapper<Recommendation> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Recommendation::getUserId, userId)
               .eq(Recommendation::getIsClick, 0);
        recommendationMapper.delete(wrapper);
        
        // 保存新的推荐记录
        for (Long restaurantId : restaurantIds) {
            Recommendation recommendation = new Recommendation();
            recommendation.setUserId(userId);
            recommendation.setRestaurantId(restaurantId);
            recommendation.setRecommendType(1); // 混合推荐
            recommendation.setScore(BigDecimal.valueOf(scores.getOrDefault(restaurantId, 0.0))
                .setScale(4, RoundingMode.HALF_UP));
            recommendation.setIsClick(0);
            recommendation.setCreateTime(LocalDateTime.now());
            recommendationMapper.insert(recommendation);
        }
    }
    
    /**
     * 记录用户点击推荐
     */
    @Transactional
    public void recordClick(Long recommendationId) {
        recommendationMapper.markAsClicked(recommendationId);
    }
    
    /**
     * 获取餐厅详情(包含推荐菜品)
     */
    private List<RestaurantVO> getRestaurantDetails(List<Long> restaurantIds) {
        if (restaurantIds.isEmpty()) {
            return new ArrayList<>();
        }
        
        List<Restaurant> restaurants = restaurantMapper.selectBatchIds(restaurantIds);
        
        return restaurants.stream()
            .map(this::convertToVO)
            .sorted((a, b) -> b.getScore().compareTo(a.getScore()))
            .collect(Collectors.toList());
    }
    
    private RestaurantVO convertToVO(Restaurant restaurant) {
        RestaurantVO vo = new RestaurantVO();
        vo.setId(restaurant.getId());
        vo.setName(restaurant.getName());
        vo.setCategory(restaurant.getCategory());
        vo.setPriceLevel(restaurant.getPriceLevel());
        vo.setAvgPrice(restaurant.getAvgPrice());
        vo.setAddress(restaurant.getAddress());
        vo.setCity(restaurant.getCity());
        vo.setPhone(restaurant.getPhone());
        vo.setBusinessHours(restaurant.getBusinessHours());
        vo.setDescription(restaurant.getDescription());
        vo.setImageUrl(restaurant.getImageUrl());
        vo.setTags(restaurant.getTags());
        vo.setScore(restaurant.getScore());
        vo.setReviewCount(restaurant.getReviewCount());
        
        // 设置价格等级描述
        String[] levels = {"", "平价", "中等", "中高档", "高档"};
        vo.setPriceLevelDesc(levels[restaurant.getPriceLevel()]);
        
        return vo;
    }
}

五、系统实现

更多推荐