Spring Boot+Vue旅游指南系统:从数据建模到部署全解析
1. 项目概述与核心需求拆解1.1 旅游指南系统到底在做什么这段时间我利用业余时间完整整理并复现了一套基于Spring Boot Vue的旅游指南系统交付形态包括后端源码、前端页面、数据库初始化脚本和配套说明文档。简单说它不是一个只展示景点的静态网站而是一个带有用户注册登录、景点浏览、线路推荐、攻略文章、收藏、评论、后台管理等功能的完整业务系统。旅游指南系统这类项目在实际场景里通常被用在旅游信息门户、地方文旅宣传平台、高校课程设计或者旅行社内部系统。最开始接到这个需求时我并没有急着写代码而是先在纸上把“用户端要做什么、管理端要做什么”列清楚。这类系统最大的难点不在功能多少而在信息结构是否清晰。景点、线路、攻略、用户、评论、收藏之间天然存在一堆一对多和多对多的关系。如果一开始不把数据库关系想透后面进到前后端联调阶段必然要反复返工。我的做法是先梳理概念模型再转成物理表结构最后才开始搭接口和页面。另一个容易被低估的问题是“演示数据”。很多人拿到一份能跑的项目结果数据库里只塞了两条测试数据景点列表看起来又空又没有说服力。真正优秀的一套源代码交付应该是数据库里带有接近真实场景的演示数据这样你打开首页就能看到轮播图、景点卡片、攻略列表、评论记录而不是面对一张张空表。这也是我把“源码数据库文档”三件套并列的原因代码只是一部分能不能让接手的人快速跑起来才是关键。1.2 技术选型为什么是Spring Boot Vue很多人在做类似系统时纠结过后端用SSM还是Spring Boot前端用JSP还是Vue。我的体会是想在短时间内交付一套看得见、摸得着的系统Spring Boot Vue的组合目前是最稳妥的路线之一没有严重短板社区资料也足够多。Spring Boot的价值在于自动配置和快速启动。它和MyBatis、MySQL、拦截器、跨域配置这些东西都有非常成熟的集成方案基本不需要手动维护一堆XML配置。对旅游指南这种以CRUD为主、附带部分查询统计的业务系统来说Spring Boot配合MyBatis-Plus能把样板代码压到最低。单表增删改查交给通用Mapper处理复杂一点的统计查询用自定义SQL或QueryWrapper条件构造器就能完成不需要额外引入重量级框架。前端这边我选择Vue Element-UI组合。Element-UI的表格、表单、分页、轮播、弹窗组件非常齐全做管理后台和内容展示类页面效率极高。如果现在新起项目直接用Vue 3 Element Plus也可以差别主要体现在响应式API和组件注册方式核心设计思想不变。旅游指南系统的大部分前端工作集中在卡片列表、详情页面、表单提交和路由跳转上正好是Vue组件化思想的舒适区。1.3 这类系统适合哪些人来复现如果你是刚入门前后端分离开发的人这套旅游指南系统是非常好的练手素材。功能点覆盖了用户模块、内容展示模块、用户交互模块但又不会难到让人劝退。你准备拿来做毕业设计或项目实战案例同样合适。它不像电商系统那样需要处理复杂的订单状态机也不像社交系统那样要考虑高并发但在登录鉴权、图片上传、分页搜索、评论回复这些关键点上该有的基本全都有。复现一遍这条路走下来你对Spring Boot后端如何组织接口、Vue前端如何调用接口、MySQL表结构如何设计都会建立整体认知。我建议不要只是把代码跑起来而是动手改一两个功能点比如加一个“景点标签”字段或者把首页改成按地域筛选景点。只有亲手改过一遍你才知道哪些地方是写死逻辑哪些地方有良好扩展点这种判断力比代码本身值钱得多。2. 整体设计思路与数据建模2.1 前后端分离的架构怎么搭整套系统在架构上拆成三个部分Vue前端应用、Spring Boot后端应用、MySQL数据库。前端通过Axios发起HTTP请求后端暴露RESTful接口数据以JSON格式返回。这样做的好处是前后端各管一摊前端专注页面交互后端专注接口稳定和数据安全。实际部署时前端打包成静态资源交给Web服务器后端打包成Jar包独立运行。搭建目录结构时我习惯约定优于配置。整个项目按以下结构组织travel-guide-frontend/ # Vue前端工程 travel-guide-backend/ # Spring Boot后端工程 travel-guide-database/ # SQL脚本与初始化数据后端内部再按controller、service、mapper、entity、config、common六个包划分。这套结构非常常规但正是这种常规让后期维护和交接几乎不需要额外解释。很多人项目翻车不是因为代码写不出来而是目录乱到连自己三天后再看都不知道某个方法放在哪里。保持清晰的包结构把配置类放到config包里统一管理把返回消息封装类放到common包里后续加功能时会发现思路非常顺畅。2.2 数据库表设计核心表字段与关系数据库是旅游指南系统的地基我在这一块花的时间也最多。核心表主要有六张用户表、景点表、线路表、攻略表、评论表、收藏表外加一张用于配置首页分类的类目表。表关系大概是一个分类下面有多条景点一条景点可以被多个用户收藏一个用户可以收藏多个景点评论挂在景点或者攻略下。用户与景点之间是多对多关系通过收藏中间表解耦用户与评论之间是一对多关系。核心表结构可以简化为这样CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(255) NOT NULL, nickname VARCHAR(50), avatar VARCHAR(255), phone VARCHAR(20), role TINYINT DEFAULT 0 COMMENT 0-普通用户 1-管理员, create_time DATETIME ); CREATE TABLE t_scenic ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, category_id BIGINT, cover VARCHAR(255), summary TEXT, content LONGTEXT, address VARCHAR(255), open_time VARCHAR(50), ticket_price DECIMAL(10,2) DEFAULT 0, view_count INT DEFAULT 0, status TINYINT DEFAULT 1 );其他表的思路类似攻略表有一个author_id字段指向用户表景点的主体内容用LONGTEXT来存富文本评论表用target_type和target_id区分评论的是景点还是攻略收藏表把user_id和scenic_id做成唯一索引防止重复收藏。这里重点提醒一下不要把景点、线路、攻略全塞进一张大表那样虽然省事但字段会非常混乱查询统计也不好写。分表设计初看费事后期却会省心很多。所有时间字段统一用datetime布尔状态统一用tinyint金额统一用decimal这些约定一定要提前定好否则不同人写的代码风格会非常割裂。2.3 接口规划与权限控制接口层我分成四大块用户相关、景点内容相关、用户交互相关、后台管理相关。用户相关有登录、注册、登出、获取当前用户信息内容相关有景点分页列表、景点详情、攻略列表、线路推荐交互相关有评论列表、发表评论、收藏与取消收藏后台管理相关则是对景点、攻略、分类、用户的管理接口。权限控制没有引入Spring Security这类重型框架只是用登录拦截器加注解的方式。核心思路是用户登录成功后签发一个Token前端把Token放在请求头里后端拦截器校验Token是否存在以及是否过期。后台管理接口再额外校验用户角色要求必须是管理员。这种轻量级方案做旅游指南系统足够了毕竟它不是高敏感的金融业务不需要引入复杂的权限模型。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (!tokenService.check(token)) { response.setStatus(401); return false; } return true; }有个极其容易踩的坑就是预检请求。前后端分离环境下浏览器发复杂请求之前会先发一个OPTIONS预检请求这个请求不会带Token如果拦截器没有提前放行OPTIONS前端所有带自定义Header的请求都会失败。我在第一次联调时就吃过这个亏排查了半天才发现是拦截器把预检请求拦掉了。3. 核心功能模块的落地实现3.1 后端骨架实体、Mapper、Service、ControllerSpring Boot后端的主类、配置文件、启动流程都比较固定我用MyBatis-Plus来写数据层。实体类用TableName注解映射表名字段上用TableId和TableField处理主键和列名不一致的问题。下面这段是景点实体的典型写法Data TableName(t_scenic) public class Scenic { TableId(type IdType.AUTO) private Long id; private String name; private Long categoryId; private String cover; private String summary; private String content; private String address; private String openTime; private BigDecimal ticketPrice; private Integer viewCount; private Integer status; }查询条件的封装是列表页的重点。景点列表通常需要按分类筛选、按关键词搜索、按浏览量或价格排序。用LambdaQueryWrapper拼条件时最关键的是eq和like方法前面的布尔参数它能把前端传的空值自动忽略掉避免拼出多余SQL条件。实际联调时我遇到过不少次前端传了空字符串导致SQL条件异常用这个写法可以一劳永逸。public IPageScenic pageScenic(int page, int size, Long categoryId, String keyword) { PageScenic p new Page(page, size); LambdaQueryWrapperScenic wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Scenic::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Scenic::getName, keyword) .orderByDesc(Scenic::getViewCount); return scenicMapper.selectPage(p, wrapper); }Controller层我习惯保持薄Controller模式只做参数接收、调用Service、返回统一结果这三件事。不要为了省事把业务逻辑全堆在Controller里否则后期加一个权限校验或者改一个查询逻辑就要动好几个方法。统一返回结果我会封装一个Result类包含code、message、data三个字段前端响应拦截器根据code判断业务成功还是失败这样接口返回格式在整个项目里保持一致。3.2 前端骨架Vue项目结构、Axios封装、页面组合前端用Vue CLI创建项目后按views、components、router、api、utils几个目录组织。api目录下建议按业务模块拆文件比如user.js、scenic.js、comment.js每个文件里放对应的请求函数。这样做的好处是页面组件里不会到处散落axios调用接口一旦有调整只改一个文件。Axios封装是前端少不了的环节。我在utils/request.js里设置baseURL、请求超时、请求和响应拦截器。请求拦截器负责把Token加到Header响应拦截器负责统一处理业务状态码和401跳转。import axios from axios; const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) config.headers.Authorization token; return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { if (res.code 401) { window.location.href /login; } return Promise.reject(new Error(res.message)); } return res; }, error { return Promise.reject(error); } ); export default service;页面组合方面用户端首页用轮播图展示推荐景点下面按分类展示景点卡片列表页用分页组件和筛选条件联动详情页用路由参数加载景点详情同时拉取评论列表管理端用后台模板布局左侧菜单分类管理、景点管理、攻略管理、用户管理右侧用router-view切换页面。表单提交之前用rules做一次前端校验可以减少后端错误提醒的次数用户体验会好很多。3.3 联调阶段的数据流与传参规范前后端联调阶段最容易出的问题除了跨域就是参数类型不一致。比如后端分页接口的参数是page和size前端传的是pageNum和pageSize接口就会一直查不出数据。我后来统一约定分页参数固定用page和size时间参数统一用字符串金额统一用数字布尔值统一用0和1。联调之前先整理一份简单接口文档前端按文档来传参问题能少一大半。图片上传的处理需要单独说。景点封面、用户头像这类图片后端要提供一个统一的上传接口保存到服务器指定目录返回一个可访问的URL。数据库里只存URL或相对路径不要存二进制流。前端在提交表单时先上传图片拿到URL再把URL放进表单数据提交整个流程会更顺畅。如果遇到图片上传后刷新页面无法显示优先检查上传目录是否有读写权限以及静态资源映射是否配置正确。浏览量也是一个容易被忽略的点。景点详情页每打开一次view_count要加1。不要用先查询再更新这种方式应该直接执行原子更新UPDATE t_scenic SET view_count view_count 1 WHERE id ?。这样能避免并发环境下丢更新。越是简单的逻辑越要在细节上做对这就是“看着简单、实际有坑”的典型例子。4. 部署上线与实测记录4.1 本地跑通项目的完整步骤本地跑通整套系统的步骤比较标准我整理下来大概五步。第一步准备好环境基础组件JDK 8或17、MySQL 8、Node.js 14以上、Maven用于构建后端。第二步在数据库里执行init.sql自动建库建表并插入演示数据。第三步修改后端application.yml里的数据库用户名和密码启动Spring Boot应用确认8080端口能访问。第四步进入前端目录执行npm install修改src/utils/request.js里的baseURL指向后端地址然后执行npm run serve。第五步浏览器访问前端地址注册一个普通用户测试首页景点列表、详情、收藏、评论这些功能是否能正常走通。这段话读起来平淡实际执行时最大的坑反而集中在环境版本上。比如MySQL 8默认的认证插件是caching_sha2_password某些老版本驱动连接时会报认证错误需要把驱动版本升级到8.x。又比如Node版本过高Vue CLI某些老版本会报OpenSSL错误需要设置NODE_OPTIONS --openssl-legacy-provider。这类情况属于“环境问题占八成”代码本身不一定有问题但会让人排查得心力交瘁。4.2 部署到服务器之后遇到的关键问题本地跑通只是第一步部署到服务器后我遇到了几个很典型的坑。第一个是跨域配置失效。本地开发时前端是localhost:8080后端是localhost:9000属于不同端口必须依赖跨域配置。上线后如果把前端打包文件放到后端同目录下由同一端口提供服务跨域问题自然消失。但如果前端静态资源部署到独立端口就必须确认后端跨域配置允许的Origin包含线上域名而不是只写localhost。第二个是静态资源404。前端build之后生成dist目录资源路径默认相对当前域名根目录。如果部署到子路径比如http://ip:8080/travel/需要在vue.config.js里配置publicPath。最初我没配导致刷新页面后所有JS和CSS都404页面直接白屏。这个坑不是代码逻辑问题而是构建配置问题很多人特别容易忽略。第三个是MySQL连接失败。服务器上数据库的账号权限、防火墙端口、连接串里的serverTimezone和useSSL参数任何一个不对都会导致连接异常。我最后把连接串稳定为下面这种写法jdbc:mysql://localhost:3306/travel_guide?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai这里整理一个排查速查表遇到类似问题可以直接对照现象常见原因处理方法前端所有接口返回401拦截器没放行OPTIONS预检拦截器中对OPTIONS请求直接返回true页面刷新后JS/CSS 404publicPath配置不对vue.config.js里设置publicPath为子路径后端启动时报数据库连接失败驱动版本或时区参数不对升级MySQL驱动连接串加serverTimezone图片上传成功但访问404上传目录没有静态资源映射配置资源映射器把本地目录暴露为URL前端跨域报错允许的Origin不对在CORS配置中加上实际的线上域名4.3 运维层面的常见坑再补充几个关于数据和日志的实操经验。第一数据库备份不能只靠手动导出。我见过有人把数据放在一台没做备份的服务器上结果一次误删表只能凭记忆补数据。旅游指南项目虽然数据量不大上线后依然建议配置每天定时备份或者至少每周导出一次SQL文件。真出问题的时候备份就是救命稻草。第二日志一定要分级。后端不要把所有日志全堆在控制台要把错误日志单独写到文件里方便排查问题。可以在logback配置里对ERROR级别单独输出并带上请求参数和异常堆栈。很多部署阶段的问题比如用户反馈登录失败一看ERROR日志就能快速定位而不是靠猜。上线系统不是“能跑就行”而是要让你在出问题时能用最短时间找到原因。第三图片存储目录要单独规划。如果后端把图片写到应用根目录下的upload文件夹那么重新部署Jar包时可能会把图片一起覆盖或删除。比较稳妥的做法是配置一个独立上传目录再通过静态资源映射接口对外提供访问。这样升级代码和图片数据互不影响。5. 项目交付与文档组织的个人经验5.1 源码、数据库、文档三件套怎么整理这个项目从一开始我就给自己定了目标不只是把代码跑通而是把整个项目整理成“源码数据库文档”三件套的可交付状态。太多人写项目只关注“能不能跑”到交付时临时凑文档效果非常差。我更建议把文档写作平摊到开发过程中每天结束前花20分钟记录当天做了什么、改了哪些接口、数据库有哪些变化。这样项目做完时文档主体内容已经齐了不用返工。交付时我会准备三样东西。第一份是系统介绍文档说清楚项目背景、技术栈、主要功能、运行环境和启动步骤。第二份是数据库说明文档把每张表的用途、核心字段含义、表关系画清楚附上初始化SQL。第三份是接口文档列出主要接口的请求方式、路径、参数和返回示例。这三份文档不需要写成学术论文但要把“怎么把它跑起来”和“每个模块怎么改”写明白。特别要强调数据库文档的价值。接手代码的人往往第一件事就是打开数据库看表结构如果每张表没有注释字段含义靠猜理解成本会成倍增加。SQL脚本里尽量给每个字段加上COMMENT表名和字段名用统一的命名规范。这套习惯放在任何数据库项目里都适用。5.2 给做同类型系统的人的建议踩过几轮坑之后我的个人建议可以浓缩成三条。第一条建表前先画关系图不要在写代码过程中频繁改表。改一次表意味着实体类、Mapper、Service、前端页面都要跟着动工作量是放大倍数而非加法。第二条前端先搭好Axios和路由骨架再去做页面细节。骨架确定后所有页面都共用同一套请求封装和错误处理逻辑哪怕别人接手也能很快看懂请求是怎么发出去的。第三条测试数据要有代表性。旅游指南系统如果只有两条景点数据和一条评论数据列表页分页效果根本测不出来。数据尽量模拟真实场景比如十个分类、每个分类几篇攻略、用户评论和收藏相互交叉这样大概率在联调阶段就能发现隐藏Bug。最后再分享一个小经验把这个系统从零搭起来并不难真正有价值的部分是数据建模、权限控制和部署排错这套完整思路。我虽然只是做了一个案例但中间积累的很多细节放在其他管理类系统上同样适用。做项目最好的方式不是一口气写完所有代码而是分阶段验证每个阶段都跑通一遍再继续后面就会发现整个过程顺畅很多。