简介:这是一套面向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 name="DormDB" connectionString="Data Source=.;Initial Catalog=DormManageDB;User ID=sa;Password=123456;MultipleActiveResultSets=true" providerName="System.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; } } }几个参数你要重点关注:
MultipleActiveResultSets=true,对 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 ID=sa,但 SQL Server 只开启了 Windows 身份验证模式。
解决:先用 SQL Server Management Studio 确认实例名和登录方式。如果是命名实例,连接串改成这样:
"Data Source=localhost\\SQLEXPRESS;Initial Catalog=DormManageDB;Integrated Security=true"如果要用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.01812E+12”,或者“2023001”变成了“2023”。
原因:Excel 中该列被识别为数值格式,OLEDB 读取时把它转成了 double,再转字符串就带上了科学计数法。
解决:连接串里加IMEX=1,让混合类型列按文本读取:
string connExcel = @"Provider=Microsoft.ACE.OLEDB.12.0; Data Source=D:\students.xlsx; Extended Properties=""Excel 12.0;HDR=YES;IMEX=1""";如果加了IMEX=1仍然出问题,是因为 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 界面会卡死,进度条还要能实时更新。
这里给一个最稳的通用做法:导出 CSV,Excel 能直接打开,同时在状态栏显示进度。后台线程不要直接碰控件,用 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# 项目,第一件事永远是先跑通数据库脚本再做功能改动,这个顺序能帮你跳过真正让人头大的返工环节。希望帮到你。
本文还有配套的精品资源,点击获取