1. 为什么宿舍管理系统值得自己写一套:业务痛点与功能边界
1.1 纸质管理时代的三座大山
先说说我遇到的实际场景。之前帮某高校的宿管中心做内部系统时,办公室里还堆着几大摞纸质表格:新生入住登记表、退宿审批单、报修登记簿、卫生检查评分表。宿管阿姨每天的工作有一半是翻本子、打电话、贴通知。学生要查自己住哪间宿舍,得跑一趟楼下值班室;报修漏水,填写纸质单子之后三天没人来修也没地方催。这种状态其实就是典型的"业务在线下跑,数据在纸上存,信息在电话里传"。
真正促使我决定用 Java 技术栈做一套线上宿舍管理系统的原因,是三个很具体的痛点:第一,床位信息不透明。招生季宿管中心必须一个个打电话确认哪些床位空着,Excel 表多人维护,版本混乱,经常出现"系统说有床,现场没床"的情况。第二,报修流程没有闭环。学生报修后无法跟踪进度,维修工单靠人肉分发,做没做、做得好坏全凭记忆。第三,检查数据没有沉淀。卫生检查、违规用电记录、晚归登记这些数据散落在不同纸张上,学期末做文明宿舍评比时,统计工作量巨大。
基于这些痛点,我搭建了一套基于 SSM 框架的线上宿舍管理系统。这套系统解决的核心问题就是把"宿舍资源"和"人"的关系线上化:学生登录后可以看自己的宿舍信息、提交报修、查看通知;宿管员可以维护本楼栋的宿舍床位、登记检查记录、处理报修工单;系统管理员负责楼栋与宿舍的宏观管理、账号配置、数据统计。整个系统走完一轮开发之后,前前后后大概 8 千行代码,非常适合作为 Java Web 方向的实战项目来参考,也适合高校信息中心内部落地使用。
1.2 系统应该覆盖的业务范围
如果你也打算做宿舍管理系统,第一步不是写代码,而是把功能边界画清楚。宿舍管理系统很容易做"大而全",但实际上真正高频使用的模块其实就这几块:
- 基础档案:学生信息、宿舍楼栋、宿舍房间、床位信息。这是所有业务的地基。
- 住宿管理:新生入住分配、床位调换、退宿登记、空床位查询。
- 报修管理:学生提交报修单,宿管派单,维修工处理,学生确认,状态全程可追踪。
- 日常检查:卫生检查打分、违规电器记录、晚归登记,支持按月检索。
- 通知公告:管理员发布停水停电、安全检查、宿舍搬迁等通知,学生端按时间线查看。
- 系统管理:用户账号、角色分配、楼栋权限、基础数据字典。
这套划分本身就是从业务流程中长出来的。我做系统的第一版时贪多,把心理咨询预约、二手交易都塞了进去,结果数据库耦合严重,开发周期拖了两周。后来砍掉非核心功能,才真正跑顺。所以我想强调的是:宿舍管理系统最值得做深做透的,是"床位-学生-工单"这条主线,其他功能可以先砍掉或做成二期。
2. SSM技术选型与工程脚手架搭建:为什么这套组合依然能打
2.1 SSM组合的现实意义
讲选型之前先说明一下背景。Spring、SpringMVC、MyBatis 这个组合,在 Spring Boot 大行其道的今天,听起来有点"古早"。但我在实际做这个项目时仍然选择 SSM,有几个非常现实的理由。第一,国内很多高校的机房课和校内实训项目仍然以 SSM 为教学主干,作为学生项目或课程设计能直接匹配课程要求。第二,SSM 因为配置显式、容器结构清晰,对理解 IOC 容器、AOP、事务传播、SQL 与对象映射这些底层概念非常有帮助,很多公司老项目的维护也还是这套架构。第三,SSM 对运行环境要求低,Tomcat 8.5 搭配 JDK 1.8 就能稳定跑,部署非常简单。
整个系统的技术选型如下表:
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 视图层 | JSP + Bootstrap 4 + jQuery | 服务端渲染为主,表格用 Bootstrap Table |
| 控制层 | SpringMVC 5 | 负责请求映射、参数绑定、校验 |
| 业务层 | Spring 5 | 管理 Bean,声明式事务 |
| 持久层 | MyBatis 3 | 手写 SQL,掌控每一条查询 |
| 数据库 | MySQL 5.7 | 存储业务数据,InnoDB 引擎 |
| 构建工具 | Maven 3.6 | 依赖管理与打包 |
| 部署容器 | Tomcat 8.5 | 运行 Web 应用 |
这里我想特别解释一个问题:既然 Spring Boot 这么方便,为什么我坚持不用?因为我在这个项目中特别需要理解"框架是怎么把请求送进业务层,再落到数据库"的完整链路。Spring Boot 的自动配置确实省事,但其间隐藏的细节太多,一旦遇到请求进不来、Mapper 扫不到这类问题,排查起来反而不如 SSM 直观。如果你是第一次做完整 Java Web 项目,从 SSM 起步能建立起很扎实的框架认知。
2.2 工程目录结构与依赖配置
Maven 工程我把结构分成标准的四层,建议你也这样分,后面找问题会非常省时间:
dormitory-management ├── pom.xml ├── src │ └── main │ ├── java │ │ └── com.dorm │ │ ├── controller │ │ ├── service │ │ ├── dao │ │ ├── entity │ │ ├── interceptor │ │ └── common │ ├── resources │ │ ├── jdbc.properties │ │ ├── spring-mybatis.xml │ │ ├── spring-mvc.xml │ │ └── mybatis-config.xml │ └── webapp │ ├── WEB-INF │ │ ├── web.xml │ │ └── views │ └── staticpom.xml 里最核心的依赖我列一下,这些版本组合我实测过,互相之间没有冲突:
<properties> <spring.version>5.2.9.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> </dependencies>有一点要提醒:MySQL 驱动的版本与数据库版本要匹配。如果你用的是 MySQL 8.x,建议换成mysql-connector-java 8.0.x,并且 JDBC 驱动类名要改成com.mysql.cj.jdbc.Driver,同时在连接串里追加serverTimezone=Asia/Shanghai,否则会出现时区报错。
2.3 关键配置文件的写作顺序
SSM 整合最磨人的就是配置文件。我总结了三个配置文件的写作顺序和核心注意点。
第一个是 spring-mybatis.xml,负责数据源、SqlSessionFactory、Mapper 扫描和事务管理。数据源我用的是 Spring 自带的 DriverManagerDataSource,方便、够用,开发阶段不需要引入连接池。SqlSessionFactoryBean 的配置里务必指定mapperLocations,指向 XML 文件目录:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.dorm.entity"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean>第二个是 spring-mvc.xml,核心要做三件事:开启注解驱动、配置静态资源释放、配置视图解析器。JSP 视图解析器建议把前缀设为/WEB-INF/views/,后缀设为.jsp,这样页面统一放在 WEB-INF 下面,外部无法直接通过 URL 访问 JSP 文件,安全性会好很多。
第三个是 web.xml,注意两个地方的顺序。ContextLoaderListener负责加载 Spring 容器,DispatcherServlet负责加载 SpringMVC 容器。一定要保证 Spring 容器先启动,否则 Controller 依赖的 Service 会因为被两次实例化而产生代理混乱,这是很多人容易踩的坑。
3. 数据库是系统的地基:核心表结构与关系设计
3.1 六张核心业务表
我把数据模型精简到了七张核心表,既覆盖主要业务流程,又不至于让关系乱成一锅粥。设计时遵循一个原则:每一条学生-宿舍关系都落在一张独立的关联表里,而不是在学生表里塞一个宿舍外键,因为学生有入住、调宿、退宿的历史,需要保留轨迹。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| t_user | 登录账号表 | id, username, password, role, user_ref_id |
| t_student | 学生信息表 | id, student_no, name, gender, phone, class_name |
| t_building | 楼栋表 | id, building_name, manager_id |
| t_dormitory | 宿舍表 | id, building_id, room_no, capacity, current_count |
| t_bed | 床位表 | id, dormitory_id, bed_no, status |
| t_repair | 报修表 | id, student_id, dormitory_id, content, status, create_time |
| t_check_record | 检查记录表 | id, dormitory_id, check_type, score, remark, check_date |
单看 t_student 你会发现它没有宿舍字段,这是刻意的。学生当前住在哪个床位,通过查询床位表里student_id最指向关系即可,避免了重复存储导致的数据不一致。t_user 表通过user_ref_id关联到具体学生或管理员记录,登录身份验证完角色之后,再通过这个 ID 去定位业务表中的数据。这个设计在关联查询时多了一次 JOIN,但换来了极大的灵活性——以后如果新增"辅导员"角色,不需要改动其他表结构,只需要在 user_ref_id 指向新表即可。
3.2 床位分配的数据模型细节
床位关系是宿舍系统里最重要的关系,我把这一个关系拆成了两张表:t_dormitory 记录宿舍的容量和当前已住人数,t_bed 记录每一张床位的占用状态。举例来说,一间 4 人间宿舍,会对应 4 条 t_bed 记录,每条记录有个status字段,0 表示空床,1 表示已入住;一旦入住,student_id字段就写入了学生 ID。
为什么要这样设计?因为宿舍管理一个很常见的需求是"查空床"。如果只在 t_student 表里存放宿舍 ID,那么查询某间宿舍空了几个床位,只能用count去统计学生数,再和容量做减法。这个方案在调宿频繁时会漏掉一种情况——一个学生可能暂时没有分配宿舍但并没有退宿,此时他就"凭空消失"了。床位表模型下,所有学生和床位的绑定关系都明明白白,空床查询只需要一条select count(*) from t_bed where dormitory_id=#{id} and status=0。
3.3 状态字段与状态机设计
宿舍业务里到处都是"状态":床位有空闲/占用,报修有待处理/处理中/已完成,宿舍入住有在住/退宿。我强烈建议在设计表时就把状态字段的取值写死为数字枚举,并且在代码里做常量映射,不要用中文字符串直接存数据库,否则统计、筛选、前端开关判断都会变得很痛苦。
比如报修表的status字段,我定义常量类RepairStatus:
public class RepairStatus { public static final Integer PENDING = 0; // 已提交,待派单 public static final Integer PROCESSING = 1; // 处理中 public static final Integer FINISHED = 2; // 已完成 public static final Integer CANCELED = 9; // 已取消 }同时状态流转必须是有向的:学生提交后从 PENDING 开始,宿管员接单后改为 PROCESSING,维修完成后改为 FINISHED,只有 PENDING 状态下的单子可以撤销。状态机的价值在于让流程可预测、可排查,前端也能根据不同的状态显示不同的操作按钮。
4. 三类角色的权限模型与登录逻辑实现
4.1 基于Session的轻量级权限方案
我见过很多 SSM 项目一上来就引 Spring Security,配置一大堆 Filter,结果项目还没跑通,光权限配置就出了问题。宿舍管理系统这种角色只有三类的应用,用 SpringMVC 的拦截器加 Session 就足够控制权限了,清晰又容易维护。
登录逻辑流程是这样的:用户提交用户名密码,Controller 调用UserService.login(),先查 t_user 表,用 MD5 加密后的密码比对;比对成功之后,从权限配置表里查到角色 code,写入 Session,属性名为loginUser。后续所有接口只要加一层拦截器,根据 URL 前缀判断角色即可。
密码存储这里我多说一句,MD5 本身已经不是安全密码哈希了,但作为课程设计或校内系统,没有专业安全团队在场的场景下,MD5 加盐仍然比明文强很多。我实际使用的做法是在密码字段后面拼接一个固定盐值字符串,再做 MD5,可以防止简单的彩虹表破解。
4.2 拦截器的设计与配置
权限拦截我用两个维度来控制:URL 前缀和角色 code。这是一个非常简单但好用的约定:所有管理员功能挂在/admin/**下,宿管员功能挂/manager/**,学生功能挂/student/**。拦截器的工作一目了然:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); LoginUser user = (LoginUser) session.getAttribute("loginUser"); String uri = request.getRequestURI(); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } if (uri.startsWith(request.getContextPath() + "/admin/") && !"ADMIN".equals(user.getRole())) { response.setStatus(403); return false; } // manager、student 同理 return true; } }然后在 spring-mvc.xml 里注册拦截器,并设置exclude-mappings排除登录页面、登录接口、静态资源路径:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.dorm.interceptor.AuthInterceptor"/> </mvc:interceptor> </mvc:interceptors>4.3 菜单动态渲染的做法
权限控制不光是拦截请求,还涉及"看得见和看不见"。如果学生登录后页面顶部还显示"宿舍管理""楼栋管理"这种菜单,体验就很差。我的做法是在登录成功时把角色的菜单列表加载出来,放到 Session 里,然后在 JSP 页面上用<c:forEach>循环渲染:
<ul class="nav"> <c:forEach var="menu" items="${sessionScope.menus}"> <li class="nav-item"> <a href="${pageContext.request.contextPath}${menu.url}">${menu.name}</a> </li> </c:forEach> </ul>这里有一个细节:菜单列表不是在登录时实时从数据库查的,而是启动时由MenuLoader一次性加载到内存缓存,登录时根据角色复制一份进 Session。原因很简单,菜单表是一个基本不变的静态数据,每次登录都全表扫描没有必要。
5. 两个最容易出问题的核心流程:床位分配与报修处理
5.1 床位分配的并发控制与事务
床位分配是本系统中并发风险最高的操作。想象一下新生报到日:多个管理员同时操作,大家都看到一个空床位,如果系统不做并发控制,两个学生会同时被登记到同一张床上,宿舍系统直接崩溃。
我的解决方案是条件更新 + 受影响行数判定。分配床位的 SQL 不是先查后改,而是一步到位:
<update id="allocateBed"> UPDATE t_bed SET student_id = #{studentId}, status = 1 WHERE id = #{bedId} AND status = 0 </update>这条 SQL 的关键在于AND status = 0。MySQL 执行 UPDATE 时会自动给命中的行加锁,两个事务同时更新同一行时,后者会阻塞等待,前一个事务提交后再执行。如果此时该行 status 已经变成 1,后一个 UPDATE 匹配不到数据,受影响行数是 0,Service 层据此判断"抢床失败",直接抛出业务异常提示前端重新选择。这就是典型的乐观锁思路,在高并发小系统里非常适用,完全不需要引入分布式锁。
在 Service 方法上,事务注解不要省:
@Transactional(rollbackFor = Exception.class) public void assignBed(Long studentId, Long bedId) { int rows = bedDao.allocateBed(studentId, bedId); if (rows == 0) { throw new BusinessException("床位已被占用,请刷新后重试"); } // 同步更新宿舍当前人数 dormitoryDao.increaseCurrentCount(bedId); }事务放在 Service 层,保证床位状态和学生宿舍人数两个动作要么都成功,要么都回滚。这里我踩过一个真实的坑:有一次调宿功能上线,学生调走后床位释放了,但旧宿舍的 current_count 没有减,导致前台显示"宿舍已满",排查了大半天才发现是事务只包住了床位更新,漏掉了宿舍人数更新。
5.2 报修流程的状态流转与通知
报修模块虽然看起来简单,但状态流转的细节决定了系统好不好用。我在实现时把报修单设计成"学生提交 -> 宿管派单 -> 维修工处理 -> 学生确认"四步,但在第一版简化为了"提交 -> 处理中 -> 已完成"三步。精简的原因是这个系统实际使用场景里没有专职"派单员",宿管员自己就是派单员,所以不需要单独的派单状态。
代码里我用一个 Service 方法集中处理状态变更:
@Transactional public void updateRepairStatus(Long repairId, Integer fromStatus, Integer toStatus, Long operatorId) { Repair repair = repairDao.selectById(repairId); if (repair == null) { throw new BusinessException("报修单不存在"); } if (!repair.getStatus().equals(fromStatus)) { throw new BusinessException("当前状态不允许该操作"); } repairDao.updateStatus(repairId, toStatus); // 写入一条操作日志,记录经办人和时间 repairLogDao.insert(new RepairLog(repairId, operatorId, fromStatus, toStatus, new Date())); }这个方法里把fromStatus作为参数传进来有一个好处:前端按钮传过来的状态变更意图非常明确,后端正反校验都做,不会因为用户狂点按钮产生状态错乱。操作日志表在报修场景下非常有必要,否则宿舍楼长问"这个报修单为什么反复打回",后端没有任何依据可以追溯。
6. 前端交互与好用体验:JSP+Bootstrap的实战取舍
6.1 页面与数据的配合方式
虽然现在前后端分离很火,但宿舍管理系统这种并发量不大的内部系统,服务端渲染完全够用,而且开发效率非常高。JSP 页面配合 JSTL 标签做数据渲染,Bootstrap 负责样式,jQuery 负责局部交互。
列表页面我统一走"Controller 返回 ModelAndView -> JSP 用 JSTL 循环渲染 Table"的路线,查询条件通过 GET 参数传递,PageHelper 做分页:
@GetMapping("/admin/dormitory/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Model model) { PageHelper.startPage(pageNum, pageSize); List<DormitoryInfo> list = dormitoryDao.selectPage(); PageInfo<DormitoryInfo> pageInfo = new PageInfo<>(list); model.addAttribute("pageInfo", pageInfo); return "admin/dormitory_list"; }PageHelper 的用法有一个大坑:它基于 ThreadLocal 实现,PageHelper.startPage()后面必须紧跟第一条查询语句。如果你在中间穿插了其他 Mapper 查询,分页就会作用在错误的 SQL 上,导致数据错乱。更稳妥的做法是把startPage放到 Controller 最开始的位置,定量确保它后面的第一个 Mapper 调用就是目标查询。
6.2 文件上传和日期处理的坑
宿舍管理系统涉及的头像上传和报修图片上传,我用了 CommonsMultipartResolver 配置。这里有一个容易忽略的点,配置文件上传解析器时的maxUploadSize不要设置得太小,我默认设为 10MB,但同时又在前端做了文件类型校验,只允许 jpg、png、gif,双保险。
日期处理是 JSP 页面最容易出错的环节。Java 的java.util.Date在 JSON 序列化时默认输出时间戳,前端拿到一串数字没法直接用。我统一采用两种方式解决:列表渲染用 JSTL 的<fmt:formatDate>格式化;Ajax 接口返回 JSON 时,在实体类的getCreateTime()方法上直接加@JsonFormat注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime;这里 timezone 必须显式指定为 GMT+8,否则部署到服务器后,因为服务器时区可能默认是 UTC,展示出来的时间会比本地时间慢 8 小时。这个坑我帮同事排查过好几次,每次都是重新设置服务器时区才想起来,后来干脆在代码里写死时区,再也不依赖服务器环境。
7. 部署中的常见问题与排错清单
7.1 Tomcat与MySQL的编码问题
打包部署到 Tomcat 后,整套系统最容易出现的问题是中文乱码。乱码的原因通常是三层有一层编码不对:页面编码、Tomcat 连接编码、数据库连接编码。
先看页面,所有 JSP 页面开头统一设置:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>再看 Tomcat 的 server.xml,连接器上需要加上URIEncoding="UTF-8",否则 GET 请求里带的中文参数会乱:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/>最后是数据库连接串加字符集参数:
jdbc.url=jdbc:mysql://localhost:3306/dorm?useUnicode=true&characterEncoding=UTF-8这三处都设置对了,中文显示基本就不会出问题。如果线上还是乱码,检查 MySQL 表结构的默认字符集,新建表时明确指定DEFAULT CHARSET=utf8mb4。
7.2 MyBatis映射与配置的典型报错
SSM 项目调试时,MyBatis 相关报错占了很大比例。最常见的三种,我列在下面:
第一种是"Invalid bound statement (not found)",意思是 Mapper 接口方法找不到对应的 SQL 语句。原因通常是 Mapper XML 文件的 namespace 写错,或者 XML 没有被扫描到。检查 spring-mybatis.xml 的mapperLocations路径和 XML 文件实际位置是否一致,尤其注意classpath:mapper/*.xml这种写法不能匹配到 jar 包内部的目录。
第二种是"Parameter 'xxx' not found"错误。当 Mapper 方法参数超过一个且没有加@Param注解时,MyBatis 无法识别参数名。解决方法是给每个参数加@Param("xxx"),或者把参数封装到一个 DTO 对象里。我在设计时统一约定:查询条件封装成 Query 对象,这样不仅避免了参数管理的麻烦,也更符合复杂查询的扩展需求。
第三种是数据库字段和 Java 属性驼峰映射对不上。比如数据库字段building_name映射不到buildingName。两种解法:SQL 里写别名building_name AS buildingName;或者在 mybatis-config.xml 里开启全局配置:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>我建议你开启后者然后统一遵守"数据库字段下划线、Java 属性驼峰"的命名规范,这是最省心的方式。
7.3 启动慢和端口冲突
部署中还有一个常见小问题,Tomcat 启动慢。如果日志里显示很多时间花在SecureRandom上,是因为 Linux 服务器/dev/random熵池不足。解决方法是修改 JVM 参数,显式指定-Djava.security.egd=file:/dev/./urandom。这个参数看起来有点奇怪,但确实有效,启动时间能从几十秒缩短到几秒,有强迫症的话值得加上。
端口冲突就简单了,netstat -ano | grep 8080找到占用进程再处理。如果你开发机同时跑着多个 Tomcat 实例,建议每个项目的端口都不一样,并且在项目名上做好区分,比如dorm.war、library.war,部署路径清爽很多。
8. 验收时最容易被问到的三个细节
8.1 数据统计与报表
宿舍管理系统如果只是增删改查,上线后的价值感会弱很多。实际使用中,管理员最关心的是数据统计:各楼栋入住率、当月报修数量、卫生检查的平均分趋势。我在系统里加了两个统计入口,一个是对各楼栋入住率的横向对比,一个是报修工单的数量趋势图。统计 SQL 可以直接用 GROUP BY 实现,无需引入复杂的报表组件,数据量不大时性能也很好。
比如查询各楼栋空床数:
SELECT b.id, b.building_name, COUNT(d.id) AS total_rooms, COALESCE(SUM(CASE WHEN d.current_count < d.capacity THEN 1 ELSE 0 END), 0) AS available_rooms FROM t_building b LEFT JOIN t_dormitory d ON d.building_id = b.id GROUP BY b.id, b.building_nameJSP 页面上用 ECharts 折线图和柱状图展示,导入一个 echarts.min.js 就够了,不需要额外依赖。
8.2 敏感操作日志
宿舍管理系统中,调宿和退宿都属于敏感操作。学生如果反馈"我没申请调宿,床位怎么变了",系统必须能追溯。我在 t_dorm_log 表里记录了每一次宿舍变动的操作人、操作类型、变更前和变更后的状态。这个表不参与任何业务查询,纯粹为了溯源留存,但上线后被宿管中心使用频率很高,因为每学期总有几起纠纷要靠它来仲裁。
8.3 学生自主申请退宿还是宿管代操作
业务上有一个设计取舍值得思考:学生要不要能在线上自主提交退宿申请?第一版我做了这个功能,后来发现实际场景里学生退宿是需要辅导员审批、宿管验房的,流程根本走不通。后面改成"学生提交申请 -> 宿管确认 -> 床位释放"三步。这个经验也印证了之前那句话:功能设计必须贴着真实业务流程,不能为了"看起来完善"而增加虚假流程。
对于这套系统,我个人最满意的一个设计就是床位分配的条件更新方案,它用最简单的数据库机制解决了一个真实的并发问题。后来我还把同一个思路用在了网上选课系统的抢课接口里,效果同样稳定。
如果你正在做类似的 SSM 项目,我的建议是先把业务主流程的数据库关系画透,再动手写代码。数据库设计稳了,后面就顺利了;数据库关系乱了,后面每一步都会跟着踩坑。把上面几个我强调过的细节——配置文件顺序、状态机设计、并发抢占、编码与时区——提前想清楚,你会在调试上省下大量时间。