
简介本资源是一套面向计算机专业本科生及Spring Boot初学者的毕业设计级实战项目聚焦景区民宿在线预约场景解决传统旅游服务中信息不对称、预约流程低效等实际问题。压缩包共883个文件涵盖157个Java后端核心代码、70个Vue与156个JS前端交互逻辑、49个CSS样式文件、19个XML配置及2个YML配置文件辅以MySQL建表SQL、启动脚本bat和响应式页面资源整体大小28.93MB。项目采用标准MVC架构完整实现用户管理、民宿展示、在线预约、订单处理与Spring Security权限控制并提供配套数据库设计说明与源码级注释。学习者可直接导入运行深入理解Spring Boot自动配置、MyBatis数据层集成、前后端分离开发模式及响应式界面适配实践是兼具教学性、工程性与可扩展性的典型Web应用范例。 做景区民宿预约系统这类题目的人每年都不少。SpringBoot 已经是 Java 后端开发里绕不开的框架民宿预约又是典型的管理信息系统场景两者结合起来就成了很多毕业设计和课程设计的热门选题。但说实话我见过太多人拿到题目之后第一反应是去下载一份现成源码改个数据库连接、跑起来就以为万事大吉。这种作品拿到答辩现场老师随口问一句“并发下单会不会重复预订”“订单状态是怎么流转的”“房态冲突你是用什么 SQL 判定的”十有八九答不上来。这篇文章我想把做《基于SpringBoot框架开发的景区民宿预约系统的设计与实现》时从需求分析、数据库设计、核心代码实现、并发问题处理到论文整理的全过程按我的真实做法捋一遍。里面会直接给出核心表结构、关键代码和判定逻辑也会把我踩过的坑和答辩时容易被追问的点都标出来。适合正在做同类选题、打算自己动手写源码而不是纯靠下载的朋友参考。1. 景区民宿预约系统真正要解决的核心问题很多人做这个系统之前其实没有真正住过景区民宿也不知道民宿老板每天是怎么处理预订的。我简单还原一下线下场景客人打电话或者到店问“X月X日到X月X日的房还有没有”老板翻一个纸质登记本或者翻微信群里的聊天记录手动查一下有空房就口头答应然后记个时间。等客人到店发现房间已经被另一个渠道订走了纠纷就来了。这个场景里最核心的矛盾是房态信息不透明、预订决策依赖人工记忆。所以这个系统的本质不是给民宿做个展示页面也不是单纯地把线下流程搬到线上而是要解决两件事一是房态实时可查二是预订过程有据可依。系统要处理的业务对象说到底就是“一个客人在某段时间内占用某个房源”这件事。把这个占用关系用数据模型表达清楚、用代码保护起来系统的价值就出来了。1.1 需求来源和角色划分这类系统的需求来源一般有两类。第一类是课程设计或毕业设计题目需求文档里已经明确了要做什么第二类是景区周边民宿经营者想要一个轻量管理工具需求相对零散。无论哪种来源角色基本都可以划分为用户端和管理员端两块。用户端的核心需求包括注册登录、浏览民宿信息、按日期搜索可预订房源、提交预订、取消订单、查看自己的订单记录。管理端的核心需求包括维护民宿和房源信息、上下架房源、查看所有订单、确认订单、办理入住和退房、查看简单的统计报表。功能不需要多但每一条都要能说清楚它支持了什么业务操作。我不建议一上来就堆优惠券、积分、会员等级这类功能那会让系统范围完全失控。我在做的时候就把功能列表控制得非常克制核心就围绕“房源”和“订单”两个字其余都是辅助。1.2 容易被忽略的业务规则需求文档里通常不会直接写业务规则但这些东西躲不开而且是整个设计的重点一个房源在同一时间段内只能被一个有效订单占用如果客人预订了5月1日到5月3日的房间5月3日当天是可以被下一位客人预订的也就是说离店日当天不占用房源订单未支付时不能永久占用房源超过一定时间要自动释放否则用户随便下单会把房态堵死已经入住的订单不能随意取消取消订单之后房源要立刻恢复可预订状态。这些规则决定了表结构怎么设计、状态字段怎么定义、冲突检测 SQL 怎么写。如果一开始不把业务规则想清楚后面返工成本非常高。我最初做的版本就没有考虑“超时未支付自动关单”结果测试时自己下了好几单全部卡在待支付状态管理员只能手动改状态非常尴尬。2. 技术选型要能讲出理由为什么是SpringBoot加这套组合技术选型这部分不是简单写个技术清单就完了。答辩的时候老师一定会问“为什么选这个不选那个”你得能说出个一二三来。我先把我最后用的方案列出来再一个个解释理由。2.1 后端框架SpringBoot到底解决了什么问题后端我选的是 SpringBoot 2.x搭配 MyBatis Plus 做持久层框架。先说 SpringBoot 本身。早年的 SSMSpring SpringMVC MyBatis项目光配置文件就有七八个web.xml、spring-mvc.xml、spring-mybatis.xml、mybatis-config.xml每个里面还得配一堆 bean。刚接触项目的同学经常卡在环境搭建这一步连一个 HelloWorld 都跑不起来。SpringBoot 的核心价值就是“自动配置 起步依赖”把大量常规配置内置了你只需要关注业务代码。具体到开发体验上三个点最明显。第一内嵌 Tomcat不用单独装容器main 方法里直接启动。第二pom.xml 里引入 spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java 这几个依赖项目就能跑起来。第三application.yml 里配置好数据源、端口号开发环境就绪。省下来的时间全部可以投入到业务逻辑里。这里我建议大家选 SpringBoot 2.7.x 这个版本而不是最新的 SpringBoot 3.x。原因很简单3.x 基于 Jakarta EE很多旧教程里的 javax 包名都要改成 jakarta网上大量现成的代码示例直接用会报错。对于课程设计、毕业设计这种交付场景稳定、资料多、好排查问题才是最优先的。如果你在 IDEA 里新建项目时发现“Spring Initializr 默认拉不到 2.7.x”的选项直接在 pom.xml 里手动改版本号就行别在创建向导上死磕。提示pom.xml 里注意把 java.version 和 SpringBoot parent 版本对应起来。SpringBoot 2.7.x 用 Java 8 或 Java 11 都没问题如果本机装的是 JDK 17代码里要注意不要用太高版本的语法特性。2.2 持久层选型MyBatis Plus比纯MyBatis和JPA强在哪持久层我用的是 MyBatis Plus。如果有人单纯用原生 MyBatis我觉得性价比不高。MyBatis 虽然灵活但单表增删改查都要写 xml 文件和一个接口方法重复劳动非常多。Spring Data JPA 上手快但复杂的多表关联查询和动态条件筛选反而不直观尤其是在订单这种查询条件很多的表上JPA 的 Specification 写出来的代码可读性很差。MyBatis Plus 在我的理解里就是“MyBatis 的增强版”它内置了 BaseMapper单表 CRUD 不需要自己写 SQL直接用selectById、selectList、insert、updateById就能搞定。更关键的是它提供了一套强力的条件构造器 QueryWrapper可以用 Java 链式调用的方式构造动态 SQL特别适合订单列表这种“状态、日期、用户ID等多种条件组合筛选”的场景。举个实际例子我要查“某用户在某个时间段内、状态为已确认的订单”原生 MyBatis 要在 xml 里写一堆if标签做动态条件MyBatis Plus 只需要QueryWrapperReserveOrder wrapper new QueryWrapper(); wrapper.eq(user_id, userId) .eq(status, 1) .between(start_date, startDate, endDate); ListReserveOrder list reserveOrderMapper.selectList(wrapper);简洁、直观、不容易出错。对毕设项目来说这个优势能让代码量减少三分之一以上。我整理文档时对比过如果用纯 MyBatis光 Mapper xml 可能就要写两三百行而 MyBatis Plus 几乎不需要写 xml。2.3 前端、数据库和部署的搭配前端我比较推荐两种方案你可以根据自己前端基础选。第一种是服务端渲染方案用 SpringBoot 集成 Thymeleaf 模板引擎再配一套现成的后台管理模板比如 AdminLTE、Layui。这种方案的特点是代码都在同一个工程里不用额外起一个前端服务部署简单写完直接打包成 jar 就能跑。适合前端功底一般、想快速把系统搞完整的同学。第二种是前后端分离方案后端提供 RESTful API前端用 Vue 2 Element UI 或 Vue 3 Element Plus。这种方案更贴近企业实际开发视觉效果好但是工作量会明显增加而且要处理跨域、Token 认证、接口联调这些问题。如果你时间充裕、前端也还算熟练可以用分离方案如果时间紧我建议走 Thymeleaf 方案。数据库选型就很简单了MySQL 8.x 几乎是标配成本低、资料多、功能稳定。存储引擎必须用 InnoDB因为它支持事务和行级锁这两个特性在订单业务里至关重要后面讲并发控制的时候你就能感受到。字符集统一用 utf8mb4别用 utf8不然碰到 emoji 表情或者生僻字会出问题。Redis 在这个项目里不是必须的我后面聊高并发方案时会讲到什么场景才需要引入普通毕设项目用数据库事务就够了。3. 数据库设计才是整个系统的灵魂表结构和房态冲突判定很多同学做数据库设计喜欢先把表建出来然后在建表语句里东加一个字段西加一个字段。这个习惯不好。景区民宿预约系统最核心的表是订单表订单表的设计决定了整个系统的上限。我建议先把核心业务关系画出来用户要预订某个房源的一个时间段系统要记录这个预订的状态变化。围绕这个关系核心表其实只需要四张用户表、民宿表、房源表、订单表。再加上评论表和轮播图表作为功能延伸。我直接给出我最后落地的核心表结构并解释每个字段为什么存在。3.1 核心表的建表逻辑用户表CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 加密密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 角色0-用户 1-管理员, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码字段我用的是 BCrypt 加密后的密文不是明文。这个问题答辩的时候挺容易被问到的答案是“即使数据库泄露攻击者拿到也不是原文”。Spring Security 的BCryptPasswordEncoder可以直接用不需要引入整个安全框架单独引入 spring-security-crypto 依赖就够。民宿表我理解为景区内的民宿实体包含名称、地址、介绍、封面图等信息CREATE TABLE homestay ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 民宿名称, address varchar(200) DEFAULT NULL COMMENT 地址, description text COMMENT 民宿介绍, cover varchar(255) DEFAULT NULL COMMENT 封面图片URL, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0-下架 1-上架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT民宿表;房源表记录民宿下具体的某个房间或房型CREATE TABLE homestay_room ( id bigint(20) NOT NULL AUTO_INCREMENT, homestay_id bigint(20) NOT NULL COMMENT 所属民宿ID, name varchar(100) NOT NULL COMMENT 房型名称如湖景大床房, price decimal(10,2) NOT NULL COMMENT 每晚价格元, max_people int(11) NOT NULL DEFAULT 2 COMMENT 可住人数, area varchar(20) DEFAULT NULL COMMENT 房间面积, images varchar(1000) DEFAULT NULL COMMENT 房间图片多个用逗号分隔, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0-停售 1-可预订, PRIMARY KEY (id), KEY idx_homestay_id (homestay_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表;订单表是核心中的核心CREATE TABLE reserve_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 下单用户ID, homestay_id bigint(20) NOT NULL COMMENT 民宿ID, room_id bigint(20) NOT NULL COMMENT 房间ID, start_date date NOT NULL COMMENT 入住日期, end_date date NOT NULL COMMENT 离店日期, nights int(11) NOT NULL COMMENT 入住晚数, total_price decimal(10,2) NOT NULL COMMENT 订单总价, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0-待支付 1-已确认 2-已入住 3-已完成 4-已取消 5-已关闭, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_room_time (room_id, start_date, end_date), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预订订单表;注意订单表里我加了idx_room_time组合索引这是为房态冲突查询服务的。数据库里查“某个房间在某段时间内有没有被占”的时候这个索引能让查询快很多表数据量一大差异就很明显。3.2 房态冲突判定核心SQL怎么写才对民宿预约和普通商品秒杀不一样的地方在于秒杀抢的是一个确定库存量的商品而民宿预约要检查的是一个“时间段”里有没有重叠订单。比如现有订单占了 5月1日到5月3日那新订单如果是 4月30日到5月1日能不能订能因为 5月1日中午就退房了。那 5月2日到5月4日呢不能因为和现有订单重叠了。这个区间的重叠判断标准做法是用开闭区间的思路假设一个订单的入住日期是 start_date离店日期是 end_date那么它实际占用的时间是[start_date, end_date)也就是离店日当天不占用。两个时间段[A, B)和[C, D)重叠的条件是A D 并且 C B翻译成 SQL 就是SELECT COUNT(*) FROM reserve_order WHERE room_id #{roomId} AND status IN (0, 1, 2) AND start_date #{endDate} AND end_date #{startDate}如果查询结果大于0说明该房间在这个时间段里已经有有效订单了不能再接受新订单。这里的status IN (0, 1, 2)指的是状态为待支付、已确认、已入住的订单因为这些状态都占用房源。已取消、已关闭、已完成的订单不占用不需要考虑。没有杀器的正确逻辑很容易出现一个隐蔽的 bug现有订单是 5月1日到5月3日如果你想订 5月3日到5月5日有的人会写成end_date #{startDate}也就是 5月3日 5月3日判定为冲突。这就错了因为离店日中午退房中午之后完全可以接待新客人。用end_date #{startDate}就正确了。注意这个 SQL 逻辑是我在项目里实际验证过的。如果你用 MyBatis Plus 的 QueryWrapper写法是.lt(start_date, endDate).gt(end_date, startDate)别搞反了。3.3 订单状态机让订单流转有据可依订单不是建完就完事了它的生命周期一直在变化。我定义了五个有效状态和一个终态之外的取消态状态值状态名含义与触发条件0待支付用户提交订单成功后系统生成订单等待用户支付或管理员确认1已确认用户支付或管理员线下确认后订单正式生效房源锁定2已入住客人到店管理员办理入住3已完成客人退房订单完成房源释放4已取消用户支付前主动取消或管理员取消异常订单5已关闭待支付订单超时未支付系统自动关闭状态流转的关系是这样的0 可以到 1、4、51 可以到 2、42 只能到 3。3 是终态5 是终态4 也是终态。这些流转关系要在 Service 层做校验不能用一条任意的 update 语句直接改否则用户把自己订单改成“已完成”也不是没可能。我实现的时候在每个状态变更的 Service 方法里先查一遍当前状态再用条件更新UPDATE ... SET status #{newStatus} WHERE id #{id} AND status #{oldStatus}后面并发那一节会讲到这种写法其实是防止状态被并发覆盖的一种基本手段。4. 预约核心链路从搜索房源到订单落库的代码实现有了数据库表和状态定义接下来就是核心功能链路的实现了。预约流程里最关键的三个环节搜索可预订房源、提交预订、订单状态变更。我一个个拆开讲都是可以直接抄走的写法和思路。4.1 可预订房源搜索先过滤停售再排除冲突订单用户搜索房源时会传入入住日期、离店日期、人数、关键词。查询逻辑分两步第一步从homestay_room表里筛出状态为可预订、可住人数大于入住人数的房间第二步排除掉那些和目标时间段冲突的订单占用的房间。用子查询实现最直观select idselectAvailableRooms resultTypecom.example.homestay.entity.vo.RoomVO SELECT r.*, h.name AS homestay_name, h.address FROM homestay_room r LEFT JOIN homestay h ON r.homestay_id h.id WHERE r.status 1 AND h.status 1 AND r.max_people gt; #{people} if testkeyword ! null and keyword ! AND (r.name LIKE CONCAT(%, #{keyword}, %) OR h.name LIKE CONCAT(%, #{keyword}, %)) /if AND r.id NOT IN ( SELECT o.room_id FROM reserve_order o WHERE o.status IN (0, 1, 2) AND o.start_date lt; #{endDate} AND o.end_date gt; #{startDate} ) /select这里有一个细节值得说明我用了![CDATA[]]或者lt;、gt;来转义小于号和大于号。很多新手第一次写这个查询的时候直接写和MyBatis 会把它当成 XML 标签开头直接报错。遇到这种情况不要慌在 xml 里转义或者改成的写法就可以了。另外一种不用子查询的写法是 LEFT JOIN IS NULL性能在某些情况下更好但可读性差一些。对这种规模的系统子查询完全够用重点是要把业务逻辑表达清楚。4.2 提交预订三层校验缺一不可用户选定房源、填好日期点“提交预订”的瞬间后端要做的事情远不止 insert 一条订单记录。我在 Service 层做了三层校验第一层参数合法性校验。入住日期不能早于当前日期离店日期不能早于或等于入住日期。这层校验放在 Controller 层用参数注解或者手动判断都行目的是拦截明显不合法请求。第二层房源状态校验。查一下homestay_room里该房间是否存在、状态是否为 1可预订。如果用户点了一个已经下架的房源后端必须兜住这个异常。第三层房态冲突校验。就是前面那个重叠区间 SQL。这一层必须放在事务里执行而且要注意顺序先查有没有冲突再插入订单。我把核心代码写出来你直接就能用Service Transactional(rollbackFor Exception.class) public class ReserveOrderServiceImpl implements ReserveOrderService { Resource private ReserveOrderMapper reserveOrderMapper; Resource private HomestayRoomMapper homestayRoomMapper; Override public String createOrder(CreateOrderDTO dto) { // 1. 校验日期合法性 if (dto.getStartDate().isBefore(LocalDate.now())) { throw new ServiceException(入住日期不能早于当前日期); } if (!dto.getEndDate().isAfter(dto.getStartDate())) { throw new ServiceException(离店日期必须晚于入住日期); } // 2. 校验房源存在且可预订 HomestayRoom room homestayRoomMapper.selectById(dto.getRoomId()); if (room null || room.getStatus() ! 1) { throw new ServiceException(该房源不存在或已停售); } // 3. 校验房态冲突 long conflictCount reserveOrderMapper.selectCount(new QueryWrapperReserveOrder() .eq(room_id, dto.getRoomId()) .in(status, Arrays.asList(0, 1, 2)) .lt(start_date, dto.getEndDate()) .gt(end_date, dto.getStartDate())); if (conflictCount 0) { throw new ServiceException(该时间段房源已被预订请更换日期或房源); } // 4. 计算晚数和总价 long nights ChronoUnit.DAYS.between(dto.getStartDate(), dto.getEndDate()); BigDecimal totalPrice room.getPrice().multiply(BigDecimal.valueOf(nights)); // 5. 生成订单号并落库 ReserveOrder order new ReserveOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setHomestayId(room.getHomestayId()); order.setRoomId(room.getId()); order.setStartDate(dto.getStartDate()); order.setEndDate(dto.getEndDate()); order.setNights((int) nights); order.setTotalPrice(totalPrice); order.setStatus(0); reserveOrderMapper.insert(order); return order.getOrderNo(); } }订单号生成规则我是这样设计的前缀字母 TS 加时间戳再加四位随机数比如TS202506041253460001。虽然 MySQL 的 bigint 自增主键本身就能做唯一编号但订单号要暴露给用户而且格式上自己定义会更清晰所以我在业务层单独生成了订单号并加了唯一索引。4.3 订单状态变更用条件更新避免超范围操作订单确认、入住、退房这些操作管理员的处理入口不一样但核心代码套路一致。我以“确认订单”为例说一个通用写法Override public void confirmOrder(Long orderId) { int rows reserveOrderMapper.update(null, new LambdaUpdateWrapperReserveOrder() .eq(ReserveOrder::getId, orderId) .eq(ReserveOrder::getStatus, 0) .set(ReserveOrder::getStatus, 1)); if (rows 0) { throw new ServiceException(订单状态已变化无法确认请刷新后重试); } }这里最有价值的点是UPDATE ... WHERE id ? AND status 0这种条件更新写法。它保证了只有订单当前确实处于“待支付”状态时才能被置为“已确认”。如果两个管理员同时在后台操作同一个订单一个点确认、一个点取消只有一个能成功另一个会拿到影响行数为 0 的结果从而提示“状态已变化”。这个写法不需要加锁代码简单而且还天然防止了并发状态覆盖。5. 并发防超卖实训中必须跨过的坎前面已经能跑通完整的预订流程了但如果你的系统要接受“多人同时抢同一个房源”的考验就必须认真处理并发。这一块是我在实际测试里真正踩过坑的地方也是答辩时老师最喜欢追问的点。5.1 并发下为什么会出现重复预订我先说问题现象。假设某个景区民宿在五一期间只剩最后一间湖景房两个游客几乎同时下单。在单线程下第一个请求进来先查冲突发现没有冲突插入订单第二个请求进来查到冲突被拦住。但如果在高并发下两个请求同时通过了“查询房态”那一步都判断为“没有冲突”然后各自插入订单这个房间就被订出去了两次。很多同学不理解为什么会这样。核心原因在于你前面的“查冲突”和“插入订单”是两个独立的操作在数据库层面不是原子的。两个连接同时查询互不可见对方的数据查完都觉得自己是安全的然后各插各的就超卖了。我在本地用 JMeter 模拟 100 个并发请求去订同一个房源结果真的有 3 个订单成功插入问题真实存在。如果你做完系统只在页面上点两下根本发现不了这个 bug但答辩现场老师可能会拿这个问题测试你对系统的理解。5.2 正确的防重方案事务加条件更新而不是傻傻加锁很多人一提到并发就想到加锁其实得分场景。悲观锁SELECT ... FOR UPDATE是一种可行方案但代价是并发性能差而且代码里事务范围要控制得严格否则容易死锁。在高并发场景下更推荐的做法是把“防止重复”的压力转嫁给数据库的约束和条件更新。我的方案分两步。第一步给订单表增加一个唯一约束或者唯一索引让“同一个房间的同一个时间段”在数据库层面无法出现两条记录。但日期是区间MySQL 普通唯一索引没法直接对区间生效所以这一步可以改成在表里增加一个“时间槽位”字段把入住日期到离店日期的每一天展开成字符串比如“2025-05-01,2025-05-02”对这个字段做唯一索引。插入时如果同房间同槽位已经存在记录数据库直接报唯一冲突。这个方案叫做“时间槽位唯一键”用代码生成槽位字符串实现起来并不复杂但对业务侵入比较大。第二步也是我实际采用的方案把状态更新做成条件更新。因为订单状态从 0 变成 1、从 1 变成 2 这类操作本来就带条件再加上“防重复”的核心思路是在创建订单时先占位置。我可以把订单创建和房态占用设计成一条 UPDATE 语句完成而不是先查再插。但标准的民宿订单没法直接套用“库存扣减”那种写法所以我最后的落地方案是保留前面“先查冲突再插入订单”的逻辑同时给“查询冲突”加一个小锁。再简化一点就是把冲突查询所在的整个 Service 方法声明为synchronized用 JVM 的锁来保证单个应用实例内请求串行化。这样做对毕设项目完全够用但你要知道它的局限应用如果部署多个实例synchronized在不同实例之间是无效的。Override Transactional(rollbackFor Exception.class) public synchronized String createOrderWithLock(CreateOrderDTO dto) { // 先查冲突 long conflictCount reserveOrderMapper.selectCount(...); if (conflictCount 0) { throw new ServiceException(该时间段房源已被预订); } // 插入订单 reserveOrderMapper.insert(order); return order.getOrderNo(); }synchronized让同一个 JVM 内的请求串行化了这样“查冲突插入订单”这个复合操作就不会被并发拆开。配合Transactional事务会在方法体执行完后统一提交能有效防止超卖。我还额外加了一个防御性校验同一个用户重复提交同一个订单时后台会做幂等处理避免用户手抖点了两次提交按钮生成两条重复订单。5.3 更好的方案悲观锁、Redis分布式锁什么时候才值得用如果你想让代码更“高级”一点可以在冲突查询的 SQL 后面加上FOR UPDATESELECT COUNT(*) FROM reserve_order WHERE room_id #{roomId} AND status IN (0, 1, 2) AND start_date #{endDate} AND end_date #{startDate} FOR UPDATE;但这里要注意FOR UPDATE要生效查询条件必须命中索引而且它锁的是满足条件的索引记录。如果查询结果为空那它锁的就是一个间隙需要事务的隔离级别配合才行。很多同学只是在 SQL 后面加了句FOR UPDATE就以为万事大吉实际没命中索引的话锁不住数据这是我在调优时踩过的坑。如果项目引入 Redis可以用SETNX做一个分布式锁锁的 key 设计成room:lock:{roomId}:{startDate}:{endDate}抢到锁的请求才能执行查询和插入执行完释放锁。这是分布式部署时真正能用的方案但注意要做好锁超时和可重入设计否则锁没释放会导致大面积超时。我的建议是毕设项目优先用synchronized加事务简单可靠讲得清楚。如果老师追问更高阶的方案你回答出悲观锁和 Redis 分布式锁的原理和适用场景就可以了不需要真去实现一套分布式锁。能把“为什么需要”和“边界是什么”讲明白已经能体现水平。6. 把源码、数据库脚本和论文整理成一套完整交付物题目里说了要有源码、数据库和论文。很多人代码写完了但交付物的组织杂乱无章要么数据库脚本和代码里不一致要么论文和代码逻辑对不上。这一节讲讲我是怎么整理的这些细节也直接影响答辩评分。6.1 源码目录结构怎么组织才算合格项目的包名和目录结构最好一开始就规划好不要把所有类都堆在默认包下。我最终使用的结构是这样的com.example.homestay ├── HomestayApplication.java ├── common │ ├── Result.java // 统一返回结果 │ ├── ResultCode.java // 状态码枚举 │ └── exception │ ├── ServiceException.java │ └── GlobalExceptionHandler.java ├── config │ ├── MybatisPlusConfig.java │ └── WebMvcConfig.java ├── controller │ ├── UserController.java │ ├── HomestayController.java │ ├── RoomController.java │ └── OrderController.java ├── service │ ├── UserService.java │ ├── HomestayService.java │ ├── RoomService.java │ └── OrderService.java ├── service.impl │ └── ... ├── mapper │ ├── UserMapper.java │ ├── HomestayMapper.java │ ├── RoomMapper.java │ └── OrderMapper.java ├── entity │ ├── SysUser.java │ ├── Homestay.java │ ├── HomestayRoom.java │ └── ReserveOrder.java └── dto ├── CreateOrderDTO.java └── QueryRoomDTO.java统一返回结果类Result很值得写。它的结构包括 code、message、data 三个字段所有接口都返回这个对象前端处理起来统一调试也方便。全局异常处理器把 ServiceException 统一转成 HTTP 200 加业务错误码的响应体这样不用每个接口都写 try-catch代码会干净很多。6.2 数据库脚本和初始化数据数据库脚本我放在sql目录下分成两个文件schema.sql是建库建表语句data.sql是初始化数据。初始化数据里必须包含一个管理员账号和一些基础民宿房型数据不然别人拿到源码后跑起来是一张空表体验很差。管理员密码我用 BCrypt 预先生成一个密文写进 data.sql而不是明文。很多开源项目也是这么做的。这样你放进代码里的默认密码是明文但数据库里存的是密文登录逻辑里用 BCrypt 匹配既安全又方便。数据库脚本里的表名、字段名必须和代码里的实体类字段映射一致。这个问题特别容易出实体类里写了驼峰字段名startDate数据库里建的是start_date如果你的application.yml没有开启map-underscore-to-camel-case: true查询结果就全是 null。MyBatis Plus 默认开启了下划线转驼峰但如果你改动过配置一定要检查这个开关。6.3 毕业设计论文的写作思路与框架论文部分我想说一个核心观点论文不是给用户看的说明书而是给老师看的技术论证报告。它要回答三个问题你解决了什么问题、怎么解决的、解决得怎么样。我用的论文框架是典型的六章结构第一章绪论写景区的背景、民宿预订管理的现状、选题的意义、国内外研究现状。这部分不用写太长但要写出为什么需要线上预订系统重点是找出现有方式的痛点。第二章关键技术写 SpringBoot、MyBatis Plus、MySQL 这些技术的简介和选型理由。每个技术两三百字就够不要大段抄书重点是写清楚这个技术在你的系统里承担了什么职责。第三章系统分析写可行性分析和需求分析。需求分析里要画用例图。虽然我不能在这里画图但你论文里最好用建模工具画一下用户用例和管理员用例分开。还要写清楚业务规则包括状态流转规则和时间段重叠判定规则。第四章系统设计写总体架构、功能模块设计、数据库设计。数据库设计这章要写出 E-R 图和数据库表结构每张表的字段、类型、约束都要列清楚。这是论文里最容易被老师细看的部分。第五章系统实现展示核心功能的截图和关键代码片段。截图要真实不要用网上的图。代码片段选最核心的比如房态冲突检测 SQL、订单创建 Service、并发控制方案不要贴大段低价值代码。第六章系统测试写测试环境、功能测试用例表和结果、并发测试结果。可以用一个表格列出测试编号、测试项、预期结果、实际结果、是否通过。并发测试那里如果能把“压测前超卖3个订单、加锁后全部通过”的数据对比放进去会非常真实。摘要的写法有个通用公式背景一句话、系统用了什么技术一句话、系统有哪些功能模块一句话、主要解决的核心问题一句话、测试结果一句话。不要超过 300 字关键词选 4 到 5 个比如 SpringBoot、MyBatis Plus、民宿预订、数据库设计、并发控制。提示论文里的代码和数据库表必须和你实际交付的源码保持完全一致。老师拿到论文后经常做的一件事是照着论文里的功能清单去跑你的系统如果论文写了“支持用户取消订单”但代码里没有取消方法那就是明显的缺陷。宁可砍掉做不完的功能也不要往论文里堆假功能。我在整理交付物的时候还会额外写一个README.md包含项目介绍、环境要求JDK 版本、Maven 版本、MySQL 版本、启动步骤导入数据库脚本、修改 application.yml、运行 main 方法、默认账号信息、项目结构和功能列表。这个文件本身不加分但它能让老师更容易跑起来你的项目减少沟通成本间接影响评分。最后再分享一个小技巧答辩前把“房态冲突检测 SQL”“订单状态机”“并发防超卖”这三个你自己亲手实现的核心点照着代码各写一段伪代码放在手边老师一追问你就能非常从容地展开。这几个点既是整个系统的骨架也是你从一堆同类项目中跳出来的关键。真正写过一个系统的和只下载过一个仓库的在这些问题上开口就能分辨出来。本文还有配套的精品资源点击获取