首页 / 资讯中心 / 文章详情

JavaEE酒店管理系统:SSM实战+房态管理+报表导出

JavaEE酒店管理系统:SSM实战+房态管理+报表导出 ★ FEATURED ARTICLE
简介这是一套面向计算机专业本科生的JavaEE毕业设计实战项目——小型酒店管理系统聚焦企业级Web应用开发全流程帮助学习者掌握SSHSpringStrutsHibernate框架整合与多层架构实践。资源包含262个文件涵盖49个核心Java源码、25个JSP页面实现用户交互与数据展示、24个XML配置文件含Spring容器、Hibernate映射及Struts路由、54个编译后class文件及54个运行依赖jar包辅以图片资源与系统配置文件整体压缩包18.69MB结构完整、开箱即用。已有1694人学习下载适合Java初学者进阶至企业开发场景可直接部署运行、调试源码、理解MVC分层逻辑并参考订单管理、房间状态调度、用户权限控制等真实业务模块的代码实现方式是SSH技术栈学习与毕设落地的高价值参考范例。1. 小型酒店管理系统毕业设计JavaEE不是Demo是能住人的系统去年帮某高校导师带毕设三个学生交上来“酒店系统”——两个连房态都刷不出来的静态页面一个用Swing写完就崩的桌面程序。直到看到这个JavaEE项目它真能跑在Tomcat上前台扫码入住、后台实时调价、退房自动结账、报表导出Excel连凌晨三点的续住提醒邮件都配好了定时任务。这不是PPT架构图是完整走通了“预订→入住→消费→退房→对账”全链路的可部署系统。适合Java初学者练手Spring MVCMyBatis整合也够得上中小型民宿/快捷酒店的实际管理需求——它没堆微服务但把事务边界划得清清楚楚没上Redis缓存但用数据库锁乐观更新扛住了并发查房连打印小票的格式都预留了POS接口。如果你正卡在“学完SSM却写不出完整业务系统”的临界点或者需要一份能过答辩、还能真上线跑三个月的毕设基线这个项目就是你该拆的第一份源码。2. 技术栈与工程结构从pom.xml到web.xml的硬核落地2.1 依赖选型为什么只用Spring MVCMyBatis而不是Spring Boot项目采用传统JavaEE分层架构Controller-Service-Dao-Entity核心依赖如下!-- pom.xml 关键片段 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.31/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.11/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency提示没选Spring Boot不是技术落后而是教学场景刚需——你需要亲手配web.xml加载DispatcherServlet手动写applicationContext.xml管理事务调试SqlSessionFactoryBean初始化失败时的日志。这些“麻烦事”恰恰是理解IoC容器生命周期和MyBatis执行器原理的必经之路。我当年就是靠debugDefaultSqlSession的update()方法才真正搞懂一级缓存失效的触发条件。2.2 目录结构src/main下的真实战场src/ ├── main/ │ ├── java/ │ │ └── com/example/hotel/ │ │ ├── controller/ // Controller注解类处理HTTP请求 │ │ ├── service/ // 接口实现类Transactional标注事务边界 │ │ ├── dao/ // MyBatis Mapper接口无XML注解式 │ │ ├── entity/ // POJO含Table/Id/Column等JPA注解 │ │ └── util/ // 工具类DateUtil、EmailSender、PrintHelper │ ├── resources/ │ │ ├── application.properties // 数据库连接、邮件配置 │ │ └── log4j2.xml // 日志级别控制重点看ERROR日志输出路径 │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml // Servlet注册、字符编码过滤器、Spring上下文加载 │ │ └── spring-mvc.xml // Controller扫描、视图解析器、拦截器配置 │ ├── static/ // CSS/JS/图片无构建工具直接引用 │ └── views/ // JSP模板login.jsp, roomList.jsp等 └── test/ └── java/ // JUnit测试RoomServiceTest.java含事务回滚测试关键细节web.xml中ContextLoaderListener加载根上下文service/daoDispatcherServlet加载MVC上下文controller二者父子关系明确所有Mapper接口用Select(SELECT * FROM room WHERE status #{status})注解避免XML文件分散views/下JSP严格遵循MVC分离roomList.jsp只做展示c:forEach遍历requestScope.rooms无任何Java代码块。2.3 数据库设计一张room表如何支撑动态房价策略MySQL建表语句直击业务痛点CREATE TABLE room ( id bigint NOT NULL AUTO_INCREMENT, room_number varchar(20) NOT NULL COMMENT 房间号如A101, room_type enum(SINGLE,DOUBLE,SUITE) NOT NULL DEFAULT DOUBLE, status enum(VACANT,OCCUPIED,CLEANING,MAINTENANCE) NOT NULL DEFAULT VACANT, base_price decimal(10,2) NOT NULL DEFAULT 280.00 COMMENT 基础房价, current_price decimal(10,2) NOT NULL DEFAULT 280.00 COMMENT 当前售价促销时可变, last_cleaned datetime DEFAULT NULL COMMENT 最后清洁时间, PRIMARY KEY (id), UNIQUE KEY uk_room_number (room_number) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房价调整记录表审计用 CREATE TABLE price_history ( id bigint NOT NULL AUTO_INCREMENT, room_id bigint NOT NULL, old_price decimal(10,2) NOT NULL, new_price decimal(10,2) NOT NULL, operator varchar(50) NOT NULL COMMENT 操作人账号, operate_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_room_id (room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意current_price字段是业务关键——它允许运营人员在后台实时调价而base_price作为基准值保留。每次调价都写入price_history既满足财务审计要求又避免价格被误操作覆盖。这种“状态历史”双表设计在毕设里属于高分细节。2.4 核心配置文件application.properties里的魔鬼参数# database config jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456 # email config (for checkout notification) email.hostsmtp.qq.com email.port587 email.usernamehotel_systemqq.com email.passwordyour_app_password # 注意QQ邮箱需用独立密码非登录密码 email.fromhotel_systemqq.com # file export path (for report download) export.path/opt/hotel/reports/关键点说明serverTimezoneAsia/Shanghai必须显式指定否则MySQL 8.0会因时区问题导致datetime字段插入为0000-00-00 00:00:00邮件密码必须是QQ邮箱的“SMTP授权码”不是账户密码这是学生最容易翻车的点export.path路径需在Linux下提前创建并赋权sudo mkdir -p /opt/hotel/reports sudo chown tomcat:tomcat /opt/hotel/reports。3. 核心模块拆解一个入住请求从进入到返回的完整链路3.1 入住流程Controller → Service → Dao 的三层协作以/room/checkin接口为例看数据如何穿越三层Controller层接收参数并校验Controller RequestMapping(/room) public class RoomController { Autowired private RoomService roomService; PostMapping(/checkin) ResponseBody public Result checkIn(RequestBody CheckInRequest request) { // 1. 前端传参校验非空、手机号格式、身份证号合法性 if (!ValidatorUtil.isMobile(request.getPhone())) { return Result.fail(手机号格式错误); } if (!IDCardUtil.isValid(request.getIdCard())) { return Result.fail(身份证号不合法); } // 2. 调用Service执行业务逻辑 try { CheckInResult result roomService.checkIn(request); return Result.success(result); } catch (BusinessException e) { return Result.fail(e.getMessage()); } } }逻辑说明RequestBody自动将JSON转为CheckInRequest对象ValidatorUtil和IDCardUtil是项目自研工具类非Spring Validation——更贴近企业级参数校验习惯异常统一捕获BusinessException避免底层异常泄露。Service层事务边界与业务规则Service public class RoomServiceImpl implements RoomService { Autowired private RoomMapper roomMapper; Autowired private GuestMapper guestMapper; Autowired private OrderMapper orderMapper; Override Transactional(rollbackFor Exception.class) public CheckInResult checkIn(CheckInRequest request) { // 1. 查询房间状态加行锁防止并发抢房 Room room roomMapper.selectByIdForUpdate(request.getRoomId()); if (!VACANT.equals(room.getStatus())) { throw new BusinessException(房间已被占用或正在清洁); } // 2. 插入客人信息身份证号唯一索引防重复 Guest guest new Guest(); guest.setIdCard(request.getIdCard()); guest.setName(request.getName()); guest.setPhone(request.getPhone()); guestMapper.insert(guest); // 3. 创建订单关联房间、客人、入住时间 Order order new Order(); order.setRoomId(request.getRoomId()); order.setGuestId(guest.getId()); order.setCheckInTime(new Date()); order.setStatus(CHECKED_IN); orderMapper.insert(order); // 4. 更新房间状态乐观锁版本号检查 room.setStatus(OCCUPIED); int updated roomMapper.updateById(room); if (updated 0) { throw new BusinessException(房间状态已变更请刷新后重试); } // 5. 返回结果含订单号、预计退房时间 CheckInResult result new CheckInResult(); result.setOrderNo(order.getOrderNo()); result.setExpectedCheckout(new Date(System.currentTimeMillis() 24*60*60*1000)); return result; } }参数说明selectByIdForUpdate()对应SQLSELECT * FROM room WHERE id #{id} FOR UPDATE确保查询期间其他事务无法修改该行updateById()内部使用MyBatis的Update(UPDATE room SET status#{status}, versionversion1 WHERE id#{id} AND version#{version})实现乐观锁Transactional保证四步操作原子性客人插入失败则订单不创建、房间不更新。Dao层MyBatis注解式MapperMapper public interface RoomMapper { Select(SELECT * FROM room WHERE id #{id}) Room selectById(Param(id) Long id); Select(SELECT * FROM room WHERE id #{id} FOR UPDATE) Room selectByIdForUpdate(Param(id) Long id); Update(UPDATE room SET status#{status}, versionversion1 WHERE id#{id} AND version#{version}) int updateById(Param(room) Room room); }注意Param注解不可省略否则MyBatis无法绑定#{id}参数FOR UPDATE子句在MySQL中生效Oracle需改为FOR UPDATE NOWAIT。3.2 房态实时刷新WebSocket推送的轻量级实现系统未用SockJS或STOMP而是基于原生WebSocket实现房态秒级同步Component ServerEndpoint(/ws/roomStatus) public class RoomStatusWebSocket { private static final SetSession sessions Collections.synchronizedSet(new HashSet()); OnOpen public void onOpen(Session session) { sessions.add(session); System.out.println(新WebSocket连接 session.getId()); } OnMessage public void onMessage(String message, Session session) { // 客户端发送REFRESH时主动推送最新房态 if (REFRESH.equals(message)) { sendRoomStatusToAll(); } } OnClose public void onClose(Session session) { sessions.remove(session); } // 后台调用此方法广播如Service层调用 public static void broadcastRoomStatus() { String json getLatestRoomStatusJson(); // 调用Service获取JSON字符串 for (Session session : sessions) { try { session.getBasicRemote().sendText(json); } catch (IOException e) { sessions.remove(session); } } } }前端JSP中监听script const ws new WebSocket(ws://localhost:8080/hotel/ws/roomStatus); ws.onmessage function(event) { const data JSON.parse(event.data); // 更新页面上的房态卡片绿色空闲红色入住 data.rooms.forEach(room { document.getElementById(room- room.id).className room.status VACANT ? room-vacant : room-occupied; }); }; /script提示WebSocket连接由RoomController在checkIn()成功后调用RoomStatusWebSocket.broadcastRoomStatus()触发比轮询更高效且无需额外引入Spring Boot WebSocket依赖。3.3 报表导出Apache POI生成带样式的ExcelReportController中导出月度营收报表GetMapping(/report/monthly) public void exportMonthlyReport(HttpServletResponse response) throws IOException { ListMonthlyReportItem items reportService.getMonthlyReport(); // 创建Excel工作簿 XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(月度营收报表); // 设置表头样式 XSSFCellStyle headerStyle workbook.createCellStyle(); headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 写入表头 String[] headers {日期, 房间数, 入住率, 营收(元), 平均房价}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 写入数据行 for (int i 0; i items.size(); i) { Row row sheet.createRow(i 1); MonthlyReportItem item items.get(i); row.createCell(0).setCellValue(item.getDateStr()); row.createCell(1).setCellValue(item.getRoomCount()); row.createCell(2).setCellValue(item.getOccupancyRate() %); row.createCell(3).setCellValue(item.getRevenue()); row.createCell(4).setCellValue(item.getAvgPrice()); } // 自动列宽 for (int i 0; i headers.length; i) { sheet.autoSizeColumn(i); } // 响应输出 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamemonthly_report_ new SimpleDateFormat(yyyyMMdd).format(new Date()) .xlsx); workbook.write(response.getOutputStream()); workbook.close(); }关键点XSSFWorkbook支持.xlsx格式兼容Office 2007autoSizeColumn()避免列宽过窄导致内容显示不全response.setHeader()中的filename必须用英文数字中文名需URL编码此处简化处理实际生产环境应加URLEncoder.encode(月度报表.xlsx, UTF-8)。4. 避坑指南部署与运行中踩过的5个真实坑4.1 现象Tomcat启动报错java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest原因项目使用Servlet 4.0规范但Tomcat 8.5默认只提供Servlet 3.1 APIHttpServletRequest类在tomcat/lib/servlet-api.jar中而IDEA/Maven未正确引入该依赖。解决在pom.xml中添加provided依赖并确认Tomcat版本匹配dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency同时确认Tomcat版本≥9.0Servlet 4.0要求若用Tomcat 8.5需降级为javax.servlet-api:3.1.0并修改web.xml头声明。4.2 现象登录成功后跳转到/WEB-INF/views/index.jsp浏览器显示404原因web.xml中welcome-file-list未配置且Spring MVC的InternalResourceViewResolver前缀为/WEB-INF/views/但Tomcat默认禁止访问WEB-INF目录下的资源。解决在web.xml中添加欢迎页配置并确认spring-mvc.xml中视图解析器配置正确welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list!-- spring-mvc.xml -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean注意InternalResourceViewResolver会自动将逻辑视图名index拼接为/WEB-INF/views/index.jsp该路径由Servlet容器内部转发不经过HTTP请求因此不会404。4.3 现象MySQL插入中文显示为??数据库字符集已设为utf8mb4原因JDBC URL缺少characterEncodingutf8mb4参数且MySQL服务端my.cnf中未配置[client]段。解决修改application.propertiesjdbc.urljdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8mb4检查MySQL配置文件/etc/my.cnfLinux或my.iniWindows[client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci重启MySQL服务并重建数据库DROP DATABASE hotel_db; CREATE DATABASE hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4.4 现象调价功能保存后price_history表无记录room.current_price未更新原因RoomService.checkPriceChange()方法中未开启事务或Transactional注解所在类未被Spring管理如new方式创建实例。解决确认RoomService实现类上有Service注解确认spring-mvc.xml中context:component-scan扫描到了service包在checkPriceChange()方法内添加日志验证事务是否生效Transactional(rollbackFor Exception.class) public void checkPriceChange(Long roomId, BigDecimal newPrice, String operator) { System.out.println(事务开始 TransactionSynchronizationManager.isActualTransactionActive()); // 应输出true // ... 业务逻辑 }4.5 现象导出Excel时中文乱码文件名显示为?????.xlsx原因response.setHeader()中filename参数未按浏览器兼容方式编码。解决根据不同浏览器UA动态编码String userAgent request.getHeader(User-Agent); String fileName 月度报表_ new SimpleDateFormat(yyyyMMdd).format(new Date()) .xlsx; String encodedFileName; if (userAgent.contains(MSIE) || userAgent.contains(Trident)) { encodedFileName URLEncoder.encode(fileName, UTF-8).replaceAll(\\, %20); } else if (userAgent.contains(Firefox)) { encodedFileName ?UTF-8?B? Base64.getEncoder().encodeToString(fileName.getBytes(UTF-8)) ?; } else { encodedFileName new String(fileName.getBytes(UTF-8), ISO-8859-1); } response.setHeader(Content-Disposition, attachment; filename encodedFileName);5. 二次开发实战从“能跑”到“能改”的3个关键改造点5.1 改造一接入微信扫码支付替换原现金结算原系统结账仅支持现金现需对接微信Native支付。核心改动在OrderService.payOrder()方法Override Transactional(rollbackFor Exception.class) public PayResult payOrder(Long orderId, String payType) { Order order orderMapper.selectById(orderId); if (!CHECKED_IN.equals(order.getStatus())) { throw new BusinessException(订单状态异常无法支付); } // 1. 构造微信统一下单参数 MapString, String params new HashMap(); params.put(appid, wx1234567890abcdef); // 替换为你的公众号AppID params.put(mch_id, 1234567890); // 商户号 params.put(nonce_str, UUID.randomUUID().toString().replace(-, )); params.put(body, 酒店住宿费); params.put(out_trade_no, order.getOrderNo() _ System.currentTimeMillis()); params.put(total_fee, String.valueOf(order.getTotalAmount().multiply(new BigDecimal(100)).longValue())); // 单位分 params.put(spbill_create_ip, 127.0.0.1); params.put(notify_url, http://yourdomain.com/order/wechatNotify); // 异步回调地址 params.put(trade_type, NATIVE); // 2. 签名并发送请求使用WxPayUtil工具类 String sign WxPayUtil.generateSignature(params, your_mch_key); // 商户API密钥 params.put(sign, sign); String xml WxPayUtil.mapToXml(params); String resultXml HttpUtil.post(https://api.mch.weixin.qq.com/pay/unifiedorder, xml); // 3. 解析返回的code_url二维码链接 MapString, String resultMap WxPayUtil.xmlToMap(resultXml); if (SUCCESS.equals(resultMap.get(return_code)) SUCCESS.equals(resultMap.get(result_code))) { String codeUrl resultMap.get(code_url); // 4. 更新订单状态为PAYING order.setStatus(PAYING); order.setPayType(payType); orderMapper.updateById(order); return new PayResult(true, codeUrl); // 前端用qrcode.js生成二维码 } else { throw new BusinessException(微信下单失败 resultMap.get(err_code_des)); } }关键点total_fee必须为整数分不能带小数notify_url需公网可访问本地开发可用ngrok映射微信回调接口/order/wechatNotify需验签并更新订单状态为PAID同时触发房态变更调用RoomStatusWebSocket.broadcastRoomStatus()。5.2 改造二增加多角色权限控制前台/客房部/财务原系统无权限区分所有账号均可操作全部功能。新增Role和UserRole表后用Spring Security 5.7非Boot版实现!-- pom.xml 添加 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-web/artifactId version5.7.10/version /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-config/artifactId version5.7.10/version /dependencyweb.xml中注册过滤器filter filter-namespringSecurityFilterChain/filter-name filter-classorg.springframework.web.filter.DelegatingFilterProxy/filter-class /filter filter-mapping filter-namespringSecurityFilterChain/filter-name url-pattern/*/url-pattern /filter-mappingspring-security.xml配置security:http auto-configfalse use-expressionstrue security:intercept-url pattern/login accesspermitAll/ security:intercept-url pattern/css/** accesspermitAll/ security:intercept-url pattern/js/** accesspermitAll/ !-- 角色权限 -- security:intercept-url pattern/room/** accesshasRole(FRONT_DESK) or hasRole(HOUSEKEEPING)/ security:intercept-url pattern/report/** accesshasRole(FINANCE)/ security:intercept-url pattern/admin/** accesshasRole(ADMIN)/ security:form-login login-page/login default-target-url/index authentication-failure-url/login?errortrue/ security:logout logout-success-url/login?logouttrue/ /security:http security:authentication-manager security:authentication-provider security:jdbc-user-service >div idoccupancyChart stylewidth: 100%; height: 400px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script // 从后台API获取7天入住率数据 fetch(/dashboard/occupancyTrend) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(occupancyChart)); chart.setOption({ title: { text: 近7天房间入住率趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.days }, yAxis: { type: value, name: 入住率(%) }, series: [{ name: 入住率, type: line, data: data.rates, smooth: true, areaStyle: {} }] }); }); /script后端DashboardController提供APIRestController RequestMapping(/dashboard) public class DashboardController { Autowired private DashboardService dashboardService; GetMapping(/occupancyTrend) public MapString, Object getOccupancyTrend() { ListOccupancyDay trend dashboardService.get7DayOccupancy(); MapString, Object result new HashMap(); result.put(days, trend.stream().map(OccupancyDay::getDateStr).collect(Collectors.toList())); result.put(rates, trend.stream().map(OccupancyDay::getRate).collect(Collectors.toList())); return result; } }血泪经验ECharts 5.x需用echarts.init()而非echarts.init(document.getElementById(...))否则在JSP中可能因DOM未加载完成报错fetch需在script标签中不能放在head里否则document.getElementById找不到元素。从那以后我每次接手毕设项目都强制走一遍「数据库建表→Tomcat部署→登录→开房→调价→结账→导报表」全流程哪怕只是本地跑通。因为真正的系统能力不在UML图里而在凌晨两点调试FOR UPDATE锁超时的日志中。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站