前后端分离旅游管理系统实战:从SpringBoot+Vue到Nginx部署全流程
发布时间:2026/9/26 11:34:09来源:尧图网络
前后端分离旅游管理系统听起来像是个教科书项目但真把一个SpringBootVueMyBatisMySQL的完整系统从零搭起来、跑通、部署上线中间涉及的东西远比“增删改查”那五个字多。最近我正好把这套系统的完整源码、数据库脚本和部署步骤重新整理了一遍从模块拆分、接口设计到Nginx上线每一步都实际跑过踩坑记录也在手边。这篇就当是一次项目复盘适合正在学前后端分离、准备毕设或者想练手部署的同学照着这套流程走一遍基本能把“明确需求—设计表结构—写后端接口—搭前端页面—部署发布”整条链路吃透。1. 项目整体设计与技术选型拆解1.1 前后端分离架构的落地思路前后端分离的核心是把“页面渲染”和“接口数据”彻底拆开。后端只负责输出JSON前端负责展示和交互两者通过HTTP接口通信。我用这套思路构建的旅游管理系统分为两个端一个面向普通游客包含景点浏览、线路查询、在线下单另一个面向运营管理员负责景点管理、线路管理、订单处理、用户管理等。这样拆分带来的好处非常直观前后端可以并行开发真实团队里前端和后端不用互相等前端可以独立部署在Nginx上后端只提供接口服务接口可以被多个端复用以后做小程序、App后端基本不用动。打个比方这就像把后厨和前厅分开后厨只负责把菜做好JSON数据前厅专注摆盘和服务Vue组件渲染。如果混在一起后厨既要炒菜又要端盘子改一个页面就得动整套服务扩展性很差。1.2 技术栈选型的底层逻辑技术栈选的是SpringBoot Vue MyBatis MySQL这套组合在中小型管理系统里非常主流选它背后是有原因的。SpringBoot解决的是“配置地狱”问题。传统SSM要写大量XML配置SpringBoot把这些东西都简化了内嵌Tomcat一个java -jar就能跑起来。项目里我只需要关注业务代码不用操心容器搭建。Vue解决的是“页面维护困难”的问题。组件化开发让页面复用度非常高管理后台配合ElementUI表单、表格、弹窗这些常用组件直接拿来用开发效率比jQuery时代高太多。MyBatis解决的是“SQL不可控”的问题。旅游业务天然需要多表关联查询景点、线路、订单、评论、用户这些场景下MyBatis的SQL手写能力反而变成优势。用动态SQL标签可以很优雅地处理多条件查询。MySQL则是稳定且免费的关系型数据库配合Navicat或DataGrip工具表结构设计和数据排查都很快。对于这种量级的系统MySQL完全够用。也有人问我为什么不直接用若依这类前后端分离脚手架若依确实把权限管理、代码生成都封装好了但正因为封装太全很多人改完之后根本不知道底层发生了什么。自己从零搭一遍SpringBootVueMyBatis对接口分层、SQL编写、路由守卫这些基本功的理解会扎实很多。1.3 功能模块划分与数据库表设计这套系统的功能模块我按业务拆成了六块模块说明用户管理注册、登录、角色区分普通用户/管理员景点管理景点信息维护、上下架、门票价格管理线路管理旅游线路、天数、价格、行程安排维护订单管理用户下单、取消、订单状态流转评论管理游客对景点或线路的评分与留言数据统计订单量、销售额的简单汇总展示数据库表设计是项目里最先要定下来的事表设计不合理后面写接口会非常痛苦。我第一期用了五张核心表-- 用户表 CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, nickname varchar(50) DEFAULT NULL, role int(1) DEFAULT 1 COMMENT 1普通用户 2管理员, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 景点表 CREATE TABLE scenic ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, cover varchar(255) DEFAULT NULL, description text, price decimal(10,2) DEFAULT NULL, city varchar(50) DEFAULT NULL, status int(1) DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表稍微特殊一点需要同时关联用户、景点、线路我通常建议不要硬塞冗余字段而是通过外键逻辑关联查询时再用JOIN或子查询取数据。这里必须强调一个细节密码一定不能明文存储。学生项目里经常看到密码存明文这是非常严重的安全隐患。我用的BCrypt加密注册时加密入库登录时用加密库校验即使数据库泄露密码也无法被直接还原。2. 核心功能实现与难点解析2.1 后端分层业务代码与接口设计后端严格采用三层架构Controller只接收参数、返回结果Service负责业务逻辑、事务控制Mapper只做数据读写。这种分层的核心价值是把代码职责切清楚后续加需求、排查问题都能准确定位。以景点列表接口为例RestController RequestMapping(/api/scenic) public class ScenicController { Autowired private ScenicService scenicService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, ScenicQuery query) { return Result.success(scenicService.page(pageNum, pageSize, query)); } }Controller里看不到任何SQL逻辑它只负责接收前端传来的参数然后交给Service处理统一包装成Result返回。权限控制这块我用的是拦截器加自定义注解的方式。登录成功后后端把用户信息查询出来前端请求时在Header带上Token拦截器里校验Token并识别用户角色public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !TokenUtil.isAdmin(token)) { response.setStatus(401); return false; } return true; } }具体到业务接口凡是以/admin开头的路径全部走这个拦截器。这样做的好处是不用每个Controller都写一遍权限判断代码非常干净。事务控制也是后端重点。用户下单时需要校验库存、查询线路、创建订单、扣减库存任何一个步骤失败都不能让数据出现“半成品”。解决方式就是在Service方法上加上TransactionalTransactional(rollbackFor Exception.class) public OrderResult createOrder(OrderCreateDTO dto) { // 1. 校验线路是否有效 // 2. 创建订单生成唯一订单号 // 3. 扣减库存或剩余名额 // 4. 返回订单信息 }如果哪天订单量大了这种写法还要考虑并发问题。我见过最典型的场景两个人同时抢最后一个名额各自都校验通过了最后库存变成负数。解决上可以用数据库的乐观锁在UPDATE时加条件影响行数为0说明没抢到update travel_route set stock stock - 1 where id #{id} and stock 02.2 MyBatis动态SQL与分页插件实操MyBatis最强大也是最容易翻车的点就是动态SQL和XML配置。以线路多条件查询为例select idselectRouteList resultTypecom.travel.entity.TravelRoute select * from travel_route where if testdestination ! null and destination ! and destination like concat(%, #{destination}, %) /if if teststatus ! null and status #{status} /if if testminPrice ! null and price #{minPrice} /if /where order by create_time desc /select注意这里我用的是 标签而不是直接拼“where 11”。 会自动处理第一个条件前面的and/or避免SQL拼接时的经典错误。分页功能用的是PageHelper分页插件。这个插件在SpringBoot里接入非常简单maven引入dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency然后在yml里配置pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true调用方式PageHelper.startPage(pageNum, pageSize); ListTravelRoute routeList travelRouteMapper.selectRouteList(query); PageInfoTravelRoute pageInfo new PageInfo(routeList);pageInfo里已经包含了total、pageNum、pageSize、pages这些字段前端拿到之后直接驱动分页组件。PageHelper使用有一个关键的坑startPage方法必须紧跟在你要分页的那条查询语句之前而且只对该第一条查询生效。如果startPage之后又执行了别的查询分页就失效了。多表关联查询时count统计也可能计算错误这种情况我建议自己写countSql来兜底。实测下来掌握这三个要点基本不会翻车插件依赖引入、startPage位置、复杂SQL的自定义count。2.3 前端Vue路由、状态与页面搭建前端我用的Vue 2 Vue Router Vuex Axios ElementUI。这套组合在后台管理类项目里特别顺手。Vue Router负责页面跳转。游客端和后台分开管理后台页面带上嵌套路由const routes [ { path: /, component: Home }, { path: /scenic/:id, component: ScenicDetail }, { path: /login, component: Login }, { path: /admin, component: AdminLayout, meta: { requiresAuth: true }, children: [ { path: scenic, component: AdminScenic }, { path: order, component: AdminOrder }, { path: user, component: AdminUser } ] } ];路由参数是前端必会的点。列表页跳到详情页时用params或query传参// 传递参数 this.$router.push({ path: /scenic/ row.id }); // 详情页接收 this.$route.params.id路由守卫控制登录状态我习惯用router.beforeEach统一处理router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.matched.some(record record.meta.requiresAuth) !token) { next(/login); } else { next(); } });Axios请求封装是前端一个必须做好的事。如果不封装每个页面都写一遍URL前缀、请求超时、Token拼接后期维护会非常痛苦。我在项目里单独建了一个request.jsconst service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { config.headers[Authorization] localStorage.getItem(token); return config; }); service.interceptors.response.use(res { const data res.data; if (data.code 401) { localStorage.removeItem(token); location.href /login; return Promise.reject(data); } return data; });在响应拦截器里做统一错误处理只要后端返回401就强制跳登录页这样每个接口都不用写重复的鉴权判断。页面本身没什么特殊技巧ElementUI的table、dialog、form组合起来一个管理后台就成型了。重点提醒表格数据上的按钮操作比如上架、下架、删除最好都加二次确认ElementUI的this.$confirm足够用。2.4 前后端联调统一返回与跨域处理前后端联调最怕的就是“你说的数据结构和我拿到的不是一回事”。所以我从一开始就定了统一返回结构public class Result { private Integer code; private String message; private Object data; // code 200 成功401 未登录500 系统异常 }所有后端接口都返回Result对象前端只看code字段判断成功失败data里面才是真正的业务数据。这样接口参数一旦有变化只需要看Result类和数据字段不需要猜。跨域问题也是联调阶段的高频事故。前端开发时跑在8080端口后端跑在9090端口浏览器会直接拦截跨域请求。我通常用两种方案开发环境首选前端proxy代理在vue.config.js里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } };这样前端代码里请求地址写/api/scenic/list开发时由webpack-dev-server帮我们转发到后端不涉及浏览器跨域策略。生产环境前后端被Nginx统一代理基本不存在跨域。但如果后端单独对外开放就需要配置CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(*) .allowedHeaders(*) .allowCredentials(true); } }注意一个细节当allowCredentials(true)时allowedOrigins不能写成*必须使用allowedOriginPatterns否则启动后跨域请求依然被拦。这是我实际踩过才知道的。3. 从源码到上线的完整部署流程3.1 环境准备JDK、Maven、Node、MySQL部署前先把环境装齐。我以Windows为例Linux命令大同小异。JDK我用的是JDK 8。安装后一定要配JAVA_HOME环境变量否则Maven和SpringBoot都跑不起来。验证方法命令行输入java -version能输出版本号就是成功。Maven用3.6以上版本配置settings.xml里的阿里云镜像不然下载依赖会慢到怀疑人生mirror idaliyunmaven/id mirrorOfcentral/mirrorOf namealiyun maven mirror/name urlhttps://maven.aliyun.com/repository/public/url /mirrorNode建议用14或16版本npm也配个淘宝镜像npm config set registry https://registry.npmmirror.comMySQL我用的是8.0。安装时注意选对认证方式MySQL 8默认caching_sha2_password老版本的客户端工具可能连不上可以在安装向导里切到mysql_native_password。另外服务要设为自启动不然重启电脑后容易忘记手动启动。3.2 数据库初始化与配置数据库那步首先执行SQL脚本建库建表CREATE DATABASE IF NOT EXISTS travel DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE travel;然后把项目里提供的init.sql文件完整执行一遍表结构和测试数据就有了。后端数据库连接信息在application.yml里配置spring: datasource: url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.travel.entity configuration: map-underscore-to-camel-case: true这两个配置很多人容易漏一是driver-class-nameMySQL 8要写com.mysql.cj.jdbc.Driver老版本才写com.mysql.jdbc.Driver 二是url里的serverTimezoneAsia/Shanghai不写的话连接时大概率报时区错误。3.3 后端打包jar方式与外置TomcatSpringBoot后端有两种常见部署方式。第一种是jar方式也是我最推荐的。项目的pom.xml里确认打包方式为jar然后执行mvn clean package -DskipTests打包完成后在target目录下会生成travel-1.0.0.jar直接运行java -jar target/travel-1.0.0.jarSpringBoot内嵌了Tomcat所以不需要额外安装Tomcat。只要服务器有JDK这个jar拷过去就能跑。第二种是外置Tomcat方式适用于公司服务器已经装了Tomcat、统一管理容器的场景。需要改三处pom.xml里改成war包packagingwar/packaging排除内嵌Tomcat依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency启动类继承SpringBootServletInitializerSpringBootApplication public class TravelApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(TravelApplication.class); } public static void main(String[] args) { SpringApplication.run(TravelApplication.class, args); } }重新打包后把war包复制到Tomcat的webapps目录启动Tomcat访问路径会比jar方式多一层应用路径例如http://ip:8080/travel/。前后端分离的项目里我基本都用jar方式。因为静态资源已经交给Nginx了后端只需要跑接口内嵌Tomcat足够再单独装一个Tomcat容器反而多此一举。3.4 前端构建与Nginx上线前端构建命令很简单npm install npm run buildbuild完成会生成dist目录这就是我们要交给Nginx的静态文件。先检查vue.config.js里的publicPath如果是默认的/部署到域名根目录没问题如果要部署到子路径必须改成相对路径或完整子路径否则css、js加载路径会错。Nginx配置是部署的核心下面是一份可直接套用的配置server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:9090/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }location /api/把前端请求代理到后端9090端口后端接口不需要暴露到公网。try_files $uri $uri/ /index.html这行必须写。Vue Router如果用了history模式前端刷新某个子路由时Nginx按路径找文件找不到会直接404。try_files会把所有不存在的路径重写到index.html交给前端路由接管。配置改完后执行nginx -t nginx -s reload没有报错的话访问你的域名系统就能正常使用了。4. 常见问题与排查技巧实录4.1 开发过程中的高频异常开发期遇到的问题普遍集中在接口调用和SQL映射上。我把实际遇到的典型问题整理成了一个速查表异常现象可能原因解决办法调用Mapper报Invalid bound statementmapper.xml没扫描到或namespace不对检查yml里mapper-locations是否指向xml目录xml文件namespace必须与接口全限定名一致页面请求全部401登录接口没放行或Token没传后端拦截器排除/login路径前端请求拦截器里确认设置了AuthorizationSQL报错where子句语法错误多条件查询拼接问题用 标签不要拼where 11返回字段全是null实体属性与数据库列名不一致开启map-underscore-to-camel-case或结果映射里加resultMapPOST请求中文乱码字符编码不一致数据库url加characterEncodingutf8请求统一使用UTF-8关于Invalid bound statement我多说一句这个报错70%以上是开发工具没把xml文件编译到target目录。maven里src/main/java下的xml默认不一定被复制需要把mapper.xml放在src/main/resources/mapper目录这才是最稳妥的做法。4.2 部署运行时的典型故障本地跑通不代表线上能跑通部署阶段的问题往往更隐蔽。常见故障如下MySQL 8启动后连接报“Server returns invalid timezone”这是url没有加serverTimezone参数。加到Asia/Shanghai即可。Nginx部署后首页能打开点进子路由刷新就404。这是路由模式与try_files配置不匹配。history模式一定要配try_files。前端打包后白屏控制台报“Uncaught SyntaxError”。多半是publicPath写错了或者dist的静态文件路径不对。打开打包后的index.html看src路径就能确认原因。SpringBoot后端启动了但前端请求接口超时。先检查服务器安全组/防火墙是否放行后端端口。如果前后端在同一台机器建议让Nginx直接代理到127.0.0.1不要开放公网端口。数据库无法连接先确认几个点MySQL服务是否启动、端口是否被占用、root账号能不能远程登录、防火墙是否放行。用命令行工具先直连一次排除网络问题再查应用配置。4.3 实测总结的独家避坑经验最后分享几条常规文档里不会写的经验全是我实际踩过的。第一图片上传必须考虑部署路径问题。项目里景点封面图、用户头像如果存本地千万别把路径写死成localhost。我最早就是这样本地开发好好的部署到服务器之后所有图片全裂。正确做法是把上传目录做成独立配置项存相对路径再用Nginx或SpringBoot的虚拟路径映射对外访问。第二分页插件的日志输出要留意。PageHelper自动生成的count语句有时会很慢尤其是多表关联查询时。如果数据量上来了建议给常用分页查询的表加上合理的索引count语句也可以通过自定义countSql优化。第三订单号生成别用时间戳。并发场景下时间戳重复概率很高订单号要有唯一性保障推荐用“日期雪花ID”或“日期随机数流水号”并在数据库里给订单号字段加唯一索引兜底。第四前后端联调时把网络面板当成第一排查窗口。很多新手遇到接口报错第一反应是去后端日志找原因。其实打开浏览器DevTools的Network看请求有没有发出、状态码是什么、响应内容是什么往往比看后端日志更快定位问题。后端日志我一般是最后才翻。这套系统从数据库设计到上线部署我前后跑了两遍才把流程彻底理顺。如果你也在做类似的前后端分离项目我的建议是先别急着加花哨功能把用户、景点、线路、订单这条主链路走通再慢慢补评论、统计、权限这些细节。把表结构设计好、接口规范定清楚后面每一步都会顺畅很多。
网站建设高端定制企业官网