简介:面向JavaEE课程设计与毕业设计场景的小型酒店管理系统源码包,基于Spring、Struts与Hibernate的经典SSH组合实现,适合希望理解分层架构、MVC请求流转与ORM映射的学生和初中级开发者。压缩包共262个文件、约18.69MB,包含49个java源文件、54个class、54个jar依赖、25个jsp页面及24个xml配置,另有properties、css、js和图片等静态与说明文件,基本覆盖实体层、Service层、Action控制器、视图与数据库配置的完整链路。项目围绕用户与权限、房间状态与价格、预订入住退房、收费账单、报表统计和系统设置等模块展开,可对照源码梳理事务管理、拦截跳转、数据绑定及JSP展示的实现方式,也便于排查依赖版本、数据库映射和页面传参等常见问题。目前已有1695人学习下载,适合作为课程作业参考、毕业设计原型或SSH框架入门实践材料。
1. 前台凌晨对不上账,这套 Java 小型酒店管理系统要补的就是这个缺口
凌晨一点交接班,纸质登记本上三间房的押金对不上,房态卡还插在墙上,谁也说不清 302 到底是退了还是换了房。这类场景在小旅馆、民宿、县城商务酒店里并不少见,也是「Java 小型酒店管理系统 毕业设计」这个题目年年有人选的原因——它把房态、预订、入住、退房、结算这条最朴素的业务线,完整装进一个能跑起来的工程里。这套基于 JavaEE(SSM)的小型酒店管理系统,前端 JSP 配 Bootstrap,后端 Spring + SpringMVC + MyBatis,MySQL 落库,功能覆盖客房台账、在线预订、入住登记、换房退房、账单结算和营业报表。适合两类人:一是要交毕业设计、需要完整演示流程的同学;二是接手小体量酒店系统、想找个骨架往上加功能的开发者。读完你至少能判断它值不值得下载、跑起来要改哪几个文件、哪些地方一改就容易翻车。
2. 技术栈与工程骨架:SSM 三层怎么把酒店业务装进去
2.1 选型盘点:为什么这类项目绕不开 SSM + JSP + MySQL
先看它依赖了什么。这套工程的典型组合是 Spring 5 + SpringMVC + MyBatis 3,MySQL 8 存数据,Druid 做连接池,PageHelper 管分页,JSP + JSTL + Bootstrap 3 + jQuery 渲染后台页面,Maven 管依赖,打 war 包丢 Tomcat。这套组合不新,但对毕业设计和中小型业务系统来说很稳:分层清晰,答辩时 controller、service、mapper 三层能讲明白;服务端渲染不用单独起前端工程,一台机器就能跑全栈;MySQL 免费、资料多,出问题搜得到。
依赖版本我在 pom 里一般这样锁,避免不同机器拉到的版本不一致:
<properties> <spring.version>5.2.8.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> <mysql.version>8.0.21</mysql.version> </properties> <dependencies> <!-- SpringMVC:DispatcherServlet、@Controller、视图解析都在这 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- spring-jdbc 提供 DataSourceTransactionManager,@Transactional 靠它生效 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 与 Spring 的粘合层,负责把 Mapper 接口代理成 Bean --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.5</version> </dependency> <!-- 分页插件,客房列表、订单列表、账单列表三处都要用 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> <!-- MySQL 8 的驱动类名是 com.mysql.cj.jdbc.Driver,别再用旧包名 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies>参数说明:spring.version统一管理 Spring 全家桶版本,改一处全生效;mybatis-spring2.x 对应 MyBatis 3.5,版本错配会在启动时报NoSuchMethodError;PageHelper 5.x 的拦截器要手动配进SqlSessionFactory,只加依赖不配置是不会分页的。这里没有引 Spring Boot,是因为原始工程按传统 SSM 的 web.xml 方式组织,硬套 Boot 反而要把配置全拆一遍。
2.2 目录结构:controller、service、mapper 各自装什么
工程是标准 Maven webapp 布局,目录拆得比较干净,拿到源码先看这棵树:
hotel-management ├── pom.xml ├── sql │ └── hotel_db.sql # 建库建表 + 初始化管理员和房型 ├── src/main/java/com/hotel │ ├── controller # 只收参数、做校验、包装返回 │ ├── service # 业务接口 │ │ └── impl # 业务实现,事务边界在这里 │ ├── mapper # MyBatis 接口,方法名对应 XML 的 id │ ├── entity # 与表字段一一对应 │ ├── dto # 页面提交参数 │ ├── vo # 页面展示对象,带联表字段 │ ├── interceptor # 登录拦截、角色校验 │ ├── exception # BizException + 全局异常处理器 │ └── util # 单号生成、金额计算、日期工具 ├── src/main/resources │ ├── spring │ │ ├── applicationContext.xml # 根容器:数据源、Mapper、事务 │ │ └── spring-mvc.xml # 子容器:Controller、视图、拦截器 │ ├── mybatis/mybatis-config.xml │ ├── mapper # 与接口同名的 XML │ ├── jdbc.properties │ └── log4j.properties └── src/main/webapp ├── WEB-INF │ ├── web.xml │ └── views # JSP:登录、房态看板、订单、账单 └── static # bootstrap、jquery、css、img职责上我是这么理解的:controller 层不写业务判断,只做三件事——接参数、@Valid校验、把 service 返回结果包成统一 JSON;service 层承担全部业务规则,比如房态能不能入住、押金够不够扣、退房时账单怎么算;mapper 层只写 SQL,不做判断。entity、dto、vo 分开看着啰嗦,但实际改起来很省事:数据库加字段不影响页面传参,页面改表单不用动实体类。很多同学图省事全用一个实体类,结果改一个字段牵连三处,血泪经验。
2.3 数据表设计:房态、订单、入住、账单四张表怎么连
核心表不多,客房、客人、预订订单、入住单、账单、系统用户,六张表撑起整条业务线。先看最关键的客房表和入住单:
-- 客房表:房态和版本号是并发控制的关键 CREATE TABLE room ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE COMMENT '房间号,如 302', type_id INT NOT NULL COMMENT '房型:单人间/标间/大床房', floor INT COMMENT '楼层,用于看板分组', price DECIMAL(10,2) NOT NULL COMMENT '日单价', status VARCHAR(16) NOT NULL DEFAULT 'IDLE' COMMENT 'IDLE 空闲 / BOOKED 已订 / LIVING 在住 / CLEAN 待打扫', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', remark VARCHAR(200) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 入住单:一次入住一条记录,退房只改状态不删数据 CREATE TABLE check_in ( checkin_id BIGINT PRIMARY KEY AUTO_INCREMENT, checkin_no VARCHAR(32) NOT NULL UNIQUE COMMENT '入住单号 CI+日期+序号', room_id INT NOT NULL, customer_id INT NOT NULL, arrive_time DATETIME NOT NULL, leave_time DATETIME NOT NULL, deposit DECIMAL(10,2) DEFAULT 0.00, status VARCHAR(16) NOT NULL DEFAULT 'LIVING' COMMENT 'LIVING/OUT', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_room (room_id), KEY idx_customer (customer_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;表之间的关系是:customer一对多check_in,同一个客人按手机号复用档案;room一对多check_in,一间房的历史入住记录全留着,方便查账;order是预订单,预订成功把房间状态改成BOOKED,客人到店后订单转成入住单;bill在退房时生成,记录房费、押金抵扣、实收金额。这里有个设计取舍值得说:房态没有用布尔字段,而是用字符串状态,因为业务上要区分"已预订但人没到"和"在住",两个布尔位会组合出非法状态,字符串状态配合一章要讲的状态机,校验起来更直接。
| 表名 | 主要职责 | 关键字段 |
|---|---|---|
| room | 客房台账与实时房态 | status、version、price |
| customer | 客人档案 | phone(唯一)、name、id_card |
| order | 预订订单 | order_no、room_id、status |
| check_in | 入住单 | checkin_no、room_id、status |
| bill | 退房账单 | checkin_id、room_fee、real_pay |
| sys_user | 后台账号与角色 | username、role |
3. 核心链路拆解:一次「入住登记」从 JSP 表单到数据库落库
3.1 房态状态机:IDLE、BOOKED、LIVING、CLEAN 之间的合法迁移
房态是整个系统的地基。最怕的写法是到处room.setStatus("LIVING"),改到最后自己都不知道哪些状态能跳到哪些状态。我一般把合法迁移收敛成一张表,写在 service 层统一校验:
| 当前状态 | 触发动作 | 目标状态 | 业务含义 |
|---|---|---|---|
| IDLE | 创建预订 | BOOKED | 锁定房间,防止重复售卖 |
| BOOKED | 办理入住 | LIVING | 订单转入住单,收押金 |
| BOOKED | 取消预订 | IDLE | 释放房间,需记录取消原因 |
| LIVING | 换房 | LIVING | 原房转 CLEAN,新房占用 |
| LIVING | 退房结算 | CLEAN | 生成账单,房间待打扫 |
| CLEAN | 保洁完成 | IDLE | 重新可售 |
状态机写成枚举加校验方法,比散落在各个 controller 里可靠得多:
public enum RoomStatus { IDLE, BOOKED, LIVING, CLEAN; // 合法迁移白名单:key 是当前状态,value 是允许到达的状态 private static final Map<RoomStatus, Set<RoomStatus>> FLOW = new HashMap<>(); static { FLOW.put(IDLE, EnumSet.of(BOOKED)); FLOW.put(BOOKED, EnumSet.of(LIVING, IDLE)); FLOW.put(LIVING, EnumSet.of(CLEAN)); FLOW.put(CLEAN, EnumSet.of(IDLE)); } public boolean canTransferTo(RoomStatus target) { return FLOW.getOrDefault(this, Collections.emptySet()).contains(target); } }逻辑说明:FLOW是静态白名单,canTransferTo只做判断不执行更新,真正的更新交给带条件的 SQL,两道防线。这样做的直接好处是换房、退房这些复杂操作,只要在 service 里调一次canTransferTo,非法迁移当场抛业务异常,前端能给出明确提示,不会出现"房间明明有人还显示空闲"这种脏数据。
3.2 Controller 与 Service:参数校验、事务边界和押金计算
入住登记这条链路涉及三张表的写操作:更新房态、写入或复用客人档案、插入入住单。三步里任何一步失败都必须整体回滚,所以事务的边界要卡在 service 方法上,controller 只负责接参数。
先看 DTO 和 controller:
public class CheckInDTO { @NotNull(message = "房间不能为空") private Integer roomId; @NotBlank(message = "客人姓名不能为空") private String customerName; @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不对") private String phone; @NotNull(message = "入住时间不能为空") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private Date arriveTime; @NotNull(message = "离店时间不能为空") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private Date leaveTime; @DecimalMin(value = "0.00", message = "押金不能为负") private BigDecimal deposit; }@Controller @RequestMapping("/checkin") public class CheckInController { @Autowired private CheckInService checkInService; @PostMapping("/submit") @ResponseBody public Result submit(@Valid CheckInDTO dto, BindingResult binding) { // 只回第一条错误,JSP 页面用 layer 弹一下就够了 if (binding.hasErrors()) { return Result.fail(binding.getFieldError().getDefaultMessage()); } checkInService.checkIn(dto); return Result.ok("入住办理成功"); } }@Valid触发 JSR-303 校验,BindingResult必须紧跟在被校验参数后面,否则 Spring 直接抛BindException;@DateTimeFormat解决 JSP 表单传字符串到Date的转换,页面日期控件的格式要和 pattern 对齐,不然绑定直接失败。接下来是真正干活的 service:
@Service public class CheckInServiceImpl implements CheckInService { @Autowired private RoomMapper roomMapper; @Autowired private CustomerMapper customerMapper; @Autowired private CheckInMapper checkInMapper; @Override @Transactional(rollbackFor = Exception.class) public void checkIn(CheckInDTO dto) { Room room = roomMapper.selectById(dto.getRoomId()); if (room == null) { throw new BizException("房间不存在"); } // 状态机兜底:只有空闲或已预订的房间能办理入住 RoomStatus current = RoomStatus.valueOf(room.getStatus()); if (!current.canTransferTo(RoomStatus.LIVING)) { throw new BizException("当前房态不可办理入住"); } // 乐观锁更新,影响行数为 0 说明有人抢先改了 int rows = roomMapper.occupy(dto.getRoomId(), room.getVersion()); if (rows == 0) { throw new BizException("房间状态已变更,请刷新后重试"); } // 按手机号复用客人档案,避免同一个人反复入住产生重复数据 Customer customer = customerMapper.selectByPhone(dto.getPhone()); if (customer == null) { customer = new Customer(); customer.setName(dto.getCustomerName()); customer.setPhone(dto.getPhone()); customerMapper.insert(customer); } CheckIn checkIn = new CheckIn(); checkIn.setCheckinNo(OrderNoUtil.next("CI")); checkIn.setRoomId(dto.getRoomId()); checkIn.setCustomerId(customer.getCustomerId()); checkIn.setArriveTime(dto.getArriveTime()); checkIn.setLeaveTime(dto.getLeaveTime()); checkIn.setDeposit(dto.getDeposit()); checkIn.setStatus("LIVING"); checkInMapper.insert(checkIn); } }参数和逻辑说明:rollbackFor = Exception.class是必须的,Spring 默认只对RuntimeException回滚,业务里抛受检异常时事务不会回退;OrderNoUtil.next("CI")生成形如CI20240612001的单号,前缀区分入住单和预订单,方便对账时肉眼识别;押金这里只做非负校验,真正的房费计算放在退房,按实际入住天数乘日单价再扣押金,避免入住时算早了还要改。
3.3 MyBatis 映射与并发扣房:带条件的 UPDATE 才是保命符
并发这块是毕业设计里最容易被忽略、上线演示时最容易翻车的点。两个人同时点"办理入住",如果 SQL 只按主键更新,后一条会把前一条覆盖掉,结果一间房开出两张入住单。解决办法是把状态判断写进 WHERE:
<!-- RoomMapper.xml --> <update id="occupy"> UPDATE room SET status = 'LIVING', version = version + 1 WHERE room_id = #{roomId} AND version = #{version} AND status IN ('IDLE', 'BOOKED') </update> <!-- 退房释放:只允许在住状态迁到待打扫 --> <update id="release"> UPDATE room SET status = 'CLEAN', version = version + 1 WHERE room_id = #{roomId} AND status = 'LIVING' </update>这两条 SQL 的返回值都是受影响行数,Java 侧拿到 0 就直接抛业务异常让用户重试。为什么用乐观锁而不是SELECT ... FOR UPDATE?小型酒店并发量低,冲突概率小,乐观锁不需要长时间持有行锁,对连接池更友好;真遇到节假日集中入住,重试一次也够用。另一个细节是version字段每次都加一,退房、换房、保洁完成都要带上,保证任何状态变更都走同一套并发控制。
列表页统一用 PageHelper,避免每张表手写 limit:
public PageInfo<RoomVO> pageRoom(RoomQuery query, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); // 只对紧跟的第一条查询生效 List<RoomVO> list = roomMapper.selectByQuery(query); return new PageInfo<>(list); }PageHelper.startPage必须紧贴查询语句,中间插任何一次别的 mapper 调用,分页参数就会被吃掉,这是插件最常见的误用。
4. 本地跑通:从导入工程到登录后台的完整步骤
4.1 环境清单与数据库初始化
跑这套工程,环境版本对上了能省掉一半问题。我一般用这套组合:JDK 8、Maven 3.6 以上、MySQL 5.7 或 8.0、Tomcat 8.5 或 9.0、IDEA 社区版就够。JDK 别上 17,传统 SSM 加 JSP 在javax包名下,Tomcat 10 换成jakarta命名空间后,web.xml和 JSP 标签库会直接报找不到类。
先把库建起来,脚本在sql/hotel_db.sql,包含建表语句和初始数据:
# 建库,字符集一定指定 utf8mb4,否则客人姓名里的生僻字会变问号 mysql -uroot -p -e "CREATE DATABASE hotel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" # 导入建表脚本和初始数据 mysql -uroot -p hotel_db < sql/hotel_db.sql # 验证表是否建全 mysql -uroot -p -e "USE hotel_db; SHOW TABLES;"脚本里一般会带两条初始数据:管理员账号和几种房型。管理员账号常见做法是admin / 123456,密码用 MD5 存,第一次登录后记得在个人设置里改掉。房型表初始化单人间、标间、大床房三档价格,后面加房间时直接选房型,价格自动带出来。
4.2 配置文件与启动顺序,四个文件改动点在哪
配置分两层容器,这是 SSM 新手最容易懵的地方:applicationContext.xml是根容器,装数据源、Mapper、事务管理器;spring-mvc.xml是子容器,只装 controller、视图解析器、拦截器。子容器能看到根容器的 Bean,反过来不行,所以 service 不能被 mvc 扫描到,否则事务代理会失效。
先改数据库连接,就一个文件:
# jdbc.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hotel_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的密码参数说明:characterEncoding=utf8防止中文乱码;serverTimezone=GMT%2B8解决 MySQL 8 时区报错,%2B是加号的 URL 编码;allowPublicKeyRetrieval=true在 MySQL 8 默认加密插件下不加会连不上;useSSL=false本地开发关掉省事。
然后是 mvc 配置,重点在静态资源放行和拦截器:
<!-- spring-mvc.xml --> <!-- 只扫 controller,别把 service 扫进来 --> <context:component-scan base-package="com.hotel.controller"/> <mvc:annotation-driven/> <!-- JSP 里的 bootstrap、jquery 全靠这两行放行,否则全是 404 --> <mvc:resources mapping="/static/**" location="/static/"/> <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.hotel.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>InternalResourceViewResolver的前后缀决定 controller 返回"room/list"时去找/WEB-INF/views/room/list.jsp,JSP 放在 WEB-INF 下浏览器不能直接访问,只能走 controller,这是一层朴素的安全隔离。拦截器排除登录页和静态资源,其余请求检查 session 里的用户,没登录就重定向回登录页。根容器的配置重点是数据源和事务:
<!-- applicationContext.xml 片段 --> <context:component-scan base-package="com.hotel"> <!-- 把 Controller 排除掉,交给子容器 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.hotel.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>MapperScannerConfigurer里用sqlSessionFactoryBeanName而不是sqlSessionFactory引用,是为了避免属性占位符过早初始化导致数据源配置没生效,这个坑我在两个项目里都踩过。mybatis-config.xml记得打开驼峰映射,下面避坑章会细说。
4.3 登录走查:按角色把主流程点一遍
启动方式两种:IDEA 里配 Tomcat 本地部署,或者命令行打包丢 webapps。命令行更接近真实部署:
# 跳过测试先跑起来,测试用例依赖数据库环境,第一次跑容易卡住 mvn clean package -DskipTests # war 包复制到 Tomcat,上下文路径取 hotel cp target/hotel-management.war $CATALINA_HOME/webapps/hotel.war # 启动后访问 http://localhost:8080/hotel/登录进去别急着点功能,按这条线走一遍,能提前发现八成配置问题:登录页能否正常加载样式(验证静态资源放行)、输入错误密码是否有提示(验证全局异常处理)、登录成功是否跳房态看板(验证拦截器和 session)、新增一间房再刷新列表(验证写库和分页)、办一次入住再退房(验证事务和房态流转)、最后看一眼营业报表有没有数(验证联表统计 SQL)。每一步都对上了,这套工程就算真正跑通了。
5. 常见问题排查:这套系统起不来时先查这五处
5.1 中文乱码,从连接串一路追到 JSP
现象:客人姓名存进数据库变成问号,或者页面显示成乱码。原因通常有三层:数据库或表没用 utf8mb4;JDBC 连接串少了characterEncoding=utf8;web.xml没配编码过滤器,POST 表单按 ISO-8859-1 解码。解决顺序是从底往上查:先SHOW CREATE TABLE customer看字符集,再核对连接串,最后在web.xml最前面加CharacterEncodingFilter并设forceEncoding=true。JSP 页面头部再补一行<%@ page contentType="text/html;charset=UTF-8" %>,三层都对齐基本不会再犯。
5.2 静态资源全 404,DispatcherServlet 的拦截范围没配对
现象:登录页能出来,但 Bootstrap 样式全丢,F12 一片 404。原因是web.xml里 DispatcherServlet 映射到/,把静态资源请求也接管了,容器默认的静态资源处理器被顶掉。解决是在spring-mvc.xml里加<mvc:resources mapping="/static/**" location="/static/"/>,再补一个<mvc:default-servlet-handler/>兜底;同时确认 JSP 里引用路径用的是${pageContext.request.contextPath}/static/...,别写死成绝对路径,换上下文路径就失效。
5.3 @Transactional 不生效,三种典型写法
现象:入住办到一半抛异常,房态改了但入住单没写进去,数据半截。原因八九不离十是这三种:注解加在 controller 上,而事务管理器管的是 service 层;spring-mvc.xml把 service 也扫进去了,导致事务代理被子容器覆盖;方法里try-catch把异常吞了没往外抛,Spring 感知不到失败。解决是注解只加在 service 实现类的公共方法上、根容器扫 service、异常统一抛给全局处理器,并且显式写rollbackFor = Exception.class。自调用(同类方法内部互调)也会绕过代理,这个最隐蔽。
5.4 字段映射空值,驼峰和下划线的默认差异
现象:查询不报错,但 VO 里roomNo、createTime全是 null,只有 id 有值。原因是数据库字段是下划线命名,Java 属性是驼峰,MyBatis 默认不自动转换。解决是在mybatis-config.xml里加<setting name="mapUnderscoreToCamelCase" value="true"/>。另一种情况是 XML 里手写了resultMap但列名和属性名对不上,这种只能逐个核对,别指望自动映射兜底。改完重启不生效,先确认SqlSessionFactoryBean真的加载了这个配置文件。
5.5 日期绑定失败和 Tomcat 版本错配
现象一:提交入住表单报 400,日志里是Failed to convert String to Date。原因是 DTO 的日期字段没加@DateTimeFormat,或者页面控件格式和 pattern 不一致。现象二:换了 Tomcat 10 后启动直接报ClassNotFoundException: javax.servlet.Servlet。原因是 Tomcat 10 起包名从javax迁到jakarta,而这套工程用的是旧命名空间。解决是把 Tomcat 降到 9.0,或者把整个工程的 Servlet API 依赖和 JSP 标签库一起升级,后者改动面太大,毕业设计阶段没必要。还有一个隐性坑是 JDK 17 配 Spring 5.2 以下版本,CGLIB 代理会报模块访问错误,降 JDK 或升 Spring 二选一。
6. 二次开发改造:把毕业设计升级成能演示的完整项目
这套骨架跑通之后,往上加东西的思路比代码本身重要。我一般按"低风险、能演示、可回退"三个标准挑改造点,先做不破坏原有链路的。
第一处改造是订单超时自动取消。预订了没来入住,房间一直挂在BOOKED,房态看板就会骗人。加一个定时任务,每十分钟扫一次超过三十分钟未入住的订单,把订单置为取消、房间释放回IDLE:
@Component public class OrderTimeoutTask { @Autowired private OrderMapper orderMapper; @Autowired private RoomMapper roomMapper; // 每 10 分钟执行一次,cron 六位:秒 分 时 日 月 周 @Scheduled(cron = "0 0/10 * * * ?") @Transactional(rollbackFor = Exception.class) public void cancelTimeoutOrder() { List<Order> list = orderMapper.selectTimeout(30); for (Order o : list) { orderMapper.updateStatus(o.getOrderId(), "CANCEL"); // 释放房间,SQL 里带 status = 'BOOKED' 条件,避免误放已入住房间 roomMapper.releaseFromBooked(o.getRoomId()); } } }别忘了在spring-mvc.xml或根容器加<task:annotation-driven/>,否则注解不生效,任务永远不跑。验证方式很简单:手动插一条三十分钟前的预订单,等一个周期看房态有没有回到空闲,日志里有没有执行记录。
第二处是操作日志。退房金额对不上时,没有日志就是黑匣子。用 AOP 切 service 层的写方法,把操作人、方法名、入参、耗时记一张sys_log表,出问题直接按单号查。第三处是把 JSP 逐步换成 REST 接口加前端页面,先挑报表这种只读页面试水,controller 加@ResponseBody返回 JSON,老页面保留不动,新旧并行,出问题能立刻回退。这种渐进式改造比一次性重写稳得多,也是我这些年做存量系统唯一敢用的方式。
验证方法上,我习惯用 Spring Test 加@Transactional让每个用例跑完自动回滚,重点覆盖入住、退房、换房三个写链路;接口层用 Postman 存一组请求,改完代码一键回放对比返回结构有没有变。从那以后我每次接类似的毕业设计改造,都先把登录拦截器和事务配置单独跑一遍再动业务代码,因为这两个地方一旦出问题,后面所有功能都会跟着遭殃。希望帮到你。
本文还有配套的精品资源,点击获取