简介:这是一份基于SSM+MySQL的酒店管理系统完整项目代码与数据库,专为毕业设计、期末大作业和课程设计场景打造,也可作为Java Web入门后的综合练习项目。系统覆盖房间管理、预订、入住、订单、用户及评论等核心模块,代码带详细注释,结构清晰,即使Java新手也能快速读懂;下载后只需简单配置环境即可部署运行,曾被作者作为高分项目获得导师认可。资源包共112个文件,以Java源码、XML配置、JSP前端页面和SQL数据库脚本为主,辅以properties配置、CSS/JS及少量图片素材,压缩包整体约5.94MB,便于下载与迁移;目录按Controller、Service、Mapper等分层,数据库脚本内置基础数据,方便快速启动和二次开发。目前已有193人学习,适合需要一套可直接参考的SSM酒店管理项目的开发者使用。
1. 基于SSM+MySQL的酒店管理系统:为什么毕业设计总选它,自己写一份要多久
你解压出“基于SSM+MySQL的酒店管理系统项目代码+数据库”之后,导入IDEA不是缺包就是连不上数据库,SQL脚本导进去了登录还是白屏。这不是操作问题,是SSM项目在“作者电脑能跑”和“你电脑能跑”之间隔着一堆隐形差异。
标题里的SSM指Spring、SpringMVC、MyBatis,MySQL负责存储。酒店管理系统成为毕业设计和大作业的常客,是因为业务线足够有料:房间有类型和状态,顾客要预订、入住、退房,钱要算,管理员要分权限。把这套流程用三层架构表达出来,课程设计考察点基本就覆盖了。
我按“搭建骨架→设计库表→串主流程→避坑→验收”的顺序,把这套系统从零讲到能演示。适合手上有代码跑不起来的人、想重写没头绪的人、怕答辩被问设计理由的人。弯路我已经替你走过。
2. 把SSM骨架立起来:三层框架怎么协作,最小配置该给哪些参数
2.1 为什么这个题目死守SSM:课程设计需要的是“能解释”的程序
这两年新人上手都喜欢Spring Boot,因为启动快、配置少,跑起来就是一坨自动配置。但很多学校的毕业设计大纲和验收表就是按SSM出的,原因也很现实:SSM要求你把Spring的Bean管理、SpringMVC的请求分发、MyBatis的SQL映射一层层手动装配起来,答辩提问才有东西可讲。换成Spring Boot,很多环节自动完成,问到底层时容易“黑匣子”。
SSM三个框架的分工记住一句话就行:Spring是一个容器,用IOC管理Service、Mapper这些对象的生命周期,用AOP处理事务;SpringMVC负责把HTTP请求分发到Controller,再返回视图或JSON;MyBatis负责把Mapper接口方法翻译成SQL,并在XML里写动态SQL。酒店管理系统无非就是“请求进来→调Service→调Mapper→查MySQL→渲染页面”,这套分工刚刚好。
2.2 标准Maven工程长什么样:依赖版本别瞎选
先在根目录建一个Maven的war工程,不要用jar,因为最终要丢进Tomcat。src/main下拆成java、resources、webapp三个目录:java里按controller、service、mapper、entity、util分包;resources放spring配置、MyBatis配置、jdbc.properties;webapp放JSP、web.xml和静态资源。很多人拿到项目改半天,就是因为Controller包和Mapper包扫描范围对不上,类加载到了但代理没生成。
pom.xml里的依赖清单是第一个拦路虎。给你一份我用过的组合套餐:
<properties> <spring.version>5.2.22.RELEASE</spring.version> <mybatis.version>3.5.9</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.5</version> </dependency> </dependencies>这组版本的前提是你用Tomcat 8或9、JDK 8。凡是依赖里带javax.servlet的,都是给Tomcat 9以下准备的;如果你装的是Tomcat 10,接口变成了jakarta.servlet,Spring那些旧版本全都不认识,DispatcherServlet都起不来。这是我见过最多的“项目跑不了”原因,没有之一。
2.3 三个配置文件缺一不可:数据源、Spring容器、SpringMVC入口
先建一个jdbc.properties放在resources根目录,这里假设你已经按常规的MySQL安装教程装好8.x版本,并建好了库:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hotel_ssm?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的密码注意几个参数的用途:characterEncoding=utf8控制中文存储;serverTimezone解决MySQL 8时区报错;allowPublicKeyRetrieval=true是专门给MySQL 8的caching_sha2_password插件用的,不加会报“Public Key Retrieval is not allowed”,这个问题后面避坑章节还会展开。
Spring核心配置文件我习惯叫applicationContext.xml,骨架是这样:
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <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="10"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.xxx.hotel.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.xxx.hotel.mapper"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>这里最关键的是MapperScannerConfigurer,它把com.xxx.hotel.mapper包下所有接口扫描一遍,为每个接口生成一个代理对象,Service里才能用@Autowired直接注入。typeAliasesPackage的作用是让Mapper XML里的resultType不用写全路径,比如只写“RoomType”就够。事务管理器的注入很讲究,必须和Service层用同一个数据源,否则@Transactional锁不住连接。
SpringMVC的配置放在spring-mvc.xml:
<context:component-scan base-package="com.xxx.hotel.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:resources mapping="/static/**" location="/static/"/>controller单独扫描,别把service包也扫进来;视图解析器指向WEB-INF/jsp,好处是用户没法直接通过URL访问JSP源码;static放CSS、JS和图片。web.xml里需要注册DispatcherServlet、ContextLoaderListener和CharacterEncodingFilter。CharacterEncodingFilter的配置很重要,很多中文乱码就是因为缺了它:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>注意:DispatcherServlet的url-pattern是“/”,它会把所有请求都交给SpringMVC,包括静态资源,所以spring-mvc.xml里必须配mvc:resources放行static目录。如果这个顺序错了,页面样式会全部404。
2.4 跑通第一个列表接口:从实体类到JSON返回
骨架配完,先别急着写业务,用一个“查询房间类型”接口验证整套链路是否通。实体类写在entity包里:
public class RoomType { private Integer id; private String typeName; private Double price; private Integer bedNum; private String remark; // getter / setter }Mapper接口写在mapper包里:
public interface RoomTypeMapper { List<RoomType> listAll(); }对应的XML放在resources/mapper目录下,文件名叫RoomTypeMapper.xml:
<mapper namespace="com.xxx.hotel.mapper.RoomTypeMapper"> <select id="listAll" resultType="RoomType"> SELECT id, type_name, price, bed_num, remark FROM t_room_type ORDER BY id </select> </mapper>Service层照例写接口和实现,Controller再封装一层:
@Controller @RequestMapping("/roomType") public class RoomTypeController { @Autowired private RoomTypeService roomTypeService; @RequestMapping("/list") @ResponseBody public List<RoomType> list() { return roomTypeService.listAll(); } }这一段看起来简单,但至少卡住过三个人:一个是resultType忘写导致映射失败,一个是Mapper XML的namespace写错导致找不到方法,还有一个是@ResponseBody返回JSON时Jackson依赖没加,页面直接白屏。启动Tomcat后访问http://localhost:8080/hotel-ssm/roomType/list,能返回JSON就说明框架链路通了,再往上盖酒店业务才有底气。
3. 酒店管理系统的数据库设计:从表结构到初始化数据
3.1 核心表拆成几张:让“数据关系”在答辩时说得清
酒店管理系统的表结构我一般拆五张核心表:t_admin管理员表、t_customer顾客表、t_room_type房间类型表、t_room房间表、t_order订单表。有人还会加消费记录表,但大作业做到第五张表已经能讲清楚了。核心是t_order,它把顾客、房间、房价、时间串在一起。
DDL如下,直接跑在一个空库里就行:
CREATE DATABASE hotel_ssm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hotel_ssm; CREATE TABLE t_admin ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), role VARCHAR(20) DEFAULT 'admin', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_customer ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, id_card VARCHAR(20) NOT NULL, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_room_type ( id INT AUTO_INCREMENT PRIMARY KEY, type_name VARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL, bed_num TINYINT DEFAULT 2, area INT, remark VARCHAR(200) ); CREATE TABLE t_room ( id INT AUTO_INCREMENT PRIMARY KEY, room_no VARCHAR(10) NOT NULL UNIQUE, room_type_id INT NOT NULL, floor TINYINT, status TINYINT DEFAULT 0 COMMENT '0-空闲 1-已入住 2-打扫中 3-维修', CONSTRAINT fk_room_type FOREIGN KEY (room_type_id) REFERENCES t_room_type(id) ); CREATE TABLE t_order ( id INT AUTO_INCREMENT PRIMARY KEY, customer_id INT NOT NULL, room_id INT NOT NULL, check_in_date DATETIME, check_out_date DATETIME, actual_out_date DATETIME, price DECIMAL(10,2) COMMENT '入住时房价快照', deposit DECIMAL(10,2) DEFAULT 0, total_amount DECIMAL(10,2) DEFAULT 0, status TINYINT DEFAULT 0 COMMENT '0-已预订 1-已入住 2-已退房 3-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES t_customer(id), CONSTRAINT fk_order_room FOREIGN KEY (room_id) REFERENCES t_room(id) );我建议把数据库名和表名统一用t_前缀,理由是答辩讲PPT时容易指认,也避免把MySQL的保留字当表名。外键约束可以建,但注意“删除房间”之前必须处理订单;如果觉得麻烦,也可以只保留逻辑外键,在Java代码里维护关联,但答辩被问到“你怎么保证引用完整性”时,有物理外键更好解释。
3.2 订单状态和房间状态:两套状态字段不要写进同一个值
订单表的status表达的是业务进度:0已预订、1已入住、2已退房、3已取消。房间表的status表达的是房间可用性:0空闲、1已入住、2打扫中、3维修。两张表的状态要联动,但要分开维护,因为“订单已退房”和“房间已打扫”未必同时发生。
经常有同学只设一个订单状态,退房时直接把订单改成“已退房”,房间状态没有改,第二天同房间又被卖出去,数据库里出现两单撞房。正确做法是:入住时同时执行“订单status=1”和“房间status=1”;退房时执行“订单status=2”和“房间status=2”;服务员做房完成后,再把“房间status=0”。状态机变化最好放在Service层统一控制,而不是在Controller里一改再改。
3.3 初始化数据:房号规则、房价和账号密码
空表没法演示,初始化数据里要注意三件事:房间编号规则、房型价格、管理员账号。房号我习惯用楼层+两位房号,比如1001、1002表示1层01/02房间,2001表示2层,前端下拉框也好看。价格用DECIMAL(10,2)而不是DOUBLE,后面算钱才不会出现16.199999这种浮点数。
INSERT INTO t_admin (username, password, real_name, role) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', 'admin'), ('manager', 'e10adc3949ba59abbe56e057f20f883e', '客房部经理', 'manager'); INSERT INTO t_room_type (type_name, price, bed_num, area, remark) VALUES ('单人间', 128.00, 1, 18, '1.8米床'), ('标准间', 188.00, 2, 24, '双床'), ('大床房', 228.00, 2, 28, '2米大床'), ('套房', 428.00, 2, 45, '客厅+卧室'); INSERT INTO t_room (room_no, room_type_id, floor, status) VALUES ('1001', 1, 1, 0), ('1002', 1, 1, 0), ('1003', 2, 1, 1), ('2001', 3, 2, 0), ('2002', 4, 2, 0); INSERT INTO t_customer (name, id_card, phone) VALUES ('张三', '110101199001011234', '13800138000');上面的password是“123456”的MD5值:e10adc3949ba59abbe56e057f20f883e。如果你在代码里直接用明文比对,这行就废了;如果不想做MD5,也可以把password改成明文,但答辩容易被追问安全隐患。我一般建议至少做个MD5,这是最简单也最能拿去讲的“安全设计”。
3.4 权限设计:不用堆五张RBAC表,一个角色字段就够用
毕业设计的权限管理做到“管理员登录才能访问后台、不同角色看到不同菜单”这个粒度就差不多了。t_admin表里加了一个role字段,admin对应超级管理员,manager对应普通管理员。登录成功后把对象放进Session,然后用一个SpringMVC拦截器统一判断。
拦截器里判断Session中的admin是否为空,为空就跳登录页。如果还要细分权限,在拦截器里再判断一下role:
public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Admin admin = (Admin) session.getAttribute("admin"); if (admin == null) { response.sendRedirect(request.getContextPath() + "/admin/login"); return false; } if ("manager".equals(admin.getRole()) && request.getRequestURI().contains("/admin/user")) { response.sendRedirect(request.getContextPath() + "/index"); return false; } return true; } }别一上来就上Spring Security,课设里那套登录流程和十几个跳转可能会让配置越调越乱。手写拦截器简单、可控,答辩时还能讲出“这是AOP思想的体现”,比你抄一份看不懂的框架代码强得多。
4. 从页面到接口:把登录、入住、退房三个主流程串起来
4.1 登录页与Session:把对象放进去,别把密码放进去
前端页面用JSP足够,登录表单提交到/admin/login,Controller里先按用户名查出admin对象,再比对密码。比对的逻辑要写在Web层之外的Service里,这样拦截器、测试都能复用。核心代码:
@Controller @RequestMapping("/admin") public class AdminController { @Autowired private AdminService adminService; @RequestMapping("/login") public String login(String username, String password, HttpSession session, Model model) { Admin admin = adminService.findByUsername(username); if (admin == null) { model.addAttribute("error", "用户名不存在"); return "login"; } if (!admin.getPassword().equals(Md5Util.md5(password))) { model.addAttribute("error", "密码错误"); return "login"; } session.setAttribute("admin", admin); return "redirect:/index"; } }Md5Util是自己写的一个工具类,里面用MessageDigest把用户输入的密码做一次MD5,再和数据库里的密文比对。登录成功后Session里存的是admin对象,页面直接用${sessionScope.admin.realName}显示当前登录人。千万不要把明文密码也塞进去,避免JSP输出时不注意打了password字段。
4.2 入住登记:两步写操作请放进一个事务
入住登记本来不复杂,但涉及两张表:先给t_order插一条状态为“已入住”的记录,再把t_room的状态改成“已入住”。如果这两步不是原子操作,就会出现订单生成但房态没变,或者房态变了但订单没成的脏数据。我习惯把业务写在OrderService里,加@Transactional。
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RoomMapper roomMapper; @Transactional(rollbackFor = Exception.class) public Integer checkIn(Order order) { Room room = roomMapper.selectById(order.getRoomId()); if (room == null || room.getStatus() != 0) { throw new RuntimeException("房间不存在或已被占用"); } order.setStatus(1); order.setPrice(room.getPrice()); order.setCheckInDate(new Date()); orderMapper.insert(order); Room update = new Room(); update.setId(order.getRoomId()); update.setStatus(1); roomMapper.updateStatus(update); return order.getId(); } }这段代码有几个细节容易被忽略。第一,price要取房间当前价格写入订单,而不是前台传多少钱就收多少钱,因为退房时房型可能调价了,订单里的价格才是“快照”。第二,orderMapper.insert要拿到自增主键,Mapper XML里得加useGeneratedKeys="true" keyProperty="id",否则order.getId()是null。第三,事务方法必须public,并且从Controller其他Service调用,不能在实现类内部用this.checkIn()自己调用,否则代理切不进去。
4.3 退房结账:延时费、押金抵扣和金额快照
退房是最容易翻车的页面,因为涉及天数计算、延时费和押金。我给出的算法是先算出应入住天数和实际入住天数,再判断是否超出预计退房时间,超出6小时加收半天房费;最后把总金额写回订单。
public double calculateOrderAmount(Order order) { if (order.getCheckInDate() == null || order.getActualOutDate() == null) { return BigDecimal.ZERO.doubleValue(); } long hours = Duration.between(order.getCheckInDate(), order.getActualOutDate()).toHours(); int days = (int) Math.ceil(hours / 24.0); if (days <= 0) { days = 1; } BigDecimal roomFee = order.getPrice().multiply(BigDecimal.valueOf(days)); BigDecimal overtimeFee = BigDecimal.ZERO; if (order.getCheckOutDate() != null && order.getActualOutDate().after(order.getCheckOutDate())) { long overHours = Duration.between(order.getCheckOutDate(), order.getActualOutDate()).toHours(); if (overHours > 6) { overtimeFee = order.getPrice().multiply(BigDecimal.valueOf(0.5)); } } return roomFee.add(overtimeFee).doubleValue(); }这里避开了两个坑:一是天数计算不用自然日,而是按小时向上取整,避免“住了一晚但跨天”的误判;二是金额运算用BigDecimal,不能用double直接乘。如果你连退房流程都做不完,也要保证“房态能释放”,最少把实际退房时间、订单状态、房间状态三个字段更新正确。押金逻辑我一般简化成“退房时总金额先从押金中抵扣,多退少补”,代码里只多一步减法,答辩前记得在演示里说清。
4.4 订单查询和分页:MyBatis动态SQL是烤肉店里的孜然
后台管理页面通常需要按“订单状态、顾客姓名、入住时间”筛选订单,这种多条件组合用MyBatis的动态SQL是最舒服的。Mapper接口接收一个Map或VO,XML里用where、if拼接,参数不需要的语句不会进SQL,避免了“拼SQL同时要拼and”的恶心事。
<select id="searchOrders" resultType="Order" parameterType="map"> SELECT o.*, c.name AS customerName, r.room_no AS roomNo FROM t_order o LEFT JOIN t_customer c ON o.customer_id = c.id LEFT JOIN t_room r ON o.room_id = r.id <where> <if test="status != null"> AND o.status = #{status} </if> <if test="customerName != null and customerName != ''"> AND c.name LIKE CONCAT('%', #{customerName}, '%') </if> <if test="beginDate != null and beginDate != ''"> AND o.check_in_date >= #{beginDate} </if> <if test="endDate != null and endDate != ''"> AND o.check_out_date <= #{endDate} </if> </where> ORDER BY o.create_time DESC </select>注意小于号必须用<、大于号用>,不然XML解析会报错。首页和分页可以手写,也可以引入PageHelper插件,推荐PageHelper,因为一行PageHelper.startPage(pageNum, pageSize)就能接住列表,再包一层PageInfo就能拿到total。
5. SSM+MySQL酒店管理系统避坑指南:五个让新手卡到凌晨的问题
5.1 启动Tomcat报ClassNotFoundException:依赖没打进去
现象:IDEA里编译没有错,一启动Tomcat就抛NoClassDefFoundError或ClassNotFoundException,最常见的是spring-webmvc / mybatis-spring这类类找不到。还有的报错在Tomcat启动日志里,写着“Unable to instantiate Servlet [dispatcher]”。
原因:pom.xml明明引入了依赖,IDEA的Dependencies里也能看到,但部署到Tomcat时没有把依赖的jar包打进WEB-INF/lib。Maven工程必须用war包方式打包,IDEA的Tomcat部署也依赖Artifacts配置;如果你直接导入源码,本地Maven仓库又缺这些jar,情况就会雪上加霜。
解决:在Maven面板执行mvn package,然后用压缩工具打开生成的war,确认WEB-INF/lib里有几十个jar。IDEA用Tomcat启动时,在File→Project Structure→Artifacts里检查“Available Elements”是否把library files加入右侧的Output Layout。如果运行环境是Tomcat 10,还要换成jakarta.servlet-api依赖,并把Spring版本升级到6.x或把运行环境换回Tomcat 9;这两个办法选一个,否则永远起不来。
5.2 页面和数据库全是中文问号:三层字符集一起乱
现象:登录页输入“管理员”,提交后显示的欢迎语是“????”,MySQL里t_admin表也存成了问号。更隐蔽的是,直接写进页面的中文正常,管理员从数据库读出来的中文就乱。
原因:乱码不是一个环节的问题。前端表单默认按页面编码提交,Tomcat默认用ISO-8859-1解码,MySQL连接没带characterEncoding参数,表字段默认又是latin1,这四个环节只要有一层不一致,中文就走样。只改JDBC URL不改过滤器,只改过滤器不变表结构,都会白忙。
解决:先把四层统一成UTF-8。web.xml里配置CharacterEncodingFilter,url-pattern用/*,它在请求进入Controller前把请求体和响应都转成UTF-8。JSP页面顶部加<%@ page contentType="text/html;charset=UTF-8" %>,并把IDE右下角编码切到UTF-8。JDBC URL追加useUnicode=true&characterEncoding=utf8。最后把已有的表转成utf8mb4:ALTER TABLE t_admin CONVERT TO CHARACTER SET utf8mb4;。四步顺序不能乱,改完一步就重启验证一次。
5.3 MySQL 8连接报Public Key Retrieval is not allowed:时区和认证插件
现象:本地项目刚启动没问题,跑几小时重启后报“Public Key Retrieval is not allowed for user”,或者数据库端是MariaDB、连接端还是MySQL驱动,直接给你抛“Communications link failure”。
原因:MySQL 8默认用caching_sha2_password认证插件,JDBC需要服务端的公钥来完成密码加密传输;旧驱动或没打开allowPublicKeyRetrieval时,连接拒绝。时区参数没指定时,MySQL会读系统时区,而Linux下的CST或“中国标准时间”这类写法JDBC不认识,于是爆出unrecognized time zone。
解决:url改为
jdbc:mysql://localhost:3306/hotel_ssm?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true同时确认驱动是8.x。如果还是报时区,在MySQL命令行执行SET GLOBAL time_zone='+08:00';。如果是MariaDB,把依赖换成org.mariadb.jdbc:mariadb-java-client,并按MariaDB的驱动类名org.mariadb.jdbc.Driver修改driverClassName。
5.4 事务没生效,订单插进去了房态没更新:引擎和方法可见性
现象:入住登记成功,订单表里新记录出现了,但房间状态永远停在“空闲”;或者反过来,退房时房间释放了,订单金额没算出来,数据对不上。
原因:第一,表引擎是MyISAM,它根本不支持事务,InnoDB语法没作用。第二,@Transactional加在private方法上或同一个Service实例内部调用另一个方法,SpringAOP代理没法切到私有方法和内部调用,事务等于摆设。第三,Service里catch异常后没重新抛,Spring判定业务正常提交了,自然不会回滚。
解决:先执行SHOW TABLE STATUS WHERE Name='t_order';,Engine列如果不是InnoDB,就执行ALTER TABLE t_order ENGINE=InnoDB;,同时把其他业务表全部转成InnoDB。再检查Service方法:事务方法必须public,而且必须从Controller或其他Service调用,不能在实现类内部用this.checkIn()这种写法。最后检查catch代码块,至少throw new RuntimeException("退房失败", e);把异常抛出去。
5.5 部署到服务器后样式全丢、上传图片神秘消失:路径写死是万恶之源
现象:本地用IDEA内置Tomcat调试,页面CSS/JS正常;打成war丢到Linux后,样式加载404,上传的图片只在当前会话能看,重启就丢。
原因:JSP里写了相对路径,比如../static/css/main.css,本地contextPath是/依赖正常,改到服务器后contextPath变成/hotel-ssm,相对路径就错位。图片上传保存到了项目webapps下的一个临时目录,Tomcat每次clean或重启都会清理,有时还会被系统临时文件清理程序删掉。
解决:JSP里统一用<%=request.getContextPath()%>或${pageContext.request.contextPath}拼绝对路径。上传文件改为保存到配置的磁盘绝对路径,比如upload.path=/data/hotel/uploads/,代码读取该配置;同时在SpringMVC配置里添加
<mvc:resources mapping="/uploads/**" location="file:/data/hotel/uploads/"/>把磁盘路径映射成URL可访问。这样即使重启,图片也还在磁盘上,主机的路径也不会因为部署位置而漂移。
6. 验证与进阶:给自己的酒店管理系统写一份验收清单
系统写完之后,别急着打包交差,先按“核心流程”走一遍验收清单,我把答辩时常用的流程列成一张表:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 使用admin身份登录 | 跳转后台首页,顶部显示管理员姓名 |
| 2 | 查看房态列表 | 1001、1002空闲,1003显示已入住 |
| 3 | 新增一条房间类型 | 房型列表出现新类型,价格用DECIMAL显示 |
| 4 | 为张三办理入住(选1001) | 订单状态变成已入住,房态列表1001变成已入住 |
| 5 | 模拟退房(设置实际退房时间晚2小时) | 订单状态变已退房,金额=1天房费,房间状态变打扫中 |
| 6 | 把房间1001标记为打扫完成 | 房态列表恢复空闲 |
| 7 | 用管理员manager登录 | 能进首页但不能访问用户管理页 |
| 8 | 用错误密码登录 | 页面出现错误提示,不跳转 |
演示时按这张表走,每个操作都能对应一个业务模块,答辩老师一般不会再难为你。如果时间富余,还可以在“订单管理”页加一个简单的分组统计:按订单状态展示柱状图或饼图,图表数据用MyBatis的group by查出来返回JSON,前端用ECharts绘制,这是最廉价的“系统亮点”。
再往上进阶,方向很明确:把SpringMVC+JSP换成Spring Boot+Vue,后端接口保留不变,数据库不用改;把MyBatis升级成MyBatis Plus,消除大量BaseMapper样板代码;房态查询加一个Redis缓存,避免每次刷新都打MySQL;退房金额计算抽成策略模式,方便以后增加钟点房、长租房计费规则。这些升级每做一步都依赖你现在这套SSM基础。
我经手过不少毕业设计项目,最大的教训就是:跑不起来的代码等于零。提前一天把Maven环境、MySQL脚本、Tomcat版本固化好,比临答辩前再去调任何一个变量都值。后面的路你可以按这份清单把项目完整走一遍,再把每个模块的日志打出来,做到每点一个按钮都知道它的执行链路,答辩基本稳了。希望帮到你。
本文还有配套的精品资源,点击获取