尧图精选

SpringBoot2+Vue3前后端分离旅游指南系统全栈开发实战

🕒 发布时间:2026/10/2 2:36:43 📁 来源:尧图网络
接到“Java Web旅游出行指南系统”这个题目的时候我第一反应是这不就是景点列表加个搜索框嘛。等真正动手才发现在SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0这套组合下一个看起来普通的管理系统处处都是细节。版本差异、依赖坐标、连接串参数、前端跨域任何一个环节卡住都能耗掉半天。这篇我把完整实现过程拆开来讲从选型理由到表结构设计从后端CRUD到前端页面再到最后的打包上线全程附上我实际验证过的代码和踩坑记录。这套系统适合正在做毕业设计、或者刚接触前后端分离想练一个完整项目的朋友。业务场景是典型的旅游指南游客查景点、看路线、读攻略、收藏点赞管理员做内容维护和用户管理。核心功能不复杂但覆盖了一个全栈项目从零到一的所有必经环节做完这一套再遇到类似系统基本都能直接套用。1. 技术栈选择为什么这套组合是实打实的稳妥方案1.1 四个组件各管哪一段先把这个技术栈的分工理清楚。SpringBoot2负责后端接口内嵌Tomcat打一个jar包就能跑Vue3负责前端页面配合Vite做构建工具开发时热更新非常快MyBatis-Plus是持久层框架或者说是一个增强版的MyBatis单表CRUD几乎不用写SQLMySQL8.0管数据存储。这四个东西合在一起对应一个典型的前后端分离架构前端通过axios请求后端接口后端用MyBatis-Plus操作MySQL数据以JSON格式返回。和早年JSP时代不一样页面和后端逻辑完全分开开发、测试、部署都干净很多。1.2 和旧方案比到底强在哪我在选型时顺手整理过一个对比这里直接放出来方便还在纠结的人看一眼就明白方案页面开发方式后端开发效率部署方式学习资料SSM JSPJSP标签渲染前后端耦合手写大量SQL和XML打war包扔Tomcat资料偏老SpringBoot Thymeleaf模板渲染仍是服务端页面中规中矩jar包直接跑够用但不够现代SpringBoot2 Vue3完全前后端分离MyBatis-Plus大幅减少SQL前端静态文件Nginx反代大量现成教程和组件SpringBoot2负责后端接口内嵌Tomcat打一个jar包就能跑Vue3负责前端页面配合Vite做构建工具开发时热更新非常快MyBatis-Plus是持久层框架也可以理解为一个增强版MyBatis单表CRUD几乎不用写SQLMySQL8.0管数据存储。这个组合放到招聘市场上也很常见做完这个项目再去接触其他框架起点不会低。1.3 为什么特意选SpringBoot2而不是3.x这里聊一点容易被忽略的细节。现在SpringBoot3也普及了但很多院校项目和教学资料仍是2.x体系尤其写毕业设计任务书时SpringBoot2和MyBatis-Plus的搭配最成熟。MyBatis-Plus的mybatis-plus-boot-starter目前对SpringBoot2的支持非常稳定而SpringBoot3在底层有Jakarta命名空间的变化部分老教程里的代码直接搬过去会报错。所以我的建议是做课程设计或毕业设计按SpringBoot2.7.18这个最终版本走所有老坑都已经被前人填平了踩起来心里有底。2. 数据模型设计旅游指南系统的六张核心表2.1 先想清楚业务实体再写表我不建议一上来就写建表语句先把业务实体画清楚。旅游指南系统分两个角色普通游客和管理员。游客侧需要的核心功能是浏览景点、查看旅游路线、阅读攻略文章、收藏内容、发表评论管理员侧需要维护景点、路线、文章数据管理用户状态。从这些需求抽象实体最少需要六张表用户表user、景点表scenic_spot、旅游路线表travel_route、攻略文章表article、收藏表favorite、评论表comment。景点和文章是内容主体收藏和评论是互动记录用户表支撑登录鉴权。表名核心作用关键字段user用户登录与个人信息username, password, nickname, rolescenic_spot景点信息维护name, city, price, heat, statustravel_route推荐路线发布title, days, budget, contentarticle攻略文章管理user_id, title, view_count, contentfavorite收藏关系记录user_id, target_type, target_idcomment评论互动数据user_id, target_type, content2.2 景点表和收藏表的设计要点景点表是最核心的内容表字段设计要同时兼顾展示和查询。我在设计时加了city字段用于城市维度筛选加了heat字段做热度排行用status做上下架控制。这里分享一个实际经验images字段不要单独建子表存多张图片直接在景点表里用一个TEXT类型存JSON数组就行开发简单读取时前端JSON.parse一下就好。对于毕设和中小型项目过度设计反而增加工作量。收藏表用了一个稍微特别的设计target_type加target_id的多态结构。这样一张收藏表既能收藏景点又能收藏路线和文章避免了拆成三张收藏表的麻烦。评论表也用同样的思路。下面是收藏表的建表SQLCREATE TABLE favorite ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, target_type tinyint NOT NULL COMMENT 1-景点 2-路线 3-攻略, target_id bigint NOT NULL COMMENT 目标内容ID, create_time datetime DEFAULT NULL COMMENT 收藏时间, PRIMARY KEY (id), UNIQUE KEY uk_user_target (user_id,target_type,target_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表;这个唯一索引uk_user_target很重要它从数据库层面保证同一个用户不会重复收藏同一个内容。我之前偷懒没加后来接口层判断漏了一个场景导致出现重复收藏数据加了唯一索引再配合前端提示问题就彻底解决了。2.3 排序、分页与索引的取舍MySQL8.0对索引和查询优化做得不错但前提是你得建对索引。我的原则是经常用来查询过滤的字段建普通索引需要保证唯一性的业务字段建唯一索引。比如景点表的city字段、路线表的status字段查询频率很高。而create_time字段默认按时间排序的也顺手加个索引。MySQL8.0和5.7相比有个明显优势支持了窗口函数比如ROW_NUMBER()、RANK()做排行榜类需求非常方便。不过MyBatis-Plus的wrapper不太好直接封装窗口函数我实际开发中为了简洁还是用普通ORDER BY heat DESC的方式实现热度排行性能完全够。3. 后端落地SpringBoot2与MyBatis-Plus 的 CRUD 与业务查询3.1 初始化项目和关键依赖项目用Maven构建Java版本用8或11都行。SpringBoot版本我建议直接锁2.7.18这是SpringBoot2的最后一个小版本修复了大量已知问题。依赖配置如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里特别提醒MyBatis-Plus的3.5.3.1版本和SpringBoot2.7配合是目前最稳的搭配。之前我试过3.5.5以上的一些新版本部分API有调整网上很多教程代码直接复制会编译不过白白浪费时间。3.2 配置文件里的关键参数application.yml是后续排查问题首先要看的地方。数据源配置里有两个容易踩坑的参数serverTimezoneAsia/Shanghai和useSSLfalse。MySQL8.0的时区默认是UTC如果你不显式指定服务器时区插入的时间会比北京时间差8个小时。另外从MySQL8.0开始默认启用SSL认证请求如果不加useSSLfalse本地开发时控制台会刷一堆SSL警告。完整配置看下面spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel_guide?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0allowPublicKeyRetrievaltrue这个参数是和MySQL8.0的caching_sha2_password认证插件配套用的。老版本MySQL驱动不认或者报 “Public Key Retrieval is not allowed”基本都是这个问题。另外开发阶段StdOutImpl日志一定要开它会在控制台打印所有SQL语句排查问题时能直观看到MyBatis-Plus实际执行了什么。3.3 通用CRUD让代码量直接砍半MyBatis-Plus最大的价值就是内置了BaseMapper和IService单表增删改查接口基本不用自己写SQL。一个实体类对应一个Mapper接口继承BaseMapperT就自动获得十几个方法。为了进一步减少代码我写了一个BaseController把保存、删除、修改、按ID查询这四件通用操作抽上去public abstract class BaseControllerT { Autowired protected IServiceT service; PostMapping public ResultBoolean save(RequestBody T entity) { return Result.success(service.save(entity)); } DeleteMapping(/{id}) public ResultBoolean delete(PathVariable Long id) { return Result.success(service.removeById(id)); } PutMapping public ResultBoolean update(RequestBody T entity) { return Result.success(service.updateById(entity)); } GetMapping(/{id}) public ResultT get(PathVariable Long id) { return Result.success(service.getById(id)); } }具体业务Controller继承这个基类再补充自己的查询接口就可以。这个方法在实际开发中非常实用尤其是景点、路线、文章这类内容表通用操作的逻辑几乎一模一样不用每个Controller写五遍重复代码。3.4 业务查询Wrapper用得好SQL不用敲业务查询是体现MyBatis-Plus能力的地方。比如首页要展示10个热门景点直接用LambdaQueryWrapperListScenicSpot hotList scenicSpotService.lambdaQuery() .eq(ScenicSpot::getStatus, 1) .orderByDesc(ScenicSpot::getHeat) .last(limit 10) .list();再比如景点列表页的分页搜索需要按关键词、城市筛选并支持分页PageScenicSpot page new Page(pageNum, pageSize); LambdaQueryWrapperScenicSpot wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(keyword), ScenicSpot::getName, keyword) .eq(StringUtils.hasText(city), ScenicSpot::getCity, city) .orderByDesc(ScenicSpot::getHeat); scenicSpotService.page(page, wrapper);lambdaQuery()这种方式比字符串写字段名安全得多重构实体类字段时不会出现“改了实体名但SQL还是旧字段”的问题。要注意的是Wrapper里的条件判断比如StringUtils.hasText(keyword)这种写法前端不传关键词时条件自动跳过避免查出一堆无关数据。3.5 后端开发中的三个高频雷区第一个雷是LocalDateTime序列化格式不对。如果不配置jackson的时间格式默认输出是一长串时间戳或ISO格式前端没法直接展示。解决办法就是我在3.2节里写的那两行配置全局生效。第二个雷是逻辑删除字段。表里加了deleted字段做逻辑删除后所有查询自动追加deleted0条件但如果你在唯一索引里包含了这个字段删过的数据再插入时可能冲突。实际处理时要么把逻辑删除字段也放进唯一索引要么干脆不用唯一索引在Service层判断。第三个雷是统一返回值问题一定要封装一个ResultT类code、msg、data三段式前端统一处理。我见过不封装直接返回Map的项目联调时改一处漏一处非常痛苦。4. Vue3 前端工程化搭建与页面实现4.1 Vite项目初始化和目录规划前端用Vite创建项目是最快的一条命令搞定npm create vitelatest travel-front -- --template vue cd travel-front npm installNode.js版本要求18以上Vite5起对低版本Node已经不支持了。创建完项目后我把目录结构重新整理了一遍按功能和职责分层这是后面多人协作或者自己维护时降低心智负担的关键src/ ├── api/ # 接口请求定义按模块拆文件 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Pinia 状态 ├── utils/ # 工具函数axios 封装放这里 └── views/ # 页面视图组件项目核心页面包括首页、景点列表页、景点详情页、路线列表页、攻略文章页、个人中心、后台管理页。每个页面一个文件夹页面内的私有组件就近放。4.2 axios封装把请求逻辑收敛到一处axios封装是前端必做的一步。我统一在utils/request.js里创建实例配置基础URL、超时时间并在请求拦截器和响应拦截器里做统一处理。请求拦截器里携带token响应拦截器里统一判断返回码。这样每个API模块都只需要关注数据获取不需要重复写错误处理逻辑。import axios from axios const request axios.create({ baseURL: /api, timeout: 15000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( res { const { code, data, msg } res.data if (code 200) return data alert(msg || 请求失败) return Promise.reject(new Error(msg)) }, err { alert(网络异常请稍后重试) return Promise.reject(err) } ) export default request注意这里的baseURL: /api是一个通用做法。开发环境让Vite代理转发到后端生产环境让Nginx代理转发前端代码完全不用改。具体的API模块建议按页面拆分例如api/scenic.jsimport request from /utils/request export function getHotScenicList() { return request.get(/scenic/hot) } export function getScenicPage(params) { return request.get(/scenic/page, { params }) } export function getScenicDetail(id) { return request.get(/scenic/${id}) }4.3 核心页面实现与组件拆分思路景点列表页是最典型的列表场景。我把它拆成搜索栏、筛选栏、列表卡片、分页器四个子组件然后用父组件统一管理数据请求和状态。Vue3的ref声明响应式数据onMounted里调用接口。分页参数和搜索参数放在同一个响应式对象里每次搜索或翻页时调用同一个请求方法。这样逻辑非常清晰一个方法负责组装参数一个方法负责请求数据一个变量负责承载列表。详情页要注意的参数是/scenic/:id用useRoute()拿路由参数然后调用详情接口。页面里包含景点图片轮播、基本信息、地图位置、游客评论几个板块。评论提交后会刷新评论列表这里我用了一个简单的reloadKey递增的方式强制子组件重新渲染比手动调用子组件方法省事。4.4 列表搜索条件保留的坑与解法这个是实际开发中我折腾最久的一个点。用户在景点列表页搜索了“杭州”点进一个景点详情再返回列表页时Vue3默认会重新创建组件搜索关键词和页码全丢了体验非常差。Vue2时代也遇到过这个问题Vue3里解法有几种我最推荐的是用keep-alive配合路由meta配置// 路由配置 { path: /scenic, name: ScenicList, component: () import(/views/scenic/ScenicList.vue), meta: { keepAlive: true } }template router-view v-slot{ Component } keep-alive :includecachedViews component :isComponent / /keep-alive /router-view /template注意缓存之后组件不会重新走onMounted所以列表数据的拉取要放到onActivated钩子里。onMounted只在首次进入时触发onActivated每次从缓存中返回都会触发这样既能保留搜索条件又能刷新最新数据。这个坑如果不提前处理后面被导师或产品提出来返工成本非常高。4.5 Vue3开发中的几个习惯转变Vue3的Composition API确实好用但习惯养成需要时间。我自己的经验是模板里的v-model、v-for用法基本没变但生命周期全部改名了beforeDestroy变成了onBeforeUnmountdestroyed变成了onUnmounted。组件通信方面$emit变成了defineEmits$props变成了defineProps。响应式数据一定要搞清楚ref和reactive的区别基础类型用ref对象数组用reactive。但也不是绝对ref会自动拆包很多场景统一用ref反而省心。前端引入UI框架我用的是Element Plus组件标签和Element UI基本一致文档也全。用Vite项目建议装一个unplugin-vue-components插件实现按需引入打包体积会小不少。还有一个细节Vite环境变量要用import.meta.env直接用process.env会在浏览器里报process is not defined这个是Vue3和Vite的项目经常遇到的低级坑。5. MySQL8.0 安装与连接本地、Docker 和驱动配置5.1 版本选择为什么锁定MySQL8.0现在很多旧教程还在用MySQL5.7新项目建议直接上8.0。原因有几个一是8.0的默认字符集是utf8mb4对中文和emoji支持更好不需要额外改配置二是8.0在查询优化器、窗口函数方面有明显改进三是企业环境里8.0已经是主流学新不学旧。MySQL8.0和5.7最大的区别之一就是默认认证插件变成了caching_sha2_password这个后续连接数据库时会直接影响驱动选择。5.2 Windows本地安装的实操要点本地开发我在Windows上装的是zip版MySQL8.0解压后配置环境变量然后执行初始化命令mysqld --initialize-insecure --usermysql net start mysql--initialize-insecure的意思是初始化时生成一个空密码的root用户本地开发方便。初始化完成后用mysql -u root -p登录密码直接回车。然后执行ALTER USER rootlocalhost IDENTIFIED BY 123456;设置新密码。注意MySQL8.0的密码规则默认要求至少8位如果你用“123456”这种短密码会报错需要先执行下面这条命令把密码强度调低SET GLOBAL validate_password.policy LOW;这是很多教程没写到的细节。第一次在MySQL8.0里设短密码失败时不要慌十有八九就是这个策略问题。5.3 Docker安装方式五分钟起一个实例如果不想污染本地环境用Docker是更好的选择。一条命令就能起一个带数据目录的MySQL8.0docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -v mysql-data:/var/lib/mysql \ mysql:8.0.33 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci-v mysql-data:/var/lib/mysql是数据卷挂载容器删除后数据还在这个一定要加。TZAsia/Shanghai是为了容器内时间为北京时间。Docker方式对毕设答辩很有用直接把容器打包到演示环境不用现场折腾数据库安装。5.4 连接串和驱动最后一次踩坑提醒后端连接MySQL8.0驱动类必须是com.mysql.cj.jdbc.Driver老驱动com.mysql.jdbc.Driver在新版本中已经不能用了。连接串里必须包含时区和编码参数这是我反复强调的jdbc:mysql://localhost:3306/travel_guide?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue缺少allowPublicKeyRetrievaltrue时连接可能会报Public Key Retrieval is not allowed异常缺少serverTimezone时时间和本地对不上缺少characterEncodingutf8时中文可能乱码。这三个参数我建议每次写连接串都直接带上不管本地还是线上环境。6. 联调、打包与上线让前后端跑在同一套环境6.1 开发环境跨域用代理而不是CORS前端开发服务器在5173端口后端在8080端口跨域问题必然会遇到。我的建议是优先用Vite代理解决而不是在后端开启CORS。代理的好处是线上环境可以用Nginx做同样的事前端代码零改动。在vite.config.js里配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/scenic/page时Vite会转发到http://localhost:8080/api/scenic/page。后端接口统一以/api开头既方便代理规则收敛也方便后期在Nginx层做统一路由。6.2 前端打包和后端构建一条命令一个文件前端构建npm run build生成dist目录里面是纯静态文件。后端构建mvn clean package -DskipTests生成一个可执行的jar包。整个系统的部署形态就变成了Nginx托管前端静态文件同时把/api请求反代到后端jar端口。jar包拉起后端服务java -jar travel-guide-0.0.1-SNAPSHOT.jar --server.port8080这种部署方式的最大好处是前端文件可以随意丢到任意Web服务器上后端无状态多实例部署也方便。6.3 Nginx配置不要让刷新页面变成404前后端分离部署里最容易踩的坑是前端路由用了history模式刷新某个二级页面时Nginx找不到对应的静态文件直接返回404。解决办法是Nginx配置里加一个try_files所有路径都回退到index.htmlserver { listen 80; server_name your-domain.com; root /opt/travel/dist; 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; } }图片和上传文件静态访问也可以走这个Nginx把上传目录单独映射一个location。流量不大时一个Nginx加一个jar包就是一套完整的旅游指南系统上线架构。6.4 上线的最后几个提醒上线后最容易出的问题集中在文件和端口上。文件上传的路径要写成绝对路径不要用相对路径否则jar包在不同目录下启动时容易找错文件夹。端口要注意服务器安全组是否放行了80和8080。数据库连接串里的IP开发环境用localhost、线上环境要改成云数据库的内网地址这个改漏了会白白排查很久。日志方面启动jar时建议加一句java -jar travel-guide-0.0.1-SNAPSHOT.jar app.log 21 app.log里记着SpringBoot的全部运行日志排查问题基本都从它入手。最后再说几句大实话这个项目做完一遍之后我最大的体会是全栈项目真正的难点不在某个单一技术而在技术之间的衔接处。数据库驱动、连接参数、跨域配置、打包部署这些问题单独拿出来都不难但它们散落在整个链路里每到一个环节就会卡一次。把这条链路完整走通一次胜过看十遍教程。后面如果还想继续扩展方向也很明确热度排行可以用Redis做缓存减少数据库压力用户体系可以接Spring Security做细粒度权限管理员端可以做一个数据看板统计每日访问量。小程序端需要开发时也可以直接复用这套后端接口前后端分离的优势在这时候就体现出来了。就先写到这里如果你正在做类似的项目希望这篇能帮你少踩几个坑。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →