ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的旅游网站管理系统:数据库设计与前后端联调实战

基于SpringBoot+Vue的旅游网站管理系统:数据库设计与前后端联调实战 做旅游类项目很多人一开始把精力全花在页面和功能上觉得景区图库够多、详情页够花哨就差不多了。但真正接过“完整版”交付的人心里都清楚一套能被企业拿去上线运营的旅游网站管理系统关键不在表面颜值而在架构选型是否扛得住真实业务、表结构能不能支撑后续迭代、前后端接口边界划得够不够干净。我最近完成的一套基于SpringBootVueMyBatisMySQL的安康旅游网站管理系统源码就是把整条链路从需求分析到数据库设计再到接口联调和打包部署完整走了一遍落地的过程中踩了不少坑也积累了很多可以复用的经验。这篇博文就从整体设计、数据库、后端实现、前端对接、部署排查这几个维度把这套系统的核心思路和实操细节全部拆开讲透。这套系统面向的是安康本地旅游业务包含前台门户和后台管理两大块既要做给游客观看的景区展示、旅游线路、在线预订和留言咨询也要做给管理员操作的景点维护、订单管理、轮播图配置、数据统计。对于正在做毕业设计、个人练手项目或者准备承接中小型旅游网站外包的技术人来说这个项目骨架可以直接拿来改甚至换皮换个城市就能交付。我写这篇文章的目标也很简单把那些常规文档不会写的选型理由、表设计取舍、联调细节、部署坑都交代清楚让你不只是拿到源码而是能理解这套系统为什么是这么设计的。1. 项目整体设计与架构拆解1.1 技术选型背后的逻辑选技术栈这事我一直觉得不能只盯着“流行”两个字看要看项目边界和团队接手成本。安康旅游网站管理系统属于典型的中小规模业务系统用户量级初期可能每天几百到几千访问数据量也就是几十万级别但业务场景相对完整内容展示、会员体系、下单预订、后台管理该有的都有。这种项目用SpringBootVueMyBatisMySQL这套组合是性价比最高的选择。SpringBoot做后端基础框架自动配置和内置Tomcat让项目能快速起步不需要像传统SSM那样繁琐的XML装配。Maven管理依赖一个spring-boot-starter-web就解决了Web层的大部分配置这对后期部署和维护都很友好。Vue作为前端框架核心优势是组件化开发景点卡片、轮播图、分页条这些UI模块都能拆成独立组件复用后台管理界面和前台门户也可以共用一个项目再用路由拆分开发效率比原生页面高出太多。MyBatis在这套系统里扮演的角色更重要。旅游网站的查询场景非常复杂景点列表要按地区筛、按价格区间筛、按关键词模糊搜索线路表还要关联多个景点这种多条件组合查询恰恰是MyBatis的强项通过动态SQL可以灵活拼装查询条件而不是在代码里写一堆if判断去构造查询语句。SQL由开发人员自己掌控意味着遇到性能瓶颈时可以直接优化SQL不会像JPA那样在某些复杂查询场景下显得束手束脚。MySQL数据库则胜在轻量稳定、生态完善尤其对于这类crud密集型业务InnoDB引擎的事务支持和行级锁机制足够满足并发下单场景运维成本也比Oracle、PostgreSQL这类数据库更亲民。有开发者会问为什么不加Redis做缓存、用Spring Cloud做微服务我的答案是没必要。一个单体应用能解决的业务规模强行上微服务只会增加部署复杂度和故障排查成本。Redis确实可以在后期做热点景点缓存但第一期先把核心链路跑通比追求“看起来高级”更为重要。1.2 功能模块划分与用户角色分析这套系统在功能设计时我按照用户角色把系统分成三个端游客端、会员端、管理端。三个角色看到的页面和操作权限完全不同代码层面通过路由守卫和后台接口权限做双重控制。游客端是面向未登录用户的门户页面核心功能包括浏览安康景点列表、查看景点详情、查看旅游线路、搜索景点、阅读网站公告。这个端的设计目的是“引流和展示”重点是信息呈现的清晰度和页面加载速度。会员端是注册登录后用户的专属空间功能包括个人信息修改、密码修改、旅游线路下单预订、我的订单查询、订单取消、景点留言评价。这个端的核心是订单交易闭环也是整个系统数据流最复杂的部分。管理端是后台管理系统使用者是网站运营人员功能包括景点信息管理新增、编辑、上下架、删除、旅游线路管理、轮播图管理、订单审核与状态更新、用户管理、留言回复、基础数据统计。管理端要求操作效率高我采用左侧菜单加右侧内容区的经典后台布局表格操作列统一放编辑和删除按钮弹窗表单做数据录入。一个值得强调的设计点是游客端和管理端虽然视觉风格差异很大但我没有拆成两个独立的前端项目而是在同一个Vue项目中通过路由前缀区分例如/portal开头的走门户页面/admin开头的走管理后台。这样做的最大好处是公共组件和工具函数可以共享前端打包出来的产物也只有一个目录部署时不需要处理多个静态资源和多个子应用的跨域问题。1.3 项目目录结构与工程组织方式后端工程采用标准的Maven多模块思路但考虑到项目体量我没有强行拆成多个module而是用单模块内的分层包结构。这样既保留了代码层次清晰的特点又避免了多模块之间依赖管理带来的复杂度。实际结构大致如下travel-server ├── src/main/java/com/ankang/travel │ ├── controller # 接口层只做参数接收和结果返回 │ ├── service # 业务层处理核心逻辑和事务边界 │ ├── mapper # MyBatis Mapper接口 │ ├── entity # 数据库实体类 │ ├── dto # 数据传输对象接收前端请求参数 │ ├── vo # 视图对象返回给前端的结果模型 │ ├── config # 配置类跨域、拦截器、MyBatis配置 │ ├── common # 统一返回结果、全局异常、常量 │ └── util # 工具类JWT、日期处理、订单号生成前端工程用Vite构建目录结构采用views按页面模块放、components按公共组件放的方式travel-web ├── src │ ├── api # 接口请求模块每个业务域一个文件 │ ├── assets # 静态资源 │ ├── components # 公共组件分页、弹窗、图片上传 │ ├── router # 路由配置 │ ├── store # 状态管理用户信息、token │ ├── views # 页面组件 │ │ ├── portal # 前台门户相关页面 │ │ └── admin # 后台管理相关页面 │ ├── utils # 工具函数axios封装、格式化 │ └── App.vue这种分包方式的核心思路是接口层和业务层解耦、实体对象和视图对象分离。特别是entity和vo分开这一点我见过很多项目图省事直接把数据库实体返回给前端结果密码字段、逻辑删除字段全部暴露出去后续改起来非常痛苦。分层清晰了接口的重构和维护成本才会降下来。2. 数据库设计与核心表结构2.1 表设计思路与ER关系梳理数据库设计是整个系统的基础我花在表设计上的时间比写接口的时间还多。原因很简单代码不好可以重构表设计一旦上线后改动牵涉到历史数据迁移、接口兼容、报表统计逻辑代价非常大。根据业务需求我规划了七张核心表用户表、景点表、旅游线路表、订单表、轮播图表、留言表、管理员表。表之间的关系不算复杂用户和订单是一对多线路和订单是一对多景点和线路是多对多一条线路可以包含多个景点一个景点也可以出现在多条线路中用户和留言是一对多。多对多关系我通过单独的关联表route_scenic来维护除了两个外键还增加了排序号字段用于控制线路中景点的先后展示顺序。设计时我坚持了几个原则。第一主键统一使用bigint自增ID避免分布式环境下UUID做主键导致索引过大、插入性能下降的问题。第二每张表都保留create_time和update_time两个时间字段MyBatis的自动填充功能可以让代码统一维护后续排查数据问题时会方便很多。第三不轻易使用物理删除所有核心业务表都增加deleted字段做软删除避免误删后无法恢复数据。2.2 核心表字段详解用户表是会员体系的基础关键字段如下字段名类型说明idbigint主键自增usernamevarchar(50)登录账号唯一索引passwordvarchar(100)密码BCrypt加密后的密文nicknamevarchar(50)用户昵称phonevarchar(20)手机号avatarvarchar(255)头像图片地址statustinyint状态1正常0禁用deletedtinyint软删除标记create_timedatetime注册时间这里有一个关键设计点密码字段长度为100而不是常见的50。因为BCrypt加密后的字符串长度就是60位以上我给100是预留算法升级的空间。很多新手在这里踩坑密码字段设成varchar(32)用MD5加密存进去倒是够了但一旦想升级为更安全的BCrypt就会发现字段长度不够非常被动。景点表是整个内容展示的核心字段包括景点名称、所属地区、景点封面图、详情图集、门票价格、开放时间、景点介绍、状态。其中图片字段我用的是text类型存JSON数组允许存多张图片路径。这里不单独建景点图片表是基于业务体量的考虑一个景点的图片数量撑死十几张用JSON数组存完全够用查询时一次取出不需要额外联表查询性能反而更好。订单表是业务核心需要重点说明。订单号我用order_no类型为varchar(50)生成规则是“时间戳用户ID随机数”保证并发场景下不重复。订单状态用status字段做枚举值0待支付、1已支付、2已取消、3已完成、4退款中。金额字段用decimal(10,2)这里必须强调任何涉及钱的数据都禁止用float或double二进制浮点数在精度上天然有缺陷计算总价时会产生0.10.2不等于0.3这类问题。线路与景点的关联表route_scenic设计如下字段名类型说明idbigint主键route_idbigint线路IDscenic_idbigint景点IDday_numberint行程第几天sort_orderint同一天内排序这个表的意义不只是维持多对多关系day_number和sort_order两个字段直接支撑了前端“行程安排”页面的展示逻辑按天分组、按序展示景点不需要在Java代码里做复杂排序。2.3 索引设计与查询性能优化索引设计上我犯了两次错才摸到门道。第一次把所有查询字段都加了索引结果写入变慢、索引空间膨胀第二次又过于节约搜索接口在数据量过万后明显卡顿。最终的索引策略是高频的查询条件加索引低频的组合查询用联合索引覆盖。用户表的username字段设为唯一索引登录时按账号精准查询时命中唯一索引效率最高。订单表按照查询频次设计user_id建普通索引支撑“我的订单”查询status建普通索引支撑管理端按状态筛选订单组合条件user_idstatus的查询场景我评估过核心是user_id的等值匹配单独建user_id索引已经能过滤掉绝大多数数据不需要再建联合索引。线路和景点关联表route_id必须建索引因为前端展示线路详情时要根据线路ID查出全部关联景点。分页查询也是旅游网站的高频场景。这里我建议先走LIMIT offset, size数据量到十万级以上再考虑游标分页或者延迟关联。对于当下的业务体量在create_time上建立索引之后分页查询的执行时间从几百毫秒降到了几十毫秒完全够用。每次写复杂SQL之前我都习惯用EXPLAIN看一眼执行计划重点观察type字段是不是const或ref如果出现ALL全表扫描就要检查索引是否失效了。3. 后端核心实现SpringBootMyBatis3.1 分层设计与统一响应结构后端开发最忌讳把所有逻辑堆在Controller里。我采用的层次结构是Controller接收参数、Service处理业务、Mapper操作数据库层次之间通过DTO和VO传递数据。Controller层不出现任何SQL相关逻辑也不直接操作实体类Service层负责事务管理、业务校验、异常抛出Mapper层只管数据读写不承载业务判断。统一响应结构是接口设计的第一步我定义了一个Result类包含code、message、data三个字段。成功返回code200业务失败返回code500参数校验失败返回code400未登录返回code401。这样的好处是前端axios拦截器可以统一根据code做提示处理和跳转不需要每个接口单独判断HTTP状态码。代码示例如下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; } }配合RestControllerAdvice做全局异常处理业务代码里只需要抛出自定义的BusinessException异常消息会被统一包装成Result结构返回给前端。这样避免了try-catch到处飞舞代码可读性会好很多。3.2 MyBatis动态SQL与多表联查实战MyBatis在这套系统里最大的存在感体现在景点搜索接口上。搜索条件有区域、价格区间、关键词三个条件都可以为空也可以组合出现。如果用JDBC手工拼SQL很容易出现WHERE后直接跟AND导致SQL语法错误的情况用MyBatis的动态SQL就安全得多。我给出一个多条件查询的实际片段select idselectScenicByCondition resultMapScenicResultMap SELECT * FROM scenic where if testarea ! null and area ! AND area #{area} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if AND deleted 0 /where ORDER BY create_time DESC /selectwhere标签会自动处理首个条件的AND前缀这样不需要在每个if里手动加and或考虑是不是第一条。还有个细节LIKE模糊查询用CONCAT(%, #{keyword}, %)而不是%${keyword}%前者是预编译参数绑定能防范SQL注入后者是字符串拼接一旦keyword里包含特殊字符就可能造成注入风险。安全合规这块MVN全链路都要绷紧这根弦。线路详情查询需要联查关联表获取景点列表这种一对多映射我用resultMap的collection标签来处理resultMap idRouteDetailMap typeRouteVO id propertyid columnid / result propertyname columnname / collection propertyscenicList ofTypeScenicVO selectcom.ankang.travel.mapper.ScenicMapper.selectByRouteId columnid / /resultMap这里采用了嵌套查询的方式执行时MyBatis会根据线路id再发起第二次查询好处是编写简单坏处是N1问题。当前业务场景线路查询并发量不高这个方案可以接受。如果在高并发场景下建议改成连表查询加collection直接映射一次SQL查完所有关联数据。3.3 事务处理与安全设计订单下单接口是典型的需要事务保护的场景。流程包含创建订单记录、扣减线路库存、如果订单包含门票还需要更新景点预订数量。任何一个环节失败都需要回滚之前的操作。我在Service层方法上加了Transactional注解来开启事务。这里分享一个非常容易踩的坑Spring事务的默认回滚规则是只回滚RuntimeException和Error如果方法抛出的是受检异常比如Exception事务是不会自动回滚的。所以我一般在业务中抛出的都是自定义的运行时异常BusinessException并且在使用Transactional时明确指定rollbackFor Exception.class防止后续改动时有人抛出了别的异常类型导致数据不一致。安全设计这块密码加密我选择了BCrypt算法而不是MD5或SHA。MD5和SHA都属于摘要算法加密速度快但现在GPU算力下暴力破解非常轻松。BCrypt内部自带随机盐每次加密的结果都不一样而且可以通过调节cost参数控制计算耗时有效提高暴力破解成本。引入Spring Security中的BCryptPasswordEncoder工具类即可。用户认证采用的是JWT方案。用户登录成功后服务端根据用户ID和过期时间生成一个token字符串返回给前端前端存储在localStorage中每次请求在请求头Authorization字段携带。后端定义一个拦截器对需要登录的接口进行token解析和验证解析失败直接返回401。需要注意token过期时间的策略太短影响用户体验太长有安全风险当前系统设置24小时过期这个时间可以根据实际运营情况调整。4. 前端Vue实现要点与接口对接4.1 Vue项目结构与路由设计前端我使用的是Vue 3组合式API结合Vite构建工具。初始化项目命令很简单npm create vitelatest travel-web -- --template vue选择Vite而不是Vue CLI主要是构建速度快开发体验好配置简洁。如果读者电脑上Node版本较低建议先把Node升级到16以上Vite才能正常运行。路由设计是前端架构的核心。我在router/index.js里同时配置了门户和管理后台的路由import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /portal }, { path: /portal, component: () import(/views/portal/Layout.vue), children: [ { path: , component: () import(/views/portal/Home.vue) }, { path: scenic/:id, component: () import(/views/portal/ScenicDetail.vue) }, { path: route/:id, component: () import(/views/portal/RouteDetail.vue) }, { path: login, component: () import(/views/portal/Login.vue) }, ] }, { path: /admin, component: () import(/views/admin/Layout.vue), meta: { requiresAuth: true }, children: [ { path: scenic, component: () import(/views/admin/ScenicManage.vue) }, { path: route, component: () import(/views/admin/RouteManage.vue) }, { path: order, component: () import(/views/admin/OrderManage.vue) }, ] } ]路由懒加载通过component: () import(...)实现按页面拆分代码块避免首屏加载全部js导致白屏时间过长。后台管理路由统一加了meta.requiresAuth标记配合全局前置守卫判断用户是否登录、是否有访问权限没有登录一律重定向到登录页。4.2 Axios封装与接口联调接口请求模块我统一封装在utils/request.js里核心作用是统一处理baseURL、token注入、响应错误码提示。代码核心逻辑如下import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器附加token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理业务码 request.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } else if (res.code 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) window.location.href /portal/login return Promise.reject(res) } else { ElMessage.error(res.message || 系统异常) return Promise.reject(res) } }, error { ElMessage.error(error.message || 网络请求失败) return Promise.reject(error) } )前端开发中跨域问题是绕不开的。我推荐在开发环境使用Vite的代理配置解决在vite.config.js中加入server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/scenic/list时会自动代理到后端http://localhost:8080/api/scenic/list开发时不需要后端单独配置跨域过滤器。但生产部署时我建议由Nginx统一代理静态资源和后端接口跨域问题在Nginx层面就处理掉了后端完全不需要开放跨域。4.3 核心页面功能实现思路门户首页的轮播图我使用了Element Plus的el-carousel组件数据结构是后端返回的banner列表包含图片地址、跳转路径、排序字段。这里需要注意的是图片路径的处理如果后端存的是相对路径/uploads/xxx.jpg前端就需要在axios请求时拼接上静态资源服务器的完整域名。我使用的是在vite.config.js中配置公开资源路径或者直接把图片路径处理逻辑放到工具函数中统一处理。景点列表页是高频访问页面组件结构是搜索栏列表分页。搜索栏包含地区下拉框、价格输入框、关键词输入框点击搜索时重置pageNum为1重新请求接口。分页组件使用Element Plus的el-pagination绑定total和current-page监听current-change事件重新拉取数据。后台景点管理页是典型的表格加弹窗表单模式。表格列包括景点名称、所属地区、门票价格、状态、创建时间、操作列。点编辑或新增时打开el-dialog内部是景点表单。图片上传使用el-upload组件上传成功后后端返回图片URL再把URL放进表单字段中。弹窗提交成功后刷新表格数据并关闭弹窗同时对错误信息做toast提示。一个实用的经验分享前端所有携带金额的字段展示格式化要尤其小心。后端返回的金额是字符串类型128.00前端直接用没问题但哪天后端改成返回128这种数字格式金额分位符的问题就会冒出来。我的习惯是后端一律按decimal转字符串返回给前端避免前端浮点运算导致展示错误。5. 部署上线与常见问题排查5.1 本地环境搭建与打包部署整套系统的本地开发环境只需要四样东西JDK 8或17、Maven 3.6、Node.js 16、MySQL 5.7或8.0。JDK版本方面要特别注意Spring Boot 2.x系列用JDK8最稳定Spring Boot 3.x需要JDK17以上选择的SpringBoot版本直接决定了JDK版本不能随意搭配。我当前这套源码用的是Spring Boot 2.7.x对应JDK8虽然版本不算最新但生态兼容性最好遇到问题搜索时答案也最容易找到。后端打包命令mvn clean package -DskipTests打包成功后在target目录下生成travel-server-1.0.0.jar直接用java -jar启动java -jar travel-server-1.0.0.jar --spring.profiles.activeprodapplication-prod.yml中配置的是生产使用的MySQL连接串和端口。前端打包npm run build生成dist目录后部署方案我推荐把jar包和dist目录放到同一个服务器使用Nginx做静态文件服务和反向代理配置示例如下server { listen 80; server_name travel.example.com; # 前端静态资源 location / { root /opt/travel/dist; index index.html; try_files $uri $uri/ /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 /uploads/ { alias /opt/travel/uploads/; } }try_files $uri $uri/ /index.html这行配置非常关键。前端如果使用history路由模式用户直接访问/portal/scenic/3或者刷新页面时Nginx会把请求转发给index.html由Vue路由接管跳转到对应页面。不加这行配置刷新页面就会出现404。5.2 高频问题排查实录第一个高频问题就是MySQL 8.0的SSL连接报错。现象是启动项目时报java.sql.SQLNonTransientConnectionException: SSL connection error。原因在于MySQL 8.0默认开启SSL而JDBC连接串没有配置对应的SSL参数。解决方法是在数据库连接串上加上useSSLfalseallowPublicKeyRetrievaltrue。第二个是时区问题。MySQL 8.0和JDBC默认时区不一致连接串需加上serverTimezoneAsia/Shanghai否则查询时间字段会比实际时间差8小时。这两个参数的完整连接串是jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue第三个问题是MyBatis查询结果中文乱码。排查思路很简单依次检查数据库表字符集是否为utf8mb4、连接串是否包含characterEncodingutf8、页面编码是否是UTF-8。数据库建库时我习惯直接用CREATE DATABASE travel_db DEFAULT CHARACTER SET utf8mb4;从根源上避免乱码。第四个是SpringBoot版本过高导致的问题。Spring Boot 3.x默认使用Jakarta EE规范很多第三方库如果还停留在javax包直接引入就会报ClassNotFoundException。如果项目引用的组件版本不兼容SpringBoot 3.x建议直接用SpringBoot 2.7.x不必盲目追新。选型要贴合实际项目依赖。第五个是端口占用问题表现为启动时报Port 8080 was already in use。解决方式一是换端口java -jar xxx.jar --server.port8081二是查杀占用进程Windows用netstat -ano | findstr 8080查出PID后taskkill /PID xxx /FLinux用lsof -i:8080查看进程。5.3 安全加固与性能优化建议系统上线只是起点安全加固和性能优化是紧接着要做的两件事。安全层面当前系统在MyBatis层已经使用#{}预编译参数防止SQL注入登录密码使用BCrypt加密接口通过JWT做认证鉴权这些是基线要求。另外还要注意文件上传接口的安全上传时校验文件扩展名和Content-Type限制图片大小防止恶意上传可执行文件。性能层面第一步优化是把热点景点数据缓存到Redis中。首页景点列表、轮播图这类数据访问频次极高、更新频率很低非常适合做缓存。Redis缓存思路是查询时先读缓存缓存未命中再查数据库并回填缓存更新景点数据时主动删除对应缓存。对于当前系统体量这个优化可以把首页接口响应时间从200毫秒左右压到20毫秒以内。订单表随着业务运行会越来越大建议定期归档历史订单或者按照月份做表分区。数据库层面开启慢查询日志定期检查执行时间超过1秒的SQL针对性地优化索引。每次核心功能上线前我都习惯在测试环境用wrk或者JMeter做一轮简单压测至少摸清当前系统的吞吐量上限和瓶颈位置这样上线后遇到性能问题心里才有底。写在最后的几点体会这套安康旅游网站管理系统从头到尾走下来我最深的感受是完整版源码的价值不在于能跑起来而在于每一条数据流都有清晰的来龙去脉每一个接口都有配套的前端调用每一次表设计都考虑到了业务的真实走向。踩过几次坑之后现在做单体管理系统我已经有一套固定的工作流先画表结构、再定接口语义、然后才动手写页面。顺序反过来多半要返工。如果你正在拿这套源码二次开发我建议先从数据库表入手读懂每张表之间的关系再去跑通一条完整的下单链路最后才去改页面样式。这个顺序能帮你快速掌握整个项目的骨架而不是陷入细节里出不来。
返回列表