ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java学生宿舍管理系统实战:Spring Boot+MyBatis-Plus业务落地指南

Java学生宿舍管理系统实战:Spring Boot+MyBatis-Plus业务落地指南 简介这是一套基于Java技术栈开发的学生宿舍管理系统毕业设计项目源码面向计算机专业本科生及Java初学者解决高校宿舍管理中学生、楼宇、宿舍、入住等多角色协同与数据维护的实际问题。资源包共661个文件以400个JavaScript前端交互脚本、46个CSS样式文件支撑Bootstrap响应式界面35个Java后端业务类与28个XML配置文件构成SpringBoot核心架构辅以MySQL建表SQL、HTML页面及少量MP4演示视频整体压缩包大小为72.37MB。已有467人学习下载资源提供完整可运行工程含MySQL 5/8双版本适配包、管理员/宿管/学生三端权限控制逻辑、全量增删改查及Excel导入导出功能代码结构清晰、注释规范适合作为课程设计参考、毕设二次开发或SpringBootBootstrap全栈实践范例。1. 为什么一个“学生宿舍管理系统”能成为Java工程师的练手分水岭不是所有Java Web项目都叫“学生宿舍管理系统”但几乎所有刚学完Spring Boot、MyBatis-Plus、Thymeleaf的开发者都会在简历里写上这么一句“独立开发基于Java的学生宿舍管理系统”。它不像电商或IM系统那样卷架构也不像算法题那样纯考逻辑——它是一块真实业务毛细血管级的试金石要管6栋楼、327间房、1892名学生、43位宿管员要处理退宿时水电费自动结算、调宿时床位状态实时锁死、晚归登记需对接校园门禁时间戳还要让辅导员导出Excel时保留合并单元格格式让后勤处看报表时能按楼栋/年级/性别三维度交叉筛选。这不是CRUD堆砌而是用Java把现实里的管理规则翻译成可执行、可回滚、可审计的代码逻辑。你写的不是“增删改查”是“张三搬进302室后原301室的空床状态必须同步失效且该床位在当日22:00后不可再被分配”——这种带时间窗、带状态链、带角色权限的业务流才是Java工程师从培训班走向真实产线的关键跃迁点。适合刚跑通Spring Boot启动页、想验证自己能否把“学过的技术”拧成一股绳的人。2. 用Spring Boot MyBatis-Plus快速搭起骨架从零生成实体、Mapper、Service到Controller2.1 选型理由为什么不用纯JDBC或Hibernate而坚定用MyBatis-Plus纯JDBC写法太原始每张表都要手写PreparedStatement、手动setString()、手动rs.next()光是宿舍表dormitory 学生表student 入住记录表check_in_record三张关联表光是查“某楼某层所有未满员房间及当前入住学生名单”就要写20行SQL15行结果映射改个字段就得全量重测。Hibernate又太重二级缓存配置复杂、N1查询陷阱隐蔽、懒加载异常堆栈难定位。而MyBatis-Plus的TableName、TableField注解LambdaQueryWrapper让你用Java方法名直接表达业务意图——比如lambdaQueryWrapper.eq(Student::getDormId, 302).orderByDesc(Student::getCheckInTime)比写SQL更贴近自然语言且编译期就能检查字段是否存在。更重要的是它的CodeGenerator能根据数据库反向生成全套代码省掉80%模板代码把精力留给真正的业务逻辑。2.2 用MyBatis-Plus Generator自动生成基础代码三步落地提示此步骤要求已建好MySQL数据库推荐8.0并创建好dorm_db库。若未建库请先执行CREATE DATABASE dorm_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;第一步在pom.xml中引入MyBatis-Plus代码生成器依赖注意版本对齐Spring Boot 2.7.x对应MP 3.5.3.1dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.3.1/version /dependency dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId /dependency第二步编写代码生成器主类放在src/test/java下运行后删掉即可public class CodeGenerator { public static void main(String[] args) { // 1. 数据源配置 AutoGenerator generator new AutoGenerator(); DataSourceConfig dataSourceConfig new DataSourceConfig() .setUrl(jdbc:mysql://localhost:3306/dorm_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue) .setUsername(root) .setPassword(your_password) .setDriverName(com.mysql.cj.jdbc.Driver); generator.setDataSource(dataSourceConfig); // 2. 全局配置 GlobalConfig globalConfig new GlobalConfig() .setOutputDir(System.getProperty(user.dir) /src/main/java) // 输出路径 .setAuthor(your_name) .setOpen(false) // 生成后不打开文件夹 .setSwagger2(true); // 启用Swagger2注解 generator.setGlobalConfig(globalConfig); // 3. 包配置重点按模块分包 PackageConfig packageConfig new PackageConfig() .setParent(com.dorm) // 根包名 .setEntity(entity) // 实体类包 .setMapper(mapper) // Mapper接口包 .setService(service) // Service接口包 .setServiceImpl(service.impl) // Service实现包 .setController(controller); // Controller包 generator.setPackageInfo(packageConfig); // 4. 策略配置指定表名开启Lombok、逻辑删除、乐观锁等 StrategyConfig strategyConfig new StrategyConfig() .addInclude(dormitory, student, check_in_record, dorm_staff) // 指定生成哪些表 .setNaming(NamingStrategy.underline_to_camel) // 数据库下划线转驼峰 .setColumnNaming(NamingStrategy.underline_to_camel) .setEntityLombokModel(true) // 开启Lombok .setRestControllerStyle(true) // 生成RestController .setControllerMappingHyphenStyle(true); // URL支持连字符 generator.setStrategy(strategyConfig); generator.execute(); } }第三步运行main方法观察控制台输出。成功后你会在src/main/java/com/dorm/下看到entity/Dormitory.java含TableName(dormitory)、TableId(type IdType.AUTO)、TableField(floor_num)等注解mapper/DormitoryMapper.java继承BaseMapperDormitory自带selectList()等15方法service/DormitoryService.java接口定义service/impl/DormitoryServiceImpl.java默认实现含Service和Autowired private DormitoryMapper mapper;controller/DormitoryController.java带RestController RequestMapping(/dormitory)含GetMapping等基础REST方法。关键参数说明addInclude(...)必须显式列出表名否则生成空包。建议初期只加dormitory和student两张核心表跑通再加setEntityLombokModel(true)生成Data NoArgsConstructor AllArgsConstructor避免手写getter/settersetRestControllerStyle(true)生成RestController而非Controller省去ResponseBody重复标注setControllerMappingHyphenStyle(true)使URL路径为/dormitory-floor-list而非/dormitoryFloorList更符合RESTful习惯。3. 把“宿舍分配”变成可执行逻辑用状态机事务边界实现业务原子性3.1 为什么不能用简单UPDATE——宿舍分配的三个隐性约束很多新手写分配逻辑就是一条SQLUPDATE student SET dorm_id ? WHERE id ?。但真实场景中这会立刻翻车约束1床位容量限制。302室是4人间当前已有3人入住第4人可分配第5人再分配就超员约束2状态锁死。学生A正在办理退宿手续状态为CHECKING_OUT此时学生B不能被分配到同一房间约束3操作可逆性。分配失败时不能只回滚SQL还要还原前端页面的“已选中”状态、释放临时占用的床位ID。所以必须用服务层事务内存状态校验数据库行锁三重保障。我们以DormitoryService.allocateStudentToRoom(Long studentId, Long dormId)为例拆解其骨架Transactional(rollbackFor Exception.class) public Result allocateStudentToRoom(Long studentId, Long dormId) { // 1. 查询学生与宿舍加FOR UPDATE锁住这两行 Student student studentMapper.selectById(studentId); Dormitory dorm dormitoryMapper.selectById(dormId); // 2. 内存校验宿舍是否满员 int currentCount checkInRecordMapper.selectCount( new QueryWrapperCheckInRecord().eq(dorm_id, dormId).eq(status, ACTIVE) ); if (currentCount dorm.getCapacity()) { return Result.fail(宿舍 dorm.getBuilding() - dorm.getRoomNumber() 已满员); } // 3. 内存校验学生是否处于可分配状态 if (!NORMAL.equals(student.getStatus())) { return Result.fail(学生状态异常 student.getStatus()); } // 4. 执行分配插入入住记录非UPDATE学生表 CheckInRecord record new CheckInRecord(); record.setStudentId(studentId); record.setDormId(dormId); record.setStatus(ACTIVE); record.setCheckInTime(LocalDateTime.now()); checkInRecordMapper.insert(record); // 5. 更新学生当前宿舍ID仅作冗余索引非主数据源 student.setDormId(dormId); studentMapper.updateById(student); return Result.success(分配成功入住时间 record.getCheckInTime()); }逻辑说明Transactional保证整个方法内所有DB操作要么全成功要么全回滚selectById后不立即更新而是先做内存判断避免“先查后更”导致的ABA问题关键动作是插入check_in_record记录而非UPDATEstudent表——因为一个学生可能多次入住不同宿舍历史记录必须保留student.setDormId()只是冗余字段用于快速查询“某学生当前住哪”真正权威数据在check_in_record表中通过WHERE student_id ? AND status ACTIVE获取。3.2 用Redis分布式锁防并发超分当10个宿管同时点“分配”上面代码在单机环境OK但部署到多节点时两个请求几乎同时查到“302室还有1个空位”然后都执行插入导致超员。解决方案用Redis锁住dorm:302:allocate这个key。// 在allocateStudentToRoom方法开头加入 String lockKey dorm: dormId :allocate; Boolean isLocked redisTemplate.opsForValue() .setIfAbsent(lockKey, locked, Duration.ofSeconds(10)); if (!isLocked) { return Result.fail(操作过于频繁请稍后再试); } try { // 原有业务逻辑... } finally { // 必须用Lua脚本删除防止误删其他线程的锁 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(script, Boolean.class), Collections.singletonList(lockKey), locked); }参数说明setIfAbsent(..., Duration.ofSeconds(10))锁有效期10秒避免死锁finally块中的Lua脚本确保只有加锁者才能删锁防止A线程锁过期后B线程删掉A刚续上的新锁若Redis不可用降级方案是直接抛异常Result.fail(系统繁忙请稍后重试)绝不跳过锁逻辑。4. 避坑学生宿舍管理系统上线前必踩的5个坑附现象、原因、解法4.1 现象导出Excel时中文全是乱码数字列显示为科学计数法原因Apache POI默认用XSSFWorkbook.xlsx生成文件但未设置单元格样式且HTTP响应头未声明Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charsetutf-8。解法① 在导出方法中显式设置字体和数据格式XSSFCellStyle style workbook.createCellStyle(); XSSFFont font workbook.createFont(); font.setFontName(微软雅黑); font.setFontHeightInPoints((short) 10); style.setFont(font); style.setDataFormat(workbook.createDataFormat().getFormat()); // 强制文本格式 cell.setCellStyle(style);② Controller返回时添加响应头response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charsetutf-8); response.setHeader(Content-Disposition, attachment;filename URLEncoder.encode(宿舍导出.xlsx, UTF-8));4.2 现象晚归登记时间精确到秒但查询“今日晚归”时漏掉23:59:59的数据原因前端传参date2024-06-15后端用LocalDate.parse(date).atStartOfDay()生成2024-06-15T00:00:00查询条件WHERE check_time ? AND check_time ?中结束时间设为atTime(23,59,59)但数据库存储用的是datetime(3)毫秒级23:59:59.999被截断。解法统一用LocalDateTime的plusDays(1).minusNanos(1)生成截止时间LocalDate date LocalDate.parse(param.getDate()); LocalDateTime start date.atStartOfDay(); LocalDateTime end date.plusDays(1).minusNanos(1); // 精确到纳秒 queryWrapper.between(check_time, start, end);4.3 现象MyBatis-Plus分页插件查总数时SQL报错Incorrect argument to native function: COUNT(*)原因MySQL 8.0.22默认关闭sql_mode中的ONLY_FULL_GROUP_BY但MyBatis-Plus的PageHelper在count查询时未自动去除GROUP BY字段导致count语句非法。解法在application.yml中配置分页插件忽略group bymybatis-plus: configuration: default-scripting-language: org.apache.ibatis.scripting.xmltags.XMLLanguageDriver pagination: count-column: count(*) # 关键禁用自动count优化 optimize-count-sql: false或升级MyBatis-Plus至3.5.3.1该版本已修复此问题。4.4 现象学生修改手机号后登录态失效但Token未主动踢出原因JWT Token中存了手机号作为用户标识手机号变更后旧Token仍可用直到过期。解法在StudentService.updateMobile()中主动清除Redis中该用户的Token// 删除以手机号为key的所有token假设存为 mobile:138****1234:token redisTemplate.delete(mobile: oldMobile :token); redisTemplate.delete(mobile: newMobile :token); // 新号也清一次防重 // 同时更新数据库student表的salt字段使旧密码hash失效 student.setSalt(UUID.randomUUID().toString().substring(0, 8)); studentMapper.updateById(student);4.5 现象调宿操作中学生A从301调到302但301室状态未更新为“空闲”原因调宿是两步操作先删原记录再插新记录若第一步成功第二步失败事务回滚但前端已刷新页面显示“已调宿”造成状态不一致。解法改为单条SQL原子更新用INSERT ... ON DUPLICATE KEY UPDATEINSERT INTO check_in_record (student_id, dorm_id, status, check_in_time) VALUES (?, ?, ACTIVE, NOW()) ON DUPLICATE KEY UPDATE dorm_id VALUES(dorm_id), check_in_time NOW(), status ACTIVE;前提是student_id为唯一索引业务要求一个学生只能有一条ACTIVE记录。这样无论执行几次结果都幂等。5. 让系统真正“活”起来用WebSocket实现实时床位状态广播与消息推送5.1 为什么需要WebSocket——传统轮询的三大硬伤宿舍管理员盯着后台页面等学生提交退宿申请后手动刷新平均2分钟刷一次晚归登记后辅导员手机收不到提醒要等第二天晨会才知晓调宿操作后同楼层其他宿管不知情可能重复分配。这些场景暴露了HTTP短连接的本质缺陷延迟高轮询间隔设短如5秒服务器压力陡增设长如30秒业务感知滞后无效流量大90%的轮询请求返回“无更新”带宽和CPU白耗状态不一致A宿管刚分配完302室B宿管刷新页面前两人看到的床位状态不同。WebSocket建立长连接后服务端可主动推送{type:BED_UPDATE,dormId:302,available:2}前端收到立即更新UI无需等待。5.2 Spring Boot集成WebSocket三步完成双向通信第一步添加依赖pom.xmldependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency第二步配置WebSocket端点WebSocketConfig.javaConfiguration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new DormWebSocketHandler(), /ws/dorm-status) .setAllowedOrigins(*); // 生产环境请替换为具体域名 } }第三步编写处理器DormWebSocketHandler.java重点实现afterConnectionEstablished和handleTextMessageComponent public class DormWebSocketHandler extends TextWebSocketHandler { private final MapString, WebSocketSession sessions new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String userId session.getHandshakeAttributes().get(userId).toString(); sessions.put(userId, session); // 连接建立后主动推送当前所有宿舍状态快照 sendAllDormStatus(session); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 接收前端心跳或指令如 {cmd:REFRESH_STATUS} String payload message.getPayload(); if (payload.contains(REFRESH_STATUS)) { sendAllDormStatus(session); } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { sessions.remove(session.getId()); } // 关键当宿舍状态变更时如分配/退宿调用此方法广播 public void broadcastDormUpdate(Long dormId, Integer availableCount) { String msg String.format({\type\:\BED_UPDATE\,\dormId\:%d,\available\:%d}, dormId, availableCount); sessions.values().forEach(s - { try { if (s.isOpen()) { s.sendMessage(new TextMessage(msg)); } } catch (IOException e) { sessions.remove(s.getId()); } }); } private void sendAllDormStatus(WebSocketSession session) throws IOException { ListDormitory dorms dormitoryMapper.selectList(null); StringBuilder sb new StringBuilder([); for (int i 0; i dorms.size(); i) { Dormitory d dorms.get(i); int available getAvailableCount(d.getId()); // 查询当前空床位数 sb.append(String.format({\id\:%d,\available\:%d}, d.getId(), available)); if (i dorms.size() - 1) sb.append(,); } sb.append(]); session.sendMessage(new TextMessage(sb.toString())); } }前端监听示例Vue3 setup语法onMounted(() { const socket new WebSocket(ws://localhost:8080/ws/dorm-status?userId${userId}); socket.onmessage (event) { const data JSON.parse(event.data); if (data.type BED_UPDATE) { // 找到对应宿舍DOM更新剩余床位数 document.getElementById(dorm-${data.dormId}).innerText 剩余${data.available}床; } }; });5.3 生产级加固连接保活、异常重连、消息幂等保活机制客户端每30秒发{cmd:PING}服务端收到后回复{cmd:PONG}超时60秒未收到则主动close重连策略前端WebSocket onclose后指数退避重连1s→2s→4s→8s最多5次失败后提示“网络异常请刷新页面”消息幂等服务端对BED_UPDATE消息加msgId字段前端用Map缓存最近10条msgId重复ID直接丢弃。我当年在高校信息中心部署时最深的教训是别信“测试环境OK”一定要用真实宿管账号在Chrome多个标签页同时操作模拟10人并发点击“分配”按钮看WebSocket是否真能扛住。有次没加连接数限制300个长连接把Tomcat线程池打满整个系统假死。后来加了session.setMaxIdleTimeout(300_000)5分钟无消息自动断开问题消失。希望帮到你。本文还有配套的精品资源点击获取
返回列表