温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片!
1. 项目背景与意义
随着旅游业的快速发展和人们生活水平的不断提高,自助游、自由行逐渐成为主流出行方式。然而,面对海量的景点信息和参差不齐的攻略内容,游客往往难以在短时间内筛选出符合自身兴趣和行程安排的景点,容易出现“选择困难”和“信息过载”的问题。
传统的旅游网站多以静态信息展示为主,缺乏针对用户个性化需求的智能推荐能力。因此,设计并实现一个基于SpringBoot的景点旅游推荐系统,具有重要的现实意义:
- 提升用户体验:根据用户的浏览历史、收藏行为和评分记录,主动推荐符合其偏好的景点,减少搜索成本。
- 促进旅游消费:精准推荐能够有效激发用户的出行意愿,带动景区门票、酒店住宿、餐饮等周边消费。
- 数据驱动运营:系统积累的用户行为数据可为景区管理和旅游平台运营提供决策支持。
- 技术实践价值:项目涵盖前后端分离、数据库设计、推荐算法、接口开发等完整技术链路,适合作为毕业设计或课程综合实践。
2. 系统技术栈
本系统采用前后端分离架构,后端基于SpringBoot框架构建,前端使用Vue.js配合Element UI组件库,数据库选用MySQL,并集成Redis作为缓存层以提升推荐接口的响应速度。
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | SpringBoot 2.7.x | 快速构建RESTful API,内置Tomcat容器 |
| 持久层 | MyBatis-Plus | 简化数据库操作,支持分页查询和条件构造器 |
| 数据库 | MySQL 8.0 | 存储用户、景点、评论、收藏等核心业务数据 |
| 缓存 | Redis | 缓存热门景点列表和用户推荐结果 |
| 前端框架 | Vue.js 3 + Element UI | 实现单页应用,提供友好的交互界面 |
| 推荐算法 | 基于用户的协同过滤 | 根据相似用户的评分行为生成个性化推荐 |
| 构建工具 | Maven | 管理项目依赖和构建流程 |
3. 系统功能模块设计
系统整体划分为前台用户端和后台管理端两大模块,具体功能如下:
3.1 用户端功能
- 用户注册与登录:支持用户名密码登录,使用JWT令牌维持会话状态。
- 景点浏览与搜索:按地区、类型、热度等条件筛选景点,支持关键词模糊搜索。
- 景点详情查看:展示景点介绍、图片、开放时间、门票价格、用户评分等信息。
- 收藏与评分:用户可收藏感兴趣的景点,并对游览过的景点进行评分和评论。
- 个性化推荐:系统根据用户的历史行为,在首页推荐可能感兴趣的景点。
3.2 管理端功能
- 景点管理:管理员可对景点信息进行增删改查,上传景点图片。
- 用户管理:查看用户列表,禁用违规账号。
- 评论审核:审核用户提交的评论内容,删除不当言论。
- 数据统计:展示景点访问量、收藏量、评分分布等统计图表。
4. 数据库设计
系统核心数据表包括用户表、景点表、收藏表、评分表和评论表。以下为主要表结构说明:
| 表名 | 主要字段 | 说明 |
|---|---|---|
| user | id, username, password, nickname, avatar, role | 用户基本信息,role区分普通用户和管理员 |
| scenic | id, name, city, type, description, image, price, open_time | 景点基础信息 |
| favorite | id, user_id, scenic_id, create_time | 用户收藏记录 |
| rating | id, user_id, scenic_id, score, comment, create_time | 用户评分与评论 |
5. 核心代码实现
5.1 SpringBoot启动类
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TravelRecommendApplication { public static void main(String[] args) { SpringApplication.run(TravelRecommendApplication.class, args); } }5.2 景点实体类
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("scenic") public class Scenic { @TableId(type = IdType.AUTO) private Long id; private String name; private String city; private String type; private String description; private String image; private BigDecimal price; private String openTime; }5.3 景点查询接口
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @RequestMapping("/api/scenic") public class ScenicController { @Resource private ScenicMapper scenicMapper; @GetMapping("/list") public Page<Scenic> list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String city, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<Scenic> wrapper = new LambdaQueryWrapper<>(); if (city != null && !city.isEmpty()) { wrapper.eq(Scenic::getCity, city); } if (keyword != null && !keyword.isEmpty()) { wrapper.like(Scenic::getName, keyword); } return scenicMapper.selectPage(new Page<>(page, size), wrapper); } }5.4 基于用户的协同过滤推荐算法
import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.*; import java.util.stream.Collectors; @Service public class RecommendService { @Resource private RatingMapper ratingMapper; @Resource private ScenicMapper scenicMapper; /** 基于用户的协同过滤推荐 */ public List<Scenic> recommendForUser(Long userId, int topN) { // 1. 获取所有评分记录 List<Rating> allRatings = ratingMapper.selectList(null); // 2. 构建 用户-景点 评分矩阵 Map<Long, Map<Long, Integer>> userScenicMap = new HashMap<>(); for (Rating rating : allRatings) { userScenicMap .computeIfAbsent(rating.getUserId(), k -> new HashMap<>()) .put(rating.getScenicId(), rating.getScore()); } // 3. 计算当前用户与其他用户的相似度(余弦相似度) Map<Long, Double> similarityMap = new HashMap<>(); Map<Long, Integer> currentUserRatings = userScenicMap.getOrDefault(userId, Collections.emptyMap()); for (Map.Entry<Long, Map<Long, Integer>> entry : userScenicMap.entrySet()) { Long otherUserId = entry.getKey(); if (otherUserId.equals(userId)) { continue; } Map<Long, Integer> otherRatings = entry.getValue(); double similarity = cosineSimilarity(currentUserRatings, otherRatings); if (similarity > 0) { similarityMap.put(otherUserId, similarity); } } // 4. 根据相似用户评分预测当前用户对未浏览景点的评分 Map<Long, Double> predictScoreMap = new HashMap<>(); for (Map.Entry<Long, Double> simEntry : similarityMap.entrySet()) { Long otherUserId = simEntry.getKey(); double sim = simEntry.getValue(); Map<Long, Integer> otherRatings = userScenicMap.get(otherUserId); for (Map.Entry&lt;Long, Integer&gt; ratingEntry : otherRatings.entrySet()) { Long scenicId = ratingEntry.getKey(); if (currentUserRatings.containsKey(scenicId)) { continue; } predictScoreMap.merge( scenicId, sim * ratingEntry.getValue(), Double::sum ); } } // 5. 按预测评分降序排序,取前N个景点 return predictScoreMap.entrySet().stream() .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(topN) .map(entry -> scenicMapper.selectById(entry.getKey())) .filter(Objects::nonNull) .collect(Collectors.toList()); } /** 计算两个用户评分向量的余弦相似度 */ private double cosineSimilarity(Map<Long, Integer> userA, Map<Long, Integer> userB) { Set<Long> commonScenics = new HashSet<>(userA.keySet()); commonScenics.retainAll(userB.keySet()); if (commonScenics.isEmpty()) { return 0.0; } double dotProduct = 0.0; double normA = 0.0; double normB = 0.0; for (Long scenicId : commonScenics) { dotProduct += userA.get(scenicId) * userB.get(scenicId); } for (Integer score : userA.values()) { normA += score * score; } for (Integer score : userB.values()) { normB += score * score; } if (normA == 0 || normB == 0) { return 0.0; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } }5.5 推荐接口
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.List; @RestController @RequestMapping("/api/recommend") public class RecommendController { @Resource private RecommendService recommendService; @GetMapping("/{userId}") public List<Scenic> recommend(@PathVariable Long userId) { return recommendService.recommendForUser(userId, 10); } }6. 系统测试与总结
通过对系统各功能模块进行接口测试和页面功能验证,景点浏览、搜索、收藏、评分以及个性化推荐等功能均能正常运行。推荐模块在用户评分数据较为充足时,能够给出较为合理的景点推荐结果。
本系统基于SpringBoot实现了景点旅游推荐系统的核心功能,涵盖了用户管理、景点管理、评分收藏和协同过滤推荐等模块,具备一定的实用性和扩展性。后续可进一步引入基于内容的推荐算法、热门景点实时排行以及基于地理位置的服务,以提升推荐效果和用户体验。