做旅游网站管理系统这个选题算是计算机专业项目里特别常见的一类了。但说实话同样的题目有人做出来只是把CRUD堆一遍有人做出来能讲清业务链路、讲清每个技术决策的来龙去脉这两者的差距面试官和答辩老师一眼就能看出来。今天我想分享的就是一套基于SpringBootVue的喀什旅游网站管理系统从零搭建、设计数据库、写后端接口、做前端页面到最终部署联调的全过程。这套系统覆盖了用户注册登录、景点展示与搜索、旅游线路规划、酒店预订、评论互动、后台管理等功能技术栈就是标题里那套经典的JavaMySQLMyBatis整体是一个非常典型的前后端分离项目。适合正在做毕业设计或者想系统入门全栈开发的人参考。1. 项目整体设计与技术选型思路1.1 为什么选SpringBootVue而不是其他组合先说后端。SpringBoot出现之前用SpringMVC做项目配置那是一大堆web.xml、spring.xml、数据源、事务管理器、扫描包、视图解析器……新手光是把环境跑起来就得折腾一两天。SpringBoot把这些默认配置全部接管内嵌Tomcat直接一个main方法就能启动加上起步依赖机制想用什么功能就往pom里加什么starter开箱即用。对于这种旅游网站管理系统来说后端就是一堆REST接口SpringBoot做这部分非常顺手几乎没有多余的样板代码。再说前端。Vue的核心优势是组件化和响应式。页面拆成组件景点卡片、轮播图、评论区、导航栏每个都是独立单元改一处不会影响全局。数据双向绑定让页面状态管理变得很自然比如用户搜索关键词、筛选景点类型、选择日期页面视图会跟着数据实时变化写起来比直接用jQuery操作DOM要省心太多。再说了现在前端招聘市场Vue的占有率摆在那学这套东西对后续找工作也有直接帮助。前后端分离还有个实际好处开发的时候前端和后端可以并行。定好接口文档后端专心写接口前端先用mock数据画页面最后联调时再统一对接。一个人做整个项目的时候这个优势同样明显——逻辑清晰出错的时候能立刻判断是前端问题还是后端问题不会一锅粥。1.2 喀什旅游业务的核心需求拆解喀什的旅游资源很丰富古城、沙漠、帕米尔高原、民俗风情游客来之前最关心的是有哪些景点怎么安排路线住哪里方便当地有什么特色活动所以这个系统我把它拆成了两个端来设计。游客端景点列表和详情、按区域和主题筛选、查看旅游线路、酒店信息浏览、发表评论和评分、在线预订线路或房间。 管理端景点信息增删改查、线路上架下架、酒店和房间管理、评论审核、订单状态管理、基础数据统计。用户角色就两类普通游客和管理员。游客可以注册、登录、浏览、预订、评论管理员登录后台维护所有内容数据。系统整体走的是内容展示在线交易互动评论这个模型本质上跟很多电商站类似但业务细节要更贴合旅游场景比如景点有开放时间、门票价格、建议游玩时长线路有行程天数、成团人数这些都是做表结构时要提前想到的。2. 数据库设计与核心表结构2.1 核心业务表与字段设计先看用户表。常规字段id、username、password、nickname、phone、avatar、role0普通用户1管理员、create_time。这里有一个必须强调的点密码绝对不要明文存储至少用BCrypt做hashSpring Security里的BCryptPasswordEncoder可以直接用否则数据库一泄露所有账号密码全暴露了。景点表是内容核心。字段设计我建议这样id、name、region区域比如喀什古城、塔什库尔干、type类型自然风光/人文历史/民俗体验、description长文本介绍、cover_image封面图URL、gallery图片列表存JSON多张图、price门票价格、openhours开放时间、play_duration建议游玩时长、status上下架状态、view_count浏览量、create_time。price用decimal(10,2)方便处理小数点图片存多张用JSON字符串比单独建子表省事查询还不用join。酒店和房间要拆两张表。酒店表hotelid、name、address、grade星级、image、description、status。房间表roomid、hotel_id、room_type大床房/双床房、price、stock可订数量、area。线路表routeid、title、days行程天数、price、cover、description、route_detail每天的行程安排。订单表是整个交易链路的核心。orders设计为id、order_no订单号唯一、user_id、product_type0线路/1房间、product_id、quantity、total_price、status0待支付/1已支付/2已取消/3已完成、create_time、pay_time。为什么要单独建订单表而不是在景点表里加个购买人数字段因为订单是核心交易数据跟景点是1对多的关系游客可以同时订线路和房间订单必须独立成表才能完整记录整个交易流程。评论表也要好好设计id、user_id、spot_id、content、rating1-5分、create_time、status0待审核/1已通过/2已删除。评论审核这个状态字段很多新手容易忽略实际上旅游网站是公开展示的评论不审核很容易被垃圾信息刷屏。2.2 表关系与索引优化这些表的关联关系并不复杂用户到订单是1对多酒店到房间是1对多游客对景点的评论通过comment表关联user和spot。在物理设计上我不太建议强行加数据库外键约束原因是MyBatis项目里我们通常用代码控制关联关系加外键会降低插入和更新性能而且业务调整时外键会成为迁移和修改的阻碍。逻辑外键已经足够表达关系物理外键能不用就不用。索引方面有几个必加orders表的user_id、order_no唯一索引、statusscenic_spot表的region和typecomment表的spot_id。这些都是查询的高频入口。这里有个真实的性能坑很多新手建表时不加索引等数据量上来查某个用户订单扫全表几十万行接口直接超时。索引不是越多越好但一定要建在真正高频筛选的列上。字符集一定要用utf8mb4而不是utf8。原因很简单utf8在MySQL里最多支持3字节像emoji这种4字节字符会存不进去直接报错。旅游网站的评论区用户发个表情再正常不过所以建库时就要指定utf8mb4排序规则用utf8mb4_unicode_ci就行。建库语句大概长这样CREATE DATABASE kashi_travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;存储引擎用InnoDB支持事务、崩溃恢复能力强适合订单这种对数据一致性要求高的业务。表结构确定之后一定要写一个init.sql把建表语句和基础数据都准备好之后部署直接source导入省得在服务器上一张一张建。3. 后端核心模块实现SpringBootMyBatis3.1 项目初始化与分层架构创建项目用Spring Initializr就行Java版本建议8或11这两个版本依然是目前生产环境的主力。核心依赖不多dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.x/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency代码分层就按经典的三层结构走controller接收请求、参数校验、返回统一结果service处理具体业务逻辑mapper是MyBatis的数据访问层。再加上entity实体、dto接收参数、config配置类、common公共类。这种分层不是形式主义最大的好处是当业务复杂到一定程度一次修改不会牵一发动全身。比如景点删除功能如果你直接在controller里调mapper.deleteById将来要加删除前判断该景点有没有未完成订单就得改controller而有了service层这个逻辑天然放在service里controller保持不动。application.yml配置可以参考这份server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/kashi_travel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.kashi.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置强力建议打开。数据库字段是create_timeJava实体是createTime打开后MyBatis自动映射不用每条SQL都写别名能省下大量体力。接口返回格式我统一封装成Result类包含code、message、data三个字段。code为200表示成功401表示未登录500表示服务器错误。所有接口都返回这个结构前端axios拦截器只要判断code就能统一处理错误不用每个接口单独写异常分支。配合RestControllerAdvice全局异常处理器业务里抛出的异常统一捕获成Result返回避免把Java堆栈直接暴露给前端这点在答辩和面试时很加分。3.2 用户登录鉴权与订单接口实现用户登录我采用JWT方案。前端提交账号密码后端校验通过后生成一个包含userId和role的token返回前端存到localStorage之后每次请求都在header里带Authorization。后端写一个LoginInterceptor拦截需要登录的接口从token里解析用户信息放行或拒绝。生成JWT用jjwt库代码也就几十行String token Jwts.builder() .setSubject(userId.toString()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 3600_000)) .signWith(secretKey) .compact();JWT比传统Session方案好在哪Session在集群部署时要考虑session共享问题JWT把用户状态放在客户端token里后端不需要存session天然适合前后端分离和水平扩展。缺点就是token签发后无法服务端主动失效所以一般把过期时间设短一点比如2小时前端拦截到401就跳回登录页重新登录。订单接口是整个后端逻辑最密集的地方。以预订酒店房间为例前端提交hotelId、roomId、入住日期、房间数后端要做这几件事解析token拿到userId校验用户存在查询room记录校验库存和价格生成订单号可以用时间戳加随机数保证唯一计算总价插入orders表把room的库存减掉预订数量。第2到第5步的核心在于事务控制。如果只查询价格然后直接插入订单两个用户同时订最后一间房两个请求都读到stock1都下单成功但实际只能住一个人这就是典型的超卖。解决方案是给service方法加Transactional注解而且扣库存的SQL一定要带库存判断update room set stock stock - #{num} where id #{roomId} and stock #{num}执行完检查update返回的影响行数为0说明库存不足直接抛异常回滚事务。3.3 MyBatis动态SQL与分页查询实战景点列表页有各种筛选条件区域、类型、价格区间、关键词。如果每种组合写一个固定SQL那得写几十条。MyBatis动态SQL就是为这种场景设计的在xml里用where和if标签灵活拼查询select idsearchSpots resultTypecom.kashi.entity.ScenicSpot select * from scenic_spot where if testregion ! null and region ! and region #{region} /if if testtype ! null and type ! and type #{type} /if if testkeyword ! null and keyword ! and (name like concat(%, #{keyword}, %) or description like concat(%, #{keyword}, %)) /if /where order by view_count desc /select注意like查询里用的是concat函数拼接%不要直接在外部拼好再传进来那样会有SQL注入风险。MyBatis的#{}是预编译占位符自动规避注入${}才是直接字符串拼接除非是排序字段这种场景否则坚决用#{}。分页用PageHelper插件使用方式非常简单PageHelper.startPage(pageNum, pageSize); ListScenicSpot list spotMapper.selectAll(); PageInfoScenicSpot pageInfo new PageInfo(list);PageHelper会自动拦截下一条SQL生成带limit的查询并统计总数PageInfo里自带total、pages、pageNum这些信息前端直接拿来渲染分页组件。关于MyBatis缓存这里也说一下。一级缓存默认开启同一个SqlSession里重复查询会命中二级缓存需要手动开启配置。但这个项目我建议暂时不开二级缓存原因很简单查询结果跟数据库直接相关景点一更新就容易出现脏读等业务量大了以后交给Redis管理缓存会更稳妥。4. 前端Vue页面与核心交互实现4.1 前端工程化与axios网络层封装前端我用Vue CLI创建项目路由用vue-routerUI组件库用Element UI这样管理后台和用户页面都能快速搭出规范的表单表格不用从零写CSS。目录结构按views页面、components组件、router路由、api接口、utils工具来组织。axios封装是整个前端能否省心的关键。我在src/utils/request.js里创建一个axios实例设置baseURL指向后端地址加两个拦截器请求拦截器从localStorage取token塞到Authorization头响应拦截器统一处理返回结构code不等于200就弹提示或者跳登录页。这样业务代码里每个接口调用都是干净的一行export function getSpotDetail(id) { return request.get(/api/spot/${id}) }路由设计上游客端页面有首页、景点列表、景点详情、线路列表、酒店列表、我的订单、登录注册管理端页面有控制台、景点管理、线路管理、酒店管理、订单管理、评论管理。两种角色之间用一个路由守卫判断进入管理端页面前检查当前用户role是否为1不是就重定向到登录页。但这里要特别注意前端守卫只是用户体验层面的保护真正权限控制必须后端接口也做同样的校验否则有人绕过前端直接调接口就崩了。4.2 游客端核心页面景点列表、详情与预订流程景点列表页是整个系统对外展示的第一张脸我把它做成顶部分类筛选栏加左侧区域筛选、右侧景点卡片瀑布流的布局。筛选条件变化时触发重新请求后端接口带上参数后端用刚才那个动态SQL拼出对应结果。景点卡片上展示封面图、名称、区域、类型、门票价格、浏览量点击进入详情页。详情页是游客信息密度最高的页面。顶部是图片轮播接着是基本信息表格包括开放时间、建议游玩时长、门票价格然后是详细介绍文字底部是评论区。评论加载用分页每次10条用户提交评论后清空输入框重新拉第一页数据。这里容易踩一个坑提交评论后如果还用当前页的数据刷新因为分页偏移问题可能显示不出刚提交的内容。我的处理是提交成功后直接调用加载第一页评论让评论列表回到最新状态。预订流程分线路预订和酒店房间预订两条链路。线路预订相对简单游客选日期、输人数、确认订单、生成待支付订单酒店房间预订需要先选房间类型和入住天数系统自动算总价。订单确认页要把房间信息、入住日期、总价这些关键信息展示清楚还要留一个取消按钮对应后端把状态改成已取消、库存加回去。订单列表页和详情页也要随时能查这些都是游客信任感的来源。景点详情页的核心展示代码大概长这样template div v-ifspot el-carousel el-carousel-item v-foritem in spot.gallery :keyitem img :srcitem / /el-carousel-item /el-carousel h1{{ spot.name }}/h1 el-rate :model-valuespot.rating disabled/el-rate p{{ spot.description }}/p el-button typeprimary clickhandleBook立即预订/el-button /div /template4.3 管理后台CRUD表格、图片上传与数据统计管理员登录后进入管理后台第一个页面是控制台我接入了ECharts做两个统计图近7日订单量的柱状图景点访问量Top5的饼图。数据来自后端一个简单的聚合查询接口。这个功能不复杂但对项目完整度的提升非常明显答辩时也能体现工程量。ECharts配置项比较多建议先去官网看基础柱状图的demo再改数据源踩坑最少。景点管理页是整个后台最典型的CRUD页面。Element UI的el-table展示景点列表工具栏放新增按钮和搜索框操作列有编辑、删除、上下架。新增和编辑共用一个弹窗表单用isEdit变量区分插入还是更新。图片上传用el-upload组件配合后端一个upload接口文件传到服务器uploads目录接口返回URL再把URL填到表单的coverImage字段。这里提醒一下上传接口必须做文件类型和后缀校验只允许jpg、png、webp这些白名单格式大小也限制一下2MB以内比较合理否则被传个恶意脚本就麻烦了。订单管理页做两件事查看所有订单、修改订单状态。表格里展示用户昵称、产品类型和名称、数量、总价、状态、下单时间状态用el-tag显示不同颜色订单号加复制按钮方便管理员记录。5. 联调部署与避坑指南5.1 前后端联调与跨域配置本地联调时前端开发服务器跑在8081后端跑在8080前端调后端接口就产生了跨域。解决办法有两种后端加CORS配置或者前端用devServer的proxy代理转发。我建议开发阶段用proxy方案前端写接口时直接写相对路径/api/xxx由devServer把请求代理到后端真实地址以后部署上线前端代码一行都不用改。上线时前端打包成静态文件交给Nginx托管再让Nginx把/api开头的请求反向代理到后端服务整个架构非常清晰。开发阶段的vue.config.js配置很简单devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }如果非要用后端CORS方案那就配一个WebMvcConfigurer注意allowedOriginPatterns要用通配符配合allowCredentials(true)时不能直接用allowedOrigins(*)这是很容易踩的版本兼容坑。5.2 常见问题速查表与解决实录我把自己开发过程中遇到的典型问题和解决方案整理成一张表基本都是新手必踩的坑。现象可能原因解决方案启动时MyBatis报BindingException: Invalid bound statementMapper接口和XML的namespace不匹配或XML没被扫描到检查mapper-locations路径是否正确确认XML的namespace与接口全限定名一致后端启动报数据库连接失败MySQL没启动、URL错误、密码错误先用客户端软件测试连接确认库名存在检查serverTimezone参数前端请求后端报跨域前后端没配代理或CORS没放开开发阶段用devServer proxy上线用Nginx反代同源接口返回401但用户已登录token过期、拦截器放行路径没配、前端没带header查token有效期、拦截器注册路径、axios请求拦截器是否正确附加Authorization页面刷新后404vue-router用的history模式服务端没做兜底开发时用hash模式history模式上线要在Nginx配try_filesMyBatis查询结果字段全为null没开map-underscore-to-camel-case下划线字段没映射配置里打开该选项或SQL里写列别名还有几个容易被忽略的细节后端项目里涉及上传图片、生成临时数据的目录上线前要给足写权限否则部署到Linux服务器上进程没有对应目录的写权限上传图片会静默失败排查半天才找到原因。MySQL导入数据时如果报Unknown collation错误多半是客户端版本太低升级到8.x再导就行。写在最后。这类管理系统本质上是一个典型的业务加技术的复合项目代码并不高深真正有价值的地方在于你能否把游客要什么、管理员要什么翻译成表结构、接口、页面再串联成一个能跑通的闭环。如果你正在做类似的毕业设计或者想入门全栈我的建议是先花一两天把用户故事、业务流程、角色权限梳理清楚画一张逻辑图数据表设计就会顺水推舟地清晰起来。一套完整的旅游网站系统做下来你对SpringBoot和Vue的理解一定比看十遍文档要深这也是这类项目能成为经典练手题目的原因。