SpringBoot+Vue旅游网系统:从数据库设计到前后端部署全栈实战
发布时间:2026/10/2 11:53:13来源:尧图网络
最近把一套基于SpringBoot Vue的旅游网系统完整整理了一遍从后端接口、前端页面到数据库脚本、部署文档都重新核对过。这套东西不是那种只搭了个壳的演示项目而是把景点、线路、酒店、攻略、订单、用户这些旅游业务的核心模块都串起来了拿到手之后可以直接跑也可以拿来当全栈学习的骨架。对正在做课设、毕设或者想系统性入门SpringBoot Vue前后端分离开发的朋友来说参考价值比较直接。整个项目围绕“旅游网”这个业务场景展开核心是让游客能浏览景点和旅游线路、查看酒店、收藏攻略、下单预订同时给管理员提供一套后台管理界面去做内容维护和订单处理。开发端用SpringBoot做后端接口Vue做前端页面MySQL存业务数据前后端通过JSON交互。下面按实际项目整理的顺序把整体设计、数据库、后端、前端、部署以及踩坑过程都拆开讲一遍尽量讲得具体一点。1. 项目概览与系统功能拆解1.1 这套旅游网系统到底能做哪些事功能设计上这套旅游网系统分成游客端和管理员端两条线。游客端是给人正常浏览和下单用的管理员端是给运营人员维护数据用的。游客端主要功能景点浏览按分类、地区、热度筛选景点景点详情页展示图文介绍、开放时间、门票价格、用户评价。旅游线路系统里维护了多条跟团游、自由行线路游客可以查看行程安排、费用说明、出发日期和剩余名额。酒店预订按城市和日期搜索可预订酒店查看房型、价格、设施提交预订后生成订单。攻略社区用户发布游记和攻略支持图片上传其他用户可以查看文章、评论、收藏。订单中心用户查看自己的订单状态进行取消、支付模拟、评价等操作。用户中心注册、登录、修改个人信息、查看收藏列表。管理员端主要功能景点、线路、酒店、攻略等内容的增删改查图片上传、上下架状态控制。订单管理查看全部订单、按状态筛选、进行发货/确认/退款等操作。用户管理用户列表、禁用/启用账号。数据统计简单的订单量、访问量统计用图表展示。这里要特别说明一下像“支付模拟”这种设计不是因为不能接真实支付而是很多课设、毕设在本地环境下不方便申请微信支付或者支付宝支付的商户号所以先用一个模拟状态的按钮把订单从待支付推进到已支付等真正上线需要接支付时再替换成对应的支付SDK即可。这种留接口的思路在实际项目里很常见。1.2 技术栈选型背后的逻辑这套项目的技术栈是SpringBoot Vue配套的还有MyBatis Plus、JWT、MySQL、Redis。选这套组合不是因为它“最流行”而是因为它非常契合中小型前后端分离项目的开发节奏。层次技术选型主要用途后端框架SpringBoot 2.x提供RESTful接口、自动装配、内嵌Tomcat持久层MyBatis Plus简化单表CRUD配合MyBatis写复杂查询权限认证JWT无状态登录态管理缓存Redis缓存热点景点数据、验证码等前端框架Vue 2.xSPA页面开发UI组件库Element UI后台管理界面组件HTTP请求Axios前后端数据交互数据库MySQL 5.7业务数据持久化项目管理Maven后端依赖管理后端选SpringBoot主要是看中它的生态成熟、配置少、启动快对新手也友好。MyBatis Plus是个很实用的加分项内置了BaseMapper单表增删改查几乎不用写SQL开发者可以把精力放在联表查询和业务逻辑上。JWT用来做无状态认证前后端分离场景下比传统的Session更合适因为接口可以直接给不同客户端复用。前端选Vue 2 Element UI算是国内中小型后台管理系统非常成熟的一套组合。Vue的响应式数据绑定写起来效率高Element UI提供现成的表格、表单、弹窗、分页组件管理端页面基本上就是组装组件的过程。游客端则自己写了部分页面样式比如景点卡片、线路时间线、订单步骤条保证用户能看到的内容更有设计感而不是满屏都是后台表格。2. 数据库设计旅游业务的核心表结构2.1 表结构总览与ER思路数据库是这套项目里最值得先看的部分。业务表没有设计好后面写接口和页面都会很别扭。这套系统的数据库我用的库名是travel_db核心表大概有这几张表名说明关键字段user用户表id, username, password, nickname, avatar, phone, statusscenic_spot景点表id, name, cover, images, address, area, price, description, statustravel_line旅游线路表id, title, cover, days, price, start_place, destination, itinerary, stockhotel酒店表id, name, cover, city, address, star, description, statushotel_room房型表id, hotel_id, name, price, stock, area, bed_typeorders订单表id, order_no, user_id, product_type, product_id, product_name, price, status, create_timearticle攻略文章表id, user_id, title, cover, content, status, view_countarticle_comment评论表id, article_id, user_id, content, create_timecollect收藏表id, user_id, target_type, target_id从ER关系上看user是核心主体它跟orders、article、collect都是1对多的关系hotel和hotel_room是1对多orders表里的product_type字段用来区分订单到底是景点门票、旅游线路还是酒店房型这样不用为每种商品拆三张订单表逻辑更统一。景点表里的images字段我直接存的是JSON数组格式的字符串例如[/images/spot1.jpg,/images/spot2.jpg]。这样设计是有意为之的图片列表本身就是整体读取、整体展示很少需要单独对某一张图片做更新操作用JSON字段存储比额外建子表更简单适合这种中小规模项目。当然如果业务复杂到需要单独维护每张图片的排序、分组、状态那还是应该拆成图片子表。2.2 订单表与库存设计的坑做这种带预订属性的系统库存和订单并发是最容易出问题的地方。旅游线路有剩余名额酒店房型有剩余数量游客下单时必须扣减库存。最保守的方式是直接在Service层写int count update(update travel_line set stock stock - 1 where id ? and stock 0); if (count 0) { throw new RuntimeException(名额不足); }这种带条件的UPDATE天然带了乐观锁效果数据库行锁会保证同一时间只有一个请求能成功扣减而且通过stock 0这个条件挡住了超卖。我实际测试下来这种写法对于课设和中小流量系统完全够用不需要引入复杂的MQ或分布式锁。订单状态我用一个int字段存储0待支付、1已支付、2已取消、3已完成注释里写清楚枚举含义。在查询时用MyBatis Plus的LambdaQueryWrapper按状态过滤非常顺手。这里有个经验状态字段不要用字符串否则查询和统计都不方便而且容易写错值。2.3 SQL脚本初始化与测试数据项目附带的SQL脚本是直接从Navicat导出再手动清理过的导入时要注意几点选择utf8mb4字符集不然中文可能乱码emoji表情也存不进去。不要直接在带外键约束的情况下乱删表先按依赖关系DROP。SQL里自带了一批测试数据包括10个景点、6条线路、8个酒店、若干攻略这样启动后前台页面能看到完整效果不需要自己慢慢录数据。导入命令可以直接用mysql -uroot -p travel_db travel_db.sql如果是用Navicat或DataGrip导入直接建一个同名数据库然后选择运行SQL文件即可。SQL脚本里每张表都有注释字段名也尽量用见名知意的方式命名比如order_no、product_name、create_time这样后端实体类映射时不容易混淆。3. SpringBoot后端核心模块解析3.1 项目目录结构与分层思路后端工程结构我整理成了一套比较标准的写法拿到源码后可以一眼看清每个包的作用src/main/java/com/travel ├── TravelApplication.java ├── common │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config │ ├── WebMvcConfig.java │ └── RedisConfig.java ├── controller │ ├── admin │ └── portal ├── service │ ├── impl ├── mapper ├── entity ├── dto └── utilscontroller包下分admin和portal两个子包这样做的好处是权限边界非常清楚。admin开头的接口都要求管理员Tokenportal开头的接口是游客端数据。service层写业务逻辑mapper层只做数据访问entity对应数据库表dto放接收参数的模型。分层不是越多越好而是要让每一层只做一件事。很多新手喜欢把所有逻辑堆在Controller里面一时是快了但接口多了之后会非常难受。我这里坚持Controller只做参数接收和结果封装业务判断放到Service这样排查问题的时候能顺着层一层层找而不是在一坨方法里翻。3.2 登录鉴权JWT 拦截器登录鉴权用JWT实现核心逻辑分三块登录时签发Token、拦截器校验Token、前端请求携带Token。登录接口接收用户名和密码密码通过MD5加盐后与数据库比对比对成功就生成Token返回给前端。Token工具类里做一个简单的封装public class JwtUtils { private static final String SECRET travel-secret-key; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器做的事情就是读取Header里的Authorization字段解出用户Id和角色把用户信息放到ThreadLocal里方便后续业务取用。注意拦截器里要排除登录接口、注册接口、部分游客端公开接口比如景点列表和详情就不需要登录也能看。WebMvcConfig里注册拦截器并且把静态资源放行掉否则上传的图片带Token也访问不到这是个很容易被忽略的细节。3.3 文件上传与静态资源映射旅游网站里图片是刚需景点轮播图、线路封面、酒店图片、攻略配图都涉及上传。考虑到这套项目要方便本地跑我用的是本地磁盘存储方式上传目录配置在application.yml里file: upload-dir: D:/travel/upload/上传接口接收MultipartFile生成UUID文件名按日期分目录存放然后返回带/images前缀的访问路径。后端需要做一个虚拟路径映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceHandler(file: uploadDir); } }这样前台用就能直接访问。如果你想把存储层升级成MinIO也不难引入minio依赖把上传逻辑里的FileOutputStream替换为MinIO客户端的putObject即可接口返回的路径改成MinIO的Bucket访问地址。热搜里经常有人问minio怎么加到SpringBoot里其实就是要处理好配置、客户端初始化、上传和访问这四个环节和你做本地文件上传的流程是一模一样的。3.4 统一返回封装与全局异常处理所有接口统一返回Result对象结构是{ code: 200, message: 操作成功, data: {} }Result提供静态方法success()和error()Service直接返回业务数据Controller统一包装这样前端Axios拦截器里只需要判断code是否为200即可不需要每个页面各自处理错误。全局异常处理用RestControllerAdvice捕获两种异常业务异常和系统异常。业务异常是主动throw的比如“库存不足”、“登录已过期”系统异常是空指针、数据库连接之类的问题统一返回500提示避免把敏感信息直接暴露给前端。这一层看起来简单但实际体验提升非常大前端再也不用拿到一堆看不懂的英文报错了。4. Vue前端实现与前后端联调4.1 前端目录结构与路由设计前端工程我用Vue CLI创建目录结构大概是src ├── api ├── assets ├── components ├── router ├── store ├── views │ ├── admin │ └── portal ├── App.vue └── main.js路由设计上admin区域统一走一个带侧边栏布局的路由组件子路由对应景点管理、线路管理、酒店管理、订单管理等页面。portal区域则对应首页、景点列表、景点详情、线路列表、酒店列表、攻略列表、用户中心等页面。这里提醒一下前端路由懒加载是必须的否则首屏一次性加载所有页面在开发环境可能没感觉打包上线后体积会非常大。写法就是{ path: /admin/scenic, name: AdminScenic, component: () import(/views/admin/ScenicList.vue) }4.2 Axios封装与Token注入项目里封了一个request.js统一创建Axios实例设置baseURL和超时时间添加请求拦截器和响应拦截器。请求拦截器负责把localStorage里的Token拼到Header里响应拦截器负责统一处理code和错误状态。service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use(res { const { code, message, data } res.data if (code 200) { return data } if (code 401) { localStorage.removeItem(token) router.push(/login) } Message.error(message) return Promise.reject(new Error(message)) })Token失效跳登录页这个逻辑一定要有因为JWT会有过期时间用户长时间不操作后再点页面接口就会返回401。如果没有统一处理页面就会一直报错但用户不知道发生了什么。加了跳转之后至少体验上是闭环的。4.3 前端页面和接口联调的注意事项页面联调阶段有几个比较烦人的问题我在写这套项目时几乎都遇到过。第一是本地跨域。Vue开发服务器默认端口是8080SpringBoot是8080CORS如果不配置前端请求根本发不出去。解决办法有两种后端配置CorsFilter或者前端配置devServer的proxy。我更推荐用proxy因为上线后前后端通常是同一域名由Nginx转发proxy的方式更接近生产环境。devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }第二是分页参数名要对齐。MyBatis Plus默认的翻页参数是current和size而我前端用的是pageNum和pageSize两者对不上就会翻页失效。这种情况最好的方式不是改前端或后端而是统一封装一个分页请求工具把前端参数翻译成后端能识别的字段。第三是日期格式。MySQL的datetime传到前端会变成一个带T的字符串显示起来很不友好我在后端给时间字段加了JsonFormat注解统一成yyyy-MM-dd HH:mm:ss前端表格直接渲染即可。5. 本地环境搭建与项目启动5.1 环境准备拿到源码后第一步是准备环境。后端用的JDK建议1.8Maven 3.6MySQL 5.7Redis可选装因为部分页面做了缓存如果不需要缓存可以先关掉Redis相关配置。前端需要Node.js 14以上npm版本6以上。建议版本对照表软件版本建议说明JDK1.8SpringBoot 2.x稳定支持Maven3.6依赖管理MySQL5.7或8.0脚本通用Redis5.0可先不装不影响主体运行Node.js14.x前端构建Vue CLI4.x或5.x创建工程和调试5.2 后端启动步骤IDEA打开后端工程后等待Maven下载依赖。然后修改application.yml里的数据库连接和上传目录确认Redis配置是否启用。运行TravelApplication主类控制台出现SpringBoot启动日志后后端就算起来了。一个常见问题是Lombok没装或者版本不对导致实体类里的Data编译报错。IDEA需要在Settings里安装Lombok插件并且开启Annotation Processing。这个碰到的人很多提前说明一下能省不少时间。5.3 前端启动步骤前端工程用WebStorm或VSCode打开先在根目录执行npm install如果下载慢可以把镜像源切换成国内镜像npm config set registry https://registry.npmmirror.com然后执行npm run serve默认端口是8081浏览器访问localhost:8081就能看到前台页面。后台管理地址是localhost:8081/admin管理员账号和密码在SQL脚本里已经初始化一般是admin / 123456。登录后可以进入管理后台修改景点、添加酒店、处理订单所有操作都会实时写入后端接口。6. 部署上线与常见问题速查6.1 前后端分离部署方案开发环境跑通之后下一步通常就是部署到服务器。前后端分离项目我常用的部署方式是把后端打成Jar包、前端打包成dist目录然后用Nginx统一托管。后端打包mvn clean package -DskipTests生成的travel-server.jar直接丢到服务器上执行java -jar travel-server.jar前端打包npm run builddist目录上传到服务器Nginx配置大致如下server { listen 80; server_name www.example.com; root /var/www/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 /images/ { alias /www/travel/upload/; } }这里有个关键点前端打包时的axios baseURL要改成/api而不是写死http://localhost:8080否则部署到服务器后请求地址还是本地环境页面数据加载不出来。项目文档里把这两种环境下的配置方式都写清楚了照着改就行。6.2 常见问题排查表结合我整理源码时的实操经验把最容易遇到的几个问题列成一张速查表问题现象可能原因解决办法前端启动后页面打不开Node版本过低或依赖缺失升级Node删除node_modules重新npm install后端启动报数据库连接失败application.yml里密码错误或MySQL未启动核对账号密码检查3306端口上传图片后访问404静态资源映射路径不对检查WebMvcConfig里的映射和file.upload-dir路径前端请求接口404baseURL路径没有匹配后端Controller的RequestMapping统一用/api前缀Nginx做转发登录后接口提示未登录Token过期或Header名不一致检查前端Authorization字段和后端拦截器取值控制台中文乱码编码未设置为utf8mb4数据库连接加characterEncodingutf8景点列表无法翻页分页参数不匹配统一前端pageNum/pageSize与MyBatis Plus的current/size这里想单独聊一下数据库连接串。MySQL 8.0版本的驱动名称和5.7不一样如果你本地用的是MySQL 8.0而pom里还是老版本驱动会报ClassNotFound或者连接超时。文档里我特意写了一个兼容性说明5.7用com.mysql.jdbc.Driver8.0用com.mysql.cj.jdbc.Driver同时URL后面要增加serverTimezoneAsia/Shanghai来消除时区报错。6.3 源码与文档的使用建议源码里附带了一份完整的项目文档里面包含系统需求说明、功能模块图、数据库设计说明、接口文档、运行部署手册。文档的接口部分把每个接口的请求方式、参数、返回结果都整理成了表格调试接口时可以直接对照。我的建议是拿到源码后不要先着急改业务而是按这个顺序来读先看数据库SQL脚本搞懂表之间的关系。启动后端用Swagger或者Knife4j把接口文档过一遍。启动前端对照页面把每个功能怎么调接口走一遍。最后再按自己的需求去改某个模块比如给酒店加一个“推荐”标记或者给景点加一个“评分”字段。这样一轮下来你会对前后端如何协作有一个完整的认识而不是看到一个大的工程不知道从哪里下手。文档里还留了一个TODO列表标注了哪些功能可以继续扩展比如接入地图选点、对接真实支付、增加短信验证码登录等这些都是后续二次开发很好的方向。另外补充一个从实际使用中得到的体会做这种前后端分离项目最容易翻车的不是某个复杂算法而是环境不一致。同一套代码我虚拟机上跑没问题换到同学的电脑上就各种报错绝大多数都是JDK版本不对、Node版本不对、数据库版本不对这三件事导致的。所以如果你也是在自己电脑上复现别人项目第一件事不是改代码而是先把环境对齐。把这套项目完整跑通一遍再动手改业务后面很多概念都会顺手很多。
网站建设高端定制企业官网