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

SSM酒店系统事务一致性实战:从入住到对账的原子操作设计

SSM酒店系统事务一致性实战:从入住到对账的原子操作设计 ★ FEATURED ARTICLE
简介本资源是一套面向高校Java初学者与课程设计实践者的SSM框架实战项目聚焦酒店管理业务场景完整覆盖用户注册登录、客房预订、入住退房、订单管理、员工权限控制等核心功能模块适用于Java期末大作业、课程设计及毕业设计参考。压缩包共887个文件包含22个Java后端逻辑类、23个JSP页面模板、236个JavaScript交互脚本、184个PNG与240个GIF图形资源、56个CSS样式文件及1个SQL数据库脚本辅以Layui、UEditor、Video.js等主流前端组件整体体积9.42MB结构清晰、依赖明确小白可直接导入IDE运行调试。目前已有304人学习下载项目为作者纯手写实现代码规范、注释充分配套数据库已初始化并支持一键导入附带完整目录说明与基础部署指南助力快速理解SSM三层架构整合逻辑与Web系统开发全流程。1. 这不是又一个CRUD Demo一个能真实走通「入住→退房→财务对账」闭环的SSM酒店系统课程设计拿95分的关键在于它把事务边界钉死了你肯定见过那种“用户管理房间管理订单管理”三页表单堆出来的Java课设——点新增弹个框、点删除没提示、改完数据刷新页面就丢。但这个基于SSM的酒店管理系统不一样它用Transactional把「客户登记→分配房间→生成入住单→扣减库存→更新房间状态」这5步锁在一个数据库事务里哪怕第4步扣库存失败前3步也自动回滚不会出现“人已入住但房间状态还是空闲”的玄学现场。我帮某高校三个班做课设辅导时发现95分以上的作业几乎都卡在事务一致性上——而这个项目连押金退款的冲正逻辑都写了RefundService.rollbackDeposit()还配了MySQL存储过程做日结汇总。它适合两类人一是大二大三刚学完Spring事务传播机制、想拿高分又怕翻车的小白二是需要快速搭出可演示业务流、不被答辩老师问住的赶工党。别被标题里的“课程设计”骗了——它的Controller层有完整的参数校验链、Service层有按角色隔离的数据权限过滤、Mapper XML里连foreach批量插入都加了useGeneratedKeystrue是真·可跑、可调、可讲清楚每行代码为啥这么写的工程级Demo。2. 环境准备与项目结构解剖从解压到IDEA识别看清SSM三层骨架怎么长成酒店系统的血肉2.1 JDK、Tomcat、MySQL版本选择为什么必须用JDK8u202Tomcat8.5MySQL5.7这个项目源码里pom.xml明确声明了java.version1.8/java.version且所有Lombok注解如Data、Slf4j都是基于JDK8的字节码增强实现。如果你强行用JDK11mvn compile会报Cannot find symbol——因为Lombok 1.18.12项目所用版本不兼容JDK11的模块系统。Tomcat选8.5而非9.x是因为web.xml里用了listener注册ContextLoaderListener而Tomcat9默认启用Servlet4.0规范要求web.xml最低版本为3.0但本项目web.xml头声明的是version2.5直接启动会抛org.xml.sax.SAXParseException。MySQL必须用5.7关键在hotel_db.sql文件里有一句CREATE TABLE room ( ..., create_time datetime DEFAULT CURRENT_TIMESTAMP )——MySQL5.6不支持datetime字段的CURRENT_TIMESTAMP默认值会报错Invalid default value for create_time。我一般会建个独立目录放运行环境/opt/env/jdk8u202、/opt/env/tomcat85、/opt/env/mysql57避免和本机其他项目冲突。# 验证JDK版本必须输出1.8.0_202 java -version # 验证MySQL是否支持datetime默认值执行后应返回1行 mysql -u root -p -e SELECT VERSION(); mysql -u root -p -e SELECT sql_mode; # 如果返回结果含STRICT_TRANS_TABLES需临时关闭课设环境可接受 mysql -u root -p -e SET GLOBAL sql_mode(SELECT REPLACE(sql_mode,STRICT_TRANS_TABLES,));提示sql_mode里带STRICT_TRANS_TABLES会导致INSERT INTO guest时若id_card为空直接报错而本项目前端校验不严格建议课设阶段先关掉答辩前再补全前端JS校验。2.2 项目目录结构还原src/main/java下藏着3个关键包不是所有“com.xxx”都值得细看解压后你会看到标准Maven结构但重点不在pom.xml的依赖列表而在src/main/java下的实际包组织。真正承载业务逻辑的是这三个包com.hotel.controller只有6个Controller类每个对应一个核心业务域RoomController.java、OrderController.java、AccountController.java没有冗余的TestControllercom.hotel.service.implRoomServiceImpl.java里有changeRoomStatus()方法它调用roomMapper.updateStatusById()后立刻调用orderMapper.selectByRoomIdAndStatus()查关联订单——这是为后续“房间状态变更触发订单状态联动”埋的钩子com.hotel.mapperXML文件命名直白RoomMapper.xml但resultMap里association嵌套了guest对象说明订单查询时已预加载客户信息避免N1查询。src/main/resources里applicationContext.xml是Spring核心配置但别急着改它——事务管理器tx:annotation-driven transaction-managertransactionManager/已绑定DataSourceTransactionManager你只要在Service方法上加Transactional就生效。而spring-mvc.xml里mvc:resources路径映射了/static/**到classpath:/static/所以layui.css等前端资源才能被正确加载。2.3 数据库初始化实操执行hotel_db.sql前必须做的3件事hotel_db.sql不是扔进MySQL就完事。我拆过27个同类型课设80%的人卡在这一步先创建数据库并指定编码关键CREATE DATABASE hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hotel_db;注意必须用utf8mb4否则客户姓名里的生僻字如“䶮”、“堃”存入会变问号。utf8在MySQL里实际是utf8mb3不支持4字节Unicode。检查SQL文件头部是否有SET NAMES utf8mb4;打开hotel_db.sql第一行应该是SET NAMES utf8mb4;。如果没有手动加在CREATE TABLE语句前否则INSERT INTO guest VALUES (..., 张䶮, ...)会乱码。执行时禁用外键检查仅课设环境SET FOREIGN_KEY_CHECKS 0; -- 粘贴hotel_db.sql全部内容 SET FOREIGN_KEY_CHECKS 1;原因room表的type_id外键指向room_type但SQL文件中room_type建表语句在room之后不关外键检查会报ERROR 1215 (HY000): Cannot add foreign key constraint。执行完后验证-- 应返回4个表guest, order_info, room, room_type SHOW TABLES; -- 检查room表status字段默认值是否为available SHOW COLUMNS FROM room LIKE status;3. 核心模块拆解RoomController到RoomMapper的完整链路看SSM如何把“换房”变成原子操作3.1 “换房”功能的Controller层参数校验不是摆设而是防御性编程的第一道墙RoomController.java里的changeRoom()方法是整个系统事务边界的入口。它接收的不是裸JSON而是封装好的ChangeRoomRequest对象PostMapping(/changeRoom) ResponseBody public Result changeRoom(Valid RequestBody ChangeRoomRequest request) { // 1. 校验原房间是否存在且状态为occupied Room oldRoom roomService.getRoomById(request.getOldRoomId()); if (oldRoom null || !occupied.equals(oldRoom.getStatus())) { return Result.fail(原房间不存在或未入住); } // 2. 校验新房间是否存在且状态为available Room newRoom roomService.getRoomById(request.getNewRoomId()); if (newRoom null || !available.equals(newRoom.getStatus())) { return Result.fail(新房间不可用); } // 3. 执行换房核心业务逻辑 boolean success roomService.changeRoom(request.getOldRoomId(), request.getNewRoomId(), request.getOrderId()); return success ? Result.success() : Result.fail(换房失败); }注意三点Valid触发ChangeRoomRequest类上的NotNull校验如oldRoomId不能为空失败时Spring MVC自动返回400错误不用手写if两次roomService.getRoomById()查的是缓存还是DB看RoomServiceImpl.java里Cacheable(room)注解——它用Spring Cache抽象层底层是ConcurrentHashMap课设环境够用但别在答辩时吹“用了Redis缓存”返回Result对象是统一响应格式Result.success()内部是new Result(200, success, null)前端AJAX直接取data.code判断。3.2 Service层的Transactional5行代码撑起事务一致性但陷阱在Propagation.REQUIRED的默认行为RoomServiceImpl.java的changeRoom()方法是事务核心Transactional(rollbackFor Exception.class) Override public boolean changeRoom(Long oldRoomId, Long newRoomId, Long orderId) { try { // 步骤1更新原房间状态为vacant待清洁 roomMapper.updateStatusById(oldRoomId, vacant); // 步骤2更新新房间状态为occupied roomMapper.updateStatusById(newRoomId, occupied); // 步骤3更新订单中的房间ID orderMapper.updateRoomIdByOrderId(orderId, newRoomId); // 步骤4插入换房日志关键审计依据 ChangeLog log new ChangeLog(); log.setOldRoomId(oldRoomId); log.setNewRoomId(newRoomId); log.setOrderId(orderId); log.setChangeTime(new Date()); changeLogMapper.insert(log); // 步骤5触发消息课设简化版实际可发MQ notifyRoomChanged(oldRoomId, newRoomId); return true; } catch (Exception e) { log.error(换房事务失败, e); throw e; // 让Transactional捕获并回滚 } }为什么rollbackFor Exception.class不能省因为默认只回滚RuntimeException及其子类。如果notifyRoomChanged()里抛出IOExceptionchecked exception事务不会回滚导致“房间状态已改但日志没写”的脏数据。这里throw e是显式抛出确保Spring AOP代理能拦截。注意notifyRoomChanged()方法体是空的// TODO: 实际对接微信通知但方法签名上有Async注解——说明作者预留了异步通知扩展点课设答辩时可以说“已设计消息解耦后续可接入短信网关”。3.3 Mapper层的XML细节foreach批量操作和selectKey主键回填不是炫技而是刚需RoomMapper.xml里最值得抠的是batchUpdateStatus()方法它用于日结时批量更新房间状态update idbatchUpdateStatus UPDATE room SET status CASE WHEN id IN foreach collectionroomIds itemid open( separator, close) #{id} /foreach THEN cleaning ELSE status END WHERE id IN foreach collectionroomIds itemid open( separator, close) #{id} /foreach /update这段SQL生成的是UPDATE room SET status CASE WHEN id IN (1,2,3) THEN cleaning ELSE status END WHERE id IN (1,2,3)比循环调用updateStatusById()快10倍以上。而OrderMapper.xml里插入订单时用selectKey回填主键insert idinsert parameterTypeOrder selectKey keyPropertyid resultTypelong orderAFTER SELECT LAST_INSERT_ID() /selectKey INSERT INTO order_info (room_id, guest_id, check_in_time, status) VALUES (#{roomId}, #{guestId}, #{checkInTime}, #{status}) /insertorder_id是自增主键但订单生成后要立即用这个ID生成支付二维码课设虽未实现但代码预留了order.getId()调用所以必须用selectKey在INSERT后立刻取ID而不是useGeneratedKeystrue那是MyBatis 3.4的写法本项目用的是3.2.8。4. 前端交互与LayUI集成为什么说“静态资源路径配错整个系统白搭”4.1 static目录结构真相/static/css/layui.css的引用路径必须和web.xml的 mvc:resources 完全匹配项目src/main/webapp/static/目录下有这些关键文件static/ ├── css/ │ ├── layui.css # LayUI框架CSS │ ├── ueditor.min.css # 富文本编辑器CSS │ └── video-js.css # 视频播放器CSS课设未用但留着备用 ├── js/ │ ├── layui.js # LayUI核心JS │ ├── ueditor.config.js # UEditor配置 │ └── app.js # 本项目业务JS含房间状态切换逻辑 └── images/ └── logo.pngweb.xml里配置了servlet-mapping servlet-namedefault/servlet-name url-pattern/static/*/url-pattern /servlet-mapping而spring-mvc.xml里mvc:resources mapping/static/** locationclasspath:/static/ /这意味着浏览器请求/static/css/layui.css→ Spring MVC拦截 → 映射到classpath:/static/css/layui.css但如果app.js里写layui.use([form, table], function(){...})LayUI会自动加载/static/modules/form/form.js而这个路径不在mvc:resources映射范围内解决方案在web.xml里追加一行servlet-mapping servlet-namedefault/servlet-name url-pattern/static/modules/*/url-pattern /servlet-mapping否则控制台报Failed to load resource: the server responded with a status of 404 ()表格渲染不出来。4.2 房间状态切换的JS逻辑LayUI table.reload()背后是RESTful API的真实调用room_list.jsp里初始化表格的代码table.render({ elem: #roomTable, url: /room/list, // 对应RoomController.list() cols: [[ {field:id, title:房间号}, {field:type, title:房型}, {field:status, title:状态, templet:#statusTpl}, // 自定义模板 {title:操作, toolbar:#barDemo} ]], page: true });关键在{field:status, templet:#statusTpl}它引用了页面底部的script模板script typetext/html idstatusTpl {{# if(d.status available){ }} span classlayui-badge layui-bg-green空闲/span {{# } else if(d.status occupied){ }} span classlayui-badge layui-bg-blue入住/span {{# } else { }} span classlayui-badge layui-bg-orange待清洁/span {{# } }} /script当点击“换房”按钮时JS调用table.on(tool(roomTable), function(obj){ if(obj.event changeRoom){ layer.open({ type: 2, content: /room/changePage?roomId obj.data.id, area: [600px, 400px] }); } });这里/room/changePage返回的是change_room.jsp页面它通过input typehidden nameoldRoomId value${param.roomId}把原房间ID传给表单提交时POST到/room/changeRoom——整个流程是纯服务端渲染AJAX局部刷新没用Vue/React符合课设技术栈要求。4.3 UEditor富文本集成为什么课设里它只用来编辑“酒店公告”却暴露了SSM文件上传的完整链路notice_edit.jsp里初始化UEditorvar ue UE.getEditor(container, { serverUrl: /ueditor/upload, toolbars: [[ bold, italic, underline, fontborder, strikethrough, superscript, subscript ]] });serverUrl指向/ueditor/upload对应UeditorController.javaRequestMapping(value /upload, method RequestMethod.POST) ResponseBody public MapString, Object upload(HttpServletRequest request, HttpServletResponse response) { // 1. 解析multipart/form-data MultipartHttpServletRequest multipartRequest (MultipartHttpServletRequest) request; MultipartFile file multipartRequest.getFile(upfile); // UEditor固定字段名 // 2. 保存到服务器课设存到/webapp/upload/ String fileName System.currentTimeMillis() _ file.getOriginalFilename(); File dest new File(request.getSession().getServletContext().getRealPath(/upload/) / fileName); file.transferTo(dest); // 3. 返回UEditor要求的JSON格式 MapString, Object result new HashMap(); result.put(state, SUCCESS); result.put(url, /upload/ fileName); // 浏览器可直接访问的URL return result; }这个方法暴露了SSM文件上传的完整路径前端input typefile nameupfile→后端MultipartHttpServletRequest解析 →file.transferTo(dest)写入磁盘 →返回相对URL供前端img src/upload/xxx.jpg渲染。课设答辩时老师问“图片存在哪”你就指着/webapp/upload/目录说“存在项目根目录下和war包一起部署”比说“存在云存储”更可信。5. 避坑指南那些让90%同学在部署时抓狂的5个具体问题及血泪解法5.1 现象启动Tomcat后访问http://localhost:8080显示404但http://localhost:8080/hotel能进首页原因项目在IDEA里被识别为hotel模块但pom.xml中artifactId是hotel-system导致WAR包名是hotel-system.war而Tomcat默认解压为hotel-system目录。但web.xml里display-name是hotel部分IDEA版本会以display-name为准部署。解决在IDEA的Project Structure → Artifacts里将Output Directory改为/path/to/tomcat/webapps/hotel并勾选Build on make。或者直接修改pom.xml的artifactId为hotel保持一致。5.2 现象登录成功后跳转到/room/list页面空白浏览器控制台报Uncaught ReferenceError: layui is not defined原因room_list.jsp顶部有script src/static/js/layui.js/script但LayUI的layui.js里依赖layui.css中的CSS变量如--layui-bg-color而layui.css未被正确加载。解决检查room_list.jsp中CSS引入顺序必须先引入CSS再引入JSlink relstylesheet href/static/css/layui.css link relstylesheet href/static/css/layui.ext.css script src/static/js/layui.js/script5.3 现象执行INSERT INTO guest时MySQL报错Data truncation: Data too long for column id_card at row 1原因guest表的id_card字段是VARCHAR(18)但插入的身份证号是18位X如11010119900307271X总长度19。解决修改hotel_db.sql中guest表创建语句id_card varchar(20) NOT NULL COMMENT 身份证号,然后重新执行SQL先DROP TABLE guest;再重建。5.4 现象OrderController.java里RequestMapping(/order)的接口返回405 Method Not Allowed原因OrderController.list()方法上写了GetMapping但前端AJAX用$.post()调用HTTP方法不匹配。解决统一用RequestMapping并指定methodRequestMapping(value /order/list, method RequestMethod.GET) ResponseBody public Result list() { ... }或者前端AJAX改为$.get(/order/list)。5.5 现象修改applicationContext.xml后重启Tomcat控制台报org.springframework.beans.factory.BeanCreationException: Error creating bean with name sqlSessionFactory原因applicationContext.xml里bean iddataSource的driverClassName写成了com.mysql.jdbc.Driver但MySQL5.7推荐用com.mysql.cj.jdbc.Driver旧驱动已废弃。解决替换为property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/hotel_db?useSSLfalseamp;serverTimezoneAsia/Shanghai/注意URL中要写成amp;否则XML解析失败。6. 二次开发实战从“支持微信扫码支付”切入看如何在不破坏原有SSM结构的前提下安全扩展6.1 支付扩展的架构定位为什么必须新增pay模块而不是往order里硬塞现有order模块职责是“订单生命周期管理”创建、入住、退房、取消而支付是独立领域涉及第三方API、异步回调、对账等。强行在OrderService里加payWithWechat()方法会违反单一职责原则且让事务变得不可控支付成功但订单状态没更新或反之。正确做法是新建com.hotel.pay包遵循“防腐层”模式PayService定义支付能力接口createOrder()、queryStatus()WechatPayServiceImpl实现类封装微信API调用PayCallbackController处理微信服务器的异步通知。这样OrderController只需调用payService.createOrder(orderId)支付成功后微信回调/pay/callback由PayCallbackController更新订单状态——业务逻辑解耦测试也方便MockPayService即可测订单流程。6.2 新增支付模块的4个必改文件清单附代码片段文件路径修改点关键代码pom.xml增加微信支付SDK依赖dependencygroupIdcom.github.wechatpay-apiv3/groupIdartifactIdwechatpay-apache-httpclient/artifactIdversion0.4.8/version/dependencyapplicationContext.xml注册PayServiceBeanbean idpayService classcom.hotel.pay.impl.WechatPayServiceImplproperty namemchId value1900000109//beanOrderController.java在createOrder()后调用支付String payUrl payService.createOrder(order.getId()); return Result.success(payUrl);src/main/webapp/static/js/app.js前端跳转到微信支付页window.location.href data.payUrl;其中WechatPayServiceImpl.createOrder()的核心逻辑Override public String createOrder(Long orderId) { // 1. 查询订单金额复用现有OrderMapper Order order orderMapper.selectById(orderId); BigDecimal amount order.getAmount(); // 2. 构造微信统一下单参数 WechatPayParams params new WechatPayParams(); params.setOutTradeNo(ORDER_ orderId); // 商户订单号 params.setTotalFee(amount.multiply(new BigDecimal(100)).intValue()); // 分为单位 params.setBody(酒店入住订单); // 3. 调用微信API使用wechatpay-apache-httpclient SDK try { WechatPayResponse response wechatPayClient.unifiedOrder(params); return response.getCodeUrl(); // 返回扫码URL } catch (Exception e) { log.error(微信下单失败, e); throw new RuntimeException(支付系统异常); } }注意wechatpay-apache-httpclientSDK要求配置证书路径课设可简化为setCertPath(classpath:apiclient_cert.p12)证书文件放在src/main/resources/下。6.3 支付回调的安全验证为什么PayCallbackController必须校验签名且不能只验时间戳微信支付回调是HTTP POST任何人伪造请求都能调用/pay/callback。必须做三重校验验签名微信在请求头Wechatpay-Signature里放了RSA-SHA256签名用商户私钥验签验时间戳请求头Wechatpay-Timestamp与当前时间差不能超15分钟防重放验订单状态回调里带out_trade_no必须查数据库确认该订单尚未支付成功避免重复更新。PayCallbackController.java片段PostMapping(/callback) ResponseBody public String handleCallback(RequestBody String body, RequestHeader(Wechatpay-Timestamp) String timestamp, RequestHeader(Wechatpay-Nonce) String nonce, RequestHeader(Wechatpay-Signature) String signature) { // 1. 时间戳校验 long ts Long.parseLong(timestamp); if (Math.abs(System.currentTimeMillis() / 1000 - ts) 900) { return FAIL; // 微信要求返回FAIL字符串 } // 2. 签名校验调用SDK工具类 if (!wechatPayVerifier.verify(body, timestamp, nonce, signature)) { return FAIL; } // 3. 解析回调JSON更新订单 JSONObject json JSON.parseObject(body); String outTradeNo json.getJSONObject(resource).getString(out_trade_no); String orderId outTradeNo.replace(ORDER_, ); // 只有订单状态为created才更新 Order order orderMapper.selectById(Long.valueOf(orderId)); if (created.equals(order.getStatus())) { order.setStatus(paid); order.setPayTime(new Date()); orderMapper.updateById(order); } return SUCCESS; // 微信要求返回SUCCESS字符串 }从那以后我每次给课设加第三方服务都强制走一遍“回调验签幂等更新”双校验流程——哪怕只是模拟支付也要把PayCallbackController的单元测试写全因为答辩时老师最爱问“如果黑客伪造回调怎么办”。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站