很多同学毕设选“宿舍管理系统”,源码下载下来却发现不知道从哪讲起、哪段代码能突出亮点、被老师一问就卡壳。这个题目看着简单,真正动手才发现坑不少。我前后带过几届学弟学妹做过这套系统,也帮人远程调过不少跑不起来的 SSM 和 Django 代码,这篇就把从需求分析到最终答辩的全流程经验一次说清楚。无论你手里是一份独立开发的源码,还是网上找的“源码+LW+调试文档”整套资料,这篇都能帮你把它变成真正能讲明白、能改能扩展的项目。
1. 这个系统到底解决什么问题:宿舍管理的业务痛点和需求边界
1.1 宿舍管理难在哪里
宿舍管理的核心不是“登记宿舍号”,而是大量日常事务的流转。以前靠宿管手写台账的时候,最头疼的是三件事:查某个学生住哪间房要翻半小时纸质表;学生报修了不知道师傅上门没有;月底核算水电费全靠Excel人工填。换成管理系统之后,本质是要把这些离散的事务变成有状态、有记录、可追溯的流程。
所以做需求分析时不要只盯着“学生表”“宿舍表”两个对象,至少要把下面几条业务流程理清楚:
- 入住流程:新生报到或转宿时,先查空床位,再分配宿舍,同时生成入住记录。
- 退宿流程:清空床位、结算水电、更新宿舍已住人数。
- 报修流程:学生提交报修单,宿管审核派单,维修工接单回填结果,学生确认。
- 水电费流程:定期抄表,生成账单,学生缴费,记录缴费状态。
- 访客流程:访客登记,离校销记,保障楼栋安全。
这些流程看起来简单,但它们之间是有状态依赖的。比如退宿时发现水电没结清,就不能释放床位;报修单处于“派单中”时,学生不能重复提交。系统真正的价值,就是把这种状态约束落到代码和数据库里。
1.2 SSM和Django怎么分工
很多人在一个系统里同时看到 SSM 和 Django 会困惑:这不是两套技术栈吗?其实这正是这个项目的特色,也是它的加分点。合理的设计是让两套框架各自负责擅长的部分:
- SSM(Spring + SpringMVC + MyBatis)负责管理端和核心业务,也就是管理员、宿管用的后台管理平台,包含宿舍分配、学生管理、报修审批、数据统计这些重业务逻辑的功能。
- Django 负责学生端和对外接口,比如学生微信端或移动端的 API 服务,包含登录、查宿舍、提交报修、查账单这些轻量级高频操作。
这样分工的动机很简单:后端管理系统强调事务控制、权限隔离和复杂查询,这是 Spring 生态的老本行;学生端接口更看重快速开发、清晰的数据序列化和灵活的身份认证,Django REST Framework 在这点上比 SSM 写 Controller 再手工转 JSON 快得多。
我在实际带项目时,还遇到过一种情况:有些同学拿到的标题里含“SSM+Django”,但下载的源码其实只有 SSM 一套。这种也不用慌,可以保留 SSM 作为唯一后端,把 Django 的角色改成一个数据可视化模块或爬虫,只要在文档里讲清楚“为什么额外引入 Django”,就成了设计亮点,而不是结构冗余。
2. 技术架构设计:双技术栈如何整合到一个系统里
2.1 总体架构与分层
整个系统最常见的分层是:
- 表现层:管理端是 JSP 或 Thymeleaf 页面,学生端是微信小程序或 H5,通过 HTTP 调用后端接口。
- 控制层:SSM 的 SpringMVC Controller,Django 的 View / DRF 的 APIView。
- 业务层:SSM 用 Service 接口 + 实现类,Django 用 service 层或者直接在 model 里写业务方法。
- 持久层:MyBatis 的 Mapper 接口 + XML 映射;Django 的 ORM Model。
- 数据库:两边共用同一个 MySQL 实例,按业务前缀区分表。
设计时一定要明确:数据库是两套框架的唯一交集,两边都只操作自己的表,不要互相侵入对方的模块。比如学生端只通过 Django 写 student_profile、repair_order 这些表,管理端只通过 SSM 写 dormitory、user、check_record 这些表。如果某张表两边都要操作,就约定一方只读、一方只写,否则很容易出现事务边界混乱。
2.2 两边如何对接:接口、数据库与登录状态
双框架整合最常踩的坑是登录状态不互通。SSM 管理端习惯用 HttpSession 保存登录状态,Django 端如果也用 session,两边的 session id 互相不认。我的做法是:
- SSM 管理端继续用 Session + 拦截器校验,保持管理后台的传统实现方式,课上也好解释“SpringMVC 拦截器如何实现登录控制”。
- Django 学生端改用 JWT(JSON Web Token),登录接口签发 token,之后每次请求在 Authorization 头里带上 token,由 Django 端的中间件统一校验。
这样两套框架的登录态完全隔离,互不干扰,职责清晰。数据库层面呢,两边的用户信息如果都存 user 表会冲突,我会拆成 sys_user(系统用户,管理员和宿管)和 student(学生),学生表里的账号密码字段由 Django 端写入和校验,SSM 端需要展示学生信息时只读联查。
2.3 环境与工程结构
如果你打算自己从头搭建,参考下面的工程结构:
dormitory-system/ ├── dorm-admin/ # SSM 管理端 │ ├── src/main/java │ │ ├── com.dorm.controller │ │ ├── com.dorm.service │ │ ├── com.dorm.mapper │ │ └── com.dorm.entity │ ├── src/main/resources │ │ ├── spring-context.xml │ │ ├── spring-mvc.xml │ │ ├── spring-mybatis.xml │ │ └── mapper/*.xml │ └── src/main/webapp/WEB-INF ├── dorm-student/ # Django 学生端 │ ├── manage.py │ ├── dorm_uauth/ # 认证 app │ ├── repair/ # 报修 app │ ├── billing/ # 账单 app │ └── utils/ └── docs/ # 论文 + 调试文档Maven 管理 SSM 依赖时,重点关注 spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、druid 连接池,Java 版本建议 8 或 11,太高会出现 Tomcat 版本兼容问题。Django 端我用 3.2 LTS,配合djangorestframework、pyjwt、drf-yasg(接口文档)这几个包,足够覆盖整个学生端。
3. 数据库设计与核心表结构:宿舍分配的状态机是关键
3.1 核心表设计
宿舍管理系统的表不需要太多,但每张表都承担明确职责。我常用的核心表如下:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| dorm_building | building_id, name, manager_name | 楼栋信息 |
| dorm_room | room_id, building_id, room_no, bed_count, used_beds, status | 房间信息,status 标记状态 |
| student | student_no, name, gender, college, class_name, phone | 学生基本信息,不含宿舍外键 |
| check_in_record | record_id, student_no, room_id, bed_no, check_in_time, status | 入住/退宿流水 |
| repair_order | order_id, student_no, room_id, type, description, status, handler | 报修工单 |
| bill | bill_id, student_no, room_id, bill_type, amount, period, status | 水电等账单 |
| sys_user | user_id, username, password, role | 管理员/宿管账号 |
| notice | notice_id, title, content, publish_time | 公告信息 |
一个常见的设计误区是把 dorm_room 里加一个 student_no 字段直接表示“谁住这里”。这在一人一房的情境下勉强能用,但在宿舍场景里一间房可能住 2 人、4 人、6 人,必须通过 check_in_record 这张流水表来维护“房间和学生的当前关系”。我在带项目时反复强调:有人住的字段,永远用状态字段或关联表,不要用“迁移后残留”的单个字段硬撑。
3.2 宿舍分配状态机的实现
宿舍分配是整个系统的业务核心,也是一张状态图能讲清楚的部分。房间状态我设计为:
- 空闲(0):床位数未满,可以继续分配。
- 已满(1):已住人数等于床位数,不可分配。
- 维护(2):房间处于维修中,暂停分配。
床位状态细化到每个床位:空闲、已分配、维修中。分配学生时,事务里做三件事:查可用房间和床位,更新床位状态,生成入住流水。这三件事必须在同一个数据库事务里完成,否则会出现“流水生成但床位没改”的数据不一致。
从 MyBatis 的实现角度,Service 方法上直接加@Transactional,注意抛出的异常要是RuntimeException才能触发回滚。Spring 默认只对运行时异常回滚,检查异常不会自动回滚,这是毕设代码里最常见的问题。
3.3 水电、报修等扩展业务的数据建模
报修单是一个经典的“状态流转”业务,建议用一个 status 字段标识:
待审核(0) -> 已派单(1) -> 处理中(2) -> 已完成(3) -> 已确认(4)还可以加一个cancel状态让学生撤销,但要限定只有“待审核”状态才能撤销。在 Django 端实现时,用 choices 定义状态值,每次更新都用 QuerySet 的update()方法并结合条件过滤,比先取出 Python 对象再改字段更安全。比如:
RepairOrder.objects.filter( order_id=order_id, status=RepairOrder.Status.WAIT ).update(status=RepairOrder.Status.CANCEL)这样能避免并发情况下两个人同时改同一单的状态。账单数据建议按月生成,比如每月 1 号跑一个定时任务或由管理员在 SSM 端点击“生成当月账单”,根据宿舍房间上月和本月的水电表读数差值乘以单价得到金额,写进 bill 表,学生端通过 Django 接口查到未缴账单,缴费后更新 status。
4. 代码实现中值得记录的细节与踩坑
4.1 MyBatis 动态SQL与复杂查询
SSM 端很多功能都是“按条件搜索列表”,这种需求用 MyBatis 的动态 SQL 特别合适。比如学生按姓名、学院、宿舍楼组合查询:
<select id="selectStudentByCondition" resultType="com.dorm.entity.StudentVo"> SELECT s.student_no, s.name, s.college, r.building_id, r.room_no FROM student s LEFT JOIN check_in_record c ON s.student_no = c.student_no AND c.status = 1 LEFT JOIN dorm_room r ON c.room_id = r.room_id <where> <if test="name != null and name != ''"> AND s.name LIKE CONCAT('%', #{name}, '%') </if> <if test="college != null and college != ''"> AND s.college = #{college} </if> <if test="buildingId != null"> AND r.building_id = #{buildingId} </if> </where> ORDER BY s.student_no </select>注意LEFT JOIN的写法:它需要关联当前有效的入住记录(c.status = 1),而不是直接 join student 和 room 两张表。如果你用 inner join 或者不带状态条件,退宿过的学生就会查到历史房间信息。这个坑我在调试别人代码时出现过不止一次。
4.2 Django ORM 的常用操作与删除对象
Django 端也要处理不少数据操作。学生端最常见的几个操作模式:
- 查询当前学生的未缴账单:
Bill.objects.filter(student_no=request.user, status__in=[0, 1]).order_by('-period') - 分页:DRF 的分页类配置
PageNumberPagination,每页默认 10 条。 - 删除对象:比如退宿申请审批通过后,删除对应的床位占位记录,用
CheckInRecord.objects.filter(record_id=id).delete()。
说个细节,.delete()是有返回值的,返回一个(total_deleted, dict)元组,里面是每个表删了几行。如果你写了result = queryset.delete()拿到的不是行数而是元组,别搞混。还有一点,Django 的删除默认是软删除吗?不是,它是物理删除。所以要养成习惯:涉及学生历史流水的数据,千万不要物理删除,应该加一个is_active字段,用update(is_active=False)做“逻辑删除”。我见过有同学把入住记录直接 delete 掉,查历史入住情况的时候一片空白,最后还是从备份里恢复的。
4.3 并发抢房与数据一致性
宿舍分配在新生报到季会有真实的高并发场景:多个同学同时抢同一间房的最后一个床位。数据库层面如果只做“先查床位数,再 update”,必然出现超卖。解决办法有两个:
一是乐观锁。在 dorm_room 表加version字段,更新时带上版本号:
<update id="decreaseUsedBeds"> UPDATE dorm_room SET used_beds = used_beds + 1, version = version + 1 WHERE room_id = #{roomId} AND version = #{version} AND used_beds < bed_count </update>如果受影响行数为 0,说明房间被别人抢了,事务回滚并提示“该房间已满”。这是面试时极容易问到的点,也是真正值得写进论文的设计。
二是数据库行锁,在 MySQL 的 InnoDB 引擎下用SELECT ... FOR UPDATE锁住房间行,再执行更新。行锁实现直观,但并发性能略差。宿舍分配的并发量远没有秒杀那么高,两种方式都够用,我一般推荐乐观锁,因为它不依赖长事务。
4.4 两套系统的时间与状态同步
Django 端和 SSM 端如果部署在同一台机器上,绕不开时区问题。MySQL 连接串里设置serverTimezone=Asia/Shanghai,Django 的settings.py里设置TIME_ZONE = 'Asia/Shanghai',同时USE_TZ = True后存进数据库的是 UTC 时间。如果你发现学生端展示的“报修时间”比实际慢了 8 小时,八成就是时区设置不一致。建议两边的插入时间都直接取数据库当前时间NOW(),业务侧不要自己拼时间字符串,统一以数据库时间为准。
5. 权限控制与接口安全:三种角色怎么管
5.1 SSM端拦截器与Django端中间件的权限校验
宿舍管理系统的用户角色通常有三种:管理员、宿管、学生。权限设计上我建议走“角色-菜单-操作”的轻量 RBAC:
- 管理员:拥有全部菜单,可以分配宿舍、管理楼栋、查看报表。
- 宿管:只管理自己负责的楼栋,可以处理报修、录入水电读数、发布公告。
- 学生:只看到自己的宿舍、报修单和账单。
SSM 端实现权限控制最经典的方式是自定义拦截器,继承HandlerInterceptorAdapter,在preHandle里判断 Session 中的登录用户以及访问路径的角色要求:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { SysUser user = (SysUser) request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 检查角色和路径权限 if (user.getRole() == 1 && !hasPermission(request.getRequestURI())) { response.setStatus(403); return false; } return true; }在spring-mvc.xml里用<mvc:interceptors>配置拦截路径,/admin/**必须登录,/admin/super/**必须管理员,存在跨角色需求时再细化。Django 端用 DRF 的话,直接对视图加IsAuthenticated权限类,再配合自定义IsStudent这类权限类,保证学生只能访问自己的数据。CommonMeta 层再做一层“行级权限”控制,比如报修列表强制过滤student_no=request.user,防止越权看别人的报修单。
5.2 Token、密码存储与日志记录
学生端用 JWT 后,要特别注意 token 的过期时间。我给毕设项目用的是 2 小时过期,用户长期使用时改用一个 7 天的 refresh token 或在密码中增加 remember 逻辑。Django 里校验 token 的中间件可以这样写:
class JWTAuthMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): auth = request.headers.get('Authorization') if auth and auth.startswith('Bearer '): token = auth.split(' ')[1] payload = verify_jwt_token(token) if payload: request.user_id = payload['user_id'] return self.get_response(request)注意:Django 自带request.user在 REST Framework 里会被 DRF 的认证机制覆盖,所以不要在普通中间件里强行给request.user赋值,用一个自定义属性request.user_id更安全。
密码存储方面,SSM 端用BCryptPasswordEncoder或spring-security-crypto里的BCrypt,无论如何不要用 MD5 明文存。Django 的django.contrib.auth.hashers自带 PBKDF2 加密,直接用make_password和check_password就行。论文里写清楚“密码不可逆加密存储”,是很稳妥的技术主张。日志方面,SSM 配置 Logback 输出到文件,Django 用 logging 配置记录请求方法、路径、状态码,虽然不影响功能,但可以应对“如何证明你的系统有安全意识”这类提问。
6. 答辩讲解与功能演示:亮点怎么讲,追问怎么答
6.1 演示流程设计
答辩时最怕的不是不知道系统怎么用,而是从头到尾没有章法,把最有含金量的功能一笔带过。我建议按下面的顺序演示:
- 先讲登录:展示管理员、宿管、学生三种角色的差异,证明你做了权限控制。
- 演示宿舍分配:选一个房间,分配两位学生进去,用学生的视角登录,能看到自己的宿舍和床位信息。
- 演示报修全流程:学生提交报修 -> 宿管派单 -> 维修工处理完成 -> 学生确认。重点切换到数据库或日志展示状态变化,证明你的状态流程清晰。
- 演示报表统计:按楼栋统计入住率、按学院统计住宿人数。这个功能建议用 ECharts 画柱状图,视觉效果好,答辩很加分。
- 回到代码层面展示一两个亮点:比如乐观锁的 update 语句、JWT 校验中间件、MyBatis 动态 SQL 查询。
整个演示控制在十分钟左右,老师会顺着你的流程追问,你能讲清楚每一步的数据流向,这个项目基本就稳了。
6.2 高频追问与应答思路
我整理了答辩和面试中反复出现的几个问题,给出的都是能直接用的应答思路:
为什么用 SSM 而不是 SpringBoot?不要只说“老师指定用 SSM”。可以说:SSM 通过 XML 显式配置 Spring 容器、事务管理器、MyBatis 映射,让我对框架的底层整合过程理解更深;SpringBoot 虽好,但把很多细节包起来了,毕设阶段用 SSM 反而能展示对框架原理的掌握。
为什么同时用 SSM 和 Django,不统一一套?说是两个系统的定位不同:管理端是重业务、强事务的系统,用了 Java 生态的成熟事务管理和权限控制;学生端是高频轻量接口,Django 的 ORM 和 DRF 能快速开发、方便联调。两边共用同一套数据库,通过接口和表前缀解耦。
并发抢房怎么处理的?答乐观锁或行锁,重点说 update 语句带 version 条件,受影响行数为 0 说明冲突,直接抛异常回滚。如果老师追问性能,可以答“宿舍分配场景并发量不高,乐观锁足够,避免长事务锁表”。
登录状态怎么跨系统保持?答 SSM 用 Session,Django 用 JWT,两者不共享;学生端接口用 token 鉴权,管理端用 Session + 拦截器。这其实是两套独立的认证体系,各有分工。
数据库事务边界在哪?比如分配宿舍,在一个 Service 方法内开启事务,完成查房、更新床位、插入住记录三步,任何一个环节失败整体回滚。要想不被问倒,建议把几个核心 Service 方法上的@Transactional都标出来,弄清楚为什么这些方法需要事务。
前端数据量大了怎么办?答 MySQL 分页配合索引。重点给dorm_room的building_id、used_beds建联合索引,给repair_order的student_no、status建索引,查询效率明显提高。Django 端再配一个PageNumberPagination分页,完全能支撑万人级学校的使用。
我个人带项目的体感是,这套“SSM 管理端 + Django 学生端”的混合架构,只要讲清楚设计动机和边界,是比单一框架更出彩的地方。很多同学毕设代码写完了但不会讲,核心问题就是对关键决策缺少“为什么”的思考。你要把每个设计点都当成面试题来准备——这不是背答案,而是理解当初为什么这么选、踩了什么坑才改成这样。如果你手头的源码和文档结构比较乱,先把上面的表结构和状态设计对一遍,再按演示流程理出主线,答辩时会从容很多。最后补一句:正式答辩前,一定把宿舍满房、重复报修、错误账号登录这类边界情况亲手测一遍,因为这些往往就是老师想点开看看的地方。