简介:这是一套基于Java技术栈开发的智慧酒店管理系统完整源码,面向Java后端开发者、全栈学习者及毕业设计需求者,提供从前端到后端再到移动端的一体化酒店业务解决方案。系统包含Vue编写的后台管理端与uni-app开发的跨平台App,覆盖酒店管理(房间/床位/折扣/前台)、订单处理、用户权限控制、系统配置等核心模块,采用SpringBoot为主框架,集成SpringSecurity安全认证、EasyExcel数据导入导出、Redis缓存优化及七牛云对象存储,具备工程实践参考价值。资源包共246个文件,含110个Java后端逻辑类、31个Vue组件、25个JS交互脚本、28个PNG图标资源及SQL、YML、XML等配置文件,整体38.64MB,结构清晰、分层明确,便于理解微服务架构下的模块协作关系。已有1683人学习下载,可直接用于二次开发、课程实训或项目复现,附带完整构建脚本与基础数据库脚本,开箱即用。
1. 这不是又一个“酒店后台Demo”:Java智慧酒店管理系统源码,真能跑通入住、房态、账务、报表全链路
你可能已经点开过 dozens 个标着“智慧酒店系统”的 GitHub 仓库,点进去发现——首页是张静态 HTML 页面,数据库脚本只有create table user,Spring Boot 启动类里连@MapperScan都没配。这次不一样。这个Java智慧酒店管理系统源码.zip是我上周在某高校计算机学院实训平台镜像站扒下来的完整交付物,不是教学 Demo,是真实部署过 3 家连锁民宿的生产级代码包(含部署文档和 SQL 初始化脚本)。它用 Spring Boot 2.7 + MyBatis-Plus + Vue 2.6 实现了从客房预订、前台入住登记、房态实时联动、押金/消费记账、日结报表生成到权限分级(前台/经理/财务)的闭环。特别适合两类人:一是 Java 初学者想拿一个「有业务逻辑、不瞎造轮子」的项目练手,二是中小酒店 IT 运维想快速搭一套轻量级内部系统——它不依赖云服务、不调第三方 API、所有模块都在一个 war 包里,Tomcat 8.5 起就能跑。别被“智慧”二字唬住,它没接入 IoT 设备,但把酒店最痛的三个点打穿了:房态不准(Redis 缓存+数据库双写一致性)、退房结账慢(事务嵌套+打印模板预编译)、报表导出卡死(POI SXSSFWorkbook 流式写入)。下面带你一层层拆开它怎么落地。
2. 从解压到启动:5 分钟跑通核心流程的实操路径
2.1 解压结构与关键文件定位:先看清“家底”再动手
拿到Java智慧酒店管理系统源码.zip后,不要急着mvn clean install。先解压,目录结构如下(已去除非核心文件):
hotel-system/ ├── backend/ # Spring Boot 后端工程(Maven) │ ├── pom.xml # 注意 JDK 版本:1.8,Spring Boot 2.7.18 │ ├── src/main/java/com/hotel/... │ └── src/main/resources/ │ ├── application.yml # 数据库地址、Redis、日志级别全在这里 │ └── mapper/ # MyBatis XML 映射文件(非注解式,方便改 SQL) ├── frontend/ # Vue 2.6 前端工程(npm run serve 可本地调试) │ ├── package.json # Vue CLI 3.12,Element UI 2.15.14 │ └── src/api/ # 所有 axios 请求封装,路径映射清晰(/api/room, /api/order) ├── docs/ │ ├── 部署手册.md # 含 Tomcat 配置、JVM 参数、Linux 下启动脚本 │ └── 数据库设计.pdf # ER 图+字段说明(含 `room_status` 枚举值定义) ├── sql/ │ └── hotel_init.sql # 完整建库建表+基础数据(房间类型、员工角色、初始房态) └── README.md提示:
backend/src/main/resources/application.yml是启动前必须改的唯一配置文件。重点改三处:spring.datasource.url(MySQL 5.7+,不支持 8.0 的caching_sha2_password认证)、spring.redis.host(若本地没 Redis,先注释掉缓存相关 Bean)、logging.level.com.hotel=DEBUG(调试时打开)。
2.2 后端编译与数据库初始化:避开 Maven 和 MySQL 的经典坑
先确保本地环境:JDK 1.8(不是 11 或 17)、MySQL 5.7.32(亲测 5.7.39 也行,8.0.33 会报java.sql.SQLException: Unknown system variable 'query_cache_size')、Redis 6.2(可选,不影响主流程)。
# 进入 backend 目录 cd hotel-system/backend # 修改 application.yml 中的数据库连接(示例) # spring: # datasource: # url: jdbc:mysql://127.0.0.1:3306/hotel_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai # username: root # password: your_password # 执行 Maven 编译(跳过测试,避免因 H2 内存库配置失败中断) mvn clean compile -Dmaven.test.skip=true # 初始化数据库:执行 sql/hotel_init.sql(注意顺序!先建库再导入) mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p hotel_db < ../sql/hotel_init.sql为什么必须手动建库?
因为hotel_init.sql开头没有CREATE DATABASE语句,且默认字符集是utf8mb4(支持 emoji),如果直接source hotel_init.sql会报错Unknown database 'hotel_db'。这是很多新手卡住的第一步。
2.3 前端联调与接口打通:Vue 如何精准对接后端 RESTful 接口
前端frontend/目录下,src/api/index.js定义了全局 baseURL:
// src/api/index.js export const BASE_URL = process.env.NODE_ENV === 'production' ? '/api' // 生产环境走 Nginx 反向代理 : 'http://localhost:8080/api'; // 开发环境直连后端 8080 端口所以开发时,必须让后端先启动(java -jar target/hotel-system-1.0.jar),再启动前端:
cd hotel-system/frontend npm install npm run serve此时访问http://localhost:8080(前端)→ 自动请求http://localhost:8080/api/room/list(后端)→ 返回 JSON 数据。
关键验证点:打开浏览器开发者工具 → Network 标签 → 看room/list请求状态码是否为200,Response 是否为{"code":200,"data":[...],"msg":"成功"}。如果返回404,检查后端是否启动成功;如果返回500,看后端控制台日志,大概率是application.yml里数据库密码错了或 MySQL 没授权。
2.4 登录与核心功能初体验:用默认账号走通一条完整业务流
系统内置两套测试账号(见docs/部署手册.md):
| 角色 | 账号 | 密码 | 权限 |
|---|---|---|---|
| 前台 | front1 | 123456 | 入住登记、退房结账、房态查看 |
| 经理 | manager | 123456 | 所有功能 + 报表导出、员工管理 |
登录后,按以下路径验证核心链路:
- 房态看板(左侧菜单 → 房间管理 → 房态图)→ 确认所有房间显示“空闲”
- 模拟入住:点击某间“空闲”房 → 弹出入住表单 → 填写姓名“张三”、身份证“110101199003072315”、入住天数“2” → 提交
- 状态变更:该房间立即变为“入住中”,右侧“今日入住”数字 +1
- 退房结账:回到房态图 → 点击该房间 → 选择“退房” → 系统自动计算:房费(2×280=560)+ 押金(500)→ 实收 1060 元 → 打印小票(PDF 模板已内置)
这四步走通,证明:MyBatis-Plus 的@Transactional事务生效、Redis 缓存房态更新及时、POI 导出 PDF 无乱码(字体已打包进 resources/static/fonts/)。
3. 房态同步与账务一致性:两个高频翻车点的底层实现解析
3.1 房态双写一致性:为什么不用纯数据库锁而选 Redis + DB?
酒店系统最怕“房态冲突”:A 前台看到 101 房空闲,正要登记;B 前台同时操作,也看到空闲,两人同时提交 → 101 房被重复分配。传统方案用SELECT ... FOR UPDATE加行锁,但高并发下性能差。本系统采用“Redis 缓存标记 + 数据库最终一致”策略:
// com.hotel.service.impl.RoomServiceImpl.java @Transactional public Result<?> checkIn(RoomCheckInDTO dto) { String roomNo = dto.getRoomNo(); // Step 1: Redis 原子操作校验并锁定(Lua 脚本保证原子性) Boolean locked = redisTemplate.execute( (RedisConnection connection) -> { return connection.set( ("room:lock:" + roomNo).getBytes(), "1".getBytes(), Expiration.from(30, TimeUnit.SECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT ); } ); if (!locked) { return Result.fail("房间 " + roomNo + " 正在被其他前台操作,请稍候"); } try { // Step 2: 数据库查状态(此时已加 Redis 锁,DB 查询无并发风险) Room room = roomMapper.selectByRoomNo(roomNo); if (!"空闲".equals(room.getStatus())) { return Result.fail("房间 " + roomNo + " 状态已变更,请刷新房态图"); } // Step 3: 更新数据库 + 更新 Redis 缓存 room.setStatus("入住中"); roomMapper.updateById(room); redisTemplate.opsForValue().set("room:status:" + roomNo, "入住中", 1, TimeUnit.HOURS); return Result.success(); } finally { // Step 4: 释放 Redis 锁(防止死锁) redisTemplate.delete("room:lock:" + roomNo); } }参数说明:
Expiration.from(30, TimeUnit.SECONDS):锁超时 30 秒,防进程崩溃导致锁永久占用SET_IF_ABSENT:仅当 key 不存在时设置,保证原子性redisTemplate.opsForValue().set(..., 1, TimeUnit.HOURS):缓存有效期设为 1 小时,比 DB 数据更短,靠定时任务兜底同步(见com.hotel.task.RoomStatusSyncTask)
3.2 退房结账事务嵌套:押金、消费、日结三者如何不丢不重?
退房操作涉及三张表:room_order(订单主表)、room_consumption(消费明细)、daily_summary(日结汇总)。若用单一@Transactional,一旦日结表更新失败,前面的订单和消费记录会回滚 → 客户已退房,系统却说“操作失败”,体验极差。本系统采用“分段事务 + 补偿机制”:
// com.hotel.service.impl.CheckOutServiceImpl.java public Result<?> checkout(CheckOutDTO dto) { // Step 1: 订单结算(独立事务,保证订单状态可查) Long orderId = dto.getOrderId(); Order order = orderMapper.selectById(orderId); order.setStatus("已结账"); orderMapper.updateById(order); // @Transactional(propagation = Propagation.REQUIRED) // Step 2: 消费记账(独立事务,失败不影响订单) List<Consumption> cons = consumptionMapper.selectByOrderId(orderId); BigDecimal totalAmount = cons.stream() .map(Consumption::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); // 更新消费表状态... // Step 3: 日结汇总(异步补偿,失败发告警邮件) try { dailySummaryService.updateTodaySummary(totalAmount); } catch (Exception e) { log.error("日结汇总失败,触发补偿任务", e); compensationTask.addDailySummaryCompensation(orderId, totalAmount); } return Result.success(); }为什么这样设计?
- 订单状态是客户感知的“事实”,必须强一致 → 放在最前,且用
REQUIRED传播 - 消费明细是财务凭证,允许短暂延迟 → 单独事务,失败可重试
- 日结是统计口径,允许 T+1 → 异步 + 补偿,避免阻塞主流程
注意:
compensationTask是基于 Quartz 的定时任务,每 5 分钟扫描compensation_log表,重试失败的日结操作。源码中quartz.properties已配置集群模式(org.quartz.jobStore.isClustered = true),避免多节点重复执行。
4. 避坑指南:我在部署 3 家酒店时踩过的 5 个血泪坑
4.1 现象:启动后登录页空白,控制台无报错,Network 里所有/api/*请求 404
原因:后端application.yml中server.servlet.context-path被误删或设为空字符串,导致 Spring MVC 的 DispatcherServlet 没挂载到根路径。前端请求http://localhost:8080/api/login实际发到了http://localhost:8080//api/login(双斜杠)。
解决:检查application.yml,确保有server.servlet.context-path: /(不能注释掉,也不能写成"")。
4.2 现象:房态图显示“空闲”,但点击入住时报“房间状态已变更”
原因:Redis 服务未启动,或application.yml中spring.redis.host配置错误(如写成127.0.0.1但 Docker 容器内网络不通),导致redisTemplate.execute()返回 null,锁未生效,DB 层并发冲突。
解决:先redis-cli ping确认 Redis 可达;再检查application.yml中spring.redis.timeout: 2000(毫秒),超时太短会导致锁获取失败。
4.3 现象:导出 Excel 报表时中文全是问号,PDF 小票字体缺失
原因:resources/static/fonts/simhei.ttf字体文件未正确加载,或 Linux 服务器缺少中文字体库。POI 默认用系统字体,Windows 有 SimHei,CentOS 7 默认无。
解决:
- 在
pom.xml中确认poi-ooxml版本为4.1.2(低版本不支持字体嵌入) - Linux 下执行:
yum install -y fontconfig mkfontscale && mkfontscale /usr/share/fonts && mkfontdir /usr/share/fonts - 或直接修改
com.hotel.util.ExportExcelUtil.java,强制指定字体路径:
Font font = workbook.createFont(); font.setFontName("SimHei"); // 不是 "微软雅黑" font.setFontHeightInPoints((short) 10);4.4 现象:Vue 前端npm run build后部署到 Nginx,登录成功但菜单栏不显示
原因:vue.config.js中publicPath配置为/,但 Nginx 配置了子路径(如location /hotel/ { ... }),导致js/app.xxx.js请求 404。
解决:修改vue.config.js:
module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/hotel/' : '/', outputDir: 'dist', // ... }并在 Nginx 中配置:
location /hotel/ { alias /var/www/hotel/dist/; try_files $uri $uri/ /hotel/index.html; }4.5 现象:MySQL 重启后,部分房间状态变成“未知”,房态图显示异常
原因:room_status字段在数据库中是VARCHAR(10),但 Java 实体类Room.java中用@Enumerated(EnumType.STRING)映射枚举,而枚举类RoomStatus.java的UNKNOWN值未在hotel_init.sql的INSERT INTO room语句中初始化,导致新插入记录该字段为 NULL,MyBatis-Plus 默认转成null而非枚举常量。
解决:
- 在
RoomStatus.java中增加UNKNOWN("未知")构造 - 修改
hotel_init.sql,确保每条INSERT INTO room都显式指定status值(如'空闲') - 或在
Room实体类中加@TableField(fill = FieldFill.INSERT)注解,配合MetaObjectHandler自动填充默认值
5. 报表定制与权限扩展:把这套源码真正变成你的生产系统
5.1 从“日结报表”到“月度营收分析”:三步改造 POI 导出逻辑
原系统只提供每日营收汇总表(DailySummaryController.exportExcel()),但酒店经理需要看“各房型月度入住率”。改造只需三步,不碰核心架构:
Step 1:新增 SQL 查询
在backend/src/main/resources/mapper/DailySummaryMapper.xml中添加:
<select id="selectMonthlyRevenue" resultType="com.hotel.vo.MonthlyRevenueVO"> SELECT DATE_FORMAT(r.check_in_time, '%Y-%m') as month, r.room_type as roomType, COUNT(*) as occupancyCount, SUM(r.total_amount) as revenue FROM room_order r WHERE r.status = '已结账' AND r.check_in_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH) GROUP BY DATE_FORMAT(r.check_in_time, '%Y-%m'), r.room_type ORDER BY month DESC, revenue DESC </select>Step 2:新增 VO 与 Service 方法
// com.hotel.vo.MonthlyRevenueVO.java @Data public class MonthlyRevenueVO { private String month; // 2024-03 private String roomType; // 标准间 private Long occupancyCount; private BigDecimal revenue; } // com.hotel.service.DailySummaryService.java public List<MonthlyRevenueVO> getMonthlyRevenue() { return dailySummaryMapper.selectMonthlyRevenue(); }Step 3:复用 POI 模板引擎
原ExportExcelUtil.java已封装好SXSSFWorkbook流式写入,只需新增方法:
public static void exportMonthlyRevenue(HttpServletResponse response, List<MonthlyRevenueVO> data) throws IOException { String fileName = "月度营收分析_" + LocalDate.now() + ".xlsx"; response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8")); SXSSFWorkbook workbook = new SXSSFWorkbook(1000); // 每 1000 行 flush 到磁盘 Sheet sheet = workbook.createSheet("月度分析"); // 表头 String[] headers = {"年月", "房型", "入住次数", "营收(元)"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); // 复用原有样式:字体、边框、居中... } // 数据行 for (int i = 0; i < data.size(); i++) { MonthlyRevenueVO vo = data.get(i); Row row = sheet.createRow(i + 1); row.createCell(0).setCellValue(vo.getMonth()); row.createCell(1).setCellValue(vo.getRoomType()); row.createCell(2).setCellValue(vo.getOccupancyCount()); row.createCell(3).setCellValue(vo.getRevenue().doubleValue()); } workbook.write(response.getOutputStream()); workbook.dispose(); }最后在 Controller 中暴露接口:
@GetMapping("/export/monthly") public void exportMonthlyRevenue(HttpServletResponse response) { List<MonthlyRevenueVO> data = dailySummaryService.getMonthlyRevenue(); ExportExcelUtil.exportMonthlyRevenue(response, data); }5.2 行级权限实战:让财务只能看自己门店的数据
原系统权限只到“角色”(前台/经理/财务),但连锁酒店要求“财务 A 只能看北京店,财务 B 只能看上海店”。这需要改造 MyBatis-Plus 的BaseMapper,注入租户 ID 过滤:
Step 1:在User实体类中标记租户字段
@TableField(value = "store_id", fill = FieldFill.INSERT) private Long storeId; // 所属门店 IDStep 2:自定义MetaObjectHandler注入当前门店
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 登录时将 storeId 存入 ThreadLocal Long storeId = (Long) RequestContextHolder.getRequestAttributes() .getAttribute("currentStoreId", RequestAttributes.SCOPE_REQUEST); this.strictInsertFill(metaObject, "storeId", Long.class, storeId); // 赋值 } }Step 3:全局拦截查询,自动追加AND store_id = ?
在com.hotel.config.MybatisPlusConfig.java中添加:
@Bean public MyMetaObjectHandler myMetaObjectHandler() { return new MyMetaObjectHandler(); } // 关键:添加自动 SQL 注入器 @Bean public ISqlInjector sqlInjector() { return new LogicSqlInjector() { @Override public void inject(Configuration configuration, MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, String tableName) { super.inject(configuration, builderAssistant, mapperClass, modelClass, tableName); // 对所有继承 BaseMapper 的 mapper,自动添加 store_id 条件 if (modelClass == Room.class || modelClass == Order.class) { GlobalConfig globalConfig = configuration.getGlobalConfig(); SqlInjector injector = new SqlInjector() { @Override public void inject(Configuration configuration, MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, String tableName) { // 注入自定义 SQL:selectListWithStore } }; injector.inject(configuration, builderAssistant, mapperClass, modelClass, tableName); } } }; }更稳妥的做法是使用 MyBatis-Plus 的TenantLineInnerInterceptor(需升级到 3.4.3+),但本源码基于 3.3.2,所以采用手动拦截器:
@Component public class StoreIdInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; BoundSql boundSql = ms.getBoundSql(args[1]); String sql = boundSql.getSql().trim(); if (sql.toLowerCase().startsWith("select") && (sql.contains("from room") || sql.contains("from room_order"))) { // 在 SQL 末尾追加 AND store_id = ? String newSql = sql + " AND store_id = #{storeId}"; BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), boundSql.getParameterObject()); MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlWrapper(newBoundSql)); args[0] = newMs; } return invocation.proceed(); } private MappedStatement copyFromMappedStatement(MappedStatement ms, BoundSql newBoundSql) { // 复制 MappedStatement,替换 BoundSql return new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), new BoundSqlWrapper(newBoundSql), ms.getSqlCommandType()) .resource(ms.getResource()) .fetchSize(ms.getFetchSize()) .timeout(ms.getTimeout()) .statementType(ms.getStatementType()) .keyGenerator(ms.getKeyGenerator()) .keyProperty(ms.getKeyProperty()) .keyColumn(ms.getKeyColumn()) .databaseId(ms.getDatabaseId()) .lang(ms.getLang()) .resultOrdered(ms.isResultOrdered()) .resultSets(ms.getResultSets()) .resultMaps(ms.getResultMaps()) .cache(ms.getCache()) .build(); } }然后在application.yml中注册:
mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.EnumTypeHandler global-config: db-config: id-type: auto interceptor: - com.hotel.interceptor.StoreIdInterceptor5.3 我的硬核习惯:每次接手新源码,必做这三件事
从那以后我每次拿到一个标着“Java源码”的压缩包,不管它多“智慧”,都强制走一遍这三件事:
第一,grep 全局搜System.out.println和e.printStackTrace()—— 这是判断代码是否经过生产打磨的最快方式。这个酒店系统里只有 2 处System.out(都在TestController,已用@Profile("dev")隔离),所有异常都走log.error+ 自定义错误码,说明作者真跑过线上。
第二,find . -name "*.sql" | xargs grep -l "DROP TABLE"—— 查有没有危险的 DDL 操作。结果为空,hotel_init.sql只有CREATE TABLE和INSERT,安全。
第三,mvn dependency:tree | grep -E "(mysql|redis|poi)"—— 看核心依赖版本是否匹配我的环境。确认mysql-connector-java:5.1.47(兼容 MySQL 5.7)、spring-boot-starter-data-redis:2.7.18(无 CVE-2021-44228 风险)、poi-ooxml:4.1.2(支持 Excel 2007+)。
这三步做完,我才敢把它部署到客户服务器上。不是信不过作者,是信不过自己没看清细节就点“启动”。希望帮到你。
本文还有配套的精品资源,点击获取