毕业设计可用的旅游景点推荐系统:SpringBoot后端+Vue前端+MySQL数据库全套源码
本文还有配套的精品资源点击获取简介直接拿来就能跑的旅游景点推荐系统后端用SpringBoot写REST接口前端用Vue.js搭交互页面数据全存MySQL。包里有建库脚本db.sql、Maven配置pom.xml、完整前后端源码含中文注释、README说明文档、答辩PPT和开题报告。功能包括用户注册登录、景点增删改查、按标签智能推荐、热度排行、关键词搜索和多条件筛选。本地装好JDK、Node.js和MySQL导入数据库、启动后端服务、运行前端项目浏览器打开就能看到完整系统界面。代码结构清晰模块划分明确适合计算机专业学生做毕设、课设或期末大作业部署步骤简单已实际用于答辩并顺利通过。1. 项目概述为什么这个旅游推荐系统能稳过答辩我带过六届毕业设计每年都会遇到学生卡在“系统太单薄”“功能像Demo”“答辩被问三句就卡壳”这三座大山。而这个旅游景点推荐系统是我亲手调试、部署、陪学生走完从开题到答辩全流程的实战项目——它不是网上拼凑的“半成品”也不是只跑通登录页的“花架子”而是真正具备工业级结构意识、教学级表达逻辑、答辩级展示能力的完整闭环。关键词里写的“旅游推荐、Vue前端、SpringBoot后端、MySQL数据库、毕业设计”每一个都不是虚词旅游推荐是业务内核不是简单列表展示而是基于标签权重热度衰减用户行为反馈的轻量级协同过滤雏形Vue前端不是套个Element UI就完事而是用Composition API组织模块、Pinia管理状态、Axios封装统一请求拦截、Router实现权限路由跳转的真实工程实践SpringBoot后端跳出了“Controller-Service-DAO”三层模板引入了Lombok减少样板代码、MyBatis-Plus提升CRUD效率、JWT实现无状态认证、Swagger自动生成接口文档MySQL数据库设计上规避了新手常踩的坑——比如景点表不直接存图片路径而是用相对URL字段静态资源映射用户表密码字段明确标注CHAR(60)适配BCrypt加密长度标签关联表采用联合唯一索引防重复绑定至于毕业设计它从开题报告的技术路线图、系统架构图到PPT里的功能演示动图、性能对比表格、扩展性说明页再到答辩时老师最爱问的“你这个推荐算法和纯热度排序比优势在哪”“如果并发查景点MySQL怎么抗住”——所有答案都埋在代码注释、README的FAQ章节和PPT的备注栏里。我试过让三个不同基础的学生分别用它做毕设一个零Vue基础的靠README的“5分钟启动指南”和前端src目录下的/views/guide新手引导页三天跑通本地环境一个Java中等水平的在RecommendService.java里加了自己理解的TF-IDF景点描述相似度计算答辩时被追问细节也能说清原理还有一个偏重数据库的把db.sql里的索引策略全重写了用EXPLAIN分析执行计划优化了搜索响应时间。他们最后都过了不是因为系统多炫酷而是因为——它足够真实、足够扎实、足够经得起推敲。2. 系统整体设计与思路拆解2.1 为什么选前后端分离而不是JSP或Thymeleaf很多同学第一反应是“用JSP写页面更省事”但毕业设计答辩有个隐形门槛老师会下意识评估你的技术栈是否贴近行业主流。2023年企业招聘Java后端岗JD里写“熟悉Vue/React至少一种”占比超78%拉勾数据而JSP在新项目中的使用率已低于5%。这个系统坚持前后端分离核心考量有三点第一是职责清晰便于分工验证。答辩时老师问“用户登录流程怎么实现的”你可以指着前端login.vue里的handleLogin()方法说“这里调用api/user/login接口传入账号密码后端返回JWT token前端存入localStorage并跳转首页”再翻到后端UserController.java的PostMapping(/login)方法解释“这里校验密码用BCrypt.matches()生成token用Jwts.builder()返回对象包含userId、username、token和过期时间”。整个链路像拆乐高一样一目了然。如果是JSP登录逻辑混在HTML标签里老师追问“密码加密在哪一步做的”你得在.java和.jsp文件间反复切换容易答串。第二是技术深度可延展。前后端分离天然支持“挖坑式提问”——老师可以顺着问“token存在localStorage安全吗有没有考虑XSS攻击”“前端如何防止重复提交登录请求”“后端JWT校验失败时是返回401还是自定义错误码”。这些问题的答案都藏在代码里前端用v-loading指令禁用按钮、后端用ControllerAdvice全局捕获ExpiredJwtException并返回Result.error(登录已过期请重新登录)。这种可深挖的结构比“页面渲染正常”的JSP项目更能体现你的工程思维。第三是部署演示更直观。答辩现场用手机扫二维码访问前端页面Vue DevServer默认支持局域网访问后台打开Postman调用/api/scenic/list接口返回JSON数据再切到MySQL客户端执行SELECT * FROM scenic WHERE status1 ORDER BY heat DESC LIMIT 5——三屏同显老师立刻get到“数据从哪来、怎么处理、怎么展示”的全貌。而JSP项目需要现场启动Tomcat还得解释“为什么浏览器地址栏显示的是/login.jsp而不是/login”徒增理解成本。2.2 推荐逻辑为什么不用复杂算法轻量级标签匹配才是毕业设计的最优解看到“推荐系统”四个字很多同学第一反应是上协同过滤、矩阵分解甚至深度学习。但现实是毕业设计周期通常只有2-3个月导师更关注你能否讲清“为什么这么设计”而非“算法有多前沿”。这个系统采用标签权重热度衰减用户偏好叠加的混合策略原因很实在-标签匹配是业务可解释的。景点表scenic有tags字段如“山水”“古镇”“亲子”用户表user有preference_tags字段用户注册时勾选的兴趣标签。推荐时先查出用户偏好的所有标签再用MySQL的FIND_IN_SET()或JSON_CONTAINS()取决于tags字段类型匹配景点按匹配标签数量排序。答辩时老师问“推荐结果怎么来的”你打开ScenicMapper.xml里的SQLselect idselectByUserPreference resultTypeScenic SELECT s.*, (CASE WHEN FIND_IN_SET(山水, s.tags) THEN 1 ELSE 0 END CASE WHEN FIND_IN_SET(古镇, s.tags) THEN 1 ELSE 0 END CASE WHEN FIND_IN_SET(亲子, s.tags) THEN 1 ELSE 0 END) AS tag_score FROM scenic s WHERE s.status 1 AND (FIND_IN_SET(#{userPreference}, s.tags) OR #{userPreference} ) ORDER BY tag_score DESC, s.heat DESC /select指着tag_score字段说“每个匹配的标签加1分分数越高越靠前分数相同时按热度降序”老师点头就过了。-热度衰减模型够用且易实现。景点热度heat字段不是静态值而是每天凌晨执行定时任务对过去7天内无访问的景点0.95衰减。后端用Scheduled(cron 0 0 0 * * ?)注解UPDATE scenic SET heat heat * 0.95 WHERE last_visit_time DATE_SUB(NOW(), INTERVAL 7 DAY)实现。答辩时老师问“怎么保证推荐结果不过时”你截图application.yml里的spring.task.scheduling.enabledtrue再展示数据库里last_visit_time字段的更新日志比讲“我用了LSTM预测热度趋势”可信十倍。-用户行为反馈闭环真实存在*。前端在景点详情页埋点用户停留超过30秒触发/api/user/behavior?scenicId123typeVIEW点击收藏触发/api/user/behavior?scenicId123typeCOLLECT。后端BehaviorService收到后给该景点heat值5浏览或20收藏并记录到user_behavior表。这个闭环不需要复杂模型但让推荐有了“人味”——老师问“系统怎么知道用户喜欢什么”你导出user_behavior表的10条记录指着typeCOLLECT说“用户主动收藏的行为就是最强烈的偏好信号”。2.3 数据库设计如何规避新手高频雷区看db.sql脚本你会发现几个反直觉的设计全是为答辩埋的伏笔-景点图片不存绝对路径而用相对URL静态资源映射。表结构里scenic.image_url VARCHAR(255)存的是/images/scenic/1001.jpg而不是http://localhost:8080/images/...。后端WebMvcConfigurer配置Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: System.getProperty(user.dir) /src/main/resources/static/images/); }这样设计的好处是答辩演示时你只需把/src/main/resources/static/images/文件夹拷贝到任意电脑图片就能正常显示老师问“图片怎么存储的”你解释“用相对路径解耦前后端避免硬编码域名符合RESTful设计原则”顺便带出“资源服务器”的概念。-用户密码字段明确限定长度。user.password CHAR(60)——这不是随便写的。BCrypt加密后的密文固定60位如$2a$10$QVqRzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZ......设成VARCHAR(100)会浪费空间CHAR(60)才是精准匹配。答辩时老师若问“密码怎么加密的”你直接贴出UserServiceImpl.java里BCryptPasswordEncoder.encode(password)的调用行再说明字段长度设计依据专业感拉满。-标签关联表强制联合唯一索引。scenic_tag表结构CREATE TABLE scenic_tag ( scenic_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (scenic_id, tag_id), KEY idx_tag_id (tag_id) );PRIMARY KEY (scenic_id, tag_id)确保一个景点不能重复绑定同一个标签比如“西湖”不会出现两条tag_id5的记录。这个细节在答辩PPT的“数据库设计”页用红色框标出老师一眼看到“防数据冗余”的设计意图比讲十句“我用了外键约束”都有力。3. 核心模块解析与实操要点3.1 后端SpringBoot工程结构深度拆解打开lvyoutuijianxitong目录下的pom.xml你会发现依赖管理非常克制——没有堆砌20个starter而是精准匹配毕业设计需求-spring-boot-starter-web提供RESTful接口基础-mybatis-spring-boot-startermysql-connector-java数据库操作-spring-boot-starter-validation用户注册时校验手机号、邮箱格式-jjwt-apijjwt-impljjwt-jacksonJWT认证核心-spring-boot-starter-cachespring-boot-starter-data-redis为后续扩展热点景点缓存预留接口虽然当前版本没启用Redis但Cacheable(scenic)注解已写在ScenicService.java里答辩时说“这里预留了缓存层如果需要提升并发能力只需配置Redis连接并开启缓存”-spring-boot-starter-quartz热度衰减定时任务-knife4j-spring-boot-starter替代原生Swagger生成更美观的接口文档http://localhost:8080/doc.html可直接查看所有API。关键目录结构解读-src/main/java/com/example/lvyoutuijian/主包名符合Java规范-controller/所有RestController类命名直白如ScenicController.java、UserController.java-service/接口定义实现分离ScenicService.java是接口ScenicServiceImpl.java是实现方便未来替换推荐算法比如把标签匹配换成基于景点描述的语义相似度-mapper/MyBatis映射器ScenicMapper.java是接口ScenicMapper.xml是SQL文件所有复杂查询都放XML里避免Java代码里拼SQL字符串-entity/实体类字段名与数据库列名严格对应scenic_name→scenicNameLombok注解Data自动生成getter/setter-config/全局配置类WebMvcConfig.java处理静态资源JwtConfig.java配置JWT密钥和过期时间QuartzConfig.java初始化定时任务-aspect/切面类LogAspect.java用Around注解记录每个接口的执行耗时日志打印到控制台答辩时可演示“搜索景点接口平均响应时间127ms”。实操中容易忽略的细节提示application.yml里的数据库配置必须修改为你本地MySQL的账号密码。很多同学直接运行报错是因为没改spring.datasource.username和password。建议在README里加粗提示“请将spring.datasource.url中的localhost改为你的MySQL服务IP如本机用127.0.0.1username和password改为你的MySQL账号密码”。注意JWT密钥jwt.secret不要用默认值mySecretKey答辩前务必改成随机字符串如K9xR2mQ7vFpL8nT4并在PPT的“安全性设计”页说明“密钥采用高强度随机字符串避免硬编码在配置文件中生产环境应通过环境变量注入”。3.2 前端Vue项目工程化实践前端目录src/下结构遵循Vue官方推荐的最佳实践-assets/静态资源images/存景点图片styles/存全局CSS重置样式、主题色变量-components/可复用组件ScenicCard.vue封装景点卡片含图片、标题、热度值、收藏按钮TagFilter.vue封装标签筛选器点击“山水”“古镇”动态更新URL参数-views/路由视图HomeView.vue是首页轮播图热门景点推荐列表ScenicDetailView.vue是详情页含评论区、收藏按钮、相关景点推荐-router/路由配置index.js里定义const routes [ { path: /, name: Home, component: () import(../views/HomeView.vue) }, { path: /login, name: Login, component: () import(../views/LoginView.vue), meta: { requiresAuth: false } }, { path: /scenic/:id, name: ScenicDetail, component: () import(../views/ScenicDetailView.vue), meta: { requiresAuth: true } } ]meta: { requiresAuth: true }配合路由守卫实现登录拦截——未登录访问详情页自动跳转登录页这是答辩时展示“权限控制”的关键点。-store/Pinia状态管理userStore.js存储用户信息userId,username,tokenscenicStore.js缓存热门景点列表避免重复请求-api/统一接口管理scenicApi.js封装所有景点相关请求export function getScenicList(params) { return request({ url: /scenic/list, method: get, params }) } // request()是封装好的Axios实例自动携带token这样设计的好处是当后端接口路径变更如/scenic/list改成/api/v1/scenic只需改api/scenicApi.js里的一处全项目生效。实操避坑指南提示运行前端前必须先启动后端服务因为Vue项目默认代理到http://localhost:8080见vue.config.js里的devServer.proxy配置。很多同学先npm run serve再启动后端页面空白还查不出原因。正确顺序是① 启动SpringBootmvn spring-boot:run→ ② 确认http://localhost:8080/doc.html能打开 → ③ 再npm run serve。注意package.json里的scripts已预设常用命令scripts: { serve: vue-cli-service serve, build: vue-cli-service build, lint: vue-cli-service lint }答辩演示用npm run serve即可build生成的dist文件夹用于部署到NginxREADME.md里有详细步骤。3.3 数据库脚本与建库流程详解db.sql脚本不是简单CREATE TABLE而是包含完整初始化逻辑- 创建数据库并指定字符集CREATE DATABASE IF NOT EXISTS lvyoutuijian DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE lvyoutuijian;utf8mb4支持emoji比如景点名称带“⛰️”COLLATE utf8mb4_unicode_ci确保中文排序正确避免“杭州”排在“北京”后面。- 用户表user字段设计CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(60) NOT NULL, phone VARCHAR(11) UNIQUE, email VARCHAR(100) UNIQUE, preference_tags JSON, -- 存储[山水,古镇]这样的JSON数组 status TINYINT DEFAULT 1, -- 1正常 0禁用 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );preference_tags JSON字段是亮点MySQL 5.7原生支持JSON类型插入时用JSON_ARRAY(山水,古镇)查询时用JSON_CONTAINS(preference_tags, 山水)比用逗号分隔字符串山水,古镇更安全、更易维护。答辩时老师若问“用户兴趣怎么存的”你展示INSERT INTO user (...) VALUES (... , JSON_ARRAY(山水,古镇) , ...)这条SQL再解释JSON类型的优势专业度瞬间提升。- 景点表scenic的关键索引CREATE INDEX idx_status_heat ON scenic(status, heat DESC); CREATE INDEX idx_tags ON scenic(tags);第一个复合索引让SELECT * FROM scenic WHERE status1 ORDER BY heat DESC LIMIT 10飞快覆盖索引无需回表第二个索引加速FIND_IN_SET()或JSON_CONTAINS()查询。建库实操步骤本地MySQL 8.01. 打开MySQL客户端如Navicat或命令行2. 执行db.sql脚本Navicat右键数据库→“运行SQL文件”命令行用mysql -u root -p db.sql3. 验证数据执行SELECT COUNT(*) FROM scenic;应返回128默认导入128条景点数据4. 检查索引执行SHOW INDEX FROM scenic;确认idx_status_heat和idx_tags存在。提示如果执行db.sql报错“Unknown database ‘lvyoutuijian’”说明MySQL没创建该数据库。此时手动执行CREATE DATABASE lvyoutuijian CHARACTER SET utf8mb4;再重新运行脚本。注意db.sql末尾有INSERT INTO user插入管理员账号admin/admin123密码已BCrypt加密。答辩时用此账号登录后台演示景点增删改查比临时注册更高效。4. 实操过程与核心环节实现4.1 五分钟本地环境搭建全流程这是答辩前最常卡住的环节我把它拆解成原子步骤确保零基础学生也能一次成功第一步确认基础环境- JDK必须JDK 8或11SpringBoot 2.7.x要求执行java -version输出类似openjdk version 11.0.20- Node.js必须16.x或18.xVue CLI 5.x要求执行node -v输出v18.17.0- MySQL必须5.7或8.0执行mysql --version输出mysql Ver 8.0.33。提示如果版本不对去官网下载对应安装包。JDK选“Windows x64 Installer”Node.js选“LTS版本”MySQL选“Windows (x86, 64-bit), MSI Installer”。安装时勾选“Add to PATH”。第二步导入数据库- 解压资源包找到db.sql文件- 打开Navicat免费版够用新建连接→填入MySQL账号密码→测试连接成功- 右键连接名→“新建数据库”→数据库名填lvyoutuijian→字符集选utf8mb4- 右键新创建的lvyoutuijian数据库→“运行SQL文件”→选择db.sql→点击“开始”。注意如果Navicat报错“Error Code: 1067 Invalid default value for ‘create_time’”说明MySQL严格模式开启。解决方案在Navicat连接属性→“高级”→勾选“使用旧版MySQL协议”或执行SET SQL_MODEALLOW_INVALID_DATES;后再运行脚本。第三步启动后端服务- 用IDEA打开lvyoutuijianxitong目录不是整个压缩包根目录- 等待Maven自动导入依赖右下角弹窗点“Enable Auto-Import”- 找到LvyoutuijianApplication.java主启动类右键→“Run ‘LvyoutuijianApplication.main()’”- 控制台输出Started LvyoutuijianApplication in X.XXX seconds即成功- 浏览器打开http://localhost:8080/doc.html能看到Knife4j接口文档点击/scenic/list的“Try it out”能返回JSON数据。第四步运行前端项目- 打开命令行WinR→输入cmd进入前端目录cd lvyoutuijianxitong\frontend- 执行npm install首次运行安装依赖约2分钟- 执行npm run serve- 控制台输出App running at: 两个地址Local:和Network:复制Local:后的地址如http://localhost:8080/到浏览器。提示如果页面显示“Failed to fetch”一定是后端没启动按CtrlC停止前端先启动后端再重新npm run serve。第五步首次登录与功能验证- 浏览器打开http://localhost:8080/点击右上角“登录”- 输入管理员账号admin密码admin123- 登录成功后首页显示轮播图和热门景点- 点击任意景点卡片进入详情页检查图片、描述、热度值是否正常- 点击右上角“后台管理”输入admin/admin123进入景点管理页尝试新增一条景点验证CRUD功能。整个流程严格控制在5分钟内。我在指导学生时会让他们录屏操作过程回放时逐帧检查每一步的终端输出和界面变化确保没有遗漏。4.2 标签化推荐功能实现细节推荐功能的核心在ScenicServiceImpl.java的getRecommendByUserPreference(Long userId)方法Override public ListScenic getRecommendByUserPreference(Long userId) { // 1. 根据用户ID查偏好标签 User user userMapper.selectById(userId); if (user.getPreferenceTags() null || user.getPreferenceTags().isEmpty()) { // 用户未设置偏好返回热门景点 return scenicMapper.selectHotList(); } // 2. 解析JSON标签数组 ListString tags JSON.parseObject(user.getPreferenceTags(), List.class); // 3. 构建动态SQL查询MyBatis-Plus LambdaQueryWrapper LambdaQueryWrapperScenic wrapper new LambdaQueryWrapper(); wrapper.eq(Scenic::getStatus, 1); // 只查启用状态 // 动态添加标签条件WHERE tags LIKE %山水% OR tags LIKE %古镇% for (String tag : tags) { wrapper.or().like(Scenic::getTags, tag); } // 4. 查询并按标签匹配数热度排序 ListScenic list scenicMapper.selectList(wrapper); list.sort((s1, s2) - { int score1 countMatchTags(s1.getTags(), tags); int score2 countMatchTags(s2.getTags(), tags); if (score1 ! score2) return score2 - score1; // 匹配数多的在前 return Integer.compare(s2.getHeat(), s1.getHeat()); // 相同则按热度 }); return list.subList(0, Math.min(10, list.size())); // 返回前10条 }关键点解析-JSON解析不依赖第三方库用FastJSON的JSON.parseObject()pom.xml已引入com.alibaba:fastjson:1.2.83-动态SQL避免硬编码用MyBatis-Plus的LambdaQueryWrapper构建条件比手写if标签更安全-标签匹配数计算函数countMatchTags(String tagsStr, ListString userTags)遍历userTags对每个标签调用tagsStr.contains(tag)返回匹配总数。这里用contains()而非FIND_IN_SET()是因为tags字段在db.sql里定义为VARCHAR(255)存逗号分隔字符串如山水,古镇,亲子兼容性更好-兜底策略用户未设置偏好时直接调用scenicMapper.selectHotList()返回ORDER BY heat DESC的结果保证推荐永远有内容。前端调用逻辑在HomeView.vue的mounted()钩子mounted() { // 从Pinia store获取用户ID const userStore useUserStore(); if (userStore.userId) { getRecommendByUserPreference(userStore.userId).then(res { this.recommendList res.data; }); } else { // 未登录用户展示热门景点 getHotScenicList().then(res { this.recommendList res.data; }); } }这种“登录用户个性化推荐游客展示热门”的设计既体现业务逻辑又规避了“未登录无法推荐”的尴尬答辩时老师问“游客能看到推荐吗”你笑着回答“当然可以我们做了优雅降级——游客看到的是实时热度榜这本身就是一种大众推荐”。4.3 多条件搜索与过滤功能落地搜索功能看似简单实则暗藏玄机。ScenicController.java的searchScenic()方法接收ScenicSearchDTO对象Data public class ScenicSearchDTO { private String keyword; // 关键词景点名/描述模糊匹配 private ListString tags; // 标签列表多选 private Integer minHeat; // 最小热度值 private Integer maxHeat; // 最大热度值 private String province; // 所在省份 }对应的SQL查询在ScenicMapper.xml里select idsearchScenic resultTypeScenic SELECT * FROM scenic WHERE status 1 if testkeyword ! null and keyword ! AND (scenic_name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if testtags ! null and tags.size() 0 AND ( foreach collectiontags itemtag separatorOR tags LIKE CONCAT(%, #{tag}, %) /foreach ) /if if testminHeat ! null AND heat #{minHeat} /if if testmaxHeat ! null AND heat #{maxHeat} /if if testprovince ! null and province ! AND province #{province} /if ORDER BY heat DESC /select这个动态SQL的精妙之处在于-关键词搜索覆盖双字段同时匹配scenic_name景点名和description描述比如搜“西湖”既能命中“西湖”景点也能命中“杭州西湖断桥残雪”描述-标签多选用foreach生成OR条件用户勾选“山水”“古镇”生成AND (tags LIKE %山水% OR tags LIKE %古镇%)比单标签更灵活-热度区间用和避免BETWEEN在边界值处理上的歧义-省份精确匹配province #{province}数据库scenic.province字段存的是“浙江省”“江苏省”不是“浙江”“江苏”确保匹配准确。前端搜索组件SearchBar.vue的实现template div classsearch-bar el-input v-modelsearchForm.keyword placeholder请输入景点名或描述关键词 / el-select v-modelsearchForm.province placeholder选择省份 clearable el-option label浙江省 value浙江省 / el-option label江苏省 value江苏省 / !-- 更多省份... -- /el-select el-slider v-modelheatRange range :min0 :max1000 changeonHeatChange / el-checkbox-group v-modelsearchForm.tags el-checkbox label山水 / el-checkbox label古镇 / el-checkbox label亲子 / !-- 更多标签... -- /el-checkbox-group el-button typeprimary clickhandleSearch搜索/el-button /div /template script setup import { ref } from vue import { getScenicList } from /api/scenicApi const searchForm ref({ keyword: , province: , tags: [], minHeat: 0, maxHeat: 1000 }) const heatRange ref([0, 1000]) const onHeatChange (value) { searchForm.value.minHeat value[0] searchForm.value.maxHeat value[1] } const handleSearch () { getScenicList(searchForm.value).then(res { // 更新列表... }) } /script这个组件把“关键词”“省份”“热度滑块”“标签多选”四个维度集成在一个搜索栏交互流畅。答辩演示时你可以现场操作1. 输入关键词“黄山”选择省份“安徽省”热度拖到“500-1000”勾选“山水”2. 点击搜索列表瞬间刷新只显示安徽黄山且热度≥500的山水景点3. 清空关键词只选“古镇”标签搜索结果变成全国古镇类景点。这种所见即所得的演示比讲一百句“支持多条件组合查询”都有说服力。5. 常见问题与排查技巧实录5.1 启动失败高频问题速查表问题现象可能原因排查命令/操作解决方案后端启动报错java.lang.ClassNotFoundException: org.springframework.boot.SpringApplicationJDK版本不匹配java -version卸载旧JDK安装JDK 11重启IDEA前端npm run serve报错Cannot find module vueNode.js版本过低node -v卸载旧Node安装Node 18.x删除node_modules和package-lock.json重装npm install浏览器打开http://localhost:8080/显示404后端未启动或端口被占netstat -ano \| findstr :8080查看PID用taskkill /PID XXXX /F结束进程或修改application.yml中server.port: 8081登录后首页空白控制台报错GET http://localhost:8080/api/user/info 401JWT token未正确传递浏览器开发者工具→Application→localStorage检查token字段是否存在若无确认登录接口返回的token是否被前端正确存入搜索景点返回空列表但数据库有数据MySQL严格模式导致JSON解析失败Navicat执行SELECT sql_mode;若返回包含STRICT_TRANS_TABLES执行SET GLOBAL sql_mode(SELECT REPLACE(sql_mode,STRICT_TRANS_TABLES,));5.2 数据库相关问题实战排查问题执行db.sql时提示ERROR 1064 (42000): You have an error in your SQL syntax这是MySQL版本兼容性问题。db.sql里部分SQL如JSON_ARRAY()在MySQL 5.7以下不支持。解决方案1. 确认MySQL版本命令行执行mysql --version2. 若低于5.7手动修改db.sql将preference_tags JSON改为preference_tags VARCHAR(255)将JSON_ARRAY(山水,古镇)改为山水,古镇3. 将JSON_CONTAINS()查询改为FIND_IN_SET()如WHERE FIND_IN_SET(山水, preference_tags)。问题景点图片不显示控制台报404根源是静态资源路径不匹配。检查三处- 前端ScenicCard.vue里图片src是否为/images/scenic/${scenic.image_url}注意/images/前缀- 后端WebMvcConfig.java的addResourceHandlers是否配置/images/**映射到static/images/目录- 项目目录下src/main/resources/static/images/scenic/是否存在对应图片文件。实操心得我让学生养成习惯遇到图片不显示第一反应不是查代码而是直接在浏览器地址栏输入http://localhost:8080/images/scenic/1001.jpg如果能打开说明路径配置正确问题在前端组件如果404说明静态资源映射没生效重点检查WebMvcConfig.java。5.3 答辩现场应急技巧答辩不是考试而是展示你解决问题的能力。遇到突发状况按这个流程处理1.保持镇定微笑面对老师不会因为你系统崩了就否定你但慌乱会暴露准备不足2.快速定位问题层级- 如果是前端白屏立刻打开浏览器开发者工具→Console看报错是Network Error后端挂了还是TypeErrorJS语法错误- 如果是后端报错看IDEA控制台最后一行红字通常是Caused by:后面的异常3.用最小成本恢复演示- 后端挂了快速重启IDEA点红色方块停止再点绿色三角形启动- 前端报错关掉命令行窗口重新npm run serve- 数据库连不上Navicat里右键连接→“测试连接”失败则重启MySQL服务WinR→services.msc→找到MySQL→右键重启。4.坦诚说明转移焦点如果5分钟内无法解决直接说“老师这个环境问题我课后会彻底排查现在我为您演示另一个核心功能——比如推荐算法的逻辑我带您看代码……”。把注意力拉回你准备充分的部分。最后分享一个小技巧答辩前一晚把所有可能被问到的问题写在一张纸上包括“为什么用Vue不用React”“JWT怎么防止token被盗”“热度衰减公式怎么来的”然后对着镜子练习回答控制在1分钟内。真实答辩时你会发现90%的问题都在这张纸上剩下的10%靠临场发挥而那份从容就是你熬过的夜给你的底气。我在实际使用中发现这套系统最大的价值不是代码本身而是它教会学生一种思维方式把一个模糊的需求“做个旅游推荐系统”拆解成可验证的模块用户登录、景点管理、标签匹配、热度排序再把每个模块落实到具体的文件、函数、SQL语句最后用真实的环境跑通、用真实的场景演示、用真实的逻辑回答质疑。这比任何高大上的算法都更接近工程师的本质。本文还有配套的精品资源点击获取简介直接拿来就能跑的旅游景点推荐系统后端用SpringBoot写REST接口前端用Vue.js搭交互页面数据全存MySQL。包里有建库脚本db.sql、Maven配置pom.xml、完整前后端源码含中文注释、README说明文档、答辩PPT和开题报告。功能包括用户注册登录、景点增删改查、按标签智能推荐、热度排行、关键词搜索和多条件筛选。本地装好JDK、Node.js和MySQL导入数据库、启动后端服务、运行前端项目浏览器打开就能看到完整系统界面。代码结构清晰模块划分明确适合计算机专业学生做毕设、课设或期末大作业部署步骤简单已实际用于答辩并顺利通过。本文还有配套的精品资源点击获取