yangyi před 4 měsíci
rodič
revize
d169171cb4
3 změnil soubory, kde provedl 1584 přidání a 0 odebrání
  1. binární
      doc/1781679009307-Web应用开发-项目报告模板(1).docx
  2. 67 0
      doc/doc.md
  3. 1517 0
      doc/work.md

binární
doc/1781679009307-Web应用开发-项目报告模板(1).docx


+ 67 - 0
doc/doc.md

@@ -0,0 +1,67 @@
+1 实验内容
+1.1 项目背景与选题介绍
+简要介绍所选 Web 项目主题、应用场景、解决的实际问题、项目意义。
+1.2 项目开发目标与功能概述
+说明本项目需要实现的核心功能、技术目标、学习目标、整体业务流程。
+1.3 开发环境与技术栈介绍
+硬件 / 系统环境
+JDK、Tomcat、MySQL、Eclipse 开发工具
+核心技术:Spring、SpringMVC、MyBatis、JSP、Servlet、拦截器、数据校验、文件上传、事务控制等
+
+2 系统总体设计
+2.1 需求分析
+功能性需求、非功能性需求、用户操作流程说明。
+2.2 系统架构设计
+介绍 SSM 三层 / 五层架构、MVC 设计模式、整体架构图。
+2.3 数据库设计
+E-R 图设计
+数据表结构设计(字段、类型、约束、主键说明)
+数据库设计思路与规范说明
+2.4 功能模块设计
+分模块介绍:登录模块、数据 CRUD 模块、拦截器权限模块、文件上传模块等。
+
+3 项目开发实现过程
+3.1 项目搭建与框架整合
+详细说明:
+项目结构创建
+SSM 三大框架整合流程
+核心配置文件(spring、springmvc、mybatis、web.xml)配置说明
+3.2 数据库实现
+SQL 脚本编写、数据表创建、测试数据插入、数据库连接配置。
+3.3 业务功能代码实现
+分层说明:
+POJO 实体类设计
+DAO 层数据访问实现
+Service 层业务逻辑与事务控制
+Controller 请求处理与视图跳转
+JSP 前端页面实现、动态数据渲染
+3.4 核心技术亮点实现
+根据自己项目任选:
+表单数据校验实现
+SpringMVC 拦截器完整生命周期实现(登录拦截、权限控制)
+文件上传 / 下载功能实现
+Spring 事务管理实现
+分页 / 条件查询功能实现
+3.5 技术难点与问题解决
+记录开发过程中出现的 BUG、报错、兼容性问题、配置错误、解决方案与总结。
+
+4 系统测试与结果展示
+4.1 功能测试说明
+测试环境、测试方法、测试原则。
+4.2 核心功能运行截图与说明依次展示:
+项目启动成功截图
+登录验证功能截图
+数据增删改查截图
+拦截器权限拦截效果截图
+文件上传下载截图
+错误提示、校验提示效果截图
+4.3 系统整体运行效果分析
+说明项目整体稳定性、功能完整性、业务流程是否通顺。
+4.4 项目不足与优化方向
+分析当前项目缺陷、可优化点、后续扩展思路。
+
+5 总结与心得
+5.1 项目整体总结
+5.2 学习收获与团队分工总结
+5.3 SSM 框架开发经验总结
+

+ 1517 - 0
doc/work.md

@@ -0,0 +1,1517 @@
+# 宿舍管理系统设计与实现
+
+---
+
+## 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 页面<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 图
+
+```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[展示当前住宿信息<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 完整依赖**
+
+```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
+<?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
+<?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
+<?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** — 日志配置(控制台 + 滚动文件):
+
+```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
+<?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** — 数据库连接配置:
+
+```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<DormitoryBuilding> selectAll();
+    DormitoryBuilding selectById(Long id);
+    int insert(DormitoryBuilding building);
+    int update(DormitoryBuilding building);
+    int deleteById(Long id);
+}
+```
+
+`DormitoryBuildingMapper.xml`:
+
+```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` 为例,展示选床核心逻辑:
+
+```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`(完整实现,含分页查询和核心选床逻辑):
+
+```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` 为例(完整实现,含选床向导和头像上传):
+
+```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` 为例:
+
+```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 登录拦截器
+
+```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
+<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 权限拦截器
+
+```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
+<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 层使用:
+
+```java
+@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`):
+
+```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
+<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` 语句,业务代码几乎零改动。