news 2026/10/1 8:54:46

C#三层架构实战:宿舍信息管理系统从Excel到数据库的落地指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#三层架构实战:宿舍信息管理系统从Excel到数据库的落地指南

简介:这是一套面向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 Status=1 WHERE BedId=@id和if (性别==楼栋性别)混在窗体代码里,改一个规则就要重新编译整个界面层,测试也没法单独测业务逻辑。

三层架构的切法是:UI 层只负责收集输入和展示结果,BLL 层负责判断“这个床位能不能分配”“这个学生有没有未退宿记录”,DAL 层只负责执行参数化 SQL 并返回 DataTable 或实体对象。这样 BLL 可以脱离窗体单独用控制台测试,DAL 换 MySQL 还是 SQL Server 只改连接字符串和少量方言。常见做法是再加一个 Model 层放实体类,但严格说它不算独立的一层,只是数据载体。

2.2 项目结构、引用关系与数据库选型

我一般用 Visual Studio 建一个解决方案,里面放四个项目:Dormitory.UI(WinForms)、Dormitory.BLL(类库)、Dormitory.DAL(类库)、Dormitory.Model(类库)。引用关系是 UI 引用 BLL 和 Model,BLL 引用 DAL 和 Model,DAL 引用 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 字段加默认值 0,CheckIn 的 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=.;Database=DormitoryDB;Integrated Security=True;"; // 执行查询,返回 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,不需要手动 Open,Fill 会自己管理连接状态。参数说明:ConnStr里的Server=.表示本机默认实例,如果装了命名实例要写成Server=.\SQLEXPRESS;Integrated Security=True表示用 Windows 身份验证,如果要用 SQL 账号密码就改成User Id=sa;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 Status=1 WHERE BedId=@bid AND Status=0"; 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 Status=1 WHERE BedId=@bid AND Status=0这个条件里的AND Status=0是防并发抢床位的后悔药。如果两个宿管同时给两个学生分配同一张床,第二个人的 UPDATE 会影响 0 行,事务回滚,不会出现两人住一张床的玄学数据。

3.2 退宿与床位释放的完整链路

退宿操作要更新 CheckIn 的 CheckOutDate 为当前时间,同时把 Bed.Status 改回 0。同样用事务包起来。BLL 里先判断该学生是否有在住记录,没有就返回“无在住记录”。DAL 事务里两条 SQL:UPDATE CheckIn SET CheckOutDate=GETDATE() WHERE StudentId=@sid AND CheckOutDate IS NULL和UPDATE Bed SET Status=0 WHERE BedId=(SELECT BedId FROM CheckIn WHERE StudentId=@sid 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 Status=0条件,或者没有用事务。解决:按 3.1 的事务写法,UPDATE 带状态条件,影响行数为 0 就回滚。另外可以在 Bed 表的 Status 字段上加一个行版本或直接用UPDATE ... WHERE Status=0的原子性来兜底。

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; END

DAL 调用时用CommandType.StoredProcedure,参数照传。这样分页逻辑在数据库端完成,UI 层只翻页。注意OFFSET...FETCH需要 SQL Server 2012 及以上,MySQL 用LIMIT。存储过程的好处是执行计划缓存,坏处是调试不如代码直观,所以只把最复杂的查询放进去,简单增删改还是留在 DAL 里。

验证方法上,我习惯在 BLL 层写一个控制台测试项目,不启动 WinForms 直接调 BLL 方法,用断点看返回值。比如分配床位后查 Bed 表 Status 是否为 1,CheckIn 表是否多了一条记录,退宿后 Status 是否回 0。这样比在窗体里点按钮快得多。另外数据库改动前先备份,或者用事务包住测试数据,测完回滚。

最后说个我自己的习惯:每加一个 BLL 方法,先在 DAL 里把对应的 SQL 在 SSMS 里跑通,确认结果集正确,再写 C# 调用。直接写 C# 再调试 SQL 字符串,翻车概率高得多。三层架构的边界守住了,后面加查寝记录、报修管理都是照葫芦画瓢。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 8:54:40

VMware Fusion 免费版下载安装与注册全攻略:Mac 虚拟机实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 8:53:41

接口自动化框架的工程化发布:从Jenkins流水线到灰度落地

“接口自动化”这个词,我听了无数次,也见团队里做过无数个项目。大部分测试同学以为难点在写用例、调参数、做断言,可真把一个接口自动化框架从零搭到能在团队里持续跑、每天定时出报告、迭代后不崩,我最大的体会是:难…

作者头像 李华
网站建设 2026/10/1 8:53:13

ESP32-P4NRW32X深度解析:从架构到H264硬编码实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 8:51:57

机器学习试卷自动化批改与知识图谱构建实践

简介:本资源是一份面向高校计算机、人工智能及相关专业本科生的机器学习课程期末复习试卷,聚焦核心知识点梳理与应试能力提升。试卷内容系统覆盖机器学习基本概念(监督/无监督/半监督学习)、主流算法(逻辑回归、决策树…

作者头像 李华