
简介这是一套面向C#初学者与课程设计需求的宿舍信息管理系统源码采用WinForm界面结合SQL Server数据库并按三层架构Models、DAL、BLL、UI组织代码适合用来理解分层开发思想与增删查改的完整实现。系统功能覆盖管理员登录注册、账号信息修改以及学生宿舍信息的添加、删除、精确或模糊查询与修改基本满足高校宿舍管理的日常业务场景。资源包共126个文件以43个cs源码文件为核心辅以dll、pdb、resx、resources、config、csproj等编译与配置类文件另有少量txt说明与jpg图片整体约495KB结构紧凑便于直接运行调试。目前已有155人学习下载读者可借此获得一套可直接参考的三层架构项目模板对照源码梳理实体层、数据访问层与业务逻辑层的调用关系并在此基础上扩展功能或完成课程设计任务。1. 从一张Excel表到三层架构宿舍信息管理系统到底该怎么落地每年九月开学宿管中心最头疼的不是床位够不够而是那张越滚越大的 Excel 表。新生入住要改毕业生离校要删调宿要跨表核对查个空床位得翻三遍筛选。用 C# 在 Windows 上做一套基于三层架构的宿舍信息管理系统配一个正经数据库就是把这堆手工活变成可查询、可追溯、可并发的软件流程。三层架构在这里不是学院派炫技而是把界面、业务规则、数据访问拆开让改一个查寝逻辑不用动窗体代码换数据库不用重写业务判断。这套东西适合两类人一是课程设计或毕设需要完整可运行项目的学生二是刚转 C# 上位机、想拿一个真实增删改查场景练手的初级开发者。下面按我实际搭过的一套方案从选型到建库到排错讲清楚。2. 三层架构在宿舍管理场景里怎么切UI、BLL、DAL 的边界与选型2.1 为什么宿舍管理适合三层而不是把 SQL 写进按钮事件很多初学者做 WinForms 项目习惯在按钮的 Click 事件里直接拼 SQL 字符串连上数据库执行完把结果塞进 DataGridView。这种写法在只有一张学生表时能跑但宿舍管理至少涉及学生、宿舍楼、房间、床位、入住记录五张表还要处理床位占用状态、性别与楼栋匹配、退宿后床位释放这些规则。一旦把UPDATE Bed SET Status1 WHERE BedIdid和if (性别楼栋性别)混在窗体代码里改一个规则就要重新编译整个界面层测试也没法单独测业务逻辑。三层架构的切法是UI 层只负责收集输入和展示结果BLL 层负责判断“这个床位能不能分配”“这个学生有没有未退宿记录”DAL 层只负责执行参数化 SQL 并返回 DataTable 或实体对象。这样 BLL 可以脱离窗体单独用控制台测试DAL 换 MySQL 还是 SQL Server 只改连接字符串和少量方言。常见做法是再加一个 Model 层放实体类但严格说它不算独立的一层只是数据载体。2.2 项目结构、引用关系与数据库选型我一般用 Visual Studio 建一个解决方案里面放四个项目Dormitory.UIWinForms、Dormitory.BLL类库、Dormitory.DAL类库、Dormitory.Model类库。引用关系是 UI 引用 BLL 和 ModelBLL 引用 DAL 和 ModelDAL 引用 Model。UI 不直接引用 DAL这是三层架构的硬规矩破了这条线后面就会失控。数据库选型上课程设计和中小规模宿舍管理用 SQL Server Express 或 MySQL 都行。SQL Server 在 Windows 上装起来省事Visual Studio 自带连接工具MySQL 跨平台、部署轻配合 Navicat 或 DBeaver 管理也顺手。我下面以 SQL Server 为例连接字符串用Integrated Security或账号密码都行。表结构核心是五张表名关键字段说明StudentStudentId, Name, Gender, ClassName, Phone学生基本信息BuildingBuildingId, BuildingName, GenderType楼栋GenderType 区分男/女RoomRoomId, BuildingId, RoomNo, Capacity房间容量决定床位数BedBedId, RoomId, BedNo, Status床位Status 0空闲 1占用CheckInCheckInId, StudentId, BedId, CheckInDate, CheckOutDate入住记录建库脚本用一条CREATE DATABASE DormitoryDB起头然后逐表建。注意 Bed 表的 Status 字段加默认值 0CheckIn 的 CheckOutDate 允许 NULL 表示在住。2.3 从建库到 DAL 第一个查询方法的可复现步骤先在 SQL Server Management Studio 里执行建库建表然后回到 Visual Studio 写 DAL。DAL 里我习惯用一个SqlHelper静态类封装连接和命令避免每个方法都写一遍using SqlConnection。核心代码如下// Dal/SqlHelper.cs using System; using System.Data; using System.Data.SqlClient; namespace Dormitory.DAL { public static class SqlHelper { // 连接字符串放在配置文件更规范这里为演示直接写常量 private static readonly string ConnStr Server.;DatabaseDormitoryDB;Integrated SecurityTrue;; // 执行查询返回 DataTable适合绑定 DataGridView public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(ConnStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); SqlDataAdapter adapter new SqlDataAdapter(cmd); DataTable dt new DataTable(); adapter.Fill(dt); // Fill 内部会打开和关闭连接 return dt; } } // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(ConnStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } } }这段代码的关键点是params SqlParameter[]它让调用方必须传参数化对象从源头堵住 SQL 注入。ExecuteQuery用SqlDataAdapter.Fill不需要手动 OpenFill 会自己管理连接状态。参数说明ConnStr里的Server.表示本机默认实例如果装了命名实例要写成Server.\SQLEXPRESSIntegrated SecurityTrue表示用 Windows 身份验证如果要用 SQL 账号密码就改成User Idsa;Password你的密码。接着写一个具体的 DAL 方法比如查所有空闲床位// Dal/BedDal.cs public static DataTable GetFreeBeds() { string sql SELECT b.BedId, b.BedNo, r.RoomNo, bld.BuildingName FROM Bed b JOIN Room r ON b.RoomId r.RoomId JOIN Building bld ON r.BuildingId bld.BuildingId WHERE b.Status 0; return SqlHelper.ExecuteQuery(sql); }这里用 JOIN 把楼栋名和房间号一起查出来UI 层直接绑定就能显示不用在窗体里再查一次。参数没有需要外部传入的所以没加 SqlParameter。如果按楼栋筛选就加AND bld.BuildingId bid然后传new SqlParameter(bid, buildingId)。3. BLL 层怎么写业务规则床位分配、退宿与性别校验3.1 床位分配的业务判断不能放在 UI 里床位分配是这套系统里业务规则最密集的操作。UI 层只应该拿到“学生ID”和“床位ID”两个值剩下的判断全在 BLL。具体要判断学生是否存在、床位是否空闲、学生性别是否和楼栋性别类型一致、学生是否已有未退宿记录。这四条任何一条不过都不应该执行更新。BLL 方法签名我一般写成public static string AssignBed(int studentId, int bedId)返回空字符串表示成功返回非空字符串表示失败原因。这样 UI 层直接弹 MessageBox 显示返回值不用在 UI 里写 if-else 判断业务。// Bll/CheckInBll.cs public static string AssignBed(int studentId, int bedId) { // 1. 学生是否存在 DataTable stu StudentDal.GetById(studentId); if (stu.Rows.Count 0) return 学生不存在; // 2. 床位是否空闲同时取出楼栋性别类型 DataTable bed BedDal.GetBedWithBuilding(bedId); if (bed.Rows.Count 0) return 床位不存在; if (Convert.ToInt32(bed.Rows[0][Status]) ! 0) return 该床位已被占用; // 3. 性别校验 string stuGender stu.Rows[0][Gender].ToString(); string buildingGender bed.Rows[0][GenderType].ToString(); if (stuGender ! buildingGender) return 学生性别与楼栋类型不匹配; // 4. 是否已有在住记录 if (CheckInDal.HasActiveCheckIn(studentId)) return 该学生已有未退宿记录; // 5. 事务插入入住记录 更新床位状态 return CheckInDal.AssignBedTransaction(studentId, bedId) ? : 分配失败请重试; }这段代码里第 5 步必须用事务因为插入 CheckIn 和更新 Bed.Status 要么都成功要么都回滚否则会出现床位被占但没有入住记录的脏数据。DAL 里的事务写法// Dal/CheckInDal.cs public static bool AssignBedTransaction(int studentId, int bedId) { using (SqlConnection conn new SqlConnection(SqlHelper.ConnStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { string sql1 INSERT INTO CheckIn(StudentId,BedId,CheckInDate) VALUES(sid,bid,GETDATE()); string sql2 UPDATE Bed SET Status1 WHERE BedIdbid AND Status0; using (SqlCommand cmd1 new SqlCommand(sql1, conn, tran)) using (SqlCommand cmd2 new SqlCommand(sql2, conn, tran)) { cmd1.Parameters.AddWithValue(sid, studentId); cmd1.Parameters.AddWithValue(bid, bedId); cmd2.Parameters.AddWithValue(bid, bedId); cmd1.ExecuteNonQuery(); int rows cmd2.ExecuteNonQuery(); if (rows 0) { tran.Rollback(); return false; } // 并发下床位被抢 tran.Commit(); return true; } } catch { tran.Rollback(); return false; } } } }注意UPDATE Bed SET Status1 WHERE BedIdbid AND Status0这个条件里的AND Status0是防并发抢床位的后悔药。如果两个宿管同时给两个学生分配同一张床第二个人的 UPDATE 会影响 0 行事务回滚不会出现两人住一张床的玄学数据。3.2 退宿与床位释放的完整链路退宿操作要更新 CheckIn 的 CheckOutDate 为当前时间同时把 Bed.Status 改回 0。同样用事务包起来。BLL 里先判断该学生是否有在住记录没有就返回“无在住记录”。DAL 事务里两条 SQLUPDATE CheckIn SET CheckOutDateGETDATE() WHERE StudentIdsid AND CheckOutDate IS NULL和UPDATE Bed SET Status0 WHERE BedId(SELECT BedId FROM CheckIn WHERE StudentIdsid AND CheckOutDate IS NULL)。第二条子查询在更新后 CheckOutDate 已不为 NULL所以要先取 BedId 再更新或者把顺序反过来先更新 Bed 再更新 CheckIn。我一般先查在住记录的 BedId再分别更新逻辑更清晰。3.3 UI 层怎么调 BLL 并处理返回值WinForms 里一个“分配床位”按钮的 Click 事件应该只有几行private void btnAssign_Click(object sender, EventArgs e) { int studentId int.Parse(txtStudentId.Text.Trim()); int bedId int.Parse(dgvFreeBeds.CurrentRow.Cells[BedId].Value.ToString()); string result CheckInBll.AssignBed(studentId, bedId); if (result ) { MessageBox.Show(分配成功); RefreshFreeBeds(); // 重新加载空闲床位列表 } else { MessageBox.Show(result); } }UI 层不出现任何 SQL 关键字不出现事务不出现性别判断。这就是三层架构在宿舍管理里最直接的收益窗体代码从几百行缩到几十行业务规则集中在 BLL 可单独测试。4. 避坑与排查连接失败、并发抢床位、DataGridView 绑定的血泪经验4.1 连接 SQL Server 报“无法打开登录所请求的数据库”现象程序启动后第一次查询就抛异常提示无法打开 DormitoryDB。原因通常是连接字符串里的数据库名拼错或者 SQL Server 服务没启动或者当前 Windows 账号没有该库的访问权限。解决先在 SSMS 里用同一套身份验证方式连一次确认库存在且能查然后检查连接字符串的Database后面是否和实际库名完全一致包括大小写敏感的情况。如果是命名实例Server.要改成Server.\SQLEXPRESS。权限问题就在 SSMS 里给当前 Windows 用户授予 db_datareader 和 db_datawriter。4.2 并发分配同一床位导致两人入住现象两个宿管同时操作系统显示都分配成功但数据库里只有一条 CheckIn 记录Bed 状态却是 1。原因就是前面说的没有在 UPDATE 里加AND Status0条件或者没有用事务。解决按 3.1 的事务写法UPDATE 带状态条件影响行数为 0 就回滚。另外可以在 Bed 表的 Status 字段上加一个行版本或直接用UPDATE ... WHERE Status0的原子性来兜底。4.3 DataGridView 绑定 DataTable 后修改不生效现象把 DataTable 直接赋给dgv.DataSource用户在表格里改了单元格点保存却写不回数据库。原因DataTable 是内存副本DataGridView 的修改只改内存不会自动回写数据库。解决要么用SqlDataAdapter配合SqlCommandBuilder做自动更新要么在保存按钮里遍历dgv.Rows逐行取值调 BLL 的更新方法。我一般用后者因为业务规则要在 BLL 里校验自动更新会绕过 BLL。4.4 参数化查询里 SqlParameter 顺序和名称对不上现象明明传了参数查询结果为空或报“必须声明标量变量 id”。原因SQL 里写的是studentId代码里new SqlParameter(id, ...)名称不一致。解决参数名和 SQL 里的占位符严格一致包括 符号。用AddWithValue时也要注意类型比如 int 和 string 混用可能导致隐式转换失败。4.5 窗体关闭后数据库连接未释放现象程序运行一段时间后报“超时时间已到但是尚未从池中获取连接”。原因有的代码手动conn.Open()后忘了conn.Close()或者SqlDataReader没关。解决统一用using包裹 SqlConnection、SqlCommand、SqlDataReader让 Dispose 自动释放。SqlHelper 里用 SqlDataAdapter.Fill 就不用手动管连接。如果必须用 DataReader记得CommandBehavior.CloseConnection。5. 进阶技巧用存储过程收口复杂查询与分页当宿舍管理要查“某楼栋某层所有房间的床位占用情况”并分页显示时DAL 里拼 SQL 会越来越长。我一般把这类复杂查询写成存储过程DAL 只负责传参和接结果。比如按楼栋和页码查床位CREATE PROCEDURE GetBedsByBuildingPaged BuildingId INT, PageIndex INT, PageSize INT AS BEGIN SELECT b.BedId, b.BedNo, r.RoomNo, b.Status, s.Name AS StudentName FROM Bed b JOIN Room r ON b.RoomId r.RoomId LEFT JOIN CheckIn c ON b.BedId c.BedId AND c.CheckOutDate IS NULL LEFT JOIN Student s ON c.StudentId s.StudentId WHERE r.BuildingId BuildingId ORDER BY r.RoomNo, b.BedNo OFFSET (PageIndex-1)*PageSize ROWS FETCH NEXT PageSize ROWS ONLY; ENDDAL 调用时用CommandType.StoredProcedure参数照传。这样分页逻辑在数据库端完成UI 层只翻页。注意OFFSET...FETCH需要 SQL Server 2012 及以上MySQL 用LIMIT。存储过程的好处是执行计划缓存坏处是调试不如代码直观所以只把最复杂的查询放进去简单增删改还是留在 DAL 里。验证方法上我习惯在 BLL 层写一个控制台测试项目不启动 WinForms 直接调 BLL 方法用断点看返回值。比如分配床位后查 Bed 表 Status 是否为 1CheckIn 表是否多了一条记录退宿后 Status 是否回 0。这样比在窗体里点按钮快得多。另外数据库改动前先备份或者用事务包住测试数据测完回滚。最后说个我自己的习惯每加一个 BLL 方法先在 DAL 里把对应的 SQL 在 SSMS 里跑通确认结果集正确再写 C# 调用。直接写 C# 再调试 SQL 字符串翻车概率高得多。三层架构的边界守住了后面加查寝记录、报修管理都是照葫芦画瓢。希望帮到你。本文还有配套的精品资源点击获取