尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Spring Boot + Vue酒店预订系统实战:从设计到部署全流程解析
前段时间帮朋友做了一个内部管理用的酒店预订小系统技术栈就是现在最主流的 Spring Boot Vue。做完之后感觉这套组合非常适合中小型业务系统尤其是酒店预订这种有大量状态流转、前后端交互频繁的场景。整个过程踩了不少坑也总结出一些可以直接抄作业的经验。这篇就把整个项目的设计思路、核心代码、以及一些操作细节完整写出来已经尽量少贴那些没有营养的配置片段把重点全部放在业务实现上。1. 项目核心设计与技术选型思路1.1 模块化设计从一个真实预订流程说起酒店预订系统看着简单其实里面的业务链条一点都不短。用户要能注册登录、浏览房型、查看房态日历、提交预订、在线支付或者到店支付、然后还要能查看订单状态、取消订单。管理端就更麻烦了需要管理房间类型、管理真实房间号、设置不同时间段的价格、处理订单确认与入住登记。我第一次做这类项目的时候直接把所有功能塞进一个大的业务类里面结果改需求的时候差点把自己逼疯。后来总结出一条经验预订系统一定要按领域划分模块而不是按操作划分模块。我最终划分成下面五个模块每个模块只做自己领域内的事情模块名称负责内容对外提供能力用户模块注册、登录、个人信息认证与鉴权房型模块房型信息、图片、设施房型查询与筛选房间模块具体房间号、房态管理库存查询与扣减订单模块订单创建、状态流转、取消预订核心流程支付模块模拟支付、回调支付体系对接按实际需要取舍支付流水记录这种划分的好处非常明显房型是静态数据房间是动态数据两者分开后价格规则、库存判断的逻辑就清晰了。订单模块只要面向房间模块要库存完全不用关心房型下面到底挂了多少个真实房间。1.2 技术栈选型为什么是Spring Boot Vue这个组合现在几乎成了前后端分离项目的标配选它不是因为跟风而是因为它真的适合这种项目。后端用 Spring Boot 的理由很实在Spring Boot 的自动配置能大大减少 XML 配置文件。对于酒店预订这种业务复杂度中等、但数据表关联较多的系统Spring Boot 加上 MyBatis Plus 之后CRUD 写起来非常快。我这次就是用 Spring Boot 2.x MyBatis Plus 来做的分页查询、逻辑删除、自动填充这些功能都是现成的。Spring Boot 的另一个好处是生态成熟。拿权限控制来说Spring Security 或者 JWT 都可以直接集成拿参数校验来说validation-api可以直接用注解完成。不需要像早期 SSM 框架那样什么都要自己写配置。前端选 Vue 的原因也很直接Vue 的中文社区文档非常友好对于酒店预订这类管理后台加用户端双重界面结构的系统Vue 的单文件组件模式让页面复用变得很方便。比如后面要说到的房型卡片在用户端和管理端都要显示一个.vue组件两个地方 import 就行。还有一个不能忽略的原因前后端分离的开发模式非常适合预订系统这类需要多人协作、频繁改动界面的项目。前端调接口只要有后端同事提供的 Swagger 文档就能并行开发不需要像 JSP 时代那样前端页面嵌在后端代码里面改个样式都要重启整个服务。注意我这里说的 Vue 是指 Vue 2 Element UI 的组合Vue 3 Element Plus 现在也已经很成熟了。如果是全新项目用 Vue 3 也可以核心思路完全一样只是个别 API 写法需要调整。2. 数据库设计与核心表结构2.1 用户、房型、房间、订单四张核心表怎么设计数据库设计是这个项目的关键表结构如果设计得好后面写代码就是纯粹的搬运工作。我第一次设计的时候把房型和房间合并成一张表结果怎么设计都别扭。后来想明白了房型是商品房间是库存。这个类比非常重要。一个酒店可能有 100 间大床房房型这 100 间就是 100 个房间记录。预订的时候不是选“大床房”就行而是要从大床房这个房型下面找到任意一个未占用的具体房间号。如果合并成一张表每次查询房型列表都要查 100 条记录再去重完全是浪费。用户表和订单表比较简单核心的建表语句我直接贴出来带有注释方便理解-- 用户表 CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, id_card varchar(30) DEFAULT NULL COMMENT 证件号码敏感信息按业务需求决定是否存储, status tinyint(4) DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 房型表 CREATE TABLE t_room_type ( id bigint(20) NOT NULL AUTO_INCREMENT, type_name varchar(50) NOT NULL COMMENT 房型名称比如豪华大床房, area decimal(10,2) DEFAULT NULL COMMENT 面积(㎡), bed_type varchar(20) DEFAULT NULL COMMENT 床型大床/双床, max_people int(11) DEFAULT NULL COMMENT 可住人数, price decimal(10,2) NOT NULL COMMENT 门市价, inventory int(11) NOT NULL COMMENT 可用库存房间总数, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, description text COMMENT 房型描述, status tinyint(4) DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房型表; -- 房间表 CREATE TABLE t_room ( id bigint(20) NOT NULL AUTO_INCREMENT, room_type_id bigint(20) NOT NULL COMMENT 所属房型ID, room_number varchar(20) NOT NULL COMMENT 具体门牌号比如8801, floor int(11) DEFAULT NULL COMMENT 所在楼层, status tinyint(4) DEFAULT 1 COMMENT 1可预订 2占用 3维修, PRIMARY KEY (id), UNIQUE KEY uk_room_number (room_number), KEY idx_room_type_id (room_type_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房间表; -- 订单表 CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 下单用户, room_type_id bigint(20) NOT NULL COMMENT 预订的房型, room_id bigint(20) DEFAULT NULL COMMENT 实际分配的房间ID, check_in_date date NOT NULL COMMENT 入住日期, check_out_date date NOT NULL COMMENT 离店日期, night_count 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已取消, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_room_type_id (room_type_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单表的room_id字段单独提出来说一下。这个字段不是在创建订单的时候就要填的而是等管理员接单确认之后才分配。为什么这么设计因为同一个房型下有多个房间用户预订的时候只关心有没有房不关心具体住哪间。等到用户到店或者管理员确认订单时再从空闲房间里挑一个分配这样能避免早期固定房间导致的资源浪费。2.2 库存扣减的并发处理是关键酒店预订系统的数据库设计还有一个逃不开的问题并发下单怎么保证不超卖。最经典的方案就是乐观锁。在房型表里加一个version字段每次扣减库存的时候带上版本号条件更新。比如用户预订了一间大床房执行下面的更新语句UPDATE t_room_type SET inventory inventory - 1, version version 1 WHERE id #{roomTypeId} AND inventory 0 AND version #{version};如果影响行数为 0说明要么版本号不对要么库存已经没了这时候直接告诉用户“该房型已被抢光”不需要引入 Redis 分布式锁这种重型组件。对于中小型酒店系统这个方案简单有效完全够用。用户下单核心部分的伪代码如下// 开启事务 Transactional(rollbackFor Exception.class) public OrderResult createOrder(CreateOrderDTO dto) { // 1. 生成订单号用时间戳加随机数 String orderNo generateOrderNo(); // 2. 查询房型检查状态和库存 RoomType roomType roomTypeMapper.selectById(dto.getRoomTypeId()); if (roomType null || roomType.getStatus() ! 1) { return OrderResult.fail(该房型已下架); } // 3. 计算总价 BigDecimal totalPrice calculatePrice(roomType, dto.getCheckInDate(), dto.getCheckOutDate()); // 4. 通过乐观锁扣减库存 int rows roomTypeMapper.deductInventory(roomType.getId(), roomType.getVersion()); if (rows 0) { throw new BusinessException(手慢了该房型库存不足); } // 5. 保存订单为待支付状态 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(dto.getUserId()); order.setRoomTypeId(dto.getRoomTypeId()); order.setCheckInDate(dto.getCheckInDate()); order.setCheckOutDate(dto.getCheckOutDate()); order.setTotalPrice(totalPrice); order.setStatus(0); orderMapper.insert(order); return OrderResult.success(order); }注意第 4 步和第 5 步之间的顺序。先扣库存再保存订单如果保存订单失败事务回滚会让库存也回滚这个不用太担心。但反过来如果先保存订单再扣库存就有可能出现订单保存成功、库存扣减失败的情况需要在代码里做补偿逻辑麻烦得多。关于日期冲突那种“指定日期有没有房”的判断逻辑其实是一样的查询订单表统计check_in_date和check_out_date区间与请求日期区间有重叠的订单数量再用总房间数去减。SQL 写起来稍微绕一点但核心判断条件就是SELECT COUNT(*) FROM t_order WHERE room_type_id #{roomTypeId} AND status IN (0, 1, 2) -- 待支付、已确认、已入住都算占用 AND check_in_date #{requestCheckOut} AND check_out_date #{requestCheckIn}这段 SQL 很有代表性。startA endB AND startB endA这个公式可以用来判断任意两个区间是否重叠不只是日期价格区间、时间区间都能用。3. 后端核心业务实现3.1 JWT登录鉴权与静态资源处理前后端分离项目里传统的 Session 方案不好用了因为前端可能部署在完全不同的域名下CORS 跨域携带 Cookie 的逻辑会带来一堆问题。我自己在项目中一直用 JWTJSON Web Token。JWT 的实现思路非常简单用户登录成功后后端生成一个包含用户 ID、用户名、过期时间的 token 返回给前端。前端把 token 存到localStorage或sessionStorage里每次请求的时候放到 HTTP Header 的Authorization字段。后端用一个拦截器统一校验 token解析成功就把用户信息放到ThreadLocal或者请求上下文里后续的业务代码直接取。Spring Boot 里配置一个拦截器很轻量Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 if (request.getRequestURI().contains(/user/login) || request.getRequestURI().contains(/room/list)) { return true; } String token request.getHeader(Authorization); if (token null) { response.setStatus(401); return false; } // 解析并校验 token LoginUser loginUser JwtUtil.parseToken(token.replace(Bearer , )); if (loginUser null) { response.setStatus(401); return false; } // 往请求上下文中放用户信息 request.setAttribute(loginUser, loginUser); return true; } }这里有一个我后来才注意到的细节放行接口不要只放行/user/login还要放行首页的房型列表以及所有 OPTIONS 请求。因为浏览器发复杂请求的时候会先发一个预检请求OPTIONS如果不放行前端调试时会发现所有接口都报跨域错误还找不到原因。酒店预订系统里的图片资源也要特别注意。房型照片往往保存在本地磁盘或者对象存储里不建议直接放在代码目录下。我在application.yml里做了虚拟路径映射变成这样spring: servlet: multipart: max-file-size: 10MB mvc: static-path-pattern: /images/** resources: static-locations: file:D:/upload/,classpath:/static/这样前端访问http://localhost:8080/images/room1.jpg时实际上读的是本地磁盘D:/upload/room1.jpg文件。好处是以后换服务器或者备份图片只需要拷贝一个目录不用重新打包项目。3.2 为什么订单状态机这一环最重要订单状态是酒店预订系统最容易被忽视却最核心的一环。我见过不少系统订单状态就是简单的一个数字字段代码里到处散落着if (status 1)这样的判断一旦状态流转变复杂代码立刻变成一团乱麻。我的建议是把订单状态流转画成一张图然后严格按照图来写代码。酒店预订订单的状态流转其实并不复杂待支付0- 已确认1前提是用户付了款待支付0- 已取消4用户主动取消或超时未支付已确认1- 已入住2用户到店办理入住已确认1- 已取消4管理员或用户取消已入住2- 已退房3用户离店结算这个状态机图写清楚之后代码只需要一个方法专门处理状态变化public void updateOrderStatus(Long orderId, Integer fromStatus, Integer toStatus) { int rows orderMapper.updateStatusIfMatch(orderId, fromStatus, toStatus); if (rows 0) { throw new BusinessException(订单状态已变化无法执行当前操作); } }updateStatusIfMatch的 SQL 里包含WHERE status #{fromStatus}条件这样就能保证并发情况下不会出现“两个操作同时把同一个订单从不同状态改成不同状态”的问题。这个技巧比不加条件的更新安全得多值得作为通用模式用到其他项目里。另外订单取消之后要记得把库存加回来。这个逻辑在取消订单事务里一定要同步做不能靠定时任务去补否则会出现用户已经取消成功、第二天别人还是订不了房的尴尬情况。4. 前端核心页面与接口联调4.1 首页房型展示与搜索筛选用户端首页一般是这样的布局顶部是导航栏中间是一张大轮播图下面就是房型列表。房型列表的数据是通过接口从后端拿的前端拿到房型数据后渲染成卡片式布局。Vue 的v-for渲染列表非常直观div classroom-grid el-row :gutter16 el-col :span6 v-foritem in roomTypeList :keyitem.id el-card :body-style{ padding: 0px } img :srcitem.coverImage classroom-image / div classroom-info h3{{ item.typeName }}/h3 p{{ item.area }}㎡ · {{ item.bedType }} · 可住{{ item.maxPeople }}人/p div classroom-price span classprice{{ item.price }}/span el-button typeprimary sizesmall clickgoDetail(item.id)查看详情/el-button /div /div /el-card /el-col /el-row /div搜索筛选功能的设计也有讲究。用户搜索“大床房”的时候后端不能只做一个简单的LIKE %大床房%因为用户还可能会筛选价格范围、入住日期。所以后端搜索接口的设计应该是这样GetMapping(/room/list) public Result pageList(RoomQueryDTO query) { LambdaQueryWrapperRoomType wrapper new LambdaQueryWrapper(); // 模糊搜索名称 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(RoomType::getTypeName, query.getKeyword()); } // 价格区间过滤 if (query.getMinPrice() ! null) { wrapper.ge(RoomType::getPrice, query.getMinPrice()); } if (query.getMaxPrice() ! null) { wrapper.le(RoomType::getPrice, query.getMaxPrice()); } // 日期筛选调用自定义方法判断指定日期区间是否有房 if (query.getCheckInDate() ! null query.getCheckOutDate() ! null) { ListLong availableTypeIds orderMapper.findAvailableRoomTypes( query.getCheckInDate(), query.getCheckOutDate()); if (availableTypeIds.isEmpty()) { return Result.success(new PageResult(0, Collections.emptyList())); } wrapper.in(RoomType::getId, availableTypeIds); } // 只查上架状态 wrapper.eq(RoomType::getStatus, 1); // 分页处理 PageRoomType page new Page(query.getPageNum(), query.getPageSize()); roomTypeMapper.selectPage(page, wrapper); return Result.success(new PageResult(page.getTotal(), page.getRecords())); }日期筛选用in条件是后面修改的效果。一开始我用的是遍历所有房型、逐个判断有没有房数据量少的时候没问题房型一多就特别慢。后来优化成直接在数据库里先算好哪些房型在指定日期区间内还有房再对房型 ID 做in查询速度提升非常明显。这段逻辑让我意识到一个问题在数据库里过滤数据比在 Java 代码里过滤数据效率高得多。能用 SQL 完成的筛选不要搬到代码里来做特别是涉及多表关联的时候。4.2 登录、购物车式预订与个人中心用户在用户端订房的流程是这样的选房型 - 选日期 - 填入住人信息 - 提交订单。如果把酒店预订和电商购物类比这个过程其实就是“把房型加入购物车然后结算”的简化版。不过酒店预订不需要购物车因为一次预订通常只涉及一个房型、一个日期区间。倒是“选择日期区间”这个交互有太多值得注意的坑。Element UI 的日期范围选择器el-date-picker返回的是一对日期前端在传给后端之前要做好格式转换// 前端提交预订时 const params { roomTypeId: this.activeRoomType.id, checkInDate: this.dateRange[0], checkOutDate: this.dateRange[1], guestName: this.form.guestName, guestPhone: this.form.guestPhone }; // 直接提交由后端统一计算入住晚数和总价这里我特别强调“后端统一计算”因为前端传过来的日期只是字符串后端拿到之后要自己校验日期合法性、计算nightCount和totalPrice。千万不要信任前端传来的价格和晚数否则用户直接改请求参数就能把价格改掉哪怕没有支付漏洞也会搞乱订单统计。后端日期计算逻辑比较简单DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd); LocalDate checkIn LocalDate.parse(dto.getCheckInDate(), formatter); LocalDate checkOut LocalDate.parse(dto.getCheckOutDate(), formatter); // 入住日期不能早于今天当天预订当天入住按业务需求处理 if (checkIn.isBefore(LocalDate.now())) { throw new BusinessException(入住日期不能早于今天); } // 离店日期必须晚于入住日期 if (checkOut.isBefore(checkIn.plusDays(1))) { throw new BusinessException(离店日期必须晚于入住日期); } long nights DAYS.between(checkIn, checkOut); BigDecimal totalPrice roomType.getPrice().multiply(BigDecimal.valueOf(nights));个人中心的订单列表也比较常规有一个地方值得注意订单列表的前端展示状态要用 tag 标签来显示不同颜色。待支付是黄色已确认是蓝色已入住是绿色已退房是灰色已取消是红色。颜色区分一下用户在后台一眼就能看清订单状态。5. 部署、环境配置与问题排查实录5.1 前后端分离项目如何本地一键启动做这种项目除了代码本身环境的配置经常能卡住新手一整天。我把自己验证过的本地启动流程整理出来照着一步步做就能跑起来。后端启动步骤本地安装 JDK 8 或 11配置好JAVA_HOME环境变量。安装 MySQL 5.7 或 8.x把上面的建表语句执行一遍再插入几条测试数据。用 IDEA 导入后端项目等待 Maven 下载依赖。这一步最容易出问题的地方是 Maven 镜像源不对导致依赖下载卡住。解决办法是在settings.xml里配置阿里的镜像源mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror修改application.yml里的数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver运行启动类看到 Spring Boot 的启动日志后访问 Swagger 地址如果集成了的话测试接口。前端启动步骤安装 Node.js 14 以上版本建议用 LTS 版本。进入前端项目根目录执行npm install安装依赖。修改前端配置里的后端接口地址。开发环境推荐在config/dev.env.js或.env.development里把后端地址指向http://localhost:8080。执行npm run serve命令行会输出一个本地地址浏览器打开即可。需要特别注意的一点是前后端分离项目开发阶段一定要开跨域代理而不是在后端配置一个CrossOrigin注解一了百了。CrossOrigin只能解决本地开发阶段的问题一上生产环境如果前后端分别部署在不同域名下跨域问题依然存在。开发阶段的前端跨域代理配置Vue CLI 中的vue.config.jsmodule.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };这样前端页面里请求/api/user/login实际上就会被代理到http://localhost:8080/user/login前后端开发时端口完全隔离互不干扰。5.2 常见异常与解决清单以下是这个项目整个开发过程中遇到频率最高的几个问题整理成表格方便查阅异常或问题现象根本原因解决方法前端请求接口报 404前端代理路径写错或者pathRewrite没生效确认前端实际发送的请求路径和后端接口路径是否一致不要在 Controller 类上写多余的 RequestMapping后端注入Autowired报空指针类没有交给 Spring 管理或接口没有被 Mapper 扫描到启动类上加上MapperScan(com.example.mapper)或每个 Mapper 上加Mapper表单提交后所有字段都是 null前端没有加Content-Type: application/json或者后端没有RequestBody注解确保后端接口方法参数上加了RequestBody前端用 axios 时必须设置正确的Content-Type部署后图片加载不出来静态资源路径映射配置不对检查application.yml中的static-locations配置确认路径最后带不带/用户能直接访问未登录才能看到的页面前端没有拦截路由Vue Router 中加全局前置守卫判断本地 token 是否存在数据库中文乱码连接串没有加characterEncodingutf8或者数据库表不是 utf8mb4在application.yml的连接 URL 中加参数并把表结构改成 utf8mb4 字符集下单时库存扣了但订单没生成乐观锁更新成功但后续插入异常事务没有正确开启在 Service 方法上加Transactional(rollbackFor Exception.class)并确保 AOP 切面没有被屏蔽关于最后一个情况多说一句Transactional默认只回滚RuntimeException如果业务代码里抛出的是自定义的Exception必须指定rollbackFor Exception.class否则事务不会回滚就会出现库库存没了但订单也没了的严重问题。还有一个排查问题的通用思路我想特别强调一下前端报错优先看浏览器 Network 面板后端报错优先看 Console 日志。很多新手遇到接口 500 错误第一反应是去前端代码里面翻逻辑折腾半天发现根本没效果。正确做法是先看后端控制台Stack Trace 已经告诉你了具体哪一行代码有问题直接修对应的地方就可以了。5.3 部署到云服务器需要注意的细节项目做完要部署上线我建议用最经典的三层结构前端项目npm run build打包生成静态文件用 Nginx 托管。后端项目 Maven 打包生成 jar 包通过 Java 命令启动或者用 Shell 脚本配合 nohup 后台运行。MySQL 数据库独立运行生产环境不要用 root 账号裸奔单独建一个业务账号并授权到指定库名。Nginx 配置前后端分离的关键是 location 规则server { listen 80; server_name your-domain.com; # 前端静态页面 location / { root /var/www/hotel-frontend/dist; try_files $uri $uri/ /index.html; # Vue Router 的 history 模式必须加这行 } # 后端接口转发 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里try_files那行特别重要。如果用的 Vue Router 的 history 模式用户直接访问http://your-domain.com/user/detail/1时页面不存在不加这行配置 Nginx 会返回 404加了之后就会自动转到 index.html由前端路由接管。后端启动命令参考nohup java -jar hotel-server.jar --spring.profiles.activeprod \ --server.port8080 /app/logs/hotel.log 21 --spring.profiles.activeprod的意思是加载application-prod.yml这个配置文件。里面数据库地址、文件路径之类的一般配置在仓库的.gitignore之外单独维护不能提交到公开代码仓库。6. 项目扩展方向与个人经验总结这个项目做完之后如果想继续升级我个人认为有几个方向比较有价值第一个方向是接入真实支付。模拟支付在 Demo 项目里没问题但真正上线的话微信支付、支付宝支付的对接是逃不掉的。核心逻辑就是用户下单后把订单信息提交给支付平台平台返回一个预支付凭证你把这个凭证拿去生成支付二维码。用户扫码支付后支付平台会异步回调你的服务器接口你在回调里更新订单状态和支付流水。第二个方向是前后端代码的权限细化。目前很多 Demo 项目都是把管理员和普通用户放在同一个登录流程里只是给个isAdmin字段简单区分。真要能扛住实际业务需要引入更完善的权限模型比如 RBAC基于角色的访问控制这样后期扩展员工管理、权限分配都会顺滑很多。第三个方向是数据分析。酒店运营非常看重数据每天入住率、RevPAR每间可售房收入、各房型热度趋势、预订来源渠道分布。这些数据都可以从订单表和浏览记录表里统计出来前端做一个仪表盘页面展示后端写几个聚合查询接口这个功能做完之后项目的实战价值会提升非常多。最后说点我个人的实际感受吧。这类管理系统项目最锻炼人的地方不在于技术栈有多深而在于业务逻辑的缜密度。库存怎么不超卖、订单状态怎么流转才不出错、跨域和图片路径这类细节怎么处理得干净利落每一个问题背后都有真实的业务考量。如果你是刚接触 Spring Boot Vue 的开发者照着这个项目从数据库表结构、后端接口、前端页面一路写下去过程中遇到的所有报错都属于正常现象。关键是要边写边思考为什么这么设计把表结构设计的思路、乐观锁解决并发的思路、Nginx 配置前后端联动的思路真正吃透比照着敲一遍代码有意义得多。等这些问题全部解决完你会发现自己对前后端分离项目的整体把控能力已经上了一个大台阶。
RELATED

相关推荐

顺序表增删查改详解:从内存布局到动态数组实现

顺序表增删查改详解:从内存布局到动态数组实现

顺序表大概是所有数据结构教材里的第一座山,也是很多人第一次意识到“原来数据还能这么存”的地方。它的增删查改,代码看起来不多,但每一行都藏着内存布局、边界条件、复杂度分析这些基本功。我见过不少初学者,链表写得飞起&#…

📅 2026/10/13 4:02:14
Retrofit原理与实战:从动态代理到网络层优雅设计

Retrofit原理与实战:从动态代理到网络层优雅设计

接手过超过三年生命周期项目的开发者,大概率都有过被网络请求代码支配的经历:散落各处的AsyncTask、手写的HttpURLConnection封装、回调里套回调的线程切换。直到Retrofit出现,这套混乱才真正被终结。我最早接触Retrofit是在某内部工具类App重…

📅 2026/10/13 4:02:14
Linux虚拟机ens33网卡获取不到IP?三步排查搞定无地址故障

Linux虚拟机ens33网卡获取不到IP?三步排查搞定无地址故障

很多玩虚拟机的朋友应该都撞见过这个场景:登录系统后敲一句ifconfig,结果 ens33 那一行要么压根不出现,要么只有 MAC 地址和一个孤零零的 inet6,IPv4 地址就是不见踪影。群里问一圈,答案五花八门,有人让改配…

📅 2026/10/13 4:02:14
MORE NEWS

更多资讯

📰

PyTorch关键点检测实战:从自建数据集到模型训练与推理

简介:这份资源面向具备一定深度学习基础、希望动手实践关键点检测的开发者与研究人员,围绕PyTorch中的Keypoint R-CNN展开,解决在自建数据集上训练关键点检测模型时数据组织、模型配置与训练评估等实际问题。压缩包共116个文件,约…

📰

PyTorch keypoint_rcnn 训练自建数据集:从标注到推理全流程

简介:这份资源面向深度学习与计算机视觉方向的学习者和开发者,聚焦如何用PyTorch中的Keypoint R-CNN在自建数据集上训练关键点检测模型,可应用于姿态估计、人脸特征点定位等场景。压缩包共116个文件,约8.55MB,包含8个p…

📰

STM32F072C8T6最小核心板设计:从选型到PCB布局全攻略

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

📰

定制Claude Code Mods:从黑盒到可控AI编程工具

如果你用过几天 Claude Code 这类 agentic 命令行编程工具,你大概率会产生一种感受:它像一个手脚麻利但性格倔强的外包程序员,写得快、改得狠,可你总觉得它并不真正和你站在同一套规则里。我最初的想法特别朴素——能不能在这一层…

📰

Spring Boot表单如何发送PUT/DELETE请求?HiddenHttpMethodFilter原理与配置

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

📰

Dinic最大流算法详解:分层图、多路增广与当前弧优化

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

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬