ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot+Vue民宿预定毕设:防重叠预定与权限控制实战解析

SpringBoot+Vue民宿预定毕设:防重叠预定与权限控制实战解析 简介一份基于SpringBoot的民宿在线预定平台设计与实现面向Java方向毕业设计、课程作业及想掌握前后端分离开发的学习者。资源完整覆盖了民宿预订的典型业务流程用户注册登录、条件搜索与筛选、房源浏览与评价、房型选择、在线支付、订单管理、实时客服及收藏功能同时提供民宿业主端与管理员端的管理能力适合作为SpringBootVue全栈项目的实战参考。压缩包共780个文件包含Java后端源码、Vue前端页面、JS逻辑、HTML/CSS静态资源、SVG图标、XML/yml配置及SQL数据库脚本等整体大小17.62MB目录结构清晰便于对照学习与部署运行。目前已有85人学习下载。整套资料既能帮助理解真实项目从数据库设计到前后端交互的实现细节也可为毕业设计写作提供系统性的代码支撑与功能范本适合毕业答辩前的快速实操与二次开发。1. 拿到一套民宿预定毕设先别急着点运行拿到一套 SpringBoot 民宿预定毕设源码第一反应通常是双击 2-run.bat 等它跑起来。但这类项目真正花时间的不是启动而是三件事同一房型在重叠日期段怎么防重复预定三种角色用户、业主、管理员的权限怎么落到接口上前后端分离时登录态怎么维持。这三处也正是毕设答辩时老师最爱追问的细节。这套基于 Spring Boot Vue 前后端分离的民宿在线预定平台覆盖民宿搜索、房型下单、订单状态流转、业主管理房源、管理员审核等模块适合正在做同类毕设需要对照实现的人也适合接手一套 VueSpringBoot 源码但没理清调用链路的开发者。本文从工程结构、数据模型、前后端接口对接讲到可实际演示的验证流程中间会给出可直接抄走的 SQL、Java 和 JavaScript 代码。2. 工程结构与Spring Boot配置三个bat一台戏2.1 Maven生命周期install、run、package分别干了什么拿到压缩包后建议先别急着用 IDE 打开把根目录的文件清单过一遍。.classpath说明这套工程曾被 Eclipse/STS 系工具打开过mvnw.cmd是 Maven Wrapper1-install.bat、2-run.bat、3-build.bat三个脚本基本决定了项目的构建方式。# 1-install.bat 核心命令 mvnw.cmd clean install -DskipTests # 2-run.bat 核心命令 mvnw.cmd spring-boot:run # 3-build.bat 核心命令 mvnw.cmd clean package -DskipTests三个脚本对应的阶段和产出物如下。脚本执行目标典型产物1-install.batclean install安装到本地 Maven 仓库供多模块联调使用2-run.batspring-boot:run本地直接启动进程默认监听 80803-build.batclean packagetarget 目录下生成可执行 jar-DskipTests表示跳过测试代码的编译和执行毕业设计里 test 目录常残留不完整用例或依赖本地环境的断言直接跳过能省掉大量环境问题。如果连测试类都不想编译可以换-Dmaven.test.skiptrue。在多模块工程里install和package区别明显install 会把产物装进本地仓库供其他模块引用这套民宿项目如果是单模块日常开发用 2-run.bat最终交付用 3-build.bat 就够了。Maven Wrapper 解决的是「本机没装 Maven 也能构建」的问题。mvnw.cmd会读取.mvn/wrapper/maven-wrapper.properties中的 distributionUrl 去下载指定版本的 Maven。国内网络环境下载官方地址经常卡死常见做法是把 distributionUrl 改成阿里云镜像仓库中对应版本的 zip 包。如果下载还是失败打开 IDEA 的 Maven 设置把 Maven home path 指向 IDEA 自带的 Maven效果一致。2.2 application.yml里决定成败的参数Spring Boot 项目的核心配置集中在src/main/resources/application.yml。毕业设计中大量「启动报错」都出在数据源参数上尤其是 MySQL 8 的时区、SSL、公钥检索三个配置项。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/homestay?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezoneAsia/Shanghai必须加否则 MySQL 8 驱动会因无法识别本地时区抛异常useSSLfalse避免本机 SSL 握手警告allowPublicKeyRetrievaltrue解决 MySQL 8 使用 caching_sha2_password 插件时客户端报 Public Key Retrieval is not allowed 的问题。map-underscore-to-camel-case把数据库的create_time自动映射为实体的createTime这是 MyBatis-Plus 下最省事的命名转换方式。log-impl设置为 StdOutImpl 后每次 SQL 执行都会打印完整语句和参数排查联表查询、分页、条件构造器问题时比看业务日志快得多。2.3 从.classpath和.bak文件反推项目历史.classpath是 Eclipse 系工具生成的工程文件用 IDEA 打开时直接选择 pom.xml 重新解析不要双击.classpath。而那一批.bak文件是前人在改代码时留下的回退副本update-password.vue.bak对应修改密码页面IndexAsideStatic.vue.bak、IndexHeader.vue.bak、BreadCrumbs.vue.bak是后台管理布局中的侧边栏、顶栏和面包屑组件index.html.bak则是前端入口页的备份。.bak文件只要没有被 import 就不会参与打包但对全局搜索和后续接手的人会造成干扰改完功能确认稳定后应当清理掉。保留一份原始版本再动手改代码这个习惯本身没问题——真正的坑在于改完后忘了把.bak挪出 src 目录导致 vite/webpack 构建时对不认识的文件扩展名报错。3. 民宿预定数据模型与防重叠预定库存和日期的博弈3.1 用户、民宿、房型三张底层表怎么建这套系统的角色分为用户、民宿业主、管理员三类。落地到数据库不需要建三张独立用户表一个用户表加角色字段即可。民宿业主可以理解为「拥有民宿资源的用户」管理员则是后台维护者。CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL COMMENT 登录名, password VARCHAR(255) NOT NULL COMMENT BCrypt加密, role TINYINT DEFAULT 1 COMMENT 0管理员 1用户 2业主, phone VARCHAR(20), email VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 用户表; CREATE TABLE t_homestay ( id BIGINT PRIMARY KEY AUTO_INCREMENT, owner_id BIGINT NOT NULL COMMENT 关联t_user.id, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL, address VARCHAR(200), description TEXT, main_picture VARCHAR(500), min_price_per_night DECIMAL(10,2) COMMENT 用于列表页价格筛选, avg_rating DECIMAL(2,1) DEFAULT 5.0, facilities VARCHAR(500) COMMENT WiFi,停车,厨房,空调, status TINYINT DEFAULT 0 COMMENT 0待审核 1上架 2下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 民宿表; CREATE TABLE t_room_type ( id BIGINT PRIMARY KEY AUTO_INCREMENT, homestay_id BIGINT NOT NULL, name VARCHAR(50) NOT NULL COMMENT 大床房/双床房, price_per_night DECIMAL(10,2) NOT NULL, stock INT DEFAULT 1 COMMENT 该房型房间数, area SMALLINT, bed_type VARCHAR(30), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 房型表;民宿的facilities字段用逗号分隔满足毕设级别的模糊搜索够了如果要做价格区间、设施数量、评分组合筛选更规范的做法是拆出民宿-设施关联表但这会把查询语句复杂化多数毕设并不会走到这一步。status用于民宿审核流业主提交后是 0管理员审核通过置 1违规下架置 2。这里要注意游客只能看到 status1 的民宿业主只能查到 owner_id 等于自己 id 的民宿列表查询接口必须把这些过滤条件带上。3.2 订单表与状态机设计订单是整个系统的核心数据载体也是最容易暴露设计缺陷的地方。很多毕设直接把订单表建成「用户 id 民宿 id 日期 金额」四件套缺少房型粒度也没考虑状态流转演示时一取消订单就乱了套。CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务单号, user_id BIGINT NOT NULL, room_type_id BIGINT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, nights INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status VARCHAR(20) NOT NULL COMMENT PENDING/PAID/CONFIRMED/CANCELLED/FINISHED, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) COMMENT 订单表;订单不直接关联民宿表而出关联到room_type_id因为同一个民宿下有多个房型不同房型价格、库存都不同下单最小粒度是「某个房型的某个日期段」。check_in_date和check_out_date用 DATE 而不是 DATETIME避免用户选日期时被时分秒干扰。order_no用业务单号而不是自增 id在支付回调、客户报障、对账时都能避免暴露订单量并且更好记。订单状态流转建议控制在五个值以内状态值含义允许流转PENDING待付款下单后锁定库存PAID、CANCELLEDPAID已支付CONFIRMED、CANCELLED退款CONFIRMED业主确认入住FINISHEDFINISHED已完成退房无CANCELLED已取消无待付款订单也占用库存否则会出现「用户下单未付款另一个用户同一日期订到同一房间」的问题。毕设里可以用定时任务把超过 30 分钟未支付的 PENDING 订单批量置为 CANCELLED释放库存。用户主动取消的订单同样要释放库存这个动作要放在删除或修改订单状态的事务里一起做。3.3 日期重叠判定为什么count和between都会出错民宿预定最关键的一段逻辑是同一个房型新订单的入住日期区间不能与已有有效订单重叠。新手最容易写错两个地方。第一种写成等值判断check_in_date #{date}这只处理了入住日期恰好相同的场景用户 8 月 1 日入住、8 月 3 日退房和已有订单 8 月 2 日入住、8 月 4 日退房完全重叠却查不出来。第二种用 BETWEEN只能覆盖单日落在区间内的情况两头都超出区间时会漏掉。正确的区间重叠判定条件是这样SELECT COUNT(*) FROM t_order WHERE room_type_id #{roomTypeId} AND status IN (PENDING, PAID, CONFIRMED) AND check_in_date #{newCheckOut} AND check_out_date #{newCheckIn}逻辑就是两条线段相交的数学条件已有订单的入住日要早于新订单的退房日已有订单的退房日要晚于新订单的入住日。两个条件同时成立说明日期段有交集。查出来的数量如果大于等于该房型的stock说明这个时段已经没有可用房间。状态只算 PENDING、PAID、CONFIRMEDCANCELLED 和 FINISHED 不占用库存——FINISHED 是退房完成之后的终态日期已经过去不需要参与可用性判断。举个例子验证已有订单是 8 月 1 日到 8 月 3 日新订单是 8 月 3 日到 8 月 5 日。check_in_date(8月1日) 8月5日成立check_out_date(8月3日) 8月3日不成立所以判定为不重叠。这符合民宿行业「同一天只允许一个订单入住退房当天可以再接新客」的惯例。如果业务上要求退房当天也不放房把第二个条件改成即可。3.4 超卖与并发事务、乐观锁和分布式锁的边界检查库存和插入订单必须放在同一个事务里否则两个并发请求同时读到剩余库存为 1两个都能通过校验就会超卖。这是 java 面试里事务隔离级别和锁机制最常见的问法也是答辩时老师一定会追问的点。实际的 Service 层是这样组织的Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { RoomType roomType roomTypeMapper.selectById(dto.getRoomTypeId()); if (roomType null) { throw new BizException(房型不存在); } int nights (int) ChronoUnit.DAYS.between(dto.getCheckInDate(), dto.getCheckOutDate()); if (nights 0) { throw new BizException(入住日期必须早于退房日期); } Integer occupied orderMapper.countOccupied( dto.getRoomTypeId(), dto.getCheckInDate(), dto.getCheckOutDate()); if (occupied roomType.getStock()) { throw new BizException(该时段已无空房); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setRoomTypeId(dto.getRoomTypeId()); order.setCheckInDate(dto.getCheckInDate()); order.setCheckOutDate(dto.getCheckOutDate()); order.setNights(nights); order.setTotalAmount(roomType.getPricePerNight() .multiply(BigDecimal.valueOf(nights))); order.setStatus(OrderStatus.PENDING); orderMapper.insert(order); return convertToVO(order); }Transactional保证 countOccupied 和 insert 要么一起成功要么一起回滚。rollbackFor Exception.class很关键Spring 默认只对 RuntimeException 回滚如果抛出的是受检异常事务不会回滚库存状态就错乱了。countOccupied对应 3.3 节那段重叠判定 SQL它查出来的是当前已经被占用的房型数量只有这个数量小于房型库存时才允许插入订单。事务解决了原子性但解决不了高并发下的「都查到库存为 1都通过校验」这类问题。毕设答辩中有两种可行的升级方案。第一种是给t_room_type加一个version整数字段更新时执行UPDATE t_room_type SET stock stock - 1, version version 1 WHERE id ? AND version ?受影响行数为 0 就说明房屋已被其他人预定这是乐观锁方案。第二种是用 Redis 的SETNX以房型 id 为 key 加锁释放时用 Lua 脚本删除这是分布式锁方案。对单机部署的毕设来说事务加乐观锁已经完全够用能在答辩时说出两种方案的适用边界就已经超出大多数同学的水平。4. Vue Spring Boot前后端分离登录、搜索与axios封装4.1 JWT登录与HandlerInterceptor拦截器前后端分离项目里Session 方案要处理跨域携带 Cookie、CSRF 等问题毕业设计里用 JWT 反而是实现成本最低、演示效果最直观的方案。用户登录成功后后端签发一个 token前端每次请求把它塞进请求头后端拦截器统一校验。PostMapping(/api/auth/login) public ResultString login(RequestBody LoginDTO dto) { User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (user null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BizException(用户名或密码错误); } String token JwtUtil.createToken(user.getId(), user.getRole()); return Result.success(token); }LambdaQueryWrapper是 MyBatis-Plus 提供的条件构造器.eq(User::getUsername, dto.getUsername())相当于WHERE username ?比手写 XML 少一步且类型安全。passwordEncoder是BCryptPasswordEncoder的实例注册时用encode加密登录时用matches校验这里不能把数据库里的密文拿出来和dto.getPassword()做 equals 比较因为 BCrypt 每次加密盐值都不同。拦截器侧的逻辑是放行/api/auth/login其余/api/**请求先取 Authorization 头解析 JWT把 userId 存到 Request attribute 或 ThreadLocal 里供 Controller 使用。游客浏览房源列表的接口不在拦截范围内但下单、收藏、修改密码这些接口必须登录。这里有个容易漏掉的细节登录接口返回的 token 中要带上角色字段后面判断「当前用户是不是这个民宿的业主」「是不是管理员」都靠它避免每查一次权限就要回表查一次用户。4.2 民宿搜索接口与MyBatis-Plus分页摘要里提到的「按目的地、入住日期、退房日期、人数搜索」是列表页核心接口。搜索列表不需要先做日期过滤把日期可用性放到房型详情接口里判断理由有两个列表页一次要返回一二十条民宿每条都去查订单表SQL 复杂且响应慢用户通常先看民宿条件再选日期详情页做日期校验已经足够。GetMapping(/api/homestay/search) public ResultPageHomestayVO search(HomestayQueryDTO query) { PageHomestay page new Page(query.getPage(), query.getSize()); LambdaQueryWrapperHomestay wrapper new LambdaQueryWrapper(); wrapper.eq(Homestay::getStatus, 1); if (StringUtils.hasText(query.getCity())) { wrapper.like(Homestay::getCity, query.getCity()); } if (query.getMinPrice() ! null) { wrapper.ge(Homestay::getMinPricePerNight, query.getMinPrice()); } if (query.getMaxPrice() ! null) { wrapper.le(Homestay::getMinPricePerNight, query.getMaxPrice()); } wrapper.orderByDesc(Homestay::getAvgRating); PageHomestay result homestayMapper.selectPage(page, wrapper); return Result.success(convertToVO(result)); }分页对象Page两个构造参数是页码和每页条数selectPage会自动拼接 LIMIT 并查出总记录数。ge和le对应和like做模糊匹配。城市筛选用like而不是eq因为传过来的可能是「杭州」也可能是「浙江杭州」这种带省市的表述模糊匹配容错率更高。前端传.size参数时要限制最大值比如限制到 50防止一次拉全表数据拖垮数据库。价格筛选设计在民宿表的min_price_per_night上而不是房型价格上是为了让列表页响应够快——列表只需要给出「这家民宿最低多少钱起步」真正精确价格留给房型维度计算。4.3 update-password.vue背后的axios拦截器update-password.vue.bak这个文件对应前端「修改密码」页面它背后依赖 axios 封装。页面里只负责表单校验和调接口所有带 token、处理统一错误的事情都收敛在 request 拦截器里。import axios from axios import { Message } from element-ui import { getToken, removeToken } from ./auth const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { if (getToken()) { config.headers[Authorization] getToken() } return config }) service.interceptors.response.use( res { if (res.data.code 200) { return res.data.data } Message.error(res.data.msg || 请求失败) return Promise.reject(new Error(res.data.msg)) }, err { if (err.response err.response.status 401) { removeToken() location.href /login } else { Message.error(err.message || 系统异常) } return Promise.reject(err) } ) export default service这段代码把公共逻辑收拢到两处请求拦截器负责给每一个请求自动附带 token响应拦截器负责统一处理错误码和 401 跳转。修改密码页面实际调用时只用关心业务数据流export function updatePassword(data) { return request({ url: /user/password, method: PUT, data }) }开发环境前后端端口不同需要配置代理。Vue 项目根目录的vue.config.js里加上这一段devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }/api开头的请求会被转发到http://localhost:8080浏览器里看到的请求地址始终是同源的不触发跨域也不需要后端额外配置 CORS。很多毕设前后端接口通了但浏览器控制台报 CORS 错误多半就是少了这个代理或后端没有放行跨域。前端用代理解决开发环境跨域生产环境则用 Nginx 把/api反向代理到后端服务这种部署方式在答辩时提一句会比「我直接改了后端允许所有跨域」专业得多。5. 从运行到答辩验证链路、金额精度与两个加分改造5.1 全链路验证顺序先把数据库建好执行项目里 sql 目录下的初始化脚本确认 t_user 里有测试账号、t_homestay 里至少有一条 status1 的民宿数据。然后执行 1-install.bat 装依赖再执行 2-run.bat 启动后端日志出现Tomcat started on port(s): 8080后启动前端。验证时不要跳着点按用户视角走完整条链路登录取得 token搜索城市民宿进入详情选房型选日期下单把订单状态从 PENDING 改成 PAID再到后台确认入住。每走一步看一次数据库对应记录确认 t_order 的 status、total_amount、nights 都在按预期变化。5.2 金额计算的两个细节订单金额计算里价格和总金额的字段类型必须用BigDecimal不能用 double。0.1 0.2在浮点数里等于0.30000000000000004金额差一分钱在演示时不容易发现但后续接支付平台对账必然出问题。nights用ChronoUnit.DAYS.between(checkInDate, checkOutDate)计算不要手写日期相减再除以 86400000前者会自动处理跨月和夏令时。下单日期校验时入住日期不能早于今天退房日期必须晚于入住日期这两个校验放在 Controller 层还是 Service 层我一般把它放在 Service 层入口做避免绕过 Controller 直接调 Service 的调用方式跳过校验。5.3 支付宝沙箱与乐观锁防重复支付演示完模拟支付后可以顺手做两个低成本改造这比增加新页面更能体现工程能力。第一把「确认支付」替换为支付宝沙箱支付。在支付宝开放平台申请一个沙箱应用后端增加两个接口一个创建支付订单并返回支付链接一个接收支付宝异步通知。核心代码只有两处验证签名的逻辑放在 Service 层入口Controller 只做参数绑定支付回调成功后执行orderService.paySuccess(orderNo)把状态从 PENDING 流转到 PAID 并释放对应支付单。第二给 t_order 增加version字段支付回调里用乐观锁更新状态int updated orderMapper.updateStatusWithVersion( orderNo, OrderStatus.PAID, OrderStatus.PENDING, version); if (updated 0) { throw new BizException(订单状态已变更请刷新后重试); }对应 SQL 是UPDATE t_order SET status #{newStatus}, version version 1 WHERE order_no #{orderNo} AND status #{oldStatus} AND version #{version}。受影响行数为 0 说明订单已经被并发处理过防止支付平台重复通知导致同一订单被结算两次。这套逻辑同样适用于「取消订单」和「确认入住」。不管换成微信支付还是云闪付验签和状态流转都放在 Service 层入口替换的只是支付通道实现类订单状态机本身不用动。本文还有配套的精品资源点击获取
返回列表