简介这是一套基于SpringBoot与Vue双端协同开发的酒店客房管理系统实战项目面向Java全栈初学者及毕业设计开发者解决中小型酒店数字化管理中客房预订、商品销售、员工协同与用户互动等核心业务场景。资源包共625个文件含195个Java后端逻辑类、147个Vue前端页面组件、169个MyBatisPlus映射XML及30个JS工具脚本涵盖完整前后端工程结构压缩包仅4.28MB轻量易导入适配IDEAVSCode开发环境。已有147人学习下载项目已集成Redis缓存、Shiro权限控制、Ant Design Vue UI组件库及ApexCharts数据可视化模块并包含公告管理、客房预约下单、商品采购、留言评价、部门/职位/员工/用户等13项功能模块的可运行代码。内容预览显示含Lombok配置、MyBatisPlus模板文件及标准化分层代码结构entity/mapper/service/controller便于理解企业级分层架构与RESTful接口设计规范。 从我在实际项目里折腾酒店管理系统的经验讲起吧。这个基于SpringBoot酒店客房管理系统听起来是个典型的毕业设计或课程项目但真往生产环境做的时候水挺深的。SpringBoot本身降低了搭建成本可客房状态流转、防超卖、价格策略这些业务逻辑才是真正决定系统能不能用的关键。这篇文章我就从头到尾拆一遍把我踩过的坑和验证过的方案都写出来无论你是刚学SpringBoot的新手还是想快速搭一套管理系统做二次开发都能直接参考。1. 整体设计与技术选型1.1 为什么选SpringBoot而不是Spring MVC早些年做这类管理系统主流方案是Spring MVC加一堆XML配置一个web.xml里塞满监听器、过滤器、DispatcherServlet光把环境跑起来就能耗掉半天。SpringBoot最大的价值是约定大于配置内嵌Tomcatspring-boot-starter-web一拉main方法一跑Web服务就起来了。对于酒店客房管理这种典型CRUD加少量业务流的系统SpringBoot能让我把精力全压在业务实现上而不是配置地狱里。还有一个现实原因现在企业招人、毕业设计答辩SpringBoot几乎是默认标签。它生态成熟找资料容易遇到问题一搜一大片。虽然SpringCloud全家桶更高级但对单体酒店管理系统属于过度设计。我的建议是先用SpringBoot把单体做好将来真有微服务需求再按模块拆不迟。1.2 系统模块划分与功能清单酒店客房管理系统核心不是管理客房而是管理客房的状态和订单流转。我按业务域把系统拆成下面几块客房管理房型维护、房间信息、房态管理空闲/入住/打扫/维修预订管理线上/前台预订、预订确认、取消、超时释放入住退房办理入住、换房、续住、退房、押金管理账务管理房费计算、订单支付、退款、账单打印会员管理会员等级、积分、折扣统计报表入住率、营收统计、房态报表系统管理用户、角色、权限、操作日志每个模块之间是有关联的比如预订后房间状态要变成已预订入住后变成入住中退房后变成待打扫。这个状态机如果没设计好后面全是坑。我下面会重点讲这个。1.3 技术栈与数据库设计我的技术栈选型如下层面技术后端框架SpringBoot 2.7.xORMMyBatis-Plus 3.5.x数据库MySQL 8.0缓存Redis 5.x安全认证Spring Security JWT接口文档Knife4jSwagger增强版前端Vue 3 Element Plus前后端分离部署Docker / Linux Nginx为什么用MyBatis-Plus而不是JPA因为酒店管理系统报表类SQL很多多表联查、动态条件查询MyBatis-Plus的QueryWrapper加自定义XML能完美兼顾开发效率和SQL可控性。数据库设计我重点说三张核心表房间表room字段类型说明idbigint主键room_numbervarchar房间号room_type_idbigint房型IDstatustinyint0-空闲 1-入住 2-打扫 3-维修 4-预留floorint所在楼层订单表reservation字段类型说明idbigint主键order_novarchar订单编号room_idbigint房间IDcustomer_namevarchar客户姓名check_in_datedate入住日期check_out_datedate退房日期statustinyint0-待支付 1-已确认 2-已入住 3-已退房 4-已取消房态记录表room_status_log字段类型说明idbigint主键room_idbigint房间IDold_statustinyint原状态new_statustinyint新状态change_timedatetime变更时间operatorvarchar操作人为什么一定要有room_status_log因为酒店经常需要追溯这个房间昨天为什么不能卖没有日志出了纠纷没人说得清。这是我在真实需求里被坑过一次才补上的毕业设计里有这个表答辩也能加分。2. 核心业务实现与难点2.1 房态状态机设计与流转控制房态是整个系统的心脏。我定义的状态有空闲、已预订、入住中、打扫中、维修中。状态流转必须严格限制不能出现入住中直接跳到空闲这种非法操作。我用一个枚举类来管public enum RoomStatus { FREE(0, 空闲), RESERVED(1, 已预订), OCCUPIED(2, 入住中), CLEANING(3, 打扫中), MAINTENANCE(4, 维修中); private final int code; private final String desc; // 合法的状态流转 public static boolean canChange(int from, int to) { switch (from) { case 0: return to 1 || to 2; // 空闲可预订、可入住 case 1: return to 2 || to 4; // 已预订可入住、可转维修 case 2: return to 3 || to 4; // 入住中只能退房后打扫或维修 case 3: return to 0 || to 4; // 打扫完变空闲或转维修 case 4: return to 0; // 维修完变空闲 default: return false; } } }实际开发中状态流转要放在Service层统一处理不能直接在Controller里随意调用setStatus。我习惯写一个RoomStatusService.changeStatus(roomId, newStatus, operator)方法内部先查旧状态校验合法性再更新再写日志。这样无论哪个接口触发状态变化逻辑都是收敛的。2.2 防超卖订房时的并发控制酒店客房最怕超卖——两个客人在同一时间订同一间房。用普通update room set status 1 where id ?很容易出问题。我第一次做的时候用synchronized单机行但后来部署集群就废了。正确做法是数据库乐观锁或Redis分布式锁。我的方案是双保险第一层数据库乐观锁。在room表加一个version字段Version private Integer version;更新时MyBatis-Plus会自动带上version条件更新的行数返回0就说明冲突。第二层Redis分布式锁锁的key用room:lock:{roomId}boolean locked redisTemplate.opsForValue() .setIfAbsent(room:lock: roomId, 1, Duration.ofSeconds(10));拿到锁后再查一次房间状态确认还是空闲才创建订单。订完房要记得释放锁而且要在finally里释放防止Redis宕锁死。另外预订日期范围也要校验。一个房间某天被订了那这天的状态就该是不可订。我建了一张room_calendar表按天记录每个房间当天是否可售判断冲突时就查这张表比直接查订单表快得多。2.3 价格策略与房费计算房费计算看着简单实际有很多边界。比如钟点房按小时算全日房按夜算跨天怎么算取消订单要不要扣费我的实现是把价格策略做成策略模式平日价周一至周四周末价周五至周日节假日价单独维护可以覆盖默认价格房费计算的核心就是按天拆段每天应用不同价格。比如客人5月1日入住5月3日退房那么5月1日按节假日价5月2日按节假日价5月3日不收费退房日不计。逻辑写出来也不复杂public BigDecimal calcRoomFee(LocalDate checkIn, LocalDate checkOut, Long roomTypeId) { BigDecimal total BigDecimal.ZERO; for (LocalDate date checkIn; date.isBefore(checkOut); date date.plusDays(1)) { total total.add(getDailyPrice(roomTypeId, date)); } return total; }但要注意价格在订单创建后就该锁定后续改房价不能影响已创建的订单。所以我设计订单表里冗余一个unit_price字段存的是每天的房价快照后续计算完全基于快照。这个经验很重要——别天真地每次计算都去查最新价格早晚被投诉。2.4 权限管理Spring Security JWT酒店系统有前台、主管、财务、管理员等角色不同角色看到的菜单和操作按钮不一样。我用Spring Security做认证和授权JWT做无状态token。核心配置就是放行登录接口和静态资源其他接口都要带tokenConfiguration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/login, /doc.html, /webjars/**).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() .and() .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); } }这里说一个容易忽略的地方Spring Security默认会过滤所有请求如果你用SwaggerKnife4j别忘记把/swagger-resources/**、/v2/api-docs也放行不然接口文档都打不开。JWT最好设置短过期时间比如2小时再加上Redis做自动续期。酒店前台收银的电脑经常挂机一天token过期用户就要重新登录体验很差。我后来改成用Redis续期——每次请求刷新过期时间超过7天没操作才强制下线。3. 项目搭建与实操过程3.1 用IDEA快速创建SpringBoot项目不管你是用IDEA还是Eclipse搭建步骤差不多。IDEA里选File - New - Project - Spring InitializerGroup填com.hotelArtifact填hotel-adminJava版本用8或11都行SpringBoot版本建议选2.7.x。别一上来选3.x很多教程和第三方库还没完全适配后面很痛苦。依赖要勾这几个Spring WebMySQL DriverMyBatis FrameworkRedisSpring SecurityLombok生成项目后先把pom.xml补上MyBatis-Plus和Knife4j依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi2-spring-boot-starter/artifactId version4.3.0/version /dependency3.2 配置文件application.yml的写法application.yml是SpringBoot的脸面。我习惯把环境相关的配置拆成多份application-dev.yml、application-prod.yml主配置里用spring.profiles.active切换。关键的配置项有这些server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 timeout: 3000ms mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0特别注意map-underscore-to-camel-case: true不然数据库字段room_number映射不到roomNumber。还有一个坑MySQL的serverTimezone不配的话会有8小时时差。以前在本地没问题放到Linux服务器上日期就乱了最后发现就是没加serverTimezoneAsia/Shanghai。3.3 MyBatis-Plus代码生成器手写实体类、Mapper、Service太浪费时间。MyBatis-Plus官方有个代码生成器但我更推荐用IDEA插件MyBatisX连数据库后右键表直接生成。生成前先把表建好字段注释写清楚生成出来的代码基本不用改。生成的实体类统一继承ModelMapper继承BaseMapperService继承IService写CRUD的代码量能省一半。3.4 核心接口设计与示例以预订房间为例接口设计如下POST /api/reservation/create请求参数{ roomIds: [101, 102], customerName: 张三, customerPhone: 13800138000, checkInDate: 2025-06-01, checkOutDate: 2025-06-03, guestCount: 2 }返回结果统一用ResultT包装public class ResultT { private Integer code; private String message; private T data; // 省略构造方法、getter/setter }创建订单的Service逻辑Transactional(rollbackFor Exception.class) public ResultReservationVO createReservation(ReservationDTO dto) { // 1. 校验日期 if (dto.getCheckInDate().isAfter(dto.getCheckOutDate())) { return Result.error(入住日期不能晚于退房日期); } // 2. 校验房间是否存在且状态可预订 for (Long roomId : dto.getRoomIds()) { boolean locked lockRoom(roomId); if (!locked) return Result.error(房间正忙请重试); try { Room room roomMapper.selectById(roomId); if (room.getStatus() ! RoomStatus.FREE.getCode()) { return Result.error(房间 room.getRoomNumber() 不可预订); } // 3. 查房态日历确认日期范围可售 int conflict roomCalendarMapper.countConflict(roomId, dto.getCheckInDate(), dto.getCheckOutDate()); if (conflict 0) return Result.error(房间 room.getRoomNumber() 该时间段已被预订); // 4. 更新房间状态为“已预订” changeRoomStatus(roomId, RoomStatus.RESERVED.getCode(), 前台预订); // 5. 生成订单记录 Reservation reservation buildReservation(dto, room); reservationMapper.insert(reservation); } finally { unlockRoom(roomId); } } return Result.success(null); }注意这里用了Transactional因为要保证扣库存 创建订单是原子操作。如果两台服务同时请求即使代码里加了分布式锁事务也要能兜底。用户量不大时Transactional加乐观锁完全够用。3.5 前后端分离与跨域处理前端用Vue后端用SpringBoot必然遇到跨域。我是这么解决的写一个全局CORS配置类而不是在Controller上加CrossOrigin统一管理更干净。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }如果用了Spring SecurityOPTIONS预检请求要在SecurityConfig里放行不然前端所有跨域请求都被拦截这个坑我排查了一下午。4. 常见问题与排查技巧4.1 SpringBoot版本过高导致的问题前面我强调选2.7.x是有原因的。有次我用SpringBoot 3.2.0结果MyBatis-Plus还没适配jakarta.*包项目直接启动报错。新版本把javax改成了jakarta很多老依赖全废。热词里提到springboot版本太高idea新建项目没有springboot 3.4.3选项——版本太新反而不好找学习资料和兼容库。我的建议生产环境选SpringBoot 2.7.18这是2.x最后一个版本稳定且到了维护末期坑基本都被填平了。等生态完全迁到3.x再升级也不迟。4.2 事务不生效的经典场景事务失效最常见的三种原因类内部调用this.method()被调用时Spring AOP拦截不到因为代理对象没走进去。方法不是publicTransactional默认只对public方法生效不是public直接静默失效。异常被捕获吞掉try-catch把异常吃了事务不知道该回滚。我处理的方案很简单统一规范事务注解只加在Service的public方法上内部调用通过Autowired注入自己或者把方法拆到另一个Service。异常抛到顶层Controller由全局异常处理器统一包装。4.3 Redis缓存与数据库一致性问题房间状态如果频繁变更缓存很容易脏读。我一开始做的是先更新数据库再删除缓存但并发高时还是会有旧值写回。后来我把房态缓存做成短TTL比如5分钟。酒店系统的实时性要求没那么高短TTL完全够用还能避免复杂的消息队列方案。具体操作查询房态时先走Redis缓存没有就查库并写入Redis设置过期时间。更新时直接删除对应的缓存key下次查询重新加载。4.4 大文件上传下载问题有些酒店系统要上传身份证照片、合同扫描件SpringBoot默认单文件大小限制是1MB肯定不够。在配置里调整spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB上传后的文件不要存数据库存磁盘或OSS数据库只存路径。文件下载推荐用ResponseEntity支持断点续传和在线预览比HttpServletResponse直接写流更好用。4.5 接口安全对接与API Key热词里提到apikey安全对接和token验证避开——这类场景一般出现在开放接口给第三方系统调用时。酒店系统如果对接OTA平台携程、美团、飞猪不能直接把JWT发给第三方因为JWT有有效期且不好撤销。我的做法是给第三方分配一对appId和appSecret请求时带上appId、时间戳、随机数和签名。签名规则是MD5(appId timestamp nonce appSecret)服务端用同样的算法校验同时判断时间戳超过5分钟就拒绝。这样即使用户token泄露第三方也无法绕过权限。SpringBoot实现固定sign校验的AOP拦截器即可不复杂。5. 部署上线与扩展思考5.1 打jar包与Linux部署SpringBoot部署最方便的就是打可执行jar包。在项目根目录执行mvn clean package -DskipTests然后就会在target目录下生成hotel-admin.jar。直接丢到服务器上java -jar hotel-admin.jar --spring.profiles.activeprod但这样启动后进程会占住终端关闭SSH就断。用nohupnohup java -jar hotel-admin.jar --spring.profiles.activeprod hotel.log 21 如果要守护进程我推荐用systemd配置一个hotel.service单元文件[Unit] DescriptionHotel Admin Service Afternetwork.target [Service] Useradmin ExecStart/usr/bin/java -jar /opt/hotel/hotel-admin.jar --spring.profiles.activeprod Restartalways RestartSec5 [Install] WantedBymulti-user.target这样服务挂掉会自动拉起来比手写shell脚本靠谱多了。5.2 Docker部署SpringBoot项目Docker部署更干净写一个DockerfileFROM openjdk:8-jre-alpine WORKDIR /app COPY hotel-admin.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar, --spring.profiles.activeprod]构建镜像docker build -t hotel-admin:1.0 .运行docker run -d --name hotel-admin -p 8080:8080 --restartalways \ -v /etc/hotel/application-prod.yml:/app/application-prod.yml \ hotel-admin:1.0配置文件和jar包分离方便换环境不用重新打镜像。5.3 国产化环境适配信创热词里提到改成信创的话是否需要东方通的tongweb。如果你所在的单位要求信创环境SpringBoot默认内嵌Tomcat确实不能直接用需要把打好的war包部署到东方通TongWeb上。改动并不大将spring-boot-starter-web的依赖排除内嵌Tomcat把启动类继承SpringBootServletInitializer并重写configure方法打包方式由jar改为war但有个隐含问题SpringBoot3.x基于Jakarta规范TongWeb等国产中间件需要确认是否支持对应版本。我了解到的TongWeb 7.0开始支持Servlet 4.0对应javax命名空间支持Jakarta的可能要TongWeb 7.0及以上特定版本。所以如果你有信创适配需求建议SpringBoot选2.7.x兼容性会好很多。数据库方面MySQL如果被要求换成达梦或者人大金仓MyBatis-Plus有专门的mybatis-plus-dialect扩展但要在方言配置里切换。我还没实际换过但提前把SQL写标准一点少用MySQL独有的ON DUPLICATE KEY UPDATE这类语法能降低迁移成本。5.4 K8s部署与运维热词里提到k8s部署springboot项目和springboot admin怎么一键部署。K8s部署比Docker复杂核心是写Deployment和Service。我用的部署文件大致是apiVersion: apps/v1 kind: Deployment metadata: name: hotel-admin spec: replicas: 2 selector: matchLabels: app: hotel-admin template: metadata: labels: app: hotel-admin spec: containers: - name: hotel-admin image: registry.example.com/hotel-admin:1.0 ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: prod livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10这里要提前引入spring-boot-starter-actuator暴露健康检查端点K8s才能知道服务是不是活着。5.5 SpringBoot Admin监控如果不想上K8s那套又想要监控SpringBoot Admin是个好东西。它分服务端和客户端服务端是一个独立的SpringBoot应用客户端就是你的业务系统。启动后在服务端界面就能看到内存、线程、请求映射、日志级别这些信息。配置很简单客户端引入dependency groupIdde.codecentric/groupId artifactIdspring-boot-admin-starter-client/artifactId version2.7.10/version /dependency然后在application.yml里指定服务端地址同时在服务端开启安全认证。这个监控不需要额外付费比裸奔强太多了。6. 资源映射与静态文件处理6.1 SpringBoot的资源映射配置热词里提到springboot 如何做资源映射。在做酒店系统时客人身份证照片、房间照片、合同PDF这些文件通常存在服务器某个目录下不在classpath里。默认SpringBoot无法通过URL直接访问外部目录需要配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:/opt/hotel/upload/); } }这样请求http://localhost:8080/upload/room/101.jpg就能映射到/opt/hotel/upload/room/101.jpg。这个配置很简单但我见过不少人卡在这里以为SpringBoot不支持外部静态资源其实是没配addResourceHandlers。6.2 文件下载内容Base64编码问题热词里有springboot下载文本文档, 为啥文档内容是base64.encode之后的内容。这个问题很有意思通常出现在前后端分离时后端把文件转成Base64字符串返回给前端前端再解码成文件。但如果你直接请求下载接口浏览器收到的是纯文本内容就是Base64编码后的数据看起来像乱码。正确做法是区分场景如果是给前端提供文件供下载应设置Content-Type: application/octet-stream和Content-Disposition: attachment; filenamexxx.txt用HttpServletResponse写原始字节流。如果是给前端展示图片可以直接返回文件URL由前端img src加载。如果一定要用Base64应该返回JSON结构{ fileName: a.txt, base64: ... }前端再解码并处理好大文件内存压力。大多数情况下我推荐直接返回URL简单直接还能利用Nginx做静态文件缓存和限流。7. 热点问题与面试延伸7.1 SpringBoot与SpringCloud的区别热词群里反复出现面试题springboot与springcloud区别确实这是Java后端的必考题。放在酒店系统这个场景里如果你做的是单体酒店管理系统用一个SpringBoot应用就够了。但如果将来要扩展成酒店集团多个酒店独立部署各个服务之间需要服务发现、配置中心、网关、负载均衡那就要上SpringCloud。简单粗暴地理解SpringBoot是构建单个微服务的脚手架SpringCloud是管理一堆SpringBoot微服务的整体方案。它们不是二选一而是底层和上层的关系。面试时候这么说面试官一般会点头。7.2 SpringBoot常用注解盘点做酒店系统时我常用的注解就这些RestController/Controller控制层RequestMapping/GetMapping/PostMapping路由映射RequestBody/RequestParam/PathVariable参数绑定Service/Repository业务层和持久层注册Autowired/Resource依赖注入Configuration/Bean配置类Value/ConfigurationProperties配置属性绑定Transactional事务管理Aspect/AroundAOP切面EnableScheduling/Scheduled定时任务定时任务在酒店系统里很实用比如每天早上6点自动释放超时未支付的预订订单半夜自动备份数据给VIP客户发入住提醒都可以用Scheduled来做。7.3 事件机制在业务解耦中的应用热词里提到springboot事件机制,如何监听某个事件。酒店系统里典型的场景是客人退房后需要同时做账务结算、触发房间打扫、更新会员积分、发送离店短信。如果把这些逻辑都塞在退房方法里代码会越来越长而且任何一步报错会影响主流程。SpringBoot的事件机制可以完美解耦。我定义一个CheckOutEventpublic class CheckOutEvent extends ApplicationEvent { private final Reservation reservation; public CheckOutEvent(Object source, Reservation reservation) { super(source); this.reservation reservation; } }退房时只发布事件applicationEventPublisher.publishEvent(new CheckOutEvent(this, reservation));然后在别的类里监听EventListener public void handleCheckOut(CheckOutEvent event) { roomService.triggerCleaning(event.getReservation().getRoomId()); }这样退房主流程就不关心后置动作以后加新动作只需要新增监听器不用改老代码。这个设计如果写进简历会让项目显得有思考。8. 一些实操中容易踩的坑再补充几个我实际开发中踩过、网上又很少提到的坑。8.1 时间日期处理酒店系统对日期极其敏感。前端传2025-06-01这种字符串后端如果用Date接收很容易出现时区问题和格式问题。我建议全项目统一用LocalDate和LocalDateTime。在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8接口参数用DateTimeFormat(pattern yyyy-MM-dd)这样才能保证前端传的字符串被正确解析。8.2 逻辑删除与唯一约束冲突很多表需要逻辑删除但逻辑删除后像room_number这种唯一字段再插入相同房间号时数据库唯一索引会冲突。解决办法是唯一索引不建在room_number单列上而是建(room_number, deleted)组合唯一索引。MyBatis-Plus的逻辑删除字段默认是0和1你可以改成deleted_time作为标记或者用全局唯一ID避免冲突。8.3 Swagger分组与生产环境关闭接口文档用于前后端联调很爽但生产环境暴露给外部是安全隐患。我一般在配置里加一个开关knife4j: enable: true然后配合Spring profile生产环境把enable设为false或者用Profile(dev)标注配置类。别为了省事把文档一直开着。8.4 基于SpringBoot的PDF XSS攻击防护热词里提到springboot解决pdf xss攻击。这个场景一般是在PDF在线预览时PDF内容可能包含恶意JavaScript。如果用的是浏览器的PDF插件XSS风险取决于浏览器内核。如果是自己开发PDF解析渲染注意对文件名、文本内容做HTML转义同时对外部传入的PDF文件做安全检查比如用Apache PDFBox校验格式防止恶意文件上传和执行。这块不是SpringBoot特有的但SpringBoot里容易忽略我提一句。写在最后做一套基于SpringBoot的酒店客房管理系统从表面看就是一个后台管理项目但真要做好涉及事务、并发、缓存、权限、部署每一个环节。我个人体会最深的是不要急着写代码先把房态状态机和预订流程在纸上画清楚否则后面改起来会让你怀疑人生。另外项目一定要留一手——比如加一层操作日志表、加一个缓存开关、把文件存储路径做成可配置这些都是在真实项目中被逼出来的经验。如果你正准备做毕业设计或者练手项目我建议从SpringBoot 2.7开始把用户登录、订房下单、退房结算这三条主链路跑通再慢慢加花活。数据库表设计、状态流转、防并发这几个点是答辩时最容易被追问的提前把思路理清楚答起来会自信很多。最后再分享一个小技巧开发时把MyBatis的SQL日志打开也就是log-impl: org.apache.ibatis.logging.stdout.StdOutImpl所有执行的SQL、参数、结果都会打印在控制台排查问题时你会感谢自己开了这个配置。等上线前再关掉就行。本文还有配套的精品资源点击获取