做完这个宿舍管理系统,我最大的感受是:这类“看着很简单”的管理系统,真正往企业级靠拢的时候,复杂度其实藏在一堆不起眼的业务细节里。最近正好有同行问我要这套 SpringBoot + Vue + MyBatis + MySQL 的宿舍管理系统源码,我就干脆把整个项目的设计思路和落地过程完整写下来。这篇东西不是单纯的代码堆叠,而是一份从业务需求到技术实现的项目解读,适合三类人看:正在做毕业设计或课程设计的学生、想快速交付同类管理系统拿经验的开发者、以及企业内部要做行政信息化建设、需要评估类似系统原理的技术人员。
市面上“宿舍管理系统源码”的资源不少,但大多只有一份能跑通的代码,没有讲清楚为什么要这样设计,模块边界在哪,数据库为什么建这些表,权限为什么要这样划分。这篇文章的目的就是把“能跑”和“能用”之间的距离给补齐。
1. 宿舍管理系统解决的是什么问题:从手工台账到线上办公
1.1 系统的业务角色与其说闭环
宿舍管理系统本质上解决的是住宿资源的管理问题。没系统的时候,很多企业和学校靠的是什么?一张 Excel 表,甚至一沓纸质入住登记单。人员一多、楼栋一多,查一个人的住宿信息要翻半天,宿舍分配全凭宿管员记忆,退宿时床位有没有空出来、物品有没有损坏,全靠口头对接。
所以系统里第一个要建模的是角色。按常见企业级场景,宿舍管理系统至少要拆出三类角色:
- 超级管理员:维护基础数据,比如宿舍楼、宿舍房间、床位信息,管理系统内的用户与权限,查看全系统的数据统计和审计日志。
- 宿管员:负责日常业务操作,包括入住登记、退宿处理、调宿审批、报修派单、访客登记审核,通常只管理自己负责的楼栋。
- 学生或住户:查看自己的住宿信息、提交报修工单、查看公告和水电费用记录。
有了角色之后,业务流程自然就能串起来了。整个系统最核心的流程是“入住—日常管理—退宿”:
- 入住阶段:新员工或新生入职入学后,由宿管员录入基本信息,核对住宿资格,选择宿舍楼和床位,生成入住记录。
- 日常管理:住户提交报修,宿管员派单维修;访客到访登记;每月生成水电费记录。
- 退宿阶段:住户离开时进行退宿登记,床位置为可分配状态,最后保留一条历史入住记录方便后续审计。
1.2 企业级版本比课设多出来的东西
很多课程设计版本的宿舍管理系统,功能就是简单的“增删改查”——用户登录、宿舍信息维护、入住登记、退宿登记,没了。但参考真正能上线的企业级源码,差别主要在几个地方:
第一是权限模型要做细。课设里通常只有“管理员”和“普通用户”两档,但企业里宿管员不能修改系统配置,普通住户只能操作自己的数据。更细一点,宿管员只能看到自己管理的楼栋数据,不能跨楼栋操作。这就需要一个角色-菜单-权限点的模型,而不是简单在用户表里加一个“是否管理员”的字段。
第二是业务流程要有状态。比如入住单有“待审批”“已分配”“已入住”“已退宿”几个状态,报修工单有“待受理”“维修中”“已完成”“已评价”,房间床位有“空闲”“锁定”“占用”。数据在流转过程中会经历多个状态,系统需要记录状态变化的轨迹。
第三是操作审计要留痕。现实场景里,总会遇到“这个床位明明空着怎么不能分配”“是谁把数据改掉的”之类的问题。企业级系统会给关键操作写入审计日志:操作人、操作时间、操作内容、修改前后的值。这个功能看似不起眼,排障时却能救命。
第四是并发场景和唯一性约束。开学季高峰,多个宿管员同时操作,有可能把同一个床位分配给两个人。业务层一定要有校验,数据库层也需要唯一约束或锁机制兜底,不然数据一乱,后面全乱。
2. 技术栈的选用逻辑:为什么偏偏是SpringBoot+Vue+MyBatis+MySQL
2.1 后端选型:SpringBoot把项目复杂度压下来了
先聊后端。SpringBoot能够成为这类管理系统的默认选择,不是没有理由的。最直接的感受是省掉了大量框架集成工作。传统要做SpringMVC+Spring+MyBatis的整合,得写一堆XML配置文件,数据源、事务、扫描包、视图解析器,一个都不能漏。而SpringBoot用起步依赖把常用的组件打包,配置集中在application.yml里,几分钟就能拉起一个可运行的项目。
另外,SpringBoot内嵌了Web服务器,打包成可执行JAR就能直接跑,不用单独安装配置Tomcat。这对于部署到客户的服务器、或者交付给非专业运维的甲方,都友好得多。源码项目里通常还会配套单元测试、监控组件,这些在后期维护时价值很大。
2.2 持久层与数据库:MyBatis的可控性和MySQL的务实性
持久层用MyBatis,很多人问为什么不选Spring Data JPA。对于宿舍管理系统这类SQL逻辑不算复杂、但查询条件变化频繁的业务,MyBatis的SQL可控性反而是最强优势。房间条件查询要支持按楼栋、楼层、床位状态、房间类型任意组合筛选,MyBatis的动态SQL写起来非常直观,自己写的SQL心里有底,出现问题也容易排查。
MySQL的选择就更是务实了。部署简单、运维成本低、社区资料多,对于几百人几千人规模的宿舍管理完全够用。如果硬要选Oracle或者PostgreSQL,不是不行,但成本和维护门槛会明显提升。小规模业务场景下,MySQL的性价比最高。需要说明的是,这个项目在设计时遵循MySQL 8.0的窗口函数等新特性,但核心业务表基本不依赖版本特性,迁移到5.7版本也能跑,兼容性还是留了余地的。
2.3 前端选型:Vue组件化让管理后台的开发效率提升一个档次
前端用Vue,理由就更直观了。管理后台的页面长得很像:左侧菜单、顶部栏、中间的表格和表单弹窗。Vue的组件化正好契合这种场景,把搜索栏、表格、弹窗、分页都封装成可复用组件,新页面就是组合组件的过程。
配合Element UI或Element Plus这类组件库,一个完整的管理界面半天就能搭出来。而且Vue的双向数据绑定让表单交互非常自然,省去手动操作DOM的大量时间。再加上Vue Router做前端路由、Vuex或Pinia做全局状态管理,整个前端工程的结构清晰,多人协作也不容易出现代码冲突。
2.4 这套组合的边界在哪里
技术选型没有绝对正确,只能说在特定场景下最合适。SpringBoot+Vue+MyBatis+MySQL这套组合适合中小规模业务、团队人数不多、需要快速交付的管理系统。如果业务复杂度大到需要分布式事务、数据量达到亿级、或者前端交互极其复杂的程度,那就不适合了,需要引入微服务、读写分离、前端工程化重构等手段。但就宿舍管理这类系统来说,这套技术栈是“正好够用且不过度设计”的典型选择。
3. 数据库设计是这类系统的命根子:核心表和关系的落地
3.1 最核心的四张表:用户、宿舍楼、房间、入住记录
宿舍管理系统里,数据库设计决定了业务能走多远。源码项目的初始化SQL脚本里,表大概有十来张,但最核心的是四张表。
**用户表(sys_user)**是系统的基础,字段大致包括:用户ID、用户名、密码(数据库里存的是加密后的哈希值)、真实姓名、角色类型、联系电话、所属楼栋ID、状态。这样一个表既能承载超级管理员和宿管员,也能承载住户,属于经典的单表多角色设计。简单示意:
CREATE TABLE `sys_user` ( `user_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '登录账号', `password` VARCHAR(100) NOT NULL COMMENT '登录密码(加密)', `real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名', `role_type` TINYINT NOT NULL COMMENT '角色: 1-超级管理员 2-宿管员 3-住户', `building_id` BIGINT DEFAULT NULL COMMENT '关联宿舍楼ID', `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', `status` TINYINT NOT NULL DEFAULT '1' COMMENT '状态: 0-禁用 1-启用', `create_time` DATETIME NOT NULL COMMENT '创建时间', PRIMARY KEY (`user_id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';**宿舍楼表(dorm_building)和宿舍房间表(dorm_room)**是一对多的关系。宿舍楼记录楼栋名称、地址、层数、每层房间数、负责人、联系电话。宿舍房间记录所属楼栋ID、房间编号、所在楼层、房间类型、床位数、已住人数、房间状态。这里要强调一个设计细节:床位信息最好单独建表,不要在房间表里用“床位数”这一个字段糊弄过去。
床位单独建表(dorm_bed)的好处很明显:每个床位有自己的状态(空闲/占用/锁定),可以精确控制分配逻辑,还能记录是由哪位住户在住。如果只是房间表里存一个床位数,你根本没法知道哪个床位空出来了、哪个床位被占了。
**入住记录表(check_in_record)**是业务核心,也是所有数据关联的中心。字段包括:记录ID、住户ID、楼栋ID、房间ID、床位ID、入住时间、退宿时间、入住状态、办理人ID。这张表把一次入住行为完整记录下来,后续查历史、做统计都靠它。
3.2 报修、访客、水电费这些周边表怎么关联
部署过真实系统之后就会知道,光有核心住宿管理是不够的。日常运营中,报修、访客、水电费、公告这些模块一个都缺不了。
报修工单表(repair_order)关联的是住户、房间、维修人员和处理状态,字段包括报修人ID、房间ID、报修类型(水、电、家具、门窗)、内容描述、工单状态、维修人、处理时间、用户评价。这张表要注意状态的流转:报修提交后,如果宿管员不派单,住户那边要能看到“待受理”的状态;维修完成后,住户可以确认完成。状态字段建议用数字常量维护,不要用字符串散落在代码里。
访客登记表(visitor_record)是安保需求,记录访客姓名、电话、访问的房间、事由、进入时间、离开时间和登记人。水费电费表(fee_record)则按房间和月份记录费用,字段包括房间ID、费用类型(水费/电费/网费)、账期月份、金额、缴费状态、缴费时间。公告表(notice)发布宿舍管理通知,住户登录后能看到最新公告。
这些周边表通过外键或逻辑关联到房间表和用户表,并不复杂,但在实际设计时容易犯一个毛病——只想着页面需要什么就建什么表,结果后面要加字段时各种改表结构。我的经验是,表设计要把“未来可能扩展”的字段预留一部分,比如报修表里预留一个“备注”、费用表里预留一个“减免金额”之类的。
3.3 数据库设计里最容易忽略的三个点
第一,逻辑删除还是物理删除,要提前想清楚。住宿记录千万不要物理删除,退宿之后这些数据都是历史档案,后面可能涉及费用结算、纠纷追溯。所以核心业务表都应该加一个is_deleted字段或者通过“状态”字段实现逻辑上的历史留存。比如住宿记录不删除,只是把状态改成“已退宿”。
第二,唯一性约束是最后的兜底。业务层做校验只是第一道防线,数据库约束才是第二道。比如同一个人不能同时存在两个“入住中”的住宿记录,虽然MySQL不支持部分唯一索引,但你可以在设计上通过“用户ID+状态字段”的组合查询配合事务来保证,更稳妥的方案是加一个“是否有效”字段并和用户ID做联合唯一约束。
第三,索引要建在查询路径上。宿舍管理系统的查询模式相对固定:按楼栋查房间、按状态查床位、按住户查入住记录。所以build_id、room_status、user_id这些字段一定要建索引。实际开发时可以在查询日志里观察慢SQL,把出现频率最高的查询条件拎出来建复合索引。
4. 后端代码架构拆解:SpringBoot+MyBatis项目的实际分层
4.1 包结构规划与职责划分
源码拿到手,先别急着跑,先看包结构。一个规范的后端项目,包结构本身就是文档。宿舍管理系统源码的常见结构长这样:
com.example.dormitory ├── config # 配置类:跨域、拦截器、MyBatis配置、全局异常处理 ├── controller # 接口层:接收前端请求,做参数校验 ├── service # 业务层:核心业务逻辑,事务控制在这层 ├── mapper # 持久层接口:定义数据库操作方法 ├── entity # 实体类:对应数据库表 ├── dto # 数据传输对象:接口出参入参的载体 ├── vo # 视图对象:给前端返回的展示对象 ├── common # 通用组件:统一返回结果、分页对象、常量类 ├── security # 认证与权限相关 └── util # 工具类:JWT工具、日期处理、密码加密为什么这么分层?目的是让依赖关系单向流动:Controller 调 Service,Service 调 Mapper,Mapper 操作数据库。任何一层都不要跨层调依赖,尤其是Controller里不要直接写SQL逻辑。这样改起来有边界,出了问题也能快速定位。
4.2 登录认证与权限拦截:Token怎么发、怎么验
后端有个绕不开的模块,登录认证。宿舍管理系统里住户、宿管员、超级管理员是三种不同权限,登录后必须区分。项目里使用的是基于JWT的Token认证方案,流程是这样的:
- 用户提交用户名密码到登录接口。
- 后端把密码用BCrypt算法校验(数据库存储的不是明文),校验通过后生成一个Token。
- Token里面携带用户的ID、角色类型和过期时间,用一个密钥签名。
- 前端把Token存起来,每次请求都放在请求头里。
- 后端的拦截器拦截所有接口,从请求头取出Token并校验签名、过期时间,校验通过后把用户信息放入当前上下文。
用JWT而不是Session,根本原因是前后端分离架构下服务端不方便保存会话状态,客户端每次请求自带头信息,后端只负责无状态验证。代码上核心就是拦截器,逻辑大致如下:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } // 解析token,验证签名和过期时间 Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); // 把用户信息放入ThreadLocal,供后续Service使用 UserContext.setUserId(claims.get("userId").toString()); UserContext.setRoleType(claims.get("roleType").toString()); return true; } }接口层的权限控制可以在拦截器里统一判断,也可以在具体方法上使用自定义注解,比如@RequireRole(role = "admin")。对于价值不大的页面,前端隐藏按钮就够用了;但真正要做权限封闭,后端接口必须要校验。我在项目里给分配宿舍、修改用户、删除记录这些敏感接口都加了角色校验。
4.3 MyBatis XML里的动态SQL:以宿舍条件查询为例
宿舍列表查询是典型的多条件组合查询:用户可能传楼栋ID、可能传楼层、可能传房间状态、可能传房间类型,这些条件可有可无。如果为每一种组合写一个SQL方法,那代码就是灾难。MyBatis的动态SQL专门解决这个问题:
<select id="listRooms" resultType="com.example.dormitory.vo.RoomVO"> SELECT r.room_id, r.room_no, r.floor, r.room_type, r.bed_num, r.live_num, r.status, b.building_name FROM dorm_room r LEFT JOIN dorm_building b ON r.building_id = b.building_id <where> <if test="buildingId != null and buildingId != ''"> AND r.building_id = #{buildingId} </if> <if test="floor != null"> AND r.floor = #{floor} </if> <if test="status != null and status != ''"> AND r.status = #{status} </if> <if test="roomType != null and roomType != ''"> AND r.room_type = #{roomType} </if> </where> ORDER BY r.building_id, r.floor, r.room_no </select>这里用<where>标签自动拼接SQL,能够自动处理多余的AND,条件都不传时不会报错,返回全部数据。这个查询带分页,项目中通常配合分页插件使用,分页参数由框架自动拦截处理,不用手写LIMIT。掌握动态SQL是使用MyBatis的核心技能,业务里八成以上的复杂查询都能用它解决。
4.4 宿舍分配这个核心业务的完整实现链路
宿舍分配是整个系统最核心的业务逻辑。它的实现质量直接决定系统能不能真正用起来。接口大致是:POST /api/checkin/assign,参数包括住户ID、楼栋ID、房间ID、床位ID。
程序要做的事,按顺序排:
- 校验住户存在且在有效状态。
- 校验该住户没有正在生效的入住记录。
- 校验目标床位存在且状态为“空闲”。
- 生成入住记录,状态为“已入住”。
- 把床位状态改为“占用”,把住户信息关联到床位。
- 把房间的已住人数加一,如果已住人数达到床位数,房间状态改为“已满”。
这些操作必须在一个事务里完成。如果生成入住记录成功,但更新床位状态失败,那这条数据就脏了。最合适的做法是在Service方法上加@Transactional注解:
@Transactional(rollbackFor = Exception.class) public void assignDorm(AssignDormDTO dto) { // 1. 校验住户 UserDO user = userMapper.selectById(dto.getUserId()); if (user == null || user.getStatus() != 1) { throw new BusinessException("住户不存在或已被禁用"); } // 2. 校验重复入住 int activeCount = checkInRecordMapper.countActiveByUserId(dto.getUserId()); if (activeCount > 0) { throw new BusinessException("该住户已存在有效入住记录"); } // 3. 校验床位状态,使用带条件的UPDATE防止并发 int rows = dormBedMapper.updateStatusIfFree(dto.getBedId(), user.getId()); if (rows == 0) { throw new BusinessException("床位已被占用,请刷新后重试"); } // 4. 生成入住记录 // 5. 更新房间已住人数 }特别注意第3步,这里用了条件UPDATE来做并发控制。UPDATE dorm_bed SET status = 1, current_user_id = #{userId} WHERE bed_id = #{bedId} AND status = 0,如果更新的受影响行数为0,说明改的时候床位已经被别人抢占了,直接抛出异常并回滚事务。这是解决“同一个床位被两个人同时抢到”这个问题最务实的方案,比在代码里先用SELECT再UPDATE要安全得多。
5. 前端Vue部分的实现要点:页面怎么搭、接口怎么对
5.1 Vue工程结构、路由与状态管理
前端项目打开后,先看目录结构。规范的Vue工程会把不同职责的代码拆开,宿舍管理系统典型的目录结构是:
src ├── api # 接口请求封装,按业务模块拆文件 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── store # 全局状态管理 ├── views # 页面级组件 │ ├── login │ ├── dashboard │ ├── dormitory │ ├── checkin │ ├── repair │ └── system └── utils # 工具函数路由配置里,前端路由和权限要配合。登录页是公开路由,但“宿舍管理”“入住管理”“系统设置”这些页面需要校验Token才能进。实现方式是在Vue Router的全局前置守卫里判断:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path === '/login') { next(); } else if (!token) { next('/login'); } else { next(); } });全局状态管理主要保存当前登录用户信息和可访问菜单。登录成功后把用户ID、角色、真实姓名放入store,页面根据角色动态生成菜单和按钮。
5.2 管理后台的典型页面:以入住管理页面为例
入住管理页面是宿舍管理系统里交互最复杂的页面之一。页面结构分为几块:顶部搜索区,支持按入住状态、楼栋、房间号搜索;中间是表格区,列出入住记录,每一行有“详情”“退宿”“调宿”按钮;底部是分页器。
表格列的常见配置是:
| 列名 | 说明 |
|---|---|
| 住户姓名 | 来自用户表的真实姓名 |
| 所属楼栋 | 楼栋名称 |
| 宿舍房号 | 房间编号 |
| 床位号 | 床位的编号 |
| 入住时间 | 入住记录里的时间字段 |
| 状态 | 入住中/已退宿,用不同颜色的Tag展示 |
点击“分配宿舍”按钮,会弹出一个对话框,里面嵌套了三个下拉选择器:先选楼栋,再选房间,最后选床位。这里的联动逻辑是:选完楼栋后,发送请求获取该楼栋下处于“未满”状态的房间;选完房间后,获取该房间下“空闲”状态的床位。这个交互很常见,但要注意每次切换上级级联时清空下级的旧数据,否则会出现选了新楼栋却带着旧房间ID的情况。
5.3 axios封装、跨域配置和Token注入
前端所有接口请求,建议都通过一个统一的axios实例发送。这样可以在请求拦截器里统一注入Token,在响应拦截器里统一处理错误码:
import axios from 'axios'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { // 统一处理业务错误 if (res.code === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(new Error(res.message)); } return res; }, error => { // 处理网络错误或HTTP状态码非2xx return Promise.reject(error); } ); export default service;跨域是前后端分离项目里最常见的坑。开发环境下,前端跑在8080端口,后端跑在8081端口,浏览器会拦截跨域请求。最简单的解决办法是在Vue的vue.config.js里配置代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } };这样前端请求/api/xxx时,开发服务器会把请求转发到后端的8081端口,浏览器看到的是同源的请求,跨域问题就消失了。生产环境部署时,通常由Nginx配置反向代理来实现同样的效果。
6. 让整套源码跑起来:本地部署步骤与高频问题
6.1 环境清单与启动顺序
拿到源码,第一件事是跑起来,跑起来才能边看边改。环境准备清单如下,不同版本的依赖需要匹配:
| 依赖 | 版本建议 |
|---|---|
| JDK | 8 或 11 |
| Maven | 3.6 以上 |
| Node.js | 14 以上(Vue2项目),18以上(Vue3项目) |
| MySQL | 5.7 或 8.0 |
| IDE | IntelliJ IDEA 或 VSCode |
后端启动顺序很简单:先在MySQL里执行数据库初始化脚本,然后修改application.yml里的数据库连接信息,接着用Maven打包或直接在IDEA里运行Application类。前端启动更直接:npm install安装依赖,然后npm run serve启动开发服务器。
启动先后顺序建议是先启动MySQL并导入数据,再启动后端,等后端成功监听端口后,最后启动前端。这样出现问题可以快速定位是哪一环没就位。
6.2 按配置字段、端口占用等高频启动报错
跑源码最容易遇到下面这些报错,我把典型现象和排查方法整理成表格:
| 报错现象 | 根本原因 | 解决方法 |
|---|---|---|
Communications link failure | 数据库连不上 | 检查MySQL是否启动、连接地址和端口是否正确 |
Access denied for user | 数据库账号密码错误 | 核对application.yml里的用户名密码 |
Invalid bound statement | Mapper接口与XML映射不匹配 | 检查Mapper接口方法名与XML中的id是否一致,检查namespace |
Port 8081 already in use | 端口被占用 | 换一个端口,或在配置里修改server.port |
Failed to configure a DataSource | 数据源配置没有生效 | 确认为什么没有排除数据源自动配置,或检查pom依赖 |
npm ERR! ERESOLVE | 前端依赖冲突 | 升级npm版本,或使用npm install --legacy-peer-deps |
这几个是出现频率最高的。还有一个隐蔽的问题容易被忽略:MySQL 8.0以上的驱动连接串需要带时区和SSL相关参数,比如serverTimezone=Asia/Shanghai,不然启动时会报时间区错误。源码里如果把这些参数写好了,就不用动;没写好的话要自己补上。
7. 做了两三个宿舍管理系统之后,我的经验沉淀
7.1 最容易翻车的三个业务逻辑边界
这个项目我从数据库设计到部署完整走下来,感受最深的是业务逻辑的边界情况,比功能本身更容易把人绊倒。
第一个是重复入住问题。刚开发时只想着“分配宿舍”这一个动作,没有考虑住户可能同时存在多条入住记录。后来在测试阶段发现,如果一个住户退宿没走完流程,又去走分配宿舍的入口,就会在数据库里留两条记录,导致统计混乱。解决思路是查询入住记录时,永远要过滤“当前有效”的记录,分配前优先校验。
第二个是性别混排问题。宿舍管理系统必须考虑混合宿舍楼的场景。如果一栋楼既有男生宿舍又有女生宿舍,分配宿舍时没有按住户性别过滤可选房间,就会出现男住户被分配到女生楼层的情况。这类问题最好是数据库层面引入“房间性别属性”字段,分配时强制校验。
第三个是水电费重复计费。每月生成水电费记录时,如果以“房间+月份”作为唯一键,就能避免一台数据被重复生成。但如果不加唯一约束,脚本跑两遍或者操作人员多点了几次,就会多出费用记录,住户端会投诉。这类“重复”问题的共同解法,都是一个数据库唯一约束或者幂等ID解决。
7.2 权限安全与并发场景的加固建议
安全方面,哪怕是个管理系统,也不能马虎。密码存储至少要用BCrypt,不要用MD5,MD5加盐虽然比什么都没有强,但彩虹表攻击仍然是个隐患。BCrypt可以自动加盐,且每次生成的哈希值都不一样,配合登录频率限制基本能挡住常见攻击。
接口安全上,除了登录接口,其他接口都要过JWT拦截器。有一些接口虽然不太起眼,但后端还是要校验数据归属权,比如住户只能修改自己的资料,不能把别人的宿舍信息改了。不要指望前端做控制,前端只是UI层面的展示,后端才是安全边界。
并发方面,除了床位分配时的条件更新,还要注意报表统计场景。比如统计某栋楼入住率时,如果数据量大,查询会比较慢,可以考虑用汇总表或加缓存。不过在宿舍管理系统的体量下,数据库层面的索引和分页已经够用,不必过度设计引入多余的中间件。
7.3 我给后来者整理的三条建议
可以给打算用这套源码做二次开发或者学习的朋友们整理几条建议:
- 先在草稿纸上画出核心业务的状态流转图,把入住、退宿、报修、缴费这些流程的前置条件和后置动作都列出来,再开始看代码。状态没理清,后面越改越乱。
- 不要一上来就改前端页面样式,先在后端跑通核心接口,确认数据流是通的,再回过来对前端组件。很多新手把时间花在调按钮样式上,代码逻辑却还没跑通过。
- 业务表加字段是正常需求,但要注意在添加字段后同步修改实体类、Mapper XML、前端表单和表格列。一个字段要改四个地方,漏一个就出bug。
我后来把系统的状态常量全部收敛到了一个枚举类和常量类里,前端也对应维护了一份常量表,这样就避免在代码里到处写魔法数字。另外花了不少时间把“分配宿舍”的并发操作优化到条件更新+事务回滚的方式,这个改动价值最大,直接杜绝了高峰期的床位冲突问题。如果让我重做一遍,我会在项目初期就把状态字段的枚举设计得更完备,因为后面扩展状态时,改代码比加字段麻烦得多。