SpringBoot+Vue景区民宿预约系统:状态机设计与防超卖实战
做了好几个民宿预约类的管理系统后我发现一个规律绝大多数教程和毕业设计项目都在讲CRUD、讲页面美化但真正决定一个预约系统能不能用的是订单状态怎么流转、房间库存怎么防止超卖、日期区间怎么判断冲突。这些才是预约业务的地基也是面试时最容易暴露短板的地方。这个项目我用了SpringBoot Vue MySQL MyBatis这套经典组合做的是一个前后端分离的景区民宿预约系统。游客可以在前端按日期选房、下单、查看订单前台或管理员可以在后台确认入住、办理退房、管理民宿和房型数据。整套系统覆盖了从房源展示、日期筛选、创建订单到支付回调、状态流转的完整闭环。无论你是准备毕业设计答辩还是想找一个能往简历上写、能讲清楚业务逻辑的全栈项目都值得把这篇看完。我会从这套系统最核心的部分开始讲起包括功能边界怎么划、数据库表怎么设计、订单状态机怎么定义、后端接口怎么写才不容易出并发问题、前端页面怎么组织以及我实际开发中踩过的几个坑。1. 需求拆解民宿预约系统真正难的不是CRUD很多人在动手写这类系统时第一步就偏了。拿到需求后立刻建表、写接口、画页面结果做到一半发现订单状态乱套、库存对不上、日期判断出错推倒重来。做民宿预约系统先要把业务闭环想清楚再谈技术实现。1.1 三类用户和一条核心业务线景区民宿和普通酒店有点不一样。酒店的房间规格相对统一民宿则是一栋楼里可能有一间大床房、两间双床房、一间家庭套房每种的挂牌价和可订数量都不同。所以我的角色设计是游客前端用户浏览民宿列表、查看房型与价格、按日期查询可订房间、创建订单、模拟支付后查看订单状态。前台运营人员处理订单执行确认入住和退房操作管理游客的入住登记信息。管理员维护民宿基础信息、管理房型与库存、查看全部订单、处理退款请求。核心业务线只有一条游客选日期 → 查可用房型 → 创建订单 → 支付 → 到店办理入住 → 退房结算。所有功能都是围绕这条线展开的。1.2 功能边界哪些必须做哪些可以砍掉做课程设计或毕业设计最忌讳的就是功能堆砌。结合大多数景区民宿的实际管理场景我最终确定的功能清单是这样的功能模块具体功能优先级民宿与房型管理民宿信息增删改查、房型设置、价格设置、库存设置必须有日期查询与库存展示按入住/离店日期查询可订房型与剩余数量必须有在线预约下单选择房型、填写入住人信息、生成订单必须有支付模拟模拟支付成功回调改变订单状态必须有订单管理游客查看自己的订单、取消待支付订单必须有前台入住/退房操作确认入住、办理退房必须有数据统计今日入住数、本月营业额、房型热度加分项我项目里没有做真实的第三方支付对接支付环节用模拟回调替代。原因很简单真实支付需要商户号、回调验签、证书配置复杂度会翻好几倍但对预约业务本身的理解帮助不大。这个取舍在答辩时可以主动讲出来说明你清楚业务和技术之间的边界这反而是加分点。1.3 前后端分离的模块划分技术栈选的是SpringBoot 2.x Vue 2.xElement UI MySQL 5.7/8.0 MyBatis。前后端通过RESTful接口通信统一返回结构是{ code, message, data }。后端模块划分我按业务域而不是按技术层拆controller接收请求参数校验返回统一响应。service业务逻辑这是核心。订单创建、状态校验、库存判断都在这一层。mapperMyBatis的Mapper接口配合XML里的SQL语句。entity与数据库表对应的实体类。common统一返回结果、异常处理、工具类。前端按页面划分游客端民宿列表页、民宿详情页含房型展示、创建订单页、订单列表页。管理端登录页、Dashboard、民宿管理页、房型管理页、订单管理页。提示前后端分离项目跨域问题一定要处理。我在后端加了一个CorsConfig配置类允许指定前端的地址跨域访问。测试时如果前端请求报跨域错误优先检查这里。2. 数据库设计订单状态机的建模是关键数据库设计直接决定后面写代码的顺畅程度。民宿预约系统的表结构不少我挑最核心的几张表展开讲。2.1 数据表结构与字段说明用户表user用户表比较简单存账号、密码、手机号、角色。密码我用的MD5加盐存储实际商用会换成BCrypt但这个项目里MD5加盐足够说明你对安全的意识。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 密码(MD5加盐), phone VARCHAR(20) DEFAULT NULL, role TINYINT DEFAULT 1 COMMENT 角色: 0-管理员 1-游客, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB;民宿表homestay和房型表room_type民宿表存景区信息、地址、简介、封面图。房型表挂在民宿下面存房型名称、挂牌价、总库存比如大床房¥388/晚共5间。CREATE TABLE homestay ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, address VARCHAR(200) DEFAULT NULL, description TEXT, cover_url VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1-营业 0-停业, PRIMARY KEY (id) ) ENGINEInnoDB; CREATE TABLE room_type ( id INT NOT NULL AUTO_INCREMENT, homestay_id INT NOT NULL, type_name VARCHAR(50) NOT NULL COMMENT 如: 大床房/双床房, price DECIMAL(10,2) NOT NULL COMMENT 每晚价格, total_count INT NOT NULL COMMENT 该类型的房间总数, PRIMARY KEY (id) ) ENGINEInnoDB;订单表orders这是全系统最核心的表字段设计直接反映业务理解深度。CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id INT NOT NULL COMMENT 下单游客ID, room_type_id INT NOT NULL COMMENT 预订的房型ID, check_in_date DATE NOT NULL COMMENT 入住日期, check_out_date DATE NOT NULL COMMENT 离店日期, nights INT NOT NULL COMMENT 入住晚数, guest_name VARCHAR(50) NOT NULL COMMENT 入住人姓名, guest_phone VARCHAR(20) NOT NULL COMMENT 入住人电话, total_price DECIMAL(10,2) NOT NULL COMMENT 订单总价, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态: 0-待支付 1-已支付 2-已入住 3-已退房 4-已取消 5-退款中, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_room_type_date (room_type_id, check_in_date, check_out_date) ) ENGINEInnoDB;订单编号我生成规则是yyyyMMddHHmmss 4位随机数再用唯一索引兜底。为什么不用自增ID当订单号因为订单号会出现在前端、短信、对账单里连续自增的ID容易暴露平台单量而且从业务上订单号本就应该具备一定随机性。2.2 状态机设计为什么推荐用数字状态而非字符串订单状态是整个系统的灵魂。我见过不少项目用字符串状态比如pending、paid、checked_in好处是直观坏处是扩展和判断麻烦写SQL时还得注意大小写。我用的是数字状态枚举在Java里定义一个枚举来承载状态和状态流转规则public enum OrderStatus { PENDING_PAY(0, 待支付), PAID(1, 已支付), CHECKED_IN(2, 已入住), CHECKED_OUT(3, 已退房), CANCELLED(4, 已取消), REFUNDING(5, 退款中); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public static OrderStatus fromCode(int code) { for (OrderStatus status : values()) { if (status.code code) { return status; } } throw new IllegalArgumentException(未知状态: code); } }状态流转规则我用一张表明确控制绝不允许订单状态随意跳转当前状态允许执行的操作目标状态待支付0支付、取消已支付1、已取消4已支付1办理入住、申请退款已入住2、退款中5已入住2办理退房已退房3退款中5同意退款管理员已取消4已退房3无终态已取消4无终态这个状态机在代码层面我用一个transition方法统一校验public OrderStatus transition(OrderStatus target) { switch (this) { case PENDING_PAY: if (target PAID || target CANCELLED) return target; break; case PAID: if (target CHECKED_IN || target REFUNDING) return target; break; case CHECKED_IN: if (target CHECKED_OUT) return target; break; case REFUNDING: if (target CANCELLED) return target; break; default: break; } throw new IllegalStateException(非法状态流转: this - target); }提示订单状态变更必须走统一的Service方法比如updateStatus(orderId, targetStatus)在里面先判断当前状态再更新。禁止到处直接写UPDATE语句改状态否则状态机形同虚设。2.3 日期重叠与房间占用判断预约系统的宿命是处理日期冲突。游客入住时间是连续的判断某个房型在某个区间是否可订其实就是判断这个区间内是否每一天都有余房。实现思路有两种思路一库存扣减型。room_type表里存总库存下单成功就扣减库存退房/取消就加回库存。这种方式适合房型数量不多、按房型售卖的场景实现简单但要注意并发和状态流转时的库存回补。思路二占用明细型。单独建一张room_occupied表每成功一单就插入一条占用记录。查询时统计对应时间内的占用数量再用总库存减去占用数量得到剩余可订量。这种方式更灵活能支撑哪几天满房的日历展示。我最终选了思路一作为主逻辑因为代码量少、清晰。但在可订房型展示这个接口上我用了思路一的变体查询某日期区间内的有效订单按房型分组统计已占用的房间数用总库存减掉它。SELECT room_type_id, SUM(nights) AS occupied_nights FROM orders WHERE status IN (1, 2) -- 已支付和已入住才占用房间 AND check_in_date #{checkOutDate} AND check_out_date #{checkInDate} GROUP BY room_type_id;这个SQL里的日期判断条件值得我们仔细看一遍check_in_date #{checkOutDate} AND check_out_date #{checkInDate}这是判断两个区间是否重叠的标准写法。假设一个订单是6月1日入住、6月3日离店占6月1日、2日两晚你查6月2日入住、6月4日离店是否可订订单的 check_in_date6.1 查询的 check_out_date6.4→ 成立订单的 check_out_date6.3 查询的 check_in_date6.2→ 成立两条都为true说明重叠这个房型在6月2日不可订。反之如果查6月3日入住新订单check_in6.36.1 6.3 成立6.3 6.3 不成立相等说明前一个订单刚好退房不冲突所以边界当天退房、当天入住是允许的正好符合民宿的惯例退房日房间腾出来新客人当天下午入住。3. 后端核心接口下单与状态流转的完整实现数据库设计好了后端最重要的就是两块创建订单时怎么保证数据不出错状态变更时怎么保证流程不乱。这两个接口写明白了其余接口都是常规增删改查。3.1 创建订单接口事务、校验、库存三步缺一不可创建订单的接口路径我定义为POST /api/orders参数包含房型ID、入住日期、离店日期、入住人姓名、入住人电话。Service层的核心逻辑有五步校验房型存在且民宿营业中。校验日期合法性入住日期不能早于今天离店日期必须晚于入住日期。查询该房型在目标区间内是否还有余房。计算总价(离店日期 - 入住日期) × 每晚价格。生成订单号插入订单记录。第3步是最容易出问题的地方。我写了一个独立的可用库存方法public boolean isRoomAvailable(Integer roomTypeId, LocalDate checkIn, LocalDate checkOut) { // 查询该房型的有效订单占用记录 ListOrder occupiedOrders orderMapper.selectOccupiedOrders(roomTypeId, checkIn, checkOut); int occupiedCount 0; for (Order order : occupiedOrders) { // 简单处理一单占用一间房。如果订单横跨整个查询区间就计为1。 occupiedCount 1; } RoomType roomType roomTypeMapper.selectById(roomTypeId); return occupiedCount roomType.getTotalCount(); }这里我按一单占一间房处理因为系统里每种房型的订单默认对应一间房。如果你的业务支持同一订单订多间那要引入room_count字段把这部分数量加进去。然后整个方法加上TransactionalTransactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto) { // 1. 校验房型 RoomType roomType roomTypeMapper.selectById(dto.getRoomTypeId()); if (roomType null) { throw new BizException(500, 房型不存在); } // 2. 校验日期 if (dto.getCheckInDate().isBefore(LocalDate.now())) { throw new BizException(400, 入住日期不能早于今天); } if (!dto.getCheckOutDate().isAfter(dto.getCheckInDate())) { throw new BizException(400, 离店日期必须晚于入住日期); } // 3. 校验库存 if (!isRoomAvailable(dto.getRoomTypeId(), dto.getCheckInDate(), dto.getCheckOutDate())) { throw new BizException(500, 该日期区间已满房); } // 4. 计算金额 long nights ChronoUnit.DAYS.between(dto.getCheckInDate(), dto.getCheckOutDate()); BigDecimal totalPrice roomType.getPrice().multiply(new BigDecimal(nights)); // 5. 生成订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setRoomTypeId(roomType.getId()); order.setCheckInDate(dto.getCheckInDate()); order.setCheckOutDate(dto.getCheckOutDate()); order.setNights((int) nights); order.setGuestName(dto.getGuestName()); order.setGuestPhone(dto.getGuestPhone()); order.setTotalPrice(totalPrice); order.setStatus(OrderStatus.PENDING_PAY.getCode()); orderMapper.insert(order); return order; }Transactional保证第5步插入订单时如果出异常前面的任何写操作都回滚。特别注意默认情况下事务只对RuntimeException回滚所以我把异常定义成BizException这个RuntimeException的子类并且显式声明rollbackFor Exception.class防止以后有人抛受检异常导致不回滚。提示有经验的面试官会追一个问题——如果下单时不加锁两个并发请求同时查到有余房怎么办这个问题在文章第5节专门展开讲这里先留个悬念。3.2 状态变更接口同一个方法里做校验和更新支付回调、取消订单、确认入住、退房、退款本质上都是同一个动作把订单从状态A改成状态B。我封装了一个统一方法public void changeOrderStatus(Long orderId, OrderStatus targetStatus) { Order order orderMapper.selectById(orderId); if (order null) { throw new BizException(404, 订单不存在); } OrderStatus currentStatus OrderStatus.fromCode(order.getStatus()); OrderStatus newStatus currentStatus.transition(targetStatus); orderMapper.updateStatus(orderId, newStatus.getCode()); }支付回调我单独写了一个payCallback方法模拟第三方支付结果通知。实际的回调接口会包含签名验签、幂等处理这里简化为校验订单存在、状态为待支付然后调用changeOrderStatus(orderId, PAID)。取消订单有一个细节待支付状态可以随便取消但已支付订单要取消必须走退款流程。所以前端的取消订单按钮只有在订单状态为待支付时才显示已支付订单如果要取消要调退款申请接口。3.3 MyBatis中那些容易忽略的配置与SQL写法项目里我用的是XML方式写Mapper相比注解XML能处理更复杂的动态SQL排查问题也更方便。几个容易踩的配置点单独拎出来说。驼峰映射。数据库字段check_in_date对应Java属性checkInDate很多新手在这里踩坑结果查出全是null。在application.yml里一定要开启mybatis: configuration: map-underscore-to-camel-case: true动态SQL用set标签。更新订单状态时只更新需要的字段避免误伤。用set标签能把字段列表拼装中多余的逗号自动去掉update idupdateStatus UPDATE orders set if teststatus ! nullstatus #{status},/if if testupdateTime ! nullupdate_time #{updateTime},/if /set WHERE id #{id} /update时间段条件查询用where标签select idselectOrdersByCondition resultTypecom.example.entity.Order SELECT * FROM orders where if testuserId ! nullAND user_id #{userId}/if if teststatus ! nullAND status #{status}/if if testcheckInDate ! nullAND check_in_date gt; #{checkInDate}/if if testcheckOutDate ! nullAND check_out_date lt; #{checkOutDate}/if /where ORDER BY create_time DESC /select注意XML里和要转义成gt;和lt;否则XML解析直接报错。这个坑我当年排了半天。还有一个细节MyBatis的一级缓存默认是开启的且作用域是SqlSession。Spring整合后每次数据库操作都是独立开启和关闭SqlSession的所以一级缓存一般不会造成脏读。但如果你在同一个事务里先查询订单、再修改订单状态、再查询订单第二次查询可能会命中一级缓存拿到旧数据。解决办法是在修改后手动调用sqlSession.clearCache()或者干脆在测试时禁止一级缓存。这个在开发调试阶段特别容易让人困惑。4. 前端Vue页面与预约流程体验设计前端这块我用的是Vue 2.6 Element UI Vue Router Axios。页面逻辑不复杂但有几个地方比较考验设计4.1 页面路由结构与权限控制路由分两块游客端和管理端。游客端不需要登录就能浏览民宿列表下单前要求登录管理端必须登录且角色为管理员才能访问。const router new VueRouter({ routes: [ { path: /, component: Home, meta: { title: 民宿列表 } }, { path: /homestay/:id, component: HomestayDetail, meta: { title: 民宿详情 } }, { path: /order/create, component: OrderCreate, meta: { requiresAuth: true, title: 下单 } }, { path: /order/list, component: OrderList, meta: { requiresAuth: true, title: 我的订单 } }, { path: /login, component: Login, meta: { title: 登录 } }, { path: /admin, component: AdminLayout, meta: { requiresAuth: true, requiresAdmin: true }, children: [ { path: dashboard, component: Dashboard }, { path: homestay, component: AdminHomestay }, { path: orders, component: AdminOrders } ] } ] });路由守卫里做权限控制核心代码router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); return; } if (to.meta.requiresAdmin) { const role localStorage.getItem(role); if (role ! 0) { next({ path: / }); return; } } next(); });提示前端的权限控制只能决定显示不显示真正的安全校验必须在后端做。后端的接口要判断登录用户的角色不能让游客直接调用管理接口。我后端的做法是写了一个简单的拦截器校验请求头里的Token再通过Token解析出UserId和Role。4.2 选日期与可订房型联动民宿详情页是整个前端交互最核心的页面。游客先选择入住日期和离店日期页面会实时请求后端查询该区间内有哪些房型可订、剩余几间。这里用到了Element UI的日期范围选择器并做了两个限制今天之前的日期不可选。离店日期必须在入住日期之后。联动查询的接口是GET /api/room-types/available?homestayIdxxcheckIn2025-06-01checkOut2025-06-03。返回的数据结构[ { roomTypeId: 1, typeName: 大床房, price: 388.00, remaining: 3 }, { roomTypeId: 2, typeName: 双床房, price: 458.00, remaining: 0 } ]剩余为0的房型前端要置灰并在卡片上显示已满房。这个交互细节很加分因为它比用户提交后才提示无房体验好太多。4.3 Axios封装与统一错误处理请求封装我做了统一处理核心是把Token自动加进请求头把后端的code ! 200时弹出提示把401状态跳转到登录页service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { this.$message.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); this.$router.push(/login); } return Promise.reject(error); } );4.4 订单列表与状态按钮的显隐控制订单列表页有个很容易被忽视的细节不同状态的订单可操作按钮完全不同。下拉框里待支付订单显示去支付和取消订单已支付订单显示申请退款已入住订单显示确认退房这一步通常由前台操作但为了演示方便我在模拟环境里也开放给用户。前端用v-if按状态控制按钮组div v-iforder.status 0 el-button typeprimary clickhandlePay(order)去支付/el-button el-button clickhandleCancel(order)取消订单/el-button /div div v-else-iforder.status 1 el-button clickhandleRefund(order)申请退款/el-button /div div v-else-iforder.status 2 el-button typewarning clickhandleCheckOut(order)办理退房/el-button /div5. 实测中踩过的坑并发超卖、日期边界与缓存脏读做这类系统跑通正常流程不算本事真正考验人的是那些偶然出现的问题。我把实际开发中遇到过的四个问题完整记录在这里这些问题在答辩时讲出来会非常加分。5.1 并发下单导致超卖不加锁的库存校验形同虚设问题是这么暴露的我写了一个压测脚本模拟两个游客同时对只剩最后一间的房型下单。结果两个订单都创建成功但房型只有一间。原因不难理解两个请求并发进入都先执行isRoomAvailable()查询发现剩余1间满足条件然后各自插入订单。两个插入都成功但库存逻辑上已经被占用了2间。解决方案用MySQL的SELECT ... FOR UPDATE对房型记录加悲观锁。在事务里查询房型时锁定这一行其他事务必须等当前事务提交后才能继续查。select idselectByIdForUpdate resultTypecom.example.entity.RoomType SELECT * FROM room_type WHERE id #{id} FOR UPDATE /select然后在createOrder方法里把查房型语句换成这个加锁版本RoomType roomType roomTypeMapper.selectByIdForUpdate(dto.getRoomTypeId());注意加悲观锁的前提是createOrder方法里有Transactional因为锁要持有到事务提交才释放。如果方法没有事务FOR UPDATE的锁会在语句执行完就释放等于白加。有锁之后的效果第一个请求查到房型并加锁第二个请求的selectByIdForUpdate会阻塞等第一个事务提交后才继续此时它再查库存发现已经不足抛出该日期区间已满房。提示悲观锁适合并发量不高的管理类系统实现简单、逻辑直观。如果日订单量上了几千再考虑把库存放到Redis里用Lua脚本做原子扣减或者在数据库加乐观锁版本号。做课程设计讲清楚悲观锁的原理和适用场景就够了。5.2 日期区间重叠判断写反了边界日期的处理讲究最初我写日期重叠判断时用的是check_in_date #{checkOutDate} AND check_out_date #{checkInDate}。这个写法边界很尴尬某订单当日退房新游客当日入住本应该允许但这个判断会判定两个区间重叠导致房间被误判为不可订。我之前解释过退房日的房间当天会腾出来所以重叠判断的边界应该是左闭右开——订单占用的日期区间是[check_in_date, check_out_date)即包含入住日不包含离店日。对应SQL就是check_in_date #{checkOutDate} AND check_out_date #{checkInDate}这个细节特别适合在答辩时讲——能讲清楚边界日期的处理证明你真的理解业务而不是只会照着教程敲代码。5.3 MyBatis一级缓存导致的订单状态不刷新有段时间测试人员反馈一个诡异问题订单支付后再查订单详情状态还是待支付。反复刷新都不变。最后定位到原因在同一个SqlSession内先查了订单状态0然后执行支付回调把状态改成1再查订单时命中一级缓存拿到的还是状态0的旧对象。Spring整合MyBatis后每次Mapper操作通常是用新的SqlSession执行的所以这个问题正常开发中很少爆发。但在同一个事务方法里如果你先selectById再执行updateStatus再selectById第二次查询就会拿到缓存里的旧数据。解决办法在更新后调用SqlSession.clearCache()强迫下一次查询走数据库。把更新和查询拆到不同事务里或者避免在同一事务内改后再查。在开发环境中配置MyBatislocal-cache-scope: statement禁用一级缓存方便尽快暴露问题。我个人建议第2种思路事务方法的职责要单一。更新状态就只更新状态返回void调用方如果还需要展示最新订单就再走一次独立的查询。职责分离能避免很多隐蔽问题。5.4 MySQL时区导致的时间偏差另一个很隐蔽的问题是时间。我在本机测试时一切正常部署到云服务器后订单创建时间比本地时间慢了8个小时。排查后发现是MySQL连接串没指定时区而服务器的系统时区和MySQL默认时区不一致。解决办法是在JDBC连接串里显式指定spring: datasource: url: jdbc:mysql://localhost:3306/homestay_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai这个问题本身不复杂但排查过程可能很费劲因为这个慢8小时的现象会让你下意识怀疑是代码逻辑问题而不会想到是数据库连接的时区配置。6. 从零搭建环境到跑通演示一份可直接照做的清单最后整理一份我从零到跑通的完整清单方便你复现也方便你在答辩前快速准备演示环境。6.1 环境准备与项目初始化后端环境JDK 1.8、Maven 3.6、MySQL 5.7、IDEA。前端环境Node.js 14、Vue CLI 4.x。MySQL侧的操作CREATE DATABASE homestay_db DEFAULT CHARACTER SET utf8mb4;然后导入项目的schema.sql里面包含上面设计的全部表和若干演示数据。我通常会预置以下演示数据管理员账号admin / admin123游客账号zhangsan / 123456两到三家景区周边民宿每家2~3种房型价格有梯度方便演示价格计算。6.2 后端启动与常见配置application.yml核心配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/homestay_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true启动后端在项目根目录执行mvn spring-boot:run或者先把项目打成jar包mvn clean package -DskipTests java -jar target/homestay-1.0.0.jar6.3 前端启动与部署前端项目在frontend目录下先装依赖再启动开发服务器npm install --registryhttps://registry.npmmirror.com npm run serve开发环境访问http://localhost:8081Vue CLI默认端口是8080和后端冲突的话要改一下vue.config.js里的devServer.port为8081。这里有个重点开发环境下前端请求后端的地址要配置代理或者在后端开启CORS。我两种方式都试过最终选择在后端加CORS全局配置因为我喜欢把最终演示打包成单体应用。演示时的最优部署方案用npm run build把Vue项目打包成静态文件然后把dist目录下的所有文件复制到SpringBoot项目的src/main/resources/static目录下。重新打包SpringBoot项目后浏览器访问http://localhost:8080就能直接看到前端页面接口走同源请求没有跨域问题演示时只需要启动一个Java进程干净利落。6.4 演示流程脚本答辩或演示时建议按这条流程走逻辑最顺游客注册/登录浏览民宿列表。进入某民宿详情页选择未来两天日期展示可订房型。选择房型填写入住人信息提交订单。在我的订单里看到待支付订单模拟支付后状态变为已支付。管理员登录后台看到这笔订单将状态置为已入住。前台办理退房订单状态变为已退房。在Dashboard里看到今日入住数、本月营业额的统计变化。这套流程覆盖了全部核心功能也能把状态机、库存判断、权限控制这些技术点讲清楚。做完这个项目后我最大的体会是预约类系统的复杂度不在页面多不多、接口多不多而在状态是否收敛、数据在并发下是否一致。状态机的设计与实现直接决定了系统上线后运维时会不会焦头烂额。如果你后续想在这个项目上继续扩展我建议优先加两样真实支付对接学习回调验签和幂等处理以及用Redis做热点房型的库存扣减学习生产级并发控制。把这两块吃透这个项目从课程设计到可上线的小系统之间的距离就真正拉近了。

相关新闻

AnyPS5实战:PS5存档备份、手柄映射与游戏库管理完整指南

AnyPS5实战:PS5存档备份、手柄映射与游戏库管理完整指南

AnyPS5这东西,我是在一次整理外接硬盘时偶然翻到的。当时手头刚好有几十个游戏截图和若干份残缺的存档备份要处理,官方那套云端同步又对我的网络不太友好,折腾了几次后就开始找本地化的管理方案。试过几个社区里流传的工具,大多数…

2026/10/11 6:18:12 阅读更多 →
SpringBoot+SSM眼科患者随访管理系统设计与实战

SpringBoot+SSM眼科患者随访管理系统设计与实战

做毕设选眼科患者随访管理系统的同学真的不少,最近我也前前后后帮人看了好几套基于Java生态的这类项目。说实话,这个题目选得很聪明——业务场景明确,不算复杂到失控,又能把增删改查、权限控制、定时任务、报表统计这些后端技术点…

2026/10/11 6:18:12 阅读更多 →
Android11预装v2签名system/app无法正常运行

Android11预装v2签名system/app无法正常运行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 6:18:12 阅读更多 →

最新新闻

安卓分屏自动左滑脚本:Auto.js+ADB实现3秒滑动挂机

安卓分屏自动左滑脚本:Auto.js+ADB实现3秒滑动挂机

先说个真实场景:我手机上装了某短视频极速版,每天要做签到和刷视频任务,手得一遍遍地左滑,滑到拇指发酸。后来我寻思,这滑动动作完全可以用脚本代替。于是就有了标题里这个功能——“循环3秒左滑-支持屏幕上下分屏”。…

2026/10/11 7:04:36 阅读更多 →
PS5控制器协议解析与Linux驱动开发指南

PS5控制器协议解析与Linux驱动开发指南

我无法基于当前输入生成符合要求的博文。原因如下:项目标题 "AnyPS5" 缺乏有效上下文:该标题本身是一个模糊的命名组合(可能指向某款非官方PS5相关工具、模拟器、兼容层、硬件方案或社区项目),但未提供任何可…

2026/10/11 7:04:36 阅读更多 →
C盘空间不足?用Codex定位并清理AppData 87.81GB的完整方案

C盘空间不足?用Codex定位并清理AppData 87.81GB的完整方案

C盘爆红这种事,经历过一次就再也不想经历。我前两天正写着代码,系统突然弹窗提示 C 盘空间不足,磁盘剩余居然只剩不到 1GB。习惯性点开“此电脑”,看到 C 盘那一整条红色,第一反应是打开应用列表准备卸载软件&#xff…

2026/10/11 7:04:36 阅读更多 →
从传统编辑器到Cursor:AI编程实战与智能重构经验总结

从传统编辑器到Cursor:AI编程实战与智能重构经验总结

1. 为什么我最终把主力编辑器换成了 Cursor先说结论:我不是因为“AI 编辑器”这个概念火才换的,而是因为一次真实的项目重构把我逼到了墙角。当时手上有一个跨平台的后端服务,代码量大概四万多行,涉及三个语言栈,历史遗…

2026/10/11 7:04:36 阅读更多 →
Simulink搭建魔术公式轮胎模型:纵向、侧向及综合滑移工况详解

Simulink搭建魔术公式轮胎模型:纵向、侧向及综合滑移工况详解

做车辆动力学仿真的朋友,十有八九都绕不开轮胎模型。我最初把整车动力学模型跑起来时,最头疼的就是轮胎力算不准——明明车辆动力学方程写得没问题,但一到极限工况,侧偏特性就对不上,跑出来的横摆角速度曲线像过山车。…

2026/10/11 7:04:36 阅读更多 →
Ionic Icon 图标完全指南:从基础用法到实战技巧

Ionic Icon 图标完全指南:从基础用法到实战技巧

1. 引言在移动应用和 Web 应用开发中,图标是界面设计中不可或缺的元素。Ionic 框架内置了一套功能强大、风格统一的图标库——Ionicons,它包含了 1300 个精心设计的图标,覆盖了日常开发中绝大部分场景。本文将带你全面了解 Ionic Icon 的使用…

2026/10/11 7:03:35 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →