很多同学在做毕业设计时会选择“民宿预约系统”这个既有实用价值又能综合运用前后端技术的课题。但理想很丰满现实很骨感从零开始搭建一个能稳定运行、逻辑清晰的系统往往会遇到不少坑。今天我就结合自己的实践经验和大家聊聊如何用SpringBoot和Vue一步步构建一个健壮的民宿预约系统希望能帮你把毕业设计从“勉强能用”提升到“清晰可扩展”的水平。1. 毕业设计中的常见痛点与应对思路在动手之前我们先梳理一下同学们在做这类系统时最容易“翻车”的几个地方并给出对应的解决思路。状态管理混乱前端页面多了之后用户登录状态、房态信息、购物车数据在各个组件间传来传去代码很快就变得难以维护。应对Vuex是Vue的官方状态管理库对于中大型应用几乎是必需品。我们将用户信息、全局配置等存储在Vuex中组件通过mapState或mapGetters获取通过mapMutations或mapActions修改数据流清晰可控。预约冲突与超订这是核心业务痛点。想象一下房间A只剩1间用户甲和用户乙同时点击预订如果不加控制系统可能成功创建两个订单导致超售。应对这涉及到并发下的数据一致性问题。解决方案是“锁”“校验”。在业务层我们可以使用数据库的乐观锁如版本号或悲观锁SELECT ... FOR UPDATE但更优雅且高性能的做法是引入Redis分布式锁在创建订单的关键步骤前先获取锁。同时在数据库层面对库存字段使用UPDATE table SET stock stock - 1 WHERE id ? AND stock 0这类原子操作利用数据库的行锁保证最终一致性。数据一致性缺失用户下单后需要扣减库存、生成订单、记录日志。如果其中一步失败其他操作必须回滚否则数据就对不上了。应对利用Spring的声明式事务管理Transactional。在Service层的方法上添加该注解确保方法内的多个数据库操作在一个事务中要么全部成功要么全部回滚。这是保证业务原子性的基础。前后端联调低效后端接口还没写好前端就只能干等着或者接口字段一变前端到处报错应对前后端协商好API文档可以使用Swagger或YApi等工具并行开发。前端初期可以使用Mock.js模拟数据。更重要的是设计一套统一的RESTful风格响应体如包含code、data、message的Result对象并利用Axios的请求/响应拦截器统一处理错误、添加Token等能极大提升联调效率和代码健壮性。2. 技术选型背后的思考为什么用A而不用B这里简单对比一下关键选型。持久层MyBatis-Plus vs JPAMyBatis-Plus我们选择了它。原因在于它对国内开发者更友好尤其是其强大的QueryWrapper可以非常灵活地构建复杂查询条件并且内置了分页、代码生成器等实用功能能极大减少CRUD的模板代码。对于需要编写复杂SQL优化的场景它也支持直接编写XML映射文件灵活性更高。JPA优点是规范标准通过方法名就能自动生成查询在简单的CRUD上非常快。但在处理复杂动态查询时其Specification或QueryDSL的写法对初学者可能有一定门槛且复杂SQL的优化不如MyBatis直接。HTTP客户端Axios拦截器 vs 封装成Vue插件两者不冲突而是结合使用。Axios拦截器是核心我们用它来请求拦截器为每个请求自动添加Authorization: Bearer ${token}头。响应拦截器统一处理HTTP状态码如401跳转登录页、解析我们自定义的Result对象将业务错误提示给用户。在此基础上我们可以将配置好的Axios实例封装成一个Vue插件或直接挂载到Vue原型上Vue.prototype.$http axiosInstance这样在任何组件中都能通过this.$http.get(...)调用更加方便统一。3. 核心模块实现细节与代码示例我们来深入三个核心模块看看代码怎么写。模块一用户认证JWT Spring Security后端使用Spring Security整合JWT。核心是自定义一个JwtAuthenticationFilter放在过滤器链中用于解析请求头中的Token并设置认证信息。// UserController.java RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public ResultString login(RequestBody LoginForm form) { // 1. 校验用户名密码 User user userService.verifyPassword(form.getUsername(), form.getPassword()); // 2. 生成JWT Token (包含userId, username等信息) String token JwtUtil.generateToken(user.getId(), user.getUsername()); // 3. 返回Token return Result.success(token); } }// 一个简化的JwtUtil工具类关键方法 Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expiration}) private Long expiration; public String generateToken(Long userId, String username) { Date now new Date(); Date expiryDate new Date(now.getTime() expiration); return Jwts.builder() .setSubject(userId.toString()) .claim(username, username) // 存放自定义信息 .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // ... 其他解析、验证Token的方法 }前端登录后将返回的Token存入Vuex和localStorage并通过Axios拦截器附加到后续请求中。模块二房态管理与缓存民宿房间的状态是否可订、价格是高频查询数据适合用缓存。// RoomController.java RestController RequestMapping(/api/room) public class RoomController { Autowired private RoomService roomService; GetMapping(/{id}) public ResultRoomVO getRoomDetail(PathVariable Long id) { // 先查缓存缓存没有则查数据库并写入缓存 RoomVO room roomService.getRoomDetailWithCache(id); return Result.success(room); } }// RoomServiceImpl.java Service public class RoomServiceImpl implements RoomService { Autowired private RoomMapper roomMapper; Autowired private RedisTemplateString, Object redisTemplate; private static final String ROOM_CACHE_KEY_PREFIX room:detail:; Override public RoomVO getRoomDetailWithCache(Long roomId) { String key ROOM_CACHE_KEY_PREFIX roomId; // 1. 从Redis查询 RoomVO cachedRoom (RoomVO) redisTemplate.opsForValue().get(key); if (cachedRoom ! null) { return cachedRoom; } // 2. 缓存未命中查询数据库 RoomVO roomFromDB roomMapper.selectDetailById(roomId); if (roomFromDB ! null) { // 3. 写入Redis设置过期时间如30分钟 redisTemplate.opsForValue().set(key, roomFromDB, 30, TimeUnit.MINUTES); } return roomFromDB; } // 当房态信息更新时如后台修改价格需要清除或更新对应缓存 Override Transactional public boolean updateRoomInfo(Room room) { boolean success roomMapper.updateById(room) 0; if (success) { // 删除旧缓存下次查询会自动加载新的 redisTemplate.delete(ROOM_CACHE_KEY_PREFIX room.getId()); } return success; } }模块三预约创建分布式锁与事务这是最核心、最需要保证一致性的业务。// BookingServiceImpl.java Service public class BookingServiceImpl implements BookingService { Autowired private RoomMapper roomMapper; Autowired private BookingMapper bookingMapper; Autowired private RedisTemplateString, String redisTemplate; private static final String BOOKING_LOCK_PREFIX lock:booking:room:; Override Transactional(rollbackFor Exception.class) public Result createBooking(CreateBookingDTO dto, Long userId) { String lockKey BOOKING_LOCK_PREFIX dto.getRoomId(); String lockValue UUID.randomUUID().toString(); // 1. 尝试获取分布式锁超时时间3秒 Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { return Result.error(系统繁忙请稍后重试); } try { // 2. 在锁内再次检查房态并扣减库存防止超卖 Room room roomMapper.selectByIdForUpdate(dto.getRoomId()); // 使用悲观锁 if (room null || room.getStatus() ! 1) { return Result.error(房间不可用); } if (room.getAvailableStock() dto.getQuantity()) { return Result.error(库存不足); } // 原子性扣减库存 int updateCount roomMapper.decreaseStock(dto.getRoomId(), dto.getQuantity()); if (updateCount 0) { // 扣减失败通常意味着库存不足 return Result.error(库存不足请刷新后重试); } // 3. 创建订单记录 Booking booking new Booking(); booking.setUserId(userId); booking.setRoomId(dto.getRoomId()); booking.setQuantity(dto.getQuantity()); booking.setTotalAmount(room.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity()))); booking.setStatus(0); // 待支付 bookingMapper.insert(booking); // 4. 其他业务逻辑如发送创建成功通知等 // ... return Result.success(预约成功请尽快支付, booking.getId()); } finally { // 5. 确保释放锁使用Lua脚本保证原子性防止误删其他线程的锁 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), lockValue); } } }前端创建订单的Vue组件关键逻辑template div button clickhandleSubmit :disabledsubmitting提交订单/button /div /template script import { createBooking } from /api/booking; export default { data() { return { submitting: false, form: { roomId: null, quantity: 1, // ... 其他信息 } }; }, methods: { async handleSubmit() { if (this.submitting) return; this.submitting true; try { const res await createBooking(this.form); if (res.code 200) { this.$message.success(res.message); // 跳转到订单详情页或支付页 this.$router.push(/booking/detail/${res.data}); } else { this.$message.error(res.message); } } catch (error) { console.error(下单失败:, error); this.$message.error(网络异常请重试); } finally { this.submitting false; } } } }; /script4. 安全性考量与性能优化安全性XSS防护后端对用户输入的富文本内容如评论进行过滤如使用Jsoup前端在展示时使用v-html要谨慎或对内容进行转义。JWT过期与刷新Token过期后前端拦截到401响应应跳转登录页。可以实现一个“静默刷新”机制用Refresh Token存于HttpOnly的Cookie中更安全来获取新的Access Token提升用户体验。SQL注入坚持使用MyBatis的#{}预编译占位符切勿直接拼接SQL字符串。接口防刷对登录、发送验证码等接口使用Redis记录IP或用户频率进行限流。性能优化Redis缓存如上文房态缓存所示对热点数据房源列表、首页推荐等进行缓存。数据库优化为booking表的user_id、room_id、create_time等字段建立索引加快查询速度。接口限流与降级使用Guava RateLimiter或Sentinel对核心接口如查询、下单进行限流防止突发流量打垮服务。前端懒加载与异步组件Vue Router使用懒加载() import(/views/Booking.vue)大型组件使用defineAsyncComponent减少首屏加载体积。5. 生产环境部署避坑指南当你准备把项目部署到云服务器时这些坑可能正等着你跨域配置陷阱开发时用CrossOrigin注解或配置WebMvcConfigurer即可。但在生产环境如果用了Nginx反向代理更推荐在Nginx层面配置CORS减少后端压力。务必明确指定允许的源Origin、方法Methods和头Headers不要简单配置为*。MySQL时区问题应用服务器和数据库服务器的时区不一致会导致DATETIME类型字段显示错误。建议在JDBC连接URL中强制指定时区jdbc:mysql://localhost:3306/db?serverTimezoneAsia/ShanghaicharacterEncodingutf8。前端路由History模式404Vue Router使用history模式后直接访问非根路径如/booking/123会返回404。需要在Nginx或Apache配置中将所有前端路由重定向到index.html。location / { try_files $uri $uri/ /index.html; }配置文件泄露切勿将包含密码、密钥的application-prod.yml提交到Git。使用环境变量或配置中心如Nacos来管理敏感信息。日志与监控生产环境一定要配置日志如Logback按天或按大小滚动归档。有条件的可以接入ELK或PrometheusGrafana进行监控。总结与延伸思考通过以上步骤一个具备基本功能、考虑了一定并发安全性和性能的民宿预约系统原型就搭建起来了。这个项目作为毕业设计已经涵盖了从需求分析、技术选型、模块设计、编码实现到部署上线的完整流程足以体现你的综合能力。当然这只是一个起点。你可以在此基础上深入思考和实践如何扩展支持多商户这涉及到系统架构的升级。你可以引入“租户”概念在数据库表中增加tenant_id字段进行数据隔离或者使用独立的数据库/模式。后台需要增加商户入驻、管理等功能权限体系也会更复杂平台管理员 vs 商户管理员。动手实现‘短信验证码登录’这是一个很好的练手功能。你需要接入阿里云、腾讯云等短信服务SDK。设计一个/api/auth/sms-code接口发送验证码并将手机号-验证码对存入Redis设置短有效期如5分钟。设计一个/api/auth/login-by-sms接口校验Redis中的验证码成功后生成JWT Token。注意防刷对同一手机号发送验证码的频率进行限制。希望这篇笔记能为你扫清一些开发路上的障碍。编程最好的学习方式就是动手去做遇到问题解决问题。祝你毕业设计顺利写出让自己满意的代码