
简介一套基于SpringBootThymeleafMySQL构建的旅游景点酒店预订网站毕业设计源码整体类似小型电商系统核心功能涵盖用户注册登录、短信验证、找回密码、景点列表与相册墙、景点购买下单、评论管理、酒店管理以及完整的后台管理模块。项目采用SpringDataJpa进行数据库操作集成阿里云短信服务、邮箱验证、OSS图片存储、RabbitMQ消息队列和Redis缓存前端以BootStrap与Layui搭建整体遵循RESTful接口风格使用JSON传递数据。压缩包共487个文件核心资源包括51个Java源文件、1个MySQL数据库脚本、34个HTML页面、37个CSS样式、67个JS交互脚本以及大量JPG/PNG/GIF图片素材同时包含少量配置文件和字体文件可完整支撑前台展示和后台管理界面整体大小约15.13MB目录结构清晰、便于导入IDE运行。已有175人学习适合需要完成毕业设计或课程设计的JavaWeb学生参考。资源附带完整数据库脚本与前后端页面可直接导入Maven工程启动也便于在此基础上扩展订单、支付、景点管理等业务功能。1. 旅游景点酒店预订网站毕业设计最稳妥的单体项目样板每年毕业季选题里总有一批“基于SpringBootXXX的XX系统”听起来像流水线产物但如果你真的打算用三个月把毕设做扎实旅游景点酒店预订网站确实是性价比最高的一类——业务域足够宽能覆盖用户、景点、酒店、订单、评论五张核心表技术栈又是招聘市场上出现频率最高的SpringbootThymeleafMySql组合前端不碰复杂框架所有页面由服务端渲染直接吐出调式起来比前后端分离少一环跨域问题。这套方案的目标读者很明确需要一份能跑通、能讲清、能答辩的Java Web毕设项目同时想顺便把Spring Boot的工程化习惯练成本能的人。它解决的核心问题不是“做一个多漂亮的网站”而是“用最少的额外依赖把CRUD写到符合毕设评阅标准的完整度”。数据库脚本和Java源代码齐全意味着你能把精力集中在理解业务闭环和排查边界问题上而不是从零搭脚手架。2. Springboot与Thymeleaf的分工为什么服务端渲染依然适合毕设项目2.1 技术选型逻辑Springboot当总线Thymeleaf当视图MySql当仓库先厘清这三者各自在项目里的角色答辩时老师第一个问题大概率就是这个。Springboot负责整个应用的生命周期管理内嵌Tomcat、自动配置数据源、扫描Controller和Service、统一异常处理。你的Java代码在Springboot的容器里被组织成Controller→Service→Mapper三层请求从浏览器进来由Controller接收参数并调用Service处理业务Service再通过MyBatis或JdbcTemplate访问MySql。Thymeleaf的角色是视图渲染器它运行在服务端。和Vue、React这类前端框架最大的不同是Thymeleaf的页面HTML文件里直接嵌入服务端变量浏览器拿到的已经是渲染完的静态HTML不需要再发Ajax请求拿JSON去拼DOM。这种模式下页面刷新频繁、交互复杂度低的系统写起来反而快——景点列表、酒店详情、订单确认页都是典型的“请求→取数→渲染”链路没有复杂的前端状态管理需求Thymeleaf的th:each和th:if足够对付。MySql则是整个系统的仓储层负责持久化用户、景点、酒店、订单、评论数据。毕设项目里一般不需要做主从复制、分库分表这类高级特性把InnoDB引擎下的表结构设计合理、索引建对、事务隔离级别设置好就比大多数同学强了。2.2 搭建工程骨架从零创建一个可运行的Springboot项目创建项目的方式有两种一种是用IDEA的Spring Initializr向导直接生成另一种是去Spring官网的 start.spring.io 下载基础压缩包。我建议用IDEA因为毕设答辩现场大概率会打开IDE演示你需要在集成环境里熟悉每个文件的位置。一个标准的SpringbootThymeleaf项目目录结构是src/main/java ├── com.example.travel │ ├── TravelApplication.java // 启动类 │ ├── controller // 控制器接收页面请求 │ ├── service // 业务逻辑层 │ ├── mapper // 数据访问层MyBatis的Mapper接口 │ ├── entity // 实体类对应数据库表 │ └── config // 配置类拦截器/WebMvc配置 src/main/resources ├── templates // Thymeleaf模板文件 │ ├── index.html │ ├── hotel.html │ ├── scenic.html │ └── order.html ├── static // 静态资源js/css/images ├── application.yml // 核心配置文件 └── mapper // MyBatis的XML映射文件 src/main/scripts └── travel.sql // 数据库初始化脚本核心配置文件application.yml是启动时最先要核对的地方关键参数如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: prefix: classpath:/templates/ suffix: .html cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.travel.entity这里有两个参数要特别说明。第一个是数据库连接串里的serverTimezoneAsia/ShanghaiMySql 8.0及以上版本默认时区是UTC如果你不显式指定JDBC连接时会报时区错误或出现整8个小时的时间偏移这在订单时间显示上是个典型翻车点。第二个是spring.thymeleaf.cachefalse开发阶段必须关掉模板缓存否则你改了HTML页面浏览器刷新还是旧内容白白浪费调试时间。mapper-locations指向XML映射文件目录这个路径配错会导致MyBatis启动时找不到SQL映射常见的报错是Invalid bound statement (not found)。接着是启动类它是整个应用的入口必须放在包结构的最外层package com.example.travel; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.example.travel.mapper) public class TravelApplication { public static void main(String[] args) { SpringApplication.run(TravelApplication.class, args); } }SpringBootApplication包含组件扫描、自动配置和启动配置三个功能它默认扫描当前包及子包下的所有Spring组件。MapperScan指定MyBatis的Mapper接口所在包不加这个注解的话每个Mapper接口都需要单独加Mapper注解毕设代码量不大但统一用MapperScan扫描包更整洁答辩讲解时也更有条理。2.3 运行流程从浏览器到数据库的完整路径一次用户点击“查看酒店详情”的请求在Springboot里走的是固定链路。Thymeleaf模板里的链接指向Controller的RequestMappingSpring MVC通过HandlerMapping找到对应的处理方法方法参数里有RequestParam或PathVariable注入的请求参数。Controller调用Service接口Service实现类里处理查询逻辑和事务边界再通过Mapper接口执行SQL。数据从MySql返回后被封装成Entity实体放进Model对象中。Controller返回视图名称时Thymeleaf模板引擎根据prefix 视图名 suffix解析到具体的HTML文件渲染时将Model中的变量填充到th:text等标签里最终输出完整的HTML响应给浏览器。这个链路里新手最容易卡住的点有两个一是Controller返回字符串时到底返回的是视图名还是JSON数据如果你在方法上漏了ResponseBody前端拿到的是HTML而不是预期的数据后台还不会报错二是Thymeleaf变量名必须和Model里放的属性名完全一致否则页面渲染时变量表达式报错一片空白。理解了这条链路后续所有接口的调试都会有方向感不再靠盲猜。3. MySql数据库设计与初始化脚本六张表撑起整个预订系统3.1 表结构与关系设计从需求反推数据库模型旅游景点酒店预订网站的数据模型按照业务边界可以拆成用户端、内容端、交易端三个板块。用户端只需要一张用户表存手机号、密码、昵称内容端是景点表和酒店表景点关注名称、简介、图片、价格酒店还需要额外关注等级、地址、联系方式交易端是订单表和收藏表这是把用户和内容连接起来的核心。最后加一张评论表让用户在预订之后可以对景点或酒店进行评价这是毕设评分中一个明显的加分项。六张表的字段设计遵循一个原则能合并的字段不要拆表能自解释的字段不要加冗余。下面是完整的数据库初始化脚本可以直接在Navicat或命令行里执行-- 创建数据库 CREATE DATABASE IF NOT EXISTS travel_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE travel_db; -- 用户表 CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码MD5加密, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, email varchar(100) DEFAULT NULL COMMENT 邮箱, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY idx_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 景点表 CREATE TABLE t_scenic ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 景点名称, description text COMMENT 景点介绍, price decimal(10,2) DEFAULT 0.00 COMMENT 门票价格, address varchar(200) DEFAULT NULL COMMENT 地址, open_time varchar(50) DEFAULT NULL COMMENT 开放时间, image varchar(255) DEFAULT NULL COMMENT 封面图片, status tinyint(4) DEFAULT 1 COMMENT 状态1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT景点表; -- 酒店表 CREATE TABLE t_hotel ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 酒店名称, star tinyint(4) DEFAULT 3 COMMENT 星级3/4/5, price decimal(10,2) DEFAULT 0.00 COMMENT 参考价格, address varchar(200) DEFAULT NULL COMMENT 地址, phone varchar(20) DEFAULT NULL COMMENT 联系电话, description text COMMENT 酒店介绍, image varchar(255) DEFAULT NULL COMMENT 酒店图片, status tinyint(4) DEFAULT 1 COMMENT 状态1营业 0停业, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT酒店表; -- 订单表 CREATE TABLE t_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(50) NOT NULL COMMENT 订单编号, user_id int(11) NOT NULL COMMENT 用户ID, scenic_id int(11) DEFAULT NULL COMMENT 景点ID, hotel_id int(11) DEFAULT NULL COMMENT 酒店ID, type tinyint(4) NOT NULL COMMENT 订单类型1景点门票 2酒店预订, num int(11) DEFAULT 1 COMMENT 数量/间夜, amount decimal(10,2) NOT NULL COMMENT 订单金额, status tinyint(4) DEFAULT 0 COMMENT 状态0待支付 1已支付 2已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY idx_order_no (order_no), KEY idx_user_id (user_id), KEY idx_scenic_id (scenic_id), KEY idx_hotel_id (hotel_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user (id), CONSTRAINT fk_order_scenic FOREIGN KEY (scenic_id) REFERENCES t_scenic (id), CONSTRAINT fk_order_hotel FOREIGN KEY (hotel_id) REFERENCES t_hotel (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT订单表; -- 收藏表 CREATE TABLE t_favorite ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, scenic_id int(11) DEFAULT NULL, hotel_id int(11) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), CONSTRAINT fk_fav_user FOREIGN KEY (user_id) REFERENCES t_user (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT收藏表; -- 评论表 CREATE TABLE t_comment ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, content varchar(500) NOT NULL COMMENT 评论内容, rating int(11) DEFAULT 5 COMMENT 评分1-5, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), CONSTRAINT fk_comment_user FOREIGN KEY (user_id) REFERENCES t_user (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT评论表;3.2 脚本设计说明字符集、外键和索引的取舍整个脚本里有几个容易被忽略但答辩时容易问到的设计决策。首先是utf8mb4字符集这是必选项——MySQL的utf8字符集最多只支持3个字节存不了emoji表情和生僻字用户的昵称出现表情符号时用utf8会导致插入报错。其次是外键约束毕设数据规模小外键能让数据一致性在数据库层面兜底不会出现订单关联的用户被直接删掉这种脏数据。但注意外键会带来额外的插入和删除性能开销生产环境一般会去掉外键交给应用层维护这个区别可以主动在答辩时提出来展示你的工程认知。索引设计上idx_user_id、idx_scenic_id、idx_hotel_id是给订单表和收藏表的频繁查询用的这三张表的查询总是带用户ID或内容ID作为条件。idx_order_no是唯一索引订单编号作为业务凭证不能重复。真正的索引优化思路是先看WHERE条件和ORDER BY字段再决定要不要加索引而不是每张表全字段建索引那样只会拖慢插入速度。3.3 演示数据的插入技巧让答辩页面有内容可看数据库脚本里只建表是不够的必须插入一批演示数据否则系统跑起来列表页全是空的答辩效果大打折扣。插入演示数据时注意两点景点和酒店各准备六到八条价格要有梯度覆盖几十元到几百元不等的区间订单数据只插少量且所有订单关联的用户ID必须是真实存在的不然页面查询用户订单时直接报空指针。INSERT INTO t_user (username, password, nickname, phone) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 管理员, 13800001111), (test, e10adc3949ba59abbe56e057f20f883e, 测试用户, 13800002222); INSERT INTO t_scenic (name, description, price, address, open_time, image, status) VALUES (西湖, 杭州标志性湖泊景区世界文化遗产, 0.00, 杭州市西湖区龙井路1号, 全天开放, /images/xihu.jpg, 1), (故宫博物院, 明清两代皇家宫殿, 60.00, 北京市东城区景山前街4号, 08:30-17:00, /images/gugong.jpg, 1), (九寨沟, 以翠海、叠瀑、彩林、雪峰闻名, 169.00, 四川省阿坝州九寨沟县, 08:00-18:00, /images/jiuzhaigou.jpg, 1); INSERT INTO t_hotel (name, star, price, address, phone, description, status) VALUES (杭州西子湖四季酒店, 5, 1280.00, 杭州市西湖区灵隐路5号, 0571-88888888, 毗邻西湖园林式设计, 1), (北京王府井文华东方酒店, 5, 1680.00, 北京市东城区王府井大街269号, 010-66666666, 俯瞰紫禁城, 1), (全季酒店西湖店, 3, 380.00, 杭州市上城区解放路142号, 0571-77777777, 经济型连锁交通便利, 1);密码字段存的是e10adc3949ba59abbe56e057f20f883e这是字符串123456的MD5值。毕设项目里密码加密一般直接用MD5虽然真正生产环境需要用BCrypt加盐但在本地演示和答辩场景中MD5简单直观且能在数据库里直接看到加密效果。注意MD5加密要在Java代码里做不要在SQL里直接用MD5()函数那样逻辑分散后不好讲解。4. 核心功能模块实现从页面到数据库的四个关键代码块4.1 登录与拦截器Thymeleaf页面渲染中使用Session的注意点用户模块是所有预订功能的前置条件不登录就不能下单。登录功能拆两块认证逻辑和会话管理。认证逻辑写在Service层查询用户表匹配用户名和MD5后的密码会话管理用HttpSession登录成功后把用户对象放进Session后续页面通过判断Session里有没有用户数据来决定显示“登录”还是“用户名/退出”。拦截器是控制访问权限更优雅的做法。定义一个HandlerInterceptor实现类在preHandle方法里检查Session然后把这个拦截器注册到WebMvcConfigurer配置类中package com.example.travel.config; import com.example.travel.interceptor.LoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/order/**, /user/**, /favorite/**) .excludePathPatterns(/, /login, /register, /scenic/**, /hotel/**, /css/**, /js/**, /images/**); } }拦截器注册配置里addPathPatterns指定需要拦截的路径前缀包括订单、用户中心、收藏三个模块excludePathPatterns放行不需要鉴权的路径如登录页、注册页、景点和酒店列表页以及静态资源目录。静态资源放行是关键否则页面里的CSS和图片全被拦截页面样式完全丢失。拦截路径的匹配规则支持Ant通配符/**表示匹配所有层级这是Spring MVC的约定注意不要和文件系统的通配符混淆。登录Controller除了接收表单参数并调用Service校验还需要在成功后重定向而不是转发。重定向用redirect:前缀这样浏览器地址栏会变化同时产生新的GET请求避免刷新页面时重复提交登录表单package com.example.travel.controller; import com.example.travel.entity.User; import com.example.travel.service.UserService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpSession; Controller public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } PostMapping(/login) public String login(RequestParam String username, RequestParam String password, HttpSession session) { User user userService.login(username, password); if (user ! null) { session.setAttribute(loginUser, user); return redirect:/; } return login; } GetMapping(/logout) public String logout(HttpSession session) { session.invalidate(); return redirect:/; } }这段代码里login方法返回视图名login当登录失败时Thymeleaf会渲染login.html页面。Service层的login方法返回 null 表示认证失败。session.invalidate()销毁整个会话退出登录时所有Session数据一并清除这是最彻底的退出方式。实际项目里还有一种常见做法是只移除登录属性而不销毁Session但毕设项目数据量小直接销毁Session更安全、更干净。4.2 景点列表与详情MySql分页查询和方法参数的三种传法景点列表页是用户进入系统后看到的第一个信息密集页面你的目标不只是把数据查出来而是让列表页有分页、有搜索、有跳转详情的能力。分页方案我推荐用PageHelper插件它基于MyBatis拦截器实现不用手写LIMIT语句对毕设项目的代码量来说侵入性最低。在pom.xml中添加依赖后Service中查询分页的代码是package com.example.travel.service.impl; import com.example.travel.mapper.ScenicMapper; import com.example.travel.entity.Scenic; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.stereotype.Service; import java.util.List; Service public class ScenicServiceImpl { private final ScenicMapper scenicMapper; public ScenicServiceImpl(ScenicMapper scenicMapper) { this.scenicMapper scenicMapper; } public PageInfoScenic getScenicList(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListScenic scenicList scenicMapper.selectByKeyword(keyword); return new PageInfo(scenicList); } }PageHelper.startPage(pageNum, pageSize)的调用必须在查询语句之前且只能对紧接着的下一条查询生效。这是PageHelper最核心的约定——它通过MyBatis的Interceptor机制在下一句SQL执行前自动拼接LIMIT子句如果你在startPage之后又执行了其他查询分页效果就会落到错误的SQL上。返回的PageInfo对象里封装了总记录数、总页数、当前页、每页大小等分页元数据直接塞进Model传给前端即可。分页参数从页面传到Controller有多种方式。最常见的是RequestParam显式绑定每个参数写一个注解参数少时直观参数多时可以封装成一个Query对象Spring MVC会自动按属性名匹配绑定PathVariable适合REST风格路径比如/hotel/detail/1里的1就是酒店ID。三种方式可以混用但要注意同一个请求里不要对同一个参数同时用Query对象和RequestParam声明Spring MVC会因为没有明确绑定规则而跳过绑定参数值永远是null。4.3 酒店预订下单MySql事务在Service层的落地下单是整个系统里最需要严谨的功能涉及订单表插入、余额或状态更新多个数据变更操作必须保证要么全部成功、要么全部回滚。Spring的Transactional注解提供了声明式事务管理——标注在Service方法上方法内部所有的数据库操作被纳入同一个事务任何一步抛出运行时异常前面执行过的SQL全部回滚package com.example.travel.service.impl; import com.example.travel.entity.Order; import com.example.travel.mapper.OrderMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; Service public class OrderServiceImpl { private final OrderMapper orderMapper; public OrderServiceImpl(OrderMapper orderMapper) { this.orderMapper orderMapper; } Transactional(rollbackFor Exception.class) public int createOrder(Integer userId, Integer scenicId, Integer hotelId, Integer type, Integer num, BigDecimal price) { Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setScenicId(scenicId); order.setHotelId(hotelId); order.setType(type); order.setNum(num); order.setAmount(price.multiply(new BigDecimal(num))); order.setStatus(0); return orderMapper.insertOrder(order); } private String generateOrderNo() { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); return sdf.format(new Date()) UUID.randomUUID().toString().replace(-, ).substring(0, 6); } }Transactional(rollbackFor Exception.class)里的rollbackFor很关键。Spring默认只对运行时异常回滚受检异常不会触发回滚如果你不显式声明rollbackFor Exception.class一旦方法里抛出IOException这类受检异常事务不会回滚数据就出现部分写入的脏状态。这是新手最容易踩的事务坑没有之一。订单编号的生成逻辑用了时间戳加随机片段确保唯一性。UUID截取前6位是为了控制长度加UNIQUE KEY索引兜底。订单金额用BigDecimal计算这是Java处理浮点金额的唯一正确选择double和float在金额计算中会出现精度丢失例如0.1 0.2 0.30000000000000004这在订单金额上不可接受。price.multiply(new BigDecimal(num))的调用方式也遵循了BigDecimal不可变对象的约定每次运算返回新对象原值不改变。4.4 Thymeleaf页面渲染列表循环、条件判断和链接拼接的模板写法Thymeleaf的模板语法是这套代码里的门面。景点列表页的核心循环和条件渲染是模板技术的关键段落。Controller把pageInfo.list放进Model模板里通过th:each遍历通过th:href拼接详情页链接通过th:if控制上下架状态显示!DOCTYPE html html langzh-CN xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title景点列表/title link relstylesheet th:href{/css/common.css} /head body div classcontainer h2热门景点/h2 div classscenic-grid div classscenic-card th:eachscenic : ${pageInfo.list} img th:src{${scenic.image}} alt景点图片 h3 th:text${scenic.name}景点名称/h3 p th:text${scenic.description}简介/p p 门票价格 span th:text${scenic.price 0 ? 免费 : #numbers.formatDecimal(scenic.price, 1, 2)} 0.00 /span /p a th:href{/scenic/detail/{id}(id${scenic.id})} classbtn查看详情/a /div /div /div /body /htmlth:eachscenic : ${pageInfo.list}的语法是在当前标签上做循环循环变量scenic在标签内部可以任意引用。th:href{/scenic/detail/{id}(id${scenic.id})}是Thymeleaf的链接表达式花括号里的{id}是路径变量占位符后面的(id${scenic.id})是实际值绑定如果Controller方法用的是PathVariable这里的链接形式必须匹配。价格显示用了一个三元表达式scenic.price 0 ? 免费 : #numbers.formatDecimal(scenic.price, 1, 2)#numbers.formatDecimal是Thymeleaf内置的数字格式化工具对象两个参数分别是最小整数位数和小数位数这样门票0元的西湖会显示“免费”不会出现尴尬的0.00字样。页面底部还要放分页导航分页条需要根据pageInfo的元数据动态渲染上一页和下一页的链接div classpagination a th:href{/scenic/list(pageNum${pageInfo.prePage})} th:if${pageInfo.hasPreviousPage}上一页/a span th:text${pageInfo.pageNum} / ${pageInfo.pages}/span a th:href{/spa/list(pageNum${pageInfo.nextPage})} th:if${pageInfo.hasNextPage}下一页/a /div这里pageInfo.prePage、pageInfo.nextPage在PageHelper中是有默认值的即使当前页是第一页prePage也是0而不是null但加上th:if判断能更好地控制显示逻辑。th:if的判定规则是变量为null、false、0、空字符串时视为不满足条件其他情况都渲染标签。合起来看这个模板处理了列表循环、条件显示、格式化、链接拼接四种典型视图需求能覆盖整个项目80%以上的页面渲染场景。5. Springboot项目常见问题与避坑指南五条亲身踩过的坑5.1 IDA启动报错Port 8080 was already in use现象点击运行控制台直接报Port 8080 was already in use应用启动失败。原因系统里已经有一个进程占用了8080端口。最常见的占用者是之前没关掉的Springboot应用副本也可能是其他开发软件的调试端口恰好用了8080。解决先确认占用进程Windows下在命令行执行netstat -ano | findstr 8080找到对应的PID然后打开任务管理器结束该进程。也可以不改端口直接一劳永逸地在application.yml里把server.port改成8081或其他不常用端口。但我建议养成先查进程再考虑改端口的习惯——如果你改端口所有Thymeleaf页面里写死的链接都要同步改那些写死绝对路径的页面会全部404。5.2 页面能打开但CSS和图片全部丢失现象HTML结构渲染出来了但页面没有任何样式所有图片裂开。原因浏览器的开发者工具控制台大概率有一串404错误指向/css/common.css或/images/xihu.jpg。大部分情况是Thymeleaf模板里用了href/css/common.css这种原生路径而不是th:href{/css/common.css}。Spring Boot的静态资源统一放在classpath:/static/下原生路径也能访问但当项目配置了server.servlet.context-path时所有静态资源的根路径会被改写原生路径就失效了。解决统一把模板里的静态资源引用全部改成Thymeleaf的URL表达式{/css/common.css}。这个表达式会自动拼接context-path项目部署到任意子路径下都不会丢样式。最笨也最彻底的排查方式是直接用浏览器访问http://localhost:8080/css/common.css如果这个地址都返回404问题在资源位置而不是模板写法如果这个地址能打开但页面里不显示问题在模板路径。5.3 MySql连接报SSL错误或时区异常现象项目启动时控制台报错关键信息是SSL connection error或The server time zone value is unrecognized。原因MySql 8.x 默认开启了SSL安全连接和UTC时区但JDBC驱动默认不信任服务器证书且本地开发环境的数据库通常没有配置时区。解决在application.yml的数据库连接串上追加两个参数useSSLfalse关闭SSL验证serverTimezoneAsia/Shanghai指定时区。注意这两个参数必须放在连接串的URL参数部分用连接顺序不影响。修改后重启应用即可。这个坑在全网的热搜词里反复出现说明影响面极广基本是MySql 8.0Spring Boot的教科书级翻车现场。5.4 Thymeleaf页面修改后不生效现象改了HTML里的文字或样式刷新浏览器还是旧内容怎么刷新都没变化。原因spring.thymeleaf.cache默认值为true模板引擎会把解析后的HTML缓存起来下次渲染直接用缓存副本。很多同学在用IDEA开发时修改了HTML但没重启应用缓存不失效。解决在开发环境的application.yml里显式配置spring.thymeleaf.cache: false。修改配置文件后必须重启应用才能生效。另一个小技巧是IDEA里按CtrlF9重新编译当前模块Thymeleaf在缓存关闭时会自动检测到模板文件的变更。有的同学改完页面喜欢用浏览器强刷CtrlShiftR这是治标不治本的问题根源在服务端缓存。5.5 Mapper方法报Invalid bound statement现象启动不报错但一调用某个Mapper方法就抛Invalid bound statement (not found): com.example.travel.mapper.ScenicMapper.selectByKeyword。原因MyBatis找不到Mapper接口对应的XML映射文件。这里的报错信息已经明确提示是接口方法没有绑定到SQL语句。常见原因有两种XML里的命名空间namespace写错没有对应到接口的全限定名或者mybatis.mapper-locations配置路径不对XML文件没有被扫描到。解决第一步检查XML文件里的mapper namespacecom.example.travel.mapper.ScenicMappernamespace必须是接口的全限定名第二步检查application.yml里的mybatis.mapper-locations: classpath:mapper/*.xml确认resources目录下的mapper文件夹里XML确实存在且路径拼写没错。IDEA有时候不会自动把resources目录下的XML文件复制到target目录如果确认路径没问题执行Build - Rebuild Project强制重新编译。6. 三张表联动的后台统计与导出功能让毕设拔高一个档次前几章实现的都是前台功能如果时间充裕建议把后台管理加上不需要做得很重但要有三个亮点订单数据统计、销量排行、订单导出。这三个功能都不是竞赛级复杂技术但足够让答辩评委看到你对业务的理解。支撑它们的是三张核心业务表的JOIN查询和MySql的聚合函数。第一个亮点是首页仪表盘用一条SQL统计出各景点的订单销量排行把MySql的GROUP BY和ORDER BY用在真实场景里比在面试题里刷十遍理解都深。第二个亮点是订单列表的批量导出用Java的POI库把订单表数据写入Excel文件这条链路完整覆盖了数据读取、内存组装、输出下载三个环节。第三个亮点是用户评论展示把评论表关联用户表和景点表用MyBatis的动态SQL处理可选查询条件。订单销量排行是三者中实现最简洁的核心代码在Mapper的XML文件里select idselectScenicSalesRank resultTypemap SELECT s.name AS scenicName, COUNT(o.id) AS orderCount, SUM(o.amount) AS totalAmount FROM t_order o INNER JOIN t_scenic s ON o.scenic_id s.id WHERE o.type 1 AND o.status 1 GROUP BY s.id, s.name ORDER BY orderCount DESC LIMIT 10 /select这条SQL返回的每行是Map而不是实体类MyBatis会自动把列名作为key、值作为value适合这种无固定结构类型的查询结果。GROUP BY后面跟了s.id, s.name两个字段因为SELECT里同时查询了两个非聚合列MySQL的ONLY_FULL_GROUP_BY模式要求分组字段必须和查询字段匹配否则直接报错。如果想在答辩时讲清楚这条SQL先解释清楚INNER JOIN的关联条件再解释分组的作用是把同一个景点的多张订单聚合到一行最后解释ORDER BY orderCount DESC的排序逻辑。关联表里的type 1过滤出门票订单status 1过滤出已支付订单这两个条件在实际业务里就是查询漏斗的两层筛子。最后一个建议是给你的项目写一份README文档把启动步骤、默认账号、技术栈版本号全部列清楚。毕设答辩现场最尴尬的场景不是功能有bug而是老师想上手点两下你还得现场回忆端口号和登录密码。README不用长半页纸足够但是能保住你最后的从容。这套方案做完你会发现Springboot的自动配置、Thymeleaf的模板渲染、MySql的表设计已经成了肌肉记忆下一次独立开发会顺畅得多。这些年我见过太多同学卡在大而全的选题上最后连基本功能都没闭环。旅游景点酒店预订网站这个方向技术栈实用、业务边界清晰、数据模型有代表意义值得你投入时间做扎实。希望帮到你。本文还有配套的精品资源点击获取