做旅游类web系统的人应该不少但真正把“文化旅游”这个方向做明白的其实不多。我最近正好搭完一个基于SpringBootVue的七彩云南文化旅游网站管理系统后端用Java、MySQL和MyBatis做数据层整体算是典型的单体前后端分离项目。这篇文章就把整个系统的设计思路、数据建模、关键接口实现、前端落地细节以及源码跑起来的完整流程都梳理一遍给准备做同类文旅系统的朋友一个可参考的底子。很多人在拿到“旅游网站管理系统”这类需求时第一反应就是套用通用的增删改查模板用户管理、文章管理、分类管理齐活。但真落到云南文旅这个场景里你会发现数据的关联方式、内容展示的形式、前后台的交互逻辑都比普通内容管理系统复杂一大截。这篇文章涉及的内容适用于正在做旅游类毕设、企业实训项目或者单纯想搞懂SpringBootVue整合的同学所有表结构和接口设计都建立在真实可跑的思路上。1. 文旅网站的管理系统难点不在编码而在业务拆解先聊一个容易被忽略的点技术本身不复杂复杂的是先想清楚——一个文旅网站里到底有哪些角色、哪些内容、内容之间什么关系。很多人一上来就建表结果做到一半发现景点和线路之间缺关联攻略模块和用户模块对不上最后返工改表结构非常痛苦。1.1 三种核心角色决定了权限模型我在设计这个系统时把用户分成三类游客不登录也能浏览景点、旅游线路、攻略文章主要覆盖的是“吸引用户来”的场景。注册用户可以收藏景点和线路、发表攻略、参与评论需要登录态。后台管理员维护分类、景点、线路、攻略的发布与审核管理公告通知。这个权限模型决定了后端的拦截范围。比如景点列表页可以匿名访问但收藏、发攻略、评论就必须持有有效token。因此我在后端单独写了一个登录拦截器HandlerInterceptor配合JWT做接口级保护而不是直接引入Spring Security那么重的安全框架。对这类中小型文旅系统来说轻量鉴权更直观调试也方便。1.2 内容实体不是“文章”那么简单云南文旅题材跟普通新闻站最大的区别在于内容实体的多样性。一个完整的系统至少需要覆盖四类核心内容内容实体典型字段后台动作景点景区名称、封面、城市、等级、门票价、经纬度、介绍新增编辑、删除、上下架旅游线路标题、天数、价格、亮点、关联多个景点维护线路与景点的多对多关系文旅攻略标题、作者、城市、正文、封面发布、审核、驳回、置顶公告资讯标题、内容、发布时间发布、下线这些实体之间不是孤立的线路要关联景点攻略要关联用户评论和收藏要同时兼容多类目标对象。比如我可以收藏一个景点也可以收藏一篇攻略这时候如果给每类内容各建一套收藏表代码会非常冗余如果用一张收藏表加targetType字段区分通用性就会好很多。1.3 模块落地清单整个系统在实际拆分时我按照下面这个模块列表来划分开发任务前台展示模块网站首页、景点列表与详情、线路列表与详情、攻略列表与详情、公告列表。用户侧模块注册登录、个人中心我收藏的、我发布的攻略、评论留言。后台管理模块仪表盘概览、景点管理、线路管理、攻略审核、分类管理、公告管理、用户管理。每个模块内部还是常规的CRUD但模块之间互相引用数据所以开发顺序建议先做数据建模和基础表结构再做后端接口最后做前端页面。如果跳过业务拆解直接写代码后面对接接口时会频繁遇到字段不足或关联结构不对的问题。2. 技术选型复盘SpringBootVueMyBatisMySQL组合的取舍逻辑这套技术栈几乎是国内web系统最主流的组合之一但选它不是因为“大家都在用”而是它在文旅业务里确实合适。我用一段话把每个组件的角色说清楚。2.1 后端SpringBoot是业务复杂度的安全底座SpringBoot最核心的价值是自动配置和生态整合。我在这套系统里用到了Web开发、数据校验、AOP请求日志、定时任务等多个能力全靠starter引入。内嵌Tomcat也让部署变成“一个jar包跑起来”不需要单独装容器。有人会问为什么不选Spring Cloud或者微服务拆分我的观点很明确这个项目的内容量、并发规模、团队维护成本都决定它应该是一名单体应用。微服务的服务发现、配置中心、链路追踪在单体系统里全是负担。做技术选型先看业务复杂度文旅内容展示和内容管理场景用单体足够稳定。2.2 持久层MyBatis对复杂查询非常友好MyBatis我用的是MyBatis-Plus这个选择从开发效率上来讲比纯JPA更直白。尤其是景点分页搜索、线路关联景点查询这类sql写到Mapper里自己掌控逻辑清晰调优也简单。举一个例子线路详情页要同时展示线路基本信息和关联的所有景点。这个查询涉及route表、route_scenic关联表、scenic表三张表用MyBatis可以写一条连表查询直接取出景点列表也可以先查线路信息再查关联景点。这两种方式各有适用场景我用后者做了两层查询并用一个事务保证一致性代码上的可读性比一条超级大JOIN更好。2.3 前端Vue负责解决两套界面的复用问题Vue在我看来最舒服的部分是组件化这套系统同时存在用户端展示页面和管理端表格页面组件复用能省大量时间。分页组件、图片上传组件、富文本编辑器、状态标签组件这些在前后台都会被用到抽成通用组件后开发效率和维护体验都明显提升。前端工程我采用Vue2 Element UI/前端框架组件库因为生态成熟稳定组件文档齐全做管理端表单和表格基本是搭积木。用户端展示页则自己写样式保证视觉效果更贴合旅游站点的氛围。2.4 数据库与版本建议JDK1.8即可不要盲目上17很多老依赖在17下有兼容问题。Spring Boot2.7.x稳定且够用。MySQL5.7或8.0都可以但要注意连接驱动和时区参数。Node环境Vue2项目建议用Node 14或16更高版本容易撞上依赖编译问题。这套选型最后跑下来的结果是开发效率高、部署成本低、排错容易。对该项目而言我不觉得有更优的替代方案。3. 数据库结构设计把景点、线路、攻略串成一张网这部分是整个系统的地基。我在建表前花了一整天梳理实体关系最终确定了九张核心表。下面的设计可以直接作为源码运行时的初始表参考。3.1 核心表清单与设计意图表名核心用途重点关注字段t_user用户账号username、password、role、statust_category景区/内容分类parent_id支持二级分类t_scenic景点信息category_id、city、level、price、lat、lngt_route旅游线路title、days、price、summaryt_route_scenic线路与景点关联route_id、scenic_id、day_num第几天去t_strategy文旅攻略user_id、city、status审核状态t_comment通用评论target_type、target_idt_favorite通用收藏user_id、target_type、target_idt_notice公告资讯title、content、status3.2 两张关键表的建表SQL建表一定要考虑到索引和状态字段。下面是景点表和线路景点关联表的示例这两张表是查询频率最高的。CREATE TABLE t_scenic ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) DEFAULT NULL COMMENT 分类id, name varchar(100) NOT NULL COMMENT 景点名称, cover varchar(255) DEFAULT NULL COMMENT 封面图, images text COMMENT 图集url逗号分隔, city varchar(50) DEFAULT NULL COMMENT 所在城市, address varchar(255) DEFAULT NULL COMMENT 详细地址, level varchar(20) DEFAULT NULL COMMENT 景区等级, price decimal(10, 2) DEFAULT 0.00 COMMENT 门票价格, lat varchar(30) DEFAULT NULL COMMENT 纬度, lng varchar(30) DEFAULT NULL COMMENT 经度, description text COMMENT 简介, status tinyint(1) DEFAULT 1 COMMENT 1显示 0隐藏, view_count int(11) DEFAULT 0 COMMENT 浏览次数, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_city (city), KEY idx_status (status) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT景点信息表; CREATE TABLE t_route_scenic ( id bigint(20) NOT NULL AUTO_INCREMENT, route_id bigint(20) NOT NULL COMMENT 线路id, scenic_id bigint(20) NOT NULL COMMENT 景点id, day_num int(11) DEFAULT 1 COMMENT 行程第几天, sort_order int(11) DEFAULT 0 COMMENT 当日排序, PRIMARY KEY (id), KEY idx_route (route_id), KEY idx_scenic (scenic_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT线路景点关联表;3.3 我在建表时避开的设计坑第一个坑是图集字段。景点详情往往十几张图有人习惯设计一个scenic_image子表这当然严谨但查询时每次都要额外查一次子表。我的做法是在scenic表里用images文本字段存逗号分隔的url数组读取时拆分字符串就行。对于展示类业务这种轻度冗余能减少一次关联查询。第二个坑是通用评论和收藏的target_type设计。类型字段我用scenic、route、strategy三个枚举值来表示关联不同内容用target_id存对应主键。好处是省了三套表代码里只需要一个类型判断。第三个坑是软删除与status字段。直接物理删除景点会导致历史线路的关联数据悬空所以我给所有内容表都保留了status字段下架只是改状态不删数据这对文旅网站来说很重要。4. 后端核心逻辑从登录鉴权到内容发布审核的实现细节数据库定了之后开发顺序建议是先写统一返回结构和异常处理再写JWT登录鉴权然后按业务模块逐个做接口最后写攻略审核和统计逻辑。4.1 统一返回结构是联调效率的关键前后端联调时最怕每个接口返回的JSON都不一样。我在项目里定义了一个通用Result类Data public class ResultT { private Integer code; 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; } }搭配RestControllerAdvice全局异常处理器业务代码里就算抛出空白异常前端拿到的结构也是固定的。这个设计看起来基础但真的能省下很多为“字段对不上”而浪费的调试时间。4.2 JWT登录与拦截器的整合方式登录流程很常规接收用户名和密码校验通过后生成JWT返回前端前端把token放在请求头Authorization里。为了防止“登录失效”这类错误在前后端闹乌龙我单独写了一个AuthInterceptor做三件事对白名单路径登录、注册、首页列表、景点详情直接放行。读取header中的token解析失败抛401异常。解析成功后将userId存入ThreadLocal方便Service层获取当前登录用户。实际开发里我非常建议统一管理白名单路径不然一旦漏掉某个地址前端未登录也会被拦截排查半天发现是忘记配置白名单特别影响节奏。4.3 景点分页检索多条件组合查询景点列表是用户端访问最频繁的接口我设计成了同时支持关键词、城市、分类、价格区间、分页参数的多条件查询。在Mapper层使用动态SQL拼接MyBatis的where标签和if标签正好解决空条件问题。查询接口的请求参数大致是pageNum、pageSize、keyword、city、categoryId、status。后台管理列表多传一个status参数用户端强制传status1以此区分“所有景点”和“已上架的景点”。4.4 线路详情关联景点的组装逻辑线路详情接口是后端逻辑里最值得说的一块。它返回的数据结构大概是{ id: 1, title: 昆明-大理-丽江经典五日游, days: 5, price: 2680, scenicList: [ {dayNum: 1, scenics: [{id: 1, name: 石林}, {id: 2, name: 滇池}]}, {dayNum: 2, scenics: [{id: 3, name: 大理古城}]} ] }直接查一张表是拿不到分组结构的。我的实现策略是先查线路基本信息再查关联景点的原始列表最后用Java8的Collectors.groupingBy按dayNum分组再转换成前端需要的嵌套结构。这个组装逻辑放在Service层Controller只负责接收请求和返回结果保持单层职责单一。4.5 攻略发布与审核流攻略是用户生成内容不能发布后直接展示必须走审核流程。我在t_strategy表里用status字段驱动状态机0表示待审核1表示审核通过2表示驳回。管理员在后台操作时审核通过会同步把状态改成1驳回则记录驳回原因用户端只查询status1的攻略。这里有一个容易被忽略的点攻略正文是富文本内容里边的图片地址是上传到本地的审核时管理员端的编辑器需要能正常渲染图片所以后台上传图片的存储路径和访问映射必须提前配好否则审核界面会看到一堆碎图。5. 前端落地一个工程里同时管理用户端和后台管理端前端部分我选择了单工程多路由的结构而不是拆成两个独立项目。原因很简单公共组件、axios封装、登录状态管理可以共用一份代码减少重复工作量。5.1 路由组织与导航守卫整个前端用Vue Router做路由分层/前缀下的页面首页、景点列表、景点详情、线路列表、线路详情、攻略列表、攻略详情、登录注册。/admin前缀下的页面后台登录、仪表盘、景点管理、线路管理、攻略审核、公告管理、用户管理。导航守卫做两级判断进入/admin之下先检查本地有没有token没有跳登录页有token再判断本地存储中的role是不是admin防止普通用户绕路进后台。5.2 axios封装里必须要处理的拦截逻辑axios不能每个页面都写一遍我在独立文件里创建实例统一配置baseURL并在请求拦截器里给所有请求自动加上token。响应拦截器里判断code如果接口返回401token过期就清除本地登录信息并跳转登录页。组件内只关心业务数据不重复处理错误弹窗和登录跳转这是前端工程质量的重要体现。真正在项目里运行流畅靠的就是这些统一处理。5.3 图片上传不要只写上传还要处理回显景点封面、攻略封面、富文本插图都要走图片上传接口。前端封装imageUpload组件调用后端/api/upload接口把返回的图片地址回填到表单字段里。上传接口后端实现时要注意保存目录和访问路径的关系。我采用的是上传文件保存到/upload目录在WebMvcConfig里注册静态资源映射把/upload/**映射到本地绝对路径。这样前端可以直接使用/upload/xxx.jpg访问图片不需要额外写图片下载接口。5.4 富文本编辑器与表单校验攻略发布和管理端的公告编辑都用了富文本编辑器存储到数据库的是HTML字符串。这个场景最需要注意的是后端接口不要拦截HTML标签。很多人在后台配置过滤器时把尖括号当成危险字符过滤了结果富文本内容存不进去报奇奇怪怪的校验错误排查很久才发现是拦截器误伤。表单校验方面前端用表单校验规则做基础校验比如用户名不能为空、密码长度6到20位、景点的经纬度要填。后端再配合注解校验做二次防护两边同时把关。6. 系统跑起来的完整操作步骤与最常踩的五个坑源码拿到手最大的问题往往不是业务逻辑而是环境不一致导致的启动失败。我把完整运行流程和踩坑记录写在这里照着做基本能在半小时内把项目跑起来。6.1 初始化数据库第一步在MySQL里创建数据库命名为travel_db编码选择utf8mb4。导入项目SQL文件后确认一下t_user表里有初始管理员数据。登录用户我建议提前准备一条管理员记录密码用MD5加密或BCrypt加密字符串。如果没有初始数据启动后无法登录后台还得去数据库手工insert非常麻烦。6.2 修改后端配置并启动后端配置文件是application.yml需要重点改两项数据库连接url、username、password。服务端口默认8080即可若被占用改掉时要同步改前端代理的target。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB用IDEA打开后端工程等待Maven依赖下载完直接运行启动类即可。后端模块化做得好时启动日志会明确提示注册了多少个接口映射如果项目启动很快但访问一直404优先看控制台中的映射日志。6.3 前端安装依赖并配置代理前端拿到代码后先执行npm install。如果node版本过高导致报错建议用nvm切换Node 14或16版本。然后找到Vue项目里的代理配置把开发环境的/api前缀代理到本地后端地址// vue.config.js module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };执行npm run serve后浏览器访问前端地址登录后台并尝试新增一条景点数据如果页面能展示并在数据库里看到新纪录说明整条链路已经跑通。6.4 实际运行中踩过的五个坑第一个坑是MySQL 8.x的时区报错。连接串里不加serverTimezoneAsia/Shanghai启动时会出现时间类型报错。上面的yml配置已经处理了。第二个坑是跨域问题。开发阶段靠vue.config.js代理解决生产环境部署时一定要在nginx里配置反向代理后端接口不要开启全局CORS否则前端部署到线上还是会有跨域风险。第三个坑是富文本图片上传失败。检查静态资源映射类有没有生效映射路径和保存路径要保持一致。第四个坑是路由刷新404。前端和后端分开部署时nginx需要把非API的请求全部try_files指向index.html否则刷新页面就404。第五个坑是数据库驱动冲突。Spring Boot 2.7默认兼容MySQL 8的驱动但如果项目里混入了老版本mysql-connector会出现连接失败检查依赖时把多余的驱动排除。我自己的实际操作习惯是每次改完后端配置都先编译一次确保没有语法错误再启动前端联调。很多线上问题看着像前后端适配问题实际根源在后端启动时异常被吞掉或Mapper.xml路径不对。整套系统从设计到跑起来最花时间的永远是联调阶段所以在前期把表结构、返回结构、状态字段定义清楚后期能少加很多班。