work.md 52 KB

宿舍管理系统设计与实现


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 设计模式:

graph TD
    subgraph 视图层 Presentation
        JSP[JSP 页面<br/>Bootstrap + jQuery]
    end
    subgraph 控制层 Controller
        C[Controller<br/>接收请求 / 调用服务 / 返回视图]
        I[Interceptor<br/>登录校验 + 权限控制]
    end
    subgraph 业务层 Service
        S[Service Interface]
        SI[ServiceImpl<br/>@Transactional 事务管理]
    end
    subgraph 持久层 Persistence
        M[Mapper Interface]
        MX[MyBatis XML 映射文件]
        P[PageHelper 分页插件]
    end
    subgraph 数据层
        DB[(MySQL Database<br/>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 图

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 功能模块设计

mindmap
  root((宿舍管理系统))
    登录模块
      学生登录
      管理员登录
      拦截器认证
    宿舍楼管理_Admin
      新增楼栋
      编辑楼栋
      删除楼栋
      分页查询
    房间管理_Admin
      新增房间
      编辑房间
      删除房间
      按楼栋筛选
      分页查询
    床位管理_Admin
      新增床位
      编辑床位
      删除床位
      按房间筛选
      分页查询
    用户管理_Admin
      新增学生
      编辑学生
      删除学生
      分页查询
      头像上传
    分配管理_Admin
      查看分配记录
      调整入住
      分页查询
    学生自助
      查看当前住宿
      浏览空闲床位
      选择床位_直接入住

学生选宿流程

flowchart TD
    A[学生登录] --> B{已有住宿记录?}
    B -->|是 status=0| C[展示当前住宿信息<br/>楼栋 → 房间 → 床位]
    B -->|否| D[查询可选宿舍楼根据学生性别筛选]
    D --> E[展示可选宿舍楼列表]
    E --> F[选择楼栋]
    F --> G[展示该楼栋可选房间有剩余床位的房间]
    G --> H[选择房间]
    H --> I[展示房间内空闲床位]
    I --> J[选择床位]
    J --> K[确认入住]
    K --> L{床位是否仍空闲?}
    L -->|是| M[创建 assignment 记录<br/>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 完整依赖

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>5.3.31</spring.version>
    <mybatis.version>3.5.6</mybatis.version>
    <mysql.version>8.0.25</mysql.version>
</properties>

<dependencies>
    <!-- Spring 核心 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <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.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.7</version>
    </dependency>

    <!-- MyBatis -->
    <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.6</version>
    </dependency>

    <!-- MySQL + 连接池 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>

    <!-- JSP + JSTL -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp.jstl</groupId>
        <artifactId>javax.servlet.jsp.jstl-api</artifactId>
        <version>1.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.taglibs</groupId>
        <artifactId>taglibs-standard-impl</artifactId>
        <version>1.2.5</version>
    </dependency>

    <!-- 分页插件 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.3.2</version>
    </dependency>

    <!-- 文件上传 -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>

    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.11</version>
    </dependency>

    <!-- 测试 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.2</version>
            <configuration>
                <port>8080</port>
                <path>/</path>
                <uriEncoding>UTF-8</uriEncoding>
            </configuration>
        </plugin>
    </plugins>
</build>

spring-mvc.xml — 配置 Spring MVC 视图解析器、文件上传和拦截器:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="...">

    <!-- 扫描 Controller 包 -->
    <context:component-scan base-package="space.anyi.controller"/>

    <mvc:annotation-driven/>
    <mvc:default-servlet-handler/>

    <!-- JSP 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!-- 文件上传解析器 -->
    <bean id="multipartResolver"
          class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="2097152"/>
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>

    <!-- 拦截器链 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <mvc:exclude-mapping path="/login"/>
            <mvc:exclude-mapping path="/static/**"/>
            <bean class="space.anyi.interceptor.LoginInterceptor"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/admin/**"/>
            <bean class="space.anyi.interceptor.AuthInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

spring-context.xml — 配置 Spring 核心、数据源、MyBatis 和事务:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="...">

    <!-- 扫描包(排除 Controller,由 spring-mvc.xml 扫描) -->
    <context:component-scan base-package="space.anyi">
        <context:exclude-filter type="annotation"
            expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!-- 加载数据库配置 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- Druid 数据源 -->
    <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}"/>
    </bean>

    <!-- MyBatis SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
        <property name="typeAliasesPackage" value="space.anyi.entity"/>
    </bean>

    <!-- Mapper 接口扫描 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="space.anyi.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

    <!-- 事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

mybatis-config.xml — 配置 PageHelper 插件和 MyBatis 设置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org/DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="cacheEnabled" value="false"/>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="helperDialect" value="mysql"/>
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>
</configuration>

logback.xml — 日志配置(控制台 + 滚动文件):

<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <property name="LOG_HOME" value="logs"/>
    <property name="MAX_HISTORY" value="30"/>
    <property name="MAX_FILE_SIZE" value="100MB"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/ssm-info.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/archive/ssm-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            <maxHistory>${MAX_HISTORY}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/ssm-error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/archive/ssm-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            <maxHistory>${MAX_HISTORY}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE_INFO"/>
        <appender-ref ref="FILE_ERROR"/>
    </root>

    <logger name="space.anyi.mapper" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
</configuration>

web.xml — 配置 DispatcherServlet、ContextLoaderListener 和字符编码过滤器:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="..."
         version="4.0">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-context.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>

    <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>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

jdbc.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 建表脚本

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;

测试数据

-- 管理员账号
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 为例:

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 为例:

package space.anyi.mapper;

import space.anyi.entity.DormitoryBuilding;
import java.util.List;

public interface DormitoryBuildingMapper {
    List<DormitoryBuilding> selectAll();
    DormitoryBuilding selectById(Long id);
    int insert(DormitoryBuilding building);
    int update(DormitoryBuilding building);
    int deleteById(Long id);
}

DormitoryBuildingMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="space.anyi.mapper.DormitoryBuildingMapper">

    <resultMap id="BaseResultMap" type="space.anyi.entity.DormitoryBuilding">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="code" column="code"/>
        <result property="gender" column="gender"/>
        <result property="floorCount" column="floor_count"/>
        <result property="description" column="description"/>
        <result property="createdAt" column="created_at"/>
        <result property="updatedAt" column="updated_at"/>
    </resultMap>

    <sql id="BaseColumns">
        id, name, code, gender, floor_count,
        description, created_at, updated_at
    </sql>

    <select id="selectAll" resultMap="BaseResultMap">
        SELECT <include refid="BaseColumns"/>
        FROM dormitory_building
        ORDER BY id
    </select>

    <select id="selectById" resultMap="BaseResultMap">
        SELECT <include refid="BaseColumns"/>
        FROM dormitory_building WHERE id = #{id}
    </select>

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO dormitory_building (name, code, gender, floor_count, description)
        VALUES (#{name}, #{code}, #{gender}, #{floorCount}, #{description})
    </insert>

    <update id="update">
        UPDATE dormitory_building
        SET name = #{name}, code = #{code}, gender = #{gender},
            floor_count = #{floorCount}, description = #{description}
        WHERE id = #{id}
    </update>

    <delete id="deleteById">
        DELETE FROM dormitory_building WHERE id = #{id}
    </delete>

</mapper>

3.3.3 业务层(Service)

以 BedService.java 为例,展示选床核心逻辑:

package space.anyi.service;

public interface BedService {
    PageResult<Bed> 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(完整实现,含分页查询和核心选床逻辑):

package space.anyi.service.impl;

@Service
public class BedServiceImpl implements BedService {

    @Autowired
    private BedMapper bedMapper;
    @Autowired
    private AssignmentMapper assignmentMapper;

    @Override
    public PageResult<Bed> selectPage(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<Bed> list = bedMapper.selectAll();
        PageInfo<Bed> pageInfo = new PageInfo<>(list);
        return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(),
                pageInfo.getPageNum(), pageInfo.getPages());
    }

    @Override
    public PageResult<Bed> selectByRoomPage(Long roomId, int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<Bed> list = bedMapper.selectByRoomId(roomId);
        PageInfo<Bed> pageInfo = new PageInfo<>(list);
        return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(),
                pageInfo.getPageNum(), pageInfo.getPages());
    }

    @Override
    public List<Bed> 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 为例(完整实现,含选床向导和头像上传):

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<DormitoryBuilding> buildings =
            buildingService.selectByGender(user.getGender());
        model.addAttribute("buildings", buildings);

        if (buildingId != null) {
            List<Room> rooms = roomService.selectAvailableByBuildingId(buildingId);
            model.addAttribute("rooms", rooms);
            model.addAttribute("selectedBuildingId", buildingId);
        }
        if (roomId != null) {
            List<Bed> 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 为例:

<%@ 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" %>
<html>
<head>
    <title>我的住宿</title>
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
<jsp:include page="../common/header.jsp"/>
<div class="container">
    <div class="row">
        <div class="col-md-4">
            <div class="card">
                <div class="card-header">个人资料</div>
                <div class="card-body text-center">
                    <c:choose>
                        <c:when test="${not empty sessionScope.user.avatar}">
                            <img src="${pageContext.request.contextPath}${sessionScope.user.avatar}"
                                 class="rounded-circle mb-2"
                                 style="width:120px;height:120px;object-fit:cover">
                        </c:when>
                        <c:otherwise>
                            <div class="mb-2" style="width:120px;height:120px;border-radius:50%;
                                background:#e9ecef;display:flex;align-items:center;justify-content:center;
                                margin:0 auto;font-size:48px;color:#6c757d">
                                ${sessionScope.user.name.charAt(0)}
                            </div>
                        </c:otherwise>
                    </c:choose>
                    <h5>${sessionScope.user.name}</h5>
                    <p class="text-muted">${sessionScope.user.studentId}</p>
                    <form action="${pageContext.request.contextPath}/student/uploadAvatar"
                          method="post" enctype="multipart/form-data">
                        <div class="custom-file mb-2">
                            <input type="file" class="custom-file-input" name="file"
                                   accept="image/*" required>
                            <label class="custom-file-label">选择头像</label>
                        </div>
                        <button type="submit" class="btn btn-sm btn-outline-primary">上传</button>
                    </form>
                </div>
            </div>
        </div>
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">住宿信息</div>
                <div class="card-body">
                    <c:choose>
                        <c:when test="${not empty assignment}">
                            <table class="table table-bordered">
                                <tr><th>宿舍楼</th><td>${assignment.buildingName}</td></tr>
                                <tr><th>房间号</th><td>${assignment.roomNumber}</td></tr>
                                <tr><th>床位号</th><td>${assignment.bedNumber}</td></tr>
                                <tr><th>入住日期</th><td>${assignment.assignDate}</td></tr>
                            </table>
                        </c:when>
                        <c:otherwise>
                            <div class="alert alert-warning">
                                你还没有分配床位。
                                <a href="${pageContext.request.contextPath}/student/select"
                                   class="btn btn-primary btn-sm ml-2">去选床</a>
                            </div>
                        </c:otherwise>
                    </c:choose>
                </div>
            </div>
        </div>
    </div>
</div>
<script src="https://cdn.staticfile.org/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
</html>

3.4 核心技术亮点实现

3.4.1 登录拦截器

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 中注册:

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/login"/>
        <mvc:exclude-mapping path="/static/**"/>
        <bean class="space.anyi.interceptor.LoginInterceptor"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/admin/**"/>
        <bean class="space.anyi.interceptor.AuthInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

3.4.2 权限拦截器

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 插件:

<configuration>
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="helperDialect" value="mysql"/>
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
</configuration>

Service 层使用:

@Override
public PageResult<User> selectUserPage(int pageNum, int pageSize) {
    PageHelper.startPage(pageNum, pageSize);
    List<User> list = userMapper.selectAll();
    PageInfo<User> pageInfo = new PageInfo<>(list);
    return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(),
                            pageInfo.getPageNum(), pageInfo.getPages());
}

3.4.4 文件上传(学生头像)

Controller 层处理(取自 StudentController.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 中已有配置:

<bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

Service 实现类中使用 @Transactional 保证选床等操作的原子性。

3.5 技术难点与问题解决

问题 原因 解决方案
文件上传返回 null 未配置 multipartResolver Bean,或 <form> 缺少 enctype="multipart/form-data" 在 spring-mvc.xml 中声明 CommonsMultipartResolver,表单添加 enctype
分页查询总页数不准确 PageHelper 的 reasonable 参数未开启,页码超出范围时返回空数据 在 mybatis-config.xml 中设置 <property name="reasonable" value="true"/>
关联查询 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 语句,业务代码几乎零改动。