ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Springboot+Thymeleaf酒店预订网站实战:环境搭建与核心流程

Springboot+Thymeleaf酒店预订网站实战:环境搭建与核心流程 简介一套基于SpringBoot与Thymeleaf的旅游景点酒店预订网站毕业设计源码面向高校计算机专业毕业生及SpringBoot入门开发者解决旅游预订类系统从零搭建到可运行、可演示的问题。系统围绕电商式业务流程设计覆盖用户注册登录、景点列表、相册墙、景点购买、评论、酒店管理和一整套后台管理系统注册验证接入SMS短信密码找回走QQ邮箱验证图片存储使用阿里云OSS并引入RabbitMQ消息队列与Redis缓存交互层采用Restful风格和JSON数据传输前端基于BootStrap与Layui。压缩包共487个文件、15.13MB包含51个Java源文件、34个HTML页面、67个JS脚本、37个CSS样式以及jpg/png/gif等大量静态资源另附1个SQL数据库脚本和Maven、properties、启动脚本等工程配置数据库采用MySql5.6JDK1.8环境即可导入运行。已有175人学习下载适合快速搭建旅游电商类毕设原型也适合借其SpringDataJpaThymeleaf整合思路进行二次开发。1. 从「跑不起来」到「打开就有页面」这个 SpringbootThymeleaf 预订网站到底能给你什么拿到一份「旅游景点酒店预订网站」的毕设源码第一反应别是急着点运行。90% 的翻车都发生在环境阶段——MySQL 连不上、Maven 依赖拉不下来、Thymeleaf 模板报错找不到变量。这个项目是一个典型的单体 Web 应用Springboot 做后端接口和业务逻辑Thymeleaf 做服务端页面渲染MySQL 存用户、景点、酒店、订单这些核心数据数据库脚本帮你把表结构一次性建好。和前后端分离的 VueSpringBoot 项目不同它把页面直接挂在后端进程里打开浏览器就能看到完整效果很适合拿来讲清楚 Java Web 的完整链路。这篇笔记适合两类人一是拿它做毕业设计、需要快速跑通并讲清楚原理的学生二是想用最小成本复现一个「能演示、能答辩、能扩展」的预订系统的开发者。我会把项目结构、表设计、核心流程和数据库脚本的落地步骤拆开讲最后告诉你哪些坑是必踩的。2. 为什么是 SpringbootThymeleaf 而不是前后端分离选型理由与项目骨架搭建2.1 Thymeleaf 在毕设场景里的不可替代性前后端分离是工业级项目的常态但放在毕设场景里它反而是个负担。你想想Vue 打包后的静态文件要丢进 Springboot 的 resources/static 目录跨域要配 CorsFilter接口文档要维护答辩时老师让你现场改个页面文案你得先 npm run build 再重启后端——这一套操作对评审老师来说远不如直接在 HTML 里改一行字来得直观。Thymeleaf 是服务端渲染模板引擎页面上的数据是后端塞进去的th:each循环列表、th:if判断状态、th:href拼链接这些语法比 JSP 干净又比 Freemarker 对 Springboot 的集成更自然。Springboot 官方 starter 直接支持不用额外写配置类这是它能成为毕设首选的根本原因。一个典型误区是有人觉得 Thymeleaf 过时了不值得学。这个判断放在 2024 年看是不对的。Springboot 官方文档里 Thymeleaf 依然是推荐模板引擎之一而且它的语法和现代前端框架的模板语法有不少相似之处学会它再去看 Vue 的v-for、v-if迁移成本很低。更重要的是毕设答辩考察的是「你能不能讲清楚数据是怎么从数据库到页面的」Thymeleaf 的链路最短、最好讲。你不需要理解 Vue 的虚拟 DOM、响应式原理这些额外概念一条链路走到底MySQL → Mapper → Service → Controller → Model → Thymeleaf → 浏览器。2.2 项目目录结构与 Maven 依赖清单先用一张表把标准的 SpringbootThymeleaf 项目结构列清楚这张结构图会在后面每个章节里反复用到。目录/文件作用关键点src/main/javaJava 源码根目录包名通常用 com.xxx.travelcontroller/接收 HTTP 请求返回页面或 JSON页面跳转用return indexservice/业务逻辑层事务注解 Transactional 放这里mapper/数据库访问层用 MyBatis 或 Spring Data JPAentity/实体类字段和数据库表列一一对应resources/templates/Thymeleaf 模板存放目录放 .html 文件resources/static/静态资源CSS/JS/图片不影响模板渲染resources/application.yml核心配置文件数据源、端口、Thymeleaf 缓存开关pom.xmlMaven 依赖清单整个项目能不能拉起来全看它pom.xml 是骨架里的骨架。很多新手拿到源码第一件事是双击运行结果import org.springframework.boot都标红那就是依赖没进来。一个最小可跑的 pom.xml 关键依赖长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency /dependencies这里的版本号是有讲究的。spring-boot-starter-parent我推荐锁死在 2.7.x不要追新。热词里总有人搜「springboot版本太高」就是踩了这个坑——Springboot 3.x 要求 JDK 17而且javax.servlet包名换成了jakarta.servlet你从网上下载的很多毕设源码还是javax开头的 import一换版本直接编译报错。MySQL 驱动在 Springboot 2.x 里用mysql-connector-java到了 3.x 改名成com.mysql:mysql-connector-j新手根本对不上。2.7.18 是 2.x 系列的最终版本安全性没大问题兼容性最好。2.3 application.yml 配置项逐个说清楚配置文件的坑比代码多。最常见的翻车现场是代码一模一样别人的能跑你的连不上数据库。问题基本都出在application.yml里。我给你一份能直接照抄的配置每行注释都写了为什么这么设server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.travel.entity逐个参数说明。serverTimezoneAsia/Shanghai必加不加的话 MySQL 8 会报 CST 时区识别错误这是 MySQL 8.x 和 JDBC 驱动之间的老矛盾。useSSLfalse是关掉 SSL 握手本地开发没必要开开启反而容易报证书错误。allowPublicKeyRetrievaltrue是给 MySQL 8 用的如果密码加密方式是 caching_sha2_password不设这个驱动会拒绝连接。thymeleaf.cachefalse在开发期一定关掉否则你改了 HTML 刷新浏览器看到还是旧页面——这个缓存问题就是热词里「thymeleaf」相关的最大血泪经验。还有一点容易被忽略mapper-locations指向classpath:mapper/*.xml如果你的 mapper 接口直接写注解 SQL 就不用配这个路径但大部分毕设源码是 XML 和注解混用的这一行配了不会出错不配反而可能在启动时提示找不到 mapper 映射。启动类上记得加MapperScan(你的mapper包路径)不加这个注解也是一样的报错。3. 把数据库脚本变成运行中的系统核心表设计与 SQL 落地3.1 旅游景点酒店预订到底需要几张表这个项目的核心业务是「用户选景点周边的酒店然后订房间」。围绕这个场景表设计至少要有 5 张基础表用户表、景点表、酒店表、房间表、订单表。别小看这个表数量很多毕设翻车就是想在表设计上玩花活搞了十几张表结果数据和代码根本对不上。我给你拆解一下每张表存在的理由。用户表负责登录注册和身份识别字段不要贪多id、username、password、nickname、phone够用了。景点表和酒店表是内容载体景点表要有name、description、image_url酒店表要有name、address、star_level星级这是预订排序的重要维度、scenic_id外键关联到景点。房间表比较关键它不只是「房间类型」的字典表还要有total_count总间数和available_count可订间数——这两个字段是并发控制的核心后面讲下单流程会重点说。订单表是业务闭环的落点user_id、room_id、check_in_date、check_out_date、status待支付/已支付/已取消这几个字段必须齐全。为什么用外键关联而不是直接在酒店表里存景点名字答案是数据一致性。比如「西湖」这个景点改名为「西湖风景区」如果酒店表里直接存的是字符串你得写一堆 UPDATE如果存scenic_id改景点表一行就搞定了。毕设答辩时老师最常问的就是表关联设计用外键字段能讲出设计意图。3.2 建表 SQL直接可执行的脚本长什么样数据库脚本是整个项目的地基但我见过太多人拿到.sql文件直接全选执行然后报错一脸懵。脚本能不能跑通关键看三件事字符集、自增主键、外键顺序。给你一份核心建表 SQL覆盖上面说的 5 张表CREATE DATABASE IF NOT EXISTS travel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE travel_db; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, nickname VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE scenic ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description TEXT, image_url VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE hotel ( id INT NOT NULL AUTO_INCREMENT, scenic_id INT NOT NULL, name VARCHAR(100) NOT NULL, address VARCHAR(255) DEFAULT NULL, star_level INT DEFAULT 3, PRIMARY KEY (id), KEY idx_scenic_id (scenic_id), CONSTRAINT fk_hotel_scenic FOREIGN KEY (scenic_id) REFERENCES scenic (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE room ( id INT NOT NULL AUTO_INCREMENT, hotel_id INT NOT NULL, room_type VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, total_count INT NOT NULL, available_count INT NOT NULL, PRIMARY KEY (id), KEY idx_hotel_id (hotel_id), CONSTRAINT fk_room_hotel FOREIGN KEY (hotel_id) REFERENCES hotel (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id INT NOT NULL, room_id INT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user (id), CONSTRAINT fk_order_room FOREIGN KEY (room_id) REFERENCES room (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段脚本的逻辑说明值得细看。第一所有表都用ENGINEInnoDB不用 MyISAM——InnoDB 支持事务和外键后面订单扣减库存时要靠事务保证数据一致。第二user表用了反引号因为user是 MySQL 的保留字不加反引号直接执行必报语法错误。第三orders表加了order_no唯一键这是一定要有的订单号唯一是商业系统的底线。第四price用DECIMAL(10,2)不用 FLOAT原因是 FLOAT 是近似值算钱算错分毫会被答辩老师抓包。3.3 用 Navicat 或命令行导入脚本的正确方式拿到数据库脚本后导入方式看起来简单但坑很深。用命令行导入是最不容易出错的mysql -u root -p travel_db.sql这个命令的意思是用 root 用户登录 MySQL然后把travel_db.sql文件里的所有 SQL 语句交给 MySQL 执行。-p后面是密码参数执行后会交互式询问密码这样比在命令行里明文写密码安全。注意是输入重定向方向别搞反了写成了会把 SQL 文件内容清空。如果报错说ERROR 1049 (42000): Unknown database说明脚本文件里没有CREATE DATABASE语句或者你没执行该语句。我一般会用 Navicat 可视化导入兜底新建连接后右键运行 SQL 文件选脚本路径编码选 utf8mb4。这里有个细节Navicat 导入时下方有个「遇到错误时继续」的勾选框默认不勾如果脚本中间某条语句出错会直接中断——这反而是好事能让你看到到底是哪条跑挂了。热词里总有人搜「idea导出数据库脚本」那是反过来从数据库导出导入和导出方向别搞混。导入完成后验证一下USE travel_db; SHOW TABLES; SELECT COUNT(*) FROM user;SHOW TABLES能看到 5 张表都建好了SELECT COUNT(*)能看到脚本里预置的测试数据是否成功插入。很多脚本会附带几条测试数据比如默认管理员账号 admin这对接下来的登录测试很重要——没有数据你只能看空页面什么都验证不了。4. 核心流程怎么跑通从登录到下单的完整链路实现4.1 登录注册Session 管理和 Thymeleaf 模板渲染所有业务系统的第一步都是登录。这个项目的用户表已经建好接下来要写的是 Controller 和模板之间的配合。先看登录 Controller 的代码怎么写Controller public class UserController { Autowired private UserService userService; GetMapping(/login) public String loginPage() { return login; } PostMapping(/login) public String doLogin(RequestParam String username, RequestParam String password, HttpSession session, Model model) { User user userService.login(username, password); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); return redirect:/index; } }这里面的逻辑说明分几层。GetMapping(/login)是打开登录页面返回的login字符串对应templates/login.htmlThymeleaf 会自动拼接前缀后缀找到模板文件。PostMapping(/login)处理表单提交RequestParam接收前端表单里的username和password字段。登录成功后把用户对象塞进HttpSession这是为了后续页面判断「当前有没有人登录」——Thymeleaf 模板里可以用${session.loginUser}拿到这个对象没登录就显示「登录/注册」按钮登录了就显示用户名。redirect:/index是重定向好处是防止刷新页面时重复提交表单这是 POST 后重定向的标准做法。对应的login.html里的表单部分form th:action{/login} methodpost input typetext nameusername placeholder用户名 required/ input typepassword namepassword placeholder密码 required/ button typesubmit登录/button p classerror th:if${error} th:text${error}/p /form注意th:action{/login}这个写法{}是 Thymeleaf 的链接表达式它会自动拼接上下文路径比写死/login更安全——如果项目加了server.servlet.context-path写死的路径直接失效。th:if${error}是条件判断后端塞进 Model 的error字段有值才显示错误提示。注册流程和登录是同一个套路只是多了个INSERT操作。userService.register(user)里要加用户名唯一性校验否则数据库的唯一索引会给用户返回一个难看的 500 错误页这是体验问题也是答辩时能讲出来的亮点。4.2 酒店列表页MyBatis 查询和 Thymeleaf 循环渲染登录进来后的主页面是景点列表或者酒店列表。以「按景点查看酒店」为例先看 SQL 怎么写select idfindHotelsByScenicId resultTypecom.example.travel.entity.Hotel SELECT h.*, s.name AS scenic_name FROM hotel h LEFT JOIN scenic s ON h.scenic_id s.id WHERE h.scenic_id #{scenicId} ORDER BY h.star_level DESC /select这段 XML 是 MyBatis 的 mapper 映射。LEFT JOIN scenic是为了把景点名字也查出来这样页面列表里可以显示「位于西湖景区」这类描述不用额外再查一次数据库。ORDER BY h.star_level DESC按星级倒序这是酒店列表最常见的排序逻辑——贵的、好的排前面。Controller 拿到数据后塞进 ModelGetMapping(/hotels) public String hotels(RequestParam Integer scenicId, Model model) { ListHotel hotels hotelService.findByScenicId(scenicId); model.addAttribute(hotels, hotels); model.addAttribute(scenicId, scenicId); return hotel-list; }hotel-list.html里用th:each循环渲染div classhotel-card th:eachhotel : ${hotels} h3 th:text${hotel.name}酒店名称/h3 p th:text${hotel.scenicName} · ${hotel.address}景区地址/p p星级 span classstar th:text${hotel.starLevel} 星/span /p a th:href{/rooms(hotelId${hotel.id})} classbtn查看房型/a /div这里的th:each是循环${hotels}是后端塞进来的列表每次循环把当前元素命名为hotel。${hotel.scenicName}对应Hotel实体类的scenicName属性——注意我前面 SQL 里用了别名AS scenic_nameMyBatis 开启了驼峰映射后会自动把scenic_name转成scenicName不开启这个配置字段就是 null。这个配置在application.yml里加一行mybatis: configuration: map-underscore-to-camel-case: true没加这行的话数据库里的star_level映射不到实体的starLevel字段页面显示全是 0。这个坑我踩过一次排查了半小时才发现是映射配置的问题。4.3 下单逻辑事务、库存扣减和订单号生成下单是整个系统最核心的业务也是答辩时老师必问的部分。先想清楚一个问题如果两个用户同时订同一间房的最后一间怎么保证不会超卖答案是用数据库的行级锁配合事务。下订单的 Service 代码Transactional public Order createOrder(Integer userId, Integer roomId, String checkIn, String checkOut) { // 1. 生成唯一订单号 String orderNo generateOrderNo(); // 2. 查询房间并加锁 Room room roomMapper.findByIdForUpdate(roomId); if (room.getAvailableCount() 0) { throw new RuntimeException(该房间已订完); } // 3. 扣减可订数量 roomMapper.decreaseAvailableCount(roomId); // 4. 创建订单 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setRoomId(roomId); order.setCheckInDate(Date.valueOf(checkIn)); order.setCheckOutDate(Date.valueOf(checkOut)); order.setStatus(0); orderMapper.insert(order); return order; }逐个参数和关键点说明。Transactional是事务注解它保证第 2、3、4 步要么全部成功要么全部回滚——如果订单创建失败那扣掉的库存也会恢复不会出现库存少了但订单没有的脏数据。第 2 步findByIdForUpdate是关键中的关键这个方法的 SQL 必须是SELECT * FROM room WHERE id #{id} FOR UPDATE。FOR UPDATE是数据库行级锁两个并发请求同时进来时第二个会等待第一个事务提交后才继续执行这样就堵住了超卖漏洞。generateOrderNo()的常见实现是private String generateOrderNo() { return ORD System.currentTimeMillis() String.format(%04d, new Random().nextInt(10000)); }这个生成规则是「时间戳 4 位随机数」优点是简单、够用。缺点是极端并发下可能重复所以数据库里uk_order_no唯一索引兜底万一重复插入会抛异常事务回滚。如果你想更稳可以用UUID.randomUUID().toString().replace(-, )但订单号 32 位太长了页面显示体验不好我一般用时间戳方案加上唯一索引保底。4.4 环境准备本地跑起来的完整步骤代码逻辑讲完了你还得先把环境弄好。这里面最让人头疼的就是 MySQL 的安装配置。热词里「mysql安装教程」「mysql安装配置教程」「mysql在windows10上怎么安装」搜的人特别多可见这块是重灾区。我不展开安装细节只说必踩的几步第一步安装完 MySQL 后第一件事是改密码策略。MySQL 8 默认密码策略要求大小写字母加数字加特殊符号很多人设了个123456直接报错ERROR 1819 (HY000): Your password does not satisfy the current policy requirements。解决方法是ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;这条 SQL 的意思是把 root 用户的密码改成 123456同时指定用mysql_native_password插件。MySQL 8 默认是caching_sha2_password老版本的 JDBC 驱动不认这个插件会报Public Key Retrieval is not allowed错误。第二步确认 MySQL 服务是否启动。Windows 下按Win R输入services.msc找到 MySQL 服务看状态是不是「正在运行」。服务没启动的话你代码里配的localhost:3306根本连不上报错是Communications link failure很多人冤枉了自己的代码。第三步如果你用的是自己本机安装的 MySQL 而不是脚本自带的注意字符集。MySQL 安装时默认字符集可能是latin1导入 SQL 里的中文全部变成乱码。检查方式SHOW VARIABLES LIKE character_set_database;如果是 latin1执行SET GLOBAL character_set_database utf8mb4;然后重新导入脚本。这步不做后面页面上景点介绍全是问号。5. 避坑指南从环境搭建到代码运行的 5 条血泪经验5.1 现象数据库连接报 SSL 错误MySQL 8 连接时报SSL connection error: protocol version mismatch或者The server host key is unknown。原因分析MySQL 8 默认开启 SSL 认证而 JDBC URL 里没配useSSLfalse驱动尝试建立 SSL 连接但本机证书不匹配。解决方式是在application.yml的数据源 URL 末尾加上useSSLfalseallowPublicKeyRetrievaltrue另外确认驱动版本不要用老旧的 5.x 驱动换成com.mysql.cj.jdbc.Driver。这个错误在企业开发里不算高频但在毕设场景几乎人人都会碰到因为本机 MySQL 基本都是默认配置。5.2 现象Springboot 启动报端口被占用或版本太高APPLICATION FAILED TO START提示Port 8080 was already in use原因是上一个没关掉的后端进程还占着端口。解决Windows 下netstat -ano | findstr 8080找到 PID然后taskkill /F /PID 该PID。更治本的做法是在application.yml里把端口换成 9090 或 8081避开系统上被其他程序占用的默认端口。还有一种情况更坑你下的源码是 Springboot 2.x但 IDEA 的 Maven 仓库里默认拉的是最新版 Springboot 3.x。3.x 里很多 2.x 的javax包名换成了jakarta编译直接报红。解决锁死spring-boot-starter-parent版本为 2.7.18不要用RELEASE这种浮动版本号。5.3 现象Thymeleaf 页面改了不生效改完 HTML 刷新浏览器页面还是旧的重启项目才变。原因是 Thymeleaf 默认开启模板缓存把渲染过的页面缓存在内存里开发期不改cachefalse就看不到新效果。解决在application.yml里加一行spring.thymeleaf.cache: false。这里有个细节容易被忽略——改完配置后必须重启生效。另外 IDEA 里改完 HTML 后按Ctrl F9重新编译Springboot DevTools 的自动重启和 Thymeleaf 缓存关闭是配套使用的只开一个效果不完整。5.4 现象数据库脚本导入报Unknown collation: utf8mb4_0900_ai_ci用 MySQL 5.7 导入生成于 MySQL 8 的 SQL 脚本时报这个错。原因是utf8mb4_0900_ai_ci是 MySQL 8 的默认排序规则5.7 根本不认识。解决方式有二一是把脚本里的所有utf8mb4_0900_ai_ci替换成utf8mb4_general_ci二是在脚本开头加一句SET NAMES utf8mb4;声明客户端字符集。这个坑在毕设里很常见因为很多人是用的 MySQL 8 写的脚本但自己电脑上装的是从热词里搜来的「mysql 5.7.44 安装过程详细」方法装的旧版本。下载前先确认自己 MySQL 版本SELECT VERSION();。5.5 现象跳转页面显示 Whitelabel Error Page 或模板解析错误页面报 404 或者一段难看的默认错误页后台日志提示Template might not exist or might not be accessible。原因是 Thymeleaf 找不到模板文件或者返回的字符串和templates/目录下的文件名对不上。解决检查templates/目录的路径层级。如果你把login.html放在templates/user/login.htmlController 里就要return user/login单写login必报错。另一个常见原因是把 HTML 放到了resources/static/目录Thymeleaf 的prefix配置只认classpath:/templates/。我用一个土办法排查直接访问/login看是不是跳转到登录页如果跳到 Whitelabel 就说明模板路径有问题把return的值改成和目录层级一一对应即可。6. 让项目从「能用」到「好讲」一个必做的验证技巧和两个加分改动先把最值得做的验证技巧说透用「一次完整的订房失败场景」来验证事务是否真的生效。具体操作是先用测试账号登录选一个available_count为 1 的房间开两个浏览器窗口同时提交订单。如果系统设计正确两个请求只有一个成功另一个会看到「房间已订完」的提示。这个验证的价值在于第一它证明了Transactional和FOR UPDATE的代码不是摆设第二它是毕设答辩时最有力的演示素材——老师问你「并发超卖怎么解决」你当场给他演示一遍比背十句理论都强。另一个加分改动是给列表页加一个搜索框。不需要引入 Elasticsearch 之类的重型工具用 MyBatis 的动态 SQL 就够select idsearchHotels resultTypeHotel SELECT * FROM hotel where if testkeyword ! null and keyword ! name LIKE CONCAT(%, #{keyword}, %) /if /where /selectwhere标签会自动处理多余的和字关键词为空时不加查询条件不为空时模糊匹配名称。这行改动不大但让系统从「只能按景点看酒店」升级成「支持关键词搜索」功能完整度提升一个档次。还有一个小技巧是给orders表加一个「取消订单」的入口。逻辑是状态从待支付改成已取消同时把available_count加回去。这个操作的 SQL 要写在同一个事务里否则取消订单后库存没恢复后面的人就订不到了。代码不复杂但它是「预订网站」业务闭环的最后一块拼图——能下单、能支付状态标记、能取消这才叫完整的预订系统。多这一个功能答辩时你能少被问一个「这块为什么没做」的尴尬问题。我的个人习惯是每次改完数据库表结构都会重新跑一遍建表脚本而不是手动ALTER TABLE这样能保证脚本永远是最终版本。有一次我直接在数据库里改了字段名忘了同步脚本结果演示环境重建时表结构和实体类对不上整整折腾了一个晚上。后来我养成了「代码里每加一个字段脚本里同步加一行」的习惯这个习惯帮我避开了很多次翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表