ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot景点文化推荐系统毕设全解析:从数据库设计到推荐算法

SpringBoot景点文化推荐系统毕设全解析:从数据库设计到推荐算法 又到了毕设选题和开发的冲刺阶段每年这个时候后台问得最多的一类问题就是“能不能推荐一个既不太难、又有技术含量、还能把论文写得丰满的 SpringBoot 项目”如果你也有同样的需求同时想避开那种满大街都是的“图书管理”“学生管理系统”那么本文介绍的“河南特色景点文化推荐系统”会是一个非常合适的选择。这套系统表面上看是一个景区门户网站但底子是一个完整的 SpringBoot 前后端分离项目带用户、景点、文化标签、收藏、评论、浏览记录还加入了基于用户行为的个性化推荐逻辑。相比普通 CRUD 系统它在技术点上多了“算法”和“数据驱动”两层内容无论做毕业设计还是写进简历里的项目经验都能比同龄人更有说服力。本文不会只贴一张截图而是把从需求分析、数据库设计、核心代码到推荐算法实现、部署上线、论文写作的完整链路拆开讲清楚。如果你正准备用 SpringBoot 做毕设这篇文章值得先收藏再读。1. 背景与核心概念1.1 为什么做“景点文化推荐系统”而不是普通旅游网站很多旅游类小系统本质上是信息展示平台管理员录入景点数据用户浏览列表、查看详情。它的数据库结构不复杂核心功能也只有增删改查答辩时几乎无话可说。而“文化推荐”这四个字把系统从“信息展示”拉升到了“数据服务”层面。河南本身是一个文旅资源非常密集的省份景点背后往往沉淀着很强的文化属性洛阳龙门石窟代表石窟艺术开封清明上河园代表宋代市井文化安阳殷墟代表甲骨文与商文明登封少林寺代表禅武文化。如果系统只把“位置”和“门票价格”存进数据库就丢了灵魂。所以这个项目的第一个理念是把“文化标签”作为景点数据的核心字段。每个景点关联一组标签例如“历史遗迹”“民俗体验”“红色旅游”“自然山水”“美食小吃”等。用户在注册和浏览过程中系统会逐步收集他对这些标签的偏好最后通过推荐算法输出“猜你喜欢”的结果。这在业务层面可以这样理解一个对“历史遗迹”感兴趣的用户访问龙门石窟后系统应该给他推荐殷墟、商丘古城这类景点而不是推荐只有自然风光但文化属性很弱的景区。这个逻辑听起来很直觉但在传统 CRUD 系统里是完全做不到的。1.2 推荐系统在当前项目里属于什么层次先给一个心理预期毕设项目不需要做出抖音那种级别的推荐引擎也不需要引入 Spark、Flink 这类大数据组件。我们只需要在 SpringBoot 项目中实现一个“基于内容Content-Based”的轻量级推荐逻辑就足够覆盖题目中的“推荐”两字。基于内容的推荐核心只有三步给每个景点打上标签向量根据用户历史行为浏览、收藏、评论统计用户标签偏好向量计算用户向量与景点向量之间的余弦相似度按相似度排序输出推荐列表。这套算法不需要额外安装中间件用 Java 集合操作就能实现。数据量在万级别以内性能完全够用。答辩时如果老师问“为什么不用协同过滤”你可以回答协同过滤依赖大量用户行为矩阵在小规模新上线系统中存在冷启动问题而基于内容的推荐可以针对单个用户立即产生推荐结果更适合文化和旅游类垂直场景。这样回答反而体现了你真正思考过选型。1.3 系统的整体业务结构作为一套完整的毕设系统它的业务范围需要覆盖前台和后台两条线前台用户端用户注册、登录、个人信息维护景点列表、景点搜索、景点详情展示文化标签筛选按历史遗迹、自然山水等筛选景点收藏、点赞、评论系统根据用户行为生成个性化推荐列表个人中心查看我的收藏、我的评论、浏览足迹。后台管理端管理员登录景点信息管理增删改查、图片上传、标签维护标签管理用户管理评论管理可以删除违规评论浏览与收藏数据统计。在这样的结构下项目至少涉及用户表、景点表、标签表、景点-标签关联表、收藏表、评论表、浏览日志表、管理员表八张核心表。我们会在第 3 节详细设计。2. 技术栈选型与项目架构2.1 2026 年前后端技术栈参考技术栈的选型要遵循一个基本原则既要体现主流性也要保证本地环境能跑起来。不需要盲目追逐最新版本但也不能用十年前的技术。下面这套组合是当前 SpringBoot 毕设项目里比较稳健的配置层次技术选型说明后端框架Spring Boot 2.7.x 或 3.x2.7 兼容性更稳3.x 要求 JDK17持久层MyBatis-Plus弱化 SQL 编写内置分页插件数据库MySQL 8.0支持 JSON 字段性能好缓存Redis 5.x / 6.x用于缓存热点景点、存储验证码等安全框架Spring Security 或 Sa-Token毕设首选 Sa-TokenAPI 简单前端Vue 3 Element Plus Axios前后端分离构建工具Maven主流构建工具部署本地 Jar 包 Nginx 反向代理也可用 Docker 打包这里需要说明一下版本问题Spring Boot 3.x 对 JDK 版本有硬性要求JDK 17如果你的电脑还是 JDK 8建议直接使用 Spring Boot 2.7.x JDK 8 的组合MySQL 8.0 和 MyBatis-Plus 都可以兼容。本文后面的代码示例以 Spring Boot 2.7 风格为主如果使用 3.x只需要把javax.*包替换为jakarta.*即可。2.2 项目整体架构项目采用前后端分离架构后端只提供 RESTful API前端通过 Axios 调用接口。整体请求流程如下前端页面Vue3 - Nginx静态资源服务 - 后端 Controller - Service - Mapper - MySQL / Redis对应后端工程的包结构如下src/main/java/com/henan/travel ├── config // 配置类跨域、MyBatis-Plus分页插件、Redis配置 ├── controller // 接口层收参、调Service、返回统一结果 ├── service // 业务层实现具体业务和推荐算法 ├── mapper // MyBatis-Plus Mapper接口 ├── entity // 数据库实体类对应表结构 ├── dto // 前端参数对象 ├── vo // 返回给前端的数据对象 ├── common // 统一状态码、统一返回类、异常处理 └── utils // 工具类如JWT工具如果不会 Spring Security或者觉得配置繁琐推荐使用 Sa-Token 做登录认证。Sa-Token 的 API 设计非常简单登录后调用StpUtil.login(userId)鉴权时调用StpUtil.checkLogin()半天就能接完。省下来的时间可以用到推荐算法和前端联调上。2.3 统一响应结构设计后端接口返回给前端的时候不建议直接返回实体对象而是统一包一层 Result这样前端不管遇到成功还是失败都能用同一个代码逻辑处理。// 文件路径src/main/java/com/henan/travel/common/Result.java public class ResultT { private Integer code; // 200 表示成功500 表示失败 private String message; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } // 省略 getter / setter }下面所有接口的返回都会使用这个结构例如查询景点详情时返回{ code: 200, message: 操作成功, data: { id: 1, name: 龙门石窟, content: 世界文化遗产位于洛阳市……, tags: [历史遗迹, 世界遗产] } }3. 数据库设计与核心表结构3.1 数据库概念模型设计在写代码之前先把数据库表设计清楚。这里我给出一个适合毕设的物理表结构直接复制到 Navicat 执行即可。景点表scenic_spot是最核心的表包含景点名称、简介、详细内容、图片、区域、开放时间、门票价格等字段此外还有一个click_count字段用于记录浏览量方便做热门景点排行。标签表tag保存景点文化标签例如“历史遗迹”“自然山水”“红色旅游”“民俗体验”等。景点与标签是多对多关系通过中间表scenic_tag关联。用户表sys_user保存账号、密码、昵称、头像。密码必须加密存储推荐使用 BCrypt 算法。行为表包括collect_record收藏、comment_record评论、browse_record浏览日志。这三张表是推荐算法的数据来源。3.2 核心建表 SQL-- 用户表 CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, create_time datetime DEFAULT NULL COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 景点表 CREATE TABLE scenic_spot ( id bigint NOT NULL AUTO_INCREMENT COMMENT 景点ID, name varchar(100) NOT NULL COMMENT 景点名称, summary varchar(500) DEFAULT NULL COMMENT 景点简介, content text COMMENT 景点详细介绍, cover_image varchar(255) DEFAULT NULL COMMENT 封面图片, region varchar(50) DEFAULT NULL COMMENT 所属地市, open_time varchar(100) DEFAULT NULL COMMENT 开放时间, ticket_price decimal(10,2) DEFAULT 0.00 COMMENT 门票价格, click_count int DEFAULT 0 COMMENT 浏览量, status tinyint DEFAULT 1 COMMENT 状态 1上架 0下架, create_time datetime DEFAULT NULL COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点表; -- 标签表 CREATE TABLE tag ( id bigint NOT NULL AUTO_INCREMENT COMMENT 标签ID, name varchar(50) NOT NULL COMMENT 标签名称, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点标签表; -- 景点标签关联表 CREATE TABLE scenic_tag ( id bigint NOT NULL AUTO_INCREMENT, scenic_id bigint NOT NULL COMMENT 景点ID, tag_id bigint NOT NULL COMMENT 标签ID, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点标签关联表; -- 收藏记录表 CREATE TABLE collect_record ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, scenic_id bigint NOT NULL COMMENT 景点ID, create_time datetime DEFAULT NULL COMMENT 收藏时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏记录表; -- 评论表 CREATE TABLE comment_record ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, scenic_id bigint NOT NULL COMMENT 景点ID, content varchar(500) DEFAULT NULL COMMENT 评论内容, create_time datetime DEFAULT NULL COMMENT 评论时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点评论表; -- 浏览日志表 CREATE TABLE browse_record ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, scenic_id bigint NOT NULL COMMENT 景点ID, browse_time datetime DEFAULT NULL COMMENT 浏览时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT浏览记录表;3.3 标签在推荐算法中的作用标签是整个推荐系统的“语义桥梁”。一个景点如果没有标签在推荐算法中就是“不可描述”的数据。设计标签时要注意两点第一标签粒度要适中不要细分到“龙门石窟佛像雕刻艺术”这种只有单个景点能匹配的粒度那会失去泛化能力推荐使用“历史遗迹”“宗教文化”“自然山水”“文化古镇”“红色旅游”“博物馆”“美食小吃”“节庆活动”这类能覆盖多个景点的中型标签。第二一个景点建议关联 3 到 5 个标签太少则信息不足太多则标签区分度下降。4. 后端核心功能实现4.1 景点管理模块后台管理端最基础的功能是景点维护。使用 MyBatis-Plus 后Mapper 层几乎不需要写 SQL只需要继承BaseMapper接口。// 文件路径src/main/java/com/henan/travel/mapper/ScenicSpotMapper.java public interface ScenicSpotMapper extends BaseMapperScenicSpot { }Service 层在新增景点时除了保存景点基本信息还要维护景点与标签的关联关系。以下是景点新增的核心逻辑// 文件路径src/main/java/com/henan/travel/service/impl/ScenicSpotServiceImpl.java Service public class ScenicSpotServiceImpl extends ServiceImplScenicSpotMapper, ScenicSpot implements ScenicSpotService { Resource private ScenicTagMapper scenicTagMapper; Override Transactional(rollbackFor Exception.class) public void addScenic(ScenicDTO dto) { // 1. 保存景点基本信息 ScenicSpot spot new ScenicSpot(); BeanUtils.copyProperties(dto, spot); this.save(spot); // 2. 保存景点和标签的关联关系 ListLong tagIds dto.getTagIds(); if (tagIds ! null !tagIds.isEmpty()) { for (Long tagId : tagIds) { ScenicTag scenicTag new ScenicTag(); scenicTag.setScenicId(spot.getId()); scenicTag.setTagId(tagId); scenicTagMapper.insert(scenicTag); } } } }这段代码里有两个值得注意的地方一是使用了Transactional事务注解确保景点信息和标签关联要么都成功要么都回滚避免出现“景点保存了但没有标签”的脏数据二是操作的是 DTO 而不是直接接收前端传来的实体避免前端传入额外的字段覆盖数据库已有内容。景点列表接口则使用 MyBatis-Plus 的分页插件实现按名称模糊搜索、按区域搜索、按标签筛选// 文件路径src/main/java/com/henan/travel/service/impl/ScenicSpotServiceImpl.java Override public PageScenicSpotVO pageScenic(ScenicQueryDTO query) { PageScenicSpot page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperScenicSpot wrapper new LambdaQueryWrapper(); // 按名称模糊查询 if (StrUtil.isNotBlank(query.getName())) { wrapper.like(ScenicSpot::getName, query.getName()); } // 按地市查询 if (StrUtil.isNotBlank(query.getRegion())) { wrapper.eq(ScenicSpot::getRegion, query.getRegion()); } // 按浏览量倒序 wrapper.orderByDesc(ScenicSpot::getClickCount); PageScenicSpot result this.page(page, wrapper); // 转换为VO并填充标签 PageScenicSpotVO voPage new Page(query.getPageNum(), query.getPageSize()); ListScenicSpotVO voList result.getRecords().stream().map(spot - { ScenicSpotVO vo new ScenicSpotVO(); BeanUtils.copyProperties(spot, vo); vo.setTags(tagMapper.selectTagsByScenicId(spot.getId())); return vo; }).collect(Collectors.toList()); voPage.setRecords(voList); voPage.setTotal(result.getTotal()); return voPage; }这里有一个常用的优化思路如果一个景点要查一次标签列表返回 10 条数据就会产生 10 次查询也就是传说中的 N1 问题。毕设规模下问题不大但如果想写得更好可以先把当前页所有景点 ID 收集起来再用一条 SQL 批量查出景点与标签的映射关系在内存中完成组装。4.2 基于标签偏好的推荐算法实现这个模块是本系统技术上的核心也是论文中最值得浓墨重彩的部分。推荐算法的第一步是把用户的历史行为转化成“用户标签偏好向量”。思路如下查询当前用户最近的浏览记录查询当前用户的收藏记录查询当前用户的评论记录分别赋予不同权重比如收藏记 3 分评论记 2 分浏览记 1 分通过每条行为关联的景点标签累加得到用户对每个标签的偏好分数。假设当前用户交互过的景点有“龙门石窟”和“殷墟”龙门石窟关联了“历史遗迹”“宗教文化”“世界遗产”殷墟关联了“历史遗迹”“考古遗址”。其中龙门石窟是收藏的殷墟是浏览过的那么用户对“历史遗迹”的偏好分就是 314对“宗教文化”是 3对“考古遗址”是 1。这样得到用户偏好向量。第二步是把所有景点都表示成标签向量。一个景点关联了哪些标签向量里对应位置就为 1否则为 0。第三步是计算用户偏好向量与每个景点向量之间的余弦相似度按相似度从大到小排列去掉用户已经浏览过的景点取前 N 条。余弦相似度公式为similarity (A · B) / (|A| * |B|)分子是用户向量和景点向量的点积分母是两个向量模长的乘积。在 Java 中可以用一个名为RecommendService的服务来实现// 文件路径src/main/java/com/henan/travel/service/impl/RecommendServiceImpl.java Service public class RecommendServiceImpl implements RecommendService { Resource private ScenicSpotMapper scenicSpotMapper; Resource private ScenicTagMapper scenicTagMapper; Resource private TagMapper tagMapper; Resource private CollectRecordMapper collectRecordMapper; Resource private BrowseRecordMapper browseRecordMapper; Resource private CommentRecordMapper commentRecordMapper; Override public ListScenicSpotVO recommend(Long userId, int limit) { // 1. 查询所有标签 ListTag allTags tagMapper.selectList(null); int tagSize allTags.size(); // 2. 构建用户标签偏好向量 double[] userVector buildUserVector(userId, allTags); // 3. 查询所有上架景点 QueryWrapperScenicSpot spotWrapper new QueryWrapper(); spotWrapper.eq(status, 1); ListScenicSpot allSpots scenicSpotMapper.selectList(spotWrapper); // 4. 记录用户已浏览过的景点ID避免重复推荐 SetLong viewedIds browseRecordMapper.selectList( new QueryWrapperBrowseRecord().eq(user_id, userId) ).stream().map(BrowseRecord::getScenicId).collect(Collectors.toSet()); // 5. 计算每个景点与用户偏好的相似度 ListScenicScore scoreList new ArrayList(); for (ScenicSpot spot : allSpots) { if (viewedIds.contains(spot.getId())) { continue; } double[] spotVector buildSpotVector(spot.getId(), allTags); double similarity cosineSimilarity(userVector, spotVector); if (similarity 0) { scoreList.add(new ScenicScore(spot, similarity)); } } // 6. 按相似度倒序取前limit条 scoreList.sort((a, b) - Double.compare(b.getScore(), a.getScore())); ListScenicSpotVO result new ArrayList(); int count Math.min(limit, scoreList.size()); for (int i 0; i count; i) { // 将景点转换为VO并填充标签代码略 } return result; } /** * 构建用户标签偏好向量 */ private double[] buildUserVector(Long userId, ListTag allTags) { double[] vector new double[allTags.size()]; MapLong, Integer tagIndexMap new HashMap(); for (int i 0; i allTags.size(); i) { tagIndexMap.put(allTags.get(i).getId(), i); } // 收藏权重 3.0 ListCollectRecord collects collectRecordMapper.selectList( new QueryWrapperCollectRecord().eq(user_id, userId)); for (CollectRecord record : collects) { ListLong tagIds scenicTagMapper.selectTagIdsByScenicId(record.getScenicId()); for (Long tagId : tagIds) { Integer idx tagIndexMap.get(tagId); if (idx ! null) { vector[idx] 3.0; } } } // 评论权重 2.0 ListCommentRecord comments commentRecordMapper.selectList( new QueryWrapperCommentRecord().eq(user_id, userId)); for (CommentRecord record : comments) { ListLong tagIds scenicTagMapper.selectTagIdsByScenicId(record.getScenicId()); for (Long tagId : tagIds) { Integer idx tagIndexMap.get(tagId); if (idx ! null) { vector[idx] 2.0; } } } // 浏览权重 1.0只保留最近30条 ListBrowseRecord browses browseRecordMapper.selectList( new QueryWrapperBrowseRecord().eq(user_id, userId) .orderByDesc(browse_time).last(limit 30)); for (BrowseRecord record : browses) { ListLong tagIds scenicTagMapper.selectTagIdsByScenicId(record.getScenicId()); for (Long tagId : tagIds) { Integer idx tagIndexMap.get(tagId); if (idx ! null) { vector[idx] 1.0; } } } return vector; } /** * 构建景点标签向量 */ private double[] buildSpotVector(Long scenicId, ListTag allTags) { double[] vector new double[allTags.size()]; ListLong tagIds scenicTagMapper.selectTagIdsByScenicId(scenicId); for (int i 0; i allTags.size(); i) { if (tagIds.contains(allTags.get(i).getId())) { vector[i] 1.0; } } return vector; } /** * 计算余弦相似度 */ private double cosineSimilarity(double[] a, double[] b) { double dot 0.0; double normA 0.0; double normB 0.0; for (int i 0; i a.length; i) { dot a[i] * b[i]; normA a[i] * a[i]; normB b[i] * b[i]; } if (normA 0 || normB 0) { return 0.0; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } }这段代码体现了完整的推荐链路。需要特别说明两点第一为什么评分权重是收藏 3、评论 2、浏览 1这是基于一个常见假设用户主动收藏的行为比被动浏览更能反映真实偏好评论则介于两者之间。这些权重值可以写死在常量里也可以做进后台配置。论文中可以对数值做敏感性分析说明不同权重对推荐效果的影响这是加分项。第二为什么要过滤掉已经浏览过的景点如果用户已经浏览过龙门石窟再把龙门石窟推荐给用户没有意义。过滤逻辑也可以在 SQL 中处理但用 Java 集合过滤更直观。当用户没有任何历史行为时用户向量全为 0无法计算相似度。这时候触发冷启动策略直接按浏览量倒序返回热门景点。这是推荐系统里的常见降级方案。// 冷启动无行为数据时推荐热门景点 if (isVectorZero(userVector)) { QueryWrapperScenicSpot wrapper new QueryWrapper(); wrapper.eq(status, 1).orderByDesc(click_count).last(limit limit); ListScenicSpot hotSpots scenicSpotMapper.selectList(wrapper); // 转换为VO返回 }4.3 用户行为记录接口推荐算法需要数据用户行为记录接口就是算法的“数据采集器”。用户浏览景点详情时前端调用接口记录浏览日志// 文件路径src/main/java/com/henan/travel/controller/BrowseController.java RestController RequestMapping(/api/browse) public class BrowseController { Resource private BrowseRecordService browseRecordService; PostMapping(/record) public ResultVoid record(RequestBody BrowseDTO dto) { StpUtil.checkLogin(); // 校验登录 Long userId StpUtil.getLoginIdAsLong(); browseRecordService.recordBrowse(userId, dto.getScenicId()); return Result.success(null); } }这里使用 Sa-Token 的StpUtil.checkLogin()做登录校验比 Spring Security 的过滤器链配置简单直观。记录浏览时可以做一个小策略同一个用户对一个景点短时间内重复浏览不重复记录直接更新浏览时间即可避免日志表被刷爆。// 文件路径src/main/java/com/henan/travel/service/impl/BrowseRecordServiceImpl.java Override public void recordBrowse(Long userId, Long scenicId) { // 查询该用户对该景点最近一条浏览记录 QueryWrapperBrowseRecord wrapper new QueryWrapper(); wrapper.eq(user_id, userId).eq(scenic_id, scenicId) .orderByDesc(browse_time).last(limit 1); BrowseRecord latest this.getOne(wrapper); if (latest ! null) { // 如果最近浏览时间距现在小于30分钟只更新时间 latest.setBrowseTime(new Date()); this.updateById(latest); } else { BrowseRecord record new BrowseRecord(); record.setUserId(userId); record.setScenicId(scenicId); record.setBrowseTime(new Date()); this.save(record); } }收藏接口和评论接口与浏览接口思路类似不再重复贴代码但在收藏接口中要注意用户对同一个景点只能收藏一次插入前先做唯一性校验否则会出现重复收藏数据。5. 前台展示与前后端联调5.1 首页推荐位设计前台首页是系统的门面布局一般包括顶部导航栏Logo、首页、景点列表、文化专题、个人中心。首页内容区域占用三块位置轮播图展示精选景点中部是“热门景点 Top10”按点击量排序最核心的位置是“猜你喜欢”调用的就是第 4.2 节的推荐接口。推荐接口返回的数据结构和景点列表基本一致前端只需要复用同一个景点卡片组件即可。5.2 Vue 前端调用推荐接口使用 Vue 3 Axios 的时候一般会在src/utils/request.js中封装一个请求实例// 文件路径src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000 }) // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(网络请求失败请检查后端服务是否启动) return Promise.reject(error) } ) export default request然后在首页组件中调用推荐接口// 文件路径src/views/Home.vue script setup import { ref, onMounted } from vue import request from /utils/request import ScenicCard from /components/ScenicCard.vue const recommendList ref([]) const loadRecommend async () { const res await request.get(/recommend/list, { params: { limit: 8 } }) recommendList.value res } onMounted(() { loadRecommend() }) /script前端组件只负责展示数据不需要关心推荐算法是如何计算的。这也是前后端分离模式的优势前端和后端可以并行开发只需要提前约定好接口文档。5.3 跨域问题处理前后端分离开发时前端运行在 5173 端口Vite 默认后端运行在 8080 端口浏览器会因为跨域拦截请求。解决方案是在后端写一个跨域配置类// 文件路径src/main/java/com/henan/travel/config/CorsConfig.java Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }生产环境部署时如果前端和后端使用同一个域名通过 Nginx 反向代理跨域问题就不存在了。但开发阶段加跨域配置能省很多烦心事。6. 部署运行指南6.1 本地开发环境启动步骤如果你拿到的是完整的源码工程按照下面的步骤可以快速在本地跑起来。第一步准备环境。安装 JDK 8 或 JDK 17根据 Spring Boot 版本确定安装 MySQL 8.0安装 Redis安装 Node.js 16 以上版本。第二步初始化数据库。在 Navicat 中新建数据库henan_travel字符集选择utf8mb4然后执行项目 sql 目录下的init.sql脚本。脚本执行完成后数据库里会生成表结构和基础测试数据。第三步修改后端配置。打开application.yml修改数据源和 Redis 连接信息# 文件路径src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/henan_travel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0第四步启动后端。在项目根目录执行mvn spring-boot:run或者在 IDEA 中直接运行主启动类。控制台出现 “Started TravelApplication” 日志即表示启动成功。第五步启动前端。进入frontend目录依次执行npm install npm run dev浏览器访问http://localhost:5173看到首页即表示前后端联调成功。6.2 生产环境部署思路毕设演示阶段一般不需要真的买服务器部署但如果论文中要写“系统部署方案”可以补充以下内容后端打包生成可执行 Jar 包上传到服务器后使用nohup java -jar travel.jar travel.log 21 命令后台运行前端通过npm run build打包生成 dist 静态文件配置 Nginx 将根目录指向 dist 文件同时把/api前缀的请求反向代理到后端 8080 端口。一个简单的 Nginx 配置片段如下server { listen 80; server_name your_domain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这样配置后前端页面和后端接口在同一个域名下不会再出现跨域问题。真正做线上部署时建议使用 HTTPS 证书加密传输尤其是登录接口涉及密码传输。7. 常见问题与排查思路7.1 启动报错排查表问题现象常见原因解决思路启动报Failed to configure a DataSource数据库连接信息错误或未安装 MySQL检查 application.yml 中的地址、端口、账号密码启动报Access denied for user数据库账号权限不足为当前账号授权数据库访问权限启动报Unable to connect to RedisRedis 未启动或端口不对本地执行redis-server启动 Redis前端页面打不开未启动前端项目或端口被占用确认 5173 端口未被占用重新执行npm run dev接口返回 401token 未携带或已过期检查前端请求拦截器是否带上 token接口返回 500SQL 异常或空指针查看后端控制台堆栈信息定位具体行上传图片失败上传目录不存在或权限不足检查 application.yml 中的上传路径配置创建目录并授权7.2 推荐列表为空这是推荐模块最容易遇到的问题。用户没有浏览、收藏和评论记录的时候用户偏好向量全为 0冷启动逻辑如果没有写对推荐结果就会是空列表。排查步骤确认用户是否登录后端是否取到了正确的 userId在数据库中查询browse_record表确认是否有该用户的浏览记录在后端日志中打印用户偏好向量确认向量是否全为 0确认是否执行了冷启动分支无行为数据时应该返回热门景点列表。7.3 MyBatis-Plus 分页失效分页失效通常是因为没有配置分页插件。MyBatis-Plus 3.4 以上版本需要手动创建分页插件 Bean// 文件路径src/main/java/com/henan/travel/config/MybatisPlusConfig.java Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }忘记配置这个插件时分页查询不会真正执行 LIMIT 语句而是把全表数据查出来然后在内存中截取数据量大时会非常慢。8. 毕设论文写作与答辩要点8.1 论文章节结构建议一套技术型毕设论文通常按照下面章节展开第一章绪论写选题背景、国内外研究现状、研究内容第二章相关技术介绍写 SpringBoot、MyBatis-Plus、Vue、推荐算法第三章需求分析写功能性需求和非功能性需求配用例图第四章系统设计写总体架构、功能模块设计、数据库设计配 E-R 图和表结构第五章系统实现按功能模块展示核心代码和运行截图第六章系统测试写测试用例、测试过程和测试结果最后是总结与展望。“相关技术介绍”这一章不要写成百科词条重点说明“为什么在这个系统中选择该技术”。比如写 Redis 时不要抄“Redis 是一个高性能的 key-value 存储系统”这种定义而要写“系统使用 Redis 缓存首页热门景点数据减少数据库查询压力同时利用 Redis 的过期策略实现验证码时效控制”。8.2 答辩高频问题老师针对推荐系统类毕设通常会问以下问题提前准备好答案会从容很多这个系统用的是哪种推荐算法为什么选它答基于内容的推荐根据用户历史行为构建标签偏好向量与景点标签向量计算余弦相似度不依赖大规模用户行为矩阵适合冷启动和小规模场景。推荐效果怎么评估答可以从三个维度说明一是用户点击率即推荐位商品的点击次数占总展示次数的比例二是推荐位商品被收藏的次数三是用户反馈问卷调查。毕设阶段建议用测试账号模拟不同用户行为对推荐结果做定性分析。如果用户量增大到百万级系统怎么扩展答可以从三方面回答第一热点景点数据使用 Redis 缓存降低数据库压力第二推荐计算可以使用定时任务预先计算结果存入 Redis 或数据库中用户请求时直接读取避免实时计算第三引入搜索引擎 Elasticsearch 做景点全文检索提高搜索效率。数据安全性如何保证答密码使用 BCrypt 加密存储接口通过 Sa-Token 做登录拦截后台管理接口单独做权限校验前端显示时对用户输入内容做转义防止 XSS 攻击。8.3 源码和论文文档的整理规范如果毕设要求提交源码和论文文档项目根目录建议按照下面的结构整理henan-travel-system/ ├── backend/ // 后端SpringBoot工程 │ ├── src/ │ ├── pom.xml │ └── sql/init.sql // 数据库初始化脚本 ├── frontend/ // 前端Vue3工程 │ ├── src/ │ └── package.json ├── docs/ │ ├── 需求分析文档.docx │ ├── 数据库设计文档.docx │ ├── 答辩PPT.pptx │ └── 部署说明.txt └── README.mdREADME.md 中如果写的是“这是基于 SpringBoot 和 Vue3 的河南特色景点文化推荐系统”记得写明使用步骤、默认管理员账号密码、技术栈列表方便答辩老师快速了解项目。9. 总结与后续优化方向到这里基于 SpringBoot 的河南特色景点文化推荐系统的核心内容已经完整拆解完了。从数据库设计到推荐算法从后端接口到前端展示从本地部署到论文答辩整条链路形成了一个可以真正落地的毕设项目。你不只是做出了一个“能跑的系统”而是真正理解了一个带推荐功能的 Web 项目应该怎么设计数据结构、怎么沉淀用户行为、怎么把数据转化成推荐结果。如果你准备拿这个题目做毕设优先关注三件事第一先把数据库表和基础 CRUD 跑通保证系统“能用”第二完善用户行为记录保证推荐算法“有数可用”第三花时间把推荐算法的代码讲清楚这是整个项目技术的“最高地标”。后续如果想继续深化可以沿着三个方向优化第一个方向是引入协同过滤当系统同时具备用户对景点的评分数据后用基于物品的协同过滤计算“看过 A 景点的人还看过哪些景点”和基于内容的推荐形成互补第二个方向是加入搜索权重把景点的文化标签、区域、名称做成综合检索提高用户找景点的效率第三个方向是做移动端适配把前端改造成响应式布局或者单独出一套微信小程序端。这些方向都能写进论文的“展望”部分让结尾不再空洞。项目源码和论文文档的完整版本如果后面有时间整理我会再单独发一篇说明文档把数据库初始化脚本、前端页面截图和论文模板的使用方式都给大家讲清楚。觉得这篇文章对你有帮助可以先收藏免得真正开始做毕设的时候找不到。动手写代码之前建议你再把第 3 节的数据库设计和第 4.2 节的推荐算法过一遍这两个部分就是整套系统的地基和心脏。
返回列表