# 宿舍管理系统设计与实现 --- ## 1 实验内容 ### 1.1 项目背景与选题介绍 高校宿舍管理是学生工作的重要组成部分。传统管理方式多依赖纸质表格或 Excel 记录,存在以下痛点: - **信息分散**:学生的住宿信息、宿舍楼的房间分配、床位状态分别记录在不同文档中,查询需跨多个文件 - **更新滞后**:学生入住、调宿、退宿等变更需层层上报,手动更新表格,容易遗漏和出错 - **学生体验差**:学生无法自主查看自己的住宿信息,选宿/换宿需线下填表申请,流程冗长 - **管理效率低**:管理员面对整栋宿舍楼的数百个床位,手动分配和调整工作量大 针对上述问题,本课题设计并实现了一个基于 SSM(Spring + Spring MVC + MyBatis)框架的宿舍管理系统,提供线上化的宿舍资源管理和学生自助服务。 ### 1.2 项目开发目标与功能概述 #### 核心功能 | 模块 | 功能说明 | |------|----------| | 登录认证 | 学生和管理员双角色登录,session 会话管理 | | 宿舍楼管理 | 管理员对宿舍楼进行增删改查操作 | | 房间管理 | 管理员按楼栋管理房间,含分页查询 | | 床位管理 | 管理员按房间管理床位状态 | | 用户管理 | 管理员管理学生账户,含头像上传 | | 分配管理 | 管理员查看和调整学生住宿分配记录 | | 学生选宿 | 学生查看当前住宿,自主选择空闲床位 | #### 技术目标 - 掌握 SSM 三大框架的整合流程与 XML 配置方式 - 理解 MVC 设计模式在 Web 项目中的分层实现 - 实践 Spring 拦截器的登录验证与权限控制 - 实现 MyBatis 分页查询(PageHelper) - 实现 Spring MVC 文件上传功能 - 掌握声明式事务管理(@Transactional) ### 1.3 开发环境与技术栈 | 类别 | 选型 | 版本 | |------|------|------| | 操作系统 | Windows / Linux | — | | JDK | Java | 8 | | Web 服务器 | Apache Tomcat | 9.x | | 数据库 | MySQL | 8.0.25 | | IDE | Eclipse / IntelliJ IDEA | — | | 构建工具 | Maven | 3.x | | **Spring Framework** | Spring + Spring MVC | 5.3.31 | | **ORM 框架** | MyBatis | 3.5.6 | | **视图技术** | JSP + JSTL | 2.3+ | | **数据库连接池** | Druid | 1.2.8 | | **分页插件** | PageHelper | 5.x | | **日志** | Logback | 1.2.11 | | **前端** | Bootstrap 4 + jQuery | — | --- ## 2 系统总体设计 ### 2.1 需求分析 #### 功能性需求 **角色一:管理员(Admin)** - 登录系统,进入管理后台 - 管理宿舍楼:新增、编辑、删除、分页查询宿舍楼 - 管理房间:按楼栋筛选,新增、编辑、删除、分页查询房间 - 管理床位:按房间筛选,新增、编辑、删除、分页查询床位 - 管理用户:添加/编辑/删除学生账户,上传学生头像 - 管理分配:查看分配记录,手动调整学生住宿 **角色二:学生(Student)** - 登录系统,进入学生端首页 - 查看当前住宿信息(楼栋、房间号、床位号) - 如未分配床位,浏览可选楼层/房间的空闲床位并自助选择 #### 非功能性需求 - **安全性**:密码加密存储(MD5 / BCrypt),拦截器保护所有非登录请求,学生无法访问管理员功能 - **事务性**:床位分配操作保证数据一致性(同一床位不能被两人同时选中) - **可用性**:使用 Bootstrap 4 响应式布局,界面清晰 - **扩展性**:分层架构便于后续功能扩展 ### 2.2 系统架构设计 系统采用 SSM 经典三层架构 + MVC 设计模式: ```mermaid graph TD subgraph 视图层 Presentation JSP[JSP 页面
Bootstrap + jQuery] end subgraph 控制层 Controller C[Controller
接收请求 / 调用服务 / 返回视图] I[Interceptor
登录校验 + 权限控制] end subgraph 业务层 Service S[Service Interface] SI[ServiceImpl
@Transactional 事务管理] end subgraph 持久层 Persistence M[Mapper Interface] MX[MyBatis XML 映射文件] P[PageHelper 分页插件] end subgraph 数据层 DB[(MySQL Database
dormitory_manage)] end JSP -- HTTP 请求 --> C C -- 调用 --> S S -- 实现 --> SI SI -- 调用 --> M M -- 引用 --> MX MX -- SQL --> DB C -- 经过 --> I ``` **请求处理流程**: 1. 用户访问 URL → `DispatcherServlet` 拦截 2. 经过 `Interceptor` 链(登录检查 → 权限检查) 3. `Controller` 接收请求参数,调用 `Service` 层 4. `Service` 层处理业务逻辑(事务控制),调用 `Mapper` 5. `Mapper` 通过 MyBatis XML 执行 SQL,操作数据库 6. 结果逐层返回,`Controller` 将数据存入 Model,转发到 JSP 视图 ### 2.3 数据库设计 #### E-R 图 ```mermaid erDiagram user ||--o{ assignment : "拥有分配记录" dormitory_building ||--o{ room : "包含房间" room ||--o{ bed : "包含床位" bed ||--o{ assignment : "被分配" user { bigint id PK varchar username UK varchar password varchar name tinyint gender varchar phone tinyint role varchar student_id varchar avatar datetime created_at datetime updated_at } dormitory_building { bigint id PK varchar name varchar code tinyint gender int floor_count varchar description datetime created_at datetime updated_at } room { bigint id PK bigint building_id FK varchar room_number int floor int capacity tinyint gender datetime created_at datetime updated_at } bed { bigint id PK bigint room_id FK varchar bed_number tinyint status datetime created_at datetime updated_at } assignment { bigint id PK bigint user_id FK bigint bed_id FK date assign_date date end_date tinyint status datetime created_at datetime updated_at } ``` #### 数据表结构 **表1:user(用户表)** | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK AUTO_INCREMENT | 用户ID | | username | VARCHAR(50) | UNIQUE NOT NULL | 登录用户名 | | password | VARCHAR(255) | NOT NULL | 登录密码(MD5加密) | | name | VARCHAR(50) | NOT NULL | 真实姓名 | | gender | TINYINT | DEFAULT 0 | 0-女,1-男 | | phone | VARCHAR(20) | | 联系电话 | | role | TINYINT | NOT NULL DEFAULT 1 | 0-管理员,1-学生 | | student_id | VARCHAR(50) | | 学号(学生特有) | | avatar | VARCHAR(255) | | 头像文件路径 | | created_at | DATETIME | | 创建时间 | | updated_at | DATETIME | | 更新时间 | **表2:dormitory_building(宿舍楼表)** | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK AUTO_INCREMENT | 楼栋ID | | name | VARCHAR(100) | NOT NULL | 楼栋名称(如"梅苑A栋") | | code | VARCHAR(20) | | 楼栋编号 | | gender | TINYINT | DEFAULT 2 | 0-女,1-男,2-混合 | | floor_count | INT | DEFAULT 6 | 楼层数 | | description | VARCHAR(500) | | 备注描述 | | created_at | DATETIME | | | | updated_at | DATETIME | | | **表3:room(房间表)** | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK AUTO_INCREMENT | 房间ID | | building_id | BIGINT | FK → dormitory_building.id | 所属楼栋 | | room_number | VARCHAR(20) | NOT NULL | 房间号(如"101") | | floor | INT | | 所在楼层 | | capacity | INT | NOT NULL | 总床位数 | | gender | TINYINT | | 性别限制(可覆盖楼栋设置) | | created_at | DATETIME | | | | updated_at | DATETIME | | | **表4:bed(床位表)** | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK AUTO_INCREMENT | 床位ID | | room_id | BIGINT | FK → room.id | 所属房间 | | bed_number | VARCHAR(10) | NOT NULL | 床位编号(如"A"、"1") | | status | TINYINT | DEFAULT 0 | 0-空闲,1-占用,2-维修 | | created_at | DATETIME | | | | updated_at | DATETIME | | | **表5:assignment(分配记录表)** | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK AUTO_INCREMENT | 记录ID | | user_id | BIGINT | FK → user.id | 学生用户 | | bed_id | BIGINT | FK → bed.id | 分配的床位 | | assign_date | DATE | NOT NULL | 入住日期 | | end_date | DATE | | 迁出日期(空=当前住宿) | | status | TINYINT | DEFAULT 0 | 0-当前住宿,1-已迁出 | | created_at | DATETIME | | | | updated_at | DATETIME | | | #### 索引与约束 - `user(username)` — 唯一索引 - `user(student_id)` — 唯一索引 - `assignment(user_id, status)` — 联合索引,快速查询学生当前住宿 - `assignment(bed_id, status)` — 联合索引,快速查询床位当前是否被占用 - `room(building_id, room_number)` — 唯一约束,同一楼栋房号唯一 ### 2.4 功能模块设计 ```mermaid mindmap root((宿舍管理系统)) 登录模块 学生登录 管理员登录 拦截器认证 宿舍楼管理_Admin 新增楼栋 编辑楼栋 删除楼栋 分页查询 房间管理_Admin 新增房间 编辑房间 删除房间 按楼栋筛选 分页查询 床位管理_Admin 新增床位 编辑床位 删除床位 按房间筛选 分页查询 用户管理_Admin 新增学生 编辑学生 删除学生 分页查询 头像上传 分配管理_Admin 查看分配记录 调整入住 分页查询 学生自助 查看当前住宿 浏览空闲床位 选择床位_直接入住 ``` #### 学生选宿流程 ```mermaid flowchart TD A[学生登录] --> B{已有住宿记录?} B -->|是 status=0| C[展示当前住宿信息
楼栋 → 房间 → 床位] B -->|否| D[查询可选宿舍楼根据学生性别筛选] D --> E[展示可选宿舍楼列表] E --> F[选择楼栋] F --> G[展示该楼栋可选房间有剩余床位的房间] G --> H[选择房间] H --> I[展示房间内空闲床位] I --> J[选择床位] J --> K[确认入住] K --> L{床位是否仍空闲?} L -->|是| M[创建 assignment 记录
status=0] L -->|否| N[提示已被他人选择] M --> O[更新床位 status=1] O --> P[跳转至住宿信息页] N --> I ``` --- ## 3 项目开发实现过程 ### 3.1 项目搭建与框架整合 #### 3.1.1 创建 Maven Web 项目 使用 Maven 的 `maven-archetype-webapp` 原型创建项目,或直接在 IDE 中新建 Maven Web 项目。项目基本信息如下: - **GroupId**: `space.anyi` - **ArtifactId**: `SSM_template` - **Packaging**: `war` - **JDK**: 1.8 **pom.xml 完整依赖** ```xml 8 8 UTF-8 5.3.31 3.5.6 8.0.25 org.springframework spring-context ${spring.version} org.springframework spring-webmvc ${spring.version} org.springframework spring-jdbc ${spring.version} org.springframework spring-tx ${spring.version} org.springframework spring-aop ${spring.version} org.aspectj aspectjweaver 1.9.7 org.mybatis mybatis ${mybatis.version} org.mybatis mybatis-spring 2.0.6 mysql mysql-connector-java ${mysql.version} com.alibaba druid 1.2.8 javax.servlet javax.servlet-api 4.0.1 provided javax.servlet.jsp.jstl javax.servlet.jsp.jstl-api 1.2.2 org.apache.taglibs taglibs-standard-impl 1.2.5 com.github.pagehelper pagehelper 5.3.2 commons-fileupload commons-fileupload 1.4 ch.qos.logback logback-classic 1.2.11 org.junit.jupiter junit-jupiter 5.10.0 test org.springframework spring-test ${spring.version} test org.apache.tomcat.maven tomcat7-maven-plugin 2.2 8080 / UTF-8 ``` **spring-mvc.xml** — 配置 Spring MVC 视图解析器、文件上传和拦截器: ```xml ``` **spring-context.xml** — 配置 Spring 核心、数据源、MyBatis 和事务: ```xml ``` **mybatis-config.xml** — 配置 PageHelper 插件和 MyBatis 设置: ```xml ``` **logback.xml** — 日志配置(控制台 + 滚动文件): ```xml %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${LOG_HOME}/ssm-info.log ${LOG_HOME}/archive/ssm-info-%d{yyyy-MM-dd}.%i.log ${MAX_FILE_SIZE} ${MAX_HISTORY} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${LOG_HOME}/ssm-error.log ${LOG_HOME}/archive/ssm-error-%d{yyyy-MM-dd}.%i.log ${MAX_FILE_SIZE} ${MAX_HISTORY} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ``` **web.xml** — 配置 DispatcherServlet、ContextLoaderListener 和字符编码过滤器: ```xml contextConfigLocation classpath:spring-context.xml org.springframework.web.context.ContextLoaderListener dispatcher org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring-mvc.xml 1 dispatcher / encodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceEncoding true encodingFilter /* ``` **jdbc.properties** — 数据库连接配置: ```properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/dormitory_manage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=root ``` #### 3.1.2 项目目录结构 ``` src/main/java/space/anyi/ ├── common/ # 通用工具类 │ ├── PageResult.java # 分页结果封装 │ └── Result.java # JSON 统一返回格式 ├── controller/ # 控制层 │ ├── AdminController.java │ ├── AssignmentController.java │ ├── BedController.java │ ├── BuildingController.java │ ├── LoginController.java │ ├── RoomController.java │ ├── StudentController.java │ └── UserController.java ├── entity/ # 实体类 │ ├── Assignment.java │ ├── Bed.java │ ├── DormitoryBuilding.java │ ├── Room.java │ └── User.java ├── interceptor/ # 拦截器 │ ├── AuthInterceptor.java # 权限控制 │ └── LoginInterceptor.java # 登录验证 ├── mapper/ # MyBatis Mapper 接口 │ ├── AssignmentMapper.java │ ├── BedMapper.java │ ├── DormitoryBuildingMapper.java │ ├── RoomMapper.java │ └── UserMapper.java └── service/ # 业务层 ├── AssignmentService.java ├── BedService.java ├── BuildingService.java ├── RoomService.java ├── UserService.java └── impl/ ├── AssignmentServiceImpl.java ├── BedServiceImpl.java ├── BuildingServiceImpl.java ├── RoomServiceImpl.java └── UserServiceImpl.java src/main/resources/ ├── jdbc.properties # 数据库连接配置 ├── logback.xml # 日志配置 ├── mybatis-config.xml # MyBatis 配置 ├── spring-context.xml # Spring + MyBatis + 事务 ├── spring-mvc.xml # Spring MVC + 视图解析器 + 文件上传 └── mapper/ # MyBatis XML 映射文件 ├── AssignmentMapper.xml ├── BedMapper.xml ├── DormitoryBuildingMapper.xml ├── RoomMapper.xml └── UserMapper.xml src/main/webapp/ ├── WEB-INF/ │ ├── web.xml # Servlet 配置 │ └── views/ # JSP 视图文件 │ ├── admin/ # 管理员页面 │ │ ├── dashboard.jsp │ │ ├── building_list.jsp │ │ ├── building_edit.jsp │ │ ├── room_list.jsp │ │ ├── room_edit.jsp │ │ ├── bed_list.jsp │ │ ├── bed_edit.jsp │ │ ├── user_list.jsp │ │ ├── user_edit.jsp │ │ └── assignment_list.jsp │ ├── student/ # 学生页面 │ │ ├── dashboard.jsp │ │ └── select_bed.jsp │ ├── common/ │ │ └── header.jsp │ ├── login.jsp │ └── error.jsp └── static/ └── uploads/ # 头像上传目录 ``` ### 3.2 数据库实现 #### SQL 建表脚本 ```sql CREATE DATABASE IF NOT EXISTS dormitory_manage DEFAULT CHARACTER SET utf8mb4; USE dormitory_manage; -- 用户表 CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 0 COMMENT '0-女, 1-男', phone VARCHAR(20), role TINYINT NOT NULL DEFAULT 1 COMMENT '0-管理员, 1-学生', student_id VARCHAR(50) UNIQUE COMMENT '学号', avatar VARCHAR(255) COMMENT '头像路径', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 宿舍楼表 CREATE TABLE dormitory_building ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, code VARCHAR(20), gender TINYINT DEFAULT 2 COMMENT '0-女, 1-男, 2-混合', floor_count INT DEFAULT 6, description VARCHAR(500), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 房间表 CREATE TABLE room ( id BIGINT AUTO_INCREMENT PRIMARY KEY, building_id BIGINT NOT NULL, room_number VARCHAR(20) NOT NULL, floor INT, capacity INT NOT NULL COMMENT '总床位数', gender TINYINT COMMENT '可覆盖楼栋性别限制', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_building_id (building_id), UNIQUE KEY uk_building_room (building_id, room_number) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 床位表 CREATE TABLE bed ( id BIGINT AUTO_INCREMENT PRIMARY KEY, room_id BIGINT NOT NULL, bed_number VARCHAR(10) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-空闲, 1-占用, 2-维修', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_room_id (room_id), UNIQUE KEY uk_room_bed (room_id, bed_number) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 分配记录表(中间表) CREATE TABLE assignment ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, bed_id BIGINT NOT NULL, assign_date DATE NOT NULL, end_date DATE COMMENT '迁出日期, NULL=当前住宿', status TINYINT DEFAULT 0 COMMENT '0-当前住宿, 1-已迁出', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), INDEX idx_bed_id (bed_id), INDEX idx_user_status (user_id, status), INDEX idx_bed_status (bed_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 测试数据 ```sql -- 管理员账号 INSERT INTO user (username, password, name, role) VALUES ('admin', '21232f297a57a5a743894a0e4a801fc3', '系统管理员', 0); -- 密码 admin123 的 MD5 -- 学生账号 INSERT INTO user (username, password, name, gender, role, student_id) VALUES ('zhangsan', 'e10adc3949ba59abbe56e057f20f883e', '张三', 1, 1, '2024001'), ('lisi', 'e10adc3949ba59abbe56e057f20f883e', '李四', 1, 1, '2024002'), ('wangwu', 'e10adc3949ba59abbe56e057f20f883e', '王五', 1, 1, '2024003'); -- 密码 123456 的 MD5 -- 宿舍楼 INSERT INTO dormitory_building (name, code, gender, floor_count) VALUES ('梅苑A栋', 'MA', 1, 6), ('兰苑B栋', 'LB', 0, 6), ('竹苑C栋', 'ZC', 2, 5); -- 房间(梅苑A栋 101~106) INSERT INTO room (building_id, room_number, floor, capacity, gender) VALUES (1, '101', 1, 4, 1), (1, '102', 1, 4, 1), (1, '103', 1, 4, 1); -- 床位(101 房间 4 个床位) INSERT INTO bed (room_id, bed_number, status) VALUES (1, 'A', 0), (1, 'B', 0), (1, 'C', 0), (1, 'D', 0); ``` ### 3.3 业务功能代码实现 #### 3.3.1 实体层(Entity) 以 `User.java` 为例: ```java package space.anyi.entity; import java.time.LocalDateTime; public class User { private Long id; private String username; private String password; private String name; private Integer gender; private String phone; private Integer role; // 0-管理员, 1-学生 private String studentId; private String avatar; private LocalDateTime createdAt; private LocalDateTime updatedAt; // getter / setter(省略) } ``` #### 3.3.2 持久层(Mapper Interface + XML) 以 `DormitoryBuildingMapper.java` 为例: ```java package space.anyi.mapper; import space.anyi.entity.DormitoryBuilding; import java.util.List; public interface DormitoryBuildingMapper { List selectAll(); DormitoryBuilding selectById(Long id); int insert(DormitoryBuilding building); int update(DormitoryBuilding building); int deleteById(Long id); } ``` `DormitoryBuildingMapper.xml`: ```xml id, name, code, gender, floor_count, description, created_at, updated_at INSERT INTO dormitory_building (name, code, gender, floor_count, description) VALUES (#{name}, #{code}, #{gender}, #{floorCount}, #{description}) UPDATE dormitory_building SET name = #{name}, code = #{code}, gender = #{gender}, floor_count = #{floorCount}, description = #{description} WHERE id = #{id} DELETE FROM dormitory_building WHERE id = #{id} ``` #### 3.3.3 业务层(Service) 以 `BedService.java` 为例,展示选床核心逻辑: ```java package space.anyi.service; public interface BedService { PageResult selectByRoomPage(Long roomId, int pageNum, int pageSize); Bed selectById(Long id); int addBed(Bed bed); int updateBed(Bed bed); int deleteBed(Long id); /** 学生选床:直接入住 */ Assignment assignBed(Long userId, Long bedId); } ``` `BedServiceImpl.java`(完整实现,含分页查询和核心选床逻辑): ```java package space.anyi.service.impl; @Service public class BedServiceImpl implements BedService { @Autowired private BedMapper bedMapper; @Autowired private AssignmentMapper assignmentMapper; @Override public PageResult selectPage(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List list = bedMapper.selectAll(); PageInfo pageInfo = new PageInfo<>(list); return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getPages()); } @Override public PageResult selectByRoomPage(Long roomId, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List list = bedMapper.selectByRoomId(roomId); PageInfo pageInfo = new PageInfo<>(list); return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getPages()); } @Override public List selectAvailableByRoomId(Long roomId) { return bedMapper.selectAvailableByRoomId(roomId); } @Override public Bed selectById(Long id) { return bedMapper.selectById(id); } @Override @Transactional public int addBed(Bed bed) { if (bed.getStatus() == null) bed.setStatus(0); return bedMapper.insert(bed); } @Override @Transactional public int updateBed(Bed bed) { return bedMapper.update(bed); } @Override @Transactional public int deleteBed(Long id) { return bedMapper.deleteById(id); } @Override @Transactional public Assignment assignBed(Long userId, Long bedId) { Bed bed = bedMapper.selectById(bedId); if (bed == null || bed.getStatus() != 0) { throw new RuntimeException("该床位不可用"); } Assignment active = assignmentMapper.selectActiveByUserId(userId); if (active != null) { throw new RuntimeException("你已有住宿记录,不可重复选择"); } Assignment assignment = new Assignment(); assignment.setUserId(userId); assignment.setBedId(bedId); assignment.setAssignDate(LocalDate.now()); assignment.setStatus(0); assignmentMapper.insert(assignment); bed.setStatus(1); bedMapper.update(bed); return assignment; } } ``` #### 3.3.4 控制层(Controller) 以 `StudentController.java` 为例(完整实现,含选床向导和头像上传): ```java package space.anyi.controller; @Controller @RequestMapping("/student") public class StudentController { @Autowired private AssignmentService assignmentService; @Autowired private BuildingService buildingService; @Autowired private RoomService roomService; @Autowired private BedService bedService; @Autowired private UserService userService; @GetMapping("/dashboard") public String dashboard(HttpSession session, Model model) { User user = (User) session.getAttribute("user"); Assignment assignment = assignmentService.selectActiveByUserId(user.getId()); model.addAttribute("assignment", assignment); return "student/dashboard"; } @GetMapping("/select") public String selectBed(@RequestParam(required = false) Long buildingId, @RequestParam(required = false) Long roomId, HttpSession session, Model model) { User user = (User) session.getAttribute("user"); // 已有住宿则跳转 Assignment existing = assignmentService.selectActiveByUserId(user.getId()); if (existing != null) { return "redirect:/student/dashboard"; } // 按学生性别筛选可用楼栋 List buildings = buildingService.selectByGender(user.getGender()); model.addAttribute("buildings", buildings); if (buildingId != null) { List rooms = roomService.selectAvailableByBuildingId(buildingId); model.addAttribute("rooms", rooms); model.addAttribute("selectedBuildingId", buildingId); } if (roomId != null) { List beds = bedService.selectAvailableByRoomId(roomId); model.addAttribute("beds", beds); model.addAttribute("selectedRoomId", roomId); } return "student/select_bed"; } @PostMapping("/assign") public String assign(Long bedId, HttpSession session) { User user = (User) session.getAttribute("user"); try { bedService.assignBed(user.getId(), bedId); } catch (RuntimeException e) { return "redirect:/student/select?error=" + e.getMessage(); } return "redirect:/student/dashboard"; } @PostMapping("/uploadAvatar") public String uploadAvatar(@RequestParam("file") MultipartFile file, HttpSession session) throws IOException { if (file.isEmpty() || !file.getContentType().startsWith("image/")) { return "redirect:/student/dashboard?error=format"; } if (file.getSize() > 2 * 1024 * 1024) { return "redirect:/student/dashboard?error=size"; } // 唯一文件名 → 保存到 /static/uploads/ → 更新用户头像 String fileName = UUID.randomUUID().toString() + file.getOriginalFilename().substring( file.getOriginalFilename().lastIndexOf(".")); String path = session.getServletContext().getRealPath("/static/uploads/"); new File(path).mkdirs(); file.transferTo(new File(path + fileName)); User user = (User) session.getAttribute("user"); userService.updateAvatar(user.getId(), "/static/uploads/" + fileName); user.setAvatar("/static/uploads/" + fileName); session.setAttribute("user", user); return "redirect:/student/dashboard"; } } ``` #### 3.3.5 视图层(JSP) 以 `student/dashboard.jsp` 为例: ```jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 我的住宿
个人资料
${sessionScope.user.name.charAt(0)}
${sessionScope.user.name}

${sessionScope.user.studentId}

住宿信息
宿舍楼${assignment.buildingName}
房间号${assignment.roomNumber}
床位号${assignment.bedNumber}
入住日期${assignment.assignDate}
你还没有分配床位。 去选床
``` ### 3.4 核心技术亮点实现 #### 3.4.1 登录拦截器 ```java package space.anyi.interceptor; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } } ``` 在 `spring-mvc.xml` 中注册: ```xml ``` #### 3.4.2 权限拦截器 ```java package space.anyi.interceptor; public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("user"); if (user == null || user.getRole() != 0) { response.sendRedirect(request.getContextPath() + "/login?error=unauthorized"); return false; } return true; } } ``` #### 3.4.3 分页查询(PageHelper) 在 `mybatis-config.xml` 中配置 PageHelper 插件: ```xml ``` Service 层使用: ```java @Override public PageResult selectUserPage(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List list = userMapper.selectAll(); PageInfo pageInfo = new PageInfo<>(list); return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getPages()); } ``` #### 3.4.4 文件上传(学生头像) Controller 层处理(取自 `StudentController.java`): ```java @PostMapping("/uploadAvatar") public String uploadAvatar(@RequestParam("file") MultipartFile file, HttpSession session) throws IOException { if (file.isEmpty() || !file.getContentType().startsWith("image/")) { return "redirect:/student/dashboard?error=format"; } if (file.getSize() > 2 * 1024 * 1024) { return "redirect:/student/dashboard?error=size"; } String originalName = file.getOriginalFilename(); String ext = originalName.substring(originalName.lastIndexOf(".")); String fileName = UUID.randomUUID().toString() + ext; String uploadPath = session.getServletContext().getRealPath("/static/uploads/"); File dir = new File(uploadPath); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(uploadPath + fileName)); User user = (User) session.getAttribute("user"); userService.updateAvatar(user.getId(), "/static/uploads/" + fileName); user.setAvatar("/static/uploads/" + fileName); session.setAttribute("user", user); return "redirect:/student/dashboard"; } ``` #### 3.4.5 事务管理 在 `spring-context.xml` 中已有配置: ```xml ``` Service 实现类中使用 `@Transactional` 保证选床等操作的原子性。 ### 3.5 技术难点与问题解决 | 问题 | 原因 | 解决方案 | |------|------|----------| | 文件上传返回 null | 未配置 `multipartResolver` Bean,或 `
` 缺少 `enctype="multipart/form-data"` | 在 spring-mvc.xml 中声明 `CommonsMultipartResolver`,表单添加 `enctype` | | 分页查询总页数不准确 | PageHelper 的 `reasonable` 参数未开启,页码超出范围时返回空数据 | 在 mybatis-config.xml 中设置 `` | | 关联查询 N+1 问题 | 床位列表页面循环查询每个床位的房间和楼栋信息 | 使用 MyBatis `LEFT JOIN` 一次性查出关联数据,`resultMap` 映射额外字段 | | 选床并发冲突 | 两个学生同时选中同一空闲床位,事务提交后出现一条床位的两个占用记录 | `assignBed` 使用 `@Transactional` 保证原子性;后续可在 SQL 层加 `SELECT ... FOR UPDATE` 悲观锁 | | 头像上传后页面不更新 | 数据库已更新但 session 中的 `user.avatar` 仍为旧值 | 上传成功后显式 `session.setAttribute("user", user)` 更新会话 | | JSP EL 表达式不解析 | Tomcat 版本默认关闭 EL 或缺少 JSTL 实现库 | 添加 `taglibs-standard-impl` 依赖,或检查 `web.xml` 中的 `web-app` 版本设置 | --- ## 4 系统测试与结果展示 ### 4.1 功能测试说明 | 模块 | 测试方法 | 预期结果 | |------|----------|----------| | 登录 | 学生/管理员凭正确/错误凭据登录 | 正确凭据跳转对应首页,错误凭据提示错误 | | 宿舍楼管理 | 增删改查操作 | 数据正常同步到数据库,列表分页显示 | | 房间管理 | 按楼栋筛选后操作 | 只显示选中楼栋的房间 | | 床位管理 | 按房间筛选,选床操作 | 空闲/占用状态正确切换 | | 学生选宿 | 未分配学生进入选床页面,选择空闲床位 | 选床成功,生成分配记录,床位状态更新 | | 重复选床 | 已分配学生再次选床 | 提示"已有住宿记录" | | 拦截器 | 未登录访问任意页面 | 跳转至登录页 | | 权限拦截 | 学生访问 admin 路径 | 提示无权限 | | 分页 | 列表页翻页操作 | 每页数据正确,页码导航正常 | | 文件上传 | 上传合法/非法文件 | 合法文件上传成功,非法格式/超大小被拒绝 | ### 4.2 核心功能运行截图与说明 > 截图待项目部署运行后补充 | 功能点 | 截图说明 | |--------|----------| | 项目启动 | Tomcat 启动成功,项目部署无异常 | | 登录页 | 输入用户名密码,选择角色 | | 学生首页(已分配) | 显示当前住宿的楼栋/房间/床位信息 | | 学生选床页面 | 展示可选房间列表和空闲床位 | | 管理员-宿舍楼列表 | 宿舍楼分页列表,含新增/编辑/删除按钮 | | 管理员-分配记录 | 所有学生的住宿分配情况 | | 拦截器效果 | 未登录访问被拦截并跳转 | | 权限拦截效果 | 学生访问 admin 路径提示无权限 | | 分页效果 | 列表底部页码导航 | | 头像上传 | 上传前/上传后头像显示 | ### 4.3 系统整体运行效果分析 (待项目部署后填写) ### 4.4 项目不足与优化方向 | 不足 | 优化方向 | |------|----------| | 密码使用 MD5 加密,强度不足 | 改为 BCrypt 或 Spring Security | | 前端校验依赖 jQuery,较为基础 | 引入 Vue.js 或 React 提升交互体验 | | 缺少批量导入功能 | 支持 Excel 批量导入学生、宿舍数据 | | 选床并发控制使用乐观锁,仍有极小概率冲突 | 引入 Redis 分布式锁 | | 无邮件/短信通知 | 分配成功后自动通知学生 | | 仅支持 PC 端 | 适配移动端或开发微信小程序 | --- ## 5 总结与心得 ### 5.1 项目整体总结 本宿舍管理系统基于 SSM 框架实现了宿舍资源的线上化管理。系统覆盖了宿舍楼、房间、床位的三级资源管理,支持管理员统一分配和学生自助选床两种模式。通过拦截器实现了登录验证和角色权限控制,利用 PageHelper 完成了列表分页,通过 CommonsMultipartResolver 实现了头像上传功能。项目完整实践了 SSM 框架整合流程中的配置、编码、调试等环节。 ### 5.2 学习收获与团队分工总结 (待项目完成后填写) ### 5.3 SSM 框架开发经验总结 - **XML 配置的精确性**:SSM 框架整合依赖多个 XML 配置文件,一个 bean 的 class 路径或属性名写错就可能导致容器启动失败,调试时需仔细核对配置文件。 - **MyBatis 的灵活性**:相比全自动 ORM,MyBatis 允许手写 SQL,在复杂关联查询(如分页含关联数据的床位查询)中更加灵活可控。 - **事务注解的便利性**:`@Transactional` 配合 AOP 可以精确控制业务方法的事务边界,选床操作中同时插入 assignment 记录和更新 bed 状态,任一失败则全部回滚。 - **拦截器的责任链模式**:多个拦截器按顺序执行,登录拦截在前、权限拦截在后,各自职责单一,维护方便。 - **分页插件的无侵入设计**:PageHelper 通过 MyBatis 插件机制在 SQL 层面自动添加 `LIMIT` 语句,业务代码几乎零改动。