基于Spring Boot与MyBatis的景区旅游管理系统构建指南 简介基于 Java、Springboot、Mybatis、MySQL、Bootstrap、Maven 构建的景区旅游管理系统项目包面向 Java 全栈学习者、毕业设计者及旅游信息化开发人员覆盖用户、景区、订单、支付、评论、地图等典型业务既可用于课程设计也可作为中小型景区管理系统的开发蓝本。压缩包共 470 个文件大小约 19.32MB包含 java 源码、xml 映射配置、html/css/js 前端页面、sql 数据库脚本、yml 环境配置和说明文档从后端逻辑到页面交互、从数据建表到环境部署均有完整呈现目录结构清晰便于按模块检索。目前已有 784 人学习下载。系统完整实现了用户注册登录、个人信息维护、景区信息展示与评价、订单预订与退订支付、地图导航、后台管理等核心模块权限管理采用 Spring Security订单模块预留第三方支付接口导航模块可对接高德或百度地图。项目代码按 controller、service、dao、mapper 分层职责清晰即使是初学者也能按图索骥同时附带数据库初始化脚本、Maven 配置、项目说明和核心业务代码能够帮助读者快速掌握前后端联调、理解自动装配原理并积累真实项目的部署经验。1. 景区旅游管理系统的技术栈选型为什么用这套组合一个景区旅游管理系统业务场景看起来不复杂景区信息维护、线路管理、门票订单、游客统计。但真正动手做的时候你会发现难点不在业务本身而在数据关联关系和并发控制上——一条线路关联多个景点一张订单关联多条线路库存和余票要实时扣减这些逻辑叠加在一起光靠简单的 CRUD 是撑不住的。选 Java Spring Boot MyBatis MySQL Bootstrap Maven 这套组合是中小型管理系统最稳妥的路线。Spring Boot 负责把整合成本压下来MyBatis 把 SQL 控制权留在开发手里MySQL 处理关系型数据Bootstrap 快速搭出后台界面Maven 管理依赖和构建。这套技术栈的另一个优势是招人容易、排错资料多遇到问题搜一下就有答案。适合正在做毕业设计、课程设计或者是小团队接景区类外包项目的开发者参考。下面从数据访问层开始把每个环节怎么落地讲清楚。2. 用 Spring Boot MyBatis MySQL 搭数据访问层分页和动态 SQL 是关键2.1 Maven 依赖与工程结构创建一个 Spring Boot 项目用 Maven 管理依赖。核心依赖就五个web、mybatis-spring-boot-starter、mysql-connector-java、lombok 可选、pagehelper-spring-boot-starter分页用。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency注意 mybatis-spring-boot-starter 的版本要和 Spring Boot 主版本匹配。Spring Boot 2.7.x 对应 MyBatis starter 2.3.xSpring Boot 3.x 需要用 mybatis-spring-boot-starter 3.0。如果启动时报Failed to auto-configure a DataSource先检查是不是版本匹配问题。2.2 景区线路表设计先想清楚关系再建表景区管理系统的核心表至少有四张景区表、线路表、订单表、订单明细表。线路和景区是多对多关系所以需要一张中间表route_scenic来关联。建表时有个原则订单相关的金额字段用 DECIMAL 不用 FLOAT避免浮点误差状态字段用 TINYINT 存数字枚举不用字符串查询和索引都更快。CREATE TABLE route ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, route_name VARCHAR(100) NOT NULL COMMENT 线路名称, route_type TINYINT NOT NULL DEFAULT 0 COMMENT 线路类型 0一日游 1多日游, price DECIMAL(10,2) NOT NULL COMMENT 成人价格, stock INT NOT NULL DEFAULT 0 COMMENT 可售库存, sold INT NOT NULL DEFAULT 0 COMMENT 已售数量, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type_status (route_type, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT旅游线路表;联合索引idx_type_status覆盖了后台管理页最常见的筛选条件。如果你还要按价格排序可以在 price 上单独加索引但不要什么字段都加写多读少的表加索引反而拖慢写入。2.3 PageHelper 分页插件三行代码接入分页景区管理后台的线路列表、订单列表都要分页。手写 LIMIT 还要数总数用 PageHelper 可以在不改 SQL 的前提下自动拦截分页。public PageInfoRouteVO pageRoutes(RouteQuery query) { // 开启分页只对紧随其后的第一条查询生效 PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListRouteVO list routeMapper.selectByCondition(query); return new PageInfo(list); }逻辑说明PageHelper.startPage()是 ThreadLocal 机制它在当前线程上下文中设置分页参数MyBatis 执行下一次查询时自动拼接LIMIT同时执行COUNT查询总数。返回的PageInfo里封装了total、pageNum、pageSize、list等字段直接塞给前端即可。参数说明pageNum从 1 开始pageSize不宜太大后台列表单页 10 到 20 条是常见做法。特别注意startPage()必须紧跟要分页的查询语句中间不能穿插其他查询否则分页参数会被错误消费。2.4 多条件查询用动态 SQL拒绝字符串拼接线路列表页通常有名称模糊搜索、类型筛选、状态筛选三个条件。如果用 Java 代码拼 SQL条件一多代码就乱。MyBatis 的where加if是标准解法。select idselectByCondition resultTypecom.example.vo.RouteVO SELECT id, route_name, route_type, price, stock, sold, status FROM route where if testrouteName ! null and routeName ! AND route_name LIKE CONCAT(%, #{routeName}, %) /if if testrouteType ! null AND route_type #{routeType} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /select逻辑说明where标签会自动去掉第一个多余的 AND 或 OR避免 SQL 语法错误。CONCAT(%, #{routeName}, %)做模糊匹配比直接在 Java 里写%拼接更安全也防止了 SQL 注入。参数说明routeType和status都是 Integer 类型前端没传值时为 nullif判断会跳过如果传了0因为判断条件是! null而不是! Integer 类型的 0 不会被误过滤。这里有个常见的坑如果用status ! 同时判断Integer 类型比较会有问题建议始终用! null判断数字类型。2.5 Mapper 接口与 XML 的映射关系Mapper 接口和 XML 文件通过 namespace 绑定XML 文件放在resources/mapper/目录下在 application.yml 里配置扫描路径。mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置很重要它让数据库的route_name自动映射到 Java 属性的routeName不需要写繁琐的 resultMap。如果属性映射不上先检查这个配置是否开启再看数据库字段和 Java 属性是不是真的对应。3. 核心业务订单状态流转和库存扣减事务和锁怎么配合3.1 订单状态机设计四个状态走完一个流程订单模块是景区管理系统的核心它的状态流转比普通 CRUD 复杂。我一般会设计四个状态待支付0、已支付1、已取消2、已完成3。流转规则待支付可取消可支付已支付可完成已取消和已完成是终态。状态变化必须通过 Service 层方法控制不允许直接 UPDATE。在订单表里加一个status字段配合update_time做乐观锁版本判断防止并发下状态被覆盖。ALTER TABLE order ADD COLUMN status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消 3已完成; ALTER TABLE order ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号;3.2 下单事务库存扣减和订单创建要同时成功用户下单时要做两件事扣减线路库存、插入订单记录。这两步必须在一个事务里否则库存扣了订单没建或者订单建了库存没扣都会出问题。Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { // 1. 乐观锁扣减库存返回影响行数 int updated routeMapper.deductStock(dto.getRouteId(), dto.getQuantity()); if (updated 0) { throw new BusinessException(库存不足或线路已下架); } // 2. 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setRouteId(dto.getRouteId()); order.setQuantity(dto.getQuantity()); order.setAmount(routeMapper.selectPrice(dto.getRouteId()).multiply(BigDecimal.valueOf(dto.getQuantity()))); order.setStatus(0); orderMapper.insert(order); return order.getId(); }对应的 XML 扣减语句用乐观锁update iddeductStock UPDATE route SET stock stock - #{quantity}, sold sold #{quantity} WHERE id #{routeId} AND stock #{quantity} AND status 1 /update逻辑说明stock #{quantity}这个条件天然防止了超卖。如果库存不足影响行数为 0事务回滚订单不会创建。Transactional(rollbackFor Exception.class)指定了遇到所有异常都回滚不只是 RuntimeException这个参数在实际业务里很关键。注意库存扣减是「先扣再查」在高并发下配合数据库行锁保证不会超卖。如果景区业务的并发量不大每秒几十单这种行锁方案完全够用不需要引入 Redis 分布式锁。3.3 支付回调的状态流转要处理重复通知景区票务系统对接微信或支付宝支付时支付结果通知是异步且可能重复的。支付回调的处理逻辑必须是幂等的同一个订单被通知多次结果一致。Transactional(rollbackFor Exception.class) public void payCallback(String orderNo, String tradeNo) { Order order orderMapper.selectByOrderNo(orderNo); if (order null) { throw new BusinessException(订单不存在); } // 已支付则直接返回不做重复处理 if (order.getStatus() 1) { return; } int updated orderMapper.updateStatusByVersion(order.getId(), 1, order.getVersion()); if (updated 0) { throw new BusinessException(订单状态已变更请勿重复操作); } orderMapper.updatePayInfo(order.getId(), tradeNo); }提示状态流转时用updateStatusByVersion配合 version 字段做乐观锁避免两个线程同时把订单从待支付改成已支付。实际开发中支付回调还要校验金额防止篡改这一步在对接支付接口时别漏掉。4. Bootstrap 前端页面整合表格渲染和参数传递是重点4.1 Bootstrap 布局先搭后台管理框架Bootstrap 做后台管理页面核心是栅格系统和表格组件。引入 Bootstrap 4 的 CSS 和 JS配合 jQuery不需要额外的前端构建工具。link relstylesheet href/static/bootstrap/css/bootstrap.min.css script src/static/jquery/jquery.min.js/script script src/static/bootstrap/js/bootstrap.min.js/script布局上用container-fluid做全宽容器左侧用col-md-2放导航右侧col-md-10放内容区。这套布局写一次后续页面复制结构就行。4.2 表格列表渲染和后端 PageInfo 对接景区线路列表页的常见做法是页面加载时发送 ajax 请求后端返回 PageInfo 数据前端用 jQuery 拼接表格行和分页栏。function loadRoutes(pageNum, pageSize) { $.ajax({ url: /api/routes, type: GET, data: { pageNum: pageNum, pageSize: pageSize }, dataType: json, success: function(res) { if (res.code 200) { renderTable(res.data.list); renderPagination(res.data); } } }); } function renderTable(list) { var html ; for (var i 0; i list.length; i) { var item list[i]; html tr td item.id /td td item.routeName /td td (item.routeType 0 ? 一日游 : 多日游) /td td¥ item.price /td td item.stock /td td button classbtn btn-sm btn-primary onclickeditRoute( item.id )编辑/button button classbtn btn-sm btn-danger onclickdeleteRoute( item.id )删除/button /td/tr; } $(#routeTableBody).html(html); }逻辑说明renderTable用字符串拼接生成表格行比 DOM 操作性能好代码也更直观。routeType用三元表达式转换成可读文字状态和类型字段在前端展示时都要做这种转换不要直接显示数字。参数说明res.data.list是当前页数据res.data.total是总记录数后端 PageInfo 的 JSON 序列化默认包含这些字段。如果返回结构对不上检查后端是否配置了 Jackson 对 PageInfo 的序列化支持PageHelper 的 PageInfo 在 Spring Boot 2.x 下默认可以正常序列化。4.3 Bootstrap 模态框做编辑参数回显要注意类型编辑弹窗用 Bootstrap Modal 组件核心是回显数据和提交更新。回显时注意后端返回的数字类型比如stock是 Integer回填到 input 的 value 时如果是 null要默认给 0避免页面上出现undefined。function editRoute(id) { $.get(/api/routes/ id, function(res) { if (res.code 200) { var data res.data; $(#editId).val(data.id); $(#editName).val(data.routeName || ); $(#editPrice).val(data.price || 0); $(#editStock).val(data.stock || 0); $(#editType).val(data.routeType); $(#editModal).modal(show); } }); }提交时用 serialize 收集表单数据转成 JSON 发送$(#editForm).submit(function(e) { e.preventDefault(); var formData $(this).serialize(); $.post(/api/routes/update, formData, function(res) { if (res.code 200) { $(#editModal).modal(hide); loadRoutes(currentPage, pageSize); // 刷新当前页 } else { alert(res.msg); } }); });逻辑说明serialize()会把表单里的字段自动编码成keyvaluekeyvalue格式Spring MVC 可以直接绑定到 DTO。刷新时用currentPage保持在当前页不要在编辑完跳回第一页这个细节影响实际使用体验。4.4 Bootstrap 表格和组件的两个常见坑第一个坑是 jQuery 的 ajax 请求默认不处理 Bootstrap 表单校验。Bootstrap 的has-error样式需要手动触发常见的做法是用 jQuery Validate 插件或者在前端拦截必填字段做简单判断。第二个坑是下拉菜单的动态渲染。Bootstrap 4 的dropdown组件如果选项是动态加载的需要重新初始化。实际开发中线路类型的下拉框如果是select而不是dropdown-menu直接用$(#routeTypeSelect).html(options)重新生成即可不需要额外初始化。5. Maven 多环境配置与联调排错把这些时间省下来5.1 Maven Profile 管理多环境配置景区项目的开发、测试、生产环境数据库地址不同。用 Maven Profile 配合 Spring Boot 的多环境配置打包时指定环境不用每次改配置文件。profiles profile iddev/id properties activated.propertiesdev/activated.properties /properties activation activeByDefaulttrue/activeByDefault /activation /profile profile idprod/id properties activated.propertiesprod/activated.properties /properties /profile /profiles对应在 application.yml 里引用spring: profiles: active: activated.propertiesMaven 打包时mvn clean package -Pprod就会替换掉activated.properties为prod加载application-prod.yml。这个技巧能避免每次部署前手改数据库连接特别是多人协作时每个人本地都用 dev 配置不会互相干扰。5.2 MyBatis 参数比较的隐蔽 BugMyBatis 里单个数字字符比较有个经典问题。如果在 XML 里写if testrouteType 1这里的1会被 MyBatis 当作 char 类型和一个 String 类型的参数比较时永远返回 false。正确写法if testrouteType 1外层单引号、内层双引号MyBatis 才会把它解析成 String 类型比较。实参数是 Integer 类型时直接用routeType 1注意两侧类型要一致。这个坑在面试题里反复出现实际开发中也容易踩排查时看日志会发现条件莫名缺失。5.3 慢 SQL 和缓存问题的排查思路景区后台如果出现列表页加载慢用 MyBatis 的日志输出 SQL 执行时间先定位是查询慢还是渲染慢。mybatis-config.xml里配置log-impl: org.apache.ibatis.logging.stdout.StdOutImpl控制台会打印完整 SQL 和参数。看到 SQL 后用EXPLAIN SELECT ...查看是否走了索引重点关注type字段是ALL还是ref/rangeALL说明全表扫描需要加索引。MyBatis 的一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 内两次相同查询不会访问数据库。但PageHelper.startPage会强制查询 count可能让一级缓存失效导致重复查询。如果发现同一请求内多次查库检查是否误用了分页插件。5.4 验证系统是否可交付的检查清单把系统跑通后用一个简单的清单验证核心链路线路管理能否正常增删改查、下单后库存是否正确扣减、重复提交订单是否被拦截、取消订单后库存是否回补。这几条是景区系统的生命线任一条出错都直接影响业务。验证完再交付比后期补坑省时间得多。本文还有配套的精品资源点击获取