用Spring Boot+MyBatis Plus搭建旅游系统网站实战指南
发布时间:2026/10/9 3:10:31 锦皓数字建站

简介这是一份基于 Java 与 SSM 框架Spring、SpringMVC、MyBatis实现的旅游系统网站完整源码项目适合 Java Web 学习者、课程设计与毕业设计开发者作为参考。项目覆盖用户注册登录、景点信息展示、线路查询预订、订单管理等核心模块完整呈现了后端业务逻辑、前端页面交互及数据库设计方案可用于快速理解 MVC 分层架构与持久层开发思路。压缩包共 725 个文件约 7.94MB其中包含 111 个 JSP 动态页面、415 个 GIF 动图、69 张 JPG 图片以及 CSS、JavaScript、jar 包和数据库文件等基本涵盖了从页面样式、前端脚本到后端代码与数据库脚本的主要组成部分。文件目录结构清晰便于按模块查找对应功能代码目前已有 1510 人学习下载。通过这份资料读者可以掌握 SSM 三大框架在实际业务中的整合方式了解旅游网站常见的业务流程设计、用户管理、景点展示、预订下单等功能的技术实现路径同时也能从数据库表设计、动态 SQL 查询等细节中积累实战经验是 Java Web 项目实践与备赛参考的不错选择。1. 旅游系统网站用 Java 自己搭值不值两三年前有个旅行社朋友找我说想把线路、酒店、景点报价做成内部网站预算不多时间也紧。我说用 Java 现成的 Spring Boot 骨架就能干不需要搬一套企业级微服务。现在复盘旅游系统网站的核心不是高并发而是把“展示线路、注册登录、下单支付”这条业务链跑顺。用 Java 搭这类系统对准备面试作品、做课程设计和给小微企业做内部站都很有价值。下面这篇文章就是我当时落地的路径技术选型、模块拆解、关键代码以及后面一定会踩的五个坑。2. 技术选型先立住为什么是 Spring Boot MyBatis Plus而不是别的2.1 单体架构就够用旅游系统网站不需要一上来就微服务旅游系统网站这个词听起来像一个大平台但你仔细拆一下业务线路、酒店、景点、用户、订单、支付。本质上就是几个模块互相调用数据关系也不复杂。以一个小旅行社每天几十到几百单的量级单体应用完全扛得住。Spring Boot 内嵌 Tomcat一个 jar 包就能跑开发调试都比微服务省心。微服务是有代价的注册中心、网关、配置中心、分布式事务这些组件每一个都是运维负担。我见过有人为了简历好看硬把旅游系统拆成四个微服务结果本地起服务要五分钟联调一个下单接口要在三个服务间查日志最后演示的时候服务调用链断了冷场。对绝大多数场景来说单体够了。如果后面流量真到了需要拆的规模你可以按“用户”“订单”“线路”这种业务边界再拆而不是现在就开始设计。面试视角也要说一句与其在项目里堆 Nacos、Gateway不如把一个单体项目里“请求怎么进来、数据怎么落库、事务怎么控制”讲清楚。Java 面试题里那些八股文往往要在具体工程现象里才能答得自然。比如 Spring Boot 自动配置到底做了什么你在旅游系统项目里改过数据源和拦截器就比背概念强得多。2.2 用 Spring Initializr 生成最小工程依赖选型与 Maven 坐标打开 start.spring.io 这一步没太多好说的关键是版本搭配。我建议用 Maven Java 8 Spring Boot 2.7.18这套组合最稳网上排查资料也多。如果你本机是 JDK 17那就直接用 Spring Boot 3.2.x注意 MyBatis Plus 要用专门的 boot3 starter。别用 JDK 17 跑 Spring Boot 2.7 的旧 jar九成会遇到底层库不兼容。在 Spring Initializr 页面上选 Spring Web、Validation、MySQL Driver、Lombok 四个依赖然后生成项目。MyBatis Plus 不在官方依赖列表里需要手动把 starter 坐标加进 pom.xmldependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies说明mybatis-plus-boot-starter的 3.5.3.2 适配 Spring Boot 2.x如果你用的是 Spring Boot 3.x请换成mybatis-plus-spring-boot3-starter版本选 3.5.5 以上。mysql-connector-j是 MySQL 8 下的驱动坐标老驱动mysql-connector-java已经改名但兼容性没问题主要看你选的 Spring Boot 版本能否自动管理版本。这个 pom 里没写version的依赖都由 Spring Boot 父工程统一管理。然后写application.yml。这是项目最早要碰的配置文件几乎是后面所有乱码和时区问题的源头server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: global-config: db-config: id-type: auto参数说明characterEncodingutf8解决中文写入乱码serverTimezoneAsia/Shanghai解决 JDBC 与 MySQL 的时区偏移否则查询LocalDateTime可能相差 8 小时。id-type: auto让 MyBatis Plus 使用数据库自增主键对应实体类里的TableId(type IdType.AUTO)。password 不要写上真实密码本地开发可以用环境变量占位后面部署再说。2.3 为什么选 MyBatis Plus单表 CRUD、分页和自定义 SQL 的边界很多 Java 工程师起步时纠结用 JPA 还是 MyBatis。旅游系统网站的表关系虽然多但大多是简单单表操作MyBatis Plus 能减少大量重复的 Mapper XML。你定义一个接口继承BaseMapperT单表插入、删除、更新、批量查询就都有了。这是 JPA 给不了的轻量敏捷。但是这也是陷阱一旦你习惯了 BaseMapper遇到多表关联、复杂统计就会走神想着用 Java 代码拼装其实效率远不如一条 SQL。所以我的原则是单表查询、分页、按条件更新用 MyBatis Plus 的 Wrapper多表 JOIN、报表统计、复杂 SQL 必须手写 Mapper XML。比如“查询线路和它的酒店列表”就不要用 LambdaQueryWrapper 连续 left join 模拟而是直接写 SQL。为了分页生效必须要配置分页拦截器否则你会遇到一个隐蔽大坑后面避坑章节单说。先看这个基础配置类Configuration MapperScan(com.example.travel.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }说明MapperScan指定 Mapper 接口所在的包包扫描不到启动时就报Invalid bound statement。分页插件是PaginationInnerInterceptor它会在执行selectPage时自动生成 count 和 LIMIT 语句。如果你的项目要用多数据源这个插件还要指定与当前数据源对应的DbType否则分页方言会判断错。配置完成后Controller 里的分页查询才能正常限制返回条数。到这里工程骨架已经立住了。下一章我从业务模块和数据表入手把旅游系统的五脏六腑拆开并给你一个从 Java 实体类生成建表 SQL 的小工具思路方便你快速搭原型。3. 把旅游系统拆成五个核心模块线路、酒店、景点、用户、订单3.1 数据库设计从 Java 实体类生成建表 SQL模型即文档旅游系统的表不算多一般我会建这些表tour_route线路表hotel_info酒店表scenic_spot景点表sys_user用户表tour_order订单表。线路表可以带一个热门标签订单表冗余线路名称和价格避免下单后线路改了导致订单历史被污染。这些设计理念在刚开始做的时候就可以带着不然面试官一问“这张表为什么这么冗余”你就答不上来。MyBatis Plus 的核心玩法是把 Java 实体类和数据库表对应起来。对应关系通过注解标记TableName(tour_route)指表名TableId指主键TableField指列名。下面是一个简化的线路实体类Data TableName(tour_route) public class TourRoute { TableId(type IdType.AUTO) private Long id; TableField(route_name) private String routeName; private BigDecimal price; private Integer days; private Integer stock; TableField(create_time) private LocalDateTime createTime; }这里有个经常问到的点MyBatis Plus 能不能根据 Java 实体类自动生成 CREATE TABLE官方没有直接提供这个功能但社区里常见做法是写一个小的 DDL 生成工具读取注解和属性类型映射成 MySQL 列类型。我自己做课程设计时就会这么干把模型类当成“唯一真相”改类后重新生成 SQL比手动维护建表脚本省事。核心逻辑如下public class DdlGenerator { public static String createTable(Class? clazz) { TableName table clazz.getAnnotation(TableName.class); if (table null) { throw new IllegalArgumentException(实体类缺少 TableName 注解); } StringBuilder sb new StringBuilder(); sb.append(CREATE TABLE ).append(table.value()).append( (\n); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(TableId.class)) { sb.append( id BIGINT NOT NULL AUTO_INCREMENT,\n); } else if (field.isAnnotationPresent(TableField.class)) { String column field.getAnnotation(TableField.class).value(); String type mapType(field.getType()); sb.append( ).append(column).append( ).append(type).append(,\n); } } sb.append( PRIMARY KEY (id)\n) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sb.toString(); } private static String mapType(Class? type) { if (type Long.class) return BIGINT; if (type String.class) return VARCHAR(255); if (type BigDecimal.class) return DECIMAL(10,2); if (type Integer.class) return INT; if (type LocalDateTime.class) return DATETIME; return VARCHAR(255); } }说明这段代码特意做了简化没有处理主键自增的注解值、没有索引和唯一键。真正的项目里我会手动补充KEY idx_route_name之类或者用 Flyway 管理 SQL 文件。工具的价值在于快速生成表结构不是替你完成数据库设计。字段类型映射规则也要按业务调整String不是永远VARCHAR(255)比如备注字段可以直接用TEXT。这类细节如果漏了生产环境线上扩列会很痛苦。如果你不想写生成器还有一个反向做法先用 MySQL Workbench / Navicat 画好表再执行 SQL 建表最后用 MyBatis Plus Generator 把表反向生成实体类。这也是大多数团队的路子。两条路都能走关键是你得清楚实体类注解和建表语句之间的一一对应关系否则 MyBatis Plus 启动时查selectById就会发现列名对不上。3.2 后端分层Controller-Service-Mapper 直连别把业务逻辑写在 Controller 里工程结构我从一开始就分成 controller / service / mapper 三层。旅游系统网站的业务不复杂不需要引入 domain 层、application 层那套 DDD 架构三层足够。Controller 只做接口参数接收和结果返回Service 负责业务规则、事务Mapper 负责 SQL。很多新手把判断逻辑写在 Controller 里等于把业务规则和 HTTP 层耦死后面改接口地址就动了业务很危险。以一个“线路分页查询”接口为例Controller 这样写RestController RequestMapping(/api/route) public class TourRouteController { Autowired private TourRouteService tourRouteService; GetMapping(/page) public ResultIPageTourRoute page( RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size, RequestParam(required false) String keyword) { LambdaQueryWrapperTourRoute wrapper Wrappers.lambdaQuery(); if (StringUtils.hasText(keyword)) { wrapper.like(TourRoute::getRouteName, keyword); } return Result.ok(tourRouteService.page(new Page(current, size), wrapper)); } }Service 接口和实现是成对出现的。MyBatis Plus 提供了IServiceT和ServiceImplM, T可以让你的 Service 直接继承现成的分页、批量保存能力public interface TourRouteService extends IServiceTourRoute { } Service public class TourRouteServiceImpl extends ServiceImplTourRouteMapper, TourRoute implements TourRouteService { }Mapper 接口只写一行继承BaseMapperTourRoute就能获得单表 CRUDMapper public interface TourRouteMapper extends BaseMapperTourRoute { }说明返回对象ResultT是自己封装的惯例包含code、message、data三个字段。这样前端能统一判断业务状态而不是依赖 HTTP 状态码。LambdaQueryWrapper比字符串QueryWrapper安全字段名写错会在编译期报错。IPage对象里除了records还有current、size、total等分页信息前端表格组件直接就能用。3.3 热门景点与线路排序用 Stream 还是 SQL后台管理页面经常要“热门线路 Top N”或“按价格排序”的逻辑。Java 8 的 Stream 排序用起来很顺手配合 Lambda 代码很短// 小数据量从缓存或一次性查询结果里做内存排序 ListTourRoute top3 routeList.stream() .filter(r - r.getStock() ! null r.getStock() 0) .sorted(Comparator.comparing(TourRoute::getPrice).reversed()) .limit(3) .collect(Collectors.toList());这段代码的意思是先把列表过滤掉无库存的线路再按价格倒序取前 3 条。Comparator.comparing(...).reversed()是排序的常用组合对数值类型倒序没问题但reversed()不会处理null所以过滤条件里要判空。不过要提醒一句Stream 排序的前提是数据已经全部加载到 Java 堆内存里。如果你的线路表有几万条每页还要内存排序那是白白浪费堆内存和 GC 时间。数据库里写Select(SELECT * FROM tour_route WHERE stock 0 ORDER BY price DESC LIMIT #{limit}) ListTourRoute selectTopPrice(Param(limit) int limit);这样查询层面就只返回需要的 3 条。ORDER BY price如果数据量大要给price建索引LIMIT不要写在 Java 代码里拼接防止 SQL 注入。记住一条分界线小批量内存排序方便批量数据库排序可靠。面试时如果被问“Java 排序有哪些”你可以从Comparable、Comparator、再到Stream排序展开但工程上先分清数据在哪儿。4. 从零跑通最小闭环注册登录、线路列表、下单支付模拟4.1 本地环境搭建JDK、Maven、MySQL 的版本搭配先把地基打好。Java 环境配置这件事我见过太多人在第一步就翻车装了 JDK 17但项目是 Spring Boot 2.7 编译的 JDK 8 字节码或者本地有多个 JDKJAVA_HOME指到旧版本IDEA 里却选了新版本编译和运行不一致。旅游系统网站这个项目建议按这个表格选型组件版本建议说明JDK8 或 17项目用 Spring Boot 2.7 选 JDK 8用 Spring Boot 3.x 选 JDK 17Maven3.6.3依赖管理和打包MySQL5.7推荐 8.0注意字符集 utf8mb4Spring Boot2.7.18稳定文档多新项目可上 3.2.xMyBatis Plus3.5.3.2匹配 Spring Boot 2.x在命令行先执行java -version、mvn -v、mysql --version三个命令查出来的版本要和表格对齐。如果java -version显示 1.8而 IDEA 的 Project Structure 里选了 17打包后运行时可能出UnsupportedClassVersionError。解决办法是统一JAVA_HOME和项目 SDK。Maven 会在构建时使用JAVA_HOME的 JDK可以在 pom 里强制指定properties java.version1.8/java.version /properties但注意java.version只是编译的源码级别真正运行 jar 的 JDK 版本必须向上兼容。Maven 的mvn -v会打印所用 Java runtime核对一下是不是你要的那一个。数据库这边建库时务必指定utf8mb4这是后面中文乱码的根因之一CREATE DATABASE travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;4.2 登录鉴权JWT 拦截器别把用户状态塞 Session旅游系统网站虽然小但登录态不能省。传统做法是把用户信息塞 Session由容器维护会话。问题是只要后面部署到多实例Session 就得放到 Redis 里否则用户会被随机踢下线。JWT 把用户标识放在客户端 token 里服务端不存会话天然适合前后端分离。对小项目来说最直接的好处是后端代码里不用写 HttpSession。我用的 JWT 库是 jjwt版本 0.11.5先加两个依赖dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency然后写一个组件封装生成和解析逻辑。注意密钥不要硬编码在代码里我用Value从配置文件读取Component public class JwtUtil { Value(${travel.jwt.secret}) private String secret; public String createToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setExpiration(new Date(System.currentTimeMillis() 86400000)) .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .compact(); } public Claims parse(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(token) .getBody(); } }说明setExpiration设置了 24 小时过期时间演示够用。signWith要求密钥长度至少 32 字节所以 application.yml 里配置的travel.jwt.secret别只写一个单词。生产环境建议用 AES 或 RSA 非对称签名这里用 HS256 是为了少绕弯。parse方法在 token 非法或过期时会抛出异常拦截器里捕获后返回 401。拦截器实现Component public class LoginInterceptor implements HandlerInterceptor { Autowired private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String auth request.getHeader(Authorization); if (auth ! null auth.startsWith(Bearer )) { try { Claims claims jwtUtil.parse(auth.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write(未登录或登录过期); return false; } } response.setStatus(401); response.getWriter().write(缺少登录凭证); return false; } }再通过 WebMvcConfig 注册拦截器并放行登录注册接口和首页线路查询Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register, /api/route/page); } }说明拦截器只挡/api/**但放行部分公开接口。这里“公开”和“登录”是两回事不要把下单和支付接口放出去。4.3 下单流程事务与乐观锁防止超卖下单是旅游系统网站最核心的链路。数据库层要有订单表tour_order状态字段status我用整数表示0 待支付、1 已支付、2 已取消、3 已完成。线路表要有库存stock字段。先写一个下单方法Transactional public Order createOrder(Long userId, Long routeId) { int updated tourRouteMapper.deductStock(routeId); if (updated 0) { throw new BizException(手慢了线路余位不足); } TourRoute route tourRouteMapper.selectById(routeId); Order order new Order(); order.setUserId(userId); order.setRouteId(routeId); order.setRouteName(route.getRouteName()); order.setPrice(route.getPrice()); order.setStatus(0); orderMapper.insert(order); return order; }deductStock需要手写在 Mapper 里用乐观锁风格的条件更新避免两条线程同时读到库存为 1 都下单成功Update(UPDATE tour_route SET stock stock - 1 WHERE id #{routeId} AND stock 0) int deductStock(Param(routeId) Long routeId);说明Transactional保证了“扣库存”和“插入订单”要么都成功、要么都回滚。deductStock的返回值是影响行数如果库存为 0SQL 不会更新任何行返回 0这时直接抛业务异常。这个写法比“先 select 判断再 update”更稳妥因为并发下 select 的结果可能已经过期。这里抛出的BizException要在全局异常处理器里转成Result返回而不是让 Spring 返回一个默认错误页。模拟支付接口就简单了只负责把待支付订单改成已支付Transactional public void payOrder(Long userId, Long orderId) { Order order orderMapper.selectById(orderId); if (order null || order.getStatus() ! 0) { throw new BizException(订单不可支付); } order.setStatus(1); orderMapper.updateById(order); }说明真实对接支付时这里不要在前端点击后立刻改状态而是先调支付平台预下单再在异步回调里更新订单状态。模拟版本只为跑通闭环面试时可以强调“你清楚真实支付的差异”。到这里用户注册、登录、线路分页、下单、支付这个最小闭环已经够了接下来进入我整理的坑。5. Java 旅游系统避坑指南5 个让我翻过车的常见问题5.1 中文乱码与 Java 编码问题现象页面访问线路名称显示成一堆问号??????或者 MySQL 命令行里看数据是正常的接口返回却是游这种乱码。原因乱码是链条问题常见两处。第一处是 JDBC URL 没有带characterEncodingutf8导致驱动用系统默认编码往服务器传字符串第二处是 MySQL 表或字段的字符集不是 utf8mb4建库时如果没指定默认 latin1中文写入就被截断或转码失败。另外如果你是直接把String字符串硬编码在 Java 代码里而file.encoding不是 UTF-8编译出的 class 文件就已经是错的。解决三处一起堵。JDBC URL 用useUnicodetruecharacterEncodingutf8建表语句带DEFAULT CHARSETutf8mb4Spring Boot 里强制请求响应编码为 UTF-8server: servlet: encoding: charset: UTF-8 enabled: true force: true配置后重启再用curl -v http://localhost:8080/api/route/page看响应头里是不是Content-Type: application/json;charsetUTF-8。如果 IDE 里代码没问题但命令行java -jar跑出来乱码就在启动命令加-Dfile.encodingUTF-8不过根治还是统一环境变量。5.2 MyBatis Plus 自动填充字段失效create_time 没值现象使用 MyBatis Plus 的insert插入订单后数据库的create_time是null明明实体类里已经写了LocalDateTime createTime。原因自动填充不是 MyBatis Plus 默认行为。需要在实体类字段上标TableField(fill FieldFill.INSERT)并且定义MetaObjectHandler实现类。只标注解不注册处理器是常见坑还有一种可能是你手动在实体类里给createTime赋过值strictInsertFill发现字段非空会跳过严格填充不会覆盖你设置的值。所以不要在业务代码里设置这两个字段让 handler 统一填。解决实体类字段TableField(value create_time, fill FieldFill.INSERT) private LocalDateTime createTime; TableField(value update_time, fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;处理器Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }说明strictInsertFill的第二个参数是实体类属性名不是数据库列名。如果列名是create_time属性名是createTime这里必须传createTime。启动时如果发现 Handler 没有被扫描到确认启动类或配置类的ComponentScan包路径覆盖了它。5.3 日期时间类型导致 Jackson 序列化报错现象接口返回订单 JSON 时LocalDateTime字段变成[2025, 4, 10, 15, 30, 0]这种数组或者直接报InvalidDefinitionException: Java 8 date/time type ... not supported。原因Jackson 默认处理java.util.Date很顺但对java.time.LocalDateTime需要额外支持模块。Spring Boot 2.x 实际已经引入了 jackson-datatype-jsr310但如果没配置write-dates-as-timestampsfalse序列化会走数值数组Spring Boot 3.x 下依赖可能被移出需要手动引入。还有一个隐蔽原因是 MySQL 驱动返回的LocalDateTime受到连接时区影响和服务器当前时区不一致表现成时间差 8 小时。解决在application.yml设置统一全局格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai serialization: write-dates-as-timestamps: false如果需要特定的字段只返回日期可以加JsonFormat(pattern yyyy-MM-dd, timezone GMT8)。注意全局date-format对LocalDateTime不一定全部生效这时优先在字段上指定JsonFormat。数据库连接的serverTimezoneAsia/Shanghai也要保持一致否则先相差 8 小时。5.4 分页查询内存溢出count 查询走了全表现象线路表数据量到了几万条调用分页接口越来越慢几百万条时直接报OutOfMemoryErrorGC 日志里看到堆被一个 List 撑爆。原因最常见的不是 MyBatis Plus 自动生成的 count 太慢而是你根本没有注册分页插件。没有拦截器时selectPage不会拼接LIMITMyBatis Plus 会把符合条件的所有数据查出来然后在内存里分页。表现就是接口正常返回但堆内存被无谓占满。另一个常见原因是分页查询中带了大字段比如线路详情detailTEXT即便LIMIT生效每次查询也把大对象加载进来积少成多。解决确保MybatisPlusConfig里配置了PaginationInnerInterceptor并且MapperScan覆盖你的 Mapper 包。然后在分页查询里只 select 需要的列LambdaQueryWrapperTourRoute qw new LambdaQueryWrapper(); qw.select(TourRoute::getId, TourRoute::getRouteName, TourRoute::getPrice); IPageTourRoute page tourRouteMapper.selectPage(new Page(1, 10), qw);这样生成的 SQL 只查三列数据行数不变但传输和大字段解析开销大幅下降。如果自动生成的 count SQL 也很慢你可以打开日志看 SQL再把 count 口径单独拆到另外的查询方法里避免影响分页主查询。深翻页场景比如直接跳转第 10000 页LIMIT 10000, 10在 MySQL 里会扫描前面 10000 行解决方案是改成基于上一页最大 id 的游标分页。旅游系统网站演示阶段用不到但面试能说出来就是加分点。5.5 启动失败端口占用与 JDK 版本不匹配现象mvn spring-boot:run或java -jar travel.jar启动时报Port 8080 was already in use或者UnsupportedClassVersionError: xxx has been compiled by a more recent version of the Java Runtime。原因前一个是本机 8080 被其他进程占了比如另一套项目的 Tomcat后一个是编译用的 JDK 比运行 jar 用的 JDK 版本高或者 Spring Boot 3 的 jar 用 JDK 8 的 JRE 去跑。Java 版本不一致是配置多个 JDK 时最容易翻车的点。解决先确认端口。Windows 用netstat -ano | findstr 8080Linux/macOS 用lsof -i:8080找到 PID 后 kill 掉Windows 是taskkill /PID pid /F。如果不想关其它进程就换端口java -jar travel.jar --server.port8081。版本不匹配就统一 JDK。用mvn -v看 Maven 用的 Java runtime用java -version看当前 JRE。Spring Boot 2.7 项目必须让两个命令都显示 1.8Spring Boot 3.2 项目要求显示 17。还可以在 pom 里加上maven-compiler-plugin的source、target和release防止编译级别飘移。老服务器如果目前是 JRE 6/7别折腾“能不能跑”直接装 JDK 再跑省下的时间是自己的。6. 进阶把旅游系统网站打磨到能演示、能面试、能商用6.1 给热门线路加缓存性能不再裸奔演示阶段可以给查询接口加一个简单的 Redis 缓存。Spring Boot 里引入spring-boot-starter-data-redis启动类上标EnableCaching然后在查询方法上加Cacheable(cacheNames route:page, key #current _ #size) public IPageTourRoute page(long current, long size) { return tourRouteMapper.selectPage(new Page(current, size), null); }说明key要带上分页参数不然不同页码会串数据。缓存命不中才查库命中后直接返回。这样面试时你能讲出“缓存穿透、缓存击穿、缓存雪崩”里最基础的命中与过期。6.2 用测试和启动日志验证你的系统没崩真正可交付的系统不是“跑起来就行”。我习惯在写完一个接口后立刻用spring-boot-starter-test里的MockMvc做冒烟测试调用分页接口断言状态码和返回结构。部署前用mvn clean package -DskipTests打出可执行 jar本地先java -jar target/xxx.jar完整启动一遍再发布。不要落到服务器上才开始编译那样遇到问题定位慢。配合spring.profiles.activeprod切换数据库配置密码和密钥从环境变量读取这样能直接推到生产环境。6.3 我的交付顺序先闭环再优化我自己做这类 Java 旅游系统网站有一个固定顺序第一版只跑通注册、线路列表、下单、支付模拟第二版才补拦截器、缓存、全局异常处理最后才考虑部署脚本。很多人一上来就想把开源商城源码改一改结果是代码能跑但问一句“下单超卖怎么防止”就答不上来面审现场反而翻车。把最小闭环的每一条链路讲清楚比堆十个功能更硬。我踩过的坑都写在前面了希望你不用再走一遍。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。