
简介这是一套面向C#课程设计的宿舍管理系统完整项目适合高校计算机专业学生用于课设参考或功能二次开发。系统区分学生与管理员两种登录角色学生端支持查看室友、宿舍报修、申请离宿、请假与销假管理端支持学生信息管理、公告发布、报修审批及请假审批并将学生数据、账号密码存储于数据库用户信息以XML记录界面采用CSkin美化同时通过WebService调用地理位置与天气接口整体功能链完整。压缩包共188个文件主要包含C#源码、资源文件、SQL及数据库脚本、配置文件、可执行程序、说明书及设计流程图等合计4.7MB目录结构清晰便于对照学习或直接运行调试。目前已有1377人学习下载。除完整源码外包内还附带docx说明书、流程图和SQL文件可帮助理解系统架构、数据库设计与核心逻辑适合课程设计答辩准备或作为C#SQL项目练手素材。1. 拿到 C#宿舍管理系统源码不建议先急着编译如果手上刚多了一份“C#宿舍管理系统.rar”绝大多数人第一反应是解压、打开 .sln、按 F5然后在“数据库连接失败”的错误弹窗里浪费掉半个下午。这个标题背后其实是典型的 WinForms SQL Server 桌面管理系统功能边界很固定学生住宿登记、床位分配与调整、退宿、来访登记、水电费记录加一点统计报表。它并不讲究高并发或分布式但把数据库表和界面交互搞明白比让代码跑起来更有价值。这类项目适合三类人做 C# 课程设计和毕设的学生需要给内部后勤做管理小工具的从业者以及刚入门 C# 想用完整项目练手的开发者。这篇文章按“数据建模 → 数据访问层 → 界面绑定 → 踩坑排查 → 收尾技巧”的顺序把一套能跑的宿舍管理系统讲清楚参数和代码都能直接抄进自己的项目。2. 数据先行宿舍管理系统的最佳表结构长什么样2.1 功能模块先拆开再决定建哪几张表宿舍管理系统和教务管理系统的本质区别在于它的核心资源是“床位”所有业务都围绕床位状态流转。一个学生的入宿、换宿、退宿落到底就是床位状态从 0 变 1、从 1 变 0 的过程。所以表结构设计的第一原则是把学生信息、宿舍信息、床位信息、住宿流水拆成独立表不要在一张表里塞全部字段。很多毕业设计项目翻车不是因为功能做不完而是因为“Student”表里出现了一列“床位字符串”或者“Dorm”表里直接存了一个“学生名单”。短时间看很方便但等你要查“A1 栋还有哪些空床位”时就不得不写字符串拆分逻辑代码越写越玄学。我一般会先把模块拆成下面五块再开始建表模块核心职责最小字段集合学生管理基本信息维护学号、姓名、班级、院系、联系电话宿舍管理楼栋、房间、床位维护楼栋号、房间号、容量、宿舍类型住宿管理入住、调宿、退宿学号、床位ID、入住时间、退宿时间报修管理设施报修登记与处理报修人、宿舍、报修内容、状态来访登记访客进出记录被访宿舍、访客姓名、来访时间拆成这五块之后表之间的关系就很清楚一个宿舍对应多个床位一个床位只能被一个当前住宿的学生占用一个学生在不同时间段可以对应多个床位但“当前住宿记录”只能有一条未退宿记录。这个边界想清楚后面的代码写起来非常顺手。2.2 四张核心表的建表 SQL 与字段选型下面用 SQL Server 语法给出核心建表脚本。宿舍管理系统最常见的技术栈就是 WinForms SQL Server类库引用和代码风格都比较统一所以我以 SQL Server 为例。CREATE TABLE dbo.Dormitory ( DormID INT IDENTITY(1,1) PRIMARY KEY, BuildingNo NVARCHAR(20) NOT NULL, -- 楼栋号如 A1 RoomNo NVARCHAR(20) NOT NULL, -- 房间号如 301 Capacity INT NOT NULL DEFAULT 4, -- 房间总床位数 DormType NVARCHAR(20) NULL, -- 普通宿舍/空调宿舍 CONSTRAINT UQ_Dorm UNIQUE (BuildingNo, RoomNo) ); CREATE TABLE dbo.Bed ( BedID INT IDENTITY(1,1) PRIMARY KEY, DormID INT NOT NULL REFERENCES dbo.Dormitory(DormID), BedNo INT NOT NULL, -- 床号1~Capacity Status TINYINT NOT NULL DEFAULT 0, -- 0空床 1已入住 2维修中 CONSTRAINT UQ_BedInDorm UNIQUE (DormID, BedNo) ); CREATE TABLE dbo.Student ( StudentID INT IDENTITY(1,1) PRIMARY KEY, StudentNo NVARCHAR(20) NOT NULL, -- 学号注意用文本不要用数字 StudentName NVARCHAR(50) NOT NULL, ClassName NVARCHAR(50) NULL, Phone NVARCHAR(20) NULL, CONSTRAINT UQ_Student UNIQUE (StudentNo) ); CREATE TABLE dbo.Allocation ( AllocationID INT IDENTITY(1,1) PRIMARY KEY, StudentID INT NOT NULL REFERENCES dbo.Student(StudentID), BedID INT NOT NULL REFERENCES dbo.Bed(BedID), CheckInTime DATETIME NOT NULL DEFAULT GETDATE(), CheckOutTime DATETIME NULL, -- 空着表示当前正在住 Remark NVARCHAR(200) NULL );逻辑说明和参数说明Capacity是房间总床位数界面联动时要用它判断“宿舍是否已住满”。Bed.Status用 TINYINT 而不是 NVARCHAR。原因很简单状态判断用数值比字符串快且不容易写错拼写。0 代表空床1 代表已入住2 代表维修中。Allocation是住宿流水表入住时间写在流水里而不是写在学生表里这样学生退宿后历史记录不会丢。StudentNo用 NVARCHAR 是刻意为之。学号如果以 0 开头或者长度超过 11 位用 INT 存储会在导入 Excel 时丢失精度。这个问题到第 5 章还会再踩一次。唯一约束UQ_BedInDorm防止同一个房间出现两个相同床号属于物理层面的数据兜底。一张表走天下的做法短平快但后面调宿和统计入住率时一定会砍到脚踝。先花 20 分钟把表建对比之后补索引和拆字段省力得多。3. 数据访问层落地连接串、SQLHelper 与分配事务3.1 连接串与 DbHelper 的最小实现WinForms 项目里最常用的数据访问方式仍然是 ADO.NET而不是 Entity Framework。原因并不复杂宿舍管理系统这类内部工具表结构固定、查询模式固定ADO.NET 写起来直观出了问题也好排查。把连接串放在 App.config 里然后写一个静态的数据库帮助类是这类项目最常见也最稳妥的做法。先看 App.config 里的连接串配置connectionStrings add nameDormDB connectionStringData Source.;Initial CatalogDormManageDB;User IDsa;Password123456;MultipleActiveResultSetstrue providerNameSystem.Data.SqlClient / /connectionStrings然后是一个最小的 DbHelper 类。宿舍管理系统的绝大多数数据库操作只需要三件套执行增删改、查询单个值、查询数据表。using System; using System.Configuration; using System.Data; using System.Data.SqlClient; public static class DbHelper { private static readonly string ConnStr ConfigurationManager.ConnectionStrings[DormDB].ConnectionString; public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (ps ! null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } public static object ExecuteScalar(string sql, params SqlParameter[] ps) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (ps ! null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteScalar(); } } public static DataTable ExecuteTable(string sql, params SqlParameter[] ps) { using (var conn new SqlConnection(ConnStr)) using (var da new SqlDataAdapter(sql, conn)) { if (ps ! null) da.SelectCommand.Parameters.AddRange(ps); var dt new DataTable(); da.Fill(dt); return dt; } } }几个参数你要重点关注MultipleActiveResultSetstrue对 WinForms 下多个 DataReader 交替读取有帮助单用户后台管理程序加上它更从容。如果不需要去掉也不影响。每个方法都用using包装连接。宿舍管理系统通常要连续打开关闭几十次数据库连接不释放连接会让程序越用越卡。params SqlParameter[]的设计是强制调用方走参数化 SQL。哪怕查询语句是拼接出来的只要参数用xxx占位就能挡住最常见的 SQL 注入。3.2 用事务把“分配宿舍”做成不会超卖的操作分配宿舍是宿舍管理系统里最容易出错的动作。常见流程是查出空床位选一张改状态插入入住流水。问题是“查出空床位”和“改状态”之间存在时间窗口如果两个管理员同时操作或者同一个窗体被点了两次同一个床位可能被分配给两个人。网上很多方案是给按钮加一个bool标志位点击后置灰防止二次点击。这个方案解决不了两个客户端同时操作的问题只能挡住单窗口的重复点击。真正的解法在数据库层用事务把“占用床位”和“写入流水”包在一起并且用受影响行数判断床位是否被抢。public bool AssignBed(int studentId, int dormId, out string errMsg) { errMsg ; const string findBedSql SELECT TOP 1 BedID FROM Bed WHERE DormID DormID AND Status 0 ORDER BY BedNo;; const string occupySql UPDATE Bed SET Status 1 WHERE BedID BedID AND Status 0;; const string insertAllocSql INSERT INTO Allocation(StudentID, BedID, CheckInTime) VALUES (StudentID, BedID, GETDATE());; using (var conn new SqlConnection(DbHelper.ConnString)) { conn.Open(); using (var tx conn.BeginTransaction()) { try { object? result null; using (var cmd new SqlCommand(findBedSql, conn, tx)) { cmd.Parameters.AddWithValue(DormID, dormId); result cmd.ExecuteScalar(); } if (result null || result DBNull.Value) { errMsg 该宿舍暂无空床位; return false; } int bedId Convert.ToInt32(result); using (var cmd new SqlCommand(occupySql, conn, tx)) { cmd.Parameters.AddWithValue(BedID, bedId); if (cmd.ExecuteNonQuery() 0) { errMsg 床位刚被其他操作占用请重试; return false; } } using (var cmd new SqlCommand(insertAllocSql, conn, tx)) { cmd.Parameters.AddWithValue(StudentID, studentId); cmd.Parameters.AddWithValue(BedID, bedId); cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch (Exception ex) { tx.Rollback(); errMsg ex.Message; return false; } } } }这段代码的逻辑核心在UPDATE Bed SET Status 1 WHERE BedID BedID AND Status 0。就算两个请求同时执行数据库锁会让其中一个ExecuteNonQuery返回 0另一个返回 1。返回 0 的那边直接回滚并提示重试床位就不会被重复分配。有人会问为什么不直接在 SQL Server 里写存储过程。也可以但我觉得在 C# 里用事务代码更直观方便在这个项目里加日志、加业务判断调试时也能直接看异常堆栈。宿舍管理系统规模小把事务放在数据访问层已经足够。4. WinForms 界面落地床位联动与入住办理4.1 用 ComboBox 和 DataGridView 把数据表搬到界面上WinForms 里绑定数据源有两个高频操作把宿舍列表塞进 ComboBox把入住名单塞进 DataGridView。先看主窗体加载事件里的宿舍下拉框绑定private void MainForm_Load(object sender, EventArgs e) { DataTable dormTable DbHelper.ExecuteTable( SELECT DormID, BuildingNo - RoomNo AS DormName FROM dbo.Dormitory ORDER BY BuildingNo, RoomNo); cboDorm.DataSource dormTable; cboDorm.DisplayMember DormName; cboDorm.ValueMember DormID; }这里DisplayMember是界面显示的文本ValueMember是下拉选择后要拿到的值。切换宿舍时需要联动更新床位下拉框private void cboDorm_SelectedIndexChanged(object sender, EventArgs e) { DataRowView? selected cboDorm.SelectedItem as DataRowView; if (selected null) return; int dormId Convert.ToInt32(selected[DormID]); DataTable bedTable DbHelper.ExecuteTable( SELECT BedID, BedNo, CASE Status WHEN 0 THEN N空床 WHEN 1 THEN N已住 ELSE N维修中 END AS BedStatusText FROM dbo.Bed WHERE DormID DormID ORDER BY BedNo, new SqlParameter(DormID, dormId)); cboBed.DataSource null; // 先清空再绑定 cboBed.DataSource bedTable; cboBed.DisplayMember BedNo; cboBed.ValueMember BedID; }这里有一个特别容易翻车的细节DataSource null必须出现在重新赋值之前。WinForms 的 ComboBox 和 DataGridView 默认缓存了原来的 DataTable直接赋新 DataTable 有可能不刷新列表项尤其是当新查询返回的行数比旧结果少时。这个坑在 DataGridView 上表现得更明显我后面第 5 章会再提到。4.2 入住、调宿、退宿三个操作串联到状态字段入住按钮做的事情本质上是把第 3 章的事务函数接到界面事件上。但在调用事务之前要先做一层业务校验这个学生是否已经住在某个床位上了。合理的业务规则是“一个学生同一时间只能有一条未退宿住宿记录”。private void btnCheckIn_Click(object sender, EventArgs e) { if (cboStudent.SelectedValue null || cboBed.SelectedValue null) { MessageBox.Show(请先选择学生和床位); return; } int studentId Convert.ToInt32(cboStudent.SelectedValue); int dormId Convert.ToInt32(cboDorm.SelectedValue); int bedId Convert.ToInt32(cboBed.SelectedValue); string checkSql SELECT COUNT(*) FROM Allocation a WHERE a.StudentID StudentID AND a.CheckOutTime IS NULL; int livingCount Convert.ToInt32(DbHelper.ExecuteScalar(checkSql, new SqlParameter(StudentID, studentId))); if (livingCount 0) { MessageBox.Show(该学生已有未退宿记录请先退宿); return; } string errMsg ; bool ok new BedService().AssignBed(studentId, dormId, out errMsg); if (ok) { MessageBox.Show(入住成功); RefreshStudentGrid(); } else { MessageBox.Show(入住失败 errMsg); } }调宿和退宿可以复用这套思路。调宿就是“旧床位退宿 新床位入住”两个动作包在一个事务里退宿则简单得多把Bed.Status改回 0同时给Allocation.CheckOutTime写入当前时间。要注意的是退宿时不能直接把Allocation记录删掉否则历史住宿记录就没了将来统计“这个床位上住过哪些人”时查不到数据。宿舍管理系统这类项目不要一上来就上复杂框架。ComboBox 绑表、DataGridView 绑表、SqlParameter 传参这三招能解决九成业务场景。多出来的需求再逐步加类加函数代码结构反而更清晰。5. 常见问题排查连接失败、DataGridView 不刷新、并发分配超卖、Excel 导入翻车5.1 “无法连接到 . ” —— 不是代码问题是实例名和认证模式问题现象源码解压后启动程序SqlConnection.Open()抛出“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。未找到或无法访问服务器”。原因绝大多数宿舍管理系统示例代码里的连接串是Data Source.代表连接本机默认实例。但你电脑上装的 SQL Server 很可能是命名实例localhost\SQLEXPRESS也可能是连接串里写了User IDsa但 SQL Server 只开启了 Windows 身份验证模式。解决先用 SQL Server Management Studio 确认实例名和登录方式。如果是命名实例连接串改成这样Data Sourcelocalhost\\SQLEXPRESS;Initial CatalogDormManageDB;Integrated Securitytrue如果要用sa登录需要先在 SSMS 的“安全性 → 登录名”里启用 sa 账号并把服务器身份验证模式改成“SQL Server 和 Windows 身份验证模式”。改了之后记得重启 SQL Server 服务。5.2 DataGridView 改了数据却不显示 —— 是 DataSource 缓存现象程序运行中新增了一条学生记录点击“刷新”按钮后DataGridView 显示的还是旧数据重启程序后又正常。原因DataGridView.DataSource还指向旧的DataTable对象。即便你重新执行查询得到了新的 DataTable直接赋给DataSource有时也不会触发完整重绘。解决刷新前先把DataSource置空再重新赋值private void RefreshStudentGrid() { DataTable dt DbHelper.ExecuteTable( SELECT s.StudentNo, s.StudentName, s.ClassName, d.BuildingNo, d.RoomNo, b.BedNo FROM Student s LEFT JOIN Allocation a ON s.StudentID a.StudentID AND a.CheckOutTime IS NULL LEFT JOIN Bed b ON a.BedID b.BedID LEFT JOIN Dormitory d ON b.DormID d.DormID); dgvStudent.DataSource null; dgvStudent.DataSource dt; }更规范的做法是用BindingSource包一层刷新时调用BindingSource.ResetBindings(false)但把DataSource null这种写法放在内部工具里完全够用。5.3 床位被两个人同时占用 —— 事务隔离 条件更新现象开两个窗口同时给同一个宿舍分配床位最终出现两个学生共用一个床位且数据库里查不到任何报错。原因“查空床位”和“更新床位状态”是两个独立步骤。SQL Server 默认的读提交隔离级别不保证两个连续操作之间没有并发穿插。解决第 3 章给出的UPDATE ... WHERE Status 0 受影响行数判断是相对稳妥的方案。如果项目里已经有存储过程也可以用UPDLOCK提示查询空床位时直接锁定行。但宿舍管理系统这种量级事务代码就够了不必上升到隔离级别。5.4 Excel 导入新生时学号被截断或变成科学计数法现象用 OLEDB 读取学生的 Excel 名单后“201812345678”变成了“2.01812E12”或者“2023001”变成了“2023”。原因Excel 中该列被识别为数值格式OLEDB 读取时把它转成了 double再转字符串就带上了科学计数法。解决连接串里加IMEX1让混合类型列按文本读取string connExcel ProviderMicrosoft.ACE.OLEDB.12.0; Data SourceD:\students.xlsx; Extended PropertiesExcel 12.0;HDRYES;IMEX1;如果加了IMEX1仍然出问题是因为 Excel 驱动有缓存第一次读取时已经确定了列类型。最稳的办法是让录入人员先把学号列在 Excel 里设为“文本格式”或者用 NPOI/ClosedXML 这类库直接读取单元格的文本值。批量写入数据库时用SqlBulkCopy效率很高几十万行数据几秒钟就能写完。5.5 部署后报“Microsoft.ACE.OLEDB.12.0 未注册”现象程序在自己电脑上运行正常拷到宿管办公室或者教室电脑后点导入 Excel 直接报“未在本机注册提供程序”。原因目标机器没有安装 Access Database Engine 驱动或者驱动版本与程序集平台位数不匹配。32 位程序配了 64 位驱动或者反过来都会报这个错。解决先确认程序生成平台。宿舍管理系统如果用的 AnyCPU在 64 位系统上默认按 64 位运行就装 64 位驱动如果项目强制 x86就装 32 位驱动。不想动项目配置的话安装“Microsoft Access Database Engine 2016 Redistributable”时选对应版本即可。另一个常见点是目标机器上如果装了 Office 2013 的 32 位版再装 64 位驱动会报冲突这种环境直接用 NPOI 读 Excel 反而省事不需要注册任何 COM 组件。6. 收尾技巧给宿舍管理系统加一个“后台导出与备份”按钮宿舍管理系统用到后面宿管老师最常提的需求是“把入住名单导出来”和“数据库能不能备份一下”。这两个都不难做但有坑导出 Excel 如果直接用 COM 引用 Excel.Application目标机器上没装 Office 就直接崩导出耗时超过几秒时WinForms 界面会卡死进度条还要能实时更新。这里给一个最稳的通用做法导出 CSVExcel 能直接打开同时在状态栏显示进度。后台线程不要直接碰控件用 BackgroundWorker 的ReportProgress回调更新进度条这是 C# 线程访问控件最标准的写法。private void btnExportCsv_Click(object sender, EventArgs e) { var worker new BackgroundWorker(); worker.WorkerReportsProgress true; worker.DoWork (s, arg) { DataTable dt DbHelper.ExecuteTable( SELECT d.BuildingNo, d.RoomNo, b.BedNo, CASE WHEN b.Status 1 THEN N已住 ELSE N空床 END AS BedState FROM Bed b JOIN Dormitory d ON b.DormID d.DormID ORDER BY d.BuildingNo, d.RoomNo, b.BedNo); string filePath Application.StartupPath \\DormReport.csv; using (var sw new StreamWriter(filePath, false, Encoding.UTF8)) { sw.WriteLine(楼栋,房间,床号,状态); for (int i 0; i dt.Rows.Count; i) { sw.WriteLine(${dt.Rows[i][BuildingNo]}, ${dt.Rows[i][RoomNo]}, ${dt.Rows[i][BedNo]}, ${dt.Rows[i][BedState]}); worker.ReportProgress((i 1) * 100 / dt.Rows.Count); } } }; worker.ProgressChanged (s, arg) { toolStripProgressBar.Value arg.ProgressPercentage; toolStripStatusLabel.Text $已导出 {arg.ProgressPercentage}%; }; worker.RunWorkerCompleted (s, arg) { toolStripProgressBar.Value 100; MessageBox.Show(导出完成); }; worker.RunWorkerAsync(); }数据库备份更直接执行一句BACKUP DATABASE就可以string bakPath Application.StartupPath $\\Backup\\Dorm_{DateTime.Now:yyyyMMdd_HHmm}.bak; string sql $BACKUP DATABASE DormManageDB TO DISK {bakPath}; DbHelper.ExecuteNonQuery(sql);建议把备份按钮做成每次启动时看一眼“是否有昨天的备份”没有就自动备份一次。数据是这类系统最值钱的资产代码可以重写宿舍名单和住宿流水丢了就很难找回来。我现在拿到任何类似的 C# 项目第一件事永远是先跑通数据库脚本再做功能改动这个顺序能帮你跳过真正让人头大的返工环节。希望帮到你。本文还有配套的精品资源点击获取