Pārlūkot izejas kodu

学生宿舍管理系统

yangyi 4 mēneši atpakaļ
revīzija
f69838dfe3
62 mainītis faili ar 3449 papildinājumiem un 0 dzēšanām
  1. 48 0
      .gitignore
  2. 160 0
      pom.xml
  3. 117 0
      sql/init.sql
  4. 26 0
      src/main/java/space/anyi/common/PageResult.java
  5. 32 0
      src/main/java/space/anyi/common/Result.java
  6. 15 0
      src/main/java/space/anyi/controller/AdminController.java
  7. 78 0
      src/main/java/space/anyi/controller/AssignmentController.java
  8. 85 0
      src/main/java/space/anyi/controller/BedController.java
  9. 70 0
      src/main/java/space/anyi/controller/BuildingController.java
  10. 41 0
      src/main/java/space/anyi/controller/LoginController.java
  11. 81 0
      src/main/java/space/anyi/controller/RoomController.java
  12. 116 0
      src/main/java/space/anyi/controller/StudentController.java
  13. 69 0
      src/main/java/space/anyi/controller/UserController.java
  14. 48 0
      src/main/java/space/anyi/entity/Assignment.java
  15. 32 0
      src/main/java/space/anyi/entity/Bed.java
  16. 31 0
      src/main/java/space/anyi/entity/DormitoryBuilding.java
  17. 35 0
      src/main/java/space/anyi/entity/Room.java
  18. 40 0
      src/main/java/space/anyi/entity/User.java
  19. 23 0
      src/main/java/space/anyi/interceptor/AuthInterceptor.java
  20. 23 0
      src/main/java/space/anyi/interceptor/LoginInterceptor.java
  21. 17 0
      src/main/java/space/anyi/mapper/AssignmentMapper.java
  22. 19 0
      src/main/java/space/anyi/mapper/BedMapper.java
  23. 18 0
      src/main/java/space/anyi/mapper/DormitoryBuildingMapper.java
  24. 18 0
      src/main/java/space/anyi/mapper/RoomMapper.java
  25. 19 0
      src/main/java/space/anyi/mapper/UserMapper.java
  26. 14 0
      src/main/java/space/anyi/service/AssignmentService.java
  27. 17 0
      src/main/java/space/anyi/service/BedService.java
  28. 16 0
      src/main/java/space/anyi/service/BuildingService.java
  29. 15 0
      src/main/java/space/anyi/service/RoomService.java
  30. 15 0
      src/main/java/space/anyi/service/UserService.java
  31. 59 0
      src/main/java/space/anyi/service/impl/AssignmentServiceImpl.java
  32. 97 0
      src/main/java/space/anyi/service/impl/BedServiceImpl.java
  33. 68 0
      src/main/java/space/anyi/service/impl/BuildingServiceImpl.java
  34. 64 0
      src/main/java/space/anyi/service/impl/RoomServiceImpl.java
  35. 83 0
      src/main/java/space/anyi/service/impl/UserServiceImpl.java
  36. 5 0
      src/main/resources/jdbc.properties
  37. 168 0
      src/main/resources/logback.xml
  38. 100 0
      src/main/resources/mapper/AssignmentMapper.xml
  39. 91 0
      src/main/resources/mapper/BedMapper.xml
  40. 68 0
      src/main/resources/mapper/DormitoryBuildingMapper.xml
  41. 82 0
      src/main/resources/mapper/RoomMapper.xml
  42. 75 0
      src/main/resources/mapper/UserMapper.xml
  43. 15 0
      src/main/resources/mybatis-config.xml
  44. 51 0
      src/main/resources/spring-context.xml
  45. 43 0
      src/main/resources/spring-mvc.xml
  46. 90 0
      src/main/webapp/WEB-INF/views/admin/assignment_add.jsp
  47. 70 0
      src/main/webapp/WEB-INF/views/admin/assignment_list.jsp
  48. 46 0
      src/main/webapp/WEB-INF/views/admin/bed_edit.jsp
  49. 87 0
      src/main/webapp/WEB-INF/views/admin/bed_list.jsp
  50. 47 0
      src/main/webapp/WEB-INF/views/admin/building_edit.jsp
  51. 94 0
      src/main/webapp/WEB-INF/views/admin/building_list.jsp
  52. 20 0
      src/main/webapp/WEB-INF/views/admin/dashboard.jsp
  53. 52 0
      src/main/webapp/WEB-INF/views/admin/room_edit.jsp
  54. 88 0
      src/main/webapp/WEB-INF/views/admin/room_list.jsp
  55. 59 0
      src/main/webapp/WEB-INF/views/admin/user_edit.jsp
  56. 91 0
      src/main/webapp/WEB-INF/views/admin/user_list.jsp
  57. 35 0
      src/main/webapp/WEB-INF/views/common/header.jsp
  58. 14 0
      src/main/webapp/WEB-INF/views/error.jsp
  59. 40 0
      src/main/webapp/WEB-INF/views/login.jsp
  60. 80 0
      src/main/webapp/WEB-INF/views/student/dashboard.jsp
  61. 80 0
      src/main/webapp/WEB-INF/views/student/select_bed.jsp
  62. 49 0
      src/main/webapp/WEB-INF/web.xml

+ 48 - 0
.gitignore

@@ -0,0 +1,48 @@
+# Maven
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+.mvn/
+
+# IntelliJ IDEA
+.idea/
+*.iws
+*.iml
+*.ipr
+out/
+!.idea/**/out/artifacts/
+!.idea/**/out/production/
+
+# Eclipse
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+# NetBeans
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+# VS Code
+.vscode/
+
+# Mac OS
+.DS_Store
+
+# Logs
+*.log
+
+# Sensitive files
+*.properties
+!src/main/resources/*.properties
+/bin/

+ 160 - 0
pom.xml

@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>space.anyi</groupId>
+    <artifactId>dormitory_manage</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>war</packaging>
+    <description>SSM</description>
+
+    <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>
+        <javax.servlet.version>4.0.1</javax.servlet.version>
+        <javax.servlet.jsp.version>2.3.3</javax.servlet.jsp.version>
+        <jstl.version>1.2.2</jstl.version>
+        <mybatis.version>3.5.6</mybatis.version>
+        <mybatis-spring.version>2.0.6</mybatis-spring.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>
+
+        <!-- Spring Web MVC(如果你的项目是Web项目) -->
+        <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>
+        <!-- Spring 事务支持(必须!) -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-tx</artifactId>
+            <version>${spring.version}</version>
+        </dependency>
+
+        <!-- Spring AOP 支持(事务注解依赖) -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aop</artifactId>
+            <version>${spring.version}</version>
+        </dependency>
+        <!-- AspectJ(如果使用 @Transactional 需要) -->
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+            <version>1.9.7</version>
+        </dependency>
+
+        <!-- ========== JSTL(JSP 标准标签库) ========== -->
+        <dependency>
+            <groupId>javax.servlet.jsp.jstl</groupId>
+            <artifactId>javax.servlet.jsp.jstl-api</artifactId>
+            <version>${jstl.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.taglibs</groupId>
+            <artifactId>taglibs-standard-impl</artifactId>
+            <version>1.2.5</version>
+        </dependency>
+
+        <!-- ========== MyBatis 分页插件 ========== -->
+        <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>
+
+        <!-- ========== MyBatis 依赖 ========== -->
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis</artifactId>
+            <version>${mybatis.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis-spring</artifactId>
+            <version>${mybatis-spring.version}</version>
+        </dependency>
+
+        <!-- ========== 数据库驱动 ========== -->
+        <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>
+        <!-- ========== Servlet API(provided) ========== -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>4.0.1</version>
+            <scope>provided</scope>
+        </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>-->
+
+</project>

+ 117 - 0
sql/init.sql

@@ -0,0 +1,117 @@
+-- ============================================================
+-- 宿舍管理系统 - 数据库初始化脚本
+-- 数据库: dormitory_manage
+-- ============================================================
+
+CREATE DATABASE IF NOT EXISTS dormitory_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+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 COMMENT='用户表';
+
+-- ============================================================
+-- 宿舍楼表
+-- ============================================================
+CREATE TABLE dormitory_building (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    name VARCHAR(100) NOT NULL COMMENT '楼栋名称',
+    code VARCHAR(20) COMMENT '楼栋编号',
+    gender TINYINT DEFAULT 2 COMMENT '0-女, 1-男, 2-混合',
+    floor_count INT DEFAULT 6 COMMENT '楼层数',
+    description VARCHAR(500) COMMENT '备注',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宿舍楼表';
+
+-- ============================================================
+-- 房间表
+-- ============================================================
+CREATE TABLE room (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    building_id BIGINT NOT NULL COMMENT '所属楼栋',
+    room_number VARCHAR(20) NOT NULL COMMENT '房间号',
+    floor INT COMMENT '所在楼层',
+    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 COMMENT='房间表';
+
+-- ============================================================
+-- 床位表
+-- ============================================================
+CREATE TABLE bed (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    room_id BIGINT NOT NULL COMMENT '所属房间',
+    bed_number VARCHAR(10) NOT NULL COMMENT '床位编号',
+    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 COMMENT='床位表';
+
+-- ============================================================
+-- 分配记录表(中间表)
+-- ============================================================
+CREATE TABLE assignment (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '学生用户',
+    bed_id BIGINT NOT NULL COMMENT '分配的床位',
+    assign_date DATE NOT NULL COMMENT '入住日期',
+    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 COMMENT='分配记录表';
+
+-- ============================================================
+-- 测试数据
+-- ============================================================
+
+-- 管理员账号 (密码: admin123, MD5: 21232f297a57a5a743894a0e4a801fc3)
+INSERT INTO user (username, password, name, role) VALUES
+('admin', '21232f297a57a5a743894a0e4a801fc3', '系统管理员', 0);
+
+-- 学生账号 (密码: 123456, MD5: e10adc3949ba59abbe56e057f20f883e)
+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');
+
+-- 宿舍楼
+INSERT INTO dormitory_building (name, code, gender, floor_count) VALUES
+('梅苑A栋', 'MA', 1, 6),
+('兰苑B栋', 'LB', 0, 6),
+('竹苑C栋', 'ZC', 2, 5);
+
+-- 房间(梅苑A栋 101~103)
+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);

+ 26 - 0
src/main/java/space/anyi/common/PageResult.java

@@ -0,0 +1,26 @@
+package space.anyi.common;
+
+import java.util.List;
+
+public class PageResult<T> {
+    private List<T> list;
+    private long total;
+    private int pageNum;
+    private int pages;
+
+    public PageResult(List<T> list, long total, int pageNum, int pages) {
+        this.list = list;
+        this.total = total;
+        this.pageNum = pageNum;
+        this.pages = pages;
+    }
+
+    public List<T> getList() { return list; }
+    public void setList(List<T> list) { this.list = list; }
+    public long getTotal() { return total; }
+    public void setTotal(long total) { this.total = total; }
+    public int getPageNum() { return pageNum; }
+    public void setPageNum(int pageNum) { this.pageNum = pageNum; }
+    public int getPages() { return pages; }
+    public void setPages(int pages) { this.pages = pages; }
+}

+ 32 - 0
src/main/java/space/anyi/common/Result.java

@@ -0,0 +1,32 @@
+package space.anyi.common;
+
+public class Result<T> {
+    private int code;
+    private String msg;
+    private T data;
+
+    private Result(int code, String msg, T data) {
+        this.code = code;
+        this.msg = msg;
+        this.data = data;
+    }
+
+    public static <T> Result<T> success(T data) {
+        return new Result<>(200, "success", data);
+    }
+
+    public static <T> Result<T> success() {
+        return new Result<>(200, "success", null);
+    }
+
+    public static <T> Result<T> error(String msg) {
+        return new Result<>(500, msg, null);
+    }
+
+    public int getCode() { return code; }
+    public void setCode(int code) { this.code = code; }
+    public String getMsg() { return msg; }
+    public void setMsg(String msg) { this.msg = msg; }
+    public T getData() { return data; }
+    public void setData(T data) { this.data = data; }
+}

+ 15 - 0
src/main/java/space/anyi/controller/AdminController.java

@@ -0,0 +1,15 @@
+package space.anyi.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+@RequestMapping("/admin")
+public class AdminController {
+
+    @GetMapping("/dashboard")
+    public String dashboard() {
+        return "admin/dashboard";
+    }
+}

+ 78 - 0
src/main/java/space/anyi/controller/AssignmentController.java

@@ -0,0 +1,78 @@
+package space.anyi.controller;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.*;
+import space.anyi.service.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+import java.util.List;
+
+@Controller
+@RequestMapping("/admin/assignment")
+public class AssignmentController {
+
+    @Autowired
+    private AssignmentService assignmentService;
+    @Autowired
+    private BuildingService buildingService;
+    @Autowired
+    private RoomService roomService;
+    @Autowired
+    private BedService bedService;
+
+    @GetMapping("/list")
+    public String list(@RequestParam(defaultValue = "1") int page,
+                       @RequestParam(defaultValue = "10") int size,
+                       Model model) {
+        PageResult<Assignment> pageResult = assignmentService.selectPage(page, size);
+        model.addAttribute("pageResult", pageResult);
+        return "admin/assignment_list";
+    }
+
+    @GetMapping("/add")
+    public String addPage(@RequestParam(required = false) Long buildingId,
+                          @RequestParam(required = false) Long roomId,
+                          Model model) {
+        List<User> students = assignmentService.selectStudentsWithoutAssignment();
+        List<DormitoryBuilding> buildings = buildingService.selectByGender(null);
+        model.addAttribute("students", students);
+        model.addAttribute("buildings", buildings);
+
+        if (buildingId != null) {
+            List<Room> rooms = roomService.selectPage(1, 999).getList();
+            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 "admin/assignment_add";
+    }
+
+    @PostMapping("/save")
+    public String save(Long userId, Long bedId, RedirectAttributes ra) {
+        try {
+            bedService.assignBed(userId, bedId);
+            ra.addFlashAttribute("success", "分配成功");
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", e.getMessage());
+        }
+        return "redirect:/admin/assignment/list";
+    }
+
+    @GetMapping("/end")
+    public String end(Long id, RedirectAttributes ra) {
+        try {
+            assignmentService.endAssignment(id);
+            ra.addFlashAttribute("success", "退宿成功");
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", e.getMessage());
+        }
+        return "redirect:/admin/assignment/list";
+    }
+}

+ 85 - 0
src/main/java/space/anyi/controller/BedController.java

@@ -0,0 +1,85 @@
+package space.anyi.controller;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.Bed;
+import space.anyi.entity.DormitoryBuilding;
+import space.anyi.entity.Room;
+import space.anyi.service.BedService;
+import space.anyi.service.BuildingService;
+import space.anyi.service.RoomService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+import java.util.List;
+
+@Controller
+@RequestMapping("/admin/bed")
+public class BedController {
+
+    @Autowired
+    private BedService bedService;
+    @Autowired
+    private BuildingService buildingService;
+    @Autowired
+    private RoomService roomService;
+
+    @GetMapping("/list")
+    public String list(@RequestParam(required = false) Long buildingId,
+                       @RequestParam(required = false) String roomNumber,
+                       @RequestParam(defaultValue = "1") int page,
+                       @RequestParam(defaultValue = "10") int size,
+                       Model model) {
+        PageResult<Bed> pageResult = bedService.searchPage(buildingId, roomNumber, page, size);
+        List<DormitoryBuilding> buildings = buildingService.selectByGender(null);
+        model.addAttribute("pageResult", pageResult);
+        model.addAttribute("buildings", buildings);
+        model.addAttribute("buildingId", buildingId);
+        model.addAttribute("roomNumber", roomNumber);
+        return "admin/bed_list";
+    }
+
+    @GetMapping("/add")
+    public String addPage(Model model) {
+        List<Room> rooms = roomService.selectPage(1, 999).getList();
+        model.addAttribute("rooms", rooms);
+        return "admin/bed_edit";
+    }
+
+    @GetMapping("/edit")
+    public String editPage(Long id, Model model) {
+        Bed bed = bedService.selectById(id);
+        List<Room> rooms = roomService.selectPage(1, 999).getList();
+        model.addAttribute("bed", bed);
+        model.addAttribute("rooms", rooms);
+        return "admin/bed_edit";
+    }
+
+    @PostMapping("/save")
+    public String save(Bed bed, RedirectAttributes ra) {
+        try {
+            if (bed.getId() == null) {
+                if (bed.getStatus() == null) {
+                    bed.setStatus(0);
+                }
+                bedService.addBed(bed);
+            } else {
+                bedService.updateBed(bed);
+            }
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", "保存失败:该房间下床位编号已存在");
+        }
+        return "redirect:/admin/bed/list";
+    }
+
+    @GetMapping("/delete")
+    public String delete(Long id, RedirectAttributes ra) {
+        try {
+            bedService.deleteBed(id);
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", e.getMessage());
+        }
+        return "redirect:/admin/bed/list";
+    }
+}

+ 70 - 0
src/main/java/space/anyi/controller/BuildingController.java

@@ -0,0 +1,70 @@
+package space.anyi.controller;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.DormitoryBuilding;
+import space.anyi.service.BuildingService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+@Controller
+@RequestMapping("/admin/building")
+public class BuildingController {
+
+    @Autowired
+    private BuildingService buildingService;
+
+    @GetMapping("/list")
+    public String list(@RequestParam(defaultValue = "1") int page,
+                       @RequestParam(defaultValue = "10") int size,
+                       @RequestParam(required = false) String name,
+                       @RequestParam(required = false) String code,
+                       @RequestParam(required = false) Integer gender,
+                       Model model) {
+        PageResult<DormitoryBuilding> pageResult =
+                buildingService.searchPage(name, code, gender, page, size);
+        model.addAttribute("pageResult", pageResult);
+        model.addAttribute("name", name);
+        model.addAttribute("code", code);
+        model.addAttribute("gender", gender);
+        return "admin/building_list";
+    }
+
+    @GetMapping("/add")
+    public String addPage() {
+        return "admin/building_edit";
+    }
+
+    @GetMapping("/edit")
+    public String editPage(Long id, Model model) {
+        DormitoryBuilding building = buildingService.selectById(id);
+        model.addAttribute("building", building);
+        return "admin/building_edit";
+    }
+
+    @PostMapping("/save")
+    public String save(DormitoryBuilding building, RedirectAttributes ra) {
+        try {
+            if (building.getId() == null) {
+                buildingService.addBuilding(building);
+            } else {
+                buildingService.updateBuilding(building);
+            }
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", "保存失败:数据重复或存在异常");
+        }
+        return "redirect:/admin/building/list";
+    }
+
+    @GetMapping("/delete")
+    public String delete(Long id, RedirectAttributes ra) {
+        try {
+            buildingService.deleteBuilding(id);
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", e.getMessage());
+        }
+        return "redirect:/admin/building/list";
+    }
+}

+ 41 - 0
src/main/java/space/anyi/controller/LoginController.java

@@ -0,0 +1,41 @@
+package space.anyi.controller;
+
+import space.anyi.entity.User;
+import space.anyi.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import javax.servlet.http.HttpSession;
+
+@Controller
+public class LoginController {
+
+    @Autowired
+    private UserService userService;
+
+    @GetMapping("/login")
+    public String loginPage() {
+        return "login";
+    }
+
+    @PostMapping("/login")
+    public String login(String username, String password, HttpSession session) {
+        User user = userService.login(username, password);
+        if (user == null) {
+            return "redirect:/login?error=true";
+        }
+        session.setAttribute("user", user);
+        if (user.getRole() == 0) {
+            return "redirect:/admin/dashboard";
+        } else {
+            return "redirect:/student/dashboard";
+        }
+    }
+
+    @GetMapping("/logout")
+    public String logout(HttpSession session) {
+        session.invalidate();
+        return "redirect:/login";
+    }
+}

+ 81 - 0
src/main/java/space/anyi/controller/RoomController.java

@@ -0,0 +1,81 @@
+package space.anyi.controller;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.DormitoryBuilding;
+import space.anyi.entity.Room;
+import space.anyi.service.BuildingService;
+import space.anyi.service.RoomService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+import java.util.List;
+
+@Controller
+@RequestMapping("/admin/room")
+public class RoomController {
+
+    @Autowired
+    private RoomService roomService;
+    @Autowired
+    private BuildingService buildingService;
+
+    @GetMapping("/list")
+    public String list(@RequestParam(required = false) Long buildingId,
+                       @RequestParam(required = false) Integer floor,
+                       @RequestParam(defaultValue = "1") int page,
+                       @RequestParam(defaultValue = "10") int size,
+                       Model model) {
+        PageResult<Room> pageResult = roomService.searchPage(buildingId, floor, page, size);
+        List<DormitoryBuilding> buildings = buildingService.selectByGender(null);
+        model.addAttribute("pageResult", pageResult);
+        model.addAttribute("buildings", buildings);
+        model.addAttribute("buildingId", buildingId);
+        model.addAttribute("floor", floor);
+        return "admin/room_list";
+    }
+
+    @GetMapping("/add")
+    public String addPage(Model model) {
+        List<DormitoryBuilding> buildings = buildingService.selectByGender(null);
+        model.addAttribute("buildings", buildings);
+        return "admin/room_edit";
+    }
+
+    @GetMapping("/edit")
+    public String editPage(Long id, Model model) {
+        Room room = roomService.selectById(id);
+        List<DormitoryBuilding> buildings = buildingService.selectByGender(null);
+        model.addAttribute("room", room);
+        model.addAttribute("buildings", buildings);
+        return "admin/room_edit";
+    }
+
+    @PostMapping("/save")
+    public String save(Room room, RedirectAttributes ra) {
+        try {
+            if (room.getGender() != null && room.getGender() == -1) {
+                room.setGender(null);
+            }
+            if (room.getId() == null) {
+                roomService.addRoom(room);
+            } else {
+                roomService.updateRoom(room);
+            }
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", "保存失败:该楼栋下房间号已存在");
+        }
+        return "redirect:/admin/room/list";
+    }
+
+    @GetMapping("/delete")
+    public String delete(Long id, RedirectAttributes ra) {
+        try {
+            roomService.deleteRoom(id);
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", e.getMessage());
+        }
+        return "redirect:/admin/room/list";
+    }
+}

+ 116 - 0
src/main/java/space/anyi/controller/StudentController.java

@@ -0,0 +1,116 @@
+package space.anyi.controller;
+
+import space.anyi.entity.*;
+import space.anyi.service.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import javax.servlet.http.HttpSession;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+@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()) {
+            return "redirect:/student/dashboard?error=empty";
+        }
+
+        String contentType = file.getContentType();
+        if (contentType == null || !contentType.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 = "";
+        if (originalName != null && originalName.contains(".")) {
+            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";
+    }
+}

+ 69 - 0
src/main/java/space/anyi/controller/UserController.java

@@ -0,0 +1,69 @@
+package space.anyi.controller;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.User;
+import space.anyi.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+@Controller
+@RequestMapping("/admin/user")
+public class UserController {
+
+    @Autowired
+    private UserService userService;
+
+    @GetMapping("/list")
+    public String list(@RequestParam(defaultValue = "1") int page,
+                       @RequestParam(defaultValue = "10") int size,
+                       @RequestParam(required = false) String name,
+                       @RequestParam(required = false) Integer gender,
+                       @RequestParam(required = false) String studentId,
+                       Model model) {
+        PageResult<User> pageResult = userService.searchPage(name, gender, studentId, page, size);
+        model.addAttribute("pageResult", pageResult);
+        model.addAttribute("name", name);
+        model.addAttribute("gender", gender);
+        model.addAttribute("studentId", studentId);
+        return "admin/user_list";
+    }
+
+    @GetMapping("/add")
+    public String addPage() {
+        return "admin/user_edit";
+    }
+
+    @GetMapping("/edit")
+    public String editPage(Long id, Model model) {
+        User user = userService.selectById(id);
+        model.addAttribute("user", user);
+        return "admin/user_edit";
+    }
+
+    @PostMapping("/save")
+    public String save(User user, RedirectAttributes ra) {
+        try {
+            if (user.getId() == null) {
+                userService.addUser(user);
+            } else {
+                userService.updateUser(user);
+            }
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", "保存失败:用户名或学号已存在");
+        }
+        return "redirect:/admin/user/list";
+    }
+
+    @GetMapping("/delete")
+    public String delete(Long id, RedirectAttributes ra) {
+        try {
+            userService.deleteUser(id);
+        } catch (RuntimeException e) {
+            ra.addFlashAttribute("error", e.getMessage());
+        }
+        return "redirect:/admin/user/list";
+    }
+}

+ 48 - 0
src/main/java/space/anyi/entity/Assignment.java

@@ -0,0 +1,48 @@
+package space.anyi.entity;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+public class Assignment {
+    private Long id;
+    private Long userId;
+    private Long bedId;
+    private LocalDate assignDate;
+    private LocalDate endDate;
+    private Integer status;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    private String userName;
+    private String studentId;
+    private String buildingName;
+    private String roomNumber;
+    private String bedNumber;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public Long getBedId() { return bedId; }
+    public void setBedId(Long bedId) { this.bedId = bedId; }
+    public LocalDate getAssignDate() { return assignDate; }
+    public void setAssignDate(LocalDate assignDate) { this.assignDate = assignDate; }
+    public LocalDate getEndDate() { return endDate; }
+    public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
+    public Integer getStatus() { return status; }
+    public void setStatus(Integer status) { this.status = status; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+    public String getUserName() { return userName; }
+    public void setUserName(String userName) { this.userName = userName; }
+    public String getStudentId() { return studentId; }
+    public void setStudentId(String studentId) { this.studentId = studentId; }
+    public String getBuildingName() { return buildingName; }
+    public void setBuildingName(String buildingName) { this.buildingName = buildingName; }
+    public String getRoomNumber() { return roomNumber; }
+    public void setRoomNumber(String roomNumber) { this.roomNumber = roomNumber; }
+    public String getBedNumber() { return bedNumber; }
+    public void setBedNumber(String bedNumber) { this.bedNumber = bedNumber; }
+}

+ 32 - 0
src/main/java/space/anyi/entity/Bed.java

@@ -0,0 +1,32 @@
+package space.anyi.entity;
+
+import java.time.LocalDateTime;
+
+public class Bed {
+    private Long id;
+    private Long roomId;
+    private String bedNumber;
+    private Integer status;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    private String roomNumber;
+    private String buildingName;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getRoomId() { return roomId; }
+    public void setRoomId(Long roomId) { this.roomId = roomId; }
+    public String getBedNumber() { return bedNumber; }
+    public void setBedNumber(String bedNumber) { this.bedNumber = bedNumber; }
+    public Integer getStatus() { return status; }
+    public void setStatus(Integer status) { this.status = status; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+    public String getRoomNumber() { return roomNumber; }
+    public void setRoomNumber(String roomNumber) { this.roomNumber = roomNumber; }
+    public String getBuildingName() { return buildingName; }
+    public void setBuildingName(String buildingName) { this.buildingName = buildingName; }
+}

+ 31 - 0
src/main/java/space/anyi/entity/DormitoryBuilding.java

@@ -0,0 +1,31 @@
+package space.anyi.entity;
+
+import java.time.LocalDateTime;
+
+public class DormitoryBuilding {
+    private Long id;
+    private String name;
+    private String code;
+    private Integer gender;
+    private Integer floorCount;
+    private String description;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public String getCode() { return code; }
+    public void setCode(String code) { this.code = code; }
+    public Integer getGender() { return gender; }
+    public void setGender(Integer gender) { this.gender = gender; }
+    public Integer getFloorCount() { return floorCount; }
+    public void setFloorCount(Integer floorCount) { this.floorCount = floorCount; }
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 35 - 0
src/main/java/space/anyi/entity/Room.java

@@ -0,0 +1,35 @@
+package space.anyi.entity;
+
+import java.time.LocalDateTime;
+
+public class Room {
+    private Long id;
+    private Long buildingId;
+    private String roomNumber;
+    private Integer floor;
+    private Integer capacity;
+    private Integer gender;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    private String buildingName;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getBuildingId() { return buildingId; }
+    public void setBuildingId(Long buildingId) { this.buildingId = buildingId; }
+    public String getRoomNumber() { return roomNumber; }
+    public void setRoomNumber(String roomNumber) { this.roomNumber = roomNumber; }
+    public Integer getFloor() { return floor; }
+    public void setFloor(Integer floor) { this.floor = floor; }
+    public Integer getCapacity() { return capacity; }
+    public void setCapacity(Integer capacity) { this.capacity = capacity; }
+    public Integer getGender() { return gender; }
+    public void setGender(Integer gender) { this.gender = gender; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+    public String getBuildingName() { return buildingName; }
+    public void setBuildingName(String buildingName) { this.buildingName = buildingName; }
+}

+ 40 - 0
src/main/java/space/anyi/entity/User.java

@@ -0,0 +1,40 @@
+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;
+    private String studentId;
+    private String avatar;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public String getUsername() { return username; }
+    public void setUsername(String username) { this.username = username; }
+    public String getPassword() { return password; }
+    public void setPassword(String password) { this.password = password; }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public Integer getGender() { return gender; }
+    public void setGender(Integer gender) { this.gender = gender; }
+    public String getPhone() { return phone; }
+    public void setPhone(String phone) { this.phone = phone; }
+    public Integer getRole() { return role; }
+    public void setRole(Integer role) { this.role = role; }
+    public String getStudentId() { return studentId; }
+    public void setStudentId(String studentId) { this.studentId = studentId; }
+    public String getAvatar() { return avatar; }
+    public void setAvatar(String avatar) { this.avatar = avatar; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 23 - 0
src/main/java/space/anyi/interceptor/AuthInterceptor.java

@@ -0,0 +1,23 @@
+package space.anyi.interceptor;
+
+import space.anyi.entity.User;
+import org.springframework.web.servlet.HandlerInterceptor;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+public class AuthInterceptor 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 || user.getRole() != 0) {
+            response.sendRedirect(request.getContextPath() + "/login?error=unauthorized");
+            return false;
+        }
+        return true;
+    }
+}

+ 23 - 0
src/main/java/space/anyi/interceptor/LoginInterceptor.java

@@ -0,0 +1,23 @@
+package space.anyi.interceptor;
+
+import space.anyi.entity.User;
+import org.springframework.web.servlet.HandlerInterceptor;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+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;
+    }
+}

+ 17 - 0
src/main/java/space/anyi/mapper/AssignmentMapper.java

@@ -0,0 +1,17 @@
+package space.anyi.mapper;
+
+import space.anyi.entity.Assignment;
+import space.anyi.entity.User;
+import java.util.List;
+
+public interface AssignmentMapper {
+    List<Assignment> selectAll();
+    Assignment selectById(Long id);
+    Assignment selectActiveByUserId(Long userId);
+    Assignment selectActiveByBedId(Long bedId);
+    int insert(Assignment assignment);
+    int update(Assignment assignment);
+    int endAssignment(Long id);
+    int deleteById(Long id);
+    List<User> selectStudentsWithoutAssignment();
+}

+ 19 - 0
src/main/java/space/anyi/mapper/BedMapper.java

@@ -0,0 +1,19 @@
+package space.anyi.mapper;
+
+import space.anyi.entity.Bed;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+public interface BedMapper {
+    List<Bed> selectAll();
+    List<Bed> selectByCondition(@Param("buildingId") Long buildingId,
+                                @Param("roomNumber") String roomNumber);
+    List<Bed> selectByRoomId(Long roomId);
+    List<Bed> selectAvailableByRoomId(Long roomId);
+    Bed selectById(Long id);
+    int insert(Bed bed);
+    int update(Bed bed);
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+    int deleteById(Long id);
+    int countActiveAssignmentsByBedId(Long bedId);
+}

+ 18 - 0
src/main/java/space/anyi/mapper/DormitoryBuildingMapper.java

@@ -0,0 +1,18 @@
+package space.anyi.mapper;
+
+import space.anyi.entity.DormitoryBuilding;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+public interface DormitoryBuildingMapper {
+    List<DormitoryBuilding> selectAll();
+    List<DormitoryBuilding> selectByCondition(@Param("name") String name,
+                                              @Param("code") String code,
+                                              @Param("gender") Integer gender);
+    List<DormitoryBuilding> selectByGender(Integer gender);
+    DormitoryBuilding selectById(Long id);
+    int insert(DormitoryBuilding building);
+    int update(DormitoryBuilding building);
+    int deleteById(Long id);
+    int countRoomsByBuildingId(Long buildingId);
+}

+ 18 - 0
src/main/java/space/anyi/mapper/RoomMapper.java

@@ -0,0 +1,18 @@
+package space.anyi.mapper;
+
+import space.anyi.entity.Room;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+public interface RoomMapper {
+    List<Room> selectAll();
+    List<Room> selectByCondition(@Param("buildingId") Long buildingId,
+                                 @Param("floor") Integer floor);
+    List<Room> selectByBuildingId(Long buildingId);
+    List<Room> selectAvailableByBuildingId(Long buildingId);
+    Room selectById(Long id);
+    int insert(Room room);
+    int update(Room room);
+    int deleteById(Long id);
+    int countBedsByRoomId(Long roomId);
+}

+ 19 - 0
src/main/java/space/anyi/mapper/UserMapper.java

@@ -0,0 +1,19 @@
+package space.anyi.mapper;
+
+import space.anyi.entity.User;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+public interface UserMapper {
+    List<User> selectAll();
+    List<User> selectByCondition(@Param("name") String name,
+                                 @Param("gender") Integer gender,
+                                 @Param("studentId") String studentId);
+    User selectById(Long id);
+    User selectByUsername(String username);
+    int insert(User user);
+    int update(User user);
+    int updateAvatar(User user);
+    int deleteById(Long id);
+    int countActiveAssignmentsByUserId(Long userId);
+}

+ 14 - 0
src/main/java/space/anyi/service/AssignmentService.java

@@ -0,0 +1,14 @@
+package space.anyi.service;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.Assignment;
+import space.anyi.entity.User;
+import java.util.List;
+
+public interface AssignmentService {
+    PageResult<Assignment> selectPage(int pageNum, int pageSize);
+    Assignment selectById(Long id);
+    Assignment selectActiveByUserId(Long userId);
+    int endAssignment(Long id);
+    List<User> selectStudentsWithoutAssignment();
+}

+ 17 - 0
src/main/java/space/anyi/service/BedService.java

@@ -0,0 +1,17 @@
+package space.anyi.service;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.Assignment;
+import space.anyi.entity.Bed;
+import java.util.List;
+
+public interface BedService {
+    PageResult<Bed> selectPage(int pageNum, int pageSize);
+    PageResult<Bed> searchPage(Long buildingId, String roomNumber, int pageNum, int pageSize);
+    List<Bed> selectAvailableByRoomId(Long roomId);
+    Bed selectById(Long id);
+    int addBed(Bed bed);
+    int updateBed(Bed bed);
+    int deleteBed(Long id);
+    Assignment assignBed(Long userId, Long bedId);
+}

+ 16 - 0
src/main/java/space/anyi/service/BuildingService.java

@@ -0,0 +1,16 @@
+package space.anyi.service;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.DormitoryBuilding;
+import java.util.List;
+
+public interface BuildingService {
+    PageResult<DormitoryBuilding> selectPage(int pageNum, int pageSize);
+    PageResult<DormitoryBuilding> searchPage(String name, String code, Integer gender,
+                                             int pageNum, int pageSize);
+    List<DormitoryBuilding> selectByGender(Integer gender);
+    DormitoryBuilding selectById(Long id);
+    int addBuilding(DormitoryBuilding building);
+    int updateBuilding(DormitoryBuilding building);
+    int deleteBuilding(Long id);
+}

+ 15 - 0
src/main/java/space/anyi/service/RoomService.java

@@ -0,0 +1,15 @@
+package space.anyi.service;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.Room;
+import java.util.List;
+
+public interface RoomService {
+    PageResult<Room> selectPage(int pageNum, int pageSize);
+    PageResult<Room> searchPage(Long buildingId, Integer floor, int pageNum, int pageSize);
+    List<Room> selectAvailableByBuildingId(Long buildingId);
+    Room selectById(Long id);
+    int addRoom(Room room);
+    int updateRoom(Room room);
+    int deleteRoom(Long id);
+}

+ 15 - 0
src/main/java/space/anyi/service/UserService.java

@@ -0,0 +1,15 @@
+package space.anyi.service;
+
+import space.anyi.common.PageResult;
+import space.anyi.entity.User;
+
+public interface UserService {
+    User login(String username, String password);
+    PageResult<User> selectPage(int pageNum, int pageSize);
+    PageResult<User> searchPage(String name, Integer gender, String studentId, int pageNum, int pageSize);
+    User selectById(Long id);
+    int addUser(User user);
+    int updateUser(User user);
+    int updateAvatar(Long id, String avatar);
+    int deleteUser(Long id);
+}

+ 59 - 0
src/main/java/space/anyi/service/impl/AssignmentServiceImpl.java

@@ -0,0 +1,59 @@
+package space.anyi.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import space.anyi.common.PageResult;
+import space.anyi.entity.Assignment;
+import space.anyi.entity.User;
+import space.anyi.mapper.AssignmentMapper;
+import space.anyi.mapper.BedMapper;
+import space.anyi.service.AssignmentService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@Service
+public class AssignmentServiceImpl implements AssignmentService {
+
+    @Autowired
+    private AssignmentMapper assignmentMapper;
+
+    @Autowired
+    private BedMapper bedMapper;
+
+    @Override
+    public PageResult<Assignment> selectPage(int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<Assignment> list = assignmentMapper.selectAll();
+        PageInfo<Assignment> pageInfo = new PageInfo<>(list);
+        return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(),
+                pageInfo.getPageNum(), pageInfo.getPages());
+    }
+
+    @Override
+    public Assignment selectById(Long id) {
+        return assignmentMapper.selectById(id);
+    }
+
+    @Override
+    public Assignment selectActiveByUserId(Long userId) {
+        return assignmentMapper.selectActiveByUserId(userId);
+    }
+
+    @Override
+    @Transactional
+    public int endAssignment(Long id) {
+        Assignment assignment = assignmentMapper.selectById(id);
+        if (assignment == null) {
+            throw new RuntimeException("分配记录不存在");
+        }
+        assignmentMapper.endAssignment(id);
+        return bedMapper.updateStatus(assignment.getBedId(), 0);
+    }
+
+    @Override
+    public List<User> selectStudentsWithoutAssignment() {
+        return assignmentMapper.selectStudentsWithoutAssignment();
+    }
+}

+ 97 - 0
src/main/java/space/anyi/service/impl/BedServiceImpl.java

@@ -0,0 +1,97 @@
+package space.anyi.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import space.anyi.common.PageResult;
+import space.anyi.entity.Assignment;
+import space.anyi.entity.Bed;
+import space.anyi.mapper.AssignmentMapper;
+import space.anyi.mapper.BedMapper;
+import space.anyi.service.BedService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDate;
+import java.util.List;
+
+@Service
+public class BedServiceImpl implements BedService {
+
+    @Autowired
+    private BedMapper bedMapper;
+
+    @Autowired
+    private AssignmentMapper assignmentMapper;
+
+    @Override
+    public PageResult<Bed> selectPage(int pageNum, int pageSize) {
+        return searchPage(null, null, pageNum, pageSize);
+    }
+
+    @Override
+    public PageResult<Bed> searchPage(Long buildingId, String roomNumber,
+                                      int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<Bed> list = bedMapper.selectByCondition(buildingId, roomNumber);
+        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) {
+        return bedMapper.insert(bed);
+    }
+
+    @Override
+    @Transactional
+    public int updateBed(Bed bed) {
+        return bedMapper.update(bed);
+    }
+
+    @Override
+    @Transactional
+    public int deleteBed(Long id) {
+        if (bedMapper.countActiveAssignmentsByBedId(id) > 0) {
+            throw new RuntimeException("该床位当前有人入住,无法删除");
+        }
+        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;
+    }
+}

+ 68 - 0
src/main/java/space/anyi/service/impl/BuildingServiceImpl.java

@@ -0,0 +1,68 @@
+package space.anyi.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import space.anyi.common.PageResult;
+import space.anyi.entity.DormitoryBuilding;
+import space.anyi.mapper.DormitoryBuildingMapper;
+import space.anyi.service.BuildingService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@Service
+public class BuildingServiceImpl implements BuildingService {
+
+    @Autowired
+    private DormitoryBuildingMapper buildingMapper;
+
+    @Override
+    public PageResult<DormitoryBuilding> selectPage(int pageNum, int pageSize) {
+        return searchPage(null, null, null, pageNum, pageSize);
+    }
+
+    @Override
+    public PageResult<DormitoryBuilding> searchPage(String name, String code, Integer gender,
+                                                    int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<DormitoryBuilding> list = buildingMapper.selectByCondition(name, code, gender);
+        PageInfo<DormitoryBuilding> pageInfo = new PageInfo<>(list);
+        return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(),
+                pageInfo.getPageNum(), pageInfo.getPages());
+    }
+
+    @Override
+    public List<DormitoryBuilding> selectByGender(Integer gender) {
+        if (gender == null) {
+            return buildingMapper.selectAll();
+        }
+        return buildingMapper.selectByGender(gender);
+    }
+
+    @Override
+    public DormitoryBuilding selectById(Long id) {
+        return buildingMapper.selectById(id);
+    }
+
+    @Override
+    @Transactional
+    public int addBuilding(DormitoryBuilding building) {
+        return buildingMapper.insert(building);
+    }
+
+    @Override
+    @Transactional
+    public int updateBuilding(DormitoryBuilding building) {
+        return buildingMapper.update(building);
+    }
+
+    @Override
+    @Transactional
+    public int deleteBuilding(Long id) {
+        if (buildingMapper.countRoomsByBuildingId(id) > 0) {
+            throw new RuntimeException("该宿舍楼下还有房间,无法删除");
+        }
+        return buildingMapper.deleteById(id);
+    }
+}

+ 64 - 0
src/main/java/space/anyi/service/impl/RoomServiceImpl.java

@@ -0,0 +1,64 @@
+package space.anyi.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import space.anyi.common.PageResult;
+import space.anyi.entity.Room;
+import space.anyi.mapper.RoomMapper;
+import space.anyi.service.RoomService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@Service
+public class RoomServiceImpl implements RoomService {
+
+    @Autowired
+    private RoomMapper roomMapper;
+
+    @Override
+    public PageResult<Room> selectPage(int pageNum, int pageSize) {
+        return searchPage(null, null, pageNum, pageSize);
+    }
+
+    @Override
+    public PageResult<Room> searchPage(Long buildingId, Integer floor, int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<Room> list = roomMapper.selectByCondition(buildingId, floor);
+        PageInfo<Room> pageInfo = new PageInfo<>(list);
+        return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(),
+                pageInfo.getPageNum(), pageInfo.getPages());
+    }
+
+    @Override
+    public List<Room> selectAvailableByBuildingId(Long buildingId) {
+        return roomMapper.selectAvailableByBuildingId(buildingId);
+    }
+
+    @Override
+    public Room selectById(Long id) {
+        return roomMapper.selectById(id);
+    }
+
+    @Override
+    @Transactional
+    public int addRoom(Room room) {
+        return roomMapper.insert(room);
+    }
+
+    @Override
+    @Transactional
+    public int updateRoom(Room room) {
+        return roomMapper.update(room);
+    }
+
+    @Override
+    @Transactional
+    public int deleteRoom(Long id) {
+        if (roomMapper.countBedsByRoomId(id) > 0) {
+            throw new RuntimeException("该房间下还有床位,无法删除");
+        }
+        return roomMapper.deleteById(id);
+    }
+}

+ 83 - 0
src/main/java/space/anyi/service/impl/UserServiceImpl.java

@@ -0,0 +1,83 @@
+package space.anyi.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import space.anyi.common.PageResult;
+import space.anyi.entity.User;
+import space.anyi.mapper.UserMapper;
+import space.anyi.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.DigestUtils;
+import java.util.List;
+
+@Service
+public class UserServiceImpl implements UserService {
+
+    @Autowired
+    private UserMapper userMapper;
+
+    @Override
+    public User login(String username, String password) {
+        User user = userMapper.selectByUsername(username);
+        if (user != null) {
+            String hashed = DigestUtils.md5DigestAsHex(password.getBytes());
+            if (user.getPassword().equals(hashed)) {
+                return user;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public PageResult<User> selectPage(int pageNum, int pageSize) {
+        return searchPage(null, null, null, pageNum, pageSize);
+    }
+
+    @Override
+    public PageResult<User> searchPage(String name, Integer gender, String studentId,
+                                       int pageNum, int pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        List<User> list = userMapper.selectByCondition(name, gender, studentId);
+        PageInfo<User> pageInfo = new PageInfo<>(list);
+        return new PageResult<>(pageInfo.getList(), pageInfo.getTotal(),
+                pageInfo.getPageNum(), pageInfo.getPages());
+    }
+
+    @Override
+    public User selectById(Long id) {
+        return userMapper.selectById(id);
+    }
+
+    @Override
+    @Transactional
+    public int addUser(User user) {
+        user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
+        return userMapper.insert(user);
+    }
+
+    @Override
+    @Transactional
+    public int updateUser(User user) {
+        return userMapper.update(user);
+    }
+
+    @Override
+    @Transactional
+    public int updateAvatar(Long id, String avatar) {
+        User user = new User();
+        user.setId(id);
+        user.setAvatar(avatar);
+        return userMapper.updateAvatar(user);
+    }
+
+    @Override
+    @Transactional
+    public int deleteUser(Long id) {
+        if (userMapper.countActiveAssignmentsByUserId(id) > 0) {
+            throw new RuntimeException("该用户当前正在住宿,无法删除");
+        }
+        return userMapper.deleteById(id);
+    }
+}

+ 5 - 0
src/main/resources/jdbc.properties

@@ -0,0 +1,5 @@
+# MySQL 8.x ??
+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

+ 168 - 0
src/main/resources/logback.xml

@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+
+    <!--
+        配置说明:
+        - scan="true": 配置文件发生变化时自动重新加载
+        - scanPeriod: 扫描间隔时间
+        - debug: 是否打印 logback 内部日志
+    -->
+
+    <!-- ========== 1. 定义变量,方便统一管理 ========== -->
+    <!-- 日志存放路径 -->
+    <property name="LOG_HOME" value="logs" />
+    <!-- 日志文件最大保留天数 -->
+    <property name="MAX_HISTORY" value="30" />
+    <!-- 单个日志文件最大大小 -->
+    <property name="MAX_FILE_SIZE" value="100MB" />
+    <!-- 日志总大小限制(可选) -->
+    <property name="TOTAL_SIZE_CAP" value="10GB" />
+
+    <!-- ========== 2. 控制台输出配置 ========== -->
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- 日志输出格式 -->
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <!--
+                格式说明:
+                %d{yyyy-MM-dd HH:mm:ss.SSS} 日期时间
+                %-5level 日志级别(左对齐,5个字符宽度)
+                %thread 线程名称
+                %logger{50} 日志记录器名称(最多50个字符)
+                %msg 日志消息
+                %n 换行
+            -->
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <!-- 日志级别阈值:只输出 INFO 及以上级别的日志到控制台 -->
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>DEBUG</level>
+        </filter>
+    </appender>
+
+    <!-- ========== 3. 文件输出配置 ========== -->
+    <!-- 3.1 普通日志文件(INFO 级别) -->
+    <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 正在记录的日志文件 -->
+        <file>${LOG_HOME}/ssm-info.log</file>
+
+        <!-- 日志输出格式 -->
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+
+        <!-- 滚动策略:基于时间和文件大小 -->
+        <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>
+            <!-- 日志总大小上限 -->
+            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
+        </rollingPolicy>
+
+        <!-- 过滤器:只输出 INFO 级别的日志 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 3.2 错误日志文件(ERROR 级别,单独记录) -->
+    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/ssm-error.log</file>
+
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+
+        <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>
+            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
+        </rollingPolicy>
+
+        <!-- 过滤器:只输出 ERROR 级别的日志 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 3.3 调试日志文件(DEBUG 级别,用于开发环境) -->
+    <appender name="FILE_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/ssm-debug.log</file>
+
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <!-- DEBUG 模式输出更详细的信息:包括行号、方法名 -->
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%method:%line] - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/archive/ssm-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            <maxHistory>7</maxHistory>  <!-- DEBUG 日志只保留7天 -->
+        </rollingPolicy>
+
+        <!-- 过滤器:只输出 DEBUG 级别的日志 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>DEBUG</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- ========== 4. 异步日志配置(提升性能) ========== -->
+    <appender name="ASYNC_INFO" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志,默认如果队列满80%会丢弃日志,设为从不丢弃 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 队列大小 -->
+        <queueSize>1024</queueSize>
+        <!-- 引用前面的 INFO 文件 appender -->
+        <appender-ref ref="FILE_INFO" />
+    </appender>
+
+    <appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
+        <discardingThreshold>0</discardingThreshold>
+        <queueSize>1024</queueSize>
+        <appender-ref ref="FILE_ERROR" />
+    </appender>
+
+    <!-- ========== 5. 日志级别配置 ========== -->
+    <!--
+        root 是根日志记录器,所有未指定包的日志都使用这个级别
+        日志级别优先级:TRACE < DEBUG < INFO < WARN < ERROR
+    -->
+    <root level="INFO">
+        <appender-ref ref="CONSOLE" />
+        <appender-ref ref="ASYNC_INFO" />
+        <appender-ref ref="ASYNC_ERROR" />
+    </root>
+
+    <!-- 开发环境可以降低 MyBatis 日志级别,查看 SQL 执行情况 -->
+    <logger name="space.anyi.mapper" level="DEBUG" additivity="false">
+        <appender-ref ref="CONSOLE" />
+        <appender-ref ref="FILE_DEBUG" />
+    </logger>
+
+    <!-- 打印 Spring 框架的 WARN 及以上级别日志,减少控制台干扰 -->
+    <logger name="org.springframework" level="WARN" />
+
+    <!-- 打印 MyBatis 的 SQL 参数绑定 -->
+    <logger name="org.apache.ibatis" level="DEBUG" />
+
+    <!-- 打印连接池日志 -->
+    <logger name="com.alibaba.druid" level="WARN" />
+
+    <!-- 打印 Thymeleaf 引擎日志(生产环境可设为 ERROR) -->
+    <logger name="org.thymeleaf" level="INFO" />
+
+</configuration>

+ 100 - 0
src/main/resources/mapper/AssignmentMapper.xml

@@ -0,0 +1,100 @@
+<?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.AssignmentMapper">
+
+    <resultMap id="BaseResultMap" type="space.anyi.entity.Assignment">
+        <id property="id" column="id"/>
+        <result property="userId" column="user_id"/>
+        <result property="bedId" column="bed_id"/>
+        <result property="assignDate" column="assign_date"/>
+        <result property="endDate" column="end_date"/>
+        <result property="status" column="status"/>
+        <result property="createdAt" column="created_at"/>
+        <result property="updatedAt" column="updated_at"/>
+        <result property="userName" column="user_name"/>
+        <result property="studentId" column="student_id"/>
+        <result property="buildingName" column="building_name"/>
+        <result property="roomNumber" column="room_number"/>
+        <result property="bedNumber" column="bed_number"/>
+    </resultMap>
+
+    <sql id="BaseColumns">
+        a.id, a.user_id, a.bed_id, a.assign_date, a.end_date,
+        a.status, a.created_at, a.updated_at
+    </sql>
+
+    <select id="selectAll" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>,
+            u.name AS user_name, u.student_id,
+            b.name AS building_name, r.room_number, d.bed_number
+        FROM assignment a
+        LEFT JOIN user u ON a.user_id = u.id
+        LEFT JOIN bed d ON a.bed_id = d.id
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        ORDER BY a.id DESC
+    </select>
+
+    <select id="selectById" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>,
+            u.name AS user_name, u.student_id,
+            b.name AS building_name, r.room_number, d.bed_number
+        FROM assignment a
+        LEFT JOIN user u ON a.user_id = u.id
+        LEFT JOIN bed d ON a.bed_id = d.id
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE a.id = #{id}
+    </select>
+
+    <select id="selectActiveByUserId" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>,
+            u.name AS user_name, u.student_id,
+            b.name AS building_name, r.room_number, d.bed_number
+        FROM assignment a
+        LEFT JOIN user u ON a.user_id = u.id
+        LEFT JOIN bed d ON a.bed_id = d.id
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE a.user_id = #{userId} AND a.status = 0
+        LIMIT 1
+    </select>
+
+    <select id="selectActiveByBedId" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>,
+            u.name AS user_name, u.student_id
+        FROM assignment a
+        LEFT JOIN user u ON a.user_id = u.id
+        WHERE a.bed_id = #{bedId} AND a.status = 0
+        LIMIT 1
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO assignment (user_id, bed_id, assign_date, status)
+        VALUES (#{userId}, #{bedId}, #{assignDate}, 0)
+    </insert>
+
+    <update id="update">
+        UPDATE assignment SET end_date = #{endDate}, status = #{status}
+        WHERE id = #{id}
+    </update>
+
+    <update id="endAssignment">
+        UPDATE assignment SET end_date = CURDATE(), status = 1
+        WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM assignment WHERE id = #{id}
+    </delete>
+
+    <select id="selectStudentsWithoutAssignment" resultMap="space.anyi.mapper.UserMapper.BaseResultMap">
+        SELECT u.id, u.username, u.password, u.name, u.gender, u.phone,
+               u.role, u.student_id, u.avatar, u.created_at, u.updated_at
+        FROM user u
+        WHERE u.role = 1
+          AND u.id NOT IN (SELECT user_id FROM assignment WHERE status = 0)
+        ORDER BY u.id
+    </select>
+
+</mapper>

+ 91 - 0
src/main/resources/mapper/BedMapper.xml

@@ -0,0 +1,91 @@
+<?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.BedMapper">
+
+    <resultMap id="BaseResultMap" type="space.anyi.entity.Bed">
+        <id property="id" column="id"/>
+        <result property="roomId" column="room_id"/>
+        <result property="bedNumber" column="bed_number"/>
+        <result property="status" column="status"/>
+        <result property="createdAt" column="created_at"/>
+        <result property="updatedAt" column="updated_at"/>
+        <result property="roomNumber" column="room_number"/>
+        <result property="buildingName" column="building_name"/>
+    </resultMap>
+
+    <sql id="BaseColumns">
+        d.id, d.room_id, d.bed_number, d.status, d.created_at, d.updated_at
+    </sql>
+
+    <select id="selectAll" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, r.room_number, b.name AS building_name
+        FROM bed d
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        ORDER BY d.id
+    </select>
+
+    <select id="selectByCondition" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, r.room_number, b.name AS building_name
+        FROM bed d
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        <where>
+            <if test="buildingId != null">
+                AND r.building_id = #{buildingId}
+            </if>
+            <if test="roomNumber != null and roomNumber != ''">
+                AND r.room_number LIKE CONCAT('%', #{roomNumber}, '%')
+            </if>
+        </where>
+        ORDER BY d.id
+    </select>
+
+    <select id="selectByRoomId" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, r.room_number, b.name AS building_name
+        FROM bed d
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE d.room_id = #{roomId} ORDER BY d.bed_number
+    </select>
+
+    <select id="selectAvailableByRoomId" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, r.room_number, b.name AS building_name
+        FROM bed d
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE d.room_id = #{roomId} AND d.status = 0
+        ORDER BY d.bed_number
+    </select>
+
+    <select id="selectById" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, r.room_number, b.name AS building_name
+        FROM bed d
+        LEFT JOIN room r ON d.room_id = r.id
+        LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE d.id = #{id}
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO bed (room_id, bed_number, status)
+        VALUES (#{roomId}, #{bedNumber}, #{status})
+    </insert>
+
+    <update id="update">
+        UPDATE bed SET room_id = #{roomId}, bed_number = #{bedNumber}, status = #{status}
+        WHERE id = #{id}
+    </update>
+
+    <update id="updateStatus">
+        UPDATE bed SET status = #{status} WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM bed WHERE id = #{id}
+    </delete>
+
+    <select id="countActiveAssignmentsByBedId" resultType="int">
+        SELECT COUNT(*) FROM assignment WHERE bed_id = #{bedId} AND status = 0
+    </select>
+
+</mapper>

+ 68 - 0
src/main/resources/mapper/DormitoryBuildingMapper.xml

@@ -0,0 +1,68 @@
+<?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="selectByCondition" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/> FROM dormitory_building
+        <where>
+            <if test="name != null and name != ''">
+                AND name LIKE CONCAT('%', #{name}, '%')
+            </if>
+            <if test="code != null and code != ''">
+                AND code LIKE CONCAT('%', #{code}, '%')
+            </if>
+            <if test="gender != null">
+                AND gender = #{gender}
+            </if>
+        </where>
+        ORDER BY id
+    </select>
+
+    <select id="selectByGender" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/> FROM dormitory_building
+        WHERE gender = #{gender} OR gender = 2 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>
+
+    <select id="countRoomsByBuildingId" resultType="int">
+        SELECT COUNT(*) FROM room WHERE building_id = #{buildingId}
+    </select>
+
+</mapper>

+ 82 - 0
src/main/resources/mapper/RoomMapper.xml

@@ -0,0 +1,82 @@
+<?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.RoomMapper">
+
+    <resultMap id="BaseResultMap" type="space.anyi.entity.Room">
+        <id property="id" column="id"/>
+        <result property="buildingId" column="building_id"/>
+        <result property="roomNumber" column="room_number"/>
+        <result property="floor" column="floor"/>
+        <result property="capacity" column="capacity"/>
+        <result property="gender" column="gender"/>
+        <result property="createdAt" column="created_at"/>
+        <result property="updatedAt" column="updated_at"/>
+        <result property="buildingName" column="building_name"/>
+    </resultMap>
+
+    <sql id="BaseColumns">
+        r.id, r.building_id, r.room_number, r.floor, r.capacity,
+        r.gender, r.created_at, r.updated_at
+    </sql>
+
+    <select id="selectAll" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, b.name AS building_name
+        FROM room r LEFT JOIN dormitory_building b ON r.building_id = b.id
+        ORDER BY r.id
+    </select>
+
+    <select id="selectByCondition" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, b.name AS building_name
+        FROM room r LEFT JOIN dormitory_building b ON r.building_id = b.id
+        <where>
+            <if test="buildingId != null">
+                AND r.building_id = #{buildingId}
+            </if>
+            <if test="floor != null">
+                AND r.floor = #{floor}
+            </if>
+        </where>
+        ORDER BY r.id
+    </select>
+
+    <select id="selectByBuildingId" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, b.name AS building_name
+        FROM room r LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE r.building_id = #{buildingId} ORDER BY r.room_number
+    </select>
+
+    <select id="selectAvailableByBuildingId" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, b.name AS building_name,
+            (SELECT COUNT(*) FROM bed WHERE room_id = r.id AND status = 0) AS available_beds
+        FROM room r LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE r.building_id = #{buildingId}
+          AND (SELECT COUNT(*) FROM bed WHERE room_id = r.id AND status = 0) > 0
+        ORDER BY r.room_number
+    </select>
+
+    <select id="selectById" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/>, b.name AS building_name
+        FROM room r LEFT JOIN dormitory_building b ON r.building_id = b.id
+        WHERE r.id = #{id}
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO room (building_id, room_number, floor, capacity, gender)
+        VALUES (#{buildingId}, #{roomNumber}, #{floor}, #{capacity}, #{gender})
+    </insert>
+
+    <update id="update">
+        UPDATE room SET building_id = #{buildingId}, room_number = #{roomNumber},
+            floor = #{floor}, capacity = #{capacity}, gender = #{gender}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM room WHERE id = #{id}
+    </delete>
+
+    <select id="countBedsByRoomId" resultType="int">
+        SELECT COUNT(*) FROM bed WHERE room_id = #{roomId}
+    </select>
+
+</mapper>

+ 75 - 0
src/main/resources/mapper/UserMapper.xml

@@ -0,0 +1,75 @@
+<?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.UserMapper">
+
+    <resultMap id="BaseResultMap" type="space.anyi.entity.User">
+        <id property="id" column="id"/>
+        <result property="username" column="username"/>
+        <result property="password" column="password"/>
+        <result property="name" column="name"/>
+        <result property="gender" column="gender"/>
+        <result property="phone" column="phone"/>
+        <result property="role" column="role"/>
+        <result property="studentId" column="student_id"/>
+        <result property="avatar" column="avatar"/>
+        <result property="createdAt" column="created_at"/>
+        <result property="updatedAt" column="updated_at"/>
+    </resultMap>
+
+    <sql id="BaseColumns">
+        id, username, password, name, gender, phone, role,
+        student_id, avatar, created_at, updated_at
+    </sql>
+
+    <select id="selectAll" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/> FROM user ORDER BY id
+    </select>
+
+    <select id="selectByCondition" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/> FROM user
+        <where>
+            <if test="name != null and name != ''">
+                AND name LIKE CONCAT('%', #{name}, '%')
+            </if>
+            <if test="gender != null">
+                AND gender = #{gender}
+            </if>
+            <if test="studentId != null and studentId != ''">
+                AND student_id LIKE CONCAT('%', #{studentId}, '%')
+            </if>
+        </where>
+        ORDER BY id
+    </select>
+
+    <select id="selectById" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/> FROM user WHERE id = #{id}
+    </select>
+
+    <select id="selectByUsername" resultMap="BaseResultMap">
+        SELECT <include refid="BaseColumns"/> FROM user WHERE username = #{username}
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO user (username, password, name, gender, phone, role, student_id, avatar)
+        VALUES (#{username}, #{password}, #{name}, #{gender}, #{phone}, #{role}, #{studentId}, #{avatar})
+    </insert>
+
+    <update id="update">
+        UPDATE user SET name = #{name}, gender = #{gender}, phone = #{phone},
+            student_id = #{studentId}
+        WHERE id = #{id}
+    </update>
+
+    <update id="updateAvatar">
+        UPDATE user SET avatar = #{avatar} WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM user WHERE id = #{id}
+    </delete>
+
+    <select id="countActiveAssignmentsByUserId" resultType="int">
+        SELECT COUNT(*) FROM assignment WHERE user_id = #{userId} AND status = 0
+    </select>
+
+</mapper>

+ 15 - 0
src/main/resources/mybatis-config.xml

@@ -0,0 +1,15 @@
+<?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>

+ 51 - 0
src/main/resources/spring-context.xml

@@ -0,0 +1,51 @@
+<?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="
+           http://www.springframework.org/schema/beans
+           http://www.springframework.org/schema/beans/spring-beans.xsd
+           http://www.springframework.org/schema/tx
+           http://www.springframework.org/schema/tx/spring-tx.xsd
+           http://www.springframework.org/schema/context
+           http://www.springframework.org/schema/context/spring-context.xsd">
+
+    <!-- 1. 扫描包:排除 Controller(留给 Spring MVC 扫描) -->
+    <context:component-scan base-package="space.anyi">
+        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
+    </context:component-scan>
+
+    <!-- 2. 加载 jdbc.properties -->
+    <context:property-placeholder location="classpath:jdbc.properties"/>
+
+    <!-- 3. 配置数据源(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>
+
+    <!-- 4. 配置 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>
+
+    <!-- 5. 配置 Mapper 扫描(自动生成代理实现类) -->
+    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
+        <property name="basePackage" value="space.anyi.mapper"/>
+        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
+    </bean>
+
+    <!-- 6. 配置事务管理器 -->
+    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
+        <property name="dataSource" ref="dataSource"/>
+    </bean>
+
+    <!-- 7. 启用注解事务管理 -->
+    <tx:annotation-driven transaction-manager="transactionManager"/>
+</beans>

+ 43 - 0
src/main/resources/spring-mvc.xml

@@ -0,0 +1,43 @@
+<?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="http://www.springframework.org/schema/beans
+       http://www.springframework.org/schema/beans/spring-beans.xsd
+       http://www.springframework.org/schema/context
+       https://www.springframework.org/schema/context/spring-context.xsd
+       http://www.springframework.org/schema/mvc
+       https://www.springframework.org/schema/mvc/spring-mvc.xsd">
+
+    <context:component-scan base-package="space.anyi.controller"/>
+
+    <mvc:annotation-driven/>
+
+    <mvc:default-servlet-handler/>
+
+    <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>

+ 90 - 0
src/main/webapp/WEB-INF/views/admin/assignment_add.jsp

@@ -0,0 +1,90 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<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">
+    <h3 class="mb-4">管理员分配床位</h3>
+
+    <c:if test="${not empty error}"><div class="alert alert-danger">${error}</div></c:if>
+
+    <form action="${pageContext.request.contextPath}/admin/assignment/save" method="post">
+        <div class="row">
+            <div class="col-md-4">
+                <div class="card mb-3">
+                    <div class="card-header">选择学生</div>
+                    <div class="card-body">
+                        <select name="userId" class="form-control" required>
+                            <option value="">请选择学生</option>
+                            <c:forEach items="${students}" var="s">
+                                <option value="${s.id}">${s.name} (${s.studentId})</option>
+                            </c:forEach>
+                        </select>
+                        <c:if test="${empty students}">
+                            <p class="text-muted mt-2">所有学生都已分配床位</p>
+                        </c:if>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-8">
+                <div class="card mb-3">
+                    <div class="card-header">选择床位</div>
+                    <div class="card-body">
+                        <div class="row">
+                            <div class="col-md-4">
+                                <label class="font-weight-bold">楼栋</label>
+                                <c:forEach items="${buildings}" var="b">
+                                    <a href="${pageContext.request.contextPath}/admin/assignment/add?buildingId=${b.id}"
+                                       class="btn btn-sm ${selectedBuildingId == b.id ? 'btn-primary' : 'btn-outline-primary'} btn-block mb-1">
+                                        ${b.name}
+                                    </a>
+                                </c:forEach>
+                                <c:if test="${empty buildings}">
+                                    <p class="text-muted">暂无宿舍楼</p>
+                                </c:if>
+                            </div>
+                            <div class="col-md-4">
+                                <label class="font-weight-bold">房间</label>
+                                <c:if test="${selectedBuildingId == null}">
+                                    <p class="text-muted">请先选择楼栋</p>
+                                </c:if>
+                                <c:forEach items="${rooms}" var="r">
+                                    <c:if test="${r.buildingId == selectedBuildingId}">
+                                        <a href="${pageContext.request.contextPath}/admin/assignment/add?buildingId=${selectedBuildingId}&roomId=${r.id}"
+                                           class="btn btn-sm ${selectedRoomId == r.id ? 'btn-success' : 'btn-outline-success'} btn-block mb-1">
+                                            ${r.roomNumber}
+                                        </a>
+                                    </c:if>
+                                </c:forEach>
+                            </div>
+                            <div class="col-md-4">
+                                <label class="font-weight-bold">床位</label>
+                                <c:if test="${selectedRoomId == null}">
+                                    <p class="text-muted">请先选择房间</p>
+                                </c:if>
+                                <c:forEach items="${beds}" var="d">
+                                    <button type="submit" name="bedId" value="${d.id}"
+                                            class="btn btn-sm btn-outline-info btn-block mb-1"
+                                            onclick="return confirm('确认将床位分配给该学生?')">
+                                        床位 ${d.bedNumber}
+                                    </button>
+                                </c:forEach>
+                                <c:if test="${selectedRoomId != null && empty beds}">
+                                    <p class="text-muted">该房间暂无空闲床位</p>
+                                </c:if>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+</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>

+ 70 - 0
src/main/webapp/WEB-INF/views/admin/assignment_list.jsp

@@ -0,0 +1,70 @@
+<%@ 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">
+    <c:if test="${not empty error}"><div class="alert alert-danger">${error}</div></c:if>
+    <c:if test="${not empty success}"><div class="alert alert-success">${success}</div></c:if>
+    <div class="d-flex justify-content-between align-items-center mb-3">
+        <h3>分配记录</h3>
+        <a href="${pageContext.request.contextPath}/admin/assignment/add" class="btn btn-primary">新增分配</a>
+    </div>
+    <table class="table table-bordered table-hover">
+        <thead class="thead-light">
+            <tr><th>ID</th><th>学生</th><th>学号</th><th>宿舍楼</th><th>房间</th><th>床位</th><th>入住日期</th><th>状态</th><th>操作</th></tr>
+        </thead>
+        <tbody>
+            <c:forEach items="${pageResult.list}" var="a">
+                <tr>
+                    <td>${a.id}</td>
+                    <td>${a.userName}</td>
+                    <td>${a.studentId}</td>
+                    <td>${a.buildingName}</td>
+                    <td>${a.roomNumber}</td>
+                    <td>${a.bedNumber}</td>
+                    <td>${a.assignDate}</td>
+                    <td>
+                        <c:choose>
+                            <c:when test="${a.status == 0}"><span class="badge badge-success">在住</span></c:when>
+                            <c:otherwise><span class="badge badge-secondary">已迁出</span></c:otherwise>
+                        </c:choose>
+                    </td>
+                    <td>
+                        <c:if test="${a.status == 0}">
+                            <a href="${pageContext.request.contextPath}/admin/assignment/end?id=${a.id}"
+                               class="btn btn-sm btn-danger" onclick="return confirm('确认办理退宿?')">退宿</a>
+                        </c:if>
+                    </td>
+                </tr>
+            </c:forEach>
+            <c:if test="${empty pageResult.list}">
+                <tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>
+            </c:if>
+        </tbody>
+    </table>
+    <nav>
+        <ul class="pagination justify-content-center">
+            <li class="page-item ${pageResult.pageNum <= 1 ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum-1}">上一页</a>
+            </li>
+            <c:forEach begin="1" end="${pageResult.pages}" var="i">
+                <li class="page-item ${i == pageResult.pageNum ? 'active' : ''}">
+                    <a class="page-link" href="?page=${i}">${i}</a>
+                </li>
+            </c:forEach>
+            <li class="page-item ${pageResult.pageNum >= pageResult.pages ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum+1}">下一页</a>
+            </li>
+        </ul>
+    </nav>
+</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>

+ 46 - 0
src/main/webapp/WEB-INF/views/admin/bed_edit.jsp

@@ -0,0 +1,46 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<html>
+<head>
+    <title>${empty bed ? '新增' : '编辑'}床位</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">
+    <h3 class="mb-3">${empty bed ? '新增' : '编辑'}床位</h3>
+    <form action="${pageContext.request.contextPath}/admin/bed/save" method="post" class="col-md-6">
+        <c:if test="${not empty bed}">
+            <input type="hidden" name="id" value="${bed.id}">
+        </c:if>
+        <div class="form-group">
+            <label>所属房间</label>
+            <select name="roomId" class="form-control" required>
+                <option value="">请选择</option>
+                <c:forEach items="${rooms}" var="r">
+                    <option value="${r.id}" ${bed.roomId == r.id ? 'selected' : ''}>${r.buildingName} - ${r.roomNumber}</option>
+                </c:forEach>
+            </select>
+        </div>
+        <div class="form-group">
+            <label>床位编号</label>
+            <input type="text" name="bedNumber" class="form-control" value="${bed.bedNumber}" required>
+        </div>
+        <c:if test="${not empty bed}">
+            <div class="form-group">
+                <label>状态</label>
+                <select name="status" class="form-control">
+                    <option value="0" ${bed.status == 0 ? 'selected' : ''}>空闲</option>
+                    <option value="1" ${bed.status == 1 ? 'selected' : ''}>占用</option>
+                    <option value="2" ${bed.status == 2 ? 'selected' : ''}>维修</option>
+                </select>
+            </div>
+        </c:if>
+        <button type="submit" class="btn btn-primary">保存</button>
+        <a href="${pageContext.request.contextPath}/admin/bed/list" class="btn btn-secondary">取消</a>
+    </form>
+</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>

+ 87 - 0
src/main/webapp/WEB-INF/views/admin/bed_list.jsp

@@ -0,0 +1,87 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<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">
+    <c:if test="${not empty error}"><div class="alert alert-danger">${error}</div></c:if>
+    <c:if test="${not empty success}"><div class="alert alert-success">${success}</div></c:if>
+    <div class="d-flex justify-content-between align-items-center mb-3">
+        <h3>床位管理</h3>
+        <a href="${pageContext.request.contextPath}/admin/bed/add" class="btn btn-primary">新增床位</a>
+    </div>
+
+    <form class="form-inline mb-3" method="get">
+        <input type="hidden" name="page" value="1">
+        <div class="form-group mr-2">
+            <label class="mr-1">宿舍楼</label>
+            <select name="buildingId" class="form-control">
+                <option value="">全部楼栋</option>
+                <c:forEach items="${buildings}" var="b">
+                    <option value="${b.id}" ${buildingId == b.id ? 'selected' : ''}>${b.name}</option>
+                </c:forEach>
+            </select>
+        </div>
+        <div class="form-group mr-2">
+            <label class="mr-1">房间号</label>
+            <input type="text" name="roomNumber" class="form-control" value="${roomNumber}" placeholder="房间号">
+        </div>
+        <button type="submit" class="btn btn-primary">搜索</button>
+        <a href="${pageContext.request.contextPath}/admin/bed/list" class="btn btn-secondary ml-1">重置</a>
+    </form>
+
+    <table class="table table-bordered table-hover">
+        <thead class="thead-light">
+            <tr><th>ID</th><th>楼栋</th><th>房间</th><th>床位号</th><th>状态</th><th>操作</th></tr>
+        </thead>
+        <tbody>
+            <c:forEach items="${pageResult.list}" var="d">
+                <tr>
+                    <td>${d.id}</td>
+                    <td>${d.buildingName}</td>
+                    <td>${d.roomNumber}</td>
+                    <td>${d.bedNumber}</td>
+                    <td>
+                        <c:choose>
+                            <c:when test="${d.status == 0}"><span class="badge badge-success">空闲</span></c:when>
+                            <c:when test="${d.status == 1}"><span class="badge badge-danger">占用</span></c:when>
+                            <c:when test="${d.status == 2}"><span class="badge badge-warning">维修</span></c:when>
+                        </c:choose>
+                    </td>
+                    <td>
+                        <a href="${pageContext.request.contextPath}/admin/bed/edit?id=${d.id}"
+                           class="btn btn-sm btn-warning">编辑</a>
+                        <a href="${pageContext.request.contextPath}/admin/bed/delete?id=${d.id}"
+                           class="btn btn-sm btn-danger" onclick="return confirm('确认删除?')">删除</a>
+                    </td>
+                </tr>
+            </c:forEach>
+            <c:if test="${empty pageResult.list}">
+                <tr><td colspan="6" class="text-center text-muted">暂无数据</td></tr>
+            </c:if>
+        </tbody>
+    </table>
+    <nav>
+        <ul class="pagination justify-content-center">
+            <li class="page-item ${pageResult.pageNum <= 1 ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum-1}&buildingId=${buildingId}&roomNumber=${roomNumber}">上一页</a>
+            </li>
+            <c:forEach begin="1" end="${pageResult.pages}" var="i">
+                <li class="page-item ${i == pageResult.pageNum ? 'active' : ''}">
+                    <a class="page-link" href="?page=${i}&buildingId=${buildingId}&roomNumber=${roomNumber}">${i}</a>
+                </li>
+            </c:forEach>
+            <li class="page-item ${pageResult.pageNum >= pageResult.pages ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum+1}&buildingId=${buildingId}&roomNumber=${roomNumber}">下一页</a>
+            </li>
+        </ul>
+    </nav>
+</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>

+ 47 - 0
src/main/webapp/WEB-INF/views/admin/building_edit.jsp

@@ -0,0 +1,47 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<html>
+<head>
+    <title>${empty building ? '新增' : '编辑'}宿舍楼</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">
+    <h3 class="mb-3">${empty building ? '新增' : '编辑'}宿舍楼</h3>
+    <form action="${pageContext.request.contextPath}/admin/building/save" method="post" class="col-md-6">
+        <c:if test="${not empty building}">
+            <input type="hidden" name="id" value="${building.id}">
+        </c:if>
+        <div class="form-group">
+            <label>楼栋名称</label>
+            <input type="text" name="name" class="form-control" value="${building.name}" required>
+        </div>
+        <div class="form-group">
+            <label>楼栋编号</label>
+            <input type="text" name="code" class="form-control" value="${building.code}">
+        </div>
+        <div class="form-group">
+            <label>性别限制</label>
+            <select name="gender" class="form-control">
+                <option value="0" ${building.gender == 0 ? 'selected' : ''}>女生</option>
+                <option value="1" ${building.gender == 1 ? 'selected' : ''}>男生</option>
+                <option value="2" ${building.gender == 2 || empty building ? 'selected' : ''}>混合</option>
+            </select>
+        </div>
+        <div class="form-group">
+            <label>楼层数</label>
+            <input type="number" name="floorCount" class="form-control" value="${building.floorCount}" required>
+        </div>
+        <div class="form-group">
+            <label>备注</label>
+            <textarea name="description" class="form-control" rows="3">${building.description}</textarea>
+        </div>
+        <button type="submit" class="btn btn-primary">保存</button>
+        <a href="${pageContext.request.contextPath}/admin/building/list" class="btn btn-secondary">取消</a>
+    </form>
+</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>

+ 94 - 0
src/main/webapp/WEB-INF/views/admin/building_list.jsp

@@ -0,0 +1,94 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<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">
+    <c:if test="${not empty error}"><div class="alert alert-danger">${error}</div></c:if>
+    <c:if test="${not empty success}"><div class="alert alert-success">${success}</div></c:if>
+    <div class="d-flex justify-content-between align-items-center mb-3">
+        <h3>宿舍楼管理</h3>
+        <a href="${pageContext.request.contextPath}/admin/building/add" class="btn btn-primary">新增楼栋</a>
+    </div>
+
+    <form class="form-inline mb-3" method="get">
+        <input type="hidden" name="page" value="1">
+        <div class="form-group mr-2">
+            <label class="mr-1">名称</label>
+            <input type="text" name="name" class="form-control" value="${name}" placeholder="楼栋名称">
+        </div>
+        <div class="form-group mr-2">
+            <label class="mr-1">编号</label>
+            <input type="text" name="code" class="form-control" value="${code}" placeholder="楼栋编号">
+        </div>
+        <div class="form-group mr-2">
+            <label class="mr-1">性别</label>
+            <select name="gender" class="form-control">
+                <option value="">全部</option>
+                <option value="1" ${gender == 1 ? 'selected' : ''}>男</option>
+                <option value="0" ${gender == 0 ? 'selected' : ''}>女</option>
+                <option value="2" ${gender == 2 ? 'selected' : ''}>混合</option>
+            </select>
+        </div>
+        <button type="submit" class="btn btn-primary">搜索</button>
+        <a href="${pageContext.request.contextPath}/admin/building/list" class="btn btn-secondary ml-1">重置</a>
+    </form>
+
+    <table class="table table-bordered table-hover">
+        <thead class="thead-light">
+            <tr>
+                <th>ID</th><th>名称</th><th>编号</th><th>性别</th><th>楼层数</th><th>备注</th><th>操作</th>
+            </tr>
+        </thead>
+        <tbody>
+            <c:forEach items="${pageResult.list}" var="b">
+                <tr>
+                    <td>${b.id}</td>
+                    <td>${b.name}</td>
+                    <td>${b.code}</td>
+                    <td>
+                        <c:choose>
+                            <c:when test="${b.gender == 0}"><span class="badge badge-danger">女</span></c:when>
+                            <c:when test="${b.gender == 1}"><span class="badge badge-primary">男</span></c:when>
+                            <c:when test="${b.gender == 2}"><span class="badge badge-secondary">混合</span></c:when>
+                        </c:choose>
+                    </td>
+                    <td>${b.floorCount}</td>
+                    <td>${b.description}</td>
+                    <td>
+                        <a href="${pageContext.request.contextPath}/admin/building/edit?id=${b.id}"
+                           class="btn btn-sm btn-warning">编辑</a>
+                        <a href="${pageContext.request.contextPath}/admin/building/delete?id=${b.id}"
+                           class="btn btn-sm btn-danger" onclick="return confirm('确认删除?')">删除</a>
+                    </td>
+                </tr>
+            </c:forEach>
+            <c:if test="${empty pageResult.list}">
+                <tr><td colspan="7" class="text-center text-muted">暂无数据</td></tr>
+            </c:if>
+        </tbody>
+    </table>
+    <nav>
+        <ul class="pagination justify-content-center">
+            <li class="page-item ${pageResult.pageNum <= 1 ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum-1}&name=${name}&code=${code}&gender=${gender}">上一页</a>
+            </li>
+            <c:forEach begin="1" end="${pageResult.pages}" var="i">
+                <li class="page-item ${i == pageResult.pageNum ? 'active' : ''}">
+                    <a class="page-link" href="?page=${i}&name=${name}&code=${code}&gender=${gender}">${i}</a>
+                </li>
+            </c:forEach>
+            <li class="page-item ${pageResult.pageNum >= pageResult.pages ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum+1}&name=${name}&code=${code}&gender=${gender}">下一页</a>
+            </li>
+        </ul>
+    </nav>
+</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>

+ 20 - 0
src/main/webapp/WEB-INF/views/admin/dashboard.jsp

@@ -0,0 +1,20 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<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="jumbotron">
+        <h1 class="display-4">管理后台</h1>
+        <p class="lead">欢迎回来,${sessionScope.user.name}</p>
+        <hr class="my-4">
+        <p>使用上方导航栏管理宿舍资源。</p>
+    </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>

+ 52 - 0
src/main/webapp/WEB-INF/views/admin/room_edit.jsp

@@ -0,0 +1,52 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<html>
+<head>
+    <title>${empty room ? '新增' : '编辑'}房间</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">
+    <h3 class="mb-3">${empty room ? '新增' : '编辑'}房间</h3>
+    <form action="${pageContext.request.contextPath}/admin/room/save" method="post" class="col-md-6">
+        <c:if test="${not empty room}">
+            <input type="hidden" name="id" value="${room.id}">
+        </c:if>
+        <div class="form-group">
+            <label>所属楼栋</label>
+            <select name="buildingId" class="form-control" required>
+                <option value="">请选择</option>
+                <c:forEach items="${buildings}" var="b">
+                    <option value="${b.id}" ${room.buildingId == b.id ? 'selected' : ''}>${b.name}</option>
+                </c:forEach>
+            </select>
+        </div>
+        <div class="form-group">
+            <label>房间号</label>
+            <input type="text" name="roomNumber" class="form-control" value="${room.roomNumber}" required>
+        </div>
+        <div class="form-group">
+            <label>楼层</label>
+            <input type="number" name="floor" class="form-control" value="${room.floor}">
+        </div>
+        <div class="form-group">
+            <label>床位数</label>
+            <input type="number" name="capacity" class="form-control" value="${room.capacity}" required>
+        </div>
+        <div class="form-group">
+            <label>性别限制</label>
+            <select name="gender" class="form-control">
+                <option value="-1" ${room.gender == null ? 'selected' : ''}>不限(继承楼栋设置)</option>
+                <option value="0" ${room.gender == 0 ? 'selected' : ''}>女生</option>
+                <option value="1" ${room.gender == 1 ? 'selected' : ''}>男生</option>
+            </select>
+        </div>
+        <button type="submit" class="btn btn-primary">保存</button>
+        <a href="${pageContext.request.contextPath}/admin/room/list" class="btn btn-secondary">取消</a>
+    </form>
+</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>

+ 88 - 0
src/main/webapp/WEB-INF/views/admin/room_list.jsp

@@ -0,0 +1,88 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<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">
+    <c:if test="${not empty error}"><div class="alert alert-danger">${error}</div></c:if>
+    <c:if test="${not empty success}"><div class="alert alert-success">${success}</div></c:if>
+    <div class="d-flex justify-content-between align-items-center mb-3">
+        <h3>房间管理</h3>
+        <a href="${pageContext.request.contextPath}/admin/room/add" class="btn btn-primary">新增房间</a>
+    </div>
+
+    <form class="form-inline mb-3" method="get">
+        <input type="hidden" name="page" value="1">
+        <div class="form-group mr-2">
+            <label class="mr-1">宿舍楼</label>
+            <select name="buildingId" class="form-control">
+                <option value="">全部楼栋</option>
+                <c:forEach items="${buildings}" var="b">
+                    <option value="${b.id}" ${buildingId == b.id ? 'selected' : ''}>${b.name}</option>
+                </c:forEach>
+            </select>
+        </div>
+        <div class="form-group mr-2">
+            <label class="mr-1">楼层</label>
+            <input type="number" name="floor" class="form-control" value="${floor}" placeholder="楼层" style="width:100px">
+        </div>
+        <button type="submit" class="btn btn-primary">搜索</button>
+        <a href="${pageContext.request.contextPath}/admin/room/list" class="btn btn-secondary ml-1">重置</a>
+    </form>
+
+    <table class="table table-bordered table-hover">
+        <thead class="thead-light">
+            <tr><th>ID</th><th>楼栋</th><th>房间号</th><th>楼层</th><th>床位数</th><th>性别</th><th>操作</th></tr>
+        </thead>
+        <tbody>
+            <c:forEach items="${pageResult.list}" var="r">
+                <tr>
+                    <td>${r.id}</td>
+                    <td>${r.buildingName}</td>
+                    <td>${r.roomNumber}</td>
+                    <td>${r.floor}F</td>
+                    <td>${r.capacity}</td>
+                    <td>
+                        <c:choose>
+                            <c:when test="${r.gender == 0}"><span class="badge badge-danger">女</span></c:when>
+                            <c:when test="${r.gender == 1}"><span class="badge badge-primary">男</span></c:when>
+                            <c:otherwise><span class="badge badge-secondary">不限</span></c:otherwise>
+                        </c:choose>
+                    </td>
+                    <td>
+                        <a href="${pageContext.request.contextPath}/admin/room/edit?id=${r.id}"
+                           class="btn btn-sm btn-warning">编辑</a>
+                        <a href="${pageContext.request.contextPath}/admin/room/delete?id=${r.id}"
+                           class="btn btn-sm btn-danger" onclick="return confirm('确认删除?')">删除</a>
+                    </td>
+                </tr>
+            </c:forEach>
+            <c:if test="${empty pageResult.list}">
+                <tr><td colspan="7" class="text-center text-muted">暂无数据</td></tr>
+            </c:if>
+        </tbody>
+    </table>
+    <nav>
+        <ul class="pagination justify-content-center">
+            <li class="page-item ${pageResult.pageNum <= 1 ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum-1}&buildingId=${buildingId}&floor=${floor}">上一页</a>
+            </li>
+            <c:forEach begin="1" end="${pageResult.pages}" var="i">
+                <li class="page-item ${i == pageResult.pageNum ? 'active' : ''}">
+                    <a class="page-link" href="?page=${i}&buildingId=${buildingId}&floor=${floor}">${i}</a>
+                </li>
+            </c:forEach>
+            <li class="page-item ${pageResult.pageNum >= pageResult.pages ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum+1}&buildingId=${buildingId}&floor=${floor}">下一页</a>
+            </li>
+        </ul>
+    </nav>
+</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>

+ 59 - 0
src/main/webapp/WEB-INF/views/admin/user_edit.jsp

@@ -0,0 +1,59 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<html>
+<head>
+    <title>${empty requestScope.user ? '新增' : '编辑'}用户</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">
+    <h3 class="mb-3">${empty requestScope.user ? '新增' : '编辑'}用户</h3>
+    <form action="${pageContext.request.contextPath}/admin/user/save" method="post" class="col-md-6">
+        <c:if test="${not empty requestScope.user}">
+            <input type="hidden" name="id" value="${requestScope.user.id}">
+        </c:if>
+        <div class="form-group">
+            <label>用户名</label>
+            <input type="text" name="username" class="form-control" value="${requestScope.user.username}" ${not empty requestScope.user ? 'readonly' : ''} required>
+        </div>
+        <c:if test="${empty requestScope.user}">
+            <div class="form-group">
+                <label>密码</label>
+                <input type="password" name="password" class="form-control" required>
+            </div>
+        </c:if>
+        <div class="form-group">
+            <label>姓名</label>
+            <input type="text" name="name" class="form-control" value="${requestScope.user.name}" required>
+        </div>
+        <div class="form-group">
+            <label>角色</label>
+            <select name="role" class="form-control">
+                <option value="1" ${requestScope.user.role == 1 || empty requestScope.user ? 'selected' : ''}>学生</option>
+                <option value="0" ${requestScope.user.role == 0 ? 'selected' : ''}>管理员</option>
+            </select>
+        </div>
+        <div class="form-group">
+            <label>性别</label>
+            <select name="gender" class="form-control">
+                <option value="1" ${requestScope.user.gender == 1 ? 'selected' : ''}>男</option>
+                <option value="0" ${requestScope.user.gender == 0 ? 'selected' : ''}>女</option>
+            </select>
+        </div>
+        <div class="form-group">
+            <label>学号</label>
+            <input type="text" name="studentId" class="form-control" value="${requestScope.user.studentId}">
+        </div>
+        <div class="form-group">
+            <label>电话</label>
+            <input type="text" name="phone" class="form-control" value="${requestScope.user.phone}">
+        </div>
+        <button type="submit" class="btn btn-primary">保存</button>
+        <a href="${pageContext.request.contextPath}/admin/user/list" class="btn btn-secondary">取消</a>
+    </form>
+</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>

+ 91 - 0
src/main/webapp/WEB-INF/views/admin/user_list.jsp

@@ -0,0 +1,91 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<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">
+    <c:if test="${not empty error}"><div class="alert alert-danger">${error}</div></c:if>
+    <c:if test="${not empty success}"><div class="alert alert-success">${success}</div></c:if>
+    <div class="d-flex justify-content-between align-items-center mb-3">
+        <h3>用户管理</h3>
+        <a href="${pageContext.request.contextPath}/admin/user/add" class="btn btn-primary">新增用户</a>
+    </div>
+
+    <form class="form-inline mb-3" method="get">
+        <input type="hidden" name="page" value="1">
+        <div class="form-group mr-2">
+            <label class="mr-1">姓名</label>
+            <input type="text" name="name" class="form-control" value="${name}" placeholder="姓名">
+        </div>
+        <div class="form-group mr-2">
+            <label class="mr-1">性别</label>
+            <select name="gender" class="form-control">
+                <option value="">全部</option>
+                <option value="1" ${gender == 1 ? 'selected' : ''}>男</option>
+                <option value="0" ${gender == 0 ? 'selected' : ''}>女</option>
+            </select>
+        </div>
+        <div class="form-group mr-2">
+            <label class="mr-1">学号</label>
+            <input type="text" name="studentId" class="form-control" value="${studentId}" placeholder="学号">
+        </div>
+        <button type="submit" class="btn btn-primary">搜索</button>
+        <a href="${pageContext.request.contextPath}/admin/user/list" class="btn btn-secondary ml-1">重置</a>
+    </form>
+
+    <table class="table table-bordered table-hover">
+        <thead class="thead-light">
+            <tr><th>ID</th><th>用户名</th><th>姓名</th><th>角色</th><th>性别</th><th>学号</th><th>电话</th><th>操作</th></tr>
+        </thead>
+        <tbody>
+            <c:forEach items="${pageResult.list}" var="u">
+                <tr>
+                    <td>${u.id}</td>
+                    <td>${u.username}</td>
+                    <td>${u.name}</td>
+                    <td>
+                        <c:choose>
+                            <c:when test="${u.role == 0}"><span class="badge badge-dark">管理员</span></c:when>
+                            <c:otherwise><span class="badge badge-info">学生</span></c:otherwise>
+                        </c:choose>
+                    </td>
+                    <td>${u.gender == 1 ? '男' : '女'}</td>
+                    <td>${u.studentId}</td>
+                    <td>${u.phone}</td>
+                    <td>
+                        <a href="${pageContext.request.contextPath}/admin/user/edit?id=${u.id}"
+                           class="btn btn-sm btn-warning">编辑</a>
+                        <a href="${pageContext.request.contextPath}/admin/user/delete?id=${u.id}"
+                           class="btn btn-sm btn-danger" onclick="return confirm('确认删除?')">删除</a>
+                    </td>
+                </tr>
+            </c:forEach>
+            <c:if test="${empty pageResult.list}">
+                <tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>
+            </c:if>
+        </tbody>
+    </table>
+    <nav>
+        <ul class="pagination justify-content-center">
+            <li class="page-item ${pageResult.pageNum <= 1 ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum-1}&name=${name}&gender=${gender}&studentId=${studentId}">上一页</a>
+            </li>
+            <c:forEach begin="1" end="${pageResult.pages}" var="i">
+                <li class="page-item ${i == pageResult.pageNum ? 'active' : ''}">
+                    <a class="page-link" href="?page=${i}&name=${name}&gender=${gender}&studentId=${studentId}">${i}</a>
+                </li>
+            </c:forEach>
+            <li class="page-item ${pageResult.pageNum >= pageResult.pages ? 'disabled' : ''}">
+                <a class="page-link" href="?page=${pageResult.pageNum+1}&name=${name}&gender=${gender}&studentId=${studentId}">下一页</a>
+            </li>
+        </ul>
+    </nav>
+</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>

+ 35 - 0
src/main/webapp/WEB-INF/views/common/header.jsp

@@ -0,0 +1,35 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<c:set var="ctx" value="${pageContext.request.contextPath}"/>
+<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
+    <a class="navbar-brand" href="${ctx}/">宿舍管理系统</a>
+    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav">
+        <span class="navbar-toggler-icon"></span>
+    </button>
+    <div class="collapse navbar-collapse" id="navbarNav">
+        <ul class="navbar-nav mr-auto">
+            <c:if test="${sessionScope.user.role == 0}">
+                <li class="nav-item"><a class="nav-link" href="${ctx}/admin/dashboard">首页</a></li>
+                <li class="nav-item"><a class="nav-link" href="${ctx}/admin/building/list">宿舍楼</a></li>
+                <li class="nav-item"><a class="nav-link" href="${ctx}/admin/room/list">房间</a></li>
+                <li class="nav-item"><a class="nav-link" href="${ctx}/admin/bed/list">床位</a></li>
+                <li class="nav-item"><a class="nav-link" href="${ctx}/admin/user/list">用户</a></li>
+                <li class="nav-item"><a class="nav-link" href="${ctx}/admin/assignment/list">分配记录</a></li>
+            </c:if>
+            <c:if test="${sessionScope.user.role == 1}">
+                <li class="nav-item"><a class="nav-link" href="${ctx}/student/dashboard">我的住宿</a></li>
+                <li class="nav-item"><a class="nav-link" href="${ctx}/student/select">选床</a></li>
+            </c:if>
+        </ul>
+        <ul class="navbar-nav">
+            <li class="nav-item dropdown">
+                <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown">
+                    ${sessionScope.user.name}
+                </a>
+                <div class="dropdown-menu dropdown-menu-right">
+                    <a class="dropdown-item" href="${ctx}/logout">退出登录</a>
+                </div>
+            </li>
+        </ul>
+    </div>
+</nav>

+ 14 - 0
src/main/webapp/WEB-INF/views/error.jsp

@@ -0,0 +1,14 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<html>
+<head>
+    <title>出错了</title>
+    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
+</head>
+<body>
+<div class="container mt-5 text-center">
+    <h1 class="display-4 text-danger">出错了</h1>
+    <p class="lead">${error}</p>
+    <a href="javascript:history.back()" class="btn btn-primary">返回</a>
+</div>
+</body>
+</html>

+ 40 - 0
src/main/webapp/WEB-INF/views/login.jsp

@@ -0,0 +1,40 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<html>
+<head>
+    <title>宿舍管理系统 - 登录</title>
+    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
+    <style>
+        body { background: #f0f2f5; display: flex; align-items: center; min-height: 100vh; }
+        .login-card { max-width: 400px; width: 100%; margin: 0 auto; }
+    </style>
+</head>
+<body>
+<div class="container">
+    <div class="login-card">
+        <div class="card shadow">
+            <div class="card-body p-4">
+                <h3 class="text-center mb-4">宿舍管理系统</h3>
+                <c:if test="${param.error != null}">
+                    <div class="alert alert-danger">用户名或密码错误</div>
+                </c:if>
+                <c:if test="${param.error == 'unauthorized'}">
+                    <div class="alert alert-warning">无权限访问,请重新登录</div>
+                </c:if>
+                <form action="${pageContext.request.contextPath}/login" method="post">
+                    <div class="form-group">
+                        <label>用户名</label>
+                        <input type="text" name="username" class="form-control" required>
+                    </div>
+                    <div class="form-group">
+                        <label>密码</label>
+                        <input type="password" name="password" class="form-control" required>
+                    </div>
+                    <button type="submit" class="btn btn-primary btn-block">登 录</button>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 80 - 0
src/main/webapp/WEB-INF/views/student/dashboard.jsp

@@ -0,0 +1,80 @@
+<%@ 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">
+    <c:if test="${param.error != null}">
+        <div class="alert alert-danger">${param.error}</div>
+    </c:if>
+
+    <div class="row">
+        <div class="col-md-4">
+            <div class="card mb-4">
+                <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" id="avatarInput"
+                                   accept="image/*" required>
+                            <label class="custom-file-label" for="avatarInput">选择头像</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 style="width:120px">宿舍楼</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>
+<script>
+    $('#avatarInput').on('change', function() {
+        var fileName = $(this).val().split('\\').pop();
+        $(this).next('.custom-file-label').html(fileName);
+    });
+</script>
+</body>
+</html>

+ 80 - 0
src/main/webapp/WEB-INF/views/student/select_bed.jsp

@@ -0,0 +1,80 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<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">
+    <h3 class="mb-4">选择床位</h3>
+
+    <c:if test="${param.error != null}">
+        <div class="alert alert-danger">${param.error}</div>
+    </c:if>
+
+    <div class="row">
+        <div class="col-md-4">
+            <div class="card mb-3">
+                <div class="card-header">1. 选择宿舍楼</div>
+                <div class="card-body">
+                    <c:if test="${empty buildings}">
+                        <p class="text-muted">暂无可用宿舍楼</p>
+                    </c:if>
+                    <c:forEach items="${buildings}" var="b">
+                        <a href="${pageContext.request.contextPath}/student/select?buildingId=${b.id}"
+                           class="btn ${selectedBuildingId == b.id ? 'btn-primary' : 'btn-outline-primary'} btn-block mb-1">
+                            ${b.name}
+                        </a>
+                    </c:forEach>
+                </div>
+            </div>
+        </div>
+        <div class="col-md-4">
+            <div class="card mb-3">
+                <div class="card-header">2. 选择房间</div>
+                <div class="card-body">
+                    <c:if test="${selectedBuildingId == null}">
+                        <p class="text-muted">请先选择楼栋</p>
+                    </c:if>
+                    <c:if test="${selectedBuildingId != null && empty rooms}">
+                        <p class="text-muted">该楼栋暂无空闲房间</p>
+                    </c:if>
+                    <c:forEach items="${rooms}" var="r">
+                        <a href="${pageContext.request.contextPath}/student/select?buildingId=${selectedBuildingId}&roomId=${r.id}"
+                           class="btn ${selectedRoomId == r.id ? 'btn-success' : 'btn-outline-success'} btn-block mb-1">
+                            ${r.roomNumber} 楼(${r.floor}F)
+                        </a>
+                    </c:forEach>
+                </div>
+            </div>
+        </div>
+        <div class="col-md-4">
+            <div class="card mb-3">
+                <div class="card-header">3. 选择床位</div>
+                <div class="card-body">
+                    <c:if test="${selectedRoomId == null}">
+                        <p class="text-muted">请先选择房间</p>
+                    </c:if>
+                    <c:if test="${selectedRoomId != null && empty beds}">
+                        <p class="text-muted">该房间暂无空闲床位</p>
+                    </c:if>
+                    <c:forEach items="${beds}" var="b">
+                        <form action="${pageContext.request.contextPath}/student/assign" method="post"
+                              style="display:inline" onsubmit="return confirm('确认入住该床位?')">
+                            <input type="hidden" name="bedId" value="${b.id}">
+                            <button type="submit" class="btn btn-outline-info mb-1">
+                                床位 ${b.bedNumber}
+                            </button>
+                        </form>
+                    </c:forEach>
+                </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>

+ 49 - 0
src/main/webapp/WEB-INF/web.xml

@@ -0,0 +1,49 @@
+<?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="http://xmlns.jcp.org/xml/ns/javaee
+         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
+         version="4.0">
+
+    <!-- 1. 配置 Spring 监听器(加载 spring-context.xml) -->
+    <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>
+
+    <!-- 2. 配置 Spring MVC 前端控制器 -->
+    <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>
+
+    <!-- 3. 字符编码过滤器 -->
+    <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>