SSM四川工商学院学生宿舍管理系统这个标题我第一眼看过去就知道是标准的JavaWeb毕设方向。SSM框架——也就是Spring SpringMVC MyBatis——加上一套围绕宿舍管理业务展开的增删改查这个组合在毕业设计里属于安全牌技术不新但够用业务不复杂但覆盖完整从需求分析到数据库设计再到前后端联调能把你大学四年学的东西串起来展示一遍。如果你正好拿到这套源码准备复现或改进这篇内容就是按过来人踩过坑之后的口吻写的。我会把选题逻辑、数据库设计、SSM整合配置、核心业务模块实现、常见报错排查、答辩准备都拆开讲一遍。适合正在做毕设的计算机相关专业学生也适合想快速了解SSM管理类项目套路的人。文章不假设你有多少基础但如果你能把Spring的IOC和AOP、MyBatis的Mapper映射这些基本概念回忆起来读起来会顺畅很多。1. 项目定位与选题逻辑为什么这个题目能被老师接受1.1 宿舍管理的核心业务场景拆解宿舍管理在高校里是一项非常琐碎但又躲不开的日常工作。每年秋季开学几千名新生要按学院、班级、性别分配宿舍和床位毕业季又要批量办理退宿核对家具和电表平时宿管员要处理学生报修的水管、空调、门锁问题记录晚归情况还要定期检查宿舍卫生算各寝室的水电费发停水停电通知。这些业务如果全靠Excel表格和纸质登记信息散在几十个文件里查一条记录要翻半天更别提统计哪个楼栋还有空床位这个月X栋电费谁没交这类交叉查询了。所以学生宿舍管理系统这类题目本质上解决的是宿舍管理信息孤岛的问题。系统把学生、辅导员、宿管员、维修工、财务收费这些角色统一收到一个Web应用里角色各自登录在自己权限内操作数据落库账目清晰查询方便。这套题目的聪明之处在于它的角色边界非常清楚系统管理员管用户、管权限、管基础数据宿舍楼栋、宿舍类型、收费标准宿管员管入住、退宿、调宿、查寝、报修处理、卫生评分学生查看自己的宿舍信息、提交报修、查看公告、查水电费角色定下来页面结构和功能菜单基本就定了。系统管理员的页面偏向数据维护宿管员的页面偏向业务操作学生的页面偏向查询与提交。权限控制用一个拦截器加Session角色判断就能实现复杂度不高还能很好地展示你对权限设计这个考点的理解。从业务角度看这个题目的范围控制也很合适。它没有电商那种复杂的订单状态机也没有社交产品那种高并发的实时系统但它包含了一个管理系统的全部标准动作增删改查、登录校验、分页搜索、状态流转、简单统计报表。工作量不大不小三个人合作可以做一个人紧一紧时间也能做完。这正是毕业设计最理想的工作量区间——既能展示你的完整开发能力又不会因为太大而烂尾。1.2 为什么选SSM而不是SpringBoot或JSPServlet先说一个常见误区很多人觉得SpringBoot比SSM高级那毕设是不是更应该用SpringBoot这个问题得分场景看。如果你是在做个人作品、实习项目那SpringBoot Vue MySQL是更接近工业界的组合。但如果你在写毕业设计大部分指导老师默认的基本功检验标准就是SSM。原因不复杂SSM是SpringBoot出现之前JavaWeb开发的绝对主流老师在脑海里对这套技术栈的代码结构、分层方式、配置写法都门儿清审论文、看答辩的时候能快速判断出你是不是真的把代码看懂了。更重要的是SSM这套组合对理解原理的要求比SpringBoot更高。SpringBoot最狠的地方是自动配置写一个起步依赖Tomcat内嵌了数据源自动装配了整合SSM的几十行配置全被隐藏在底层。你做完了可能觉得自己啥也没干缺少调试和配置的体验。而SSM从web.xml到springmvc.xml再到applicationContext.xml每一行配置都在明面上你能指着配置文件跟老师说这一行是映射Mapper路径的这一行是配置事务管理器的这种我能讲清楚原理的感觉在答辩时是碾压性的优势。三个框架的分工也简单清晰Spring是核心容器管理所有Bean的创建和依赖注入像后勤大管家SpringMVC负责接收前端请求、分发到对应Controller、再返回页面或JSON像前台接待员MyBatis负责和数据库打交道把SQL写在Mapper.xml里把查询结果自动映射成Java对象像数据库翻译官。生活化一点整个过程就是你在浏览器里点了一个查询床位按钮SpringMVC把这张请求单子送到ControllerController叫上Service层的业务员业务员不管SQL怎么写他只管调用MyBatis这个翻译官去数据库里取数取回来的数据再一层层返回给页面渲染展示。这套流水线你只要能完整走一遍SSM就算打穿了。如果你非要用SpringBoot做我也理解毕竟现在很多学校的新课程已经切到SpringBoot了。但我个人的建议是除非指导老师明确要求或者你已经在SSM上卡到完全跑不动否则别换。你拿到的是SSM源码基于这个改业务比把整个技术栈推翻重做要省时间得多。2. 数据库设计这个环节值得你花最多时间2.1 实体关系与角色边界我见过很多做毕设的人上来就急着写Controller、写页面结果写到后面发现表结构缺字段、缺关联又回头改数据库改好事了ResponseBody返回的数据结构又变连带前端一起改整个项目越改越乱写代码和需求越来越对不上。数据库设计是这类管理系统的地基地基歪了上面的代码全是裂缝。先从实体看。一个标准的SSM宿舍管理系统至少需要这些核心实体用户系统登录账号的角色表包含管理员、宿管员、学生三种角色学生学生的基本信息包括学号、姓名、性别、学院、班级、手机号、宿舍关联宿舍楼一栋物理楼比如1号楼女生公寓A栋宿舍房间挂在宿舍楼下的具体房间有房间号、可住人数、已住人数床位房间内的具体床位有床位号、状态空/已占入住记录学生入住和退宿的流水报修单学生提交的报修工单有报修类型、描述、状态卫生检查宿管员对房间的评分记录水电费房间的水电用量和缴费记录实体之间的关系也很直观宿舍楼一对多房间房间一对多床位学生入住到床位报修单和卫生检查都挂在房间下面。这些关系用外键可以做但我建议用逻辑关联——也就是通过字段把两个表连起来而不是数据库真实外键约束。原因有两个。第一真实外键在学生批量导入、逻辑删除回收、扩展历史数据时会产生一堆麻烦比如你删一个宿舍外键约束会拦住你你还得先处理下面的床位和学生。第二MyBatis本来就是写SQL的框架多表关联用SQL的JOIN或子查询就能搞定没必要让数据库层面维护关系。从开发角度来看学生表和用户表我建议分成两张用户表管登录学生表管档案。为什么因为用户表存的账号密码是访问凭证学生表存的是静态档案两者虽然一对一关联但字段意义完全不同。当然也有很多课设项目把学生信息直接塞到用户表里一张表搞定登录档案。这样更简单但也更糙。分表的好处是以后如果系统要扩展辅导员角色、家长角色不需要动学生表结构只要在用户表加角色就行。这种扩展意识是答辩时的加分项。2.2 核心表结构与建表SQL直接给几个核心表的建表SQL这是我从实际版本里整理出来的字段命名尽量统一类型选择也比较克制你本地跑MySQL5.7或8.0都能直接执行。用户表CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 密码MD5, role TINYINT NOT NULL DEFAULT 2 COMMENT 0-管理员1-宿管员2-学生, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, status TINYINT NOT NULL DEFAULT 1 COMMENT 0-禁用1-启用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;学生档案表CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, user_id INT NOT NULL COMMENT 关联user表的id, stu_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0-男1-女, college VARCHAR(100) DEFAULT NULL COMMENT 学院, class_name VARCHAR(100) DEFAULT NULL COMMENT 班级, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, room_id INT DEFAULT NULL COMMENT 当前所在房间id, bed_id INT DEFAULT NULL COMMENT 当前床位id, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-未入住1-已入住2-已退宿, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stu_no (stu_no), KEY idx_room_id (room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生档案表;宿舍房间表与床位表CREATE TABLE room ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, building_no VARCHAR(20) NOT NULL COMMENT 楼栋号如1号楼, room_no VARCHAR(20) NOT NULL COMMENT 房间号如501, capacity INT NOT NULL DEFAULT 4 COMMENT 可住人数, gender_limit TINYINT NOT NULL DEFAULT 0 COMMENT 0-男生宿舍1-女生宿舍, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-空闲1-部分住满2-已满, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_building_room (building_no, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宿舍房间表; CREATE TABLE bed ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, room_id INT NOT NULL COMMENT 房间id, bed_no VARCHAR(10) NOT NULL COMMENT 床位号如1号床, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-空床1-已占用, PRIMARY KEY (id), UNIQUE KEY uk_room_bed (room_id, bed_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT床位表;报修单表CREATE TABLE repair ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, student_id INT NOT NULL COMMENT 报修学生id, room_id INT NOT NULL COMMENT 房间id, type VARCHAR(30) NOT NULL COMMENT 报修类型水/电/门锁/空调等, description VARCHAR(500) DEFAULT NULL COMMENT 问题描述, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待处理1-维修中2-已完成, handler_name VARCHAR(50) DEFAULT NULL COMMENT 维修人员, report_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY idx_room_id (room_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修单表;这些表设计里有两个小细节值得注意。男生女生的房间可能各有不同我在room表里放了一个性别限制字段gender_limit这样它就不会让你女生分配进男寝也不会让男生分进女寝。很多课设做分配宿舍时都是写死逻辑结果一到扩展就翻车。另一个细节是student表里我保留了room_id和bed_id同时在bed表里也维护了床位状态。为什么要冗余这两个字段因为学生列表页和宿舍详情页都需要高频读取这个学生住哪的信息直接查学生表比每次JOIN三张表快得多。代价是入住退宿时必须在一个事务里同时更新多个表这个后面细讲。2.3 三个容易被忽视的数据库设计细节第一宿舍和床位为什么要拆成两张表有人会问直接在room表里放bed1、bed2、bed3、bed4四列不就行了这样做也确实能跑但你要做多少张床已占用的统计时就得去数哪个列有值要么就把SQL写得特别绕。而且如果有5人间、6人间你还要去alter table加列。拆成床表之后一个房间对应多少条bed记录就是capacity决定的统计用count(*)就行新增房型也只是往bed表里多插几条数据逻辑顺很多。第二逻辑删除还是物理删除我建议所有业务表都别做物理删除。比如学生被误退宿或者床位分配错了如果直接delete后面查历史入住记录就断了改数据也改不回来。比较成熟的做法是给像student这样的表加一个status状态位用0-未入住、1-已入住、2-已退宿这种状态来兜底要删除一条记录就把它置成某个失效状态保留历史痕迹。这个设计在答辩时可以主动说老师会知道你考虑过数据完整性的问题。第三时间字段用默认值。上表里create_time我用的是DEFAULT CURRENT_TIMESTAMPupdate_time用了ON UPDATE CURRENT_TIMESTAMP这句话什么意思就是插入时不用手动set时间数据库自动写更新时如果这一行有改动时间自动刷新。省了Java代码里一堆new Date()赋值逻辑避免日志时间比实际时间早8小时的时区乌龙。索引方面不要贪多。用户名、学号、楼栋房间唯一键已经有Unique索引了查询报修单和房间时最多再加一个普通索引比如报修表的status和room_id。毕设数据量就几万条加太多索引反而拖慢写入速度也不方便你讲解索引原理。3. 核心功能模块实现业务闭环是怎么转起来的3.1 登录与权限控制一个拦截器搞定先看登录。用户账号只有一个user表通过role字段区分管理员、宿管员、学生。前端表单提交username和password到ControllerController调ServiceService查user表比对密码成功就把user对象塞进Session。登录ControllerController public class LoginController { Autowired private UserService userService; PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.findByUsername(username); // 实际项目密码要做MD5或BCrypt加密这里先比较加密后的值 if (user ! null user.getPassword().equals(MD5Util.encrypt(password))) { session.setAttribute(loginUser, user); // 按角色分流到不同主页 if (user.getRole() 0) { return redirect:/admin/index; } else if (user.getRole() 1) { return redirect:/dorm/index; } else { return redirect:/student/index; } } model.addAttribute(msg, 用户名或密码错误); return login; } }你可能会问怎么判断某个Controller只能管理员访问最标准的做法是写一个HandlerInterceptor在SpringMVC的拦截器里检查Session中的角色。登录拦截器public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login); return false; } // 细粒度的角色控制限定某些URL只允许特定角色 String uri request.getRequestURI(); if (uri.contains(/admin/) loginUser.getRole() ! 0) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } if (uri.contains(/student/) loginUser.getRole() ! 2) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } }然后到springmvc.xml里注册这个拦截器并把它排除掉登录页面和静态资源mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/css/**/ mvc:exclude-mapping path/js/**/ mvc:exclude-mapping path/images/**/ bean classcom.example.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这里一定要把静态资源排除掉不然拦截器会把.css、.js文件也拦下来页面样式全没浏览器控制台一堆403你会一头雾水。为什么不直接用SpringSecurity或Shiro我的看法是毕设场景真没这个必要。SpringSecurity配置起来要写UserDetailsService、BCrypt加密、过滤链光是理解它内部的过滤器顺序就够消耗好几天了。而用拦截器Session做权限控制在中小型管理系统里已经完全够用而且核心逻辑一目了然课堂上讲、答辩时候讲都说得清楚。你只要在开场时说一句我这里用拦截器实现了轻量级权限控制老师就知道你有安全意识。3.2 分配宿舍多表事务的一次经典演练分配宿舍是这套系统里最考验代码质量的功能没有之一。它牵着头绪多处理不好就会出现数据不一致。先看业务规则。学生申请入住下来之后系统要做的判断包括学生的性别和房间的gender_limit必须一致房间当前已住人数不能达到最大容量capacity目标床位状态必须是0空同一个学生不能重复入住。满足条件后需要同时做三件事把床位的status置为1把student表的room_id和bed_id填上并置为已入住状态再向入住记录表插一条流水。这三件事必须在一个事务里完成——要么全部成功要么全部失败。如果学生表已经更新了床位状态却没改这个学生就会显示已入住但床位还在空状态下一个学生分房时又会被分配进来一个床位住进两个人数据就乱了。分配宿舍的核心Service逻辑Service public class DormitoryServiceImpl implements DormitoryService { Autowired private RoomMapper roomMapper; Autowired private BedMapper bedMapper; Autowired private StudentMapper studentMapper; Autowired private CheckInRecordMapper checkInRecordMapper; Override Transactional(rollbackFor Exception.class) public boolean assignRoom(Integer studentId, Integer bedId) { Bed bed bedMapper.selectById(bedId); if (bed null || bed.getStatus() ! 0) { return false; // 床位不存在或已被占用 } Room room roomMapper.selectById(bed.getRoomId()); Student student studentMapper.selectById(studentId); // 业务校验性别匹配 房间未满 学生未入住 if (room.getGenderLimit() ! student.getGender()) { throw new BusinessException(性别与宿舍类型不匹配); } if (room.getCapacity() room.getOccupiedCount()) { throw new BusinessException(房间已满请选择其他床位); } if (student.getStatus() ! 0) { throw new BusinessException(该学生已入住或已退宿不能重复分配); } // 更新床位 bed.setStatus(1); bedMapper.updateById(bed); // 更新房间已住人数 room.setOccupiedCount(room.getOccupiedCount() 1); roomMapper.updateById(room); // 更新学生住宿信息 student.setRoomId(room.getId()); student.setBedId(bedId); student.setStatus(1); studentMapper.updateById(student); // 写入入住流水 CheckInRecord record new CheckInRecord(); record.setStudentId(studentId); record.setRoomId(room.getId()); record.setBedId(bedId); record.setCheckInTime(new Date()); checkInRecordMapper.insert(record); return true; } }这段代码值得你在答辩前完整吃透。它把事务管理业务校验多表更新三个知识点一次性集齐了。老师问你这个并发情况下会不会出现问题你可以回答如果两个管理员同时给一个学生分配床位因为有事务和床位状态判断后面的那个事务会发现床位已经是1而失败回滚不会出现一个学生占两张床。另外注意事务放在ServiceImpl上Controller和Mapper层不要重复加。如果事务方法内部try了异常还不往外抛事务是不会回滚的——这个坑非常隐身答不出来很冤枉。3.3 报修、水电、卫生检查让系统有运营感报修模块看起来只是单表的CRUD但如果只做成增删改查页面和代码会显得没深度。我建议把报修单做成一圈完整的状态流转学生提交状态0待处理→宿管员派单给维修工状态1维修中记录维修人→维修完成后回填处理结果状态2已完成记录完成时间。对应的列表页学生端只显示自己提交的报修单宿管员端显示所有状态为待处理和维修中的单子并在列表上加一个处理按钮弹窗里填维修人员和处理说明。这一步不需要复杂的权限框架但你已经把系统从一个数据录入工具提升成了业务流程平台。答辩时你可以指着这个模块说这里我根据报修单状态字段做了视图区分不同角色的待办不一样。卫生检查和报修类似宿管员选择房间号打分比如90分填备注列表页顶部做一个简单的统计本周平均分、优秀宿舍top10。用一条GROUP BY语句就能做出来但展示效果非常好。水电费模块稍微复杂一点因为你得考虑月账单。简单做法是给房间表加两个月用数字段本月用水量、本月用电量宿管员每月录入一次系统按固定单价算出金额学生端查询自己的房间账单。如果想做成更完整一点就单独建一张bill表字段包括room_id、月份、水量、电量、水费、电费、状态未缴/已缴这样还能顺便做一个本月欠费名单给宿管员看。搜索和分页是所有管理页面的标配。我的建议是用PageHelper插件一行startPage就搞定物理分页不要自己去手写limit和count。手写分页容易把总记录数和页码算错PageHelper经过大量验证稳定得很。配置也很简单引入pagehelper依赖后直接在MyBatis配置里加一个拦截器即可。4. SSM配置与MyBatis实战4.1 三份配置文件的正确姿势拿到一套SSM源码最怕的就是改来改去启动报错。很多人一看配置文件就慌其实SSM整合就三份配置文件web.xml、springmvc.xml、applicationContext.xml。你能把这三份文件每一行看明白整个项目的地基就算夯实了。先看Spring核心配置applicationContext.xml。它的工作范围是不含Controller的Bean常见的有数据源、SqlSessionFactory、Mapper扫描、事务管理器context:component-scan base-packagecom.example context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/dormitory?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/这里有两个坑必须提前说。第一个是数据库URL里的编码参数。useUnicodetrue和characterEncodingutf8必须成对出现而且这里建议用utf8mb4来保证能存emoji和生僻字。serverTimezone这个参数针对MySQL8.0如果你用的是MySQL8.0驱动的com.mysql.cj.jdbc.Driver不写serverTimezone就会报Server returns invalid timezone的错时区直接写Asia/Shanghai最稳。第二个坑是component-scan扫描范围。Spring配置里把Controller排除掉Controller统一交给springmvc.xml里的另一个component-scan扫描。如果两边重复扫描SpringMVC初始化时会出现Error creating bean with name xxxController因为同一个Controller被两个容器各注册了一遍。再看springmvc.xml核心是注解驱动、视图解析器、静态资源放行mvc:annotation-driven/ context:component-scan base-packagecom.example.controller/ !-- 静态资源不走DispatcherServlet -- mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /beanviewResolver的前缀后缀你最好按自己的项目目录来改比如有人的页面放在webapp/pages下面那就改成/pages/。改错的话Controllerreturnadmin/index时会去查一个不存在的/WEB-INF/jsp/admin/index.jsp直接给你404。至于web.xml你要做的就是配置DispatcherServlet的核心映射和Spring的监听器servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:springmvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listenerurl-pattern尽量用/不要用/*。用/能走SpringMVC的Unified路径匹配静态资源也能交给default-servlet-handler用/*会把.jsp后缀也拦截掉导致视图渲染时连JSP都找不到。很多同学第一次用IDEA配Tomcat部署SSM项目打开部署配置发现没有exploded类型需要手动添加Artifacts然后把application context的路径改成/不然启动之后浏览器要访问localhost:8080/项目名/xxx才能进而你的代码里全是ctx/login上下文不对就会404。这一类环境问题放到后面一起讲。4.2 MyBatis动态SQL与多表查询MyBatis的灵魂在于Mapper.xml里写SQL。宿舍管理系统的常用查询里学生列表是最典型的可能要按姓名模糊查、按学号精确查、按学院下拉筛选、按入住状态筛选而且条件还不一定全填。这时候适合用动态SQL用一个where标签把可选条件都拼上。StudentMapper.xml中的代表查询select idselectStudentList resultTypecom.example.vo.StudentVO SELECT s.id, s.stu_no, s.name, s.gender, s.college, s.class_name, s.phone, r.building_no, r.room_no, b.bed_no, s.status FROM student s LEFT JOIN room r ON s.room_id r.id LEFT JOIN bed b ON s.bed_id b.id where if testname ! null and name ! AND s.name LIKE CONCAT(%, #{name}, %) /if if testgender ! null AND s.gender #{gender} /if if testroomId ! null AND s.room_id #{roomId} /if if teststatus ! null AND s.status #{status} /if /where ORDER BY s.update_time DESC /select这里我的习惯是left join而不是inner join。区别在哪left join即使房间或床位为空也能把学生查出来只是房间号是null对于学生列表这种加载页面来说更友好。如果改成inner join没住宿的学生就不会出现在列表里宿管员想找他改信息都找不到人。多表查询不要返回Mapper的实体对象建议新建一个VO类比如StudentVO专门放列表展示需要的字段。为什么Student表本身已经有一堆字段了你再加roomId、bedId、buildingNo如果还要带出账单信息、报修次数杂七杂八全堆在实体里实体就变成一个垃圾桶Mapper返回的数据和你页面需要的数据也不是一一对应非常混乱。VO的模式虽然多写一个类但每个类干一件事代码结构极其清楚。再讲一下分页。如果你用PageHelper// Service层 PageHelper.startPage(pageNum, pageSize); PageStudentVO page studentMapper.selectStudentList(query); PageInfoStudentVO pageInfo new PageInfo(page);上面的startPage必须紧接着下一行后就是Mapper查询。中间如果又执行了别的SQLPageHelper会把那个SQL当成分页对象分页就会乱掉这是它的一个已知问题也是很多人搞不懂自己分页为什么串数据的原因。5. 踩坑实录与排错方法论5.1 环境与启动类问题我从自己复现这套系统和其他SSM项目的过程里把高频报错整理成了一个速查表照图排查会快很多。现象原因解决方案提示ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet项目缺少Spring相关jar包或者jar没复制到Tomcat的lib目录检查Maven依赖构建时确认war包lib下是否存在spring-webmvc提示NoSuchMethodError或NoClassDefFoundErrorjar包版本冲突比如多个Spring版本混用用mvn dependency:tree检查依赖树统一Spring版本数据库连接失败Server returns invalid timezoneMySQL8.0驱动要求指定时区URL末尾加serverTimezoneAsia/Shanghai连接失败Access denied for user rootlocalhost数据库账号密码不对或MySQL授权问题用Navicat手动测一遍连接修改dataSource配置启动后访问页面404Tomcat控制台没有报错请求路径与Controller映射不匹配或项目context path不对用F12看请求URL在Controller方法加日志输出页面样式全丢控制台一堆403拦截器把静态资源拦了在springmvc.xml拦截器配置里exclude掉/css /js /images路径页面能出文本但中文全是乱码编码不统一JSP页面设置pageEncodingutf-8数据库URL加characterEncodingutf8Tomcat连接URIEncoding也设为utf-8启动时报Bean创建异常说Controller被重复注册Spring和SpringMVC扫描范围重叠Spring配置排除Controller只让springmvc.xml扫描Controller这里面最容易忽略的是乱码问题。你已经配置了数据库URL的characterEncoding也写了JSP的pageEncoding但如果Tomcat接收POST请求时默认使用ISO-8859-1表单提交的中文还是会乱。你需要在Tomcat的server.xml里给Connector加一行Connector URIEncodingUTF-8 .../或者在web.xml里配置CharacterEncodingFilter两种方式都行。我习惯两种都上双保险。5.2 业务实现中的隐蔽问题第一类是事务失效。同事做项目时遇到过在Controller里调了一个带Transactional的Service方法但Service方法是被另一个Service对象通过this.调用调出来的事务根本没生效。因为Spring的AOP是基于代理的this.调用不会经过代理对象。解决方案是让方法由外部Bean调用或者注入自己的代理对象。第二类是MyBatis参数绑定问题。Mapper接口方法如果只有一个参数这个参数用#{}随意取名都行但如果有多个参数一定要用Param注解。比如ListRoom selectRooms(Param(buildingNo) String buildingNo, Param(genderLimit) Integer genderLimit);否则MyBatis会报Parameter buildingNo not found你检查半天SQL也发现不了问题。第三类是懒加载报no session。如果你在Service里查了房间返回的Student对象里roomId为空页面端再去用student.getRoom()会报LazyInitializationException。解决很简单要么关掉延迟加载要么在Service层就把关联数据查询完整不要等到页面层才触发。第四类是日期时区问题。如果你的服务器时区和数据库时区不一致插入的报修时间可能和当前时间差好几个小时。在数据库连接串上加了serverTimezone后插入前最好再确认一下数据库的time_zone设置。5.3 排错方法论说几条我自己的排错经验这条价值我认为是最高的。遇到任何错误先看控制台的第一个报错不要看后面的Caused by链。你只需要看最上面那个异常类和消息去判断是项目结构问题、数据库连接问题还是代码逻辑问题。如果是404类错误打开浏览器F12看Network面板里那条红标的请求。它请求的URL和多路径的URL一对照问题就清楚了。很多人不看Network盯着controller代码看半天纯粹浪费时间。如果是数据库SQL问题把MyBatis日志打开。在springmvc或配置里设置setting namelogImpl valueSTDOUT_LOGGING/然后控制台会打印完整SQL和参数你复制那条SQL到Navicat里手动跑一遍看数据到底返回什么。能直接在数据库客户端跑通往往就已经能定位SQL的问题了。有时候很奇怪同一段代码在别人的机器上能跑到你这就报一堆错。这时候不要急着改代码先把JDK、Maven、Tomcat、MySQL的版本对齐。SSM项目最舒服的组合是JDK1.8 Maven3.6 Tomcat8.5/9 MySQL5.7或8.0。你要是搞了个JDK17去跑SSM会遇到一堆模块权限报错那时候不是代码问题是版本兼容问题。JDK11到17已经不支持Tomcat8.5这类老容器的某些反射操作了没必要给自己加难度。6. 答辩准备与后续拓展6.1 答辩高频问题与应答思路答辩时老师大概率会根据你的项目问下面这些问题提前准备好比临时组织语言强太多。老师常问的问题建议的应答思路为什么选这个题目从需求出发宿舍管理存在数据分散、查询难、报修无跟踪等痛点这个系统能提升管理效率SSM是什么三个框架各负责什么Spring管对象和事务SpringMVC管请求分发MyBatis管数据库操作用生活化类比讲清楚你的系统有哪些角色权限怎么做的三种角色Session存储登录用户拦截器拦截URL并判断角色说一个你遇到的最难解决的问题讲分配宿舍的事务一致性以及如何用Transactional加上状态校验解决并发下的数据错乱为什么用MyBatis不用JPA说明MyBatis对复杂SQL控制更灵活适合报表类查询和动态条件组合系统有哪些不足主动说目前没做密码找回、前端页面较简朴、并发能力有限但已经预留了角色扩展字段项目如何分层Controller-Service-Mapper三层Controller不写业务Service管事务和校验Mapper管SQL数据库哪些表为什么这么设计举例宿舍和床位拆表的理由学生表放状态位的理由最后一个系统有哪些不足的问题特别重要。别嘴硬说什么我的系统很完美也别说到处都是问题。比较好的答法是当前系统采用JSP服务端渲染交互体验相对一般密码采用的MD5加盐方式但后续可以升级为BCrypt宿舍分配目前是管理员代分配后续可以扩展为学生在线选宿。这个回答既实事求是又暗示你在思考改进方向。答辩演示的时候还有一个小技巧不要把整个项目从零启动一遍再慢慢演示。提前把服务起来把测试数据准备好浏览器打开登录页。上去之后不要先讲代码先按照管理员建楼栋→宿管员分配宿舍→学生查看信息并报修→宿管员处理报修→生成统计报表这条业务线完整走一遍。老师看到了一条完整的数据流第一印象比看代码强十倍。6.2 从毕设到作品集可以扩展的方向如果你时间充裕或者想把这个系统写进简历有几个低成本的升级方向。第一个是前后端分离。把JSP页面换成Vue或React后端提供Restful接口用JSON交互。这需要你把Controller改成RestController加上跨域配置再写几个前端页面。工作量中等但简历里可以写熟悉前后端分离开发模式比单纯SSM加JSP的含金量高不少。第二个是导入导出。很多管理类系统都有导入Excel批量初始化学生信息的需求。用EasyExcel读取学生名单批量插入数据库导出也很加分比如把某栋楼的住宿名单导成Excel给辅导员。这个功能展示面很大几乎必问。第三个是通知推送。当学生提交报修被受理时页面右上角弹一个站内通知。用WebSocket实现不算复杂但效果出彩能把你从一个只会写CRUD的人里区分出来。第四个是缓存优化。把楼栋、房间、收费标准的字典数据放到Redis里减少数据库压力。如果简历上敢写Redis这个项目刚好是你练手的试验田。不过要提醒你这些扩展都做项目工期会明显拉长期末答辩前心里没底。我建议大哥优先保住核心业务闭环完整跑通然后挑一个扩展做到能演示、能讲的程度就收手。毕竟毕设的底线是能运行、能讲清、能通过上限才是惊艳全场。我见过一个很惨的案例有人把系统扩展成前后端分离结果前端个别页面404、跨域没配好演示到一半白屏了老师对项目的评价立刻降档。稳定优先花活其次这在你做技术方案的时候是铁律。另外如果你要把这套系统放到自己的代码仓库当作品集记得做两件事。第一写一个清晰的README把技术栈、功能模块、数据库初始化脚本、启动步骤都写清楚很多简历筛选中面试官就是通过README判断你的工程素养的。第二把项目中可能涉及学校隐私的测试数据全部替换成虚构数据账号密码也改成admin/123456这类演示账号避免不必要的麻烦。最后说一点我个人对毕设源码的看法。很多同学拿到源码第一反应是赶紧跑起来看看效果这没错但跑起来只是起点。你至少要花一个晚上把Controller到Service再到Mapper的调用链读一遍把配置文件里每个标签的作用查一遍再自己手动改一个功能点比如把宿舍分配从管理员指定改成学生自由选宿这个改动不大但上线之后你对项目的理解会完全不同。做毕设说到底不是给老师交差是给自己毕业前最后一次完整的项目训练。这套学生宿舍管理系统技术不新但五脏俱全你用它能学到的东西一点不比一个大厂项目少。