SSM框架打造民宿旅游管理系统:从数据库设计到实战部署
简介:这是一套基于SSM框架(Spring+SpringMVC+MyBatis)实现的民宿旅游管理系统,含用户前台与管理后台,面向Java毕业设计及SSM框架学习者。系统采用B/S架构,使用Maven管理依赖,数据库为MySQL5.7,支持IDEA或Eclipse运行。资源共922个文件,压缩包约26.72MB,其中包含167个Java源码、58个Vue页面、156个JavaScript、46个CSS样式、55张JPG图片以及SQL脚本、配置文件与启动批处理脚本,源码、数据库脚本和配套毕业论文均在包内。平台功能覆盖用户注册登录、景点信息浏览、收藏购物车、路线规划、景点美食、住宿信息、留言反馈、订单管理等完整业务链,后台还提供用户管理、订单管理、系统管理等模块。项目经过严格调试,可直接作为毕业设计参考或二次开发基础。目前已有165人学习,适合正在寻找民宿旅游类SSM项目参考的开发者。
1. 民宿旅游管理系统还选 SSM:不只是为毕设,这个架构仍然能打
先说个反直觉的结论:在 Spring Boot 大行其道的 2024 年,每年仍有大量 Java 项目在用 SSM(Spring + Spring MVC + MyBatis)这套组合交付,尤其集中在民宿、酒店、旅游这类以 CRUD 为核心业务的 B/S 架构管理系统。原因不只是"课程就教了这个",而是 SSM 的分层足够清晰,三层结构把请求处理、业务逻辑、数据访问钉死在三处,一个中型团队或一个独立开发者反而更容易维护。你手上这个"基于 SSM 框架实现的民宿旅游管理系统含前台",本质就是一个典型的信息管理系统:后台管民宿的房间、订单、用户、评论,前台面向游客做民宿展示、房间查询、在线预订和留言。它能解决的问题非常具体——把民宿的房态和订单从 Excel 和微信聊天里搬进一个 Web 系统,同时给你一套完整可复现的 SSM 落地样本。适合谁?准备做 Java 毕设的学生、刚学完 SSM 想找一个完整项目练手的人、以及小民宿主想低成本搞一套管理系统。
2. 拆解民宿业务:SSM 的模块边界与 B/S 架构下的数据流
2.1 民宿系统的功能清单从哪里来:围绕"房态"而不是"房间"设计
很多人一拿到"民宿旅游管理系统"就开始画表,第一张往往是"房间表",然后开始犹豫要不要把床型、面积、朝向都塞进去。我建议你先别急着建表,先理业务:民宿的核心不是房间本身,而是房态——一间房在某天是"可订"还是"已订",这决定了你的订单表必须关联日期维度。一个正常的民宿系统至少包含这几块:前台的用户注册登录、民宿列表与详情展示、按日期查房、提交订单(或在线留言咨询)、支付(在毕设里通常做模拟);后台的房型管理、房间管理、订单管理、用户管理、留言管理、统计报表。功能清单出来后,模块边界就清晰了:用户模块、民宿/房型模块、订单模块、留言模块。
对应的 SSM 分层也顺理成章:Controller 层接收前台请求,Service 层处理订单状态流转,Mapper 层操作 MySQL。这里有一个常见误区——很多人把"民宿"和"房间"合并成一张表,然后在房间表里写死价格。实际项目中我一般这样设计:民宿表(homestay)存基础信息,房型表(room_type)存价格、床型、可住人数,房间表(room)存具体哪一栋哪一间,三者分开。原因很简单:同一家民宿的不同房型价格不同,同一房型可能有多间房,分表后统计和后续扩展都方便。如果你的选题里还包含"旅游"元素,通常还要加一张旅游攻略或景点推荐表,但这张表一般只做展示,不参与核心流程。
2.2 B/S 架构下 SSM 的三层协作:一次请求从 JSP 到 MySQL 的完整链路
B/S 架构意味着用户通过浏览器访问,服务端完成所有业务逻辑。在 SSM 项目中,一次"前台用户查看民宿列表"的请求,完整链路是:浏览器发起 GET 请求 → Spring MVC 的 DispatcherServlet 根据 URL 找到对应的 Controller 方法 → Controller 调用 Service 接口 → Service 调用 Mapper 接口 → MyBatis 通过 XML 或注解里的 SQL 操作 MySQL → 结果一层层返回 → Controller 把数据塞进 ModelAndView → 视图解析器渲染 JSP → 响应回浏览器。
这个链路里最容易让新手晕的是"接口"这个词——SSM 里你通常会看到一套接口加一套实现:HomestayService 接口定义方法,HomestayServiceImpl 写具体逻辑,Controller 只依赖接口。这其实是 Java 面向对象编程的经典姿势:面向接口编程。它的好处是后期换实现(比如把本地存储换成某云存储)时不用改 Controller。我知道很多做毕设的同学嫌麻烦直接 Controller 里 new ServiceImpl,能跑,但面试官或者答辩老师会问"为什么 Controller 不能直接 new Service?你这里违反了哪条设计原则?"——问到就尴尬了。所以既然标题里写了 SSM,就按标准的接口 + 实现 + 注解方式来写,别省这一步。
2.3 为什么坚持 SSM 而不是 Spring Boot:选型逻辑要和交付物对齐
这里必须先说清楚 SSM 和 Spring Boot 的关系,因为这是热词里"Spring Boot + MyBatis"与"SSM"经常被拿来做对比的点。SSM 是 Spring、Spring MVC、MyBatis 三个框架的手动整合,Spring Boot 则把 Spring 生态打包成了自动配置的快速开发框架。做真实项目我会优先选 Spring Boot,但做这个民宿系统,SSM 反而是更合理的选择——因为它要交付源码和毕业论文,SSM 的配置过程本身就是论文里"系统实现"那一章的核心内容。你想想,如果论文里写"使用 Spring Boot 自动配置完成项目搭建",那技术部分两页就没了;写 SSM 你可以讲 Spring 容器如何管理 Bean、Spring MVC 的请求映射机制、MyBatis 如何通过动态代理生成 Mapper 实现类,这些内容才是凑足篇幅且真有技术含量的部分。
另外从学习角度讲,SSM 能让你把"配置"这件事彻底搞懂。Spring Boot 把 web.xml、applicationContext.xml 全部藏起来了,你在 SSM 里得亲手配置 DispatcherServlet、扫描包、事务管理器、MyBatis 的 SqlSessionFactory——这些配置跑通一遍,你对"Spring 容器是什么""Bean 怎么被管理"的理解会上一个台阶。所以结论是:如果只是要一个能用的系统,用 Spring Boot;如果要的是"能跑 + 能讲清楚 + 论文有内容"的完整交付物,SSM 是更匹配的选择。
3. 从数据库到 Mapper:民宿系统的核心表设计与 MyBatis 落地
3.1 民宿核心表设计:用户、民宿、房型、房间、订单五张表的字段取舍
数据库设计决定了这个 SSM 项目是做起来顺手还是全程别扭。下面给出一套我在民宿管理类项目里常用的建表方案,你可以直接复制到 Navicat 或命令行执行。要注意字符集统一用 utf8mb4,因为前台评论文本可能包含 Emoji,utf8 会报 Incorrect string value 错误。
CREATE DATABASE IF NOT EXISTS homestay_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE homestay_system; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT '前台用户表'; CREATE TABLE t_homestay ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '民宿名称', address VARCHAR(255), description TEXT, cover_img VARCHAR(255) COMMENT '封面图URL', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT '民宿表'; CREATE TABLE t_room_type ( id INT PRIMARY KEY AUTO_INCREMENT, homestay_id INT NOT NULL COMMENT '所属民宿', type_name VARCHAR(50) COMMENT '如:大床房、双床房', price DECIMAL(10,2) NOT NULL COMMENT '每晚价格', bed_count INT DEFAULT 1, max_people INT DEFAULT 2, room_area VARCHAR(20), KEY idx_homestay (homestay_id) ) ENGINE=InnoDB COMMENT '房型表'; CREATE TABLE t_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_type_id INT NOT NULL COMMENT '关联房型', room_no VARCHAR(20) NOT NULL COMMENT '房间编号', KEY idx_type (room_type_id) ) ENGINE=InnoDB COMMENT '房间表,具体到门牌号'; CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(36) NOT NULL UNIQUE COMMENT '订单编号', user_id INT NOT NULL, room_type_id INT NOT NULL COMMENT '订的是房型', check_in_date DATE NOT NULL COMMENT '入住日期', check_out_date DATE NOT NULL COMMENT '离店日期', total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待确认 1已确认 2已入住 3已退房 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_date_range (check_in_date, check_out_date) ) ENGINE=InnoDB COMMENT '订单表';这段建表脚本有几个值得说明的取舍点。第一,订单表关联的是 room_type(房型)而不是具体 room(房间),这在民宿行业是常见做法——客人订的是"大床房一晚",不是"101号房",具体分房是入住时前台做的事;如果你在订单表里直接关联具体房间,就会出现客人预订成功但当天那间房被保洁临时锁定的尴尬。第二,t_order 里加了 order_no 这个唯一业务编号,而不是直接用数据库自增 id 暴露给用户,原因是订单号通常需要展示在页面和短信里,自增 id 容易被遍历抓数据。第三,price 用的 DECIMAL(10,2) 而不是 FLOAT,这个不用多说——涉及金额的类型用浮点数迟早会在对账时翻车。
3.2 MyBatis 写 Mapper:为什么民宿这个业务里动态 SQL 是刚需
MyBatis 区别于 Hibernate 和 JPA 的核心能力就是手写 SQL,而民宿系统的查询场景特别吃这个能力。拿"前台按入住日期、离店日期、人数筛选可用房型"来举例,这个查询的条件是可变的:用户可能填了日期没填人数,也可能填了人数没选民宿。用 Java 代码去拼 SQL 字符串容易出错且难维护,MyBatis 的<where>和<if>标签就是干这个的。
<select id="selectAvailableRoomTypes" resultType="map"> SELECT rt.id, rt.type_name, rt.price, rt.max_people, h.name AS homestay_name, (rt.total_rooms - IFNULL(o.booked_count, 0)) AS available_rooms FROM t_room_type rt JOIN t_homestay h ON rt.homestay_id = h.id LEFT JOIN ( SELECT room_type_id, COUNT(DISTINCT CASE WHEN #{checkIn} <= check_out_date AND #{checkOut} >= check_in_date THEN id END) AS booked_count FROM t_order WHERE status IN (0, 1, 2) GROUP BY room_type_id ) o ON rt.id = o.room_type_id <where> <if test="homestayId != null and homestayId != 0"> AND rt.homestay_id = #{homestayId} </if> <if test="minPrice != null"> AND rt.price >= #{minPrice} </if> <if test="maxPeople != null"> AND rt.max_people >= #{maxPeople} </if> AND (rt.total_rooms - IFNULL(o.booked_count, 0)) > 0 </where> ORDER BY rt.price ASC </select>这里单独解释一下#{checkIn} <= check_out_date这一段:在 XML 里<符号会被解析器当成标签开始,所以要么用<转义,要么把条件写进<![CDATA[]]>块里。这是新人在 MyBatis 里最常见的踩坑点之一,报错信息往往是 "The content of elements must consist of well-formed character data"。另外注意子查询里的COUNT(DISTINCT CASE WHEN ... THEN id END)— 用订单表反查"哪些房型在重叠日期段有订单",比在 Java 里逐条遍历订单判断日期碰撞要快得多,这也是 MyBatis 手写 SQL 的价值所在:把数据库当成计算引擎用,而不是把数据捞到内存里处理。
3.3 Mapper 接口和 XML 绑定:一个注解都不写不代表没有映射规则
SSM 项目里 Mapper 层是接口加 XML 的组合,接口定义方法签名,XML 写 SQL。这里有个很多教程没讲透的约束:接口的全限定名必须和 XML 文件的 namespace 一致,方法名必须和 SQL 标签的 id 一致。比如接口是com.example.mapper.HomestayMapper,XML 就是HomestayMapper.xml,文件放在 resources 下的com/example/mapper/目录,namespace 就是com.example.mapper.HomestayMapper。这个绑定关系由 MyBatis 启动时扫描建立,对不上就报 "Invalid bound statement (not found)"——不是 SQL 写错,不是接口写错,纯粹是名字路径不一致。
// HomestayMapper.java package com.example.mapper; import com.example.entity.Homestay; import java.util.List; public interface HomestayMapper { List<Homestay> findAll(); Homestay findById(Integer id); int insert(Homestay homestay); int update(Homestay homestay); int deleteById(Integer id); }对应 XML 里的<mapper namespace="com.example.mapper.HomestayMapper">,里面每个<select id="findAll">就对应接口的findAll()方法。参数传递上要注意:单个参数可以直接用#{任意名字},但多个参数时必须用@Param注解或在 XML 里用param1、param2,比如上面的selectAvailableRoomTypes里有checkIn、checkOut、homestayId多个参数,Mapper 接口的签名就必须是:
List<Map<String, Object>> selectAvailableRoomTypes( @Param("checkIn") String checkIn, @Param("checkOut") String checkOut, @Param("homestayId") Integer homestayId, @Param("maxPeople") Integer maxPeople, @Param("minPrice") BigDecimal minPrice );不加@Param的话 MyBatis 会直接抛 "Parameter 'xxx' not found. Available parameters are [0, 1, param1, param2]" 的异常。这个是血泪经验,每次都能在答辩现场听到同学说"我这代码明明没问题"——多参数不加@Param是 MyBatis 新手第一大翻车现场。
4. 前台功能实现:民宿展示、日期查房与订单提交的完整链路
4.1 前台民宿列表页:Controller 返回 ModelAndView 的三种写法
前台首页要展示民宿列表,这张列表的数据来源是 t_homestay 表。Controller 里常见的写法有三种,我分别列出,你可以对比一下差异。
// 写法一:返回字符串,数据放 Model @RequestMapping("/index") public String index(Model model) { List<Homestay> list = homestayService.findAll(); model.addAttribute("homestayList", list); return "front/index"; } // 写法二:返回 ModelAndView @RequestMapping("/index2") public ModelAndView index2() { ModelAndView mv = new ModelAndView("front/index"); mv.addObject("homestayList", homestayService.findAll()); return mv; } // 写法三:返回 JSON(前台页面用 Ajax 渲染) @RequestMapping("/index3") @ResponseBody public List<Homestay> index3() { return homestayService.findAll(); }写法一和写法二本质没有区别,Model 是 Spring MVC 提供的接口,ModelAndView 则是视图和数据的一个包装对象,老项目里常看到,面试也常被问"两者区别是什么"——标准答案是:功能相同,ModelAndView 适合在方法内做跳转控制,Model 更轻量,推荐在方法里只操作 Model。写法三加了@ResponseBody,返回的是 JSON 而不是视图,这种用在前后端分离的场景,但题目写的 B/S 架构 + JSP 渲染,所以我一般推荐写法一,可读性最好,JSP 里直接用${homestayList}迭代即可。注意一点:如果方法上什么都不加且方法在@Controller类里,Spring MVC 会把返回值当 JSP 路径处理,如果你返回了一个对象,就会报 "getOutputStream() has already been called" 或者视图解析失败。所以在 SSM 的 Controller 里,"这个方法是返回页面还是返回数据"必须心里有数,别混着来。
4.2 提交订单的事务边界:为什么涉及钱的代码必须加 @Transactional
订单提交是前台功能里唯一涉及多表写入的操作——插入订单记录、更新房态(如果按具体房间设计)、可能要扣减库存。这中间任何一步失败,都不能留下半截数据。Spring 的声明式事务用注解就能搞定,这是 SSM 里必须掌握的高频注解之一。
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RoomTypeMapper roomTypeMapper; @Override @Transactional(rollbackFor = Exception.class) public Order createOrder(OrderDTO dto, Integer userId) { // 1. 校验:房型存在且未下架 RoomType roomType = roomTypeMapper.findById(dto.getRoomTypeId()); if (roomType == null) { throw new BusinessException("房型不存在"); } // 2. 计算日期差得出总价 long days = (dto.getCheckOutDate().getTime() - dto.getCheckInDate().getTime()) / (24 * 60 * 60 * 1000); if (days <= 0) { throw new BusinessException("离店日期必须晚于入住日期"); } BigDecimal totalPrice = roomType.getPrice().multiply(BigDecimal.valueOf(days)); // 3. 插入订单 Order order = new Order(); order.setOrderNo(UUID.randomUUID().toString().replaceAll("-", "")); order.setUserId(userId); order.setRoomTypeId(dto.getRoomTypeId()); order.setCheckInDate(dto.getCheckInDate()); order.setCheckOutDate(dto.getCheckOutDate()); order.setTotalPrice(totalPrice); order.setStatus(0); orderMapper.insert(order); // 4. 锁定房型(可选:在 t_room_type 上维护一个锁定量) roomTypeMapper.increaseLockedCount(dto.getRoomTypeId(), days); return orderMapper.findById(order.getId()); } }@Transactional(rollbackFor = Exception.class)这行是重中之重。Spring 默认情况下只对 RuntimeException 回滚,而 checked Exception(比如 IOException)默认不回滚——这意味着如果你的 Service 方法里抛了一个业务异常,而这个异常不是 RuntimeException 的子类,那前面插进去的订单就永久留在数据库里了,这是经典的事务失效陷阱。所以我在团队里立过一个规矩:任何涉及写操作的事务注解,一律显式声明 rollbackFor = Exception.class,别依赖默认值。另一个事务边界容易踩的坑是自调用:同类里一个方法调另一个方法,比如 OrderService 里createOrder调了本类的内部方法,这个内部方法上的@Transactional是不生效的。因为 Spring 事务基于 AOP 代理,自调用走的是 this.method(),不是代理对象。你排查"为什么我的 @Transactional 没生效"时先问自己:这个方法是 Controller 直接调用的吗?还是同类内部调用的?
4.3 拦截器实现前台登录校验:HandlerInterceptor 的三步用法
前台用户下单前必须登录。SSM 里最常用的做法不是每个 Controller 方法都去 session 里判断一遍,而是写一个拦截器,统一拦截需要登录的 URL 前缀。这是 ssm 常用注解之外的另一个面试高频考点:Spring MVC 拦截器的执行时机和工作原理。
// LoginInterceptor.java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { // 未登录,重定向到前台登录页 response.sendRedirect(request.getContextPath() + "/front/login"); return false; } return true; } }拦截器写好后要在 spring-mvc.xml 里注册,并配置拦截和放行的 URL 规则。
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/front/order/**"/> <mvc:mapping path="/front/user/**"/> <mvc:exclude-mapping path="/front/user/login"/> <mvc:exclude-mapping path="/front/user/register"/> <mvc:interceptor:ref bean="loginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这里有一个实际项目中常犯的错:拦截路径写成/front/**,然后发现登录页面本身也访问不了了——你倒是先进登录页,但登录页也被拦截器拦了,于是陷入死循环。所以我列出的映射只拦截order和user两个子路径,把登录、注册、首页、民宿列表全部放行。另一个使用细节是response.sendRedirect里的地址要加request.getContextPath(),否则在带项目名的 URL 下(比如http://localhost:8080/homestay/)会跳转到端口根路径,404 没跑。这类问题常见到可以单独开一篇"Spring MVC 拦截器 10 坑",但对于这个民宿项目,记住一个原则就够:拦截范围宁小勿大,放行名单宁多勿缺。
5. SSM 民宿系统从搭建到部署的避坑指南:5 个高频故障
5.1 IDEA 里启动 Tomcat 直接报 ClassNotFoundException
现象:项目启动时在控制台抛ClassNotFoundException: org.springframework.web.context.ContextLoaderListener,或者NoClassDefFoundError: org/springframework/beans/factory/BeanFactory。 原因:SSM 项目是 WAR 包部署到 Tomcat 运行的,依赖的 jar 包必须打进WEB-INF/lib目录。你虽然把依赖加到了 IDEA 的 Project Structure 里,但没有选择 "Add as Library" 或者没把 artifact 里的 "Available Elements" 添加到部署包中。 解决:打开 Project Structure → Artifacts → 选中你的 Web 项目 → 右键点 "Put into Output Root",确保所有 Maven 依赖或手动添加的 jar 都在输出列表里。如果是 Maven 项目,更省事的做法是 pom.xml 里把打包方式调成 war,然后用 Tomcat 插件或本地 Tomcat 部署时选择 "war exploded"。排查顺序记一下:先看WEB-INF/lib里有没有 spring-web、spring-mvc、mybatis 的 jar,再看 classes 目录里有没有你编译出来的 Mapper 接口字节码。这个报错在机房调试时十次有八次是 jar 没进去。
5.2 mapper XML 里的 SQL 写对了但报 Invalid bound statement
现象:调用 Mapper 接口方法时抛org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.mapper.OrderMapper.findById。 原因:接口和 XML 的绑定失败,常见三种情况,XML 文件的 namespace 写成别的包、XML 文件名和接口名不一致、或者 XML 文件没在 target/classes 目录下(因为 IDE 没把 resources 标记为资源根目录)。还有一种很隐蔽的情况是 XML 里某个 SQL 标签的 id 和接口方法名拼错了没有报编译错误,运行到那一行才炸。 解决:首先检查applicationContext.xml里 MyBatis 的配置中 mapperLocations 是不是配了classpath:mapper/*.xml,路径拼错就是对的 XML 也能找不到。然后在 IDEA 里确认 resources 目录右键 → Mark Directory as → Resources Root。最后检查一个 XML 里每个 mapper 标签的 namespace 前缀,确认是com.example.mapper而不是com.example.entity。我见过有人把 namespace 写成实体类包名,一整个下午都在查为什么"查不到数据"——其实数据能查到才怪,接口实现根本没加载。
5.3 JSP 页面用 ${list} 取不到数据但 Java 代码里 list 明明有值
现象:后台 Controller 用model.addAttribute("homestayList", list),前端 JSP 里写${homestayList}页面渲染出来是空的,控制台没异常。检查 Java 代码发现 Service 返回的 list 里确实有 3 条数据。 原因:JSP 2.x 规范默认禁用 EL 表达式,因为你没有在 JSP 头部加<%@ page isELIgnored="false" %>。这种情况在 IDEA 新建的 JSP 模板里频繁出现,模板生成的页面默认带了这个属性,你自己手选的 JSP 可能没有。还有一个原因,依赖的 jstl 版本引入的是 EL 1.0,不兼容 2.5 及以上 Servlet 容器也会静默失效。 解决:JSP 页面顶部加上<%@ page isELIgnored="false" %>。如果是 Spring Boot 内嵌 Tomcat 跑了这个 SSM 项目还有一种奇特的坑——spring-boot 以 jar 方式启动时 JSP 不被支持,页面直接变成下载文件或报 404,这种情况要用 war 包部署到外置 Tomcat。记住一个准则:SSM + JSP 项目就该老老实实打 WAR 跑外置 Tomcat,别用 Spring Boot 的 jar 方式死磕。
5.4 数据库写入中文变问号,控制台看 SQL 没问题
现象:前台用户在前台页面的民宿评论里输入"这家民宿的风景很好",后台查询出来变成 "??????????"。检查数据库表结构,字段已经是 utf8mb4。 原因:三层连接链路上任何一层字符集不对都会出现这种问题。数据库表字符集是 utf8mb4 不代表连接字符集是 utf8mb4——MySQL 的 character_set_client、character_set_connection、character_set_result 三个参数独立影响读写。SSM 项目中你配置的 JDBC 连接串 characterEncoding 如果没写或者写错,就算表建对了也白搭。 解决:jdbc.properties 的连接串里显式加characterEncoding=utf8mb4,注意 MySQL 驱动 5.x 认characterEncoding=utf8和useUnicode=true,MySQL 驱动 8.x 才认 utf8mb4。配置如下:
jdbc.url=jdbc:mysql://localhost:3306/homestay_system?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai然后在 MySQL 客户端执行SHOW VARIABLES LIKE 'character%';检查连接字符集是否生效。如果这里是对的,还有一个容易遗漏的点:Tomcat 的server.xml里URIEncoding会在 GET 请求参数时起作用,POST 请求则不受它影响,需要检查表单页面的accept-charset="UTF-8"或让请求的 header 声明字符集。这个问题的排查顺序是:浏览器表单 → Tomcat 连接器 → JDBC 驱动 → 数据库连接 → 表结构字符集,链路长,但一条条排查下来总能定位。
5.5 查询正常但增删改不生效,不报错也不写库
现象:前台用户注册后跳转到了首页,说明注册请求走通了,但数据库 t_user 里没有新记录。日志没有异常,页面反馈正常。 原因:九成是事务没有提交或被吞掉。常见情况是 spring.xml 里配置了事务管理器但没有<tx:annotation-driven>注解支持,于是@Transactional注解完全没生效;另一种是 Service 方法里把数据库操作包在 try-catch 里,catch 到异常后自己处理掉没有 rethrow,事务管理器不知道发生错误,因为异常没传出去,默认不会回滚——但你这个场景更可能是根本没开启事务。 解决:检查 spring.xml 里DataSourceTransactionManager配置对不对,有没有和 SqlSessionFactory 用同一个数据源,<tx:annotation-driven transaction-manager="transactionManager"/>有没有写在 Spring 容器加载的配置文件里。再检查 Service 实现类是否配置了扫描包<context:component-scan base-package="com.example.service"/>——ServiceImpl 类上没有@Service注解或者扫描包没覆盖到,Spring 容器里就没有这个 Bean,注入到 Controller 的是一个空的代理对象,调用时数据操作被吞掉也是这个表现形式。排查这类问题时在 ServiceImpl 的插入方法里临时加一个System.out.println(rows),如果 rows=1 但库里没数据,铁定是事务的问题;如果 rows=0,先分析 SQL 为什么影响行数为 0。
6. 从能跑到能答辩:SSM 民宿系统的验证方法与加分细节
6.1 接口验证:不靠浏览器点,用一份测试清单
系统写完别急着截图给老师看,先自己拿 Postman 或 IDEA 的 HTTP Client 把所有 OpenAPI 过一遍。我通常列一张这样的清单逐项打勾:前台民宿列表接口返回 JSON 结构是否完整、民宿详情页浏览量是否自增、按日期查可用房型的返回结果和数据库手写 SQL 比对是否一致、提交订单后数据库订单表和锁定字段是否同步变化、重复提交订单是否导致超卖(用浏览器开两个窗口同时提交同一日期同一房型)。每一行打勾的过程中你会暴露掉至少一个边界问题,常见的比如退房日期等于入住日期的异常订单、价格计算里天数少算一天(当天入住当天退房或者跨天出 Bug)。这一遍直接用数据说话,比答辩现场被老师问住再临时看代码强太多。
6.2 代码整洁度自查:三个 IDEA 功能帮你在答辩前发现问题
用 IDEA 的 Analyze → Inspect Code 扫一遍,重点关注:未使用的 import、Controller 里直接出现了 SQL 拼接痕迹(说明有脏代码)、ServiceImpl 里有没有System.out.println之类残留代码。然后全局搜索@Autowired,确认所有注入点字段都是私有类型且没有在 Controller 里直接操作 Mapper——如果有,说明你绕过了 Service 层,答辩老师一问就会露馅。最后看一遍所有返回给前台的时间类型,如果是 Date 类型直接暴露,要么在前端用<fmt:formatDate>格式化,要么在实体类对应字段加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。这些不是功能问题,但答辩时老师看代码第一眼扫的就是规范,别在这种地方丢分。
6.3 最后的建议:把论文里的 E-R 图和技术实现对应起来
你手上这套 SSM 民宿系统,源码、数据库、论文三者是绑定的。答辩前花半小时做一件事:打开论文里的数据库设计章节,对照 t_user、t_homestay、t_order 的实际表结构,确认 E-R 图的字段和 MySQL 里的字段完全一致,很多人的论文是从网上抄的,表名字段和自己数据库差十万八千里,一被提问就圆不回来。再对照论文的"系统实现"章节,画出一次从前台请求到数据库响应的调用链,自己口头讲一遍:请求进来 DispatcherServlet 怎么分发、HandlerMapping 怎么找到 Controller、@RequestMapping里的 URL 和 JSP 里的 form action 是否完整对齐。这些都是看着简单但你很可能没验证过的细节。把这些问题处理完,这个项目才算真正闭环,你拿出去不管是答辩还是放简历,都有底气说一句这是我独立完成的 SSM 项目。希望这些从实战里磨出来的经验能帮到你,少走几步我当年走过的弯路。
本文还有配套的精品资源,点击获取