首页 / 资讯中心 / 文章详情

宿舍管理系统数据库课设:从E-R建表到触发器事务与对账验证

宿舍管理系统数据库课设:从E-R建表到触发器事务与对账验证 ★ FEATURED ARTICLE
简介面向数据库课程设计场景的学生宿舍管理系统完整资料适合正在做课程设计、需要参考双角色权限与业务闭环实现的高校学生。系统围绕寝室长与宿管员两类用户展开登录后按身份进入不同界面寝室长可查看寝室人员、提交报修、修改密码宿管员可检索与维护住宿信息、处理报修状态、管理学生账号。数据库设计覆盖寝室长、宿管员、报修及学生信息四类表单字段关系清晰便于按需求增删改。压缩包共3个文件包含rar源代码包、sql建库脚本和docx文档模板整体约2.96MB可配合Word模板整理设计报告。目前已有5057人学习适合需要快速搭建可运行原型、对照表结构与功能模块查漏补缺的读者。1. 宿舍管理系统数据库课程设计里性价比最高的那块练兵场挑数据库课程设计题目时学生宿舍管理系统几乎年年被抢。原因不玄学实体数量刚好卡在能画满一张 E-R 图、又不至于做不完的区间业务规则又天然带事务、触发器、统计查询评分点全都能踩到。这份资源给的是可直接跑的 SQL 文件、对应源代码和一份 Word 模板适合三类人第一次做课设、需要一份能改的底稿的人想把建表和业务规则对齐、答辩时不被追问倒下的人以及手里已经有代码、只缺一套规范文档结构的人。下面按“实体怎么定、表怎么建、规则怎么压进库、哪里最容易翻车、怎么自证设计没问题”的顺序拆。2. 需求到 E-R先把实体和关系钉死再谈建表2.1 从业务动作反推实体清单比盯着名词列字段靠谱多数人一上来就打开建表界面想到一个字段加一个字段做到一半发现“调宿记录放哪”答不上来。我的习惯是先列业务动作再让实体自己浮出来。宿舍管理真正会发生的动作就七类新生入住、调宿、退宿、报修、水电费结算、访客登记、按楼栋和班级查询统计。每个动作追问三个问题谁发起、改了哪些数据、事后要能查到什么。按这个套路走一遍实体清单基本长这样实体主键由哪个动作推出来备注楼栋 buildingbuilding_id入住时确定楼栋带性别类型、楼层数宿舍 roomroom_id入住、调宿、报修容量、已住人数、状态学生 studentstudent_id入住、查询学号唯一班级 classclass_id查询统计专业、年级、辅导员住宿记录 accommodationacc_id入住、调宿、退宿一学生多条历史记录报修单 repairrepair_id报修关联房间、报修人水电费 utility_billbill_id结算按房间按月份访客 visitorvisitor_id访客登记关联房间管理员 adminadmin_id登录、处理报修角色字段这张表不是给你抄的是给你对照的如果你画完 E-R 图发现只有五六张表多半是把“住宿记录”和“报修”合并进了学生表那后面的历史查询和调宿功能一定写不出来。2.2 关系基数的三个易错点学生与宿舍不是简单的一对多第一个坑学生和宿舍。直觉上“一个宿舍住多个学生”就是 1:N于是很多人在 student 表里加一个 room_id 字段了事。这样做的直接后果是学生一调宿旧记录被覆盖退宿统计、住宿历史、按学期核算全废。正确做法是引入住宿记录表 accommodation让学生和宿舍都退化成它的 1:N 关系一个学生对应多条记录一条记录对应一个学生一个房间对应多条记录一条记录对应一个房间。学生表里可以保留一个 current_room_id 作为查询冗余但它是派生字段必须和住宿记录同步不能当成唯一事实来源。第二个坑报修单的归属。报修既可能由学生发起也可能由宿管巡查发起所以 repair 表里 student_id 应该允许为空room_id 必须非空。很多人把 student_id 设成 NOT NULL结果宿管代报修的场景测试时直接插不进数据。第三个坑访客和房间。访客登记的业务粒度是“某个访客在某段时间进入了某个房间”visitor 表里只放 room_id 和姓名、进入时间、离开时间就够了不要再建一张“访客-学生”的多对多表除非需求里明确要求记录被访学生。多建一张表就多一组外键和一组答辩时要解释的关系得不偿失。2.3 E-R 图转关系模式的转换规则与字段落位画完图到落表之间还有一步转换规则其实只有三条但每年都有人在这里丢分。1:1 关系把主键并到任意一侧并加唯一约束1:N 关系把“一”端的主键放到“多”端当外键M:N 关系必须单独建关联表主键用两个外键的组合或者代理键。住宿记录表就是 M:N 的产物只是它带了床位号、入住日期这些属性所以它同时是一个实体。字段落位讲的是属性该挂在哪张表上判断标准只有一条这个属性描述的是“谁”。床位号描述的是“某个学生住进某个房间这件事”所以它在 accommodation 上房间容量描述的是房间本身所以它在 room 上学生姓名描述的是人所以它在 student 上。常见错误是把班级名称直接写进学生表正确做法是学生表存 class_id班级名称查 class 表否则改一次班名要更新几十行还容易不一致。到这一步逻辑设计阶段的产出应该是E-R 图一张、关系模式清单一份、每张表的字段说明。这份清单直接决定后面建表脚本长什么样也是 Word 模板里“逻辑结构设计”那一章的正文先把这里写顺文档不用返工。3. 建表 SQL 落地类型选型、约束顺序和索引策略3.1 字段类型选型几个每年都吵的点字符集统一用 utf8mb4不要用 utf8中文姓名、楼栋名、备注都能覆盖避免“建表时是 utf8、连接是 utf8mb4”导致的乱码。排序规则在 MySQL 8.x 上可以直接用 utf8mb4_0900_ai_ci如果教学环境是 5.7就统一写 utf8mb4_general_ci不要在同一个库里混用两种规则混用之后随便一个关联查询都会报 Illegal mix of collations。学号建议 VARCHAR(20) 而不是 INT因为很多学校的学号带年份前缀甚至带字母用整型会丢前导零。姓名 VARCHAR(30)性别用 CHAR(1) 存 M/F比 TINYINT 更可读也比 ENUM 更安全——ENUM 看起来省事但改枚举值要 ALTER TABLE而且插入非法值时在非严格模式下会静默变成空字符串属于事后很难查的玄学问题。金额一律 DECIMAL(8,2)不要用 FLOAT水电费对账差一分钱都能被追问半天。日期用 DATE时间戳用 DATETIME 并显式设置默认值别指望 TIMESTAMP 自动时区转换教学环境里那玩意儿经常和你想的不一样。状态字段统一用 VARCHAR(10) 存 IN/OUT、PENDING/DONE 这类短码比数字状态码可读答辩演示时一眼能看懂。3.2 建表脚本按依赖顺序写外键最后加建表顺序必须服从外键依赖先父后子class → building → room → student → accommodation → repair → utility_bill → visitor。下面这段是核心骨架可以直接拿去改字段-- 统一下库级字符集避免后面每张表单独指定 CREATE DATABASE IF NOT EXISTS dorm_mgmt DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE dorm_mgmt; CREATE TABLE class ( class_id INT AUTO_INCREMENT PRIMARY KEY, class_name VARCHAR(50) NOT NULL, major VARCHAR(50) NOT NULL, grade SMALLINT NOT NULL, counselor VARCHAR(30) DEFAULT NULL, UNIQUE KEY uk_class_name (class_name) ) ENGINEInnoDB; CREATE TABLE building ( building_id INT AUTO_INCREMENT PRIMARY KEY, building_no VARCHAR(10) NOT NULL, gender_type CHAR(1) NOT NULL, -- M 男生楼 / F 女生楼 floors TINYINT NOT NULL DEFAULT 6, UNIQUE KEY uk_building_no (building_no), CONSTRAINT ck_building_gender CHECK (gender_type IN (M,F)) ) ENGINEInnoDB; CREATE TABLE room ( room_id INT AUTO_INCREMENT PRIMARY KEY, building_id INT NOT NULL, room_no VARCHAR(10) NOT NULL, capacity TINYINT NOT NULL DEFAULT 4, occupied TINYINT NOT NULL DEFAULT 0, -- 冗余计数和住宿记录对账 status VARCHAR(10) NOT NULL DEFAULT AVAILABLE, -- AVAILABLE / FULL / REPAIR UNIQUE KEY uk_room (building_id, room_no), KEY idx_room_status (status), CONSTRAINT fk_room_building FOREIGN KEY (building_id) REFERENCES building (building_id) ON DELETE RESTRICT, CONSTRAINT ck_room_capacity CHECK (capacity BETWEEN 1 AND 8), CONSTRAINT ck_room_occupied CHECK (occupied 0 AND occupied capacity) ) ENGINEInnoDB; CREATE TABLE student ( student_id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL, name VARCHAR(30) NOT NULL, gender CHAR(1) NOT NULL, class_id INT NOT NULL, phone VARCHAR(20) DEFAULT NULL, status VARCHAR(10) NOT NULL DEFAULT ACTIVE, -- ACTIVE / GRADUATED / SUSPENDED UNIQUE KEY uk_student_no (student_no), KEY idx_student_class (class_id), CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES class (class_id) ON DELETE RESTRICT, CONSTRAINT ck_student_gender CHECK (gender IN (M,F)) ) ENGINEInnoDB; CREATE TABLE accommodation ( acc_id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, room_id INT NOT NULL, bed_no TINYINT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE DEFAULT NULL, status VARCHAR(10) NOT NULL DEFAULT IN, -- IN / OUT UNIQUE KEY uk_bed_active (room_id, bed_no, status), -- 防一床两人 KEY idx_acc_student (student_id, status), KEY idx_acc_room (room_id, status), CONSTRAINT fk_acc_student FOREIGN KEY (student_id) REFERENCES student (student_id) ON DELETE RESTRICT, CONSTRAINT fk_acc_room FOREIGN KEY (room_id) REFERENCES room (room_id) ON DELETE RESTRICT ) ENGINEInnoDB;这段脚本里有几个刻意的设计。uk_bed_active 用 (room_id, bed_no, status) 做唯一键是为了让同一个床位不能出现两条 statusIN 的记录退宿后 status 变成 OUT历史记录能留下新同学又能入住同一床位——这是不加触发器也能挡住重复分配的低成本手段代价是同一床位只能留一条 OUT 历史如果要求保留完整历史就改成部分索引或把唯一键挪到应用层校验。room.occupied 是冗余计数好处是列表页不用每次 COUNT坏处是必须靠事务或触发器维护这一点在第 4 章展开。CHECK 约束在 MySQL 8.0.16 之后才真正生效5.7 上写了也只是注释别以为它拦得住脏数据。报修表和账单表照着同样的方式写repair 关联 room 和 studentstudent_id 允许 NULLutility_bill 用 (room_id, bill_month) 做唯一键防重复出账金额字段 DECIMAL(8,2)。所有外键统一 ON DELETE RESTRICT不要在课程设计里用 CASCADE一旦演示时删了一栋楼住宿记录连带清空现场很难解释。3.3 索引怎么加才不白加课程设计里加索引最容易走向两个极端一个不加或者每个字段都加。判断依据只有一条——看你的查询条件和高频页面的 WHERE、JOIN、ORDER BY。宿舍管理系统里真正高频的就那么几条按学号查学生student 上的 uk_student_no 已经覆盖不用额外加。查某房间当前住户accommodation 的 (room_id, status)上面脚本已建。查某学生的住宿历史accommodation 的 (student_id, status)。报修列表按状态和时间倒序repair 上建 (status, report_time)。账单按月份和缴费状态筛utility_bill 上建 (bill_month, pay_status)。联合索引要注意最左前缀比如建了 (status, report_time)只按 report_time 查就用不上。另外别给 gender 这种只有两个值的字段单独建索引选择性太低优化器大概率直接全表扫白占空间。索引不是越多越好每个索引都会拖慢插入初始化几万条测试数据时差别很明显。3.4 初始化数据与字符集检查建完表先灌数据再谈功能。我一般用一段 Python 脚本批量造学生和住宿记录比手写 INSERT 快得多也方便造出“满员房间”“空房间”这些边界场景import random from faker import Faker # 教学环境没有就换成固定姓名列表 fake Faker(zh_CN) rows [] for i in range(1, 301): student_no f2023{i:04d} name fake.name() gender random.choice([M, F]) class_id random.randint(1, 10) rows.append((student_no, name, gender, class_id)) # 拼成批量插入一次性提交比逐条 executemany 快一个量级 values ,.join([(%s,%s,%s,%s)] * len(rows)) sql fINSERT INTO student(student_no,name,gender,class_id) VALUES {values}这里用 executemany 或拼批量 INSERT 都行关键是别在循环里一条一条 commit。造完数据后跑一句SHOW VARIABLES LIKE character_set_%确认库、表、连接三处的字符集一致再插一条带生僻字和 emoji 的备注能正常存取就说明 utf8mb4 生效了。这一步花两分钟能省掉后面文档里中文乱码的一堆麻烦。4. 把业务规则压进数据库触发器、事务与视图4.1 容量与性别校验BEFORE 触发器做校验AFTER 触发器做计数“房间住满了还能再插人”“男生被分到女生楼”是答辩最常被现场演示的两个漏洞。这类规则放在应用层也能写但课程设计的评分点往往就看数据库端有没有体现约束能力所以用触发器实现更划算。要点是把校验和计数拆成两个触发器BEFORE INSERT 只做判断不修改数据AFTER INSERT 才去更新 room.occupied。DELIMITER $$ CREATE TRIGGER trg_acc_before_insert BEFORE INSERT ON accommodation FOR EACH ROW BEGIN DECLARE v_capacity TINYINT; DECLARE v_occupied TINYINT; DECLARE v_room_gender CHAR(1); DECLARE v_room_status VARCHAR(10); DECLARE v_stu_gender CHAR(1); SELECT capacity, occupied, gender_type, status INTO v_capacity, v_occupied, v_room_gender, v_room_status FROM room r JOIN building b ON b.building_id r.building_id WHERE r.room_id NEW.room_id; IF v_room_status REPAIR THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 房间维修中不可安排入住; END IF; IF v_occupied v_capacity THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 房间已满员; END IF; SELECT gender INTO v_stu_gender FROM student WHERE student_id NEW.student_id; IF v_stu_gender v_room_gender THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 学生性别与楼栋类型不匹配; END IF; END$$ CREATE TRIGGER trg_acc_after_insert AFTER INSERT ON accommodation FOR EACH ROW BEGIN IF NEW.status IN THEN UPDATE room SET occupied occupied 1, status IF(occupied 1 capacity, FULL, AVAILABLE) WHERE room_id NEW.room_id; END IF; END$$ DELIMITER ;逻辑上分两段是有原因的BEFORE 触发器里如果去 UPDATE room读到的 occupied 还是语句执行前的旧值连续批量插入时会全部通过校验等于白写而且 MySQL 不允许在触发器里修改正在被当前语句使用的表很容易触发 1442 错误。所以校验放 BEFORE 只读计数放 AFTER 只写职责分开后面排查也清楚。SIGNAL SQLSTATE 45000 是自定义业务错误的通用写法应用层捕获后直接把 MESSAGE_TEXT 弹给用户就行。退宿同理写一个 AFTER UPDATE 触发器当 OLD.statusIN 且 NEW.statusOUT 时把 occupied 减一并把房间状态从 FULL 改回 AVAILABLE。注意触发器里要判断状态是否真的发生变化否则任何一次 UPDATE 都会触发计数数据很快就对不上。4.2 调宿事务两条 UPDATE 之间的原子性不能省调宿是这个系统里唯一必须上事务的操作要把一条住宿记录置为 OUT、插入一条新记录、两个房间的计数各改一次。中间任何一步失败数据就处于“人走了但新房间没进去”的状态。更麻烦的是并发——两个管理员同时把最后一个床位分给不同学生如果不加锁两边都能通过容量校验。START TRANSACTION; -- 按 room_id 排序再加锁避免两个事务交叉锁导致死锁 SELECT room_id, capacity, occupied FROM room WHERE room_id IN (3, 7) ORDER BY room_id FOR UPDATE; UPDATE accommodation SET status OUT, check_out_date CURDATE() WHERE acc_id 1024 AND status IN; INSERT INTO accommodation(student_id, room_id, bed_no, check_in_date, status) VALUES (215, 7, 2, CURDATE(), IN); COMMIT;FOR UPDATE 锁住的是 room 表的这两行容量校验和后续的计数更新就都在锁的保护下。排序加锁是血泪经验两个事务分别锁房间 3 和 7、再互相请求对方持有的锁就会死锁按固定顺序请求能规避掉大部分。事务里要检查 UPDATE 影响行数如果返回 0说明这条记录已经被别人退宿了应该 ROLLBACK 而不是继续插新记录。另外整个事务要尽量短别把校验提示、日志写入这些放在事务中间。4.3 视图报表页不该重写一遍多表 JOIN宿舍管理的查询页翻来覆去就那几个需求当前住宿明细、空床位列表、某楼栋入住率、欠费房间。把这些固化成视图既能让后端代码变短也能在文档里体现“外模式”的设计层次。CREATE VIEW v_current_residence AS SELECT s.student_no, s.name, s.gender, c.class_name, b.building_no, r.room_no, a.bed_no, a.check_in_date FROM accommodation a JOIN student s ON s.student_id a.student_id JOIN class c ON c.class_id s.class_id JOIN room r ON r.room_id a.room_id JOIN building b ON b.building_id r.building_id WHERE a.status IN; CREATE VIEW v_empty_bed AS SELECT r.room_id, b.building_no, r.room_no, r.capacity, r.occupied, r.capacity - r.occupied AS free_beds FROM room r JOIN building b ON b.building_id r.building_id WHERE r.status REPAIR AND r.occupied r.capacity;视图的好处是改口径只改一处比如以后要排除休学学生只在 v_current_residence 里加一个条件所有调用方自动生效。代价是视图不存数据、每次查询都展开执行所以别在视图里套视图也别指望它加速。空床位视图用的是 room.occupied 这个冗余计数如果计数维护错了视图结果就跟着错这也是第 6 章要做对账的原因。5. 避坑与排查五类真会卡住人的现场5.1 建表报 1215、删数据报 1451现象执行建表脚本时提示 Cannot add foreign key constraint或者删学生时提示 Cannot delete or update a parent row。原因基本只有三种父表还没建、外键字段和主键字段的类型或字符集不一致、被引用列没有索引。删不掉则是子表还有引用数据外键的 RESTRICT 在起作用。解决严格按 class → building → room → student → accommodation 的顺序建表外键列的类型写成和被引用列完全一致INT 对 INT别一边 INT 一边 BIGINT实在要调整顺序先建表不加外键最后统一 ALTER TABLE ADD CONSTRAINT。删数据时按子表到父表的顺序清理或者临时SET FOREIGN_KEY_CHECKS 0之后务必记得改回 1忘了这一步后面所有约束都形同虚设。5.2 关联查询报 Illegal mix of collations现象两个表单独查都正常一 JOIN 就报字符集排序规则不一致。原因建库时没指定字符集某张表用了 utf8mb4_general_ci另一张用了 utf8mb4_0900_ai_ci或者其他表是 utf8。字符集相同、排序规则不同也会报错。解决先查SELECT table_name, table_collation FROM information_schema.tables WHERE table_schemadorm_mgmt把不一致的表统一成同一个规则之后建表脚本里显式写 COLLATE别依赖服务器默认值。连接串上也加上 charsetutf8mb4三处对齐才不会反复。5.3 触发器报 1442或者计数越跑越偏现象插入住宿记录时提示 Cant update table room in stored function/trigger because it is already used by statement。原因在 BEFORE INSERT 触发器里 UPDATE 了 room 表或者触发器里又更新了自己所在的表形成递归。解决校验类逻辑放 BEFORE 只读计数类逻辑放 AFTER 只写两者不要混在一个触发器里如果确实需要在同一张表上做级联更新改用存储过程由应用层调用。计数跑偏的另一个常见原因是退宿触发器没判断 OLD.status 和 NEW.status 是否真的变了导致重复扣减加一层状态变化判断就能解决。5.4 文档里的字段和 SQL 对不上现象Word 模板里字段说明写的是 12 个字段实际建表只有 10 个或者字段名大小写不一致。原因文档和脚本分开维护改了一边忘了另一边。解决以 DDL 为唯一事实来源先定稿建表脚本再照着脚本反向填字段说明表改字段时先改 SQL 再同步文档。导出 E-R 图也用同一份脚本别手工另画一份两边必然对不上。这是答辩时最容易被翻出来的一条属于纯送分也纯送命的类型。5.5 演示数据太少功能看不出效果计数也对不上现象本地只插了十几个学生索引有没有生效看不出来入住率统计全是 0% 和 100%occupied 字段和实际人数偶尔差一两个。原因数据量太小加上冗余计数靠手改。测试时图省事直接 UPDATE room SET occupied 3忘了和 accommodation 表同步。解决至少造三千条学生、一千条住宿记录几十个房间分布在三到五栋楼边界场景满员房、空房、维修房、刚退宿的房各留几个。occupied 不允许手工改一律走触发器或事务。演示前跑一遍对账 SQL确认计数和明细一致再上台。6. 用验证脚本自证设计对账、执行计划与文档一致性设计做完不等于没问题得能自己证明。我常用的第一条是计数对账一条 SQL 就能把 room.occupied 和住宿明细的差异全找出来SELECT r.room_id, r.capacity, r.occupied, COUNT(a.acc_id) AS actual_in, r.occupied - COUNT(a.acc_id) AS diff FROM room r LEFT JOIN accommodation a ON a.room_id r.room_id AND a.status IN GROUP BY r.room_id, r.capacity, r.occupied HAVING diff 0;返回空集才算通过。第二条是床位冲突检查找出同一房间同一床位存在两条 IN 记录的情况正常情况下也应该为空。第三条是孤儿数据检查用 LEFT JOIN 找 accommodation 里指向不存在学生或房间的记录这类数据一旦出现通常是外键被临时关掉过。索引是否生效不能靠感觉用 EXPLAIN 看。EXPLAIN SELECT ... WHERE a.statusIN AND r.building_id3跑一遍重点看 type 是不是 ref 或 rangekey 是不是命中了预期的索引rows 估算值和实际数据量比例是否合理。如果 type 是 ALL说明在走全表扫检查一下查询条件有没有破坏最左前缀。这一套跑完文档里的“测试与性能分析”章节直接有素材不用编。Word 模板别只当排版工具用按它的骨架填内容反而能反过来查设计漏洞模板章节该放什么常被追问的点需求分析业务动作清单、数据字典数据字典和建表字段是否一一对应概念设计E-R 图、实体属性说明住宿记录为什么独立成表逻辑设计关系模式、范式说明冗余的 occupied 违反第几范式、凭什么保留物理设计索引、字符集、存储引擎索引服务于哪条查询系统实现关键 SQL、触发器、事务触发器为什么拆成两个测试对账 SQL、EXPLAIN 结果并发调宿怎么保证不出错填这张表的时候你会被迫回答“冗余字段凭什么留”这恰恰是答辩最容易卡住的地方能说清楚“为了列表页免 COUNT靠触发器保证一致”这个点就变成加分项而不是漏洞。从那以后我每次交这类设计都会在打包前把对账 SQL 和 EXPLAIN 各跑一遍空集和命中索引截图留档文档里字段说明和 DDL 逐条对一遍才收工。带 SQL 文件、源代码和 Word 模板的这套底稿拿来改改表名和字段就能开工省下的时间够你把触发器和事务这两块真正吃透。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站