Spring Boot新生报到及宿舍分配系统:业务建模与算法实现全解析 📅 发布时间:2026/9/10 1:58:42 👁 浏览次数: 每年八月底九月初各高校的信息中心、学工处基本都要经历一场“人肉排宿舍”的战役。我见过太多老师抱着Excel表格反复排序筛选微信群里十几个“老师XX楼还有空位吗”的消息来回轰炸报到当天更是排长队、找名字、手写分配单忙到深夜才发现某个床位被重复安排、某个班级被拆得七零八落。这也是我当初动手做“Spring Boot新生报到及宿舍分配系统”的直接原因。这个系统的核心价值很明确把新生从“信息登记—报到确认—宿舍分配—入住登记”整条链路串起来让辅导员、院系管理员、信息中心在一个后台里完成所有操作宿舍分配交给规则引擎去算而不是靠脑子记。整篇文章我围绕Spring Boot的实际落地来写从业务建模、表结构设计、分配算法到关键代码实现和开发期踩坑全部基于可运行的源码思路展开。无论你是准备做毕业设计的学生还是想给学校内部系统做改造的开发者这篇都能给你一套能直接参考的方案。1. 开学季的真实痛点这套系统到底在解决什么问题1.1 传统的“人肉排宿舍”流程到底有多痛先说一个我实际接触过的场景某校一个年级四千多名新生分布在六个院系宿舍楼有四十四栋其中还有部分楼栋是混合住宿、部分房间是六人间和四人间混排。往年他们怎么做先是教务处把新生名单导成Excel学工处按院系拆分辅导员再拿着纸质名单去宿舍楼管那儿挑房间。这个过程至少有四个致命问题。第一信息不同步。楼管的纸质登记表和学工处的Excel永远差一拍有的房间明明已经住满了五个人登记表上还写着“余位6”。第二分配规则不可控。同班级的学生被拆到不同楼层甚至不同楼栋辅导员查寝、班级管理、下发通知全是麻烦事。第三报到当天效率极低。新生要先去学工处查分班、再去宿舍楼找楼管登记、领钥匙高峰期一个窗口排队一两个小时很正常。第四事后调整无据可查。学生要求换宿舍、临时休学退宿这些都靠口头沟通系统里没有轨迹数据一塌糊涂。我接手这个需求时学工处的老师提了一个很朴素的要求“能不能让我在电脑上点一下就分配好最好能保证同班级住一起别再把宿舍弄乱了。”这句话基本就框定了系统最重要的两个业务闭环报到流程闭环和宿舍分配闭环。1.2 系统要覆盖的完整业务闭环新生报到及宿舍分配系统表面上看是两个功能模块实际是一条完整的数据流。第一个闭环是报到闭环。新生信息录入或导入后系统里先有一条“待报到”的记录。新生到校后通过扫码或者窗口核验身份、确认信息无误状态变为“已报到”。有些学校会在这个环节增加缴费核验、材料上传、军训服装领取登记等本质上都是在这个状态机的某个节点挂扩展操作。第二个闭环是宿舍分配闭环。学生完成报到后系统根据预设规则分配宿舍。分配成功后生成分配记录学生或管理员可以查看宿舍楼栋、房间号、床位号。学生拿着凭证去楼管处领钥匙楼管在系统里点“入住确认”状态变为“已入住”。这两个闭环不是割裂的而是通过一个核心实体——学生——串联起来的。实践中我会在student表上维护一个status字段用枚举值标识“未报到/已报到/已分配/已入住”而不是把状态散落在多张表里这样既方便查询也方便做流程控制。1.3 这套系统的技术定位与适用人群从技术角度看这是一个非常典型的Spring Boot全栈练习项目涉及CRUD、复杂查询、事务管理、乐观锁并发控制、业务规则算法、权限区分。但从业务角度看它的核心难度不在增删改查而在于宿舍分配这种带约束条件的资源分配逻辑——这也是这个项目比普通管理系统更有含金量的地方。它适合三类人一是准备做毕业设计的计算机专业学生业务场景完整、复杂度适中、容易演示二是学校信息中心或相关系统开发人员可以直接参考业务建模思路三是想系统学习Spring Boot MyBatis Plus项目结构的开发者这套系统的分层写法比较规范适合模仿。2. 业务底盘设计从报到单到宿舍床位的领域建模2.1 核心实体划分业务建模是这类系统最容易被忽略、却最重要的一步。很多人一上来就写代码结果做到宿舍分配时发现数据关系理不清又回头改表。我先说清楚这套系统涉及的七个核心实体学生student)新生基础信息包括学号、姓名、性别、身份证号、院系、专业、班级、联系电话、报到状态等。院系/专业/班级department / major / classes组织架构数据用于分组和分配规则判断。宿舍楼dormitory_building楼栋基础信息包括楼栋编号、名称、楼管联系方式、总楼层数、性别属性等。宿舍房间dormitory_room房间信息包括所属楼栋、房号、楼层、房间类型四人间/六人间、性别属性、床位总数等。床位bed每个房间的具体床位实时维护是否被占用。报到记录register_record学生报到的时间、操作人、核验方式等。分配记录assign_record学生宿舍分配的记录包括分配时间、操作人、分配方式手动/自动、调整原因等。这些实体之间的关系是一个院系下有多个专业一个专业下有多个班级一个班级下有多个学生一栋宿舍楼有多间房一间房有多个床位一个学生报到后可以在特定时间产生一条分配记录关联到一个床位。这里有一个容易被新手弄混的概念学生和床位的关系。我建议不要直接在设计表的时候就把student_id写在bed表上而是通过assign_record这张关联表来维护“谁住在哪个床位”。原因很简单——分配可能被调整直接写死在bed表里会导致历史追溯困难而通过关联表可以完整记录每一次调整轨迹。2.2 宿舍楼栋与房间表怎么设计才不埋坑宿舍资源建模是宿舍分配系统的地基。我看过很多失败的设计问题都出在“房间表里直接塞床位”或者“楼栋表里直接维护房号列表”这种过度简化的建模上。先说字段设计的核心注意点。宿舍楼栋表dormitory_building需要维护的最关键字段是gender_type楼栋性别属性这个字段必须存在否则分配时会出现男生分到女生楼的严重事故。其次是总楼层数total_floors和房间总数total_rooms方便做统计展示。宿舍房间表dormitory_room除了所属楼栋ID、房号room_no、楼层floor之外必须单独维护一个gender_type字段而不是只依赖楼栋。为什么因为实际场景里极少数楼栋会出现楼层分隔或分区管理的情况单独维护房间层级可以让规则更灵活排查问题时也不会被“楼栋属性”误导。另一个关键字段是bed_count床位总数和room_type房间类型。床位表bed的核心是bed_no床位编号如“A001-301-1”表示A栋301房1号床和status状态字段0-空闲1-已占用2-维修中。我强烈建议把床位作为独立表来做而不是在房间表里用“bed_1_name、bed_1_status”这种冗余列设计。独立表的优势有三点一是查询空余床位非常方便一条SQL就能统计全局空余情况二是分配时操作粒度细可以精确到床三是将来如果要做“哪张床靠窗”“哪张床离门近”这类个性化偏好扩展起来很轻松。2.3 用状态字段管理报到与入住流程这套系统里最核心的业务状态流转我在前面提到过学生从录入到入住经过四个状态。具体设计时我会在student表里维护一个status字段枚举定义如下0待报到UNREGISTERED——新生信息已导入但还没到校办理报到1已报到REGISTERED——学生已完成报到流程但尚未分配宿舍2已分配ASSIGNED——系统已分配宿舍床位但学生还没去楼管领取钥匙3已入住CHECKED_IN——学生已在宿舍楼完成入住确认有人问为什么不单独建一张“报到状态表”来记录状态变化的历史我的答案是如果你需要完整追溯状态变化的时间线那就建一张student_status_log表每次状态变更插入一条记录。但这不是初版必须做的。第一版业务中只需要在student表维护当前状态即可等系统稳定运行后再增加日志表做审计追溯。很多开发者在第一版就过度设计结果报表没做出来时间全耗在边界场景里。2.4 表结构示例落地时直接可用的核心建表语句下面给出核心表的简化版建表SQL字段会根据实际项目微调但主体结构可以直接用。CREATE TABLE student ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, student_no varchar(32) NOT NULL COMMENT 学号, name varchar(32) NOT NULL COMMENT 姓名, gender tinyint NOT NULL DEFAULT 0 COMMENT 性别0-男 1-女, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, phone varchar(11) DEFAULT NULL COMMENT 手机号, class_id bigint DEFAULT NULL COMMENT 班级ID, department_id bigint DEFAULT NULL COMMENT 院系ID, major_id bigint DEFAULT NULL COMMENT 专业ID, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0-待报到 1-已报到 2-已分配 3-已入住, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; CREATE TABLE dormitory_building ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, building_no varchar(16) NOT NULL COMMENT 楼栋编号, building_name varchar(64) NOT NULL COMMENT 楼栋名称, gender_type tinyint NOT NULL DEFAULT 0 COMMENT 楼栋性别属性0-男 1-女, total_floors int NOT NULL DEFAULT 6, total_rooms int DEFAULT 0 COMMENT 总房间数, remark varchar(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_building_no (building_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宿舍楼栋表; CREATE TABLE dormitory_room ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, building_id bigint NOT NULL COMMENT 楼栋ID, room_no varchar(16) NOT NULL COMMENT 房间号如 301, floor int NOT NULL COMMENT 楼层, room_type tinyint NOT NULL DEFAULT 0 COMMENT 房间类型0-四人间 1-六人间, gender_type tinyint NOT NULL DEFAULT 0 COMMENT 性别属性0-男 1-女, bed_count int NOT NULL DEFAULT 4 COMMENT 床位总数, status tinyint NOT NULL DEFAULT 0 COMMENT 房间状态0-启用 1-停用, PRIMARY KEY (id), UNIQUE KEY uk_building_room (building_id, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宿舍房间表; CREATE TABLE bed ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, room_id bigint NOT NULL COMMENT 房间ID, bed_no varchar(32) NOT NULL COMMENT 床位编号, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0-空闲 1-已占用 2-维修中, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), UNIQUE KEY uk_room_bed (room_id, bed_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT床位表; CREATE TABLE assign_record ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, student_id bigint NOT NULL COMMENT 学生ID, bed_id bigint NOT NULL COMMENT 床位ID, room_id bigint NOT NULL, building_id bigint NOT NULL, assign_type tinyint NOT NULL DEFAULT 0 COMMENT 分配方式0-手动 1-自动推荐, operator_id bigint DEFAULT NULL COMMENT 操作人ID, reason varchar(255) DEFAULT NULL COMMENT 调整原因, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_student_id (student_id), KEY idx_bed_id (bed_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宿舍分配记录表;这套表结构看起来简单但注意几个细节bed表里有version字段用于乐观锁后面讲并发时会细说assign_record表冗余了building_id和room_id不追求第三范式但查询效率和展示方便度大幅提升。3. 宿舍分配的核心逻辑不只是“随机安排一个床位”3.1 分配规则的优先级宿舍分配是这套系统的灵魂。很多人在网上看教程以为宿舍分配就是order by某个字段然后依次取床位真这么做出来的系统根本没法用。宿舍分配是一个带多重约束的资源匹配问题。我结合学工处实际诉求整理出分配规则的优先级如下性别隔离最高优先级男生只能分到男生楼栋/男生房间女生只能分到女生楼栋/女生房间。这是一票否决项任何情况不允许跨性别分配。同班级相邻同一个班级的学生尽可能分在同一房间、同一楼层。这是辅导员的硬性要求查寝和班级管理都靠这个。同专业优先同班级不足一个房间时剩余床位由同专业的不同班级补齐而不是与完全无关的专业混住。楼层分布均匀同班级分配时优先从低楼层向高楼层填充避免出现一个班全住顶楼、另一个班全住一楼的情况。楼栋倾向某些院系有指定的宿舍楼栋这时可以在楼栋维度上做预绑定。这个优先级顺序非常重要因为算法实现时就是按照这个顺序写判断逻辑的。有人会问为什么同专业优先级排在楼层均匀前面因为实践中学工处认为专业相同但班级不同的学生住在一起遇到调课、实训安排等通知时沟通成本最低。楼层均匀只是居住体验优化优先级低于管理便利性。3.2 三种分配模式的取舍实际开发中宿舍分配不能只有一个“全自动”按钮因为现场总有意料之外的情况。我设计的分配模块支持三种模式模式一手动分配。管理员先选择楼栋、楼层、房间系统展示该房间的空余床位列表管理员手工选择一个床位并指定学生。这种模式用于特殊情况比如学生因身体原因需要住下铺、需要与特定室友同住等。模式二系统自动推荐。管理员先选择一批学生通常是同班级的批量勾选然后点“自动分配”系统按照3.1节中的规则自动查找匹配的房间和床位给出推荐结果。管理员可以查看推荐结果后确认或调整。模式三先到先得自助选房。新生报到后通过微信小程序或浏览器进入选房页面系统展示可选房间和床位学生自己抢床位。这个模式对并发要求更高适合报名人数多、学校愿意让学生自主选择的场景。第一版建议先做手动模式和自动推荐模式自助选房放到二期。原因很现实自助选房涉及面向学生的前端页面、高并发下的抢座机制、舆情处理学生抢不到好床位会投诉这些都需要专门的运营配套。而自动推荐模式已经能解决90%的分配需求。3.3 推荐算法的伪代码与实现思路自动推荐模式的核心逻辑我用伪代码描述一下这样更直观。function autoAssign(studentList, roomList): for student in studentList: // 第一步找到符合性别的所有可用房间 candidateRooms filter(roomList, room.gender_type student.gender AND room.status 启用 AND room.availableBedCount() 0) // 第二步优先找同班级学生已经入住/已分配的房间 sameClassRooms findRoomsWhereClassmatesAlreadyAssigned( candidateRooms, student.classId) // 第三步找同专业其他班级学生入住的房间 sameMajorRooms findRoomsWhereMajorMatesAlreadyAssigned( candidateRooms, student.majorId, student.classId) // 第四步如果以上都没有找空房间优先低楼层 emptyRooms findEmptyRooms(candidateRooms, student.majorId) // 按优先级选房间 targetRoom firstAvailable(sameClassRooms, sameMajorRooms, emptyRooms) // 在目标房间里挑选空余床位默认从1号床开始按顺序 targetBed targetRoom.firstAvailableBed() // 生成分配记录 createAssignRecord(student, targetBed) updateBedStatus(targetBed, 已占用) updateStudentStatus(student, 已分配)这个思路的核心是“先找有同班同学的地方没有就找同专业的再没有才开新房”。这样做的好处是班级在一个房间的入住率会持续增加直到住满后再开新房间实现了自然聚合。实现时还有几个细节要处理批量分配时的房间锁定如果一个房间还剩2个床位而本次批了3个学生算法要能判断当前房间最多只能再放2个人剩余1人继续找下一个房间。空房间的“开房”操作找到空房间后一次只填充1个人还是填满整个房间我的做法是如果同批分配的学生中同班级剩余人数大于等于房间剩余床位数则优先填满该房间如果剩余人数不够填满一个房间则这个房间暂时不开房继续找已有同学的房间。这个逻辑是从「班级尽量集中」原则推导出来的。边界情况某个班级的学生人数是单数最后一个学生注定无法和同班同学住同一间此时算法会先找同专业的房间如果没有则随机分到同楼层、同楼栋有床位且性别匹配的房间把规则要求放到最低级别。3.4 并发场景下如何避免“一张床位被抢两次”这是宿舍分配系统里最容易被忽略、也最要命的问题。想象一下两个辅导员同时操作一个给男生宿舍A栋301分配了1号床另一个也在同一时间把同一个床位分配给了另一个学生。如果代码没有并发控制数据库里就可能出现一个床位被两个学生关联后面处理起来非常痛苦。解决方式通常有两种。第一种是数据库层面的悲观锁例如在查询房间可用床位时使用SELECT ... FOR UPDATE把符合条件的行锁住。优点是绝对安全缺点是并发量大时容易出现锁等待而且如果事务里查询范围过大锁的粒度过粗性能会下降。第二种是乐观锁利用版本号实现。我在bed表设计时特意加了version字段分配的SQL长这样UPDATE bed SET status 1, version version 1 WHERE id #{bedId} AND status 0 AND version #{oldVersion}执行完这条语句后检查受影响的行数。如果行数为0说明床位已经被别人抢走程序需要重新查找可用床位或提示“床位已被占用”。这种方式在业务量不是极大的场景下完全够用而且实现简单、性能好。实践中我是把“查询可分配床位”和“更新床位状态”放在同一个事务里且更新语句必须携带条件约束status 0这样即使两个请求同时查到同一个床位也只有一个更新成功。这里还有一个细节不要只靠version判断status条件也要写上双保险。为什么因为如果有人手动改了数据库数据version字段可能被漏维护而status条件永远有效。4. Spring Boot 核心模块的落地写法与踩坑记录4.1 技术选型为什么是 Spring Boot MyBatis Plus MySQL这套系统的技术栈基本围绕“快速开发、易维护、易演示”展开。Spring Boot作为基础框架优势不用多说自动配置、内置Tomcat、生态成熟单体应用场景下是最稳妥的选择。持久层我选了MyBatis Plus而不是原生MyBatis理由很直白这个系统大量涉及单表CRUD和简单的条件查询MyBatis Plus的BaseMapper能省掉一大半重复的Mapper XML工作内置的分页插件也足够支撑宿舍列表这种数据量的分页查询。如果你平时用Spring Data JPA更顺手这套逻辑也一样能实现只是部分查询语句的写法不同。数据库用MySQL原因是院校场景几乎全员MySQL运维成本最低而且后面如果要接报表系统、数据仓库MySQL的生态最成熟。表结构已经在第二章给出字符集一律utf8mb4避免生僻字报错。前端方面初版我建议直接用Thymeleaf Bootstrap。没别的原因就是部署简单、学习成本低、适合后台管理场景。如果你打算做前后端分离Spring Boot只提供REST API前端用Vue Element Plus那也是可以的只是在源码包里的目录结构和部署方式会不同。我个人的意见是毕业设计想展示前后端分离可以上Vue校内实际使用BootstrapThymeleaf更快更稳。4.2 项目结构与关键配置推荐使用标准的Maven单模块结构不要一上来就搞多模块业务量没到那个程度多模块反而增加理解成本。dormitory-system/ ├── pom.xml ├── src/main/java/com/example/dormitory/ │ ├── DormitoryApplication.java │ ├── common/ │ │ ├── Result.java // 统一返回结果 │ │ ├── ResultCode.java // 返回码枚举 │ │ └── GlobalExceptionHandler.java │ ├── config/ │ │ ├── MybatisPlusConfig.java // 分页插件配置 │ │ └── WebMvcConfig.java │ ├── controller/ │ │ ├── StudentController.java │ │ ├── DormitoryController.java │ │ ├── AssignController.java │ │ └── LoginController.java │ ├── entity/ │ │ ├── Student.java │ │ ├── DormitoryBuilding.java │ │ ├── DormitoryRoom.java │ │ └── Bed.java │ ├── mapper/ │ │ ├── StudentMapper.java │ │ └── BedMapper.java │ ├── service/ │ │ ├── StudentService.java │ │ ├── AssignService.java │ │ └── impl/... │ └── utils/ ├── src/main/resources/ │ ├── application.yml │ ├── mapper/ │ │ └── AssignMapper.xml │ └── templates/ │ ├── student_list.html │ ├── assign.html │ └── ... └── src/main/sql/ └── init.sqlapplication.yml的关键配置项如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dormitory_system?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password thymeleaf: cache: false mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意几个细节。数据源URL里必须带serverTimezoneAsia/Shanghai否则MySQL 8.x驱动会报时区错误。map-underscore-to-camel-case必须开启数据库字段user_name才能自动映射到实体类userName属性否则一堆查询结果是null。MyBatis Plus的分页插件配置如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个拦截器不配置的话Page对象的分页查询不会自动拼接LIMIT语句查出来的是全表数据。这是新手最容易踩的坑没有之一。4.3 核心代码从实体到Service的完整链路实体类以Student为例使用MyBatis Plus注解。Data TableName(student) public class Student { TableId(type IdType.AUTO) private Long id; private String studentNo; private String name; private Integer gender; private String idCard; private String phone; private Long classId; private Long departmentId; private Long majorId; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }Service层是重点。我以自动分配宿舍为例写一下核心Service实现的关键逻辑。Service public class AssignServiceImpl implements AssignService { Autowired private StudentMapper studentMapper; Autowired private BedMapper bedMapper; Autowired private RoomMapper roomMapper; Autowired private AssignRecordMapper assignRecordMapper; Override Transactional(rollbackFor Exception.class) public AssignResult autoAssign(ListLong studentIds) { // 1. 查询学生列表 ListStudent students studentMapper.selectList( new LambdaQueryWrapperStudent() .in(Student::getId, studentIds) .eq(Student::getStatus, 1)); // 只允许“已报到”状态的学生参加分配 if (students.isEmpty()) { throw new BusinessException(没有可分配的学生); } // 2. 按班级分组班级内排序后逐个分配 MapLong, ListStudent classGroup students.stream() .collect(Collectors.groupingBy(Student::getClassId)); ListAssignResultItem resultItems new ArrayList(); for (Map.EntryLong, ListStudent entry : classGroup.entrySet()) { ListStudent classStudents entry.getValue(); for (Student student : classStudents) { // 3. 核心分配逻辑伪代码细节略 Bed targetBed findBestBed(student); if (targetBed null) { throw new BusinessException(学生 student.getName() 分配失败无可用床位); } // 4. 乐观锁更新床位状态 int rows bedMapper.updateStatusWithVersion(targetBed.getId(), 0, 1, targetBed.getVersion()); if (rows 0) { throw new BusinessException(床位 targetBed.getBedNo() 已被占用请重试); } // 5. 更新学生状态 studentMapper.update(null, new LambdaUpdateWrapperStudent() .eq(Student::getId, student.getId()) .set(Student::getStatus, 2)); // 6. 记录分配轨迹 assignRecordMapper.insert(new AssignRecord(student.getId(), targetBed.getId(), targetBed.getRoomId(), targetBed.getBuildingId(), 1, currentUserId(), null)); resultItems.add(new AssignResultItem(student, targetBed)); } } return AssignResult.success(resultItems); } }这段代码里有一个非常容易被忽略的点整个方法必须加Transactional而且分配失败要抛出异常触发回滚。我一个朋友第一次写这个功能时没加事务结果分配了20个学生第15个学生分配失败前面14个床位已经改成“已占用”状态了数据就乱了。事务的意义不是锦上添花是这类操作的基本要求。4.4 报错排查与开发期避坑开发过程中我遇到过几个比较典型的问题列出来供参考。第一个是后端返回的日期格式不对。默认的LocalDateTime序列化结果是“2025-01-01T10:20:30”前端拿到以后还要再处理。最简单的解决方式是在application.yml里配置统一格式化spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第二个是乐观锁生效但没有正确回滚。有一次我发现updateStatusWithVersion返回0后抛出了异常但前面已变更的学生状态没有回滚。排查后发现是我在同一个类里调用了autoAssign方法而Spring的事务代理是通过AOP实现的类内部自调用不会走代理事务注解失效。解决方案很简单把需要事务的方法拆到另一个Service类里通过Spring注入后跨类调用。这个坑非常有代表性不只是这个项目会遇到任何Spring事务场景都要警惕。第三个是MyBatis Plus的字段映射问题。如果实体字段用is_xxx命名MyBatis Plus会默认映射为xxx导致查询结果全是null。我建议所有布尔字段统一用xxxFlag或直接自定义status字段。这个问题排查起来非常费劲因为不报错只是数据不对。5. 源码里值得细看的点与进一步扩展的方向5.1 源码里几个值得学习的写法这套系统的源码里除了业务功能本身还有几个写法我认为值得反复看。一个是统一返回结果与全局异常处理。所有Controller接口都返回Result对象包含code、message、data三个字段。自定义BusinessException配合RestControllerAdvice全局兜底前端只需处理一个标准的返回结构不用为每种异常写重复判断。这套写法在真实企业项目里几乎是标配。另一个是枚举状态与业务状态机的可视化。前文提到的学生status字段在代码里用枚举类维护不散落魔法数字。如下所示public enum StudentStatus { UNREGISTERED(0, 待报到), REGISTERED(1, 已报到), ASSIGNED(2, 已分配), CHECKED_IN(3, 已入住); private final Integer code; private final String desc; StudentStatus(Integer code, String desc) { this.code code; this.desc desc; } }这样写的好处是在业务代码里可以做状态流转校验比如“分配宿舍”接口只允许status为1的学生调用避免状态乱跳。还有一点是密码字段的安全处理。虽然这个系统的登录主体是管理员但如果后续开放学生登录密码千万不要明文存储至少用BCrypt哈希后入库。Spring Security自带的BCryptPasswordEncoder可以直接用或者引入hutool的BCrypt工具类。5.2 上线前要补的功能与建议如果这套系统真的要在学校环境里投入使用有几个功能模块建议优先补充。第一是宿舍调整审批流程。实际运行中学生换宿舍、调床位是高频需求。我建议为“调整申请—辅导员审批—楼管执行”建立一个轻量级工作流每条调整都写入assign_record并记录reason便于事后查询。没有这个功能宿舍管理员会被电话和微信淹没。第二是统计报表模块。至少要有各院系报到率统计、各楼栋入住率统计、各专业住宿分布、未报到学生名单。这些统计可以直接用MySQL的GROUP BY实现不需要额外引入报表工具但对管理者来说价值巨大。第三是批量导入功能。新生名单从教务系统导出后通常是一个几百上千行的Excel文件手动录入根本不现实。用EasyExcel或POI做一个导入模板校验学号重复、手机号格式、院系班级是否存在然后批量写入student表。第四是面向新生的查询入口。哪怕是只读的让新生通过输入身份证号或学号查询自己的宿舍分配结果、报到流程说明也能极大减少报到当天工作人员的压力。5.3 我对这套系统的开发体会从需求调研到第一版上线我前后大约花了两周半的时间。第一周基本耗在业务梳理和建表上真正写代码的时间反而没那么多。这个项目的难点从来不是Spring Boot本身而是业务规则的理解和建模。宿舍分配这个业务信息量不大但约束条件多、异常情况多稍不留神就会在某个边界case上翻车。我特别想强调一点不管你用不用我这套表结构宿舍分配系统里“分配记录可追溯”这个设计原则一定要坚持。当时学工处老师提出一个需求说学生家长打电话来问“为什么我家孩子住六楼不是二楼”如果你没有历史分配记录根本答不上来。有了assign_record这张表就能清楚地看到分配时间、分配方式、操作人甚至能追溯到当时的推荐算法参数。如果你正在做类似的毕设或校内系统我的建议是先把手动分配和自动推荐跑通再考虑自助选房。自动推荐算法从简单的顺序填充开始等主流程稳定了再逐步增加规则优先级。不要一上来就想把所有规则塞进去那样只会把自己绕晕。这套系统的后续扩展空间也很大可以对接企业微信消息推送分配完成后自动通知辅导员可以对接校园一卡通接口实现扫码入住可以增加宿舍卫生评比、维修上报模块把宿舍管理平台化。框架搭好了剩下的都是迭代问题。