1. 项目概述手记做民宿房源预订网站这几年算是个非常典型的全栈练手项目同时也是很多毕业设计、个人作品集里的常客。市面上类似的系统不少但大多数要么只停留在管理后台要么前端拿模板硬套真正能做到前后端分离、逻辑自洽、能跑通完整预订流程的其实并不多。这个项目标题写得很直白Spring Boot Java 民宿房源预订网站 Vue。它本质上是一个前后端分离的Web应用后端用Spring Boot提供RESTful API前端用Vue框架构建用户界面。业务上覆盖了民宿管理中常见的房型展示、搜索筛选、在线预订、订单管理、用户登录注册、后台管理等功能。我来拆一下这个项目解决的核心问题房东需要一套工具管理房源信息、房态日历、订单流水游客需要一个直观的界面浏览房源、对比价格、完成预订。传统民宿平台类似某短租App网站端的简化版需要打通这两端核心是数据和状态的一致性——尤其房态和订单状态不能出现同一间房被重复预订的尴尬。这个项目适合谁三类人刚开始接触前后端分离开发的学习者想通过一个完整业务项目把Spring Boot和Vue串起来。需要交毕业设计或面试作品的在校生需要一个功能完备、代码结构清晰的系统。中小型民宿运营者想自建一套轻量级预订系统省掉平台抽成用这个项目做原型再二次开发。我自己把这个项目完整搭过一遍连数据库、接口、前端页面到部署都跑通了下面把整个实现过程、踩过的坑、还有我认为比常规实现做得更好的一些细节一次性写清楚。2. 技术选型为什么是Spring Boot Vue这个组合2.1 后端框架Spring Boot为什么是首选Spring Boot在这个项目里的地位不用多讲它是当前Java后端开发事实上的标准。比起传统的Spring Spring MVC XML配置时代Boot最大的贡献是“约定优于配置”内嵌Tomcat容器一个spring-boot-starter-web依赖就能起步。在民宿预订网站这个业务场景里Spring Boot的优势体现在几块快速搭建RESTful APIRestControllerRequestMapping的方式写接口非常直观配合Validated做参数校验接口层很干净。数据持久化生态成熟无论是用Spring Data JPA还是MyBatis-Plus都能快速完成数据层开发。我最终选的是MyBatis-Plus后面会详细说原因。安全与认证Spring Security JWT的方案很成熟做登录鉴权、权限控制比从零手写Session管理省事得多。这个项目里用户角色分游客、房东管理员、平台运营三种用JWT做无状态认证非常适合前后端分离架构。事务管理订单创建的连环操作校验房态、生成订单、扣减库存、更新日历必须在一个事务里完成Spring的Transactional注解让我不用操心事务边界。2.2 前端框架Vue到底选Vue 2还是Vue 3Vue这块有一个常见的纠结选Vue 2还是Vue 3我的建议是直接选Vue 3 Vite Element Plus。原因很直接。Vue 3的Composition API也就是script setup语法在逻辑组织上比Vue 2的Options API清爽太多。民宿预订网站的页面交互最多的是什么是“搜索条件联动”“房态日历选择”“订单状态流转”。这些逻辑天然适合用ref、reactive、computed去组织而不是把一个页面的所有data、methods、computed堆在一起。另外Element Plus组件库配合Vue 3非常丝滑表格、表单、日期选择器、对话框这些后台管理页面常用的组件全都有现成的实现。Vite做开发服务器热更新极快比老一代Webpack dev server能节省大量等待时间。需要提一句的是如果最终部署环境比较老比如服务器上的Node版本还在12以下Vue 3 Vite可能会遇到构建兼容问题这时候退而求其次用Vue 2 Vue CLI也不是不行但我不太推荐——新项目没必要开历史的倒车。2.3 数据库选型MySQL Redis的经典组合数据存储这一层我选择的MySQL Redis组合在大多数同类项目中通用性极强。MySQL负责持久化主数据用户表、房源表、房型表、订单表、评价表、操作日志表等。民宿预订业务的核心矛盾是“房态记录”与“订单记录”的一致性MySQL事务能保证这一点。Redis负责缓存和临时状态热门房源列表、首页推荐的缓存Token的黑名单以及订单创建时的分布式锁。虽然单机部署情况下不加Redis也能跑但加上之后接口响应速度差别明显。尤其房态库存这类读多写少的数据Redis缓存能显著降低数据库压力。还有一点需要说明MySQL版本建议用5.7以上字符集统一用utf8mb4因为房源描述、民宿名称这些字段会用到生僻字符或emojiutf8mb4才能正确存储。2.4 前后端联调环境与接口文档前后端分离开发最怕的就是接口对齐扯皮。我在项目里引入了两样东西提升协作效率。第一是knife4j基于Swagger的增强文档工具后端接口定义好之后自动生成API文档前端同学直接对着文档就能知道请求参数和响应结构不用翻代码。它的界面比原生Swagger UI好看还支持接口调试。第二是统一的响应体封装。所有接口返回结构保持一致{ code: 200, message: success, data: { } }前端封装一个request.js工具模块统一处理HTTP状态码、业务错误码、Token过期这些逻辑不用在每一个页面里分别处理异常。这个约定要在一开始就定好否则项目做到一半再改返工成本很高。3. 数据库设计民宿预订系统的核心表结构3.1 用户与权限设计用户表是这个系统的基础设计上需要区分三种角色游客、房东、管理员。CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像URL, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色1-游客 2-房东 3-管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-正常 0-禁用, 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_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这里有一个细节明明可以用username做逻辑主键我仍然坚持用自增id做物理主键。原因是用户表会被订单表、评论表多处关联引用用自增整数做外键关联更高效而且用户名是不允许修改的约束条件如果未来改需求允许改用户名那么作为主键就很麻烦。密码加密用的是BCryptPasswordEncoder不是MD5。原因不用多说MD5撞库太容易了BCrypt加盐哈希的安全级别适合生产。3.2 房源与房型设计房源和房型是民宿业务的两层概念。一个房源House属于一个房东用户但一个房源可以有多个房型RoomType比如同一个小院有“大床房”“双床房”“豪华套房”三种可供预订的房型。CREATE TABLE house ( id BIGINT NOT NULL AUTO_INCREMENT, owner_id BIGINT NOT NULL COMMENT 房东用户ID, name VARCHAR(100) NOT NULL COMMENT 房源名称, address VARCHAR(255) NOT NULL COMMENT 地址, city VARCHAR(50) NOT NULL COMMENT 所在城市, description TEXT COMMENT 房源描述, cover_image VARCHAR(255) COMMENT 封面图URL, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-上架 0-下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_city (city) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源表; CREATE TABLE room_type ( id BIGINT NOT NULL AUTO_INCREMENT, house_id BIGINT NOT NULL COMMENT 所属房源ID, type_name VARCHAR(50) NOT NULL COMMENT 房型名称, price DECIMAL(10,2) NOT NULL COMMENT 价格, stock INT NOT NULL DEFAULT 1 COMMENT 房间数量, area FLOAT DEFAULT NULL COMMENT 面积平方米, bed_type VARCHAR(50) DEFAULT NULL COMMENT 床型, max_people INT DEFAULT NULL COMMENT 最多入住人数, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_house_id (house_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房型表;在设计room_type时有一个关键点stock字段表示该房型的房间数量。比如某民宿有3间大床房stock 3。预订时要保证同一时间段内预订数量不超过stock这是并发控制的难点。3.3 订单表设计状态机与幂等性订单表是整个系统里最重要的表状态多、关联多、并发操作多。CREATE TABLE order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 下单用户ID, house_id BIGINT NOT NULL COMMENT 房源ID, room_type_id BIGINT NOT NULL COMMENT 房型ID, check_in_date DATE NOT NULL COMMENT 入住日期, check_out_date DATE NOT NULL COMMENT 离店日期, total_days INT NOT NULL COMMENT 入住总天数, total_price DECIMAL(10,2) NOT NULL COMMENT 订单总价, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-待支付 2-已支付 3-已入住 4-已完成 5-已取消 6-退款中 7-已退款, customer_name VARCHAR(50) NOT NULL COMMENT 入住人姓名, customer_phone VARCHAR(20) NOT NULL COMMENT 入住人电话, 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_user_id (user_id), KEY idx_house_id (house_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单状态我用一个TINYINT字段常量类配合管理而不是用字符串枚举值。这样设计的好处是数据库存储空间小、查询性能好坏处是代码可读性略差所以在Java代码里我会定义一个OrderStatusEnum枚举来转换。order_no一定要唯一生成规则可以用时间戳随机数用户ID后几位保证并发情况下不重复。3.4 房态日历表解决“重复预订”问题的关键民宿预订最核心的难题是怎么知道某个房型在某个日期段是否可订粗暴的做法是每次查询都去订单表里用日期区间碰撞判断但这样查询效率低、逻辑复杂。更规范的做法是维护一张房态日历表house_stock_calendar。CREATE TABLE room_stock_calendar ( id BIGINT NOT NULL AUTO_INCREMENT, room_type_id BIGINT NOT NULL COMMENT 房型ID, date DATE NOT NULL COMMENT 日期, stock INT NOT NULL COMMENT 当日可用房间数, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_room_date (room_type_id, date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房态日历表;这张表的设计思路是把“连续时间段库存判断”转换为“单日库存判断”。用户在前端选择入住日期和离店日期后后端只需要做一个范围查询SELECT date, stock FROM room_stock_calendar WHERE room_type_id ? AND date ? AND date ? ORDER BY date;再逐日判断是否每天都有库存即可兼具准确性和效率。下单成功后每天数值减1订单取消或退款后对应日期数值加1。这种设计在民宿系统里是最合理的一种规避了很多同学用“订单间隔判断库存”导致的多房间冲突bug。4. 后端核心功能实现从登录认证到预订流程4.1 JWT登录认证与拦截器配置用户认证这块我用的是Spring Security JWT。项目里没有过度依赖Spring Security的复杂过滤器链而是利用了它的核心能力AuthenticationManager做登录认证JwtAuthenticationFilter做请求过滤。登录流程是这样的用户提交用户名和密码。后端UserService.login()通过AuthenticationManager.authenticate()校验。校验通过后用HmacSHA256算法生成JWT把用户ID、用户名、角色塞进payload。前端拿到Token后存到localStorage里之后的每一次请求都在Authorization请求头里带上Bearer Token。后端JwtAuthenticationFilter解析Token如果有效就把用户信息放入SecurityContextHolder后面的接口就能通过AuthenticationPrincipal拿到当前用户。这一段里最容易踩坑的是Spring Security的放行配置。登录接口、注册接口、房源搜索列表、房源详情这些必须放行但创建订单、管理房源、后台统计这些接口需要认证。我在配置里维护了一个permitAll路径列表Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests() .antMatchers(/api/auth/login, /api/auth/register, /api/houses/list, /api/houses/detail/**).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated(); return http.build(); }权限控制上我用了PreAuthorize注解做细粒度控制比如PreAuthorize(hasRole(ADMIN) or hasRole(OWNER)) PostMapping(/api/houses) public Result? createHouse(Valid RequestBody HouseDTO dto) { ... }4.2 房源搜索多条件组合查询的实现房源列表页的核心功能是搜索筛选分页。常规实现是用MyBatis-Plus的QueryWrapper拼接条件public IPageHouseVO searchHouses(HouseSearchDTO dto, PageHouse page) { QueryWrapperHouse wrapper new QueryWrapper(); if (StringUtils.hasText(dto.getCity())) { wrapper.like(city, dto.getCity()); } if (StringUtils.hasText(dto.getKeyword())) { wrapper.and(w - w.like(name, dto.getKeyword()) .or().like(description, dto.getKeyword())); } if (dto.getMinPrice() ! null) { wrapper.ge(min_price, dto.getMinPrice()); } if (dto.getMaxPrice() ! null) { wrapper.le(max_price, dto.getMaxPrice()); } wrapper.eq(status, 1); wrapper.orderByDesc(create_time); return houseMapper.selectPage(page, wrapper); }但这里有一个设计细节需要注意房源列表页通常需要显示“起价”最低房型价格这个字段如果在查询时临时关联room_type表去算MIN(price)性能会很差。我采用了一个折中方案house表里冗余一个min_price字段每次创建或修改房型价格时同步更新对应房源的min_price。这种空间换时间的思路在中小型项目里很实用。4.3 订单创建事务与并发控制订单创建是系统的核心业务必须保证数据一致性。流程分几步校验用户已登录。校验入参入住日期、离店日期、房型ID。校验日期合法性离店日期晚于入住日期不能预订过去日期。查询房态日历判断每一天是否都有库存。计算总价每天的单价加起来或者按天数乘以单价。扣减房态日历库存。生成订单记录并保存。这几步必须放在同一个事务里任何一个环节失败都要回滚Transactional(rollbackFor Exception.class) public Order createOrder(CreateOrderDTO dto, Long userId) { // 1. 参数校验 // 2. 房态校验 ListRoomStockCalendar stockList calendarMapper.selectList(...); for (RoomStockCalendar stock : stockList) { if (stock.getStock() 0) { throw new BusinessException(所选日期段内房间已满); } } // 3. 扣库存 int rows calendarMapper.decreaseStock(roomTypeId, checkInDate, checkOutDate); if (rows 0) { throw new BusinessException(库存扣减失败请重试); } // 4. 生成订单 Order order new Order(); ... orderMapper.insert(order); return order; }库存扣减我用的是UPDATE语句配合乐观锁思路不是先查再改UPDATE room_stock_calendar SET stock stock - 1 WHERE room_type_id #{roomTypeId} AND date #{checkInDate} AND date #{checkOutDate} AND stock 0;这样的好处是并发情况下只有一个请求能成功把库存扣到非负避免超卖。如果影响行数不等于预期天数就说明扣减失败回滚事务。这里要特别提醒不要用“先SELECT查询库存再UPDATE”的方式。两个用户同时查到有库存然后同时下单就会重复预订。必须让数据库在更新时去做原子性判断。4.4 订单状态流转管理订单状态是一个状态机流转规则待支付 → 用户点击支付 → 已支付已支付 → 到店办理入住 → 已入住已入住 → 退房 → 已完成待支付 → 用户取消 → 已取消已取消 → 恢复房态库存已支付 → 用户申请退款 → 退款中 → 已退款同时恢复房态库存我写了一个OrderStateMachine来做状态流转校验避免非法跳转public void changeOrderStatus(Order order, OrderStatusEnum targetStatus) { OrderStatusEnum currentStatus OrderStatusEnum.fromCode(order.getStatus()); switch (currentStatus) { case PENDING_PAYMENT: if (targetStatus ! OrderStatusEnum.PAID targetStatus ! OrderStatusEnum.CANCELLED) { throw new BusinessException(待支付订单仅可支付或取消); } break; case PAID: if (targetStatus ! OrderStatusEnum.CHECKED_IN targetStatus ! OrderStatusEnum.REFUNDING) { throw new BusinessException(已支付订单仅可入住或申请退款); } break; ... } order.setStatus(targetStatus.getCode()); }这套状态机保证了业务逻辑的严谨性也便于后续扩展退款流程、异常处理流程。5. 前端页面开发用户端与后台管理的实现细节5.1 前端项目结构与路由组织前端我用Vite Vue 3 Vue Router Pinia Element Plus搭建。项目目录结构大致如下src/ ├── api/ # 接口请求模块 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── views/ │ ├── home/ # 首页 │ ├── house/ # 房源列表、房源详情 │ ├── order/ # 订单确认、订单详情、订单列表 │ ├── user/ # 登录注册、个人中心 │ └── admin/ # 后台管理 ├── utils/ # 工具函数 └── App.vue路由守卫需要做两件事未登录用户访问需要认证的页面个人中心、订单管理、后台跳转到登录页。管理员角色访问非管理员页面跳转到403页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token); const role localStorage.getItem(role); if (to.meta.requiresAuth !token) { next(/login); } else if (to.meta.requiresAdmin role ! ADMIN) { next(/403); } else { next(); } });5.2 房源列表页的搜索交互房源列表页是用户浏览体验的门面。我实现了一个左侧筛选栏 右侧房源卡片列表的经典布局。筛选项包括城市选择下拉框入住日期和离店日期日期选择器关键词搜索房源名称、地址、描述价格区间滑块排序方式综合排序、价格从低到高、价格从高到低筛选条件的变化通过reactive对象绑定每次变化自动触发列表刷新const searchParams reactive({ city: , checkInDate: , checkOutDate: , keyword: , minPrice: null, maxPrice: null, sortRule: default, pageNum: 1, pageSize: 10 }); watch(searchParams, () { searchParams.pageNum 1; fetchHouseList(); }, { deep: true });列表页的关键是用好Element Plus的el-card和el-image组件展示房源卡片。图片懒加载用v-lazy指令避免一次加载过多图片卡住页面。5.3 房态日历选择组件的实现房态日历是前端交互最复杂的部分。用户在详情页需要选择入住日期和离店日期而可选日期受到房态库存限制。我的实现方案是用户在进入详情页时后端返回该房型未来30天的每日库存数据。前端拿到数据后渲染一个日历面板把无库存的日期置灰不可选并把连续可用日期标记出来。Element Plus的el-date-picker组件支持disabledDate函数可以动态禁用不可选日期const disabledDate (date) { const dateStr formatDate(date); // 过去日期不可选 if (date new Date()) return true; // 无库存日期不可选 const stock stockMap[dateStr]; if (stock undefined || stock.stock 0) return true; return false; };这里要特别注意日期格式化的时区问题。JavaScript的Date对象在格式化时如果不指定时区会默认用本地时区与后端返回的字符串日期格式yyyy-MM-dd比较时可能出现偏移。我写了一个工具函数用getFullYear、getMonth、getDate拼出本地日期字符串避免踩坑。5.4 订单确认页的联动计算订单确认页的核心逻辑是价格联动计算。用户选完日期、确认入住人数后前端实时计算出总价。一个容易忽略的点选了入住日期和离店日期后总天数不能简单用(离店时间 - 入住时间) / 86400000因为跨夏令时地区中国不涉及或者用户选择的边界时间会导致计算偏差。我用的是const getDaysBetween (startDate, endDate) { const start new Date(startDate); const end new Date(endDate); const timeDiff end.getTime() - start.getTime(); return Math.round(timeDiff / (1000 * 3600 * 24)); };使用Math.round而不是Math.floor更保险。价格计算逻辑const totalDays getDaysBetween(checkInDate, checkOutDate); const totalPrice computed(() { return totalDays * roomType.price; });如果项目后续要做“周末涨价”“节假日调价”这里就不能用简单乘法而是改造为根据日历表每天的价格求和。我在这个版本里预留了按日价格数组的字段方便后续升级。5.5 后台管理页面的前端实现后台管理页面是给房东或管理员用的包括房源管理、订单管理、房态管理、用户管理、数据统计五个大模块。房源管理一个表格列出当前用户的所有房源每行有编辑、上架/下架、删除按钮。新增房源通过对话框表单实现表单项包含房源名称、描述、地址、城市、封面图上传等。订单管理管理员可以查看所有订单按状态筛选对待支付订单可以标记为已取消对已支付订单可以办理入住、退房、退款。这里重点说一下封面图上传功能。我用的方式是采用本地文件存储后端接口POST /api/upload接收MultipartFile保存到服务器指定目录然后返回可访问的URL。开发环境下我用Nginx做静态文件映射把/upload/**路径映射到服务器上的物理路径location /upload/ { alias /data/upload/; }需要注意上传文件必须做扩展名校验只允许jpg、png、gif、webp等图片格式并且重命名文件避免路径穿越和文件名冲突String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename UUID.randomUUID().toString().replace(-, ) ext;6. 部署实践前后端分离到底怎么部署最省心6.1 手动部署方案适合小项目最简单直接的部署方式后端打包生成一个jar包在服务器上直接用java -jar启动。前端npm run build生成dist静态文件目录交给Nginx托管。Nginx配置里需要做两件关键事情第一托管前端静态资源server { listen 80; server_name your-domain.com; root /var/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }try_files那行是Vue Router在history模式下必须的配置否则刷新页面会出现404。第二反向代理后端接口location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }这样前端只通过/api前缀访问后端跨域问题在部署环境下直接被Nginx转发绕过了。开发环境下才需要Vite的proxy配置做跨域代理。6.2 Docker Compose一键编排如果服务器配置比较干净我推荐换用Docker Compose做整套环境的编排把MySQL、Redis、后端、前端容器一次性拉起来。version: 3.8 services: mysql: image: mysql:5.7 container_name: bnb-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: bnb_db ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:6-alpine container_name: bnb-redis ports: - 6379:6379 backend: image: openjdk:17-jdk-slim container_name: bnb-backend ports: - 8080:8080 volumes: - ./app.jar:/app.jar - ./upload:/data/upload command: java -jar /app.jar --spring.datasource.urljdbc:mysql://mysql:3306/bnb_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai --spring.redis.hostredis --file.upload-dir/data/upload depends_on: - mysql - redis frontend: image: nginx:alpine container_name: bnb-frontend ports: - 80:80 volumes: - ./dist:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - backend7. 几个让我印象深刻的坑与排查实录7.1 日期参数时区偏移的坑这是一个非常典型的坑。前端把2024-06-01传给后端后端用DateTimeFormat(pattern yyyy-MM-dd)接收时如果Spring Boot配置里没有设置serverTimezone有时接收到的日期会变成前一天或者出现JSON parse error。排查步骤看后端日志的异常信息确认是序列化问题还是参数绑定问题。检查MySQL连接串是否带了serverTimezoneAsia/Shanghai。检查前端传给后端的是字符串还是ISO日期格式的Date对象如果是new Date()JSON序列化出来的是ISO 8601格式带时区偏移后端反序列化还是可能会偏一天。我的解决办法前端所有日期字段在传参之前统一用工具函数格式化为yyyy-MM-dd字符串后端接收时用LocalDate类型JsonFormat(pattern yyyy-MM-dd) private LocalDate checkInDate;LocalDate没有时区概念配合字符串传参彻底规避了这个坑。7.2 库存扣减并发导致的数据不一致我在测试时用JMeter模拟30个用户同时预订同一房型的同一天结果订出去了30单但库存只减了18个。问题出在哪最初版本的扣库存逻辑是// 错误示例 RoomStockCalendar stock calendarMapper.selectOne(...); if (stock.getStock() 0) { stock.setStock(stock.getStock() - 1); calendarMapper.updateById(stock); }这里就是典型的“先查后改”竞态条件。30个并发请求同时SELECT到库存为3然后又同时UPDATE把库存改成2最终只减了一次。换成原子SQL之后UPDATE room_stock_calendar SET stock stock - 1 WHERE room_type_id ? AND date ? AND stock 0这样并发情况下只有3个请求能成功其他请求影响行数为0抛异常回滚问题解决。这个教训我写进代码注释里了——中小型项目里这种并发问题非常隐蔽测试环境不容易复现必须提前在设计层面规避。7.3 前端路由History模式刷新404Vue Router默认的createWebHistory()在开发环境一切正常但部署到Nginx后用户浏览到/order/123这个页面刷新一下就会出现404。原因很简单Nginx找不到/order/123这个真实文件。解决方法是Nginx配置中增加try_fileslocation / { try_files $uri $uri/ /index.html; }这一步很关键部署前一定要检查否则交付给用户后刷新页面全是白屏。7.4 JWT令牌失效后前端表现异常JWT是无状态的Token过期后后端接口返回401但用户的页面上还停留在登录状态。如果不处理这个场景会出现一种诡异现象用户看着自己还是登录状态点下单按钮却弹“未登录”。我的处理方案前端request.js工具模块在拦截器里统一处理HTTP 401状态码一旦捕获401就从localStorage里清除Token和用户信息同时弹出提示“登录已过期请重新登录”然后跳转到登录页。axios.interceptors.response.use( response response, error { if (error.response error.response.status 401) { localStorage.removeItem(token); localStorage.removeItem(role); router.push(/login); } return Promise.reject(error); } );8. 总结与扩展思路这个民宿预订网站项目麻雀虽小五脏俱全。从前端页面交互到后端事务并发控制从数据库表设计到服务器部署基本上把全栈开发的各个环节都覆盖了。我做完这整套之后的一些体会第一业务逻辑的“状态一致性”意识比写代码更重要。预订系统最核心的不是CRUD而是并发下不超卖、不重复、状态不错乱。这需要在设计阶段就把库存模型、状态机理清楚而不是写完再打补丁。第二前后端联调效率的关键是接口契约。统一响应结构、规范字段命名、提前定义好API文档联调阶段的摩擦会大幅减少。第三这个项目的扩展空间很大。想加功能的话后续可以往这些方向迭代引入Elasticsearch做更强大的房源搜索和地理位置检索引入支付网关模拟支付或接入第三方沙箱环境完成线上付费闭环增加用户评价系统、收藏功能、房东账单结算把图片迁移到云存储服务解决本地磁盘空间有限的问题加入定时任务处理超时未支付订单自动关闭如果正在做类似项目先把订单流程跑通再去美化界面这个顺序不要反。好的系统是长出来的不是画出来的。
阅读完成 · 觉得有帮助?