|
@@ -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` 语句,业务代码几乎零改动。
|